/* Weather Squatch — main state machine.
   MEADOW → WALK → DISCOVERY → WAKE → RISE/GREET → (LOADING ⇄ error) → BROADCAST → GREET…
   Owns the SquatchStage (imperative), the scroll-driven walk, the divination
   loading choreography, and the broadcast playback + caption/mouth loop. */

const { useState, useEffect, useRef, useCallback } = React;

const easeInOut = (t) => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2);

function WeatherSquatch() {
  const [scene, setScene] = useState("boot");      // boot|meadow|walk|discovery|wake|rise|greet|loading|broadcast
  const [scroll, setScroll] = useState(0);
  const [modelReady, setModelReady] = useState(false);
  const [zipError, setZipError] = useState(null);  // null | '404' | 'net'
  const [busy, setBusy] = useState(false);
  const [broadcast, setBroadcastData] = useState(null);  // {city,state,periods,words}
  const [revealed, setRevealed] = useState(0);
  const [ended, setEnded] = useState(false);
  const motion = 0.4;

  const stageRef = useRef(null);
  const canvasRef = useRef(null);
  const containerRef = useRef(null);
  const sceneRef = useRef(scene); sceneRef.current = scene;
  const divCancelRef = useRef(null);
  const bcRef = useRef(null);
  const mouthRafRef = useRef(null);
  const revealedRef = useRef(0);

  // ── Boot: create stage, load model ─────────────────────────────────────────
  useEffect(() => {
    let disposed = false;
    const boot = () => {
      if (disposed || !canvasRef.current) return;
      const stage = new window.SquatchStage(canvasRef.current);
      stageRef.current = stage;
      const fit = () => {
        const el = containerRef.current;
        if (el) stage.resize(el.clientWidth, el.clientHeight);
      };
      fit();
      const ro = new ResizeObserver(fit);
      if (containerRef.current) ro.observe(containerRef.current);
      stage._ro = ro;
      stage.setBreathAmt(motion + 0.3);
      stage.start();
      stage.load(window.MODEL_URL, window.MODEL_URL_FALLBACK).then(() => {
        if (disposed) return;
        stage.poseAsleep();
        stage.setBreathing(false);
        setModelReady(true);
      }).catch((e) => { console.warn("[WeatherSquatch] model load failed:", e); setModelReady(true); });
      setScene("meadow");
    };
    if (window.THREE && window.GLTFLoader) boot();
    else window.addEventListener("three-ready", boot, { once: true });
    return () => {
      disposed = true;
      if (stageRef.current) { stageRef.current.stop(); if (stageRef.current._ro) stageRef.current._ro.disconnect(); }
    };
  }, []);

  // ── THE WALK ───────────────────────────────────────────────────────────────
  // The camera pans across the meadow and arrives on the Squatch already lying
  // on the ground. The pan settles, he holds there a long beat, then stirs.
  const startWalk = useCallback(() => {
    window.getAudioCtx && window.getAudioCtx();   // unlock audio on user gesture
    const stage = stageRef.current;
    if (stage) { stage.poseAsleep(); stage.setBreathing(false); }
    setScene("walk");
    const dur = 4400, t0 = performance.now();
    let revealed = false;
    const step = (now) => {
      const p = Math.min(1, (now - t0) / dur);
      setScroll(easeInOut(p));
      // Once the pan brings him into frame, add slow sleeping breaths so he
      // reads as resting on the ground rather than a frozen prop.
      if (!revealed && p > 0.32) { revealed = true; if (stage) stage.setBreathing(true); }
      if (p < 1) requestAnimationFrame(step);
      else {
        setScene("discovery");
        // Long beat of him lying still on the ground, then he rises → waves → idles.
        setTimeout(runWakeSequence, 1600);
      }
    };
    requestAnimationFrame(step);
  }, []);

  const runWakeSequence = useCallback(() => {
    const stage = stageRef.current;
    setScene("wake");
    if (!stage || !stage.actions.getUp) { setScene("greet"); ambientGreet(); return; }
    // He rises off the ground (resumed at full weight, slowed so the rise reads),
    // then we reveal the greeting + ZIP panel while he waves, settling into idle.
    stage.setBreathing(false);
    stage.riseFromGround({ timeScale: 0.6, onDone: () => {
      setScene("greet");
      stage.setBreathing(true);
      stage.once("wave", { fadeIn: 0.4, onDone: () => { stage.loop("idle", 0.5); } });
    }});
  }, []);

  const ambientGreet = useCallback(() => {
    const stage = stageRef.current;
    if (stage) { stage.loop("idle", 0.5); stage.setBreathing(true); }
  }, []);

  // ── DIVINATION loading choreography ─────────────────────────────────────────
  const startDivination = useCallback(() => {
    const stage = stageRef.current;
    if (!stage) return () => {};
    let cancelled = false, step = 0;
    const next = () => {
      if (cancelled) return;
      step++;
      const r = Math.random();
      if (step % 3 === 0) {                              // pace in place (walk)
        stage.loop("walk", 0.4);
        setTimeout(next, 1800 + Math.random() * 1200);
        return;
      }
      if (r < 0.55) {                                    // look around, curious
        stage.once("look", { onDone: () => { if (!cancelled) { stage.loop("idle", 0.4); setTimeout(next, 300); } } });
      } else {                                           // settle into idle a beat
        stage.loop("idle", 0.4);
        setTimeout(next, 2200 + Math.random() * 1300);
      }
    };
    stage.setMouthTarget(0);
    stage.loop("idle", 0.3);
    setTimeout(next, 1100);
    return () => { cancelled = true; };
  }, []);

  // ── ZIP submit → fetch → broadcast ──────────────────────────────────────────
  const onZip = useCallback(async (zip) => {
    setZipError(null);
    setBusy(true);
    setScene("loading");
    divCancelRef.current = startDivination();
    try {
      const data = await window.fetchBroadcast(zip);
      divCancelRef.current && divCancelRef.current();
      beginBroadcast(data);
    } catch (e) {
      divCancelRef.current && divCancelRef.current();
      const stage = stageRef.current;
      // Confused / apologetic reaction, then back to greet.
      if (stage) stage.once("look", { onDone: () => { stage.loop("idle", 0.4); stage.setBreathing(true); } });
      setZipError(e && e.status === 404 ? "404" : "net");
      setBusy(false);
      setScene("greet");
    }
  }, [startDivination]);

  const beginBroadcast = useCallback(async (data) => {
    const stage = stageRef.current;
    const bc = new window.Broadcast({ script: data.script, audioBase64: data.audioBase64 });
    bcRef.current = bc;
    await bc.prepare();
    setBroadcastData({ city: data.city, state: data.state, periods: data.periods, words: bc.words });
    setRevealed(0); revealedRef.current = 0;
    setEnded(false);
    setBusy(false);
    setScene("broadcast");
    if (stage) { stage.loop("idle", 0.5); stage.setBreathing(true); }

    bc.onended = () => {
      setEnded(true);
      if (mouthRafRef.current) cancelAnimationFrame(mouthRafRef.current);
      if (stage) { stage.setMouthTarget(0); }
    };
    bc.start();

    const mouthGain = 1.0;
    const loop = () => {
      if (sceneRef.current !== "broadcast") return;
      if (stage) stage.setMouthTarget(bc.mouthValue * mouthGain);
      if (bc.revealedCount !== revealedRef.current) {
        revealedRef.current = bc.revealedCount;
        setRevealed(bc.revealedCount);
      }
      mouthRafRef.current = requestAnimationFrame(loop);
    };
    mouthRafRef.current = requestAnimationFrame(loop);
  }, []);

  // ── Check another location → back to GREET ──────────────────────────────────
  const onAgain = useCallback(() => {
    if (bcRef.current) bcRef.current.stop();
    if (mouthRafRef.current) cancelAnimationFrame(mouthRafRef.current);
    setEnded(false);
    setBroadcastData(null);
    setRevealed(0);
    setZipError(null);
    setScene("greet");
    const stage = stageRef.current;
    if (stage) { stage.setMouthTarget(0); stage.once("wave", { onDone: () => { stage.loop("idle", 0.45); stage.setBreathing(true); } }); }
  }, []);

  // ── Debug hook (verification only; harmless in production) ──────────────────
  useEffect(() => {
    window.__ws = {
      go: (s) => {
        setScene(s);
        const st = stageRef.current;
        if (st && ["greet", "broadcast", "loading"].includes(s)) { st.loop("idle", 0.3); st.setBreathing(true); }
      },
      stage: () => stageRef.current,
      pump: (frames = 90, dt = 0.016) => {
        const st = stageRef.current; if (!st) return;
        for (let i = 0; i < frames; i++) { if (st.mixer) st.mixer.update(dt); }
        st.renderer.render(st.scene, st.camera);
      },
    };
  }, []);
  if (typeof window !== "undefined" && window.__ws) {
    window.__ws.submitZip = onZip;
    window.__ws.endcast = () => { if (bcRef.current) bcRef.current._finish(); };
    window.__ws.results = () => {
      const m = window.mockBroadcast("97201");
      setBroadcastData({ city: m.city, state: m.state, periods: m.periods, words: m.script.split(/\s+/) });
      setRevealed(0); setScene("broadcast"); setEnded(true);
    };
    window.__ws.scrollTo = (v) => { setScene("walk"); setScroll(v); };
  }

  // ── Derived visibility ──────────────────────────────────────────────────────
  const showSquatch = ["discovery", "wake", "rise", "greet", "loading", "broadcast"].includes(scene)
    || (scene === "walk" && scroll > 0.3);   // reveal him lying on the ground as the pan arrives

  // While the camera pans, slide the Squatch leftward in lockstep with the
  // meadow layer he's resting on (speed 0.7 × 92vw pan unit) so he stays PLANTED
  // in the world and arrives at center — rather than gliding along with the camera.
  const groundSlideVw = scene === "walk" ? (1 - Math.min(1, scroll)) * 0.7 * 92 : 0;
  const inBroadcast = scene === "broadcast";

  return (
    <div className="ws-root" ref={containerRef}>
      <window.ParallaxScene scroll={scroll} motion={motion} />

      <canvas
        ref={canvasRef}
        className={"ws-canvas ws-fade " + (showSquatch ? "in" : "out")}
        style={{
          transform: "translateX(" + groundSlideVw + "vw)",
          transition: scene === "walk" ? "opacity 0.6s ease" : undefined,
        }}
      ></canvas>

      {/* vignette + ground contact shadow hint */}
      <div className="ws-vignette"></div>

      <div className="ws-ui">
        <window.TitleCard onStart={startWalk} visible={scene === "meadow"} />

        {scene === "discovery" && (
          <div className="ws-discovery-note ws-fade in">a quiet spot by the pond…</div>
        )}

        <window.ZipPanel
          onSubmit={onZip}
          busy={busy}
          error={zipError}
          visible={scene === "greet"}
        />

        <window.LoadingHint visible={scene === "loading"} />

        {broadcast && (
          <window.Captions words={broadcast.words} revealed={revealed} visible={inBroadcast && !ended} />
        )}
        {broadcast && (
          <div
            className={"ws-results ws-fade " + (inBroadcast && ended ? "in" : "out")}
            style={{ pointerEvents: inBroadcast && ended ? "auto" : "none" }}
          >
            <window.ForecastCards city={broadcast.city} state={broadcast.state} periods={broadcast.periods} />
            <window.AgainButton onAgain={onAgain} />
          </div>
        )}

        {!modelReady && scene !== "meadow" && (
          <div className="ws-modelwait">waking the Squatch…</div>
        )}
      </div>
    </div>
  );
}

(function mount() {
  const el = document.getElementById("root");
  ReactDOM.createRoot(el).render(React.createElement(WeatherSquatch));
})();

window.WeatherSquatch = WeatherSquatch;
