/* RECORD STORE GUY — FMV QTE player. Vanilla ES6, no deps. Spec: ENGINE.md */
"use strict";

// feel-tuning constants (see ENGINE.md)
const WINDOW_DEFAULT = 0.7; // s
const EARLY_GRACE = 0.25;   // s before window open where input = fail
const DEATH_HOLD = 600;     // ms on last death frame before retry
const CUE_LEAD = 0.0;       // s cue shows before window opens

const GLYPH = { left: "◀", right: "▶", up: "▲", down: "▼", action: "●" };
const KEYMAP = { ArrowLeft: "left", ArrowRight: "right", ArrowUp: "up", ArrowDown: "down",
                 " ": "action", Enter: "action" };

const $ = (id) => document.getElementById(id);
const vids = [$("vidA"), $("vidB")];
let cur = 0; // index of active video

const G = {
  data: null, state: "ATTRACT", scene: null, sceneId: null,
  wi: 0, windows: [], lives: 0, score: 0, checkpoint: null,
  debug: new URLSearchParams(location.search).has("debug"),
  blobs: new Map(), // url -> objectURL
};

// ---------- boot ----------
async function boot() {
  G.data = await (await fetch("scenes.json?v=" + Date.now())).json();
  $("hiscore").textContent = "HI-SCORE " + (localStorage.rsg_hiscore || 0);
  if (G.debug) { initDebug(); window.RSG = { G, input, vid }; }
  preload(G.data.scenes[G.data.start].clip);
  preload(G.data.scenes[G.data.start].death);
  addEventListener("keydown", onKey);
  initTouch();
  requestAnimationFrame(tick);
}

async function preload(url) {
  if (!url || G.blobs.has(url)) return G.blobs.get(url);
  try {
    const b = await (await fetch(url)).blob();
    const o = URL.createObjectURL(b);
    G.blobs.set(url, o);
    return o;
  } catch { return url; } // fall back to streaming
}

// ---------- clip playback (A/B swap) ----------
function playClip(url, onEnded) {
  const next = vids[1 - cur];
  next.src = G.blobs.get(url) || url;
  next.onended = null;
  next.currentTime = 0;
  const p = next.play();
  if (p) p.catch(() => {}); // pre-gesture autoplay rejection is fine
  next.onended = onEnded;
  next.classList.add("active");
  vids[cur].classList.remove("active");
  vids[cur].pause();
  cur = 1 - cur;
  return next;
}
const vid = () => vids[cur];

// ---------- scene flow ----------
function startGame() {
  G.lives = G.data.meta.lives;
  G.score = 0;
  G.checkpoint = G.data.start;
  $("attract").classList.add("hidden");
  playScene(G.data.start);
}

function playScene(id) {
  const s = G.data.scenes[id];
  G.state = "PLAYING"; G.sceneId = id; G.scene = s; G.wi = 0;
  if (s.checkpoint) G.checkpoint = id;
  G.windows = s.windows.map((w) => ({
    t: w.t[1] != null ? w.t : [w.t[0], w.t[0] + WINDOW_DEFAULT],
    input: w.input, cue: w.cue !== false, cued: false, done: false,
  }));
  hud();
  playClip(s.clip, onSceneClipEnd);
  // preload likely-next clips: this death, then success path
  preload(s.death).then(() => {
    const n = s.onSuccess;
    if (n && n !== "WIN") { preload(G.data.scenes[n].clip); preload(G.data.scenes[n].death); }
    else preload("clips/win.mp4");
  });
  if (G.debug) debugScene();
}

function onSceneClipEnd() {
  if (G.state !== "PLAYING") return;
  if (G.wi < G.windows.length) return fail("clip ended mid-window"); // safety net
  G.score += 1000; hud();
  const n = G.scene.onSuccess;
  if (n === "WIN") return win();
  playScene(n);
}

function fail(why) {
  if (G.state !== "PLAYING") return;
  G.state = "DEATH";
  cueHide();
  if (G.debug) $("dbg-state").textContent = "DEATH (" + why + ")";
  playClip(G.scene.death, () => {
    setTimeout(() => {
      G.lives--; G.score = Math.max(0, G.score - 100); hud();
      if (G.lives <= 0) return gameOver();
      playScene(G.sceneId); // retry the scene (checkpoints reserved for act restarts)
    }, DEATH_HOLD);
  });
}

function win() {
  G.state = "WIN";
  saveHi();
  playClip("clips/win.mp4", () => showEnd("YOU SURVIVED", "score " + G.score));
}
function gameOver() {
  G.state = "GAMEOVER";
  saveHi();
  playClip("clips/gameover.mp4", () => showEnd("GAME OVER", "score " + G.score));
}
function showEnd(title, sub) {
  const m = $("msg");
  m.innerHTML = "<h2>" + title + "</h2><p class='sub'>" + sub + "</p><p class='blink'>PRESS ANY KEY</p>";
  m.classList.remove("hidden");
  G.state = "END";
}
function toAttract() {
  $("msg").classList.add("hidden");
  $("hiscore").textContent = "HI-SCORE " + (localStorage.rsg_hiscore || 0);
  $("attract").classList.remove("hidden");
  vid().pause();
  G.state = "ATTRACT";
}
function saveHi() {
  if (G.score > (+localStorage.rsg_hiscore || 0)) localStorage.rsg_hiscore = G.score;
}

// ---------- QTE core ----------
// qteCheck is driven from THREE sources so throttling can't starve it:
// rAF (smooth when focused), video timeupdate (fires while playing even
// unfocused), and a 100ms interval backup.
function qteCheck() {
  if (G.state !== "PLAYING") return;
  const t = vid().currentTime;
  const w = G.windows[G.wi];
  if (w) {
    if (!w.cued && w.cue && t >= w.t[0] - CUE_LEAD) { w.cued = true; cueShow(w.input); }
    if (t >= w.t[1]) { w.done = "failed"; fail("too late"); }
  }
  if (G.debug) debugTick(t);
}
function tick() { qteCheck(); requestAnimationFrame(tick); }
setInterval(qteCheck, 100);
vids.forEach((v) => v.addEventListener("timeupdate", qteCheck));

function input(dir) {
  if (G.state === "ATTRACT") return startGame();
  if (G.state === "END") return toAttract();
  if (G.state !== "PLAYING") return;
  const w = G.windows[G.wi];
  if (!w) return; // windows all cleared, clip finishing
  const t = vid().currentTime;
  if (t < w.t[0] - EARLY_GRACE) return;            // idle fidget: ignore
  if (t < w.t[0]) { w.done = "failed"; return fail("too early"); } // Dragon's Lair rule
  if (dir !== w.input) { w.done = "failed"; return fail("wrong input"); }
  // pass
  w.done = "passed"; G.wi++; G.score += 100; hud();
  cueGood(dir);
  if (G.debug) debugScene();
}

// ---------- input sources ----------
function onKey(e) {
  if (e.repeat) return;
  if (G.state === "ATTRACT" || G.state === "END") return input("action");
  const d = KEYMAP[e.key];
  if (d) { e.preventDefault(); input(d); }
  if (G.debug) debugKeys(e);
}

function initTouch() {
  let sx, sy, st;
  addEventListener("touchstart", (e) => {
    sx = e.touches[0].clientX; sy = e.touches[0].clientY; st = Date.now();
  }, { passive: true });
  addEventListener("touchend", (e) => {
    const dx = e.changedTouches[0].clientX - sx, dy = e.changedTouches[0].clientY - sy;
    if (Math.hypot(dx, dy) < 30 && Date.now() - st < 400) return input("action"); // tap
    input(Math.abs(dx) > Math.abs(dy) ? (dx > 0 ? "right" : "left") : (dy > 0 ? "down" : "up"));
  }, { passive: true });
}

// ---------- HUD + cue ----------
function hud() {
  $("lives").textContent = "●".repeat(Math.max(0, G.lives));
  $("score").textContent = G.score;
}
function cueShow(dir) { const c = $("cue"); c.textContent = GLYPH[dir]; c.className = "show"; }
function cueHide() { $("cue").className = ""; }
function cueGood(dir) { const c = $("cue"); c.textContent = GLYPH[dir]; c.className = "good"; }

// ---------- debug mode (?debug=1) ----------
function initDebug() {
  $("debug").classList.remove("hidden");
  const sel = $("dbg-scene");
  for (const id of Object.keys(G.data.scenes)) {
    const o = document.createElement("option"); o.value = o.textContent = id; sel.appendChild(o);
  }
  sel.onchange = () => { $("attract").classList.add("hidden"); $("msg").classList.add("hidden");
    if (!G.lives) G.lives = G.data.meta.lives; playScene(sel.value); };
}
function debugScene() {
  const tl = $("dbg-timeline");
  tl.querySelectorAll(".dbg-win").forEach((n) => n.remove());
  const d = vid().duration || 5;
  G.windows.forEach((w) => {
    const b = document.createElement("div");
    b.className = "dbg-win" + (w.done ? " " + w.done : "");
    b.style.left = (w.t[0] / d) * 100 + "%";
    b.style.width = ((w.t[1] - w.t[0]) / d) * 100 + "%";
    tl.appendChild(b);
  });
  $("dbg-scene").value = G.sceneId;
}
function debugTick(t) {
  const d = vid().duration || 5;
  $("dbg-playhead").style.left = (t / d) * 100 + "%";
  $("dbg-time").textContent = t.toFixed(2) + "s / " + d.toFixed(2) + "s  [" + G.sceneId + " w" + G.wi + "]";
  $("dbg-state").textContent = G.state;
}
function debugKeys(e) { // , . frame-step while paused
  if (e.key === ",") { vid().pause(); vid().currentTime -= 1 / 24; }
  if (e.key === ".") { vid().pause(); vid().currentTime += 1 / 24; }
  if (e.key === "p") vid().paused ? vid().play() : vid().pause();
}

boot();
