/* 11k wrapped — Story shell: progress, nav, audio, download, transitions */

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

function App() {
  const [idx, setIdx] = useState(0);
  const [paused, setPaused] = useState(false);
  const [elapsed, setElapsed] = useState(0);
  const [transitioning, setTrans] = useState(false);

  const totalSlides = SLIDES.length;
  const isOutro = idx === totalSlides - 1;
  const duration = SLIDES[idx].duration;

  // ─── Interstitial transition between slides ────────────────────────
  const firstRenderRef = useRef(true);
  const transitionMs = getTransitionMs(idx);
  const transItems = getTransitionItems(idx);

  useEffect(() => {
    if (firstRenderRef.current) {
      firstRenderRef.current = false;
      return;
    }
    if (transitionMs <= 0) {
      setTrans(false);
      return;
    }
    setTrans(true);
    const t = setTimeout(() => setTrans(false), transitionMs);
    return () => clearTimeout(t);
  }, [idx, transitionMs]);

  // ─── Progression timer (ref-based — no stale closures) ─────────────
  const elapsedRef = useRef(0);

  useEffect(() => {
    elapsedRef.current = 0;
    setElapsed(0);
  }, [idx]);

  useEffect(() => {
    if (paused || isOutro || transitioning) return;
    let raf;
    const baseElapsed = elapsedRef.current;
    const startedAt = performance.now();
    const tick = (now) => {
      const e = baseElapsed + (now - startedAt);
      elapsedRef.current = e;
      setElapsed(e);
      if (e >= duration) {
        setIdx((i) => Math.min(totalSlides - 1, i + 1));
        return;
      }
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [idx, paused, duration, isOutro, totalSlides, transitioning]);

  // ─── Nav handlers (tap edges, hold to pause) ───────────────────────
  const holdTimer = useRef(null);
  const heldRef = useRef(false);

  const onPointerDown = useCallback(
    () => (e) => {
      e.stopPropagation();
      heldRef.current = false;
      holdTimer.current = setTimeout(() => {
        heldRef.current = true;
        setPaused(true);
      }, 220);
    },
    [],
  );

  const onPointerUp = useCallback(
    (dir) => (e) => {
      e.stopPropagation();
      clearTimeout(holdTimer.current);
      if (heldRef.current) {
        setPaused(false);
        return;
      }
      if (dir === "right") setIdx((i) => Math.min(totalSlides - 1, i + 1));
      else setIdx((i) => Math.max(0, i - 1));
    },
    [totalSlides],
  );

  // ─── Keyboard nav ──────────────────────────────────────────────────
  useEffect(() => {
    const onKey = (e) => {
      if (e.key === "ArrowRight" || e.key === " ") {
        e.preventDefault();
        setIdx((i) => Math.min(totalSlides - 1, i + 1));
      } else if (e.key === "ArrowLeft") {
        setIdx((i) => Math.max(0, i - 1));
      } else if (e.key === "p" || e.key === "P") {
        setPaused((p) => !p);
      }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [totalSlides]);

  // ─── Font embedding for share card ────────────────────────────────
  // sheet.cssRules is blocked for cross-origin stylesheets (browser security),
  // so html-to-image can't auto-embed Google Fonts. Fetch them via fetch()
  // (CORS allowed) and inline as base64 data URIs instead.
  async function embedGoogleFonts() {
    const link = document.querySelector('link[href*="fonts.googleapis.com"]');
    if (!link) return '';
    try {
      const cssText = await fetch(link.href).then(r => r.text());
      const urls = [...cssText.matchAll(/url\(([^)]+)\)/g)]
        .map(m => m[1].replace(/['"]/g, ''));
      let css = cssText;
      await Promise.all(urls.map(async (url) => {
        try {
          const blob = await fetch(url).then(r => r.blob());
          const dataUrl = await new Promise(res => {
            const reader = new FileReader();
            reader.onload = () => res(reader.result);
            reader.readAsDataURL(blob);
          });
          css = css.replace(url, dataUrl);
        } catch {}
      }));
      return css;
    } catch {
      return '';
    }
  }

  // ─── Download share card ───────────────────────────────────────────
  const [downloading, setDownloading] = useState(false);
  const downloadCard = async () => {
    setDownloading(true);
    try {
      const node = document.getElementById("share-card");
      if (!node) throw new Error("no card");
      const fontEmbedCSS = await embedGoogleFonts();
      const dataUrl = await htmlToImage.toPng(node, {
        pixelRatio: 3,
        backgroundColor: null,
        cacheBust: true,
        fontEmbedCSS,
      });
      const a = document.createElement("a");
      a.href = dataUrl;
      a.download = `11-wrapped-${DATA.year.replace(/\s|\//g, "-")}.png`;
      a.click();
    } catch (err) {
      console.error("download failed", err);
      alert("Could not render image: " + err.message);
    } finally {
      setDownloading(false);
    }
  };

  // ─── Render ────────────────────────────────────────────────────────
  return (
    <div
      className="story"
      data-screen-label={`${String(idx + 1).padStart(2, "0")} ${SLIDES[idx].id}`}
    >
      {/* Progress bars */}
      <div className="progress-row">
        {SLIDES.map((s, i) => {
          let pct = 0;
          if (i < idx) pct = 100;
          else if (i === idx)
            pct = isOutro ? 100 : Math.min(100, (elapsed / duration) * 100);
          return (
            <div className="progress-seg" key={s.id}>
              <div className="progress-fill" style={{ width: `${pct}%` }} />
            </div>
          );
        })}
      </div>

      {/* Top chrome */}
      <div className="chrome">
        <div className="brand">
          <span className="dot"></span>
          <span>11 · WRAPPED</span>
        </div>
      </div>

      {/* Nav zones */}
      <div
        className="nav-zone left"
        onPointerDown={onPointerDown("left")}
        onPointerUp={onPointerUp("left")}
        onPointerLeave={() => clearTimeout(holdTimer.current)}
      />
      <div
        className="nav-zone right"
        onPointerDown={onPointerDown("right")}
        onPointerUp={onPointerUp("right")}
        onPointerLeave={() => clearTimeout(holdTimer.current)}
      />

      {/* Slides */}
      {SLIDES.map((s, i) => {
        const Comp = s.Comp;
        const isActive = i === idx && !transitioning;
        if (i === SLIDES.length - 1) {
          return (
            <Comp
              key={s.id}
              active={isActive}
              onDownload={downloadCard}
              downloading={downloading}
            />
          );
        }
        return <Comp key={s.id} active={isActive} />;
      })}

      {/* Falling-chase transition overlay — shown between slides */}
      <div
        className={`transition-overlay ${transitioning ? "active" : ""}`}
        aria-hidden="true"
      >
        <div className="trans-bg"></div>
        {transitioning && transItems && (
          <FallingChase key={idx} items={transItems} totalMs={transitionMs} />
        )}
        {transitioning && transItems && (
          <div className="trans-label mono">
            up next — {SLIDES[idx].id.replace(/-/g, " ")}
          </div>
        )}
      </div>

      {/* Hint on first slide */}
      {idx === 0 && !transitioning && (
        <div className="hint">tap right to begin · hold to pause</div>
      )}

    </div>
  );
}

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
