/* app.jsx — Temple & Cosmos observatory. Renders from window.TC. */
const { useState, useEffect, useRef, useCallback } = React;
const H = (html) => ({ dangerouslySetInnerHTML: { __html: html } });
const reduced = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;

/* ── cosmic starfield ── */
function Cosmos() {
  const ref = useRef(null);
  useEffect(() => {
    const cv = ref.current, ctx = cv.getContext("2d");
    let W, H2, parts, orbs, raf, t0 = performance.now();
    const rnd = (a, b) => a + Math.random() * (b - a);
    function resize() {
      W = cv.width = window.innerWidth; H2 = cv.height = window.innerHeight;
      const N = Math.min(360, Math.round(W * H2 / 7000));
      parts = Array.from({ length: N }, () => ({
        x: rnd(0, W), y: rnd(0, H2), vx: rnd(-4, 4), vy: rnd(-2.4, 2.4),
        ph: rnd(0, 6.28), tw: rnd(0.2, 0.9), s: rnd(0.4, 2.2), hue: rnd(44, 58), lum: rnd(60, 82),
      }));
      orbs = Array.from({ length: 5 }, () => ({ x: rnd(0, W), y: rnd(0, H2), r: rnd(120, 280), hue: rnd(220, 255), ph: rnd(0, 6.28) }));
    }
    function draw(now) {
      const t = (now - t0) / 1000;
      ctx.clearRect(0, 0, W, H2);
      ctx.globalCompositeOperation = "lighter";
      for (const o of orbs) {
        const a = 0.05 + 0.03 * Math.sin(o.ph + t * 0.08);
        const g = ctx.createRadialGradient(o.x, o.y, 0, o.x, o.y, o.r);
        g.addColorStop(0, `hsla(${o.hue},60%,32%,${a})`); g.addColorStop(1, `hsla(${o.hue},60%,24%,0)`);
        ctx.fillStyle = g; ctx.fillRect(o.x - o.r, o.y - o.r, o.r * 2, o.r * 2);
      }
      for (const p of parts) {
        const x = ((p.x + p.vx * t) % W + W) % W, y = ((p.y + p.vy * t) % H2 + H2) % H2;
        const tw = reduced ? 0.7 : Math.sin(p.ph + p.tw * t) * 0.5 + 0.5;
        ctx.fillStyle = `hsla(${p.hue},70%,${p.lum}%,${0.18 + 0.55 * tw})`;
        ctx.beginPath(); ctx.arc(x, y, p.s * (0.85 + tw * 0.4), 0, 6.2832); ctx.fill();
      }
      ctx.globalCompositeOperation = "source-over";
      raf = requestAnimationFrame(draw);
    }
    resize(); window.addEventListener("resize", resize);
    raf = requestAnimationFrame(draw);
    return () => { cancelAnimationFrame(raf); window.removeEventListener("resize", resize); };
  }, []);
  return <canvas id="cosmos" ref={ref} />;
}

/* ── scroll reveal: geometry-based (IntersectionObserver is unreliable in
   sandboxed iframes; a scroll/resize pass always fires and preserves the
   cinematic effect). ── */
function useReveals() {
  useEffect(() => {
    if (reduced) { document.querySelectorAll(".reveal").forEach((e) => e.classList.add("in")); return; }
    const pass = () => {
      const vh = window.innerHeight || document.documentElement.clientHeight;
      document.querySelectorAll(".reveal:not(.in)").forEach((el) => {
        if (el.getBoundingClientRect().top < vh * 0.9) el.classList.add("in");
      });
    };
    let ticking = false;
    const onScroll = () => { if (!ticking) { ticking = true; requestAnimationFrame(() => { pass(); ticking = false; }); } };
    pass();                                   // reveal whatever is already in view
    requestAnimationFrame(pass);              // and again after first paint/layout
    window.addEventListener("scroll", onScroll, { passive: true });
    window.addEventListener("resize", onScroll);
    // last-resort safety: if scroll never fires in some context, never strand content
    const safety = setTimeout(() => document.querySelectorAll(".reveal:not(.in)").forEach((el) => el.classList.add("in")), 8000);
    return () => { window.removeEventListener("scroll", onScroll); window.removeEventListener("resize", onScroll); clearTimeout(safety); };
  }, []);
}

/* ── chronology rail ── */
function Rail({ active }) {
  const [show, setShow] = useState(false);
  useEffect(() => {
    const onScroll = () => setShow(window.scrollY > window.innerHeight * 0.6);
    onScroll(); window.addEventListener("scroll", onScroll, { passive: true });
    return () => window.removeEventListener("scroll", onScroll);
  }, []);
  const go = (chap) => {
    const el = chap ? document.getElementById("ch-" + chap) : document.getElementById("top");
    if (el) el.scrollIntoView({ behavior: reduced ? "auto" : "smooth", block: "start" });
  };
  return (
    <nav className={"rail" + (show ? " show" : "")} aria-label="Chronology">
      <div className="rail-title">The Recovery</div>
      <div className="rail-line">
        {window.TC.TIMELINE.map((t) => (
          <div key={t.year} className={"rail-tick" + (t.pivot ? " pivot" : "") + (t.chap && t.chap === active ? " active" : "")}
            onClick={() => go(t.chap)}>
            <span className="yr">{t.year}</span>
            <span className="lb">{t.label}</span>
          </div>
        ))}
      </div>
    </nav>
  );
}

/* ── evidence plate ── */
function Plate({ entry, idx }) {
  const [open, setOpen] = useState(false);
  return (
    <article className={"plate reveal" + (open ? " open" : "")}>
      <div className="plate-head">
        <span>Plate {String(idx + 1).padStart(2, "0")} · {entry.cats}</span>
        <span className="stars">{"★".repeat(entry.stars)}{"☆".repeat(3 - entry.stars)}</span>
      </div>
      <blockquote className="plate-q" {...H("&ldquo;" + entry.quote + "&rdquo;")} />
      <div className="plate-attr" {...H("— " + entry.attr)} />
      <button className="plate-toggle" onClick={() => setOpen((o) => !o)} aria-expanded={open}>
        <span className="chev">›</span>{open ? "Hide source" : "Source & discovery"}
      </button>
      <div className="plate-deep"><div><div className="inner">
        <span className="k">Source</span><span className="v" {...H(entry.cite)} />
        <span className="k">Surfaced</span><span className="v">{entry.discovered}</span>
        <span className="k">Tags</span><span className="v">{entry.cats}</span>
      </div></div></div>
    </article>
  );
}

/* ── augur explorer (chapter I) ── */
function AugurExplorer({ chapter }) {
  const [sel, setSel] = useState(chapter.sites[0].k);
  const site = chapter.sites.find((s) => s.k === sel);
  return (
    <div style={{ display: "flex", gap: 40, alignItems: "center", flexWrap: "wrap", justifyContent: "center" }}>
      <CosmicCenter size={380} interactive sites={chapter.sites} selected={sel} onSelect={setSel} />
      <div style={{ minWidth: 260, maxWidth: 320 }}>
        <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
          {chapter.sites.map((s) => (
            <button key={s.k} onClick={() => setSel(s.k)}
              style={{
                textAlign: "left", background: "none", cursor: "pointer", padding: "10px 0",
                borderBottom: "1px solid var(--line-2)", borderLeft: 0, borderRight: 0, borderTop: 0,
                fontFamily: "var(--mono)", fontSize: 13, letterSpacing: "0.16em", textTransform: "uppercase",
                color: sel === s.k ? "var(--gold-3)" : "var(--mist-2)", transition: "color .2s",
              }}>{s.name}</button>
          ))}
        </div>
        <p style={{ marginTop: 18, fontFamily: "var(--serif)", fontStyle: "italic", fontSize: 19, lineHeight: 1.5, color: "var(--cream)" }} {...H(site.note)} />
      </div>
    </div>
  );
}

/* ── a chapter ── */
function Chapter({ ch }) {
  return (
    <section className="chapter" id={"ch-" + ch.id} data-chap={ch.id}>
      <div className="content">
        <header className="chapter-head reveal">
          <div className="chapter-num">{ch.num}</div>
          <div className="chapter-kicker kicker">{ch.kicker}</div>
          <h2>{ch.title}</h2>
          <div className="chapter-coord">{ch.coord}</div>
        </header>
        <p className="chapter-lede reveal d1" {...H(ch.lede)} />

        <div className="motif reveal d1">
          {ch.diagram.type === "augur" ? <AugurExplorer chapter={ch} /> : <Motif chapter={ch} />}
          <div className="motif-note" {...H(ch.diagram.note)} />
        </div>

        <div className="restoration reveal">
          <div className="lab">In the Restoration</div>
          <div className="txt" {...H(ch.restoration)} />
        </div>

        <div className="plates-head reveal">
          <span>The Evidence</span><span className="ct">{ch.entries.length} sources</span>
        </div>
        {ch.entries.map((e, i) => <Plate key={i} entry={e} idx={i} />)}

        <p className="pivot reveal" {...H(ch.pivot)} />
      </div>
    </section>
  );
}

/* ── narration player ── */
function Player() {
  const aud = useRef(null);
  const [playing, setPlaying] = useState(false);
  const [pct, setPct] = useState(0);
  const [time, setTime] = useState("0:00");
  const fmt = (s) => `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, "0")}`;
  useEffect(() => {
    const a = aud.current;
    const saved = parseFloat(localStorage.getItem("tc-narration-pos") || "0");
    if (saved > 0) a.currentTime = saved;
    const onTime = () => {
      setPct(a.duration ? (a.currentTime / a.duration) * 100 : 0);
      setTime(fmt(a.currentTime));
      localStorage.setItem("tc-narration-pos", String(a.currentTime));
    };
    const onEnd = () => setPlaying(false);
    a.addEventListener("timeupdate", onTime); a.addEventListener("ended", onEnd);
    return () => { a.removeEventListener("timeupdate", onTime); a.removeEventListener("ended", onEnd); };
  }, []);
  const toggle = () => {
    const a = aud.current;
    if (a.paused) { a.play(); setPlaying(true); } else { a.pause(); setPlaying(false); }
  };
  const seek = (e) => {
    const a = aud.current, r = e.currentTarget.getBoundingClientRect();
    if (a.duration) a.currentTime = ((e.clientX - r.left) / r.width) * a.duration;
  };
  return (
    <div className="player">
      <audio ref={aud} src="assets/narration-full.mp3" preload="metadata" />
      <button className="pbtn" onClick={toggle} aria-label={playing ? "Pause narration" : "Play narration"}>
        {playing
          ? <svg width="13" height="14" viewBox="0 0 13 14" fill="currentColor"><rect x="1" width="4" height="14" /><rect x="8" width="4" height="14" /></svg>
          : <svg width="13" height="15" viewBox="0 0 13 15" fill="currentColor"><path d="M0 0l13 7.5L0 15z" /></svg>}
      </button>
      <div className="pinfo">
        <div className="plab">Narration · Guided read</div>
        <div className="pbar" onClick={seek}><div className="pfill" style={{ width: pct + "%" }} /></div>
      </div>
      <div className="ptime">{time}</div>
    </div>
  );
}

/* ── epilogue ── */
function Epilogue({ epi }) {
  return (
    <section className="epilogue" id="ch-epilogue">
      <div className="content">
        <div className="kicker reveal">{epi.kicker}</div>
        <h2 className="reveal d1" {...H(epi.title)} />
        <p className="epi-lede reveal d1" {...H(epi.lede)} />
        <div className="epi-elements reveal d2">
          {epi.elements.map((el) => <span key={el} className="el">{el}</span>)}
        </div>
        <p className="epi-close reveal" {...H(epi.close)} />
        <div className="epi-sig reveal" {...H(epi.sig)} />
        <div className="epi-sub reveal" {...H(epi.sub)} />
      </div>
    </section>
  );
}

/* ── app ── */
function App() {
  const [active, setActive] = useState(null);
  useReveals();
  useEffect(() => {
    const pick = () => {
      const mid = window.innerHeight / 2;
      let best = null, bestD = Infinity;
      document.querySelectorAll("[data-chap]").forEach((s) => {
        const r = s.getBoundingClientRect();
        if (r.top < window.innerHeight && r.bottom > 0) {
          const d = Math.abs((r.top + r.bottom) / 2 - mid);
          if (d < bestD) { bestD = d; best = s.dataset.chap; }
        }
      });
      if (best) setActive(best);
    };
    let ticking = false;
    const onScroll = () => { if (!ticking) { ticking = true; requestAnimationFrame(() => { pick(); ticking = false; }); } };
    pick();
    window.addEventListener("scroll", onScroll, { passive: true });
    return () => window.removeEventListener("scroll", onScroll);
  }, []);

  return (
    <React.Fragment>
      <Cosmos />
      <div id="cosmos-tint" />
      <Rail active={active} />
      <div className="wrap">
        <header className="hero" id="top">
          <div className="hero-top">
            <span>Lights &amp; Perfections</span>
            <span>An L&amp;P Observatory · 1842 → present</span>
          </div>
          <div className="hero-diagram"><HeroDiagram /></div>
          <h1>Temple <span className="amp">&amp;</span> Cosmos</h1>
          <div className="hero-sub">Ancient Roots of the Restoration&rsquo;s Temple</div>
          <p className="hero-lede">In 1842 a temple ceremony was introduced. Its every element would surface, over the following century, from the desert sand — from sources he could not have read.</p>
          <div className="scroll-cue"><span>Descend</span><span className="ln" /></div>
        </header>

        {window.TC.CHAPTERS.map((ch) => <Chapter key={ch.id} ch={ch} />)}

        <hr className="hairline" style={{ maxWidth: 900, margin: "0 auto" }} />
        <Epilogue epi={window.TC.EPILOGUE} />
      </div>
      <div id="vignette" />
      <Player />
    </React.Fragment>
  );
}

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

/* dismiss the loading splash once mounted */
requestAnimationFrame(() => {
  const s = document.getElementById("splash");
  if (s) { s.classList.add("gone"); setTimeout(() => s.remove(), 900); }
});
