// Three.js scene: real Ray-Ban Meta GLB, exploded reveal, hover/click → tooltip + panel.
// Replaces the SVG schematic. Reads window.SUBSYSTEMS from data.jsx.

(function () {
  const THREE = window.THREE;
  if (!THREE) { console.error('THREE missing'); return; }

  const canvas = document.getElementById('three-canvas');
  if (!canvas) return;

  // Mobile detection — drives lower DPR, simpler materials, no PMREM, no edge overlay.
  // Match by viewport width AND coarse-pointer presence so phones in landscape still
  // count as mobile but a small desktop window doesn't cripple the scene unnecessarily.
  const IS_MOBILE = (window.matchMedia && window.matchMedia('(max-width: 740px)').matches) ||
                    (window.matchMedia && window.matchMedia('(pointer: coarse)').matches && window.innerWidth < 900);
  window.__IS_MOBILE = IS_MOBILE;

  // ────────────────────────────────────────────────────────────────
  // Renderer / scene / camera
  // ────────────────────────────────────────────────────────────────
  const renderer = new THREE.WebGLRenderer({
    canvas, antialias: !IS_MOBILE, alpha: true,
    preserveDrawingBuffer: true,
    powerPreference: 'high-performance'
  });
  // Cap DPR aggressively on mobile — 1.5 is the sweet spot for retina phones (still crisp,
  // half the fragment work of DPR=3). Desktop keeps full 2x.
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, IS_MOBILE ? 1.5 : 2));
  if (THREE.SRGBColorSpace) renderer.outputColorSpace = THREE.SRGBColorSpace;
  else renderer.outputEncoding = THREE.sRGBEncoding;
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
  renderer.toneMappingExposure = 1.05;

  const scene = new THREE.Scene();
  scene.background = null;

  const camera = new THREE.PerspectiveCamera(28, 1, 0.1, 100);
  camera.position.set(0.0, 0.5, 2.6);
  camera.lookAt(0, 0, 0);

  // Lighting — cool/warm rim to give the matte frame visible volume
  const key = new THREE.DirectionalLight(0xeaf6ff, 2.2);
  key.position.set(2.5, 3.0, 2.2);
  scene.add(key);

  const rim = new THREE.DirectionalLight(0x00d4ff, 1.6);
  rim.position.set(-3.0, 1.0, -2.0);
  scene.add(rim);

  const fill = new THREE.DirectionalLight(0xff7a4d, 0.5);
  fill.position.set(1.5, -2.0, 1.0);
  scene.add(fill);

  scene.add(new THREE.AmbientLight(0x224466, 0.55));
  scene.add(new THREE.HemisphereLight(0x88ccff, 0x101820, 0.4));

  // Resize
  function resize() {
    const w = canvas.clientWidth;
    const h = canvas.clientHeight;
    if (renderer.getSize(new THREE.Vector2()).x !== w || renderer.getSize(new THREE.Vector2()).y !== h) {
      renderer.setSize(w, h, false);
      camera.aspect = w / h;
      camera.updateProjectionMatrix();
    }
  }
  new ResizeObserver(resize).observe(canvas);
  resize();

  // ────────────────────────────────────────────────────────────────
  // Load GLB
  // ────────────────────────────────────────────────────────────────
  const root = new THREE.Group();
  scene.add(root);

  // PMREM env map — required for MeshPhysicalMaterial.transmission to actually
  // transmit (without it the lens just shows a flat tint of its base color).
  // We bake a simple gradient cube into a PMREM probe so the lenses sample
  // something more interesting than flat black.
  // Skip on mobile — PMREM costs a few MB of GPU memory and the lens uses simple
  // alpha-blended transparency anyway, so the env probe contribution is minor.
  if (!IS_MOBILE) try {
    const pmrem = new THREE.PMREMGenerator(renderer);
    const tmpScene = new THREE.Scene();
    // Add a few colored emissive panels so the probe captures bright/dark areas
    const panels = [
      { c: 0x6cc8ff, p: [0,  4, 0],  s: [8, 0.1, 8] },  // ceiling cyan
      { c: 0x0a1020, p: [0, -4, 0],  s: [8, 0.1, 8] },  // floor dark
      { c: 0x002244, p: [-4, 0, 0],  s: [0.1, 8, 8] },
      { c: 0x002244, p: [ 4, 0, 0],  s: [0.1, 8, 8] },
      { c: 0x001830, p: [0, 0, -4],  s: [8, 8, 0.1] },
      { c: 0x88ddff, p: [0, 0,  4],  s: [8, 8, 0.1] },  // front bright
    ];
    panels.forEach(({c, p, s}) => {
      const m = new THREE.Mesh(
        new THREE.BoxGeometry(s[0], s[1], s[2]),
        new THREE.MeshBasicMaterial({ color: c })
      );
      m.position.set(p[0], p[1], p[2]);
      tmpScene.add(m);
    });
    const envTarget = pmrem.fromScene(tmpScene, 0.04);
    scene.environment = envTarget.texture;
    pmrem.dispose();
  } catch (e) {
    console.warn('PMREM env setup failed (transmission may look flat)', e);
  }

  const partRecords = []; // { mesh, home: Vec3, target: Vec3, subsystemId, edges }
  const meshById = {};    // subsystemId -> mesh[]

  const loader = new THREE.GLTFLoader();
  loader.load('assets/glasses.glb', (gltf) => {
    const model = gltf.scene;
    // Center & scale model — order matters: scale first so position offsets are in scaled units
    const box = new THREE.Box3().setFromObject(model);
    const size = new THREE.Vector3(); box.getSize(size);
    const center = new THREE.Vector3(); box.getCenter(center);
    // On mobile, render the device 2x smaller so the schematic chrome and
    // bottom tab bar have room. The pinch-to-resize gesture later scales
    // root.scale (a separate uniform multiplier) on top of this baseline.
    const baseFit = 1.4 / Math.max(size.x, size.y, size.z);
    const scale = baseFit * (IS_MOBILE ? 0.5 : 1.0);
    model.scale.setScalar(scale);
    model.position.sub(center.multiplyScalar(scale));
    root.add(model);
    window.__model = model;
    window.__root = root;
    window.__camera = camera;
    console.log('model size', size.x.toFixed(3), size.y.toFixed(3), size.z.toFixed(3), 'scale', scale.toFixed(4));

    // Recompute box after scale
    const bb = new THREE.Box3().setFromObject(model);
    const ext = new THREE.Vector3(); bb.getSize(ext);

    // Walk meshes and classify
    model.traverse((o) => {
      if (!o.isMesh) return;
      // Compute world-space bounding box center
      o.geometry.computeBoundingBox();
      const local = o.geometry.boundingBox.clone();
      const c = new THREE.Vector3();
      local.getCenter(c);
      o.localToWorld(c);

      // Material tweaks: turn frame matte black, lenses smoke tint, RB logos ink
      const name = (o.name || '').toLowerCase();
      const meshName = (o.name || '');

      // Re-skin everything for the "blueprint x physical" look.
      // On mobile, swap MeshPhysicalMaterial → MeshStandardMaterial everywhere — clearcoat
      // and physical-glass shader paths are the heaviest fragment work in the scene.
      let mat;
      let kind = 'frame';
      const PhysOrStd = IS_MOBILE ? THREE.MeshStandardMaterial : THREE.MeshPhysicalMaterial;
      if (name.includes('inside lens') || name.includes('inside_lens') || name.includes('sphere')) {
        kind = 'lens';
        // Lenses + small spheres — clear glass look. We DON'T use transmission here
        // because transmission samples the three.js scene only, not the HTML page
        // behind the canvas. Plain alpha-blended transparency lets the bg-grid
        // show through, which is the look we want.
        const lensProps = {
          color: 0x6cc8ff,
          metalness: 0.0, roughness: 0.05,
          transparent: true,
          opacity: 0.18,
          envMapIntensity: IS_MOBILE ? 0.6 : 1.6,
          depthWrite: false  // don't occlude objects behind the lens
        };
        if (!IS_MOBILE) Object.assign(lensProps, { clearcoat: 1.0, clearcoatRoughness: 0.05 });
        mat = new PhysOrStd(lensProps);
      } else if (name.includes('rb') || name.includes('ray ban') || name.includes('ray_ban')) {
        kind = 'chip';
        // Brand decal mesh = clickable hot-spot for subsystem 03 (Compute / SoC).
        // Treat it like a UI chip: matte cyan-tinted face with a breathing glow so it
        // reads as "selectable badge" rather than a stray white element.
        mat = new THREE.MeshStandardMaterial({
          color: 0x6cc8ff,
          metalness: 0.15, roughness: 0.55,
          emissive: 0x00d4ff,
          emissiveIntensity: 0.55,
          envMapIntensity: 0.5
        });
      } else {
        // Frame, legs, hinges — dark gunmetal, low env reflection so the frame
        // stays dark even with the lens-driven env probe in scene.
        const frameProps = {
          color: 0x1a1d22, metalness: 0.4, roughness: 0.35,
          envMapIntensity: 0.35
        };
        if (!IS_MOBILE) Object.assign(frameProps, { clearcoat: 0.8, clearcoatRoughness: 0.25 });
        mat = new PhysOrStd(frameProps);
      }
      o.material = mat;
      o.castShadow = false;
      o.receiveShadow = false;

      // Edge overlay (blueprint look) — wireframe of silhouettes only.
      // Skip entirely on mobile: each EdgesGeometry doubles draw calls and adds
      // CPU work in the animate loop for opacity tweens we can't show on a 360px screen anyway.
      let edgeMat;
      if (IS_MOBILE) {
        // Stub material so the animate loop's r.edgeMat references stay safe
        edgeMat = { opacity: 0, color: { set: () => {} }, linewidth: 1 };
      } else {
        const edges = new THREE.EdgesGeometry(o.geometry, 25);
        edgeMat = new THREE.LineBasicMaterial({
          color: 0x6cc8ff, transparent: true, opacity: 0.0
        });
        const edgeLines = new THREE.LineSegments(edges, edgeMat);
        edgeLines.userData.isEdge = true;
        o.add(edgeLines);
      }

      // Classify by world-space center after model centered+scaled
      const cw = new THREE.Vector3();
      o.getWorldPosition(cw);
      const bbox = new THREE.Box3().setFromObject(o);
      const cc = new THREE.Vector3(); bbox.getCenter(cc);
      const sz = new THREE.Vector3(); bbox.getSize(sz);

      // Heuristic mapping → subsystem id (keys 1..8 from data.jsx)
      // Coordinate system after centering: x = horizontal (+ right), y = up, z = forward toward camera
      // Mesh names in GLB: inside_lens002 (lens cluster), legs002 (full chassis+temples),
      // frame005 (front frame), frame004/006 (hinge accents), Sphere0XX_X (temple-tip caps),
      // Curve0XX (zero-size curves), RB_white002 / RAY_BAN_white002 (branding)
      let id;

      let splitByX = null;  // when set, raycaster picks id by hit-point x sign: {pos, neg}
      if (name.includes('inside_lens') || name.includes('inside lens')) {
        // Single mesh containing BOTH lenses — disambiguate at raycast time by hit.point.x
        id = '1';
        splitByX = { pos: '1', neg: '2' };  // right lens (x>0) vs left lens
      } else if (name.includes('sphere')) {
        // Temple-tip caps cluster near (-0.35,0.19,0.70) and (0.70,0.16,0.35)
        // Treat as temple ends → left/right temple
        id = (cc.x > 0) ? '6' : '7';
      } else if (name.includes('curve')) {
        // Zero-size curves (helper geometry) — group with chassis
        id = '8';
      } else if (name.includes('legs')) {
        // Full chassis with both temples — split at hit time so left temple ≠ right temple
        id = '8';
        splitByX = { pos: '6', neg: '7', mid: '8', midThreshold: 0.25 };
      } else if (name.includes('frame00') && sz.x < 0.1) {
        // Tiny hinge frames at temple ends
        id = (cc.x > 0) ? '6' : '7';
      } else if (name.includes('frame')) {
        // Main front frame around lenses
        id = '4';
      } else if (name.includes('rb') || name.includes('ray')) {
        id = '3'; // brand label = compute chip badge
      } else {
        id = '8';
      }

      // Treat the brand-decal mesh as a "selectable chip" — it pulses in idle so
      // users notice it as an interactive element rather than a stray white piece.
      const isChip = name.includes('rb') || name.includes('ray');

      partRecords.push({
        mesh: o,
        home: cc.clone(),     // world-space home center, used for explode direction
        target: new THREE.Vector3(),
        subsystemId: id,
        splitByX,             // optional {pos, neg, mid?, midThreshold?} for shared meshes
        isChip,               // idle attention pulse
        kind,                 // 'frame' | 'lens' | 'chip' — used by customizer
        edgeMat,
        baseOpacity: 1.0
      });
      (meshById[id] ||= []).push(o);
    });

    // Better classification pass for the "legs" mesh — split by part position vs. world.
    // (we accept the rough mapping — UX uses subsystem panels, not surgical separation)

    // Compute per-part EXPLODE direction from its home center (radial outward)
    partRecords.forEach((r) => {
      const dir = r.home.clone();
      // Bias outward in y for top parts, downward for nose bridge etc.
      const id = r.subsystemId;
      const v = SUBSYSTEM_DIR[id] || { x: dir.x*1.6, y: dir.y*1.6, z: 0.6 };
      r.target.set(v.x, v.y, v.z);
    });

    // ─── Customizer API ──────────────────────────────────────────────
    // Frame finishes: each preset adjusts metalness/roughness/clearcoat.
    const FRAME_FINISHES = {
      matte:     { metalness: 0.05, roughness: 0.78, clearcoat: 0.0,  clearcoatRoughness: 0.6 },
      gloss:     { metalness: 0.10, roughness: 0.18, clearcoat: 1.0,  clearcoatRoughness: 0.05 },
      metallic:  { metalness: 0.95, roughness: 0.28, clearcoat: 0.4,  clearcoatRoughness: 0.15 },
      brushed:   { metalness: 0.85, roughness: 0.55, clearcoat: 0.2,  clearcoatRoughness: 0.4 },
      tortoise:  { metalness: 0.05, roughness: 0.32, clearcoat: 0.9,  clearcoatRoughness: 0.08 },
      neon:      { metalness: 0.0,  roughness: 0.25, clearcoat: 1.0,  clearcoatRoughness: 0.05, emissive: 0x00d4ff, emissiveIntensity: 0.35 }
    };
    let frameBaseColor = new THREE.Color(0x1a1d22);
    let lensBaseColor  = new THREE.Color(0x6cc8ff);

    window.__customizer = {
      setFrameColor(hex) {
        frameBaseColor = new THREE.Color(hex);
        partRecords.forEach((r) => {
          if (r.kind !== 'frame') return;
          if (r.mesh.material && r.mesh.material.color) r.mesh.material.color.copy(frameBaseColor);
        });
      },
      setFrameFinish(name) {
        const f = FRAME_FINISHES[name] || FRAME_FINISHES.matte;
        // Publish baseline emissive for the animate loop (so neon's idle glow survives the
        // every-frame overwrite that drives hover/click highlight emissive).
        window.__frameFinishEmissive = (f.emissive != null) ? f.emissive : 0x000000;
        window.__frameFinishEmissiveIntensity = (f.emissive != null) ? (f.emissiveIntensity || 0.3) : 0;
        partRecords.forEach((r) => {
          if (r.kind !== 'frame') return;
          const m = r.mesh.material; if (!m) return;
          if ('metalness' in m) m.metalness = f.metalness;
          if ('roughness' in m) m.roughness = f.roughness;
          if ('clearcoat' in m) m.clearcoat = f.clearcoat;
          if ('clearcoatRoughness' in m) m.clearcoatRoughness = f.clearcoatRoughness;
          // Emissive: clear unless preset asks for one
          if (m.emissive) {
            if (f.emissive != null) {
              m.emissive.set(f.emissive);
              m.emissiveIntensity = f.emissiveIntensity || 0.3;
            } else {
              m.emissive.set(0x000000);
              m.emissiveIntensity = 0;
            }
          }
          m.needsUpdate = true;
        });
      },
      setLensTint(hex, opacity) {
        lensBaseColor = new THREE.Color(hex);
        partRecords.forEach((r) => {
          if (r.kind !== 'lens') return;
          const m = r.mesh.material; if (!m) return;
          m.color.copy(lensBaseColor);
          if (opacity != null) m.opacity = opacity;
          m.needsUpdate = true;
        });
      }
    };

    window.__SCENE_READY = true;
    if (window.__onSceneReady) window.__onSceneReady();
  }, undefined, (err) => {
    console.error('GLB load failed', err);
  });

  // Per-subsystem explode targets (world-space deltas)
  const SUBSYSTEM_DIR = {
    '1': { x:  0.55, y:  0.25, z:  0.50 },  // right lens
    '2': { x: -0.55, y:  0.25, z:  0.50 },
    '3': { x:  0.00, y:  0.55, z:  0.30 },
    '4': { x: -0.65, y: -0.10, z:  0.45 },
    '5': { x:  0.00, y: -0.45, z:  0.20 },
    '6': { x:  0.95, y:  0.05, z: -0.20 },
    '7': { x: -0.95, y:  0.05, z: -0.20 },
    '8': { x:  0.00, y: -0.30, z: -0.55 },
  };

  // ────────────────────────────────────────────────────────────────
  // Interaction state (driven by interactions.jsx via window.__scene*)
  // ────────────────────────────────────────────────────────────────
  let mode = 'explode';
  let hoverId = null;
  let pinnedId = null;
  let explodeAmount = 0;     // 0..1 animated
  let explodeTargetAmount = 0;

  window.__scene = {
    setMode(m) { mode = m; },
    setHover(id) { hoverId = id; },
    setPinned(id) { pinnedId = id; },
    raycastAtClient(clientX, clientY) {
      const rect = canvas.getBoundingClientRect();
      const x = ((clientX - rect.left) / rect.width) * 2 - 1;
      const y = -((clientY - rect.top) / rect.height) * 2 + 1;
      const r = new THREE.Raycaster();
      r.setFromCamera({ x, y }, camera);
      const meshes = partRecords.map(p => p.mesh);
      const hits = r.intersectObjects(meshes, false);
      if (!hits.length) return null;
      const hit = hits[0];
      const rec = partRecords.find(p => p.mesh === hit.object);
      if (!rec) return null;
      // For shared meshes (both lenses, full chassis), disambiguate by hit-point x in
      // WORLD space — that matches what the viewer sees as left/right regardless of
      // any drag-rotation applied to root.
      if (rec.splitByX) {
        // Convert world-space hit to root-local so user-applied rotation doesn't flip sides
        const local = window.__root ? window.__root.worldToLocal(hit.point.clone()) : hit.point.clone();
        const sx = local.x;
        const { pos, neg, mid, midThreshold = 0 } = rec.splitByX;
        if (mid && Math.abs(sx) < midThreshold) return mid;
        return sx > 0 ? pos : neg;
      }
      return rec.subsystemId;
    },
    rotateBy(dx, dy) {
      // user drag rotation
      root.rotation.y += dx;
      root.rotation.x += dy;
      root.rotation.x = Math.max(-0.6, Math.min(0.6, root.rotation.x));
    },
    // Multiplicative user-scale on top of the baseline fit. 1.0 = whatever the
    // initial fit decided (which itself is 0.5x on mobile vs. desktop).
    // Clamp aggressively so the user can't pinch-zoom into oblivion.
    setUserScale(s) {
      const clamped = Math.max(0.4, Math.min(2.5, s));
      root.userData.userScale = clamped;
      root.scale.setScalar(clamped);
    },
    getUserScale() {
      return root.userData.userScale != null ? root.userData.userScale : 1;
    }
  };

  // ────────────────────────────────────────────────────────────────
  // Animation loop
  // ────────────────────────────────────────────────────────────────
  let t = 0;
  function animate() {
    requestAnimationFrame(animate);
    resize();
    t += 0.0066;

    // Idle float (yaw) when no pin — disabled on mobile to lock 60fps and save battery
    if (!pinnedId && !IS_MOBILE) {
      root.rotation.y = root.rotation.y * 0.96 + (Math.sin(t * 0.6) * 0.45) * 0.04;
      root.rotation.x = root.rotation.x * 0.96 + (Math.sin(t * 0.5) * 0.10) * 0.04;
    }

    // Explode amount target — full when something is active in explode mode
    const anyActive = !!(hoverId || pinnedId);
    explodeTargetAmount = (mode === 'explode' && anyActive) ? 1.0 : (mode === 'explode' ? 0.0 : 0.0);
    explodeAmount += (explodeTargetAmount - explodeAmount) * 0.08;

    partRecords.forEach((r) => {
      const id = r.subsystemId;
      // For split meshes (e.g. legs covers chassis + both temples), the record's
      // subsystemId is a default — but raycast may resolve hover to one of the
      // split outputs. Treat ANY of those as "this record is active" so the
      // physical mesh glows when its corresponding subsystem is hovered.
      let isActive = id === hoverId || id === pinnedId;
      if (!isActive && r.splitByX) {
        const opts = [r.splitByX.pos, r.splitByX.neg, r.splitByX.mid].filter(Boolean);
        isActive = opts.includes(hoverId) || opts.includes(pinnedId);
      }

      // Per-part explode amount: full if active; small drift back if not (when something else is active)
      let f = 0;
      if (mode === 'explode') {
        if (isActive) f = explodeAmount;
        else if (anyActive) f = -0.10 * explodeAmount;
      }

      const tx = r.target.x * f;
      const ty = r.target.y * f;
      const tz = r.target.z * f;

      // Smoothly chase target offset (in mesh world space — apply on parent root via local offsets)
      // Mesh is child of model which is child of root. We modify mesh.position relative to its parent.
      // We don't know its initial local position, so cache once:
      if (r.baseLocal === undefined) r.baseLocal = r.mesh.position.clone();
      const desired = r.baseLocal.clone().add(new THREE.Vector3(tx, ty, tz));
      r.mesh.position.lerp(desired, 0.12);

      // X-ray: dim non-active
      const targetOpacity = (mode === 'xray' && anyActive && !isActive) ? 0.08 : 1.0;
      r.baseOpacity += (targetOpacity - r.baseOpacity) * 0.15;
      const m = r.mesh.material;
      m.transparent = r.baseOpacity < 0.99;
      m.opacity = Math.max(0.0, r.baseOpacity);

      // Cross-section: edge lines glow on active part
      // Chip parts also keep a faint persistent halo so they read as "selectable"
      let eOp;
      if (mode === 'cross' && isActive) eOp = 0.95;
      else if (isActive) eOp = 0.85;
      else if (r.isChip) eOp = 0.45 + 0.25 * Math.sin(t * 4.5); // subtle breathing halo
      else eOp = 0.0;
      r.edgeMat.opacity += (eOp - r.edgeMat.opacity) * 0.18;
      r.edgeMat.color.set(mode === 'cross' ? 0x00ff88 : 0x00d4ff);
      // Make active-part wireframe noticeably thicker / brighter
      if (r.edgeMat.linewidth !== undefined) r.edgeMat.linewidth = isActive ? 2 : 1;

      // Active part emissive lift — stronger so dark matte parts (frame, chassis) clearly glow.
      // Chip parts pulse softly at all times so they call attention as selectable badges.
      // Frame parts may have a finish-driven baseline glow (e.g. neon preset).
      if ('emissive' in m) {
        const baselineEm = (r.kind === 'frame' && window.__frameFinishEmissive) ? window.__frameFinishEmissive : 0x000000;
        const baselineI  = (r.kind === 'frame' && window.__frameFinishEmissiveIntensity) ? window.__frameFinishEmissiveIntensity : 0;
        const targetEm = isActive ? (mode === 'cross' ? 0x00aa55 : 0x0088cc) : baselineEm;
        m.emissive.lerpColors(m.emissive, new THREE.Color(targetEm), 0.18);
        if ('emissiveIntensity' in m) {
          let targetI;
          if (isActive) targetI = 1.1;
          else if (r.isChip) targetI = 0.45 + 0.35 * (0.5 + 0.5 * Math.sin(t * 4.5)); // breathing
          else targetI = baselineI;
          m.emissiveIntensity += (targetI - m.emissiveIntensity) * 0.18;
        }
        // For chip parts, set the idle emissive color to cyan so the breathing reads as a glow
        if (r.isChip && !isActive) {
          m.emissive.lerpColors(m.emissive, new THREE.Color(0x00d4ff), 0.18);
        }
      }
    });

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