/* The arena: canvas stage, thumbstick, HUD.
 *
 * Ported from arena-ui.js, which owned its own DOM precisely to stay out of a
 * re-render's way. React makes that easier to express rather than harder, but
 * only if the boundary is drawn in the same place:
 *
 *   The canvas element is created once per game and never re-created. React
 *   owns the element; the Stage owns the pixels. A `key` on the game means a
 *   new game gets a genuinely new canvas, and a phase change inside a game
 *   does not.
 *
 *   Snapshots — 20 a second — never enter React state. They arrive from the
 *   socket and go straight into stage.push(). Routing them through useState
 *   would re-render the tree 20 times a second to move sprites that the canvas
 *   is already drawing at 60fps on its own.
 *
 *   The HUD does re-render, because it changes a handful of times a round and
 *   is cheap. The timer bar is the exception — it needs to move between server
 *   messages, so it is written straight to the node's style from the same
 *   rAF loop rather than by re-rendering four times a second.
 */
(function () {

const useEngine = () => {
  const [engine, setEngine] = React.useState(() => window.PartyEngine || null);
  React.useEffect(() => {
    if (engine) return undefined;
    const on = () => setEngine(window.PartyEngine || null);
    window.addEventListener('party-engine-ready', on);
    return () => window.removeEventListener('party-engine-ready', on);
  }, [engine]);
  return engine;
};
window.usePartyEngine = useEngine;

window.PartyArena = function PartyArena({ v, me, send, isOwner, skew }) {
  const engine = useEngine();
  const g = v.game;

  const wrapRef = React.useRef(null);
  const canvasRef = React.useRef(null);
  const padHostRef = React.useRef(null);
  const barRef = React.useRef(null);
  const bigRef = React.useRef(null);

  const stageRef = React.useRef(null);
  const padRef = React.useRef(null);
  const lastCount = React.useRef(-1);
  const lastBig = React.useRef('');

  const [peek, setPeek] = React.useState(!!isOwner);

  /* ---- the stage: one per game, torn down with it ---- */
  React.useEffect(() => {
    if (!engine || !canvasRef.current) return undefined;
    const stage = new engine.Stage(canvasRef.current);
    stageRef.current = stage;
    stage.fit = 'bottom';           // the phone strip crops to the floor
    stage.setGame(g.key);
    stage.setRoster((v.players || []).map(p => ({
      pid: p.pid, name: p.name, char: p.char, you: p.pid === me.pid,
    })));

    const size = () => {
      const wrap = wrapRef.current;
      if (!wrap || !stageRef.current) return;
      const dpr = Math.min(2, window.devicePixelRatio || 1);
      const w = wrap.clientWidth, h = wrap.clientHeight;
      const cv = stageRef.current.canvas;
      cv.width = Math.round(w * dpr);
      cv.height = Math.round(h * dpr);
      cv.style.width = w + 'px';
      cv.style.height = h + 'px';
    };
    size();
    stage.start();
    window.addEventListener('resize', size);

    /* Snapshots bypass React entirely — straight from socket to canvas. */
    const off = window.PartyOnSnapshot(snap => stageRef.current?.push(snap));

    return () => {
      off();
      window.removeEventListener('resize', size);
      stage.stop();
      stageRef.current = null;
    };
    // Re-created only when the game changes; `key` on the component enforces it.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [engine, g.key]);

  /* Re-size when peeking toggles the stage between a glance and a screen. */
  React.useEffect(() => {
    const wrap = wrapRef.current, stage = stageRef.current;
    if (!wrap || !stage) return;
    const t = setTimeout(() => {
      const dpr = Math.min(2, window.devicePixelRatio || 1);
      const w = wrap.clientWidth, h = wrap.clientHeight;
      stage.canvas.width = Math.round(w * dpr);
      stage.canvas.height = Math.round(h * dpr);
      stage.canvas.style.width = w + 'px';
      stage.canvas.style.height = h + 'px';
    }, 340);                                   // after the flex-basis transition
    return () => clearTimeout(t);
  }, [peek]);

  /* The roster can change mid-game — a late arrival, a rename. */
  React.useEffect(() => {
    stageRef.current?.setRoster((v.players || []).map(p => ({
      pid: p.pid, name: p.name, char: p.char, you: p.pid === me.pid,
    })));
  }, [v.players, me.pid]);

  /* ---- the thumbstick ---- */
  React.useEffect(() => {
    if (!engine || !padHostRef.current) return undefined;
    const host = padHostRef.current;
    host.innerHTML = engine.controllerHTML({
      jump: g.buttons?.jump !== false,
      act: g.buttons?.act,
      moveHint: 'slide anywhere to move',
    });
    const pad = new engine.Controller(host, msg => send(msg));
    pad.onHaptic = kind => {
      try { navigator.vibrate && navigator.vibrate(kind === 'jump' ? 14 : 22); } catch { /* unsupported */ }
      const a = engine.audio;
      if (a?.sfx) (kind === 'jump' ? a.sfx.jump() : a.sfx.dash());
    };
    pad.mount();
    padRef.current = pad;
    return () => { pad.unmount(); padRef.current = null; host.innerHTML = ''; };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [engine, g.key, g.buttons?.act, g.buttons?.jump]);

  /* ---- the live bits: timer bar and the one big word ----
   * These run on rAF rather than a React tick. The bar has to move smoothly
   * between server messages and the countdown has to fire a sound on the
   * second, neither of which wants a component re-render behind it. */
  const gRef = React.useRef(g);
  gRef.current = g;
  React.useEffect(() => {
    let raf = 0;
    const tick = () => {
      raf = requestAnimationFrame(tick);
      const game = gRef.current;
      if (!game) return;
      const now = Date.now() + (skew || 0);

      const bar = barRef.current;
      if (bar) {
        if (game.endsAt && game.phase !== 'countdown') {
          const total = game.duration ? game.duration * 1000 : 42000;
          const left = Math.max(0, game.endsAt - now);
          bar.style.width = Math.max(0, Math.min(100, (left / total) * 100)) + '%';
          bar.className = left < 8000 ? 'low' : '';
        } else {
          bar.style.width = '100%';
          bar.className = '';
        }
      }

      let big = '';
      if (game.phase === 'countdown') {
        const secs = Math.max(0, Math.ceil((game.endsAt - now) / 1000));
        big = secs > 0 ? String(secs) : 'GO!';
        const a = window.PartyEngine?.audio;
        if (a?.sfx && secs !== lastCount.current) {
          lastCount.current = secs;
          if (secs > 0) a.sfx.countdown(secs); else a.sfx.go();
        }
      } else if (game.phase === 'between') {
        big = 'ROUND OVER';
      } else if (game.key === 'freeze') {
        big = game.light === 'red' ? 'FREEZE' : game.light === 'amber' ? 'SLOW' : '';
        if (game.hud?.done) big = `#${game.hud.place}`;
      } else if (game.key === 'sumo' && game.hud?.out) {
        big = 'OUT';
      }

      const el = bigRef.current;
      if (el && big !== lastBig.current) {
        lastBig.current = big;
        el.textContent = big;
        el.className = 'big-msg' + (big ? ' on' : '')
          + (big === 'FREEZE' || big === 'OUT' ? ' bad' : '')
          + (big === 'GO!' ? ' good' : '');
      }
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [skew]);

  if (!engine) {
    return (
      <div className="mm-party">
        <div className="arena" style={{ alignItems: 'center', justifyContent: 'center' }}>
          <div className="big-msg on">{window.PartyEngineError ? 'arena failed to load' : '…'}</div>
        </div>
      </div>
    );
  }

  const hud = g.hud || {};
  const stunned = !!hud.caught || !!hud.out;

  return (
    <div className="mm-party">
      <div className={`arena${peek ? ' peek' : ''}${stunned ? ' stunned' : ''}`}>
        <div className="stage-wrap" ref={wrapRef}>
          <canvas ref={canvasRef} />
          <div className="stage-tag">{g.emoji} {g.name}{g.finale ? ' · FINALE' : ''}</div>
        </div>

        <div className="ahud">
          <div className="ahud-l">
            <span className="ahud-em">{g.emoji || '🎮'}</span>
            <div style={{ minWidth: 0 }}>
              <div className="ahud-t">{g.round ? `${g.name} · ${g.round}/${g.rounds}` : g.name}</div>
              <div className="ahud-s">{g.hint || ''}</div>
            </div>
          </div>
          <div className="ahud-r">
            <div className="ahud-score">{(hud.score || 0).toLocaleString()}</div>
            {isOwner ? null : (
              <button className="peek-btn" onClick={() => setPeek(p => !p)}>👁</button>
            )}
          </div>
        </div>

        <div className="abar"><i ref={barRef} /></div>

        <div className="gamepad" ref={padHostRef} />

        <div className="big-msg" ref={bigRef} />
      </div>
    </div>
  );
};

/* The character picker draws the real critters with the real renderer, so what
 * you choose is what you get rather than an emoji standing in for it. */
window.CritterTile = function CritterTile({ charId, size = 84, locked = false }) {
  const engine = useEngine();
  const ref = React.useRef(null);
  React.useEffect(() => {
    if (!engine || !ref.current) return undefined;
    const cv = ref.current;
    const dpr = Math.min(2, window.devicePixelRatio || 1);
    cv.width = size * dpr; cv.height = size * dpr;
    cv.style.width = size + 'px'; cv.style.height = size + 'px';
    const ctx = cv.getContext('2d');
    let raf = 0;
    const draw = () => {
      raf = requestAnimationFrame(draw);
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      ctx.clearRect(0, 0, size, size);
      ctx.save();
      if (locked) ctx.filter = 'grayscale(1)';
      ctx.globalAlpha = locked ? 0.45 : 1;
      engine.drawCritter(ctx, charId, size / 2, size - 12, { r: size * 0.29, t: performance.now() / 1000 });
      ctx.restore();
    };
    raf = requestAnimationFrame(draw);
    return () => cancelAnimationFrame(raf);
  }, [engine, charId, size, locked]);
  return <canvas ref={ref} style={{ display: 'block' }} />;
};

})();
