// graph-3d.jsx — Three.js 3D force-directed concept graph.

const { useEffect: _g3_ue, useRef: _g3_ur, useState: _g3_us } = React;

function Graph3D({ concepts, onOpen, onBack, onSwitch, selectedId, graphNodeScale = 1, graphEdgeScale = 1, graphSpring = 190, onToggleSettings, backLabel, onSwitchMode, onOpenSolarSelected }) {
  const mountRef = _g3_ur(null);
  const [hov, setHov] = _g3_us(null);
  const selRef = _g3_ur(selectedId);
  selRef.current = selectedId;
  const nodeScaleRef3 = _g3_ur(graphNodeScale);
  nodeScaleRef3.current = graphNodeScale;
  const springRef3 = _g3_ur(graphSpring);
  springRef3.current = graphSpring;
  const edgeWidthRef3 = _g3_ur(graphEdgeScale);
  edgeWidthRef3.current = graphEdgeScale;

  _g3_ue(() => {
    const el = mountRef.current;
    if (!el || typeof THREE === 'undefined') return;

    // Read a CSS custom property and convert to THREE.Color via canvas
    function cssCol(prop, fallback) {
      const shell = document.querySelector('.app-shell');
      const raw = shell ? getComputedStyle(shell).getPropertyValue(prop).trim() : '';
      const c = document.createElement('canvas');
      c.width = c.height = 1;
      const ctx = c.getContext('2d');
      ctx.fillStyle = raw || fallback;
      ctx.fillRect(0, 0, 1, 1);
      const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data;
      return new THREE.Color(r / 255, g / 255, b / 255);
    }

    const COL = {
      bg: cssCol('--bg-sunk', '#111110'),
      node: cssCol('--bg-elev', '#1e1e1a'),
      border: cssCol('--fg-3', '#888880'),
      nucleus: cssCol('--c-nucleus', '#FF8800'),
      essence: cssCol('--c-essence', '#8B7FD4'),
    };

    // Renderer + scene
    const W = el.clientWidth, H = el.clientHeight;
    const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.setSize(W, H);
    renderer.setClearColor(0x000000, 0);
    el.appendChild(renderer.domElement);

    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(55, W / H, 0.5, 3000);
    camera.position.set(0, 0, 420);

    // Force nodes (3D)
    const n = concepts.length;
    const fnodes = concepts.map((c, i) => ({
      id: c.id, c,
      x: Math.cos((i / n) * Math.PI * 2) * 130,
      y: Math.sin((i / n) * Math.PI * 2) * 130,
      z: (i % 3 - 1) * 60,
      vx: 0, vy: 0, vz: 0, pinned: false,
    }));
    const fnm = {};
    fnodes.forEach(fn => { fnm[fn.id] = fn; });

    // Edges
    const edgeSet = new Set(), edges = [];
    concepts.forEach(c =>
      (c.links || []).forEach(link => {
        const tid  = typeof link === 'string' ? link : link.targetId;
        const type = typeof link === 'string' ? 'unknown' : (link.type || 'unknown');
        const key  = [c.id, tid].sort().join('\0');
        if (!edgeSet.has(key)) { edgeSet.add(key); edges.push([c.id, tid, type]); }
      })
    );

    // Shared circle texture — solid disc, antialiased edge, no blur
    const gc = document.createElement('canvas');
    gc.width = gc.height = 128;
    const gctx = gc.getContext('2d');
    gctx.beginPath();
    gctx.arc(64, 64, 60, 0, Math.PI * 2);
    gctx.fillStyle = '#fff';
    gctx.fill();
    const circleTex = new THREE.CanvasTexture(gc);

    // Node sprites + invisible hit spheres
    const spheres = [];
    fnodes.forEach(fn => {
      const r = 9 + window.fillCount(fn.c) * 1.8;
      fn.r = r;

      // Invisible hit sphere for raycasting
      const hit = new THREE.Mesh(
        new THREE.SphereGeometry(r, 8, 6),
        new THREE.MeshBasicMaterial({ visible: false })
      );
      hit.userData.nodeId = fn.id;
      hit.position.set(fn.x, fn.y, fn.z);
      scene.add(hit);
      fn.mesh = hit;
      spheres.push(hit);

      // Billboard circle sprite (always faces camera → no sharp silhouette)
      const sprite = new THREE.Sprite(new THREE.SpriteMaterial({
        map: circleTex,
        color: COL.node.clone(),
        alphaTest: 0.5,
      }));
      fn.baseSprite = r * 2.2;
      sprite.scale.set(fn.baseSprite, fn.baseSprite, 1);
      sprite.position.copy(hit.position);
      scene.add(sprite);
      fn.sprite = sprite;
    });

    // Type colors for edges
    const TYPE_COLS = {
      essence:     cssCol('--c-essence',     '#FF8800'),
      pattern:     cssCol('--c-pattern',     '#AFA9EC'),
      perspective: cssCol('--c-perspective', '#5DCAA5'),
      unknown:     COL.border.clone(),
    };
    const TYPE_OP  = { essence: 0.80, pattern: 0.70, perspective: 0.55, unknown: 0.65 };
    const TYPE_RAD = { essence: 1.5,  pattern: 1.2,  perspective: 0.9,  unknown: 0.8  };

    // One CylinderGeometry per edge — repositioned each frame
    const _vUp  = new THREE.Vector3(0, 1, 0);
    const _vDir = new THREE.Vector3();
    const _vMid = new THREE.Vector3();
    const cylEdges = [];
    edges.forEach(([aId, bId, type]) => {
      const t = type || 'unknown';
      const r = TYPE_RAD[t] || 0.8;
      const geo = new THREE.CylinderGeometry(r, r, 1, 8, 1);
      const mat = new THREE.MeshBasicMaterial({
        color: TYPE_COLS[t] ? TYPE_COLS[t].clone() : COL.border.clone(),
        transparent: true, opacity: TYPE_OP[t] || 0.65, depthWrite: false,
      });
      const mesh = new THREE.Mesh(geo, mat);
      scene.add(mesh);
      cylEdges.push({ mesh, mat, aId, bId, type: t, baseColor: TYPE_COLS[t] || COL.border });
    });

    function syncEdges(hovId) {
      cylEdges.forEach(ce => {
        const a = fnm[ce.aId], b = fnm[ce.bId];
        if (!a || !b) return;
        _vDir.set(b.x - a.x, b.y - a.y, b.z - a.z);
        const len = _vDir.length() || 1;
        _vMid.set((a.x + b.x) / 2, (a.y + b.y) / 2, (a.z + b.z) / 2);
        ce.mesh.position.copy(_vMid);
        ce.mesh.quaternion.setFromUnitVectors(_vUp, _vDir.divideScalar(len));
        const w = edgeWidthRef3.current;
        ce.mesh.scale.set(w, len, w);
        const isHot = hovId && (ce.aId === hovId || ce.bId === hovId);
        ce.mat.color.copy(isHot ? COL.nucleus : ce.baseColor);
        ce.mat.opacity = isHot ? 1.0 : (TYPE_OP[ce.type] || 0.65);
      });
    }
    syncEdges(null);

    // Force simulation (3D)
    const REPULSE = 14000, SPRING_K = 0.04, GRAVITY = 0.005, DAMPING = 0.82;
    function simStep() {
      for (let i = 0; i < fnodes.length; i++) {
        for (let j = i + 1; j < fnodes.length; j++) {
          const dx = fnodes[j].x - fnodes[i].x;
          const dy = fnodes[j].y - fnodes[i].y;
          const dz = fnodes[j].z - fnodes[i].z;
          const d2 = Math.max(dx * dx + dy * dy + dz * dz, 100);
          const d = Math.sqrt(d2), f = REPULSE / d2;
          fnodes[i].vx -= f * dx / d; fnodes[i].vy -= f * dy / d; fnodes[i].vz -= f * dz / d;
          fnodes[j].vx += f * dx / d; fnodes[j].vy += f * dy / d; fnodes[j].vz += f * dz / d;
        }
      }
      edges.forEach(([aId, bId]) => {
        const a = fnm[aId], b = fnm[bId];
        if (!a || !b) return;
        const dx = b.x - a.x, dy = b.y - a.y, dz = b.z - a.z;
        const d = Math.sqrt(dx * dx + dy * dy + dz * dz) || 1;
        const f = (d - springRef3.current) * SPRING_K;
        const fx = f * dx / d, fy = f * dy / d, fz = f * dz / d;
        if (!a.pinned) { a.vx += fx; a.vy += fy; a.vz += fz; }
        if (!b.pinned) { b.vx -= fx; b.vy -= fy; b.vz -= fz; }
      });
      fnodes.forEach(fn => {
        if (fn.pinned) return;
        fn.vx += -fn.x * GRAVITY; fn.vy += -fn.y * GRAVITY; fn.vz += -fn.z * GRAVITY;
        fn.vx *= DAMPING; fn.vy *= DAMPING; fn.vz *= DAMPING;
        fn.x += fn.vx; fn.y += fn.vy; fn.z += fn.vz;
        fn.mesh.position.set(fn.x, fn.y, fn.z);
      });
    }

    // Camera orbit (spherical coords)
    let theta = 0.3, phi = Math.PI / 2.2, orbitR = 420;
    function updateCam() {
      camera.position.set(
        orbitR * Math.sin(phi) * Math.sin(theta),
        orbitR * Math.cos(phi),
        orbitR * Math.sin(phi) * Math.cos(theta)
      );
      camera.lookAt(0, 0, 0);
    }
    updateCam();

    // Raycaster
    const raycaster = new THREE.Raycaster();
    const mouse2 = new THREE.Vector2();
    let hovId = null, dragging = false, moved = false, lx = 0, ly = 0;

    function getHit(cx, cy) {
      const rect = renderer.domElement.getBoundingClientRect();
      mouse2.x = ((cx - rect.left) / rect.width) * 2 - 1;
      mouse2.y = -((cy - rect.top) / rect.height) * 2 + 1;
      raycaster.setFromCamera(mouse2, camera);
      const hits = raycaster.intersectObjects(spheres);
      return hits.length ? hits[0].object : null;
    }

    function updateSpriteColors() {
      fnodes.forEach(fn => {
        const active = fn.id === hovId || fn.id === selRef.current;
        fn.sprite.material.color.copy(active ? COL.nucleus : COL.node);
      });
    }

    function applyHover(id) {
      if (id === hovId) return;
      hovId = id;
      updateSpriteColors();
      syncEdges(id);
      setHov(id ? concepts.find(c => c.id === id) : null);
    }

    function onDown(e) {
      moved = false;
      if (!getHit(e.clientX, e.clientY)) {
        dragging = true; lx = e.clientX; ly = e.clientY;
      }
      renderer.domElement.setPointerCapture(e.pointerId);
    }
    function onMove(e) {
      moved = true;
      if (dragging) {
        theta -= (e.clientX - lx) * 0.007;
        phi = Math.max(0.1, Math.min(Math.PI - 0.1, phi - (e.clientY - ly) * 0.007));
        lx = e.clientX; ly = e.clientY;
        updateCam();
      } else {
        applyHover(getHit(e.clientX, e.clientY)?.userData.nodeId ?? null);
      }
    }
    function onUp() {
      if (!moved && hovId) onOpen(hovId);
      dragging = false;
    }
    function onWheel(e) {
      e.preventDefault();
      orbitR = Math.max(80, Math.min(900, orbitR + e.deltaY * 0.4));
      updateCam();
    }

    renderer.domElement.addEventListener('pointerdown', onDown);
    renderer.domElement.addEventListener('pointermove', onMove);
    renderer.domElement.addEventListener('pointerup', onUp);
    el.addEventListener('wheel', onWheel, { passive: false });

    // Resize
    const ro = new ResizeObserver(() => {
      const w = el.clientWidth, h = el.clientHeight;
      renderer.setSize(w, h);
      camera.aspect = w / h;
      camera.updateProjectionMatrix();
    });
    ro.observe(el);

    // Animation loop
    let alive = true, t = 0, prevSelId = null, prevNodeScale = nodeScaleRef3.current;
    (function animate() {
      if (!alive) return;
      t += 0.01;
      simStep();
      syncEdges(hovId);
      if (selRef.current !== prevSelId) {
        prevSelId = selRef.current;
        updateSpriteColors();
      }
      const ns = nodeScaleRef3.current;
      const scaleChanged = ns !== prevNodeScale;
      if (scaleChanged) prevNodeScale = ns;
      fnodes.forEach(fn => {
        fn.sprite.position.copy(fn.mesh.position);
        if (scaleChanged) fn.sprite.scale.setScalar(fn.baseSprite * ns);
      });
      renderer.render(scene, camera);
      requestAnimationFrame(animate);
    })();

    return () => {
      alive = false;
      ro.disconnect();
      renderer.domElement.removeEventListener('pointerdown', onDown);
      renderer.domElement.removeEventListener('pointermove', onMove);
      renderer.domElement.removeEventListener('pointerup', onUp);
      el.removeEventListener('wheel', onWheel);
      renderer.dispose();
      if (el.contains(renderer.domElement)) el.removeChild(renderer.domElement);
    };
  }, []);

  return (
    <div style={{ position: "relative", height: "calc(100vh - 60px)", overflow: "hidden", background: "var(--bg-sunk)" }}>
      <div ref={mountRef} style={{ width: "100%", height: "100%" }} />

      {/* Toolbar */}
      <div style={{ position: "absolute", top: 14, left: 14, right: 14, display: "flex", alignItems: "center", gap: 12, pointerEvents: "none" }}>
        <button className="btn ghost tiny" style={{ pointerEvents: "auto" }} onClick={onBack}>
          {window.Icons.ArrowL(13)} {backLabel || "Library"}
        </button>
        <div className="tag-flat">{concepts.length} concept{concepts.length !== 1 ? "s" : ""} · 3D</div>
        <div style={{ flex: 1 }} />
        {selectedId && onSwitchMode && (
          <div style={{ display: "flex", gap: 4, background: "var(--bg-sunk)", padding: 3, borderRadius: 9, border: "1px solid var(--border-soft)", pointerEvents: "auto" }}>
            <button className="btn tiny" style={{ border: 0, background: "transparent", color: "var(--fg-3)" }} onClick={() => onSwitchMode("edit")}>{window.Icons.Edit(13)} Edit</button>
            <button className="btn tiny" style={{ border: 0, background: "transparent", color: "var(--fg-3)" }} onClick={() => onSwitchMode("read")}>{window.Icons.Eye(13)} Read</button>
            <button className="btn tiny" style={{ border: 0, background: "transparent", color: "var(--fg-3)" }} onClick={onOpenSolarSelected}>{window.Icons.Planet(13)} System</button>
            <button className="btn tiny" style={{ border: 0, background: "var(--bg-elev)", boxShadow: "0 1px 2px rgba(0,0,0,.05)", color: "var(--fg)" }}>{window.Icons.Graph(13)} Graph</button>
          </div>
        )}
      </div>

      {/* Bottom-left legend */}
      <div style={{ position: "absolute", bottom: 16, left: 16, pointerEvents: "none" }}>
        <div className="surface" style={{ pointerEvents: "auto", padding: "8px 12px", display: "flex", flexDirection: "column", gap: 4 }}>
          <div style={{ display: "flex", gap: 14, fontSize: 11.5, color: "var(--fg-2)" }}>
            {[
              { label: 'Essence',     col: 'var(--c-essence)',     w: 2.5, op: 0.8,  dash: null },
              { label: 'Pattern',     col: 'var(--c-pattern)',     w: 1.8, op: 0.7,  dash: null },
              { label: 'Perspective', col: 'var(--c-perspective)', w: 1.2, op: 0.6,  dash: '5 4' },
            ].map(({ label, col, w, op, dash }) => (
              <span key={label} style={{ display: "inline-flex", alignItems: "center", gap: 5 }}>
                <svg width={20} height={8} style={{ flexShrink: 0 }}>
                  <line x1={0} y1={4} x2={20} y2={4}
                    stroke={col} strokeWidth={w} strokeOpacity={op}
                    strokeDasharray={dash || undefined}
                  />
                </svg>
                {label}
              </span>
            ))}
          </div>
        </div>
      </div>

      {/* Bottom bar — settings + 2D / 3D toggle */}
      <div style={{ position: "absolute", bottom: 16, right: 16, pointerEvents: "none", display: "flex", gap: 8, alignItems: "center" }}>
        <button className="btn ghost tiny" style={{ pointerEvents: "auto", fontSize: 16 }} onClick={onToggleSettings} title="Graph settings">⚙</button>
        <div className="surface" style={{ pointerEvents: "auto", padding: 3, display: "flex", gap: 2, background: "var(--bg-sunk)" }}>
          <button className="btn tiny" style={{ border: 0, background: "transparent", color: "var(--fg-3)", padding: "4px 12px", fontFamily: "var(--font-mono)", fontSize: 11, letterSpacing: ".08em" }} onClick={onSwitch}>2D</button>
          <button className="btn tiny" style={{ border: 0, background: "var(--bg-elev)", boxShadow: "0 1px 2px rgba(0,0,0,.05)", color: "var(--fg)", padding: "4px 12px", fontFamily: "var(--font-mono)", fontSize: 11, letterSpacing: ".08em" }}>3D</button>
        </div>
      </div>

      {/* Hover card */}
      {hov && (
        <div style={{
          position: "absolute", bottom: 24, left: "50%", transform: "translateX(-50%)",
          background: "var(--bg-elev)", border: "1px solid var(--border)",
          borderRadius: 10, padding: "12px 16px", pointerEvents: "none",
          boxShadow: "var(--shadow-lift)", minWidth: 220, maxWidth: 380,
        }}>
          <div className="tag-flat" style={{ marginBottom: 4 }}>{hov.category}</div>
          <div className="concept-title" style={{ fontSize: 22 }}>{hov.title}</div>
          {hov.essences?.[0]?.body && (
            <div style={{ fontFamily: "var(--font-serif)", fontStyle: "italic", color: "var(--fg-2)", fontSize: 13, marginTop: 6, lineHeight: 1.5 }}>
              &ldquo;{hov.essences[0].body}&rdquo;
            </div>
          )}
          <div style={{ marginTop: 8, fontSize: 11, color: "var(--fg-3)", fontFamily: "var(--font-mono)", letterSpacing: ".06em", textTransform: "uppercase" }}>
            {window.fillCount(hov)}/5 complete
          </div>
        </div>
      )}
    </div>
  );
}

Object.assign(window, { Graph3D });
