// What's On, Chand? — scene layers (room, TV, towers, couch, Chand sprite, bubble)
// Stage is a fixed 1920x1080 canvas, scaled to fit by app.jsx.
// Asset paths are absolute (/whats-on-chand/...) — same convention as Weather
// Squatch; the page route serves the shell at the bare path, so relative
// paths would resolve against the site root.

const ASSETS = "/whats-on-chand/assets/";

const SCENE = {
  TV: { x: 540, y: 140, w: 1000, h: 787 },
  // screen cutout, in stage px (iframe oversized a touch so frame art hides edges)
  SCREEN: { x: 653, y: 187, w: 773, h: 480 },
  HOLE:   { x: 659, y: 193, w: 761, h: 468 }
};

// per-sprite anchor: bottom-center at (cx, by), height h
const CHAND_SPRITES = {
  idle:    { src: ASSETS + "chand-idle.png",     h: 980,  dx: 0,   dy: 0 },
  talking: { src: ASSETS + "chand-talking.png",  h: 984,  dx: -6,  dy: 0 },
  point:   { src: ASSETS + "chand-point.png",    h: 920,  dx: -40, dy: 0 },
  react:   { src: ASSETS + "chand-react.png",    h: 1000, dx: 0,   dy: 6 },
  watching:{ src: ASSETS + "chand-watching.png", h: 960,  dx: 0,   dy: 4 }
};
const CHAND_ANCHOR = { cx: 1645, by: 1345 };

// preload all sprite frames so mouth-flapping never flickers
Object.values(CHAND_SPRITES).forEach(s => { const i = new Image(); i.src = s.src; });

function Layer({ src, x, y, w, h, z, style, alt }) {
  return (
    <div style={{ position: "absolute", left: x, top: y, width: w, height: h, zIndex: z, overflow: "hidden", pointerEvents: "none", ...style }}>
      <img src={src} alt={alt || ""} draggable="false" style={{ width: "100%", height: "100%", display: "block" }}></img>
    </div>
  );
}

function ChandSprite({ pose, spinning, offstage }) {
  const s = CHAND_SPRITES[pose] || CHAND_SPRITES.idle;
  const w = s.h * ({ idle: 907 / 1359, talking: 943 / 1364, point: 1011 / 1286, react: 913 / 1404, watching: 968 / 1338 }[pose] || 907 / 1359);
  return (
    <div
      className={"chand-wrap" + (spinning ? " power-spin" : "") + (offstage ? " offstage" : "")}
      style={{
        position: "absolute", zIndex: 6, pointerEvents: "none",
        left: CHAND_ANCHOR.cx - w / 2 + s.dx,
        top: CHAND_ANCHOR.by - s.h + s.dy,
        width: w, height: s.h
      }}
    >
      <img src={s.src} alt={"Chand, " + pose} draggable="false" style={{ width: "100%", height: "100%", display: "block" }}></img>
    </div>
  );
}

function SpeechBubble({ text, mode, visible, size }) {
  // mode: "speech" | "thought"; size: { font, w } computed from full message length
  const s = size || { font: 31, w: 465 };
  return (
    <div className={"bubble " + (mode || "speech") + (visible ? " show" : "")}
         style={{ zIndex: 9, width: s.w, fontSize: s.font }}>
      <div className="bubble-text">{text}{mode === "thought" ? <span className="dots"><i>.</i><i>.</i><i>.</i></span> : null}</div>
      <div className="bubble-tail"></div>
      {mode === "thought" ? <div className="thought-pips"><span></span><span></span></div> : null}
    </div>
  );
}

// animated static noise, drawn only while active
function StaticNoise({ active }) {
  const ref = React.useRef(null);
  React.useEffect(() => {
    if (!active) return;
    const cv = ref.current; if (!cv) return;
    const ctx = cv.getContext("2d");
    const W = cv.width, H = cv.height;
    let raf, on = true;
    const draw = () => {
      if (!on) return;
      const img = ctx.createImageData(W, H);
      const d = img.data;
      for (let i = 0; i < d.length; i += 4) {
        const v = (Math.random() * 256) | 0;
        d[i] = v; d[i + 1] = v; d[i + 2] = v; d[i + 3] = 255;
      }
      ctx.putImageData(img, 0, 0);
      raf = requestAnimationFrame(draw);
    };
    draw();
    return () => { on = false; cancelAnimationFrame(raf); };
  }, [active]);
  if (!active) return null;
  const S = SCENE.SCREEN;
  return <canvas ref={ref} width="204" height="127" style={{ position: "absolute", left: S.x, top: S.y, width: S.w, height: S.h, zIndex: 3, imageRendering: "pixelated", pointerEvents: "none" }}></canvas>;
}

function OffAirCard({ visible }) {
  if (!visible) return null;
  const S = SCENE.SCREEN;
  return (
    <div style={{ position: "absolute", left: S.x, top: S.y, width: S.w, height: S.h, zIndex: 3, pointerEvents: "none" }}>
      <div className="offair">
        <div className="bars">
          <span style={{ background: "#c0c0c0" }}></span><span style={{ background: "#c0c000" }}></span>
          <span style={{ background: "#00c0c0" }}></span><span style={{ background: "#00c000" }}></span>
          <span style={{ background: "#c000c0" }}></span><span style={{ background: "#c00000" }}></span>
          <span style={{ background: "#0000c0" }}></span>
        </div>
        <div className="offair-text">
          <div className="offair-ch">CHANNEL 63</div>
          <div className="offair-sub">OFF AIR &nbsp;&middot;&nbsp; CHAND WILL RETURN</div>
        </div>
      </div>
    </div>
  );
}

// ── players with error signals ────────────────────────────────────────
// YouTube: bare iframes expose no error events cross-origin, so youtube
// playables use the IFrame API (YT.Player) and surface onError codes
// (2/5/100/101/150 — unavailable, restricted, embed-disallowed) to the app.
let __ytReady = null;
function ensureYT() {
  if (window.YT && window.YT.Player) return Promise.resolve();
  if (__ytReady) return __ytReady;
  __ytReady = new Promise((resolve) => {
    const prev = window.onYouTubeIframeAPIReady;
    window.onYouTubeIframeAPIReady = () => { prev && prev(); resolve(); };
    const s = document.createElement("script");
    s.src = "https://www.youtube.com/iframe_api";
    document.head.appendChild(s);
  });
  return __ytReady;
}

function YTPlayer({ videoId, onError }) {
  const hostRef   = React.useRef(null);
  const playerRef = React.useRef(null);
  React.useEffect(() => {
    let cancelled = false;
    ensureYT().then(() => {
      if (cancelled || !hostRef.current) return;
      playerRef.current = new YT.Player(hostRef.current, {
        width: "100%", height: "100%", videoId,
        playerVars: { autoplay: 1, rel: 0, modestbranding: 1 },
        events: {
          // Age-restricted/embed-disabled videos do NOT error on load — they
          // sit on a "Sign in to confirm your age" overlay. An API-initiated
          // play attempt is what provokes onError (101/150), so always ask.
          onReady: (e) => { try { e.target.playVideo(); } catch (err) { /* fine */ } },
          onError: (e) => onError && onError("youtube error " + (e && e.data)),
        },
      });
    });
    return () => {
      cancelled = true;
      try { playerRef.current && playerRef.current.destroy(); } catch (e) { /* mid-load */ }
      playerRef.current = null;
    };
  }, [videoId]);
  // YT.Player replaces this div with its iframe
  return <div style={{ width: "100%", height: "100%" }}><div ref={hostRef}></div></div>;
}

// Archive: the embed page loads fine even for dead items (the app pre-checks
// metadata before tuning) — this load-timeout is only a backup for hangs.
function ArchiveFrame({ identifier, onError }) {
  const loadedRef = React.useRef(false);
  React.useEffect(() => {
    loadedRef.current = false;
    const timer = setTimeout(() => {
      if (!loadedRef.current && onError) onError("archive load timeout");
    }, 12000);
    return () => clearTimeout(timer);
  }, [identifier]);
  return (
    <iframe
      title="Chand's TV"
      src={"https://archive.org/embed/" + encodeURIComponent(identifier) + "?autoplay=1"}
      allow="autoplay; encrypted-media; fullscreen"
      onLoad={() => { loadedRef.current = true; }}
      style={{ width: "100%", height: "100%", border: 0, display: "block", background: "#000" }}
    ></iframe>
  );
}

// video: { provider: 'youtube' | 'archive', id, key } | null
function TVScreen({ video, staticOn, offAir, osd, crtOpacity, onEmbedError }) {
  const S = SCENE.SCREEN;
  return (
    <React.Fragment>
      {/* dark tube behind everything */}
      <div style={{ position: "absolute", left: S.x, top: S.y, width: S.w, height: S.h, zIndex: 2, background: "#0a0c0e" }}>
        {video ? (
          video.provider === "youtube"
            ? <YTPlayer key={video.key} videoId={video.id} onError={onEmbedError} />
            : <ArchiveFrame key={video.key} identifier={video.id} onError={onEmbedError} />
        ) : null}
      </div>
      <StaticNoise active={staticOn} />
      <OffAirCard visible={offAir} />
      {/* CRT glass overlay */}
      <Layer src={ASSETS + "crt-overlay.png"} x={S.x} y={S.y} w={S.w} h={S.h} z={3}
             style={{ opacity: crtOpacity }} alt="" />
      {/* channel OSD */}
      <div className={"osd" + (osd ? " show" : "")} style={{ left: S.x + S.w - 170, top: S.y + 24, zIndex: 4 }}>CH 63</div>
      {/* VCR under the TV, blinking 12:00 */}
      <div className="vcr-unit" style={{ zIndex: 5 }}>
        <div className="vcr-slot"></div>
        <div className="vcr-clock">12:00</div>
      </div>
    </React.Fragment>
  );
}

function Room({ children, video, staticOn, offAir, osd, crtOpacity, pose, spinning, bubble, chandIn, onEmbedError }) {
  const TV = SCENE.TV;
  return (
    <React.Fragment>
      <Layer src={ASSETS + "room-back.png"} x={0} y={0} w={1920} h={1080} z={1} alt="Chand's living room" />
      <TVScreen video={video} staticOn={staticOn} offAir={offAir} osd={osd} crtOpacity={crtOpacity} onEmbedError={onEmbedError} />
      <Layer src={ASSETS + "tv-frame-cutout.png"} x={TV.x} y={TV.y} w={TV.w} h={TV.h} z={4} alt="MAGNAVOLT television" />
      <Layer src={ASSETS + "tower-left.png"}  x={392} y={192} w={262} h={850} z={5}
             style={{ transform: "rotate(-1.2deg)", transformOrigin: "50% 100%" }} alt="VHS tower" />
      <Layer src={ASSETS + "tower-right.png"} x={1545} y={72} w={318} h={974} z={5} alt="VHS tower" />
      <ChandSprite pose={pose} spinning={spinning} offstage={!chandIn} />
      <Layer src={ASSETS + "couch-foreground.png"} x={-20} y={758} w={1960} h={530} z={7} alt="couch" />
      <SpeechBubble text={bubble.text} mode={bubble.mode} visible={bubble.visible} size={bubble.size} />
      {children}
    </React.Fragment>
  );
}

Object.assign(window, { SCENE, Room, ChandSprite, SpeechBubble, TVScreen, StaticNoise, YTPlayer, ArchiveFrame });
