// Small 3D graph preview for a knowledge-graph card. Auto-rotates, with a
// `seed` so each card has a unique-looking topology, and a `palette` so a
// 'coming soon' card can render as desaturated.

window.CardScene = function CardScene({ seed = 1, palette = 'mixed', spin = 1 }) {
  const mountRef = React.useRef(null);

  React.useEffect(() => {
    const THREE = window.THREE;
    const mount = mountRef.current;
    if (!THREE || !mount) return;

    function rng(s) {
      return function () {
        s = (s * 9301 + 49297) % 233280;
        return s / 233280;
      };
    }
    const rand = rng(seed * 7919 + 1);

    const w = () => mount.clientWidth;
    const h = () => mount.clientHeight;

    const scene = new THREE.Scene();
    scene.fog = new THREE.FogExp2(0x07070a, 0.05);
    const camera = new THREE.PerspectiveCamera(50, w() / h(), 0.1, 100);
    camera.position.set(0, 0, 16);

    const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.setSize(w(), h());
    renderer.setClearColor(0x000000, 0);
    mount.appendChild(renderer.domElement);

    const root = new THREE.Group();
    scene.add(root);

    const palettes = {
      mixed: ['#7c7cff', '#ff6d6d', '#ffd66d', '#ff9a5b', '#ff6dc0', '#5ed694'],
      bio: ['#5ed694', '#7c7cff', '#ffd66d', '#a5f0c9'],
      legal: ['#a78bfa', '#ff6d6d', '#7c7cff', '#ffd66d'],
      muted: ['#3a3a4a', '#42424f', '#4a4a5a'],
    }[palette] || palettes?.mixed || ['#7c7cff'];
    const colors3 = palettes.map((c) => new THREE.Color(c));

    const N = 280;
    const pos = new Float32Array(N * 3);
    const col = new Float32Array(N * 3);
    const sz = new Float32Array(N);
    const cache = [];
    for (let i = 0; i < N; i++) {
      const isCore = rand() < 0.6;
      const r = isCore ? Math.pow(rand(), 0.6) * 3.5 : 3.5 + Math.pow(rand(), 1.3) * 4.5;
      const t = rand() * Math.PI * 2;
      const p = Math.acos(2 * rand() - 1);
      const x = r * Math.sin(p) * Math.cos(t);
      const y = r * Math.sin(p) * Math.sin(t);
      const z = r * Math.cos(p);
      pos[i * 3] = x; pos[i * 3 + 1] = y; pos[i * 3 + 2] = z;
      cache.push([x, y, z]);
      const c = colors3[Math.floor(rand() * colors3.length)];
      col[i * 3] = c.r; col[i * 3 + 1] = c.g; col[i * 3 + 2] = c.b;
      sz[i] = isCore ? 0.07 + rand() * 0.05 : 0.04 + rand() * 0.03;
    }
    const geom = new THREE.BufferGeometry();
    geom.setAttribute('position', new THREE.BufferAttribute(pos, 3));
    geom.setAttribute('color', new THREE.BufferAttribute(col, 3));
    geom.setAttribute('size', new THREE.BufferAttribute(sz, 1));
    const mat = new THREE.ShaderMaterial({
      uniforms: { uPx: { value: renderer.getPixelRatio() } },
      vertexShader: `
        attribute float size;
        varying vec3 vColor;
        uniform float uPx;
        void main(){
          vColor = color;
          vec4 mv = modelViewMatrix * vec4(position,1.0);
          gl_PointSize = size * (260.0 / -mv.z) * uPx;
          gl_Position = projectionMatrix * mv;
        }`,
      fragmentShader: `
        varying vec3 vColor;
        void main(){
          vec2 c = gl_PointCoord - 0.5;
          float d = length(c);
          float a = smoothstep(0.5,0.18,d);
          if(a < 0.02) discard;
          float core = smoothstep(0.35,0.0,d);
          gl_FragColor = vec4(vColor + core*0.55, a);
        }`,
      vertexColors: true, transparent: true, depthWrite: false,
      blending: THREE.AdditiveBlending,
    });
    const points = new THREE.Points(geom, mat);
    root.add(points);

    // edges
    const edgePos = [];
    const edgeCol = [];
    const base = new THREE.Color(palette === 'muted' ? 0x2a2a36 : 0x4a4a66);
    for (let i = 0; i < 200; i++) {
      const a = Math.floor(rand() * N);
      let b = -1, bestD = 999;
      for (let k = 0; k < 6; k++) {
        const bb = Math.floor(rand() * N);
        if (bb === a) continue;
        const dx = cache[a][0] - cache[bb][0];
        const dy = cache[a][1] - cache[bb][1];
        const dz = cache[a][2] - cache[bb][2];
        const d = dx * dx + dy * dy + dz * dz;
        if (d < bestD && d > 0.3) { bestD = d; b = bb; }
      }
      if (b < 0 || bestD > 6) continue;
      edgePos.push(...cache[a], ...cache[b]);
      edgeCol.push(base.r, base.g, base.b, base.r, base.g, base.b);
    }
    const egeom = new THREE.BufferGeometry();
    egeom.setAttribute('position', new THREE.Float32BufferAttribute(edgePos, 3));
    egeom.setAttribute('color', new THREE.Float32BufferAttribute(edgeCol, 3));
    const emat = new THREE.LineBasicMaterial({
      vertexColors: true, transparent: true, opacity: 0.28,
      blending: THREE.AdditiveBlending, depthWrite: false,
    });
    const lines = new THREE.LineSegments(egeom, emat);
    root.add(lines);

    let raf;
    const start = performance.now() - rand() * 8000;
    function tick() {
      const t = (performance.now() - start) / 1000;
      root.rotation.y = t * 0.18 * spin;
      root.rotation.x = Math.sin(t * 0.15) * 0.25;
      renderer.render(scene, camera);
      raf = requestAnimationFrame(tick);
    }
    tick();

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

    return () => {
      cancelAnimationFrame(raf);
      ro.disconnect();
      renderer.dispose(); geom.dispose(); egeom.dispose(); mat.dispose(); emat.dispose();
      if (mount.contains(renderer.domElement)) mount.removeChild(renderer.domElement);
    };
  }, [seed, palette, spin]);

  return <div ref={mountRef} style={{ width: '100%', height: '100%' }} />;
};
