/* Shared hooks + the wireframe "structure" object */

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

// reveal-on-scroll — manual rect check (robust in offscreen/capture iframes
// where IntersectionObserver may never fire)
function useReveal(opts = {}) {
  const ref = useRef(null);
  const [shown, setShown] = useState(false);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    let done = false;
    const reveal = () => { if (!done) { done = true; setShown(true); cleanup(); } };
    const check = () => {
      if (done) return;
      const r = el.getBoundingClientRect();
      const vh = window.innerHeight || document.documentElement.clientHeight;
      if (r.top < vh * 0.88 && r.bottom > 0) reveal();
    };
    const cleanup = () => {
      window.removeEventListener("scroll", check);
      window.removeEventListener("resize", check);
      clearTimeout(fallback);
    };
    window.addEventListener("scroll", check, { passive: true });
    window.addEventListener("resize", check);
    // initial checks (covers above-the-fold + first paint timing)
    check();
    requestAnimationFrame(check);
    const t2 = setTimeout(check, 300);
    // safety: never leave content hidden even if scroll events don't fire
    const fallback = setTimeout(reveal, 2600);
    return () => { cleanup(); clearTimeout(t2); };
  }, []);
  return [ref, shown];
}

const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

// types a stream of lines with callback when each appears
function useStream() {
  const [lines, setLines] = useState([]);
  const [running, setRunning] = useState(false);
  const timers = useRef([]);
  const clear = () => { timers.current.forEach(clearTimeout); timers.current = []; setLines([]); setRunning(false); };
  const run = useCallback((script, onDone) => {
    timers.current.forEach(clearTimeout); timers.current = [];
    setLines([]); setRunning(true);
    let t = 0;
    script.forEach((ln, i) => {
      t += ln.delay ?? (reduceMotion ? 60 : 420);
      timers.current.push(setTimeout(() => {
        setLines(prev => [...prev, ln]);
      }, t));
    });
    timers.current.push(setTimeout(() => { setRunning(false); onDone && onDone(); }, t + 260));
  }, []);
  // live mode: append lines as they stream in from the agent SSE (no fixed timeline)
  const begin = useCallback(() => { timers.current.forEach(clearTimeout); timers.current = []; setLines([]); setRunning(true); }, []);
  const push = useCallback((ln) => setLines(prev => [...prev, ln]), []);
  const end = useCallback(() => setRunning(false), []);
  return { lines, running, run, clear, begin, push, end };
}

// compact any value to a short inline string for the live feed
function shortVal(o) {
  try { const t = typeof o === "string" ? o : JSON.stringify(o); return t.length > 96 ? t.slice(0, 96) + "…" : t; }
  catch (e) { return String(o); }
}

// Subscribe to the agent's live reasoning SSE (agent/src/stream.ts -> /events). Calls
// onEvent(parsedEvent) per message; onError once if the agent can't be reached (the caller
// then falls back to the recorded demo). Returns a close() fn.
function subscribeAgent(url, { onEvent, onError }) {
  let es, opened = false;
  try { es = new EventSource(url); }
  catch (e) { onError && onError(e); return () => {}; }
  const failTimer = setTimeout(() => {
    if (!opened) { try { es.close(); } catch (e) {} onError && onError(new Error("agent unreachable")); }
  }, 2500);
  es.onopen = () => { opened = true; clearTimeout(failTimer); };
  es.onerror = () => { if (!opened) { clearTimeout(failTimer); try { es.close(); } catch (e) {} onError && onError(new Error("agent unreachable")); } };
  es.onmessage = (e) => { try { onEvent(JSON.parse(e.data)); } catch (x) {} };
  return () => { clearTimeout(failTimer); try { es.close(); } catch (e) {} };
}

// A rotating CSS wireframe object: sphere | cube | torus, tinted by accent
function Wireframe({ shape = "sphere", color = "#E8893B", size = 200, spin = true, assembling = false }) {
  const c = color;
  const wire = "var(--wire)";
  const dur = spin && !reduceMotion ? "26s" : "0s";

  const ring = (rx, idx, n) => ({
    position: "absolute", inset: 0, borderRadius: "50%",
    border: `1px solid ${c}`,
    transform: `rotateX(78deg) rotateZ(${(idx * 180) / n}deg)`,
    opacity: 0.55,
    boxShadow: `0 0 18px ${c}22`,
  });

  const stage = {
    width: size, height: size, position: "relative",
    transformStyle: "preserve-3d",
    transform: "rotateX(-16deg)",
  };
  const spinner = {
    width: "100%", height: "100%", position: "relative",
    transformStyle: "preserve-3d",
    animation: dur === "0s" ? "none" : `spinY ${dur} linear infinite`,
    transition: "opacity 0.6s ease",
    opacity: assembling ? 0.25 : 1,
    filter: assembling ? "blur(1px)" : "none",
  };

  let content;
  if (shape === "sphere") {
    content = (
      <div style={spinner}>
        {[...Array(7)].map((_, i) => (
          <div key={i} style={{
            position: "absolute", inset: 0, borderRadius: "50%",
            border: `1px solid ${i % 2 ? wire : c}`,
            transform: `rotateY(${(i * 180) / 7}deg)`,
            opacity: 0.5,
          }} />
        ))}
        <div style={{ position: "absolute", inset: 0, borderRadius: "50%", border: `1px solid ${c}`, boxShadow: `0 0 40px ${c}33, inset 0 0 30px ${c}22` }} />
      </div>
    );
  } else if (shape === "torus") {
    content = (
      <div style={spinner}>
        {[...Array(5)].map((_, i) => (
          <div key={i} style={{
            position: "absolute", inset: `${i * 7}%`, borderRadius: "50%",
            border: `1px solid ${i % 2 ? wire : c}`, opacity: 0.5,
            transform: "rotateX(72deg)",
          }} />
        ))}
        <div style={{ position: "absolute", inset: "32%", borderRadius: "50%", border: `1px solid ${c}`, transform: "rotateX(72deg)", boxShadow: `0 0 30px ${c}33` }} />
      </div>
    );
  } else { // cube
    const faces = [
      "translateZ(50px)", "rotateY(180deg) translateZ(50px)",
      "rotateY(90deg) translateZ(50px)", "rotateY(-90deg) translateZ(50px)",
      "rotateX(90deg) translateZ(50px)", "rotateX(-90deg) translateZ(50px)",
    ];
    content = (
      <div style={spinner}>
        <div style={{ position: "absolute", left: "50%", top: "50%", width: 100, height: 100, marginLeft: -50, marginTop: -50, transformStyle: "preserve-3d", animation: dur === "0s" ? "none" : `tumble ${dur} linear infinite` }}>
          {faces.map((f, i) => (
            <div key={i} style={{ position: "absolute", width: 100, height: 100, border: `1px solid ${i % 2 ? c : wire}`, transform: f, background: `${c}07`, boxShadow: `inset 0 0 24px ${c}18` }} />
          ))}
        </div>
      </div>
    );
  }

  return (
    <div style={{ display: "grid", placeItems: "center", perspective: "900px" }}>
      <div style={stage}>{content}</div>
    </div>
  );
}

// inject keyframes for 3d spins (once)
(function injectKF() {
  if (document.getElementById("__grain_kf")) return;
  const s = document.createElement("style");
  s.id = "__grain_kf";
  s.textContent = `
    @keyframes spinY { from { transform: rotateY(0); } to { transform: rotateY(360deg); } }
    @keyframes tumble { from { transform: rotateX(0) rotateY(0); } to { transform: rotateX(360deg) rotateY(360deg); } }
  `;
  document.head.appendChild(s);
})();

Object.assign(window, { useReveal, useStream, Wireframe, reduceMotion });
