/* Builder3D — Three.js scene for the cozy stick-snap game. */

const { useEffect, useRef, useState } = React;

// Preload the Levelstair logo
const levelstairLogo = new Image();
levelstairLogo.src = '/logo_w.webp';

// ===== Procedural paper texture w/ cutout patterns =====
function makePaperTexture(color, pattern) {
  const c = document.createElement('canvas');
  c.width = 256; c.height = 256;
  const ctx = c.getContext('2d');
  ctx.fillStyle = color;
  ctx.fillRect(0, 0, 256, 256);
  for (let i = 0; i < 600; i++) {
    ctx.fillStyle = `rgba(255,255,255,${Math.random() * 0.05})`;
    ctx.fillRect(Math.random() * 256, Math.random() * 256, 1, 1);
  }
  ctx.strokeStyle = '#fdf6e3';
  ctx.fillStyle = '#fdf6e3';
  ctx.lineWidth = 3;
  const cx = 128, cy = 128;
  if (pattern === 'mandala') {
    for (let i = 0; i < 12; i++) {
      ctx.save(); ctx.translate(cx, cy); ctx.rotate((i * Math.PI) / 6);
      ctx.beginPath(); ctx.ellipse(0, -50, 12, 32, 0, 0, Math.PI * 2); ctx.stroke();
      ctx.restore();
    }
    ctx.beginPath(); ctx.arc(cx, cy, 18, 0, Math.PI * 2); ctx.fill();
    ctx.fillStyle = color;
    ctx.beginPath(); ctx.arc(cx, cy, 8, 0, Math.PI * 2); ctx.fill();
  } else if (pattern === 'lotus') {
    for (let i = 0; i < 8; i++) {
      ctx.save(); ctx.translate(cx, cy); ctx.rotate((i * Math.PI) / 4);
      ctx.beginPath();
      ctx.moveTo(0, 0);
      ctx.quadraticCurveTo(20, -40, 0, -70);
      ctx.quadraticCurveTo(-20, -40, 0, 0);
      ctx.fill();
      ctx.restore();
    }
    ctx.fillStyle = '#fff8e0';
    ctx.beginPath(); ctx.arc(cx, cy, 14, 0, Math.PI * 2); ctx.fill();
  } else if (pattern === 'star') {
    ctx.beginPath();
    for (let i = 0; i < 16; i++) {
      const r = i % 2 === 0 ? 80 : 35;
      const a = (i / 16) * Math.PI * 2 - Math.PI / 2;
      const x = cx + Math.cos(a) * r, y = cy + Math.sin(a) * r;
      if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
    }
    ctx.closePath(); ctx.stroke();
    ctx.beginPath(); ctx.arc(cx, cy, 12, 0, Math.PI * 2); ctx.fill();
  } else if (pattern === 'lattice') {
    for (let i = -4; i <= 4; i++) {
      ctx.beginPath(); ctx.moveTo(0, 128 + i*30); ctx.lineTo(256, 128 + i*30 + 80); ctx.stroke();
      ctx.beginPath(); ctx.moveTo(0, 128 + i*30 + 80); ctx.lineTo(256, 128 + i*30); ctx.stroke();
    }
  } else if (pattern === 'leaf') {
    for (let i = 0; i < 6; i++) {
      ctx.save(); ctx.translate(cx, cy); ctx.rotate((i * Math.PI) / 3);
      ctx.beginPath();
      ctx.moveTo(0, 0);
      ctx.quadraticCurveTo(30, -30, 0, -80);
      ctx.quadraticCurveTo(-30, -30, 0, 0);
      ctx.stroke();
      ctx.restore();
    }
  } else {
    ctx.globalAlpha = 0.4;
    for (let i = 0; i < 80; i++) {
      ctx.beginPath();
      ctx.arc(Math.random() * 256, Math.random() * 256, Math.random() * 3, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.globalAlpha = 1;
  }
  ctx.strokeStyle = '#B08A3E';
  ctx.lineWidth = 6;
  ctx.strokeRect(3, 3, 250, 250);
  const tex = new THREE.CanvasTexture(c);
  tex.needsUpdate = true;
  return tex;
}

window.Builder3D = function Builder3D({
  blueprintKey, placedEdges, onSnap, dragging, lit, glowColor = 0xFFB347,
  decorationLevel = 0, tasselCount = 0,
  panels = {}, paperDrag = null, onPaperSnap = () => {},
  ribbonColor = 0xFFFFFF,
  showRibbons = false,
}) {
  const mountRef = useRef(null);
  const stateRef = useRef({});

  // === Mount: build scene once ===
  useEffect(() => {
    const mount = mountRef.current;
    const W = mount.clientWidth, H = mount.clientHeight;

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

    // On small touch screens pull the camera back so the lantern occupies less of the canvas —
    // leaves more empty space around the model for finger drags and edge highlights to register cleanly.
    const isSmallScreen = mount.clientWidth < 640;
    const camera = new THREE.PerspectiveCamera(42, W / H, 0.1, 100);
    if (isSmallScreen) {
      camera.position.set(2.2, 1.5, 8.0);
    } else {
      camera.position.set(2.5, 1.6, 5.5);
    }
    camera.lookAt(0, 0, 0);

    const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, preserveDrawingBuffer: true });
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(W, H);
    renderer.setClearColor(0x000000, 0);
    mount.appendChild(renderer.domElement);

    // expose a capture function so the app can save a PNG snapshot
    window.__captureLantern = (bgColor = '#0B1124') => {
      // 1) Save original camera settings
      const originalPosition = camera.position.clone();
      const originalQuaternion = camera.quaternion.clone();
      const originalAspect = camera.aspect;
      const originalSize = new THREE.Vector2();
      renderer.getSize(originalSize);

      // We want a 9:16 portrait image for the gallery
      const captureW = 1080;
      const captureH = 1920;

      renderer.setSize(captureW, captureH);
      camera.aspect = captureW / captureH;
      camera.updateProjectionMatrix();

      // 2) Compute bounding box of active elements (lantern and decoGroup)
      const box = new THREE.Box3();
      box.setFromObject(lantern);
      if (decoGroup) {
        box.expandByObject(decoGroup);
      }

      const center = new THREE.Vector3();
      const size = new THREE.Vector3();
      box.getCenter(center);
      box.getSize(size);

      // Default target center is (0, 0, 0) if calculation results in invalid bounds
      const targetCenter = new THREE.Vector3(0, 0, 0);
      if (isFinite(center.x) && isFinite(center.y) && isFinite(center.z)) {
        targetCenter.copy(center);
      }

      // We want to keep the same viewing angle, so get the direction vector from center to camera
      const dir = new THREE.Vector3().subVectors(camera.position, new THREE.Vector3(0, 0, 0)).normalize();

      // 3) Compute distance to fit bounding box
      const fovRad = camera.fov * Math.PI / 180;
      const distanceV = (size.y / 2) / Math.tan(fovRad / 2);
      const distanceH = (size.x / 2) / Math.tan(fovRad / 2) / camera.aspect;
      
      let fitDistance = 4.5;
      if (isFinite(distanceV) && isFinite(distanceH)) {
        // Add depth of the box to prevent clipping, and use a minimum fallback distance
        fitDistance = Math.max(Math.max(distanceV, distanceH) + (size.z / 2), 4.5);
      }
      
      const paddingFactor = 1.15; // 15% margin
      const targetDistance = fitDistance * paddingFactor;

      // 4) Temporarily position camera, hide floor/ground mesh, and render
      camera.position.copy(targetCenter).add(dir.clone().multiplyScalar(targetDistance));
      camera.lookAt(targetCenter);

      // Hide the ground circle to keep the exported PNG clean
      const originalGroundVisible = ground.visible;
      ground.visible = false;

      // force a fresh render so the buffer is current
      renderer.render(scene, camera);
      const glCanvas = renderer.domElement;
      // composite onto a backdrop so the saved PNG has the night-sky background instead of transparency
      const out = document.createElement('canvas');
      out.width = captureW;
      out.height = captureH;
      const ctx = out.getContext('2d');
      // radial gradient mimicking the in-app canvas background
      const grad = ctx.createRadialGradient(
        captureW / 2, captureH / 2, 0,
        captureW / 2, captureH / 2, Math.max(captureW, captureH) / 1.4
      );
      grad.addColorStop(0, '#131A36');
      grad.addColorStop(0.6, '#0B1124');
      grad.addColorStop(1, '#050913');
      ctx.fillStyle = grad;
      ctx.fillRect(0, 0, captureW, captureH);
      
      // Calculate drawing dimensions if 3D scene needs to be centered/scaled 
      // but since we updated renderer size, it exactly matches captureW and captureH
      ctx.drawImage(glCanvas, 0, 0, captureW, captureH);

      // --- ADD OVERLAYS ---
      
      // Top Left: "Happy Vesak Day 2026"
      ctx.fillStyle = '#FFD27F'; // color from BXP palette (candle)
      ctx.font = '700 85px Cinzel'; // display font
      ctx.textAlign = 'left';
      ctx.textBaseline = 'top';
      ctx.fillText("Happy Vesak Day ", 60, 80);
      ctx.font = '700 70px Manrope';
      ctx.fillText("2026", 60, 180);

      // Bottom Right: Levelstair Events & Link
      const bottomX = captureW - 60;
      let logoYOffset = 0;
      
      // Draw event link at very bottom
      ctx.fillStyle = '#A89F87'; // vellum-dim
      ctx.font = '500 40px Manrope';
      ctx.textAlign = 'right';
      ctx.textBaseline = 'bottom';
      ctx.fillText("event.levelstair.com", bottomX, captureH - 60);

      // Draw "Levelstair Events" or logo + Events above the link
      ctx.fillStyle = '#FFFFFF';
      ctx.font = '700 48px Manrope';
      const eventTextWidth = ctx.measureText(" Events").width;
      ctx.fillText(" Events", bottomX, captureH - 120);

      // If the logo loaded successfully, draw it to the left of "Events"
      if (levelstairLogo.complete && levelstairLogo.width > 0) {
        const logoHeight = 60; // match text height roughly
        const aspect = levelstairLogo.width / levelstairLogo.height;
        const logoWidth = logoHeight * aspect;
        const logoX = bottomX - eventTextWidth - logoWidth;
        const logoY = captureH - 120 - logoHeight;
        ctx.drawImage(levelstairLogo, logoX, logoY, logoWidth, logoHeight);
      } else {
        // Fallback text if logo didn't load
        ctx.fillText("Levelstair", bottomX - eventTextWidth, captureH - 120);
      }

      // 5) Restore original camera state, restore ground visibility, and render again to restore editor view
      renderer.setSize(originalSize.width, originalSize.height);
      camera.aspect = originalAspect;
      camera.updateProjectionMatrix();

      camera.position.copy(originalPosition);
      camera.quaternion.copy(originalQuaternion);
      ground.visible = originalGroundVisible;
      renderer.render(scene, camera);

      return out.toDataURL('image/png');
    };

    const amb = new THREE.AmbientLight(0xb8c4e8, 0.18);
    scene.add(amb);
    const key = new THREE.DirectionalLight(0xa8b8e0, 0.35);
    key.position.set(3, 4, 3);
    scene.add(key);
    const rim = new THREE.DirectionalLight(0x6080c0, 0.18);
    rim.position.set(-3, 1, -2);
    scene.add(rim);

    const candle = new THREE.PointLight(0xFFD27F, 0, 9, 1.6);
    scene.add(candle);

    const flame = new THREE.Mesh(
      new THREE.SphereGeometry(0.08, 12, 12),
      new THREE.MeshBasicMaterial({ color: 0xFFE1A0, transparent: true, opacity: 0 })
    );
    scene.add(flame);

    const ground = new THREE.Mesh(
      new THREE.CircleGeometry(3.0, 64),
      new THREE.MeshBasicMaterial({
        color: 0xF4C430, transparent: true, opacity: 0.05,
      })
    );
    ground.rotation.x = -Math.PI / 2;
    ground.position.y = -2.1;
    scene.add(ground);

    const lantern = new THREE.Group();
    scene.add(lantern);
    const decoGroup = new THREE.Group();
    scene.add(decoGroup);

    stateRef.current = {
      scene, camera, renderer, lantern, decoGroup,
      candle, flame, amb, groundMesh: ground,
      blueprintMeshes: [],
      placedMeshes: {},
      jointMeshes: [],
      faceMeshes: [],
      faceFilled: {},
      hoverFace: -1,
      hoverEdge: -1,
      auraMesh: null,
      W, H,
      mount,
    };

    const ro = new ResizeObserver(() => {
      const w = mount.clientWidth, h = mount.clientHeight;
      camera.aspect = w / h;
      camera.updateProjectionMatrix();
      renderer.setSize(w, h);
      stateRef.current.W = w;
      stateRef.current.H = h;
    });
    ro.observe(mount);

    let isDragging = false, lastX = 0, lastY = 0;
    let rotY = 0.4, rotX = 0.15, targetRotY = 0.4, targetRotX = 0.15;
    let auto = true;
    const dom = renderer.domElement;
    dom.style.cursor = 'grab';
    dom.addEventListener('pointerdown', (e) => {
      if (e.button !== 0 && e.button !== 2) return;
      if (stateRef.current.draggingStick) return;
      isDragging = true; auto = false;
      lastX = e.clientX; lastY = e.clientY;
      dom.style.cursor = 'grabbing';
      dom.setPointerCapture(e.pointerId);
    });
    dom.addEventListener('pointerup', (e) => {
      isDragging = false; dom.style.cursor = 'grab';
    });
    dom.addEventListener('pointermove', (e) => {
      if (!isDragging) return;
      targetRotY += (e.clientX - lastX) * 0.008;
      targetRotX = Math.max(-0.7, Math.min(0.7, targetRotX + (e.clientY - lastY) * 0.005));
      lastX = e.clientX; lastY = e.clientY;
    });

    const clock = new THREE.Clock();
    let raf = 0;
    const tick = () => {
      raf = requestAnimationFrame(tick);
      const t = clock.getElapsedTime();
      const s = stateRef.current;
      if (auto && !isDragging && !s.draggingStick) targetRotY += 0.0025;
      rotY += (targetRotY - rotY) * 0.1;
      rotX += (targetRotX - rotX) * 0.1;
      lantern.rotation.y = rotY;
      lantern.rotation.x = rotX;
      decoGroup.rotation.y = rotY;
      decoGroup.rotation.x = rotX;
      if (s.candle.intensity > 0) {
        const flick = 1 + Math.sin(t * 18) * 0.08 + Math.sin(t * 7) * 0.04;
        s.candle.intensity = s.candleTarget * flick;
        s.flame.scale.setScalar(1 + Math.sin(t * 20) * 0.1);
      }
      if (s.hoverEdge >= 0 && s.blueprintMeshes[s.hoverEdge]) {
        const m = s.blueprintMeshes[s.hoverEdge];
        const k = 1.5 + Math.sin(t * 10) * 0.25;
        m.material.emissiveIntensity = k;
      }
      renderer.render(scene, camera);
    };
    tick();

    return () => {
      cancelAnimationFrame(raf);
      ro.disconnect();
      renderer.dispose();
      mount.removeChild(renderer.domElement);
    };
  }, []);

  // === Build blueprint when key changes ===
  useEffect(() => {
    const s = stateRef.current;
    if (!s.scene) return;
    while (s.lantern.children.length) {
      const c = s.lantern.children.pop();
      c.geometry?.dispose?.();
      if (Array.isArray(c.material)) c.material.forEach(m => m.dispose());
      else c.material?.dispose?.();
    }
    s.blueprintMeshes = [];
    s.jointMeshes = [];
    s.placedMeshes = {};
    s.faceMeshes = [];
    s.faceFilled = {};

    const bp = window.BLUEPRINTS[blueprintKey];
    if (!bp) return;

    const STICK_R = 0.045;

    const ghostMatProto = new THREE.MeshStandardMaterial({
      color: 0xC9A875,
      roughness: 0.9,
      transparent: true,
      opacity: 0.18,
      emissive: 0xD4A84B,
      emissiveIntensity: 0,
    });

    const jointMat = new THREE.MeshStandardMaterial({
      color: 0x6E4F31, roughness: 0.8, metalness: 0.02,
    });
    bp.vertices.forEach(v => {
      const sph = new THREE.Mesh(new THREE.SphereGeometry(STICK_R * 1.35, 10, 8), jointMat.clone());
      sph.position.set(v[0], v[1], v[2]);
      s.lantern.add(sph);
      s.jointMeshes.push(sph);
    });

    const up = new THREE.Vector3(0, 1, 0);
    bp.edges.forEach((e, i) => {
      const a = new THREE.Vector3(...bp.vertices[e[0]]);
      const b = new THREE.Vector3(...bp.vertices[e[1]]);
      const len = a.distanceTo(b);
      const g = new THREE.CylinderGeometry(STICK_R * 0.85, STICK_R * 0.85, len, 8);
      const m = ghostMatProto.clone();
      const mesh = new THREE.Mesh(g, m);
      mesh.position.copy(a).lerp(b, 0.5);
      const dir = new THREE.Vector3().subVectors(b, a).normalize();
      mesh.quaternion.setFromUnitVectors(up, dir);
      mesh.userData.edgeIdx = i;
      mesh.userData.a = a; mesh.userData.b = b;
      s.lantern.add(mesh);
      s.blueprintMeshes.push(mesh);
    });

    if (bp.faces) {
      bp.faces.forEach((face, fi) => {
        const verts = face.vertices.map(vi => new THREE.Vector3(...bp.vertices[vi]));
        const centroid = verts.reduce((acc, v) => acc.add(v), new THREE.Vector3()).divideScalar(verts.length);
        const e1 = new THREE.Vector3().subVectors(verts[1], verts[0]);
        const e2 = new THREE.Vector3().subVectors(verts[2], verts[0]);
        const normal = new THREE.Vector3().crossVectors(e1, e2).normalize();
        if (normal.dot(centroid) < 0) normal.multiplyScalar(-1);
        s.faceMeshes.push({
          verts,
          centroidLocal: centroid,
          normal,
          kind: face.kind,
          noPaper: !!face.noPaper,
        });
      });
    }
  }, [blueprintKey]);

  // === Apply placed edges ===
  useEffect(() => {
    const s = stateRef.current;
    if (!s.blueprintMeshes?.length) return;
    const bp = window.BLUEPRINTS[blueprintKey];
    if (!bp) return;

    const STICK_R = 0.045;
    const stickMat = new THREE.MeshStandardMaterial({
      color: 0x8B6B47, roughness: 0.75, metalness: 0.05,
    });

    bp.edges.forEach((e, i) => {
      const ghost = s.blueprintMeshes[i];
      const isPlaced = placedEdges.has(i);
      const wasPlaced = !!s.placedMeshes[i];

      if (isPlaced && !wasPlaced) {
        const a = new THREE.Vector3(...bp.vertices[e[0]]);
        const b = new THREE.Vector3(...bp.vertices[e[1]]);
        const len = a.distanceTo(b);
        const g = new THREE.CylinderGeometry(STICK_R, STICK_R, len, 12);
        const mesh = new THREE.Mesh(g, stickMat.clone());
        mesh.position.copy(a).lerp(b, 0.5);
        const dir = new THREE.Vector3().subVectors(b, a).normalize();
        const up = new THREE.Vector3(0, 1, 0);
        mesh.quaternion.setFromUnitVectors(up, dir);
        mesh.scale.set(1, 0.01, 1);
        s.lantern.add(mesh);
        s.placedMeshes[i] = mesh;
        const start = performance.now();
        const animPop = () => {
          const t = Math.min(1, (performance.now() - start) / 320);
          const ease = 1 - Math.pow(1 - t, 3);
          mesh.scale.set(1, ease, 1);
          if (t < 1) requestAnimationFrame(animPop);
        };
        animPop();
        ghost.material.opacity = 0.05;
      } else if (!isPlaced && wasPlaced) {
        s.lantern.remove(s.placedMeshes[i]);
        s.placedMeshes[i].geometry?.dispose?.();
        s.placedMeshes[i].material?.dispose?.();
        delete s.placedMeshes[i];
        ghost.material.opacity = 0.18;
      } else if (!isPlaced) {
        ghost.material.opacity = 0.18;
      }
    });
  }, [placedEdges, blueprintKey]);

  // === Hover edge while dragging ===
  useEffect(() => {
    const s = stateRef.current;
    if (!s.scene) return;
    s.draggingStick = dragging;

    if (!dragging) {
      if (s.hoverEdge >= 0 && s.blueprintMeshes[s.hoverEdge]) {
        const m = s.blueprintMeshes[s.hoverEdge];
        m.material.emissiveIntensity = 0;
        m.material.opacity = placedEdges.has(s.hoverEdge) ? 0.05 : 0.18;
      }
      s.hoverEdge = -1;
      return;
    }

    const onMove = (e) => {
      const rect = s.mount.getBoundingClientRect();
      const mx = e.clientX - rect.left;
      const my = e.clientY - rect.top;
      if (mx < 0 || my < 0 || mx > rect.width || my > rect.height) {
        if (s.hoverEdge >= 0 && s.blueprintMeshes[s.hoverEdge]) {
          const m = s.blueprintMeshes[s.hoverEdge];
          m.material.emissiveIntensity = 0;
          m.material.opacity = placedEdges.has(s.hoverEdge) ? 0.05 : 0.18;
        }
        s.hoverEdge = -1;
        return;
      }
      const bestIdx = computeNearestEdge(s, mx, my, placedEdges);
      if (bestIdx !== s.hoverEdge) {
        if (s.hoverEdge >= 0 && s.blueprintMeshes[s.hoverEdge]) {
          const prev = s.blueprintMeshes[s.hoverEdge];
          prev.material.emissiveIntensity = 0;
          prev.material.opacity = placedEdges.has(s.hoverEdge) ? 0.05 : 0.18;
        }
        s.hoverEdge = bestIdx;
        if (bestIdx >= 0) {
          const m = s.blueprintMeshes[bestIdx];
          m.material.opacity = 0.85;
          m.material.emissiveIntensity = 1.5;
        }
      }
    };

    const onUp = (e) => {
      const rect = s.mount.getBoundingClientRect();
      const inside =
        e.clientX >= rect.left && e.clientX <= rect.right &&
        e.clientY >= rect.top  && e.clientY <= rect.bottom;
      if (inside && s.hoverEdge >= 0) {
        onSnap(s.hoverEdge);
      } else {
        onSnap(-1);
      }
      if (s.hoverEdge >= 0 && s.blueprintMeshes[s.hoverEdge]) {
        const m = s.blueprintMeshes[s.hoverEdge];
        m.material.emissiveIntensity = 0;
        m.material.opacity = placedEdges.has(s.hoverEdge) ? 0.05 : 0.18;
      }
      s.hoverEdge = -1;
    };

    window.addEventListener('pointermove', onMove);
    window.addEventListener('pointerup', onUp);
    return () => {
      window.removeEventListener('pointermove', onMove);
      window.removeEventListener('pointerup', onUp);
    };
  }, [dragging, placedEdges, blueprintKey, onSnap]);

  // === Apply paper panels ===
  useEffect(() => {
    const s = stateRef.current;
    if (!s.faceMeshes?.length) return;

    s.faceMeshes.forEach((fdata, fi) => {
      if (fdata.noPaper) return;
      const panel = panels[fi];
      const existing = s.faceFilled[fi];
      const wantsPanel = !!panel;

      const sig = panel ? `${panel.color}|${panel.pattern}` : null;
      if (existing && existing.userData.sig === sig) return;

      if (existing) {
        s.lantern.remove(existing);
        existing.geometry?.dispose?.();
        existing.material?.map?.dispose?.();
        existing.material?.dispose?.();
        delete s.faceFilled[fi];
      }

      if (!wantsPanel) return;

      const verts = fdata.verts;
      let positions, uvs, indices;
      if (fdata.kind === 'tri') {
        positions = new Float32Array([
          verts[0].x, verts[0].y, verts[0].z,
          verts[1].x, verts[1].y, verts[1].z,
          verts[2].x, verts[2].y, verts[2].z,
        ]);
        uvs = new Float32Array([0.5, 1,  0, 0,  1, 0]);
        indices = [0, 1, 2];
      } else {
        positions = new Float32Array([
          verts[0].x, verts[0].y, verts[0].z,
          verts[1].x, verts[1].y, verts[1].z,
          verts[2].x, verts[2].y, verts[2].z,
          verts[3].x, verts[3].y, verts[3].z,
        ]);
        uvs = new Float32Array([0, 0,  1, 0,  1, 1,  0, 1]);
        indices = [0, 1, 2, 0, 2, 3];
      }
      const geo = new THREE.BufferGeometry();
      geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
      geo.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));
      geo.setIndex(indices);
      geo.computeVertexNormals();

      const shrinkPositions = new Float32Array(positions.length);
      for (let i = 0; i < positions.length; i += 3) {
        const p = new THREE.Vector3(positions[i], positions[i+1], positions[i+2]);
        const dir = new THREE.Vector3().subVectors(fdata.centroidLocal, p).multiplyScalar(0.08);
        p.add(dir);
        shrinkPositions[i] = p.x;
        shrinkPositions[i+1] = p.y;
        shrinkPositions[i+2] = p.z;
      }
      geo.setAttribute('position', new THREE.BufferAttribute(shrinkPositions, 3));
      geo.computeVertexNormals();

      const tex = makePaperTexture(panel.color, panel.pattern);
      const mat = new THREE.MeshStandardMaterial({
        map: tex,
        roughness: 0.9,
        transparent: true,
        opacity: 0.92,
        side: THREE.DoubleSide,
        emissive: new THREE.Color(panel.color),
        emissiveMap: tex,
        emissiveIntensity: lit ? 0.85 : 0,
        depthWrite: false,
      });
      const mesh = new THREE.Mesh(geo, mat);
      mesh.userData.sig = sig;
      mesh.userData.faceIdx = fi;
      mesh.scale.setScalar(0.01);
      s.lantern.add(mesh);
      s.faceFilled[fi] = mesh;
      const start = performance.now();
      const popIn = () => {
        const t = Math.min(1, (performance.now() - start) / 280);
        const ease = 1 - Math.pow(1 - t, 3);
        mesh.scale.setScalar(ease);
        if (t < 1) requestAnimationFrame(popIn);
      };
      popIn();
    });

    Object.keys(s.faceFilled).forEach(k => {
      if (!panels[k]) {
        const ex = s.faceFilled[k];
        s.lantern.remove(ex);
        ex.geometry?.dispose?.();
        ex.material?.map?.dispose?.();
        ex.material?.dispose?.();
        delete s.faceFilled[k];
      }
    });
  }, [panels, blueprintKey, lit]);

  // === Paper-drag hover/snap ===
  useEffect(() => {
    const s = stateRef.current;
    if (!s.scene) return;
    s.draggingPaper = !!paperDrag;

    if (!paperDrag) {
      if (s.hoverFace >= 0) s.hoverFace = -1;
      Object.values(s.faceHoverGhost || {}).forEach(g => g?.parent?.remove(g));
      s.faceHoverGhost = {};
      return;
    }

    s.faceHoverGhost = s.faceHoverGhost || {};

    const onMove = (e) => {
      const rect = s.mount.getBoundingClientRect();
      const mx = e.clientX - rect.left, my = e.clientY - rect.top;
      if (mx < 0 || my < 0 || mx > rect.width || my > rect.height) {
        if (s.hoverFace !== -1) {
          if (s.faceHoverGhost[s.hoverFace]) {
            s.lantern.remove(s.faceHoverGhost[s.hoverFace]);
            delete s.faceHoverGhost[s.hoverFace];
          }
          s.hoverFace = -1;
        }
        return;
      }
      const m4 = s.lantern.matrixWorld;
      const camForward = new THREE.Vector3();
      s.camera.getWorldDirection(camForward);
      let best = -1, bestD = 80;
      const tmp = new THREE.Vector3();
      s.faceMeshes.forEach((fd, fi) => {
        if (fd.noPaper) return;
        const worldNormal = fd.normal.clone().transformDirection(m4);
        if (worldNormal.dot(camForward) > -0.05) return;
        tmp.copy(fd.centroidLocal).applyMatrix4(m4).project(s.camera);
        const px = (tmp.x * 0.5 + 0.5) * s.W;
        const py = (-tmp.y * 0.5 + 0.5) * s.H;
        const d = Math.hypot(mx - px, my - py);
        if (d < bestD) { bestD = d; best = fi; }
      });
      if (best !== s.hoverFace) {
        if (s.faceHoverGhost[s.hoverFace]) {
          s.lantern.remove(s.faceHoverGhost[s.hoverFace]);
          delete s.faceHoverGhost[s.hoverFace];
        }
        s.hoverFace = best;
        if (best >= 0) {
          const fd = s.faceMeshes[best];
          const verts = fd.verts;
          let positions, uvs, indices;
          if (fd.kind === 'tri') {
            positions = new Float32Array([
              verts[0].x, verts[0].y, verts[0].z,
              verts[1].x, verts[1].y, verts[1].z,
              verts[2].x, verts[2].y, verts[2].z,
            ]);
            uvs = new Float32Array([0.5,1, 0,0, 1,0]);
            indices = [0,1,2];
          } else {
            positions = new Float32Array([
              verts[0].x, verts[0].y, verts[0].z,
              verts[1].x, verts[1].y, verts[1].z,
              verts[2].x, verts[2].y, verts[2].z,
              verts[3].x, verts[3].y, verts[3].z,
            ]);
            uvs = new Float32Array([0,0, 1,0, 1,1, 0,1]);
            indices = [0,1,2, 0,2,3];
          }
          const shrink = new Float32Array(positions.length);
          for (let i = 0; i < positions.length; i += 3) {
            const p = new THREE.Vector3(positions[i], positions[i+1], positions[i+2]);
            const dir = new THREE.Vector3().subVectors(fd.centroidLocal, p).multiplyScalar(0.08);
            p.add(dir);
            shrink[i] = p.x; shrink[i+1] = p.y; shrink[i+2] = p.z;
          }
          const geo = new THREE.BufferGeometry();
          geo.setAttribute('position', new THREE.BufferAttribute(shrink, 3));
          geo.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));
          geo.setIndex(indices);
          geo.computeVertexNormals();
          const mat = new THREE.MeshBasicMaterial({
            color: paperDrag.color,
            transparent: true,
            opacity: 0.6,
            side: THREE.DoubleSide,
            depthWrite: false,
          });
          const ghost = new THREE.Mesh(geo, mat);
          s.lantern.add(ghost);
          s.faceHoverGhost[best] = ghost;
        }
      }
    };

    const onUp = (e) => {
      const rect = s.mount.getBoundingClientRect();
      const inside =
        e.clientX >= rect.left && e.clientX <= rect.right &&
        e.clientY >= rect.top  && e.clientY <= rect.bottom;
      const fi = inside ? s.hoverFace : -1;
      Object.values(s.faceHoverGhost).forEach(g => g?.parent?.remove(g));
      s.faceHoverGhost = {};
      s.hoverFace = -1;
      onPaperSnap(fi);
    };

    window.addEventListener('pointermove', onMove);
    window.addEventListener('pointerup', onUp);
    return () => {
      window.removeEventListener('pointermove', onMove);
      window.removeEventListener('pointerup', onUp);
    };
  }, [paperDrag, blueprintKey, onPaperSnap]);

  // === Atapattama frame decorations (white paper ribbons + middle paper fringe) ===
  useEffect(() => {
    const s = stateRef.current;
    if (!s.lantern) return;

    if (s.frameDecoGroup) {
      s.lantern.remove(s.frameDecoGroup);
      s.frameDecoGroup.traverse(c => {
        c.geometry?.dispose?.();
        c.material?.dispose?.();
      });
    }
    s.frameDecoGroup = new THREE.Group();
    s.lantern.add(s.frameDecoGroup);

    if (blueprintKey !== 'atapattama' || !showRibbons) return;

    const bp = window.BLUEPRINTS.atapattama;
    const ATA_R = 1.35;

    const makeRibbonGeometry = (points, widthAxis, width) => {
      const N = points.length;
      const positions = new Float32Array(N * 2 * 3);
      const uvs = new Float32Array(N * 2 * 2);
      const w = widthAxis.clone().normalize().multiplyScalar(width / 2);
      for (let i = 0; i < N; i++) {
        const a = points[i].clone().add(w);
        const b = points[i].clone().sub(w);
        positions[i*6 + 0] = a.x; positions[i*6 + 1] = a.y; positions[i*6 + 2] = a.z;
        positions[i*6 + 3] = b.x; positions[i*6 + 4] = b.y; positions[i*6 + 5] = b.z;
        uvs[i*4 + 0] = 0; uvs[i*4 + 1] = i / (N-1);
        uvs[i*4 + 2] = 1; uvs[i*4 + 3] = i / (N-1);
      }
      const indices = [];
      for (let i = 0; i < N - 1; i++) {
        const o = i * 2;
        indices.push(o, o+1, o+2,  o+1, o+3, o+2);
      }
      const geo = new THREE.BufferGeometry();
      geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
      geo.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));
      geo.setIndex(indices);
      geo.computeVertexNormals();
      return geo;
    };

    const paperMat = new THREE.MeshStandardMaterial({
      color: ribbonColor,
      emissive: ribbonColor,
      emissiveIntensity: lit ? 0.9 : 0.45,
      roughness: 0.95,
      transparent: true,
      opacity: 0.96,
      side: THREE.DoubleSide,
    });
    const paperMatFringe = new THREE.MeshStandardMaterial({
      color: ribbonColor,
      emissive: ribbonColor,
      emissiveIntensity: lit ? 0.8 : 0.4,
      roughness: 0.95,
      transparent: true,
      opacity: 0.94,
      side: THREE.DoubleSide,
    });
    s.atapattamaPaperMats = [paperMat, paperMatFringe];

    const bottomEdgeIndices = [];
    bp.edges.forEach((e, i) => {
      const a = bp.vertices[e[0]], b = bp.vertices[e[1]];
      if (Math.abs(a[1] + ATA_R) < 0.01 && Math.abs(b[1] + ATA_R) < 0.01) {
        bottomEdgeIndices.push(i);
      }
    });

    bottomEdgeIndices.forEach(ei => {
      if (!placedEdges.has(ei)) return;
      const e = bp.edges[ei];
      const A = new THREE.Vector3(...bp.vertices[e[0]]);
      const B = new THREE.Vector3(...bp.vertices[e[1]]);
      const edgeDir = new THREE.Vector3().subVectors(B, A).normalize();
      const STRANDS = 11;
      const RIBBON_W = 0.22;
      for (let k = 0; k < STRANDS; k++) {
        const t = (k + 0.5) / STRANDS;
        const start = new THREE.Vector3().lerpVectors(A, B, t);
        const len = 3.6 + Math.random() * 0.7;
        const pts = [];
        const N = 18;
        const phase = Math.random() * Math.PI * 2;
        const ampX = 0.09 + Math.random() * 0.07;
        const ampZ = 0.07 + Math.random() * 0.06;
        for (let i = 0; i <= N; i++) {
          const u = i / N;
          const y = start.y - u * len;
          const waveX = Math.sin(u * Math.PI * 2.4 + phase) * ampX * u;
          const waveZ = Math.cos(u * Math.PI * 1.7 + phase) * ampZ * u;
          pts.push(new THREE.Vector3(start.x + waveX, y, start.z + waveZ));
        }
        const geo = makeRibbonGeometry(pts, edgeDir, RIBBON_W);
        s.frameDecoGroup.add(new THREE.Mesh(geo, paperMat));
      }
    });

    const middleVerts = [];
    bp.vertices.forEach((v, i) => {
      if (Math.abs(v[1]) < 0.01) middleVerts.push(i);
    });

    middleVerts.forEach(vi => {
      const hasAdjPlaced = bp.edges.some((e, ei) =>
        (e[0] === vi || e[1] === vi) && placedEdges.has(ei)
      );
      if (!hasAdjPlaced) return;
      const p = new THREE.Vector3(...bp.vertices[vi]);
      const outward = new THREE.Vector3(p.x, 0, p.z);
      if (outward.lengthSq() < 0.001) outward.set(1, 0, 0);
      outward.normalize();
      const tangent = new THREE.Vector3(-outward.z, 0, outward.x);

      const STR = 8;
      const FRINGE_W = 0.085;
      const FIXED_LEN = 2.1;
      for (let k = 0; k < STR; k++) {
        // all strands begin at the same anchor point (the vertex); waves grow toward the tip
        const start = p.clone().add(outward.clone().multiplyScalar(0.06));
        const len = FIXED_LEN;
        const pts = [];
        const N = 16;
        const fanDir = (STR === 1) ? 0 : (k / (STR - 1) - 0.5);
        const swayT = fanDir * 0.22;
        const phase = (k / STR) * Math.PI * 2;
        const ampT = 0.07 + (k % 3) * 0.015; // wave amplitude along tangent
        const ampO = 0.05 + (k % 2) * 0.02;  // wave amplitude along outward
        for (let i = 0; i <= N; i++) {
          const u = i / N;
          const y = start.y - u * len;
          // sine wave grows with u so the anchor stays at one point
          const waveT = Math.sin(u * Math.PI * 2.4 + phase) * ampT * u;
          const waveO = Math.cos(u * Math.PI * 1.7 + phase) * ampO * u;
          const tangentOff = swayT * u + waveT;
          const outwardOff = waveO;
          pts.push(new THREE.Vector3(
            start.x + tangent.x * tangentOff + outward.x * outwardOff,
            y,
            start.z + tangent.z * tangentOff + outward.z * outwardOff,
          ));
        }
        const geo = makeRibbonGeometry(pts, tangent, FRINGE_W);
        s.frameDecoGroup.add(new THREE.Mesh(geo, paperMatFringe));
      }
    });
  }, [blueprintKey, placedEdges, ribbonColor, lit, showRibbons]);

  // === Light state ===
  useEffect(() => {
    const s = stateRef.current;
    if (!s.candle) return;
    s.candle.color.setHex(glowColor);
    s.flame.material.color.setHex(glowColor);
    s.candleTarget = lit ? 4.0 : 0;
    s.candle.intensity = lit ? 4.0 : 0;
    s.flame.material.opacity = lit ? 0.95 : 0;
    s.amb.intensity = lit ? 0.22 : 0.18;
    if (s.groundMesh) {
      s.groundMesh.material.color.setHex(glowColor);
      s.groundMesh.material.opacity = lit ? 0.18 : 0.05;
    }
    if (s.atapattamaPaperMats) {
      s.atapattamaPaperMats.forEach(m => {
        m.emissiveIntensity = lit ? 0.9 : 0.45;
      });
    }
    Object.values(s.faceFilled || {}).forEach(m => {
      if (m.material) m.material.emissiveIntensity = lit ? 1.1 : 0;
    });
  }, [lit, glowColor]);

  // === Decorations (tassels) ===
  useEffect(() => {
    const s = stateRef.current;
    if (!s.decoGroup) return;
    while (s.decoGroup.children.length) {
      const c = s.decoGroup.children.pop();
      c.geometry?.dispose?.();
      c.material?.dispose?.();
    }
    if (tasselCount > 0) {
      const bp = window.BLUEPRINTS[blueprintKey];
      let lowY = Infinity, lowV = [0,0,0];
      bp.vertices.forEach(v => { if (v[1] < lowY) { lowY = v[1]; lowV = v; } });
      const N = 12 * tasselCount;
      for (let k = 0; k < N; k++) {
        const a = (k / N) * Math.PI * 2;
        const r = 0.18 + Math.random() * 0.1;
        const len = 0.9 + Math.random() * 0.3;
        const g = new THREE.BufferGeometry().setFromPoints([
          new THREE.Vector3(lowV[0] + Math.cos(a) * 0.03, lowV[1] - 0.02, lowV[2] + Math.sin(a) * 0.03),
          new THREE.Vector3(lowV[0] + Math.cos(a) * r, lowV[1] - len, lowV[2] + Math.sin(a) * r),
        ]);
        s.decoGroup.add(new THREE.Line(g, new THREE.LineBasicMaterial({
          color: 0xE8B043, transparent: true, opacity: 0.9,
        })));
      }
    }
  }, [tasselCount, blueprintKey]);

  return <div ref={mountRef} className="w-full h-full" />;
};

function computeNearestEdge(s, mx, my, placedEdges) {
  const cam = s.camera;
  const W = s.W, H = s.H;
  let bestIdx = -1;
  let bestDist = 60;
  const tmpA = new THREE.Vector3();
  const tmpB = new THREE.Vector3();
  const m4 = s.lantern.matrixWorld;
  s.blueprintMeshes.forEach((mesh, i) => {
    if (placedEdges.has(i)) return;
    const a = mesh.userData.a, b = mesh.userData.b;
    tmpA.copy(a).applyMatrix4(m4).project(cam);
    tmpB.copy(b).applyMatrix4(m4).project(cam);
    const ax = (tmpA.x * 0.5 + 0.5) * W;
    const ay = (-tmpA.y * 0.5 + 0.5) * H;
    const bx = (tmpB.x * 0.5 + 0.5) * W;
    const by = (-tmpB.y * 0.5 + 0.5) * H;
    const dx = bx - ax, dy = by - ay;
    const len2 = dx*dx + dy*dy;
    let t = ((mx - ax)*dx + (my - ay)*dy) / len2;
    t = Math.max(0, Math.min(1, t));
    const px = ax + t * dx, py = ay + t * dy;
    const d = Math.hypot(mx - px, my - py);
    if (d < bestDist) {
      bestDist = d;
      bestIdx = i;
    }
  });
  return bestIdx;
}
