/* SCROLL ENGINE — the wheel scrubs a 100-frame morph, then hands off to the 137 desktop. */
const { useState, useRef, useEffect } = React;

const FRAME_COUNT = 100;
const framePath = (i) => `frames/f${String(i).padStart(3, '0')}.jpg`;
const TRACK_VH = 1440;

const BEATS = [
  { id: 'apparatus', index: '01', ch: '037', name: 'THE APPARATUS', mark: '⊗',
    title: 'THE APPARATUS', body: 'We salvage dead machines and teach them to dream. Paint, signal, and ritual — arranged until the room tilts.',
    inA: 0.10, inB: 0.15, outA: 0.20, outB: 0.25, jump: 0.165 },
  { id: 'gears', index: '02', ch: '133', name: 'THE GEARS TURN', mark: '◬',
    title: 'THE GEARS TURN', body: 'We paint the door. The machine builds the hinge. The dial is already wandering — 1/33 where 1/37 should be.',
    inA: 0.28, inB: 0.33, outA: 0.40, outB: 0.45, jump: 0.36 },
  { id: 'hunt', index: '03', ch: '13?', name: 'THE HUNT', mark: '☍',
    title: 'THE CHANNEL HUNTS', body: 'Warm dies to phosphor and the dial goes feral — 137, 139, 136. Ninety dead televisions lean in to watch it search.',
    inA: 0.52, inB: 0.57, outA: 0.68, outB: 0.74, jump: 0.60 },
  { id: 'shrine', index: '04', ch: '137', name: 'CHANNEL 137', mark: '✶',
    title: 'CHANNEL 137', body: 'The wander ends. The needle drops into 137 and holds. You have arrived at the shrine — turn the wheel and step through.',
    inA: 0.84, inB: 0.885, outA: 0.95, outB: 0.99, jump: 0.92 },
];

const NAV = [
  { idx: '037', name: 'INCOMING', frac: 0 },
  ...BEATS.map((b) => ({ idx: b.index, name: b.name, frac: b.jump })),
  { idx: '137', name: 'DESKTOP', frac: 1 },
];

function ramp(p, a, b, c, d) {
  if (p <= a || p >= d) return 0;
  if (p < b) return (p - a) / (b - a);
  if (p < c) return 1;
  return 1 - (p - c) / (d - c);
}
function channel(p) {
  if (p < 0.32) return { code: '037', tag: 'S 1/37' };
  if (p < 0.52) return { code: '133', tag: 'DRIFT' };
  if (p < 0.62) return { code: '137', tag: 'HUNTING' };
  if (p < 0.72) return { code: '139', tag: 'HUNTING' };
  if (p < 0.86) return { code: '136', tag: 'ALMOST' };
  return { code: '137', tag: 'LOCK' };
}
const clampf = (x) => Math.max(0, Math.min(1, x));

function App() {
  const Desktop137 = window.X37Desktop;
  const [loadedCount, setLoadedCount] = useState(0);
  const [loaderGone, setLoaderGone] = useState(false);
  const [deskActive, setDeskActive] = useState(false);
  const [audioOn, setAudioOn] = useState(false);

  const imgsRef = useRef([]);
  const filmRef = useRef(null);
  const ctxRef = useRef(null);
  const stageRef = useRef(null);
  const renderP = useRef(0);
  const deskActiveRef = useRef(false);
  const audioRef = useRef(null);
  const forceRenderRef = useRef(null);
  const tweenRef = useRef(null);

  const progRef = useRef(null);
  const sigPctRef = useRef(null);
  const sigChRef = useRef(null);
  const sigNameRef = useRef(null);
  const heroRef = useRef(null);
  const hintRef = useRef(null);
  const hudRef = useRef(null);
  const deskRef = useRef(null);
  const beatRefs = useRef([]);
  const navRefs = useRef([]);

  /* ---- preload frames ---- */
  useEffect(() => {
    let live = true, done = 0;
    const arr = new Array(FRAME_COUNT);
    for (let i = 0; i < FRAME_COUNT; i++) {
      const im = new Image();
      im.onload = im.onerror = () => { if (!live) return; done += 1; setLoadedCount(done); };
      im.src = framePath(i);
      arr[i] = im;
    }
    imgsRef.current = arr;
    return () => { live = false; };
  }, []);

  useEffect(() => {
    if (loadedCount >= FRAME_COUNT) { const t = setTimeout(() => setLoaderGone(true), 500); return () => clearTimeout(t); }
  }, [loadedCount]);

  /* ---- canvas sizing ---- */
  const resize = () => {
    const cv = filmRef.current, st = stageRef.current; if (!cv || !st) return;
    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    const w = st.clientWidth, h = st.clientHeight;
    cv.width = Math.round(w * dpr); cv.height = Math.round(h * dpr);
    cv.style.width = w + 'px'; cv.style.height = h + 'px';
    const ctx = cv.getContext('2d');
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    ctxRef.current = ctx;
  };

  const drawFrame = (p) => {
    const ctx = ctxRef.current, cv = filmRef.current; if (!ctx || !cv) return;
    const idx = Math.max(0, Math.min(FRAME_COUNT - 1, Math.round(p * (FRAME_COUNT - 1))));
    const img = imgsRef.current[idx];
    const w = cv.clientWidth, h = cv.clientHeight;
    ctx.fillStyle = '#000'; ctx.fillRect(0, 0, w, h);
    if (!img || !img.complete || !img.naturalWidth) return;
    const iw = img.naturalWidth, ih = img.naturalHeight;
    const zoom = p > 0.9 ? 1 + ((p - 0.9) / 0.1) * 0.12 : 1;
    const scale = Math.max(w / iw, h / ih) * zoom;
    const dw = iw * scale, dh = ih * scale;
    const dx = (w - dw) / 2, dy = (h - dh) * 0.42;
    ctx.drawImage(img, dx, dy, dw, dh);
  };

  const updateHUD = (p, target) => {
    if (progRef.current) progRef.current.style.width = (target * 100).toFixed(2) + '%';
    if (sigPctRef.current) sigPctRef.current.textContent = String(Math.round(target * 100)).padStart(3, '0');
    const ch = channel(p);
    if (sigChRef.current) sigChRef.current.textContent = ch.code;
    if (sigNameRef.current) sigNameRef.current.textContent = ch.tag;

    const heroOp = p <= 0.02 ? 1 : clampf(1 - (p - 0.02) / 0.06);
    if (heroRef.current) { heroRef.current.style.opacity = heroOp; heroRef.current.style.transform = `translateY(${(1 - heroOp) * -14}px)`; }
    if (hintRef.current) hintRef.current.style.opacity = heroOp;

    for (let i = 0; i < BEATS.length; i++) {
      const b = BEATS[i], el = beatRefs.current[i]; if (!el) continue;
      const op = ramp(p, b.inA, b.inB, b.outA, b.outB);
      el.style.opacity = op;
      el.style.transform = `translateY(${(1 - op) * 16}px)`;
    }

    if (hudRef.current) hudRef.current.style.opacity = clampf(1 - (target - 0.955) / 0.03);
    if (deskRef.current) deskRef.current.style.opacity = clampf((target - 0.95) / 0.042);

    let act = 0;
    for (let i = 0; i < BEATS.length; i++) if (target >= BEATS[i].inA) act = i + 1;
    if (target > 0.985) act = NAV.length - 1;
    for (let i = 0; i < navRefs.current.length; i++) {
      const el = navRefs.current[i]; if (el) el.classList.toggle('active', i === act);
    }
  };

  /* ---- render loop (rAF smoothing + scroll-driven fallback) ---- */
  useEffect(() => {
    resize();
    const targetOf = () => {
      const sc = document.scrollingElement || document.documentElement;
      const max = sc.scrollHeight - window.innerHeight;
      return max > 0 ? clampf(sc.scrollTop / max) : 0;
    };
    const render = (p, target) => {
      drawFrame(p);
      updateHUD(p, target);
      const on = target > 0.988 ? true : (target < 0.965 ? false : deskActiveRef.current);
      if (on !== deskActiveRef.current) { deskActiveRef.current = on; setDeskActive(on); }
    };
    forceRenderRef.current = () => render(renderP.current, targetOf());
    const onResize = () => { resize(); render(renderP.current, targetOf()); };
    window.addEventListener('resize', onResize);
    let raf, lastTs = performance.now();
    const loop = (ts) => {
      lastTs = ts || performance.now();
      const target = targetOf();
      renderP.current += (target - renderP.current) * 0.16;
      if (Math.abs(target - renderP.current) < 0.0005) renderP.current = target;
      render(renderP.current, target);
      raf = requestAnimationFrame(loop);
    };
    raf = requestAnimationFrame(loop);
    // fallback: if rAF is throttled (hidden/inactive tab), drive rendering straight off scroll
    const onScroll = () => {
      if (performance.now() - lastTs > 180) {
        const t = targetOf();
        renderP.current = t;
        render(t, t);
      }
    };
    window.addEventListener('scroll', onScroll, { passive: true });
    render(0, 0);
    return () => { cancelAnimationFrame(raf); window.removeEventListener('resize', onResize); window.removeEventListener('scroll', onScroll); };
  }, []);

  /* redraw once frames finish loading */
  useEffect(() => { if (forceRenderRef.current) forceRenderRef.current(); }, [loadedCount]);

  /* ---- scroll-lock hook (used by desktop while a window is open) ---- */
  useEffect(() => {
    let locked = false;
    window.__x37Lock = (v) => { locked = v; };
    const stop = (e) => {
      if (!locked) return;
      const el = e.target && e.target.closest ? e.target.closest('.win-body') : null;
      if (el && el.scrollHeight > el.clientHeight + 2) return; // let window bodies scroll
      e.preventDefault();
    };
    window.addEventListener('wheel', stop, { passive: false });
    window.addEventListener('touchmove', stop, { passive: false });
    return () => { window.removeEventListener('wheel', stop); window.removeEventListener('touchmove', stop); delete window.__x37Lock; };
  }, []);

  const tweenTo = (frac) => {
    if (window.__x37Lock) window.__x37Lock(false);
    const sc = document.scrollingElement || document.documentElement;
    const max = sc.scrollHeight - window.innerHeight;
    const targetY = Math.max(0, Math.min(max, frac * max));
    const start = sc.scrollTop, d = targetY - start;
    if (Math.abs(d) < 2) { sc.scrollTop = targetY; return; }
    if (tweenRef.current) clearInterval(tweenRef.current);
    const t0 = performance.now(), dur = Math.min(2600, Math.max(500, Math.abs(d) / Math.max(1, max) * 2800));
    tweenRef.current = setInterval(() => {
      const k = Math.min(1, (performance.now() - t0) / dur);
      const e = k < 0.5 ? 2 * k * k : 1 - Math.pow(-2 * k + 2, 2) / 2;
      sc.scrollTop = start + d * e;
      if (k >= 1) { clearInterval(tweenRef.current); tweenRef.current = null; }
    }, 16);
  };

  const toggleAudio = () => {
    if (audioRef.current) {
      const { ac, master } = audioRef.current;
      master.gain.linearRampToValueAtTime(0, ac.currentTime + 0.5);
      setTimeout(() => ac.close(), 650);
      audioRef.current = null; setAudioOn(false); return;
    }
    const AC = window.AudioContext || window.webkitAudioContext; if (!AC) return;
    const ac = new AC();
    const master = ac.createGain(); master.gain.value = 0; master.connect(ac.destination);
    const o1 = ac.createOscillator(); o1.type = 'sine'; o1.frequency.value = 55;
    const o2 = ac.createOscillator(); o2.type = 'sine'; o2.frequency.value = 82.4;
    const o3 = ac.createOscillator(); o3.type = 'sawtooth'; o3.frequency.value = 110;
    const lp = ac.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.value = 340; lp.Q.value = 6;
    const g3 = ac.createGain(); g3.gain.value = 0.05;
    const lfo = ac.createOscillator(); lfo.type = 'sine'; lfo.frequency.value = 0.08;
    const lfoG = ac.createGain(); lfoG.gain.value = 120;
    lfo.connect(lfoG); lfoG.connect(lp.frequency);
    o1.connect(master); o2.connect(master); o3.connect(g3); g3.connect(lp); lp.connect(master);
    o1.start(); o2.start(); o3.start(); lfo.start();
    master.gain.linearRampToValueAtTime(0.13, ac.currentTime + 1.4);
    audioRef.current = { ac, master }; setAudioOn(true);
  };

  return (
    <>
      <div id="loader" className={loaderGone ? 'gone' : ''}>
        <div className="loader-mark" aria-hidden="true">137</div>
        <div className="loader-txt">Receiving Transmission</div>
        <div className="loader-bar"><div className="loader-fill" style={{ width: (loadedCount / FRAME_COUNT * 100) + '%' }}></div></div>
        <div className="loader-count">{String(loadedCount).padStart(3, '0')} / {FRAME_COUNT} FRAMES · CH 037</div>
      </div>

      <div className="stage" ref={stageRef}>
          <canvas id="film" ref={filmRef}></canvas>
          <div className="crt-scan"></div>
          <div className="crt-vig"></div>

          <div className="hud" ref={hudRef}>
            <div className="progressbar" ref={progRef}></div>

            <div className="topbar">
              <div className="brand">
                <span className="lamp"></span>
                <span className="brand-name">STUDIO&nbsp;X37</span>
                <span className="brand-sig" aria-hidden="true">X</span>
              </div>
              <div className="sig-read">
                <span>SIG&nbsp;<b ref={sigPctRef}>000</b>%</span>
                <span className="sig-sep">//</span>
                <span className="hide-sm">CH&nbsp;<b ref={sigChRef}>037</b></span>
                <span className="sig-sep hide-sm">·</span>
                <span ref={sigNameRef} className="hide-sm">S 1/37</span>
                <button className="skip-btn pe" onClick={() => tweenTo(1)}>SKIP → 137</button>
              </div>
            </div>

            <div className="hero" ref={heroRef}>
              <div className="hero-eyebrow">TRANSMISSION // 037 — INCOMING</div>
              <div className="hero-word">STUDIO X37</div>
              <div className="hero-sig" title="STUDIO X37" aria-hidden="true">STUDIO X37</div>
              <div className="hero-sub">You are receiving this transmission. Turn the wheel to bring the signal in — the machine will build itself around you.</div>
            </div>

            <div className="scrollhint" ref={hintRef}>
              <span className="x37-label">SCROLL TO RECEIVE</span>
              <span className="chev blink">▼</span>
            </div>

            {BEATS.map((b, i) => (
              <div className="beat" key={b.id} ref={(el) => { beatRefs.current[i] = el; }}>
                <div className="beat-panel">
                  <div className="beat-seam">
                    <span className="beat-lbl">{b.mark} {b.name}</span>
                    <span className="beat-idx">CH {b.ch} // {b.index}</span>
                  </div>
                  <div className="beat-title">{b.title}</div>
                  <div className="beat-body">{b.body}</div>
                </div>
              </div>
            ))}

            <div className="scene-nav">
              {NAV.map((n, i) => (
                <button className="nav-item" key={i} ref={(el) => { navRefs.current[i] = el; }} onClick={() => tweenTo(n.frac)}>
                  <span className="nav-name">{n.name}</span>
                  <span className="nav-tick"></span>
                  <span className="nav-idx">{n.idx}</span>
                </button>
              ))}
            </div>
          </div>

          <div className="desk" ref={deskRef} style={{ pointerEvents: deskActive ? 'auto' : 'none' }}>
            {Desktop137 ? <Desktop137 active={deskActive} onExit={() => tweenTo(0)} /> : null}
          </div>

          <div className="audio-layer">
            <button className={'audio-btn' + (audioOn ? ' on' : '')} onClick={toggleAudio}>
              <span className="dot"></span>{audioOn ? 'SIGNAL ON' : 'AMBIENCE'}
            </button>
          </div>
        </div>
      <div id="track" style={{ height: TRACK_VH + 'vh' }}></div>
    </>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
