/*
 * game-photo.jsx — Game 2: see a word with the matching letter highlighted,
 * pick the photo whose name matches the word.
 *
 * Round structure: 4 photo tiles in a 2×2 RTL grid. One is the correct
 * round.image; the other three are distractor images drawn at random from
 * the remaining rounds. Position of the correct tile is randomized each
 * round (game 1 pins position; here it's a different skill — visual scan,
 * not motor habit — so we let it move).
 *
 * INVITATION + HINTS (v1.11+) — ported from game 1. After each round
 * mount a piano-ripple lifts the photo tiles in DOM order to say "tap
 * one of these". After 10s of no input one wrong tile is marked; at
 * 20s a second wrong tile is marked; at 30s the correct tile gets a
 * green glow + sparkles. Engaged flips on first onPick and cancels
 * both systems for the rest of the round.
 */

const TWEAK_DEFAULTS = window.TWEAK_DEFAULTS;

// Invitation + hint timings, URL-overridable so tests can collapse the
// 30s real-world chain to under a second. Matches game 1's keys.
const _qs = new URLSearchParams(window.location.search);
const _ms = (key, def) => {
  const v = _qs.get(key);
  return v === null || v === "" ? def : Math.max(0, +v) || def;
};
const INVITE_INTERVAL_MS   = _ms("invite", 15000);
const INVITE_STAGGER_MS    = 140;
const INVITE_LIFT_PX       = 8;
const INVITE_LIFT_MS       = 380;
// Idle-cue timing + mapping are shared from game-shared.jsx (HINT_STEPS
// [5/10/15s], HINT_CYCLE_MS, hintCueFor) — the one pattern, ADR 0028.

// Build a 4-tile grid for the round. The "tiles" carry full round objects
// so the image-slot ids stay stable (a photo dropped on bayit in game 1
// shows up here too).
function buildTiles(round, rounds) {
  const distractors = shuffle(rounds.filter((r) => r.slotId !== round.slotId)).slice(0, 3);
  const all = shuffle([round, ...distractors]);
  return all;
}

function WordCard({ word, letterIdx, highlightMode }) {
  // letterIdx = position within `word` to highlight. In our config every
  // round's letter == word[0], so we highlight character 0. (Generalizing is
  // future work.)
  //
  // v1.43: the "איפה המילה?" prompt above the word was the last readable
  // sentence in-game. A 4-5yo can't read it, and the word card with one
  // highlighted letter + 4 photos below IS the question already.
  const chars = Array.from(word);
  return (
    <div className="word-card">
      <div className="word" aria-label={word}>
        {chars.map((ch, i) => (
          <span key={i} className={i === letterIdx ? "hi" : ""}>{ch}</span>
        ))}
      </div>
    </div>
  );
}

const PHOTO_SPARK_ANGLES = [0, 45, 90, 135, 180, 225, 270, 315];

function PhotoTile({ tile, status, isCorrectTap, isWrong, disabled, hint, onPick, tileRef }) {
  // v1.45: switched from onClick → onPointerDown for parity with game 1's
  // locked-in pointerdown-commit (docs/design-system.md §4.1). The kid no
  // longer has to release on-target; the moment the finger touches a tile,
  // wrong/correct fires.
  return (
    <button
      ref={tileRef}
      type="button"
      className="photo-tile"
      data-wrong={isWrong ? "1" : "0"}
      data-correct={isCorrectTap ? "1" : "0"}
      data-disabled={disabled ? "1" : "0"}
      data-hint={hint || "none"}
      aria-label={tile.word}
      onPointerDown={(e) => { e.preventDefault(); onPick(tile); }}
    >
      <div className="pt-slot">
        {/* fit=cover so the illustration FILLS the square tile (user req,
            2026-06-04) — no cream letterbox bars. The 640×480 art is
            composed dual-mode (ADR 0022) so the subject sits in the central
            square and survives the top/bottom crop. (Reverses the v1.61
            contain default, which letterboxed.) */}
        <image-slot
          id={`slot-${tile.slotId}`}
          shape="rounded"
          radius="16"
          fit="cover"
          placeholder=""
          src={tile.image}
          alt={tile.alt}
        />
      </div>
      <div className="pt-x" aria-hidden="true">
        <svg viewBox="0 0 60 60" width="64" height="64">
          <path d="M16 16 L44 44 M44 16 L16 44" stroke="#fff" strokeWidth="8" strokeLinecap="round" fill="none" />
        </svg>
      </div>
      {hint === "correct" && (
        <span className="pt-sparkles" aria-hidden="true">
          {PHOTO_SPARK_ANGLES.map((a) => (
            <span key={a} className="pt-sparkle" style={{ "--angle": `${a}deg` }} />
          ))}
        </span>
      )}
      {isCorrectTap && (
        <span className="pt-success-burst" aria-hidden="true">
          {PHOTO_SPARK_ANGLES.map((a) => (
            <span key={a} className="pt-success-spark" style={{ "--angle": `${a}deg` }} />
          ))}
        </span>
      )}
    </button>
  );
}

function PhotoGrid({ tiles, status, wrongSlotId, correctSlotId, hintFor, onPick, tileRefs }) {
  return (
    <div className="photo-grid" role="group" aria-label="בחר את התמונה">
      {tiles.map((tile, i) => {
        const hint = status === "idle" ? hintFor(tile.slotId) : null;
        return (
          <PhotoTile
            key={tile.slotId}
            tile={tile}
            status={status}
            isCorrectTap={status === "correct" && correctSlotId === tile.slotId}
            isWrong={status === "wrong" && wrongSlotId === tile.slotId}
            disabled={status === "correct"}
            hint={hint}
            tileRef={(el) => { tileRefs.current[i] = el; }}
            onPick={onPick}
          />
        );
      })}
    </div>
  );
}

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  useApplyTweaks(t);

  const { rounds } = window.GAME_CONFIG;

  // Resume from saved progress; see shared/progress.js. v1.36 bumps
  // lastBreak so a multiple-of-5 saved round skips the break-screen
  // re-prompt and lands the kid directly in the letter.
  const saved = (typeof window !== "undefined" && window.PROGRESS) ? window.PROGRESS.load("photo") : null;
  const initialRound = saved ? saved.roundIdx : 0;
  const initialFirstTries = saved ? saved.firstTries : [];
  const initialLastBreak = saved ? Math.max(saved.lastBreak, Math.floor(saved.roundIdx / 5)) : 0;

  const [roundIdx, setRoundIdx]   = React.useState(initialRound);
  const [tiles, setTiles]         = React.useState(() => buildTiles(rounds[initialRound], rounds));
  const [status, setStatus]       = React.useState("idle");
  const [wrongSlotId, setWrong]   = React.useState(null);
  const [done, setDone]           = React.useState(false);
  const [lastBreak, setLastBreak] = React.useState(initialLastBreak);
  const [engaged, setEngaged]     = React.useState(false);
  const [inviteCycle, setInviteCycle] = React.useState(0);
  const [firstTries, setFirstTries] = React.useState(initialFirstTries);

  const tileRefs = React.useRef([]);
  const roundCleanRef = React.useRef(true);

  const breakAt = (roundIdx > 0 && roundIdx % 5 === 0) ? Math.floor(roundIdx / 5) : 0;
  const showBreak = breakAt > 0 && lastBreak < breakAt && !done && status === "idle";
  const batchRounds = showBreak ? rounds.slice(roundIdx - 5, roundIdx) : [];

  // Multi-choice accelerated hint (docs/game-baseline.md): level 1 fades all
  // the wrong tiles (@5s), level 2 glitters the correct (@10s), repeat @20s.
  const hintLevel = useHintCycle(
    !done && !showBreak && !engaged && status === "idle",
    HINT_STEPS, HINT_CYCLE_MS
  );

  const advance = React.useCallback(() => {
    if (roundIdx + 1 >= rounds.length) { setDone(true); return; }
    const next = roundIdx + 1;
    setRoundIdx(next);
    setTiles(buildTiles(rounds[next], rounds));
    setStatus("idle");
    setWrong(null);
    setEngaged(false);
    setInviteCycle(0);
    roundCleanRef.current = true;
  }, [roundIdx, rounds]);

  const restart = () => {
    setDone(false);
    setRoundIdx(0);
    setTiles(buildTiles(rounds[0], rounds));
    setStatus("idle");
    setWrong(null);
    setLastBreak(0);
    setEngaged(false);
    setInviteCycle(0);
    setFirstTries([]);
    roundCleanRef.current = true;
  };

  const round = rounds[roundIdx];

  // ── INVITATION (v1.11): a piano ripple lifts photo tiles at round
  //    mount and every 15s until the kid taps. Suppressed once hintLevel > 0
  //    so it doesn't overlap with hint visuals.
  React.useEffect(() => {
    if (done || showBreak || engaged || hintLevel > 0) return;
    const t0 = window.setTimeout(() => setInviteCycle((c) => c + 1), 350);
    const tid = window.setInterval(() => setInviteCycle((c) => c + 1), INVITE_INTERVAL_MS);
    return () => { clearTimeout(t0); clearInterval(tid); };
  }, [roundIdx, done, showBreak, engaged, hintLevel]);

  React.useEffect(() => {
    if (inviteCycle === 0) return;
    // CSS @media rules don't reach Element.animate(), so check directly.
    if (window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
    for (let i = 0; i < tiles.length; i++) {
      const el = tileRefs.current[i];
      if (!el) continue;
      el.animate(
        [
          { transform: "translateY(0)" },
          { transform: `translateY(-${INVITE_LIFT_PX}px)` },
          { transform: "translateY(0)" },
        ],
        {
          duration: INVITE_LIFT_MS,
          delay: i * INVITE_STAGGER_MS,
          easing: "cubic-bezier(.2,.9,.3,1.1)",
        }
      );
    }
  }, [inviteCycle, tiles]);

  // Unified idle cue (ADR 0028) via the shared helper. 4 tiles → NOT
  // fewOptions, so all 3 wrong fade together @t+5 (and @t+10), and the
  // correct tile glitters @t+15 (the third beat — moved from t+10 so the
  // rhythm matches every other game).
  const hintFor = React.useCallback((slotId) => {
    return hintCueFor(hintLevel, {
      correct: slotId === round.slotId,
      fewOptions: false,
    });
  }, [hintLevel, round.slotId]);

  // Hint having fired marks the round as not first-try-clean.
  React.useEffect(() => {
    if (hintLevel > 0) roundCleanRef.current = false;
  }, [hintLevel]);

  // Fire one-shot break/end sounds when those overlays open.
  React.useEffect(() => { if (showBreak && window.SOUNDS) window.SOUNDS.play("break"); }, [showBreak]);
  React.useEffect(() => { if (done && window.SOUNDS) window.SOUNDS.play("end"); }, [done]);

  // Persist resume-state on every advance; clear on done.
  React.useEffect(() => {
    if (!window.PROGRESS) return;
    if (done) { window.PROGRESS.clear("photo"); return; }
    window.PROGRESS.save("photo", { roundIdx, firstTries, lastBreak });
  }, [roundIdx, firstTries, lastBreak, done]);

  const onPick = (tile) => {
    if (status === "correct") return;
    if (!engaged) setEngaged(true);
    if (window.SOUNDS) window.SOUNDS.play("tap");
    if (tile.slotId === round.slotId) {
      if (window.SOUNDS) window.SOUNDS.play("correct");
      const wasClean = roundCleanRef.current;
      setFirstTries((prev) => [...prev, wasClean]);
      if (window.MASTERY) window.MASTERY.recordRound(round.letter, wasClean);
      setStatus("correct");
      setWrong(null);
      // v1.40 — 900 → 1200ms. Now that the full-screen CorrectOverlay is
      // gone (replaced by the per-tile burst), give the local burst room
      // to play out before the next round mounts.
      window.setTimeout(advance, 1200);
    } else {
      if (window.SOUNDS) window.SOUNDS.play("wrong");
      roundCleanRef.current = false;
      setStatus("wrong");
      setWrong(tile.slotId);
      window.setTimeout(() => { setStatus("idle"); setWrong(null); }, 700);
    }
  };

  return (
    <div className="app" style={{ gridTemplateRows: "auto auto 1fr auto" }}>
      {/* progress: 1 dot = 1 letter (default 22; truncated by ?n=) */}
      <Header index={roundIdx} total={rounds.length} />
      <WordCard
        key={`word-${roundIdx}`}
        word={round.word}
        letterIdx={0}
      />
      <div aria-hidden="true" />
      <PhotoGrid
        key={`grid-${roundIdx}`}
        tiles={tiles}
        status={status}
        wrongSlotId={wrongSlotId}
        correctSlotId={round.slotId}
        hintFor={hintFor}
        onPick={onPick}
        tileRefs={tileRefs}
      />

      {/* #169: the big green ✓ is back — it's the baseline success marker
          for single-answer-per-round games (game-contract CI-15,
          docs/game-baseline.md "The green ✓", which reversed the v1.40
          removal). The per-tile pop + spark burst still play under it. */}
      <CorrectOverlay on={status === "correct"} />

      {showBreak && (
        <BreakScreen
          batchRounds={batchRounds}
          batchFirstTries={firstTries.slice(roundIdx - 5, roundIdx)}
          completed={roundIdx}
          total={rounds.length}
          onContinue={() => { setEngaged(false); setLastBreak(breakAt); }}
          // v1.32 — see game-1 for the rationale + fix for the "../" 404.
          onStop={() => {
            window.location.href = document.baseURI;
          }}
        />
      )}
      {done && (
        <EndScreen
          onRestart={restart}
          playedRounds={rounds.slice(0, firstTries.length)}
          playedFirstTries={firstTries}
        />
      )}

      <TweaksPanel title="Tweaks">
        <SharedTweakControls t={t} setTweak={setTweak} />
      </TweaksPanel>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
