/**
 * Gaussian Splat Renderer for WiFi Sensing Visualization
 *
 * Renders a 3D signal field using Three.js Points with custom ShaderMaterial.
 * Each "splat" is a screen-space disc whose size, color and opacity are driven
 * by the sensing data:
 *   - Size  : signal variance / disruption magnitude
 *   - Color : blue (quiet) -> green (presence) -> red (active motion)
 *   - Opacity: classification confidence
 */

// Use global THREE from CDN (loaded in SensingTab)
const getThree = () => window.THREE;

// ---- Custom Splat Shaders ------------------------------------------------

const SPLAT_VERTEX = `
  attribute float splatSize;
  attribute vec3  splatColor;
  attribute float splatOpacity;

  varying vec3  vColor;
  varying float vOpacity;

  void main() {
    vColor   = splatColor;
    vOpacity = splatOpacity;

    vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
    gl_PointSize = splatSize * (300.0 / -mvPosition.z);
    gl_Position  = projectionMatrix * mvPosition;
  }
`;

const SPLAT_FRAGMENT = `
  varying vec3  vColor;
  varying float vOpacity;

  void main() {
    // Circular soft-edge disc
    float dist = length(gl_PointCoord - vec2(0.5));
    if (dist > 0.5) discard;
    float alpha = smoothstep(0.5, 0.2, dist) * vOpacity;
    gl_FragColor = vec4(vColor, alpha);
  }
`;

// ---- Color helpers -------------------------------------------------------

/** Map a scalar 0-1 to blue -> green -> red gradient */
function valueToColor(v) {
  const clamped = Math.max(0, Math.min(1, v));
  // blue(0) -> cyan(0.25) -> green(0.5) -> yellow(0.75) -> red(1)
  let r, g, b;
  if (clamped < 0.5) {
    const t = clamped * 2;
    r = 0;
    g = t;
    b = 1 - t;
  } else {
    const t = (clamped - 0.5) * 2;
    r = t;
    g = 1 - t;
    b = 0;
  }
  return [r, g, b];
}

// ---- Node marker color palette -------------------------------------------

const NODE_MARKER_COLORS = [0x00ccff, 0xff6600, 0x00ff88, 0xff00cc, 0xffcc00, 0x8800ff, 0x00ffcc, 0xff0044];

// ---- GaussianSplatRenderer -----------------------------------------------

export class GaussianSplatRenderer {
  /**
   * @param {HTMLElement} container - DOM element to attach the renderer to
   * @param {object}      [opts]
   * @param {number}      [opts.width]  - canvas width  (default container width)
   * @param {number}      [opts.height] - canvas height (default 500)
   */
  constructor(container, opts = {}) {
    const THREE = getThree();
    if (!THREE) throw new Error('Three.js not loaded');

    this.container = container;
    this.width  = opts.width  || container.clientWidth || 800;
    this.height = opts.height || 500;

    // Scene
    this.scene = new THREE.Scene();
    this.scene.background = new THREE.Color(0x0a0a12);

    // Camera — perspective looking down at the room
    this.camera = new THREE.PerspectiveCamera(55, this.width / this.height, 0.1, 200);
    this.camera.position.set(0, 14, 14);
    this.camera.lookAt(0, 0, 0);

    // Renderer
    this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
    this.renderer.setSize(this.width, this.height);
    this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    container.appendChild(this.renderer.domElement);

    // Grid & room
    this._createRoom(THREE);

    // Signal field splats (20x20 = 400 points on the floor plane)
    this.gridSize = 20;
    this._createFieldSplats(THREE);

    // Node markers (ESP32 / router positions)
    this._createNodeMarkers(THREE);

    // Dynamic per-node markers (multi-node support)
    this.nodeMarkers = new Map(); // nodeId -> THREE.Mesh
    this._THREE = THREE;

    // Body disruption blob
    this._createBodyBlob(THREE);

    // Simple orbit-like mouse rotation
    this._setupMouseControls();

    // Animation state
    this._animFrame = null;
    this._lastData = null;

    // Start render loop
    this._animate();
  }

  // ---- Scene setup -------------------------------------------------------

  _createRoom(THREE) {
    // Floor grid
    const grid = new THREE.GridHelper(20, 20, 0x1a3a4a, 0x0d1f28);
    this.scene.add(grid);

    // Room boundary wireframe
    const boxGeo = new THREE.BoxGeometry(20, 6, 20);
    const edges  = new THREE.EdgesGeometry(boxGeo);
    const line   = new THREE.LineSegments(
      edges,
      new THREE.LineBasicMaterial({ color: 0x1a4a5a, opacity: 0.3, transparent: true })
    );
    line.position.y = 3;
    this.scene.add(line);
  }

  _createFieldSplats(THREE) {
    const count = this.gridSize * this.gridSize;

    const positions = new Float32Array(count * 3);
    const sizes     = new Float32Array(count);
    const colors    = new Float32Array(count * 3);
    const opacities = new Float32Array(count);

    // Lay splats on the floor plane (y = 0.05 to sit just above grid)
    for (let iz = 0; iz < this.gridSize; iz++) {
      for (let ix = 0; ix < this.gridSize; ix++) {
        const idx = iz * this.gridSize + ix;
        positions[idx * 3 + 0] = (ix - this.gridSize / 2) + 0.5; // x
        positions[idx * 3 + 1] = 0.05;                            // y
        positions[idx * 3 + 2] = (iz - this.gridSize / 2) + 0.5; // z

        sizes[idx]     = 1.5;
        colors[idx * 3]     = 0.1;
        colors[idx * 3 + 1] = 0.2;
        colors[idx * 3 + 2] = 0.6;
        opacities[idx] = 0.15;
      }
    }

    const geo = new THREE.BufferGeometry();
    geo.setAttribute('position',    new THREE.BufferAttribute(positions, 3));
    geo.setAttribute('splatSize',   new THREE.BufferAttribute(sizes, 1));
    geo.setAttribute('splatColor',  new THREE.BufferAttribute(colors, 3));
    geo.setAttribute('splatOpacity',new THREE.BufferAttribute(opacities, 1));

    const mat = new THREE.ShaderMaterial({
      vertexShader:   SPLAT_VERTEX,
      fragmentShader: SPLAT_FRAGMENT,
      transparent: true,
      depthWrite: false,
      blending: THREE.AdditiveBlending,
    });

    this.fieldPoints = new THREE.Points(geo, mat);
    this.scene.add(this.fieldPoints);
  }

  _createNodeMarkers(THREE) {
    // Router at center — green sphere
    const routerGeo = new THREE.SphereGeometry(0.3, 16, 16);
    const routerMat = new THREE.MeshBasicMaterial({ color: 0x00ff88, transparent: true, opacity: 0.8 });
    this.routerMarker = new THREE.Mesh(routerGeo, routerMat);
    this.routerMarker.position.set(0, 0.5, 0);
    this.scene.add(this.routerMarker);

    // ESP32 node — cyan sphere (default position, updated from data)
    const nodeGeo = new THREE.SphereGeometry(0.25, 16, 16);
    const nodeMat = new THREE.MeshBasicMaterial({ color: 0x00ccff, transparent: true, opacity: 0.8 });
    this.nodeMarker = new THREE.Mesh(nodeGeo, nodeMat);
    this.nodeMarker.position.set(2, 0.5, 1.5);
    this.scene.add(this.nodeMarker);
  }

  _createBodyBlob(THREE) {
    // A cluster of splats representing body disruption
    const count = 64;
    const positions = new Float32Array(count * 3);
    const sizes     = new Float32Array(count);
    const colors    = new Float32Array(count * 3);
    const opacities = new Float32Array(count);

    for (let i = 0; i < count; i++) {
      // Random sphere distribution
      const theta = Math.random() * Math.PI * 2;
      const phi   = Math.acos(2 * Math.random() - 1);
      const r     = Math.random() * 1.5;
      positions[i * 3]     = r * Math.sin(phi) * Math.cos(theta);
      positions[i * 3 + 1] = r * Math.cos(phi) + 2;
      positions[i * 3 + 2] = r * Math.sin(phi) * Math.sin(theta);

      sizes[i] = 2 + Math.random() * 3;
      colors[i * 3]     = 0.2;
      colors[i * 3 + 1] = 0.8;
      colors[i * 3 + 2] = 0.3;
      opacities[i] = 0.0; // hidden until presence detected
    }

    const geo = new THREE.BufferGeometry();
    geo.setAttribute('position',    new THREE.BufferAttribute(positions, 3));
    geo.setAttribute('splatSize',   new THREE.BufferAttribute(sizes, 1));
    geo.setAttribute('splatColor',  new THREE.BufferAttribute(colors, 3));
    geo.setAttribute('splatOpacity',new THREE.BufferAttribute(opacities, 1));

    const mat = new THREE.ShaderMaterial({
      vertexShader:   SPLAT_VERTEX,
      fragmentShader: SPLAT_FRAGMENT,
      transparent: true,
      depthWrite: false,
      blending: THREE.AdditiveBlending,
    });

    this.bodyBlob = new THREE.Points(geo, mat);
    this.scene.add(this.bodyBlob);
  }

  // ---- Mouse controls (simple orbit) -------------------------------------

  _setupMouseControls() {
    let isDragging = false;
    let prevX = 0, prevY = 0;
    let azimuth = 0, elevation = 55;
    const radius = 20;

    const updateCamera = () => {
      const phi   = (elevation * Math.PI) / 180;
      const theta = (azimuth * Math.PI) / 180;
      this.camera.position.set(
        radius * Math.sin(phi) * Math.sin(theta),
        radius * Math.cos(phi),
        radius * Math.sin(phi) * Math.cos(theta)
      );
      this.camera.lookAt(0, 0, 0);
    };

    const canvas = this.renderer.domElement;
    canvas.addEventListener('mousedown', (e) => {
      isDragging = true;
      prevX = e.clientX;
      prevY = e.clientY;
    });
    canvas.addEventListener('mousemove', (e) => {
      if (!isDragging) return;
      azimuth   += (e.clientX - prevX) * 0.4;
      elevation -= (e.clientY - prevY) * 0.4;
      elevation  = Math.max(15, Math.min(85, elevation));
      prevX = e.clientX;
      prevY = e.clientY;
      updateCamera();
    });
    canvas.addEventListener('mouseup',   () => { isDragging = false; });
    canvas.addEventListener('mouseleave',() => { isDragging = false; });

    // Scroll to zoom
    canvas.addEventListener('wheel', (e) => {
      e.preventDefault();
      const delta = e.deltaY > 0 ? 1.05 : 0.95;
      this.camera.position.multiplyScalar(delta);
      this.camera.position.clampLength(8, 40);
    }, { passive: false });

    updateCamera();
  }

  // ---- Data update -------------------------------------------------------

  /**
   * Update the visualization with new sensing data.
   * @param {object} data - sensing_update JSON from ws_server
   */
  update(data) {
    this._lastData = data;
    if (!data) return;

    const features = data.features || {};
    const classification = data.classification || {};
    const signalField = data.signal_field || {};
    const nodes = data.nodes || [];

    // -- Update signal field splats ----------------------------------------
    if (signalField.values && this.fieldPoints) {
      const geo    = this.fieldPoints.geometry;
      const clr    = geo.attributes.splatColor.array;
      const sizes  = geo.attributes.splatSize.array;
      const opac   = geo.attributes.splatOpacity.array;
      const vals   = signalField.values;
      const count  = Math.min(vals.length, this.gridSize * this.gridSize);

      for (let i = 0; i < count; i++) {
        const v = vals[i];
        const [r, g, b] = valueToColor(v);
        clr[i * 3]     = r;
        clr[i * 3 + 1] = g;
        clr[i * 3 + 2] = b;
        sizes[i] = 1.0 + v * 4.0;
        opac[i]  = 0.1 + v * 0.6;
      }

      geo.attributes.splatColor.needsUpdate  = true;
      geo.attributes.splatSize.needsUpdate   = true;
      geo.attributes.splatOpacity.needsUpdate = true;
    }

    // -- Update body blob --------------------------------------------------
    if (this.bodyBlob) {
      const bGeo  = this.bodyBlob.geometry;
      const bOpac = bGeo.attributes.splatOpacity.array;
      const bClr  = bGeo.attributes.splatColor.array;
      const bSize = bGeo.attributes.splatSize.array;
      const bPos  = bGeo.attributes.position.array;

      const presence   = classification.presence || false;
      const motionLvl  = classification.motion_level || 'absent';
      const confidence = classification.confidence || 0;
      const breathing  = features.breathing_band_power || 0;

      // Breathing pulsation
      const breathPulse = 1.0 + Math.sin(Date.now() * 0.004) * Math.min(breathing * 3, 0.4);

      for (let i = 0; i < bOpac.length; i++) {
        if (presence) {
          bOpac[i] = confidence * 0.4;

          // Color by motion level
          if (motionLvl === 'active') {
            bClr[i * 3]     = 1.0;
            bClr[i * 3 + 1] = 0.2;
            bClr[i * 3 + 2] = 0.1;
          } else {
            bClr[i * 3]     = 0.1;
            bClr[i * 3 + 1] = 0.8;
            bClr[i * 3 + 2] = 0.4;
          }

          bSize[i] = (2 + Math.random() * 2) * breathPulse;
        } else {
          bOpac[i] = 0.0;
        }
      }

      bGeo.attributes.splatOpacity.needsUpdate = true;
      bGeo.attributes.splatColor.needsUpdate   = true;
      bGeo.attributes.splatSize.needsUpdate    = true;
    }

    // -- Update node positions (legacy single-node) ------------------------
    if (nodes.length > 0 && nodes[0].position) {
      const pos = nodes[0].position;
      this.nodeMarker.position.set(pos[0], 0.5, pos[2]);
    }

    // -- Update dynamic per-node markers (multi-node support) --------------
    if (nodes && nodes.length > 0 && this.scene) {
      const THREE = this._THREE || window.THREE;
      if (THREE) {
        const activeIds = new Set();
        for (const node of nodes) {
          activeIds.add(node.node_id);
          if (!this.nodeMarkers.has(node.node_id)) {
            const geo = new THREE.SphereGeometry(0.25, 16, 16);
            const mat = new THREE.MeshBasicMaterial({
              color: NODE_MARKER_COLORS[node.node_id % NODE_MARKER_COLORS.length],
              transparent: true,
              opacity: 0.8,
            });
            const marker = new THREE.Mesh(geo, mat);
            this.scene.add(marker);
            this.nodeMarkers.set(node.node_id, marker);
          }
          const marker = this.nodeMarkers.get(node.node_id);
          const pos = node.position || [0, 0, 0];
          marker.position.set(pos[0], 0.5, pos[2]);
        }
        // Remove stale markers
        for (const [id, marker] of this.nodeMarkers) {
          if (!activeIds.has(id)) {
            this.scene.remove(marker);
            this.nodeMarkers.delete(id);
          }
        }
      }
    }
  }

  // ---- Render loop -------------------------------------------------------

  _animate() {
    this._animFrame = requestAnimationFrame(() => this._animate());

    // Gentle router glow pulse
    if (this.routerMarker) {
      const pulse = 0.6 + 0.3 * Math.sin(Date.now() * 0.003);
      this.routerMarker.material.opacity = pulse;
    }

    this.renderer.render(this.scene, this.camera);
  }

  // ---- Resize / cleanup --------------------------------------------------

  resize(width, height) {
    this.width  = width;
    this.height = height;
    this.camera.aspect = width / height;
    this.camera.updateProjectionMatrix();
    this.renderer.setSize(width, height);
  }

  dispose() {
    if (this._animFrame) {
      cancelAnimationFrame(this._animFrame);
    }
    this.renderer.dispose();
    if (this.renderer.domElement.parentNode) {
      this.renderer.domElement.parentNode.removeChild(this.renderer.domElement);
    }
  }
}
