// 3D force-directed graph via 3d-force-graph.
// Sprite labels on every node + clickable sidebar to focus by category.

window.Graph = (function() {
  const COLORS = {
    self: "#111111",
    project: "#d73a49",
    skill: "#1f6feb",
    org: "#bf8700",
    place: "#2ea043",
    award: "#8250df",
    interest: "#6e7681",
  };
  const DARK_COLORS = { ...COLORS, self: "#f0ede5" };

  const KIND_LABEL = {
    self: "me", project: "projects", skill: "skills", org: "experience",
    place: "places", award: "awards", interest: "interests",
  };
  const SIDEBAR_ORDER = ["project", "org", "skill", "award", "place", "interest"];

  function View({ width = 900, height = 620, onSelect, filter = null, theme = "light" }) {
    const mountRef = React.useRef(null);
    const canvasRef = React.useRef(null);
    const fgRef = React.useRef(null);
    const hoverRef = React.useRef(null);
    const dataRef = React.useRef(null);
    const [hoverKind, setHoverKind] = React.useState(null); // sidebar section hover
    const [graphData, setGraphData] = React.useState(null);

    React.useEffect(() => {
      if (!canvasRef.current) return;
      if (typeof ForceGraph3D === "undefined") {
        canvasRef.current.innerHTML = '<div style="padding:20px;color:var(--mute);font-family:monospace;font-size:12px">3d-force-graph failed to load.</div>';
        return;
      }
      const palette = theme === "dark" ? DARK_COLORS : COLORS;
      const SpriteTextCtor = window.SpriteText;

      const raw = window.buildGraph();
      const links = raw.edges.map(e => ({ source: e.s, target: e.t, kind: e.kind }));
      const nodes = raw.nodes.map(n => ({ ...n }));
      const gdata = { nodes, links };
      dataRef.current = { nodes, links, byId: Object.fromEntries(nodes.map(n => [n.id, n])) };
      setGraphData({ nodes });

      const baseLabelColor = theme === "dark" ? "#f0ede5" : "#1a1612";

      const fg = ForceGraph3D()(canvasRef.current)
        .width(canvasRef.current.clientWidth || width)
        .height(height)
        .backgroundColor(theme === "dark" ? "#0a0a0c" : "#fffaf0")
        .graphData(gdata)
        .nodeRelSize(4)
        .nodeVal(n => (n.size || 10) * 0.6)
        .nodeColor(n => palette[n.kind] || "#888")
        .nodeOpacity(0.95)
        .nodeResolution(16)
        .nodeThreeObjectExtend(true)
        .nodeThreeObject(n => {
          if (!SpriteTextCtor) return null;
          const isSelf = n.kind === "self";
          const sprite = new SpriteTextCtor(n.label);
          sprite.color = isSelf ? palette.self : baseLabelColor;
          sprite.backgroundColor = theme === "dark" ? "rgba(10,10,12,0.75)" : "rgba(255,250,240,0.85)";
          sprite.padding = 2;
          sprite.borderRadius = 3;
          sprite.fontFace = '"Geist", system-ui, sans-serif';
          sprite.fontWeight = isSelf ? "600" : "500";
          sprite.textHeight = isSelf ? 6 : (n.kind === "project" || n.kind === "org") ? 4.2 : 3.4;
          // offset above sphere
          const radius = Math.cbrt((n.size || 10) * 0.6) * 4 + 2;
          sprite.position.set(0, radius + sprite.textHeight * 0.8, 0);
          return sprite;
        })
        .linkColor(l => {
          const h = hoverRef.current;
          const base = theme === "dark" ? "rgba(240,237,229,0.18)" : "rgba(0,0,0,0.18)";
          const hi = theme === "dark" ? "rgba(255,255,255,0.85)" : "rgba(0,0,0,0.75)";
          if (!h) return base;
          const sid = typeof l.source === "object" ? l.source.id : l.source;
          const tid = typeof l.target === "object" ? l.target.id : l.target;
          return (sid === h || tid === h) ? hi : (theme === "dark" ? "rgba(240,237,229,0.05)" : "rgba(0,0,0,0.05)");
        })
        .linkOpacity(0.6)
        .linkWidth(l => {
          const h = hoverRef.current;
          const sid = typeof l.source === "object" ? l.source.id : l.source;
          const tid = typeof l.target === "object" ? l.target.id : l.target;
          return (h && (sid === h || tid === h)) ? 1.2 : 0.35;
        })
        .linkDirectionalParticles(l => (l.kind === "built" || l.kind === "kin") ? 2 : 0)
        .linkDirectionalParticleWidth(1.2)
        .linkDirectionalParticleSpeed(0.004)
        .onNodeHover(n => {
          hoverRef.current = n ? n.id : null;
          if (canvasRef.current) canvasRef.current.style.cursor = n ? "pointer" : "grab";
          fg.linkColor(fg.linkColor());
        })
        .onNodeClick(n => {
          focusNode(n);
          if (onSelect && n.kind !== "self") onSelect(n);
        });

      function focusNode(n) {
        const distance = 120;
        const d = Math.hypot(n.x || 1, n.y || 1, n.z || 1) || 1;
        fg.cameraPosition(
          { x: n.x * (1 + distance / d), y: n.y * (1 + distance / d), z: n.z * (1 + distance / d) },
          n, 1400
        );
      }

      fg.d3Force("charge").strength(-120);
      fg.d3Force("link").distance(l => {
        if (l.kind === "kin" || l.kind === "sibling") return 70;
        if (l.kind === "uses" || l.kind === "embodies") return 45;
        return 60;
      });

      let angle = 0;
      const radius = 360;
      fg.cameraPosition({ x: 0, y: 0, z: radius });
      const spin = () => {
        if (!fgRef.current) return;
        angle += 0.0012;
        fg.cameraPosition({ x: radius * Math.sin(angle), y: 40 * Math.sin(angle * 0.7), z: radius * Math.cos(angle) });
        raf = requestAnimationFrame(spin);
      };
      let raf;
      const spinTimer = setTimeout(() => { raf = requestAnimationFrame(spin); }, 3500);
      const stopSpin = () => { cancelAnimationFrame(raf); raf = null; clearTimeout(spinTimer); };
      canvasRef.current.addEventListener("pointerdown", stopSpin, { once: true });
      canvasRef.current.addEventListener("wheel", stopSpin, { once: true });

      fg._focusById = (id) => {
        const n = dataRef.current.byId[id];
        if (n) { stopSpin(); focusNode(n); }
      };
      fgRef.current = fg;

      return () => {
        clearTimeout(spinTimer);
        if (raf) cancelAnimationFrame(raf);
        try { fg._destructor && fg._destructor(); } catch {}
        if (canvasRef.current) canvasRef.current.innerHTML = "";
        fgRef.current = null;
      };
    }, [theme]);

    // Resize
    React.useEffect(() => {
      if (fgRef.current && canvasRef.current) {
        fgRef.current.width(canvasRef.current.clientWidth).height(height);
      }
    }, [width, height]);

    // Filter
    React.useEffect(() => {
      const fg = fgRef.current; if (!fg) return;
      const palette = theme === "dark" ? DARK_COLORS : COLORS;
      fg.nodeColor(n => {
        if (!filter || n.kind === filter || n.kind === "self") return palette[n.kind] || "#888";
        return theme === "dark" ? "#2a2824" : "#d8d0bf";
      });
    }, [filter, theme]);

    // Sidebar: group nodes by kind
    const groups = React.useMemo(() => {
      if (!graphData) return {};
      const g = {};
      graphData.nodes.forEach(n => {
        if (n.kind === "self") return;
        (g[n.kind] = g[n.kind] || []).push(n);
      });
      return g;
    }, [graphData]);

    const sidebarBg = theme === "dark" ? "rgba(19,18,15,0.88)" : "rgba(255,250,240,0.92)";
    const ruleCol = theme === "dark" ? "rgba(240,237,229,0.1)" : "rgba(0,0,0,0.1)";

    return (
      <div style={{ position: "relative", width, height, borderRadius: 6, overflow: "hidden", background: theme === "dark" ? "#0a0a0c" : "#fffaf0" }}>
        {/* Sidebar */}
        <div style={{
          position: "absolute", left: 0, top: 0, bottom: 0, width: 220,
          background: sidebarBg, borderRight: `1px solid ${ruleCol}`,
          padding: "14px 12px", overflowY: "auto", zIndex: 3, backdropFilter: "blur(6px)",
          fontSize: 12,
        }}>
          <div className="mono" style={{ fontSize: 9.5, letterSpacing: "0.14em", textTransform: "uppercase", color: "var(--mute)", marginBottom: 10 }}>
            Index · {graphData ? graphData.nodes.length - 1 : 0} nodes
          </div>
          {SIDEBAR_ORDER.map(k => {
            const list = groups[k] || [];
            if (!list.length) return null;
            const col = COLORS[k];
            const isHot = hoverKind === k;
            return (
              <div key={k} style={{ marginBottom: 12 }}>
                <div onMouseEnter={() => setHoverKind(k)} onMouseLeave={() => setHoverKind(null)}
                  className="mono" style={{
                    display: "flex", alignItems: "center", gap: 6,
                    fontSize: 10, letterSpacing: "0.12em", textTransform: "uppercase",
                    color: "var(--mute)", marginBottom: 6, cursor: "default",
                  }}>
                  <span style={{ width: 8, height: 8, borderRadius: 99, background: col, display: "inline-block" }} />
                  {KIND_LABEL[k]} · {list.length}
                </div>
                <div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
                  {list.map(n => (
                    <button key={n.id}
                      onMouseEnter={() => {
                        hoverRef.current = n.id;
                        const fg = fgRef.current; if (fg) fg.linkColor(fg.linkColor());
                      }}
                      onMouseLeave={() => {
                        hoverRef.current = null;
                        const fg = fgRef.current; if (fg) fg.linkColor(fg.linkColor());
                      }}
                      onClick={() => {
                        const fg = fgRef.current;
                        if (fg && fg._focusById) fg._focusById(n.id);
                        if (onSelect && n.kind !== "self") onSelect(n);
                      }}
                      style={{
                        textAlign: "left", background: "transparent", border: 0,
                        padding: "4px 6px", borderRadius: 4, cursor: "pointer",
                        color: "var(--ink)", fontSize: 12.5, lineHeight: 1.25,
                        fontFamily: "inherit",
                        transition: "background 0.1s",
                      }}
                      onMouseDown={e => e.currentTarget.style.background = ruleCol}
                      onMouseUp={e => e.currentTarget.style.background = "transparent"}>
                      {n.label}
                    </button>
                  ))}
                </div>
              </div>
            );
          })}
        </div>

        {/* 3D canvas */}
        <div ref={canvasRef} style={{ position: "absolute", left: 220, right: 0, top: 0, bottom: 0, cursor: "grab" }} />

        {/* Hint */}
        <div className="mono" style={{
          position: "absolute", left: 232, bottom: 10, zIndex: 2,
          fontSize: 10, color: "var(--mute)", letterSpacing: "0.08em",
          pointerEvents: "none", textTransform: "uppercase",
        }}>
          drag to orbit · scroll to zoom · click a node
        </div>
      </div>
    );
  }

  return { View, COLORS, KIND_LABEL };
})();
