// Recreates the dense particle-cloud visual from the Financial Knowledge Graph
// screenshot. Static visual — no controls wired up — but it pans and rotates
// slowly so it feels alive and you can grab it with the mouse.

window.ViewerScene = function ViewerScene() {
  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();
    const camera = new THREE.PerspectiveCamera(50, w() / h(), 0.1, 1000);
    camera.position.set(0, 0, 22);

    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 palette = [
      new THREE.Color('#7c7cff'),
      new THREE.Color('#ff6d6d'),
      new THREE.Color('#ffd66d'),
      new THREE.Color('#ff9a5b'),
      new THREE.Color('#ff6dc0'),
      new THREE.Color('#5ed694'),
    ];
    const weights = [0.66, 0.13, 0.04, 0.10, 0.04, 0.03];
    function pickColor(r) {
      let a = 0;
      for (let i = 0; i < weights.length; i++) {
        a += weights[i];
        if (r < a) return palette[i];
      }
      return palette[0];
    }

    // Big point cloud: a dense core + halo, matching the screenshot's "blown
    // dandelion" look.
    const N = 13852; // matches the entity count from the screenshot, cute easter egg
    const pos = new Float32Array(N * 3);
    const col = new Float32Array(N * 3);
    const sz  = new Float32Array(N);
    const cache = new Float32Array(N * 3);

    for (let i = 0; i < N; i++) {
      const layer = Math.random();
      let r;
      if (layer < 0.50)      r = Math.pow(Math.random(), 1.6) * 3.5;       // core
      else if (layer < 0.92) r = 3.5 + Math.pow(Math.random(), 0.8) * 7.5; // mid
      else                   r = 11   + Math.pow(Math.random(), 1.4) * 8;  // halo
      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);
      pos[i*3] = x; pos[i*3+1] = y; pos[i*3+2] = z;
      cache[i*3] = x; cache[i*3+1] = y; cache[i*3+2] = z;
      const c = pickColor(Math.random());
      col[i*3] = c.r; col[i*3+1] = c.g; col[i*3+2] = c.b;
      sz[i] = layer < 0.5 ? 0.025 + Math.random() * 0.025 : 0.018 + Math.random() * 0.02;
    }
    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 * (400.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.1,d);
          if(a < 0.02) discard;
          float core = smoothstep(0.32,0.0,d);
          gl_FragColor = vec4(vColor + core*0.5, a);
        }`,
      vertexColors: true, transparent: true, depthWrite: false,
      blending: THREE.AdditiveBlending,
    });
    const points = new THREE.Points(geom, mat);
    root.add(points);

    // Edges — very faint, just enough to suggest connectivity
    const edgePos = [];
    const edgeCol = [];
    const base = new THREE.Color(0x42425a);
    const EDGES = 1800;
    for (let i = 0; i < EDGES; i++) {
      const a = Math.floor(Math.random() * N);
      let b = -1, bestD = 9999;
      for (let k = 0; k < 6; k++) {
        const bb = Math.floor(Math.random() * N);
        if (bb === a) continue;
        const dx = cache[a*3]-cache[bb*3];
        const dy = cache[a*3+1]-cache[bb*3+1];
        const dz = cache[a*3+2]-cache[bb*3+2];
        const d = dx*dx+dy*dy+dz*dz;
        if (d < bestD && d > 0.2) { bestD = d; b = bb; }
      }
      if (b < 0 || bestD > 5) continue;
      edgePos.push(cache[a*3],cache[a*3+1],cache[a*3+2],cache[b*3],cache[b*3+1],cache[b*3+2]);
      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.14,
      blending: THREE.AdditiveBlending, depthWrite: false,
    });
    const lines = new THREE.LineSegments(egeom, emat);
    root.add(lines);

    // mouse drag rotate (subtle)
    let dragging = false, lastX = 0, lastY = 0;
    let velY = 0, velX = 0;
    function down(e){ dragging = true; lastX = e.clientX; lastY = e.clientY; }
    function up(){ dragging = false; }
    function move(e){
      if(!dragging) return;
      velY += (e.clientX - lastX) * 0.005;
      velX += (e.clientY - lastY) * 0.005;
      lastX = e.clientX; lastY = e.clientY;
    }
    mount.addEventListener('mousedown', down);
    window.addEventListener('mouseup', up);
    window.addEventListener('mousemove', move);

    let raf;
    const start = performance.now();
    function tick() {
      const t = (performance.now() - start) / 1000;
      // ambient drift + drag inertia
      velY *= 0.94; velX *= 0.94;
      root.rotation.y += 0.0005 + velY;
      root.rotation.x += velX;
      root.rotation.x = Math.max(-1, Math.min(1, root.rotation.x));
      root.rotation.z = Math.sin(t * 0.05) * 0.03;
      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();
      mount.removeEventListener('mousedown', down);
      window.removeEventListener('mouseup', up);
      window.removeEventListener('mousemove', move);
      renderer.dispose(); geom.dispose(); egeom.dispose(); mat.dispose(); emat.dispose();
      if (mount.contains(renderer.domElement)) mount.removeChild(renderer.domElement);
    };
  }, []);

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