// Three.js hero scene. A 3D knowledge graph with colored nodes + faint edges.
// Rotates slowly, parallaxes on mouse, and the camera dollies + tilts as the
// user scrolls down the landing page.

window.HeroScene = function HeroScene({ density = 1, accent = '#7c7cff' }) {
  const mountRef = React.useRef(null);

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

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

    const scene = new THREE.Scene();
    scene.fog = new THREE.FogExp2(0x07070a, 0.04);

    const camera = new THREE.PerspectiveCamera(55, w() / h(), 0.1, 1000);
    camera.position.set(0, 0, 26);

    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);

    // entity palette mirrors the financial graph legend
    const palette = [
      new THREE.Color('#7c7cff'), // company
      new THREE.Color('#ff6d6d'), // person
      new THREE.Color('#ffd66d'), // event
      new THREE.Color('#ff9a5b'), // product
      new THREE.Color('#ff6dc0'), // regulation
      new THREE.Color('#5ed694'), // patent
    ];
    // Weighted so blue dominates like the screenshot
    const weights = [0.62, 0.12, 0.06, 0.10, 0.06, 0.04];
    function pickColor(r) {
      let acc = 0;
      for (let i = 0; i < weights.length; i++) {
        acc += weights[i];
        if (r < acc) return palette[i];
      }
      return palette[0];
    }

    // ---- Nodes ----
    const NODE_COUNT = Math.round(2200 * density);
    const positions = new Float32Array(NODE_COUNT * 3);
    const colors = new Float32Array(NODE_COUNT * 3);
    const sizes = new Float32Array(NODE_COUNT);
    const nodePos = []; // keep for edges

    for (let i = 0; i < NODE_COUNT; i++) {
      // dense core + sparse halo (like the screenshot)
      const isCore = Math.random() < 0.55;
      const r = isCore ? Math.pow(Math.random(), 0.6) * 6 : 6 + Math.pow(Math.random(), 1.3) * 12;
      const t = Math.random() * Math.PI * 2;
      const p = Math.acos(2 * Math.random() - 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);
      positions[i * 3] = x;
      positions[i * 3 + 1] = y;
      positions[i * 3 + 2] = z;
      nodePos.push([x, y, z]);
      const c = pickColor(Math.random());
      colors[i * 3] = c.r;
      colors[i * 3 + 1] = c.g;
      colors[i * 3 + 2] = c.b;
      sizes[i] = isCore ? 0.04 + Math.random() * 0.04 : 0.025 + Math.random() * 0.03;
    }

    const nodeGeom = new THREE.BufferGeometry();
    nodeGeom.setAttribute('position', new THREE.BufferAttribute(positions, 3));
    nodeGeom.setAttribute('color', new THREE.BufferAttribute(colors, 3));
    nodeGeom.setAttribute('size', new THREE.BufferAttribute(sizes, 1));

    // Round soft point sprite — drawn as a circle in the fragment shader
    const nodeMat = 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 * (320.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.15, d);
          if (a < 0.02) discard;
          // small inner brightness boost
          float core = smoothstep(0.35, 0.0, d);
          vec3 col = vColor + core * 0.45;
          gl_FragColor = vec4(col, a);
        }
      `,
      vertexColors: true,
      transparent: true,
      depthWrite: false,
      blending: THREE.AdditiveBlending,
    });

    const points = new THREE.Points(nodeGeom, nodeMat);
    root.add(points);

    // ---- Edges ----
    // Connect every Nth node to a few nearby neighbours for that faint web
    const EDGE_BUDGET = Math.round(900 * density);
    const edgePositions = [];
    const edgeColors = [];
    const baseEdge = new THREE.Color(0x4a4a66);
    for (let i = 0; i < EDGE_BUDGET; i++) {
      const a = Math.floor(Math.random() * NODE_COUNT);
      // find a moderately close neighbour
      let bestB = -1; let bestD = 999;
      for (let k = 0; k < 8; k++) {
        const b = Math.floor(Math.random() * NODE_COUNT);
        if (b === a) continue;
        const dx = nodePos[a][0] - nodePos[b][0];
        const dy = nodePos[a][1] - nodePos[b][1];
        const dz = nodePos[a][2] - nodePos[b][2];
        const d = dx * dx + dy * dy + dz * dz;
        if (d < bestD && d > 0.4) { bestD = d; bestB = b; }
      }
      if (bestB < 0 || bestD > 18) continue;
      edgePositions.push(...nodePos[a], ...nodePos[bestB]);
      edgeColors.push(baseEdge.r, baseEdge.g, baseEdge.b, baseEdge.r, baseEdge.g, baseEdge.b);
    }
    const edgeGeom = new THREE.BufferGeometry();
    edgeGeom.setAttribute('position', new THREE.Float32BufferAttribute(edgePositions, 3));
    edgeGeom.setAttribute('color', new THREE.Float32BufferAttribute(edgeColors, 3));
    const edgeMat = new THREE.LineBasicMaterial({
      vertexColors: true,
      transparent: true,
      opacity: 0.18,
      blending: THREE.AdditiveBlending,
      depthWrite: false,
    });
    const lines = new THREE.LineSegments(edgeGeom, edgeMat);
    root.add(lines);

    // ---- Pointer parallax + scroll-driven camera ----
    const mouse = { x: 0, y: 0, tx: 0, ty: 0 };
    function onMove(e) {
      const rect = mount.getBoundingClientRect();
      mouse.tx = ((e.clientX - rect.left) / rect.width - 0.5) * 2;
      mouse.ty = ((e.clientY - rect.top) / rect.height - 0.5) * 2;
    }
    window.addEventListener('mousemove', onMove, { passive: true });

    let scrollT = 0;
    function onScroll() {
      const y = window.scrollY || 0;
      // softer, longer ramp so the scene keeps drifting gently through ALL
      // landing sections rather than slamming to the end after one viewport
      scrollT = Math.min(3.0, y / (window.innerHeight * 0.8));
    }
    window.addEventListener('scroll', onScroll, { passive: true });

    // ---- Animate ----
    let raf;
    const start = performance.now();
    function tick() {
      const t = (performance.now() - start) / 1000;

      // gentle pointer easing
      mouse.x += (mouse.tx - mouse.x) * 0.04;
      mouse.y += (mouse.ty - mouse.y) * 0.04;

      // slow base rotation that keeps going regardless of scroll
      root.rotation.y = t * 0.04 + mouse.x * 0.25 + scrollT * 0.3;
      root.rotation.x = Math.sin(t * 0.05) * 0.12 + mouse.y * 0.15 - scrollT * 0.08;

      // camera stays embedded in the cloud — dollies less, drifts more,
      // so the network keeps filling the frame across every section.
      const eased = scrollT < 1 ? scrollT : 1 + Math.sin((scrollT - 1) * 0.9) * 0.35;
      camera.position.z = 24 - eased * 5;
      camera.position.y = Math.sin(scrollT * 0.5) * 3.5;
      camera.position.x = Math.sin(scrollT * 0.35) * 2.5;
      camera.lookAt(0, Math.sin(scrollT * 0.3) * 0.6, 0);
      root.position.y = 0;

      renderer.render(scene, camera);
      raf = requestAnimationFrame(tick);
    }
    tick();

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

    return () => {
      cancelAnimationFrame(raf);
      window.removeEventListener('mousemove', onMove);
      window.removeEventListener('scroll', onScroll);
      ro.disconnect();
      renderer.dispose();
      nodeGeom.dispose();
      edgeGeom.dispose();
      nodeMat.dispose();
      edgeMat.dispose();
      if (mount.contains(renderer.domElement)) mount.removeChild(renderer.domElement);
    };
  }, [density]);

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