// Hearts — a Puyo-style block puzzle game.
// 6 columns × 12 rows. Pairs of coloured hearts drop, rotate, land.
// 4+ connected same-colour pop in a fiery explosion; gravity collapses; chains multiply score.

const COLS = 6;
const ROWS = 12;
// Five colours — sprite is naturally blue, others use CSS hue-rotate filters.
const COLORS = ["B", "R", "Y", "G", "P"];
const COLOR_LABEL = {
  B: "blue",
  R: "red",
  Y: "yellow",
  G: "green",
  P: "pink",
};

function emptyBoard() {
  return Array.from({ length: ROWS }, () => Array(COLS).fill(null));
}

function randColor() {
  return COLORS[Math.floor(Math.random() * COLORS.length)];
}

function newPair() {
  return {
    a: randColor(), // pivot
    b: randColor(), // partner
    x: 2,           // pivot column
    y: 0,           // pivot row
    rot: 0,         // 0 = partner up, 1 = right, 2 = down, 3 = left
  };
}

function partnerCell(p) {
  switch (p.rot) {
    case 0: return [p.x, p.y - 1];
    case 1: return [p.x + 1, p.y];
    case 2: return [p.x, p.y + 1];
    case 3: return [p.x - 1, p.y];
  }
}

function valid(board, p) {
  const [bx, by] = partnerCell(p);
  for (const [x, y] of [[p.x, p.y], [bx, by]]) {
    if (x < 0 || x >= COLS || y >= ROWS) return false;
    if (y >= 0 && board[y][x]) return false;
  }
  return true;
}

function landPair(board, p) {
  const next = board.map(r => r.slice());
  const [bx, by] = partnerCell(p);
  if (p.y >= 0 && p.y < ROWS) next[p.y][p.x] = p.a;
  if (by >= 0 && by < ROWS && bx >= 0 && bx < COLS) next[by][bx] = p.b;
  return next;
}

function applyGravity(board) {
  const next = board.map(r => r.slice());
  for (let x = 0; x < COLS; x++) {
    let write = ROWS - 1;
    for (let y = ROWS - 1; y >= 0; y--) {
      if (next[y][x]) {
        const v = next[y][x];
        next[y][x] = null;
        next[write][x] = v;
        write--;
      }
    }
  }
  return next;
}

function findGroups(board) {
  const seen = Array.from({ length: ROWS }, () => Array(COLS).fill(false));
  const groups = [];
  for (let y = 0; y < ROWS; y++) {
    for (let x = 0; x < COLS; x++) {
      if (!board[y][x] || seen[y][x]) continue;
      const color = board[y][x];
      const stack = [[x, y]];
      const group = [];
      while (stack.length) {
        const [cx, cy] = stack.pop();
        if (cx < 0 || cx >= COLS || cy < 0 || cy >= ROWS) continue;
        if (seen[cy][cx]) continue;
        if (board[cy][cx] !== color) continue;
        seen[cy][cx] = true;
        group.push([cx, cy]);
        stack.push([cx + 1, cy], [cx - 1, cy], [cx, cy + 1], [cx, cy - 1]);
      }
      if (group.length >= 4) groups.push({ color, cells: group });
    }
  }
  return groups;
}

function Cell({ color, popping, falling }) {
  if (popping) {
    return (
      <div className="cell exploding">
        <div className="boom"></div>
      </div>
    );
  }
  if (!color) return <div className="cell empty" />;
  const cls = "cell filled heart-" + COLOR_LABEL[color] + (falling ? " falling" : "");
  return (
    <div className={cls}>
      <div className="heart"></div>
    </div>
  );
}

function NextCell({ color }) {
  if (!color) return <div className="cell empty" />;
  return (
    <div className={"cell filled heart-" + COLOR_LABEL[color]}>
      <div className="heart"></div>
    </div>
  );
}

function Game() {
  const [board, setBoard] = React.useState(emptyBoard);
  const [pair, setPair] = React.useState(newPair);
  const [next1, setNext1] = React.useState(() => ({ a: randColor(), b: randColor() }));
  const [score, setScore] = React.useState(0);
  const [chain, setChain] = React.useState(0);
  const [best, setBest] = React.useState(() => +(localStorage.getItem("rv_hearts_best") || 0));
  const [started, setStarted] = React.useState(false);
  const [gameOver, setGameOver] = React.useState(false);
  const [paused, setPaused] = React.useState(false);
  const [popping, setPopping] = React.useState(new Set());
  const [resolving, setResolving] = React.useState(false);

  const stateRef = React.useRef({ board, pair, resolving, paused, gameOver, started });
  React.useEffect(() => {
    stateRef.current = { board, pair, resolving, paused, gameOver, started };
  }, [board, pair, resolving, paused, gameOver, started]);

  // Resolve chains
  const resolve = React.useCallback(async (b) => {
    setResolving(true);
    let cur = applyGravity(b);
    setBoard(cur);
    let localChain = 0;

    while (true) {
      const groups = findGroups(cur);
      if (groups.length === 0) break;
      localChain++;
      const popSet = new Set();
      let popCount = 0;
      for (const g of groups) {
        for (const [x, y] of g.cells) popSet.add(`${x},${y}`);
        popCount += g.cells.length;
      }
      // Show explosion frames over the popping cells (cells temporarily blank
      // so the explosion shows on top; the explosion lasts ~520ms).
      setPopping(popSet);
      setChain(localChain);
      setScore(s => s + popCount * 10 * localChain);
      // Blank the cells underneath the explosion while it animates
      const blanked = cur.map(r => r.slice());
      for (const g of groups) for (const [x, y] of g.cells) blanked[y][x] = null;
      setBoard(blanked);
      await new Promise(r => setTimeout(r, 1100));
      setPopping(new Set());
      cur = applyGravity(blanked);
      setBoard(cur);
      await new Promise(r => setTimeout(r, 200));
    }
    setChain(0);
    setResolving(false);
    return cur;
  }, []);

  const spawnNext = React.useCallback((b) => {
    const fresh = { a: next1.a, b: next1.b, x: 2, y: 0, rot: 0 };
    if (!valid(b, fresh)) {
      setGameOver(true);
      setBest(prev => {
        const newBest = Math.max(prev, score);
        localStorage.setItem("rv_hearts_best", newBest);
        return newBest;
      });
      return;
    }
    setPair(fresh);
    setNext1({ a: randColor(), b: randColor() });
  }, [next1, score]);

  // Drop tick
  React.useEffect(() => {
    if (!started || gameOver || paused || resolving) return;
    const id = setInterval(() => {
      setPair(p => {
        const moved = { ...p, y: p.y + 1 };
        if (valid(stateRef.current.board, moved)) return moved;
        const newBoard = landPair(stateRef.current.board, p);
        resolve(newBoard).then(final => spawnNext(final));
        return p;
      });
    }, 620);
    return () => clearInterval(id);
  }, [started, gameOver, paused, resolving, resolve, spawnNext]);

  // Input
  React.useEffect(() => {
    const onKey = (e) => {
      if (!started) {
        if (e.key === "Enter" || e.key === " ") start();
        return;
      }
      if (gameOver) {
        if (e.key === "Enter" || e.key === " ") restart();
        return;
      }
      if (e.key === "p" || e.key === "P") { setPaused(x => !x); return; }
      if (paused || resolving) return;
      const b = stateRef.current.board;
      setPair(p => {
        let np = p;
        if (e.key === "ArrowLeft")  np = { ...p, x: p.x - 1 };
        else if (e.key === "ArrowRight") np = { ...p, x: p.x + 1 };
        else if (e.key === "ArrowDown")  np = { ...p, y: p.y + 1 };
        else if (e.key === "z" || e.key === "Z" || e.key === "ArrowUp") {
          np = { ...p, rot: (p.rot + 3) % 4 };
          if (!valid(b, np)) np = { ...np, x: np.x + (np.rot === 3 ? 1 : np.rot === 1 ? -1 : 0) };
        }
        else if (e.key === "x" || e.key === "X") {
          np = { ...p, rot: (p.rot + 1) % 4 };
          if (!valid(b, np)) np = { ...np, x: np.x + (np.rot === 3 ? 1 : np.rot === 1 ? -1 : 0) };
        }
        else if (e.key === " ") {
          let cur = p;
          while (valid(b, { ...cur, y: cur.y + 1 })) cur = { ...cur, y: cur.y + 1 };
          const newBoard = landPair(b, cur);
          resolve(newBoard).then(final => spawnNext(final));
          e.preventDefault();
          return cur;
        }
        else { return p; }
        return valid(b, np) ? np : p;
      });
      if (["ArrowDown", "ArrowLeft", "ArrowRight", "ArrowUp", " "].includes(e.key)) {
        e.preventDefault();
      }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [started, gameOver, paused, resolving, resolve, spawnNext]);

  function start() {
    setBoard(emptyBoard());
    setPair(newPair());
    setNext1({ a: randColor(), b: randColor() });
    setScore(0); setChain(0); setGameOver(false); setPaused(false);
    setStarted(true);
  }

  function restart() {
    start();
  }

  // Render board with active pair overlay. Cells from the active pair are
  // marked `falling` so CSS can spin them; settled cells freeze on frame 0.
  const display = board.map(r => r.slice());
  const falling = Array.from({ length: ROWS }, () => Array(COLS).fill(false));
  if (started && !gameOver && !resolving) {
    const [bx, by] = partnerCell(pair);
    if (pair.y >= 0 && pair.y < ROWS && pair.x >= 0 && pair.x < COLS) {
      display[pair.y][pair.x] = pair.a;
      falling[pair.y][pair.x] = true;
    }
    if (by >= 0 && by < ROWS && bx >= 0 && bx < COLS) {
      display[by][bx] = pair.b;
      falling[by][bx] = true;
    }
  }

  return (
    <div className="puyo-cabinet">
      <div className="puyo-screen">
        <div className="puyo-hud">
          <div className="puyo-hud-blk">
            <div className="lbl">SCORE</div>
            <div className="val">{String(score).padStart(6, "0")}</div>
          </div>
          <div className="puyo-hud-blk">
            <div className="lbl">HI-SCORE</div>
            <div className="val">{String(Math.max(best, score)).padStart(6, "0")}</div>
          </div>
        </div>

        <div className="puyo-play">
          <div className="puyo-board">
            <div className="puyo-board-bg" aria-hidden="true"></div>
            <div className="puyo-board-grid">
              {display.map((row, y) => (
                <div className="row" key={y}>
                  {row.map((c, x) => (
                    <Cell key={x} color={c} popping={popping.has(`${x},${y}`)} falling={falling[y][x]} />
                  ))}
                </div>
              ))}
            </div>
            {chain > 0 && (
              <div className="puyo-chain">{chain}× CHAIN!</div>
            )}
            {(!started || gameOver || paused) && (
              <div className="puyo-overlay">
                {!started ? (
                  <>
                    <div className="big start">HEARTS</div>
                    <div className="small">PRESS START O INVIO</div>
                    <button className="restart" onClick={start}>▶ START</button>
                  </>
                ) : gameOver ? (
                  <>
                    <div className="big">GAME OVER</div>
                    <div className="small">PRESS ENTER OR TAP</div>
                    <button className="restart" onClick={restart}>RESTART</button>
                  </>
                ) : (
                  <>
                    <div className="big">PAUSED</div>
                    <div className="small">PRESS P TO RESUME</div>
                  </>
                )}
              </div>
            )}
          </div>

          <div className="puyo-side">
            <div className="puyo-next-lbl">NEXT</div>
            <div className="puyo-next">
              <NextCell color={next1.b} />
              <NextCell color={next1.a} />
            </div>
            <button className="puyo-btn" onClick={() => setPaused(p => !p)}>
              {paused ? "▶ RESUME" : "❚❚ PAUSE"}
            </button>
            <button className="puyo-btn" onClick={restart}>↺ RESTART</button>
          </div>
        </div>
      </div>

      {/* on-screen pad for touch */}
      <div className="puyo-pad">
        <button onClick={() => fakeKey("ArrowLeft")}>◀</button>
        <button onClick={() => fakeKey("z")}>↺</button>
        <button onClick={() => fakeKey("ArrowDown")}>▼</button>
        <button onClick={() => fakeKey("x")}>↻</button>
        <button onClick={() => fakeKey("ArrowRight")}>▶</button>
      </div>
    </div>
  );
}

function fakeKey(key) {
  window.dispatchEvent(new KeyboardEvent("keydown", { key }));
}

window.__mountGame = function (el) {
  ReactDOM.createRoot(el).render(<Game />);
};
