// Studio WIBI — PLY point cloud viewer
// Requires window.THREE and window.PLYLoader (set by <script type="module"> in index.html).

const HIDDEN_CLASSES       = new Set([2, 6, 9]);   // Ground, Buildings, Water — excluded
const UNCLASSIFIED_CLASSES = new Set([0, 1]);       // Unclassified — lighter grey
const COLOR_UNCLASSIFIED   = '#b0b0b0';
const COLOR_OTHER          = '#1e1e1e';

function PointCloudViewer({ src }) {
  const mountRef    = React.useRef(null);
  const cameraRef   = React.useRef(null);
  const sceneRef    = React.useRef(null);
  const groupRef    = React.useRef(null);
  const matsRef     = React.useRef([]);
  const frustumHalfWRef        = React.useRef(1);
  const initialFrustumHalfWRef = React.useRef(1);
  const basePointSizeRef       = React.useRef(1);
  const panOffsetXRef          = React.useRef(0);
  const panOffsetYRef          = React.useRef(0);
  const boundsRadiusRef        = React.useRef(1);
  const isDraggingRef          = React.useRef(false);
  const lastMouseRef           = React.useRef({ x: 0, y: 0 });
  const isRotatingRef          = React.useRef(true);

  const [isRotating, setIsRotating] = React.useState(true);
  const [status, setStatus]         = React.useState('loading');

  const applyFrustum = React.useCallback(() => {
    const camera = cameraRef.current;
    const mount  = mountRef.current;
    if (!camera || !mount) return;
    const halfW  = frustumHalfWRef.current;
    const aspect = mount.clientWidth / mount.clientHeight;
    const halfH  = halfW / aspect;
    const vShift = halfH * 0.15;
    camera.left   = -halfW + panOffsetXRef.current;
    camera.right  =  halfW + panOffsetXRef.current;
    camera.top    =  halfH + vShift + panOffsetYRef.current;
    camera.bottom = -halfH + vShift + panOffsetYRef.current;
    camera.updateProjectionMatrix();
    if (initialFrustumHalfWRef.current > 0) {
      const scale = initialFrustumHalfWRef.current / halfW;
      matsRef.current.forEach(m => { m.size = basePointSizeRef.current * scale; });
    }
  }, []);

  const zoom = React.useCallback((factor) => {
    const next = frustumHalfWRef.current * factor;
    const min  = initialFrustumHalfWRef.current / 10;
    frustumHalfWRef.current = Math.min(Math.max(next, min), initialFrustumHalfWRef.current);
    applyFrustum();
  }, [applyFrustum]);

  React.useEffect(() => {
    const THREE     = window.THREE;
    const PLYLoader = window.PLYLoader;
    if (!THREE || !PLYLoader) { setStatus('error'); return; }

    const mount = mountRef.current;

    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0xf1efe9);
    sceneRef.current = scene;

    const aspect = mount.clientWidth / mount.clientHeight;
    const camera = new THREE.OrthographicCamera(-1, 1, 1 / aspect, -1 / aspect, -100000, 100000);
    camera.position.set(0, 0, 1);
    cameraRef.current = camera;

    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.setSize(mount.clientWidth, mount.clientHeight);
    mount.appendChild(renderer.domElement);
    scene.add(new THREE.AmbientLight(0xffffff, 0.5));

    const loader = new PLYLoader();
    loader.setCustomPropertyNameMapping({
      classification: ['scalar_Classification', 'scalar_classification', 'classification'],
    });

    loader.load(src, (geometry) => {
      geometry.computeBoundingBox();
      const box    = geometry.boundingBox;
      const center = new THREE.Vector3();
      box.getCenter(center);
      geometry.translate(-center.x, -center.y, -center.z);
      geometry.computeBoundingSphere();
      const r = geometry.boundingSphere.radius;
      boundsRadiusRef.current = r;

      const size     = new THREE.Vector3();
      box.getSize(size);
      const maxDim   = Math.max(size.x, size.y, size.z);
      const baseSize = 0.001 * maxDim;
      const fitRadius = (mount.clientWidth / mount.clientHeight) < 1 ? r * 0.3 : r;

      basePointSizeRef.current       = baseSize;
      frustumHalfWRef.current        = fitRadius;
      initialFrustumHalfWRef.current = fitRadius;

      const camDist = r * 2.5;
      camera.position.copy(
        new THREE.Vector3(0.3, -1.5, 0.7).normalize().multiplyScalar(camDist)
      );
      camera.up.set(0, 0, 1);
      camera.lookAt(0, 0, 0);
      camera.near = -r * 10;
      camera.far  =  r * 10;
      applyFrustum();

      const posAttr    = geometry.attributes.position;
      const clsAttr    = geometry.attributes['classification'];
      const unclassPos = [];
      const otherPos   = [];

      for (let i = 0; i < posAttr.count; i++) {
        const cls = clsAttr ? Math.round(clsAttr.getX(i)) : -1;
        if (HIDDEN_CLASSES.has(cls)) continue;
        const bucket = UNCLASSIFIED_CLASSES.has(cls) ? unclassPos : otherPos;
        bucket.push(posAttr.getX(i), posAttr.getY(i), posAttr.getZ(i));
      }

      const group = new THREE.Group();
      groupRef.current = group;
      matsRef.current  = [];

      [[unclassPos, COLOR_UNCLASSIFIED], [otherPos, COLOR_OTHER]].forEach(([pts, color]) => {
        if (pts.length === 0) return;
        const geo = new THREE.BufferGeometry();
        geo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(pts), 3));
        const mat = new THREE.PointsMaterial({ size: baseSize, color, sizeAttenuation: true });
        matsRef.current.push(mat);
        group.add(new THREE.Points(geo, mat));
      });

      scene.add(group);
      setStatus('ready');
    }, undefined, () => setStatus('error'));

    // Pan
    const onMouseDown = (e) => {
      if (e.button !== 0) return;
      isDraggingRef.current = true;
      lastMouseRef.current  = { x: e.clientX, y: e.clientY };
      mount.style.cursor    = 'grabbing';
    };
    const onMouseMove = (e) => {
      if (!isDraggingRef.current) return;
      const dx = e.clientX - lastMouseRef.current.x;
      const dy = e.clientY - lastMouseRef.current.y;
      lastMouseRef.current = { x: e.clientX, y: e.clientY };
      const uPx = (frustumHalfWRef.current * 2) / mount.clientWidth;
      panOffsetXRef.current -= dx * uPx;
      panOffsetYRef.current += dy * uPx;
      const maxPan = boundsRadiusRef.current * 0.9;
      panOffsetXRef.current = Math.max(-maxPan, Math.min(maxPan, panOffsetXRef.current));
      panOffsetYRef.current = Math.max(-maxPan, Math.min(maxPan, panOffsetYRef.current));
      applyFrustum();
    };
    const onMouseUp = () => { isDraggingRef.current = false; mount.style.cursor = 'grab'; };
    mount.addEventListener('mousedown', onMouseDown);
    document.addEventListener('mousemove', onMouseMove);
    document.addEventListener('mouseup', onMouseUp);
    mount.style.cursor = 'grab';

    let animId = 0;
    const animate = () => {
      animId = requestAnimationFrame(animate);
      if (groupRef.current && isRotatingRef.current) groupRef.current.rotation.z += 0.0008;
      renderer.render(scene, camera);
    };
    requestAnimationFrame(animate);

    const ro = new ResizeObserver(() => {
      applyFrustum();
      renderer.setSize(mount.clientWidth, mount.clientHeight);
    });
    ro.observe(mount);

    return () => {
      cancelAnimationFrame(animId);
      ro.disconnect();
      renderer.dispose();
      mount.removeEventListener('mousedown', onMouseDown);
      document.removeEventListener('mousemove', onMouseMove);
      document.removeEventListener('mouseup', onMouseUp);
      if (mount.contains(renderer.domElement)) mount.removeChild(renderer.domElement);
      cameraRef.current = null;
      sceneRef.current  = null;
      groupRef.current  = null;
    };
  }, [src, applyFrustum]);

  const btnBase = {
    width: 40, height: 40, display: 'flex', alignItems: 'center', justifyContent: 'center',
    fontSize: 18, fontWeight: 300, cursor: 'pointer',
    border: '1px solid rgba(0,0,0,0.12)',
    background: 'rgba(255,255,255,0.75)',
    color: 'rgba(0,0,0,0.70)',
    backdropFilter: 'blur(6px)', WebkitBackdropFilter: 'blur(6px)',
  };

  return (
    <div style={{ position: 'relative', width: '100%', height: '100%' }}>
      <div ref={mountRef} style={{ position: 'absolute', inset: 0 }} />

      {status === 'loading' && (
        <div style={{
          position: 'absolute', inset: 0, display: 'flex', alignItems: 'center',
          justifyContent: 'center', pointerEvents: 'none',
          fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '.14em',
          textTransform: 'uppercase', color: 'var(--grey-1)',
        }}>
          Loading point cloud…
        </div>
      )}

      <div style={{ position: 'absolute', bottom: 24, right: 24, zIndex: 10, display: 'flex', flexDirection: 'column', gap: 6 }}>
        <button style={btnBase} title={isRotating ? 'Pause rotation' : 'Resume rotation'}
          onClick={() => { isRotatingRef.current = !isRotatingRef.current; setIsRotating(r => !r); }}>
          {isRotating ? '⏸' : '⏵'}
        </button>
        <div style={{ borderTop: '1px solid rgba(0,0,0,0.12)', margin: '2px 4px' }} />
        <button style={btnBase} title="Zoom in"  onClick={() => zoom(0.8)}>+</button>
        <button style={btnBase} title="Zoom out" onClick={() => zoom(1.25)}>−</button>
      </div>
    </div>
  );
}
window.PointCloudViewer = PointCloudViewer;
