/* global React */
// Cue Index — living knowledge graph. A volume-filled sphere of nodes and
// edges that slowly auto-rotates, drifts, and evolves (nodes/edges fade in and
// out, signal pulses travel the links). Drag to orbit. Pure canvas; brand
// palette read from CSS tokens. Mirrors the structure of CueMobius so it drops
// into the same landing page the same way.
//
// Usage (matches mobius.jsx conventions — load after React, before sections):
//   <script type="text/babel" src="landing/cue-graph.jsx"></script>
//   ...then render <CueGraph /> inside a sized, position:relative container.
//
// Props (all optional):
//   theme="dark"|"light"        render mode            (default "dark")
//   density="sparse"|"balanced"|"dense"|"extra dense"  (default "balanced")
//   movement={0..120}           node-drift amount %    (default 50)
//   rotation={0..100}           auto-rotation speed %  (default 40)
//   pulses={true|false}         signal pulses on/off   (default true)
//   interactive={true|false}    drag-to-orbit on/off   (default true)
//   className, style            passthrough to wrapper

const CueGraph = (function () {
  const { useRef, useEffect } = React;

  // ---- palette (resolved from CSS tokens at mount) ----
  function hexToRgb(h) {
    h = (h || "").trim().replace("#", "");
    if (h.length === 3) h = h.split("").map((c) => c + c).join("");
    const n = parseInt(h, 16);
    if (!isFinite(n)) return [40, 89, 225];
    return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
  }
  function readColors(el) {
    const cs = getComputedStyle(el);
    const v = (n, f) => (cs.getPropertyValue(n).trim() || f);
    return {
      blue:     hexToRgb(v("--cue-color-blue", "#2859e1")),
      darkblue: hexToRgb(v("--cue-color-darkblue", "#1744c2")),
      turq:     hexToRgb(v("--cue-color-turquoise", "#00cacc")),
      lime:     hexToRgb(v("--cue-color-green", "#e2f552")),
      core:     [222, 234, 255],
      edgeHot:  [130, 172, 255]
    };
  }
  const rgba = (c, a) => `rgba(${c[0]},${c[1]},${c[2]},${a})`;

  const DENSITY_MAP = { sparse: 150, balanced: 240, dense: 330, "extra dense": 460 };
  const GOLDEN = Math.PI * (3 - Math.sqrt(5));
  const randn = () => (Math.random() + Math.random() + Math.random() - 1.5) / 1.5;
  function randDir() {
    const u = Math.random() * 2 - 1, phi = Math.random() * 6.283;
    const s = Math.sqrt(Math.max(0, 1 - u * u));
    return [s * Math.cos(phi), u, s * Math.sin(phi)];
  }

  function GraphNet(props) {
    const wrapRef = useRef(null);
    const canvasRef = useRef(null);
    // live config — updated each render so the rAF loop reads fresh values
    const cfgRef = useRef({});
    cfgRef.current = {
      theme: props.theme === "light" ? "light" : "dark",
      movement: props.movement == null ? 50 : +props.movement,
      rotation: props.rotation == null ? 40 : +props.rotation,
      pulses: props.pulses !== false,
      interactive: props.interactive !== false,
      density: DENSITY_MAP[props.density] ? props.density : "balanced"
    };
    // expose buildGraph so the density-watch effect can call it
    const apiRef = useRef(null);

    useEffect(() => {
      const wrap = wrapRef.current;
      const canvas = canvasRef.current;
      if (!wrap || !canvas) return;
      const ctx = canvas.getContext("2d");          // alpha:true → transparent
      const COL = readColors(wrap);
      const reduce = window.matchMedia &&
        window.matchMedia("(prefers-reduced-motion: reduce)").matches;

      // ---- glow / dot sprites (token colors) ----
      function makeSprite(color, stops) {
        const S = 96, c = document.createElement("canvas");
        c.width = c.height = S;
        const g = c.getContext("2d");
        const grad = g.createRadialGradient(S / 2, S / 2, 0, S / 2, S / 2, S / 2);
        for (const [pos, a] of stops) grad.addColorStop(pos, rgba(color, a));
        g.fillStyle = grad; g.fillRect(0, 0, S, S);
        return c;
      }
      const SPR = {
        glowBlue:  makeSprite(COL.blue,     [[0, 1], [0.18, 0.8], [0.5, 0.12], [1, 0]]),
        coreWhite: makeSprite(COL.core,     [[0, 1], [0.35, 0.9], [0.6, 0.25], [1, 0]]),
        dotBlue:   makeSprite(COL.darkblue, [[0, 1], [0.45, 0.95], [0.62, 0.55], [0.8, 0.12], [1, 0]]),
        turq:      makeSprite(COL.turq,     [[0, 1], [0.3, 0.85], [0.55, 0.2], [1, 0]]),
        lime:      makeSprite(COL.lime,     [[0, 1], [0.3, 0.85], [0.55, 0.2], [1, 0]])
      };

      // ---- sizing (to container, like CueMobius) ----
      let W = 0, H = 0, cx = 0, cy = 0, R = 1;
      const dpr = Math.min(2, window.devicePixelRatio || 1);
      function resize() {
        const r = wrap.getBoundingClientRect();
        W = Math.max(1, r.width); H = Math.max(1, r.height);
        canvas.width = Math.round(W * dpr);
        canvas.height = Math.round(H * dpr);
        canvas.style.width = W + "px";
        canvas.style.height = H + "px";
        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
        cx = W / 2; cy = H / 2;
        R = Math.min(W, H) * 0.40;
      }
      resize();
      const ro = new ResizeObserver(resize);
      ro.observe(wrap);

      // ---- graph model ----
      let UID = 0, nodes = [], edges = [], edgeSet = new Set(), pulses = [];
      let STRUCT_COUNT = 0, dynCount = 0;
      const DYN_TARGET = 30;

      function makeNode(x, y, z, opts) {
        opts = opts || {};
        const n = {
          uid: UID++, bx: x, by: y, bz: z,
          ax: 0.030 + Math.random() * 0.055, ay: 0.030 + Math.random() * 0.055, az: 0.030 + Math.random() * 0.055,
          fx: 0.14 + Math.random() * 0.32, fy: 0.14 + Math.random() * 0.32, fz: 0.14 + Math.random() * 0.32,
          px: Math.random() * 6.28, py: Math.random() * 6.28, pz: Math.random() * 6.28,
          size: opts.size || (0.55 + Math.random() * 0.6),
          hub: !!opts.hub, dynamic: !!opts.dynamic, accent: opts.accent || null,
          state: opts.dynamic ? "in" : "live", op: opts.dynamic ? 0 : 1,
          sx: 0, sy: 0, scale: 1, depth: 0.5
        };
        nodes.push(n); return n;
      }
      function dist3(a, b) {
        const dx = a.bx - b.bx, dy = a.by - b.by, dz = a.bz - b.bz;
        return Math.sqrt(dx * dx + dy * dy + dz * dz);
      }
      const edgeKey = (a, b) => (a.uid < b.uid ? a.uid + "-" + b.uid : b.uid + "-" + a.uid);
      function addEdge(a, b, opts) {
        opts = opts || {};
        if (a === b) return;
        const k = edgeKey(a, b);
        if (edgeSet.has(k)) return;
        edgeSet.add(k);
        edges.push({ a, b, key: k, hub: !!opts.hub, dynamic: !!opts.dynamic,
          state: opts.dynamic ? "in" : "live", op: opts.dynamic ? 0 : 1,
          w: opts.hub ? 0.6 : (0.7 + Math.random() * 0.5), life: opts.life || 0, age: 0 });
      }
      function removeEdge(e) {
        const i = edges.indexOf(e);
        if (i >= 0) edges.splice(i, 1);
        edgeSet.delete(e.key);
      }

      // ---- live churn ----
      function spawnDynamicNode() {
        const d = randDir();
        const r = 0.25 + 0.7 * Math.cbrt(Math.random());
        const accent = Math.random() < 0.18 ? "turq" : null;
        const n = makeNode(d[0] * r, d[1] * r, d[2] * r, { dynamic: true, accent, size: 0.55 + Math.random() * 0.6 });
        dynCount++;
        const near = [];
        for (const m of nodes) { if (m === n || m.state === "out") continue; near.push([dist3(n, m), m]); }
        near.sort((p, q) => p[0] - q[0]);
        const links = 1 + Math.floor(Math.random() * 3);
        for (let i = 0; i < links && i < near.length; i++) if (near[i][0] < 0.6) addEdge(n, near[i][1], { dynamic: true });
      }
      function despawnDynamicNode() {
        const live = nodes.filter((n) => n.dynamic && n.state === "live");
        if (!live.length) return;
        const n = live[(Math.random() * live.length) | 0];
        n.state = "out";
        for (const e of edges) if ((e.a === n || e.b === n) && e.state !== "out") e.state = "out";
      }
      function spawnTransientEdge() {
        const live = nodes.filter((n) => n.state !== "out");
        if (live.length < 2) return;
        const a = live[(Math.random() * live.length) | 0];
        let best = null, bestD = Infinity;
        for (let t = 0; t < 8; t++) {
          const b = live[(Math.random() * live.length) | 0];
          if (b === a) continue;
          const d = dist3(a, b); if (d < bestD) { bestD = d; best = b; }
        }
        if (best && bestD < 0.5) addEdge(a, best, { dynamic: true, life: 4 + Math.random() * 6 });
      }
      function spawnPulse() {
        const cand = edges.filter((e) => e.op > 0.5);
        if (!cand.length) return;
        pulses.push({ e: cand[(Math.random() * cand.length) | 0], t: 0,
          speed: 0.35 + Math.random() * 0.4, color: Math.random() < 0.22 ? "lime" : "turq" });
      }

      // ---- build the volume-filled sphere ----
      function buildGraph(total) {
        nodes = []; edges = []; edgeSet = new Set(); pulses = []; dynCount = 0; UID = 0;
        const nVol = Math.round(total * 0.66);
        const nShell = total - nVol;
        const structural = [];
        for (let i = 0; i < nVol; i++) {
          const d = randDir();
          const r = 0.14 + 0.86 * Math.cbrt(Math.random());
          structural.push(makeNode(d[0] * r, d[1] * r, d[2] * r, { size: 0.6 + Math.random() * 0.4 }));
        }
        for (let i = 0; i < nShell; i++) {
          const y = 1 - (i / (nShell - 1)) * 2;
          const rr = Math.sqrt(Math.max(0, 1 - y * y));
          const th = i * GOLDEN, rad = 0.94 + Math.random() * 0.08;
          structural.push(makeNode(Math.cos(th) * rr * rad, y * rad, Math.sin(th) * rr * rad, { size: 0.6 + Math.random() * 0.4 }));
        }
        // even k-NN web — consistent 3–4 links per node (no super-hubs, no orphans)
        for (let i = 0; i < structural.length; i++) {
          const a = structural[i];
          const near = [];
          for (let j = 0; j < structural.length; j++) {
            if (i === j) continue;
            near.push([dist3(a, structural[j]), structural[j]]);
          }
          near.sort((p, q) => p[0] - q[0]);
          const kk = 3 + (Math.random() < 0.35 ? 1 : 0);
          for (let k = 0; k < kk && k < near.length; k++) addEdge(a, near[k][1]);
        }
        STRUCT_COUNT = nodes.length;
        for (let i = 0; i < 16; i++) spawnDynamicNode();
        for (const n of nodes) if (n.dynamic) { n.state = "live"; n.op = 1; }
        for (const e of edges) if (e.dynamic) { e.state = "live"; e.op = 1; }
      }
      apiRef.current = { buildGraph };
      buildGraph(DENSITY_MAP[cfgRef.current.density] || 240);

      // ---- orbit (drag to rotate, with inertia) ----
      const TILT = -0.34;
      let userYaw = 0, userPitch = 0, dragging = false, lastX = 0, lastY = 0, velYaw = 0, velPitch = 0;
      function onDown(e) {
        if (!cfgRef.current.interactive) return;
        dragging = true; lastX = e.clientX; lastY = e.clientY; velYaw = 0; velPitch = 0;
        canvas.style.cursor = "grabbing";
        try { canvas.setPointerCapture(e.pointerId); } catch (_) {}
      }
      function onMove(e) {
        if (!dragging) return;
        const k = 0.006, dx = e.clientX - lastX, dy = e.clientY - lastY;
        lastX = e.clientX; lastY = e.clientY;
        userYaw += dx * k; userPitch += dy * k;
        userPitch = Math.max(-1.2, Math.min(1.2, userPitch));
        velYaw = dx * k; velPitch = dy * k;
      }
      function endDrag() { if (dragging) { dragging = false; canvas.style.cursor = "grab"; } }
      canvas.addEventListener("pointerdown", onDown);
      canvas.addEventListener("pointermove", onMove);
      canvas.addEventListener("pointerup", endDrag);
      canvas.addEventListener("pointercancel", endDrag);
      canvas.addEventListener("pointerleave", endDrag);

      // ---- animation loop ----
      let raf, watchdog, last = performance.now(), clock = 0, rotAngle = 0;
      let tNode = 0, tEdge = 0, tPulse = 0;

      function project(n, time, driftScale, sinA, cosA, sinT, cosT) {
        const ox = n.ax * driftScale * Math.sin(time * n.fx + n.px);
        const oy = n.ay * driftScale * Math.sin(time * n.fy + n.py);
        const oz = n.az * driftScale * Math.sin(time * n.fz + n.pz);
        const x = (n.bx + ox) * R, y = (n.by + oy) * R, z = (n.bz + oz) * R;
        const x1 = x * cosA + z * sinA, z1 = -x * sinA + z * cosA;
        const y2 = y * cosT - z1 * sinT, z2 = y * sinT + z1 * cosT;
        const camZ = R * 2.7, scale = camZ / (camZ - z2);
        n.sx = cx + x1 * scale; n.sy = cy + y2 * scale; n.scale = scale;
        n.depth = (z2 + R) / (2 * R);
      }

      function schedule() {
        raf = requestAnimationFrame(frame);
        clearTimeout(watchdog);
        watchdog = setTimeout(() => frame(performance.now()), 400);
      }

      function frame(now) {
        const dt = Math.min((now - last) / 1000, 0.05); last = now; clock += dt;
        const T = cfgRef.current;
        const driftScale = reduce ? 0.25 : (T.movement / 50);

        if (!reduce) {
          const period = 40 + (100 - T.rotation) * 1.6;
          rotAngle += dt * (Math.PI * 2 / period);

          tNode += dt; tEdge += dt; tPulse += dt;
          if (tNode > 0.7) { tNode = 0;
            if (dynCount < DYN_TARGET && Math.random() < 0.85) spawnDynamicNode();
            if (dynCount > DYN_TARGET * 0.55 && Math.random() < 0.6) despawnDynamicNode();
          }
          if (tEdge > 1.1) { tEdge = 0; if (Math.random() < 0.8) spawnTransientEdge(); }
          if (T.pulses && tPulse > 1.0) { tPulse = 0; if (Math.random() < 0.85) spawnPulse(); }

          for (const n of nodes) {
            if (n.state === "in") { n.op += dt / 2.2; if (n.op >= 1) { n.op = 1; n.state = "live"; } }
            else if (n.state === "out") { n.op -= dt / 2.0; }
          }
          for (const e of edges) {
            e.age += dt;
            if (e.state === "in") { e.op += dt / 1.8; if (e.op >= 1) { e.op = 1; e.state = "live"; } }
            else if (e.state === "out") { e.op -= dt / 1.6; }
            else if (e.life > 0 && e.age > e.life) { e.state = "out"; }
          }
          for (let i = nodes.length - 1; i >= STRUCT_COUNT; i--) {
            const n = nodes[i];
            if (n.state === "out" && n.op <= 0) { nodes.splice(i, 1); if (n.dynamic) dynCount = Math.max(0, dynCount - 1); }
          }
          for (let i = edges.length - 1; i >= 0; i--) {
            const e = edges[i]; if (e.state === "out" && e.op <= 0) removeEdge(e);
          }
        }

        if (!dragging) {
          userYaw += velYaw; userPitch += velPitch; velYaw *= 0.92; velPitch *= 0.92;
          userPitch = Math.max(-1.2, Math.min(1.2, userPitch));
        }
        const tilt = TILT + (reduce ? 0 : Math.sin(clock * 0.05) * 0.05) + userPitch;
        const sinA = Math.sin(rotAngle + userYaw), cosA = Math.cos(rotAngle + userYaw);
        const sinT = Math.sin(tilt), cosT = Math.cos(tilt);
        for (const n of nodes) project(n, clock, driftScale, sinA, cosA, sinT, cosT);

        draw(T.theme);
        schedule();
      }

      // ---- drawing ----
      function drawEdges(theme) {
        if (theme === "dark") {
          ctx.globalCompositeOperation = "lighter";
          for (const e of edges) {
            const op = Math.max(0, Math.min(1, e.op)); if (op <= 0) continue;
            const a = e.a, b = e.b, depth = (a.depth + b.depth) / 2;
            const vis = op * Math.min(Math.max(0, a.op), Math.max(0, b.op));
            if (vis <= 0.01) continue;
            const dB = 0.22 + depth * 0.78, baseA = (e.hub ? 0.26 : 0.13) * e.w * vis * dB;
            const lw = ((e.hub ? 0.7 : 0.5) + depth * 0.7) * ((a.scale + b.scale) / 2);
            ctx.strokeStyle = rgba(COL.blue, baseA * 0.5); ctx.lineWidth = lw * 2.2;
            ctx.beginPath(); ctx.moveTo(a.sx, a.sy); ctx.lineTo(b.sx, b.sy); ctx.stroke();
            ctx.strokeStyle = rgba(COL.edgeHot, baseA * 1.1); ctx.lineWidth = Math.max(0.4, lw * 0.8);
            ctx.beginPath(); ctx.moveTo(a.sx, a.sy); ctx.lineTo(b.sx, b.sy); ctx.stroke();
          }
        } else {
          ctx.globalCompositeOperation = "source-over";
          for (const e of edges) {
            const op = Math.max(0, Math.min(1, e.op)); if (op <= 0) continue;
            const a = e.a, b = e.b, depth = (a.depth + b.depth) / 2;
            const vis = op * Math.min(Math.max(0, a.op), Math.max(0, b.op));
            if (vis <= 0.01) continue;
            const alpha = (0.10 + depth * 0.42) * vis * (e.hub ? 1.25 : 1);
            ctx.strokeStyle = rgba(COL.blue, alpha);
            ctx.lineWidth = ((e.hub ? 0.7 : 0.5) + depth * 0.7) * ((a.scale + b.scale) / 2);
            ctx.beginPath(); ctx.moveTo(a.sx, a.sy); ctx.lineTo(b.sx, b.sy); ctx.stroke();
          }
        }
      }
      function drawNodes(theme) {
        const order = nodes.slice().sort((p, q) => p.depth - q.depth);
        if (theme === "dark") {
          ctx.globalCompositeOperation = "lighter";
          for (const n of order) {
            const op = Math.max(0, Math.min(1, n.op)); if (op <= 0.01) continue;
            const dB = 0.30 + n.depth * 0.70, vis = op * dB, sb = n.size * (n.hub ? 1.9 : 1);
            const glow = (5 + sb * 6) * n.scale * (0.8 + n.depth * 0.45);
            let gs = SPR.glowBlue;
            if (n.accent === "turq") gs = SPR.turq; else if (n.accent === "lime") gs = SPR.lime;
            ctx.globalAlpha = vis * 0.8;
            ctx.drawImage(gs, n.sx - glow / 2, n.sy - glow / 2, glow, glow);
            const core = (2.2 + sb * 2.4) * n.scale * (0.85 + n.depth * 0.4);
            ctx.globalAlpha = Math.min(1, vis * 1.15);
            ctx.drawImage(SPR.coreWhite, n.sx - core / 2, n.sy - core / 2, core, core);
          }
        } else {
          ctx.globalCompositeOperation = "source-over";
          for (const n of order) {
            const op = Math.max(0, Math.min(1, n.op)); if (op <= 0.01) continue;
            const dB = 0.30 + n.depth * 0.70, vis = op * dB, sb = n.size * (n.hub ? 1.9 : 1);
            const halo = (4 + sb * 5) * n.scale * (0.8 + n.depth * 0.4);
            let gs = SPR.glowBlue;
            if (n.accent === "turq") gs = SPR.turq; else if (n.accent === "lime") gs = SPR.lime;
            ctx.globalAlpha = vis * 0.5;
            ctx.drawImage(gs, n.sx - halo / 2, n.sy - halo / 2, halo, halo);
            const dot = (3 + sb * 3.4) * n.scale * (0.85 + n.depth * 0.4);
            ctx.globalAlpha = Math.min(1, 0.45 + vis * 0.55);
            const ds = n.accent === "turq" ? SPR.turq : (n.accent === "lime" ? SPR.lime : SPR.dotBlue);
            ctx.drawImage(ds, n.sx - dot / 2, n.sy - dot / 2, dot, dot);
          }
        }
        ctx.globalAlpha = 1;
      }
      function drawPulses(theme) {
        ctx.globalCompositeOperation = theme === "dark" ? "lighter" : "source-over";
        for (let i = pulses.length - 1; i >= 0; i--) {
          const p = pulses[i]; p.t += p.speed * 0.016;
          if (p.t >= 1 || !edgeSet.has(p.e.key) || p.e.op < 0.3) { pulses.splice(i, 1); continue; }
          const a = p.e.a, b = p.e.b;
          const x = a.sx + (b.sx - a.sx) * p.t, y = a.sy + (b.sy - a.sy) * p.t;
          const depth = a.depth + (b.depth - a.depth) * p.t, fade = Math.sin(p.t * Math.PI);
          const scale = (a.scale + b.scale) / 2, spr = p.color === "lime" ? SPR.lime : SPR.turq;
          const gs = (9 + 7) * scale * (0.7 + depth * 0.6);
          ctx.globalAlpha = fade * (0.4 + depth * 0.6) * (theme === "dark" ? 0.9 : 0.85);
          ctx.drawImage(spr, x - gs / 2, y - gs / 2, gs, gs);
          const cs = 3.2 * scale;
          ctx.globalAlpha = fade * (theme === "dark" ? 0.9 : 0.95);
          ctx.drawImage(theme === "dark" ? SPR.coreWhite : spr, x - cs / 2, y - cs / 2, cs, cs);
        }
        ctx.globalAlpha = 1;
      }
      function draw(theme) {
        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
        ctx.clearRect(0, 0, W, H);                 // stays transparent
        drawEdges(theme); drawNodes(theme); drawPulses(theme);
      }

      frame(performance.now()); // draw immediately; rAF continues from here

      return () => {
        cancelAnimationFrame(raf);
        clearTimeout(watchdog);
        ro.disconnect();
        canvas.removeEventListener("pointerdown", onDown);
        canvas.removeEventListener("pointermove", onMove);
        canvas.removeEventListener("pointerup", endDrag);
        canvas.removeEventListener("pointercancel", endDrag);
        canvas.removeEventListener("pointerleave", endDrag);
      };
    }, []); // mount once — config flows through cfgRef each render

    // rebuild only when density changes (node count differs)
    useEffect(() => {
      if (apiRef.current) apiRef.current.buildGraph(DENSITY_MAP[cfgRef.current.density] || 240);
    }, [props.density]);

    const wrapStyle = Object.assign(
      { position: "relative", width: "100%", height: "100%" },
      props.style || {}
    );
    return (
      <div className={"cue-graph" + (props.className ? " " + props.className : "")}
           ref={wrapRef} style={wrapStyle} aria-hidden="true">
        <canvas ref={canvasRef}
                style={{ position: "absolute", inset: 0, display: "block",
                         cursor: (props.interactive !== false) ? "grab" : "default",
                         touchAction: "none", background: "transparent" }} />
      </div>);
  }

  return GraphNet;
})();

window.CueGraph = CueGraph;
