/* global React, Icon, IconBtn, db */
const { useState, useEffect, useCallback, useRef, useMemo } = React;

// ============================================================
// 2048 — slide-and-merge puzzle. Reach 2048; keep going if you like.
//
// Grid is a 4×4 array of { value, id, justMerged, justSpawned }.
// `id` is stable across moves so React doesn't re-mount tiles.
// `justMerged` / `justSpawned` are one-frame flags that drive CSS
// pop and fade-in animations.
// ============================================================

const G2048_SIZE = 4;
let g2048Uid = 1;
function newTile(value) { return { value, id: g2048Uid++, justMerged: false, justSpawned: true }; }
function emptyGrid() { return Array.from({ length: G2048_SIZE }, () => Array(G2048_SIZE).fill(null)); }

function cloneGrid(g) {
  return g.map(row => row.map(cell => cell ? { ...cell, justMerged: false, justSpawned: false } : null));
}
function gridSnapshot(g) {
  // For undo — store as plain values without identity (we just need the values back)
  return g.map(row => row.map(c => c ? c.value : 0));
}
function gridFromSnapshot(snap) {
  return snap.map(row => row.map(v => v ? { value: v, id: g2048Uid++, justMerged: false, justSpawned: false } : null));
}
function emptyCells(g) {
  const cells = [];
  for (let r = 0; r < G2048_SIZE; r++) for (let c = 0; c < G2048_SIZE; c++) if (!g[r][c]) cells.push([r, c]);
  return cells;
}
function spawnRandom(g) {
  const empty = emptyCells(g);
  if (empty.length === 0) return g;
  const [r, c] = empty[Math.floor(Math.random() * empty.length)];
  const value = Math.random() < 0.9 ? 2 : 4;
  const next = cloneGrid(g);
  next[r][c] = newTile(value);
  return next;
}
function isGameOver(g) {
  if (emptyCells(g).length > 0) return false;
  for (let r = 0; r < G2048_SIZE; r++) for (let c = 0; c < G2048_SIZE; c++) {
    const v = g[r][c].value;
    if (c + 1 < G2048_SIZE && g[r][c + 1] && g[r][c + 1].value === v) return false;
    if (r + 1 < G2048_SIZE && g[r + 1][c] && g[r + 1][c].value === v) return false;
  }
  return true;
}
function maxValue(g) {
  let m = 0;
  for (const row of g) for (const c of row) if (c && c.value > m) m = c.value;
  return m;
}

// Slide one row left, return { row, gained, moved }.
// Used as the primitive for all 4 directions by transposing/reversing.
function slideRowLeft(row) {
  const filtered = row.filter(Boolean);
  let gained = 0;
  let moved = false;
  for (let i = 0; i < filtered.length - 1; i++) {
    if (filtered[i].value === filtered[i + 1].value) {
      const newValue = filtered[i].value * 2;
      filtered[i] = { ...filtered[i], value: newValue, justMerged: true };
      filtered.splice(i + 1, 1);
      gained += newValue;
    }
  }
  while (filtered.length < G2048_SIZE) filtered.push(null);
  // Detect movement
  for (let i = 0; i < G2048_SIZE; i++) {
    const before = row[i];
    const after = filtered[i];
    if ((before?.id || 0) !== (after?.id || 0) || (before?.value || 0) !== (after?.value || 0)) {
      moved = true; break;
    }
  }
  return { row: filtered, gained, moved };
}
function transpose(g) {
  return g[0].map((_, c) => g.map(row => row[c]));
}
function move(g, dir) {
  let working = cloneGrid(g);
  // Normalize so we always slide left.
  if (dir === 'right')  working = working.map(r => r.slice().reverse());
  if (dir === 'up')     working = transpose(working);
  if (dir === 'down')   working = transpose(working).map(r => r.slice().reverse());

  let totalGained = 0;
  let anyMoved = false;
  const next = working.map(row => {
    const { row: outRow, gained, moved } = slideRowLeft(row);
    totalGained += gained;
    if (moved) anyMoved = true;
    return outRow;
  });

  // Un-normalize
  let result = next;
  if (dir === 'right')  result = next.map(r => r.slice().reverse());
  if (dir === 'up')     result = transpose(next);
  if (dir === 'down')   result = transpose(next.map(r => r.slice().reverse()));

  return { grid: result, gained: totalGained, moved: anyMoved };
}

function G2048Tool() {
  const [grid, setGrid] = useState(emptyGrid);
  const [score, setScore] = useState(0);
  const [best, setBest] = useState(0);
  const [over, setOver] = useState(false);
  const [won, setWon] = useState(false);
  const [continued, setContinued] = useState(false);
  const [prev, setPrev] = useState(null); // for undo: { grid, score }
  const [loaded, setLoaded] = useState(false);
  const touchRef = useRef(null);

  // Load saved state
  useEffect(() => {
    db.kv.get('g2048State').then(rec => {
      if (rec?.v && rec.v.grid) {
        setGrid(gridFromSnapshot(rec.v.grid));
        setScore(rec.v.score || 0);
        setBest(rec.v.best || 0);
        setOver(!!rec.v.over);
        setWon(!!rec.v.won);
        setContinued(!!rec.v.continued);
      } else {
        // Fresh start
        let g = spawnRandom(emptyGrid());
        g = spawnRandom(g);
        setGrid(g);
      }
      setLoaded(true);
    });
  }, []);

  // Persist whenever it changes (after load)
  useEffect(() => {
    if (!loaded) return;
    db.kv.put({
      k: 'g2048State',
      v: { grid: gridSnapshot(grid), score, best, over, won, continued },
    });
  }, [grid, score, best, over, won, continued, loaded]);

  const doMove = useCallback((dir) => {
    if (over || (won && !continued)) return;
    const { grid: nextGrid, gained, moved } = move(grid, dir);
    if (!moved) return;
    const withSpawn = spawnRandom(nextGrid);
    const nextScore = score + gained;
    const nextBest = Math.max(best, nextScore);
    setPrev({ grid: gridSnapshot(grid), score });
    setGrid(withSpawn);
    setScore(nextScore);
    setBest(nextBest);
    if (!won && maxValue(withSpawn) >= 2048) setWon(true);
    if (isGameOver(withSpawn)) setOver(true);
  }, [grid, score, best, over, won, continued]);

  // Keyboard
  useEffect(() => {
    const onKey = (e) => {
      if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
      const map = {
        ArrowLeft: 'left', ArrowRight: 'right', ArrowUp: 'up', ArrowDown: 'down',
        a: 'left', d: 'right', w: 'up', s: 'down',
      };
      const dir = map[e.key];
      if (dir) { e.preventDefault(); doMove(dir); }
      if (e.key === 'z' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); undo(); }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  });

  // Touch / swipe
  const onTouchStart = (e) => {
    const t = e.touches[0];
    touchRef.current = { x: t.clientX, y: t.clientY };
  };
  const onTouchEnd = (e) => {
    if (!touchRef.current) return;
    const t = e.changedTouches[0];
    const dx = t.clientX - touchRef.current.x;
    const dy = t.clientY - touchRef.current.y;
    const ax = Math.abs(dx), ay = Math.abs(dy);
    if (Math.max(ax, ay) < 24) return;
    doMove(ax > ay ? (dx > 0 ? 'right' : 'left') : (dy > 0 ? 'down' : 'up'));
    touchRef.current = null;
  };

  const undo = () => {
    if (!prev) return;
    setGrid(gridFromSnapshot(prev.grid));
    setScore(prev.score);
    setOver(false);
    setPrev(null);
  };
  const newGame = () => {
    let g = spawnRandom(emptyGrid());
    g = spawnRandom(g);
    setGrid(g);
    setScore(0);
    setOver(false);
    setWon(false);
    setContinued(false);
    setPrev(null);
  };
  const keepGoing = () => setContinued(true);

  if (!loaded) return <div className="g2048-tool g2048-loading" />;

  return (
    <div className="g2048-tool">
      <header className="g2048-head">
        <div>
          <h1>2048</h1>
          <p>Slide tiles with <kbd>← ↑ → ↓</kbd> or swipe. Combine equals; reach 2048.</p>
        </div>
        <div className="g2048-scores">
          <div className="g2048-score">
            <div className="g2048-score-label">Score</div>
            <div className="g2048-score-value">{score}</div>
          </div>
          <div className="g2048-score">
            <div className="g2048-score-label">Best</div>
            <div className="g2048-score-value">{best}</div>
          </div>
        </div>
      </header>

      <div className="g2048-actions">
        <button className="lh-btn ghost" onClick={undo} disabled={!prev}>
          <Icon name="undo" />Undo
        </button>
        <button className="lh-btn primary" onClick={newGame}>
          <Icon name="refresh" />New game
        </button>
      </div>

      <div
        className="g2048-board"
        onTouchStart={onTouchStart}
        onTouchEnd={onTouchEnd}
      >
        <div className="g2048-cells">
          {Array.from({ length: G2048_SIZE * G2048_SIZE }, (_, i) => (
            <div key={i} className="g2048-cell" />
          ))}
        </div>
        <div className="g2048-tiles">
          {grid.flatMap((row, r) => row.map((cell, c) =>
            cell && (
              <div
                key={cell.id}
                className={`g2048-tile v-${cell.value} ${cell.justMerged ? 'is-merged' : ''} ${cell.justSpawned ? 'is-spawned' : ''}`}
                style={{ '--row': r, '--col': c }}
              >
                {cell.value}
              </div>
            )
          ))}
        </div>

        {(over || (won && !continued)) && (
          <div className="g2048-overlay">
            <div className="g2048-overlay-card">
              <Icon name={won ? 'emoji_events' : 'sentiment_dissatisfied'} />
              <h2>{won ? 'You reached 2048!' : 'No moves left'}</h2>
              <p>{won ? 'Keep going for a higher score, or start fresh.' : `Final score: ${score}.`}</p>
              <div className="g2048-overlay-actions">
                {won && !continued && (
                  <button className="lh-btn ghost" onClick={keepGoing}>
                    <Icon name="arrow_forward" />Keep going
                  </button>
                )}
                <button className="lh-btn primary" onClick={newGame}>
                  <Icon name="refresh" />New game
                </button>
              </div>
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

window.G2048Tool = G2048Tool;
