/* ========================================================================
   LUMINA — Workspace: 3D Scene (Three.js)
   Imported models · Niagara particles · audio-reactive primitives · splats
   Scene Outliner left · Viewport center · Inspector right
   ======================================================================== */
const { useState: useState3, useEffect: useEffect3, useRef: useRef3, useCallback: useCallback3 } = React;

const SUPPORTED_FORMATS = [".glb", ".gltf", ".obj", ".ply", ".stl"];

const SAMPLE_SHAPES = [
  { id: "cube",    label: "Cube",    icon: "■" },
  { id: "sphere",  label: "Sphere",  icon: "●" },
  { id: "torus",   label: "Torus",   icon: "◯" },
  { id: "torusknot", label: "Knot",  icon: "∞" },
  { id: "cone",    label: "Cone",    icon: "▲" },
  { id: "icosa",   label: "Icosa",   icon: "◆" },
  { id: "dodeca",  label: "Dodeca",  icon: "⬡" },
];

const ENVIRONMENTS = [
  { id: "none",       label: "No Environment", desc: "Empty stage — just your models" },
  { id: "niagara",    label: "Niagara FX",     desc: "4000 GPU particles" },
  { id: "primitives", label: "Primitives",     desc: "5 audio-reactive shapes" },
  { id: "pointcloud", label: "Point Cloud",    desc: "12 000 points environment" },
  { id: "splat",      label: "Gaussian Splat", desc: "Fuzzy splat field" },
  { id: "bars3d",     label: "Bars 3D",        desc: "Reactive spectrum bar grid" },
  { id: "helix",      label: "DNA Helix",      desc: "Twin audio-driven helix" },
  { id: "tunnel3d",   label: "Tunnel",         desc: "Flying ring corridor" },
];

function Scene3D({ params, audio }) {
  const mountRef = useRef3(null);
  const fileInputRef = useRef3(null);
  const objectsRef = useRef3({});
  const paramsRef = useRef3(params); paramsRef.current = params;
  const audioRef = useRef3(audio); audioRef.current = audio;

  const [models, setModels] = useState3([]); // {id, name, kind, color, visible, scale, rotY, audioReactive, audioBand, _obj}
  const modelsRef = useRef3(models); modelsRef.current = models;
  const [selectedId, setSelectedId] = useState3(null);
  const [environment, setEnvironment] = useState3("none");
  const envRef = useRef3(environment); envRef.current = environment;
  const [orbit, setOrbit] = useState3(true);
  const orbitRef = useRef3(orbit); orbitRef.current = orbit;
  const [grid, setGrid] = useState3(true);
  const [dragOver, setDragOver] = useState3(false);
  const [busy, setBusy] = useState3(null);

  let _mid = useRef3(0);
  const newId = () => "m" + (++_mid.current);

  /* ====== Three.js setup ====== */
  useEffect3(() => {
    if (!window.THREE || !mountRef.current) return;
    const THREE = window.THREE;
    const mount = mountRef.current;

    const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.setSize(mount.clientWidth, mount.clientHeight);
    renderer.setClearColor(0x06060a, 1);
    mount.appendChild(renderer.domElement);

    const scene = new THREE.Scene();
    scene.fog = new THREE.FogExp2(0x06060a, 0.04);

    const camera = new THREE.PerspectiveCamera(60, mount.clientWidth / mount.clientHeight, 0.1, 500);
    camera.position.set(0, 4, 14);

    // Lights
    const amb = new THREE.AmbientLight(0xffffff, 0.45);
    scene.add(amb);
    const key = new THREE.PointLight(0xff7a1a, 4, 60);
    key.position.set(8, 10, 6); scene.add(key);
    const fill = new THREE.PointLight(0x4be6ff, 3, 60);
    fill.position.set(-8, 6, -6); scene.add(fill);
    const dir = new THREE.DirectionalLight(0xffffff, 0.5);
    dir.position.set(5, 10, 5); scene.add(dir);

    // Grid
    const gridHelp = new THREE.GridHelper(40, 40, 0x5a5a64, 0x2a2a30);
    gridHelp.position.y = -2;
    scene.add(gridHelp);

    // === Environment containers ===
    const envGroup = new THREE.Group(); scene.add(envGroup);

    // Primitives env
    const primGroup = new THREE.Group();
    const primMats = [];
    const primGeoms = [
      new THREE.IcosahedronGeometry(1, 1),
      new THREE.TorusKnotGeometry(0.7, 0.22, 80, 12),
      new THREE.BoxGeometry(1.2, 1.2, 1.2),
      new THREE.OctahedronGeometry(1, 0),
      new THREE.TorusGeometry(0.9, 0.25, 12, 32),
    ];
    primGeoms.forEach((g, i) => {
      const mat = new THREE.MeshStandardMaterial({
        color: new THREE.Color().setHSL(i / 5, 0.75, 0.55),
        metalness: 0.6, roughness: 0.2,
        emissive: new THREE.Color().setHSL(i / 5, 0.9, 0.3), emissiveIntensity: 0.5,
        wireframe: i === 1 || i === 4,
      });
      primMats.push(mat);
      const m = new THREE.Mesh(g, mat);
      const a = (i / primGeoms.length) * Math.PI * 2;
      m.position.set(Math.cos(a) * 3.5, 1, Math.sin(a) * 3.5);
      primGroup.add(m);
    });
    envGroup.add(primGroup);

    // Niagara particles
    const NPART = 3000;
    const partGeom = new THREE.BufferGeometry();
    const pPos = new Float32Array(NPART * 3);
    const pVel = new Float32Array(NPART * 3);
    const pCol = new Float32Array(NPART * 3);
    const pLife = new Float32Array(NPART);
    for (let i = 0; i < NPART; i++) {
      pLife[i] = Math.random();
      pPos[i*3] = (Math.random() - 0.5) * 0.4;
      pPos[i*3+1] = (Math.random() - 0.5) * 0.4;
      pPos[i*3+2] = (Math.random() - 0.5) * 0.4;
      pVel[i*3] = (Math.random() - 0.5) * 0.04;
      pVel[i*3+1] = Math.random() * 0.08;
      pVel[i*3+2] = (Math.random() - 0.5) * 0.04;
      const c = new THREE.Color().setHSL(Math.random(), 1, 0.6);
      pCol[i*3] = c.r; pCol[i*3+1] = c.g; pCol[i*3+2] = c.b;
    }
    partGeom.setAttribute("position", new THREE.BufferAttribute(pPos, 3));
    partGeom.setAttribute("color", new THREE.BufferAttribute(pCol, 3));
    const partMat = new THREE.PointsMaterial({
      size: 0.12, vertexColors: true, transparent: true, opacity: 0.9,
      blending: THREE.AdditiveBlending, depthWrite: false, sizeAttenuation: true,
    });
    const points = new THREE.Points(partGeom, partMat);
    envGroup.add(points);

    // Point cloud
    const CLOUD = 10000;
    const cloudGeom = new THREE.BufferGeometry();
    const cPos = new Float32Array(CLOUD * 3);
    const cCol = new Float32Array(CLOUD * 3);
    for (let i = 0; i < CLOUD; i++) {
      const ang = Math.random() * Math.PI * 2;
      const r = 6 + Math.cos(ang*3) * 2;
      cPos[i*3] = Math.cos(ang) * r + (Math.random() - 0.5) * 0.6;
      cPos[i*3+1] = (Math.random() - 0.5) * 6 + Math.sin(ang*2) * 1.5;
      cPos[i*3+2] = Math.sin(ang) * r + (Math.random() - 0.5) * 0.6;
      const c = new THREE.Color().setHSL(ang / (Math.PI*2), 0.8, 0.55);
      cCol[i*3] = c.r; cCol[i*3+1] = c.g; cCol[i*3+2] = c.b;
    }
    cloudGeom.setAttribute("position", new THREE.BufferAttribute(cPos, 3));
    cloudGeom.setAttribute("color", new THREE.BufferAttribute(cCol, 3));
    const cloudMat = new THREE.PointsMaterial({ size: 0.06, vertexColors: true, transparent: true, opacity: 0.85, blending: THREE.AdditiveBlending, depthWrite: false });
    const cloud = new THREE.Points(cloudGeom, cloudMat);
    envGroup.add(cloud);

    // Splat field
    const splatTex = (() => {
      const c = document.createElement("canvas"); c.width = c.height = 64;
      const cx = c.getContext("2d");
      const g = cx.createRadialGradient(32, 32, 0, 32, 32, 32);
      g.addColorStop(0, "rgba(255,255,255,1)");
      g.addColorStop(0.5, "rgba(255,255,255,0.4)");
      g.addColorStop(1, "rgba(255,255,255,0)");
      cx.fillStyle = g; cx.fillRect(0, 0, 64, 64);
      return new THREE.CanvasTexture(c);
    })();
    const SPLATS = 500;
    const splatGeom = new THREE.BufferGeometry();
    const sPos = new Float32Array(SPLATS * 3);
    const sCol = new Float32Array(SPLATS * 3);
    for (let i = 0; i < SPLATS; i++) {
      sPos[i*3] = (Math.random() - 0.5) * 14;
      sPos[i*3+1] = (Math.random() - 0.5) * 6;
      sPos[i*3+2] = (Math.random() - 0.5) * 14;
      const c = new THREE.Color().setHSL(Math.random(), 0.8, 0.65);
      sCol[i*3] = c.r; sCol[i*3+1] = c.g; sCol[i*3+2] = c.b;
    }
    splatGeom.setAttribute("position", new THREE.BufferAttribute(sPos, 3));
    splatGeom.setAttribute("color", new THREE.BufferAttribute(sCol, 3));
    const splatMat = new THREE.PointsMaterial({ size: 1.2, vertexColors: true, transparent: true, opacity: 0.45, map: splatTex, blending: THREE.AdditiveBlending, depthWrite: false, alphaTest: 0.01 });
    const splats = new THREE.Points(splatGeom, splatMat);
    envGroup.add(splats);

    // Bars 3D — radial grid of reactive bars
    const BARS = 64;
    const barsGroup = new THREE.Group();
    const barMats = [];
    for (let i = 0; i < BARS; i++) {
      const g = new THREE.BoxGeometry(0.5, 1, 0.5);
      g.translate(0, 0.5, 0); // pivot at base
      const mat = new THREE.MeshStandardMaterial({
        color: new THREE.Color().setHSL(i / BARS, 0.8, 0.55),
        emissive: new THREE.Color().setHSL(i / BARS, 0.9, 0.4), emissiveIntensity: 0.6,
        metalness: 0.5, roughness: 0.3,
      });
      barMats.push(mat);
      const mesh = new THREE.Mesh(g, mat);
      const cols = 8, cx = i % cols, cz = Math.floor(i / cols);
      mesh.position.set((cx - 3.5) * 1.4, -2, (cz - 3.5) * 1.4);
      barsGroup.add(mesh);
    }
    envGroup.add(barsGroup);

    // DNA Helix — twin strands of spheres + rungs
    const helixGroup = new THREE.Group();
    const helixMats = [];
    const HN = 60;
    for (let i = 0; i < HN; i++) {
      const y = (i / HN - 0.5) * 12;
      const a = i * 0.5;
      for (let s = 0; s < 2; s++) {
        const ang = a + s * Math.PI;
        const sph = new THREE.Mesh(
          new THREE.SphereGeometry(0.22, 12, 8),
          (() => { const m = new THREE.MeshStandardMaterial({
            color: new THREE.Color().setHSL(s ? 0.55 : 0.08, 0.9, 0.55),
            emissive: new THREE.Color().setHSL(s ? 0.55 : 0.08, 0.9, 0.4), emissiveIntensity: 0.7,
          }); helixMats.push(m); return m; })()
        );
        sph.userData = { i, s, y, ang };
        helixGroup.add(sph);
      }
    }
    envGroup.add(helixGroup);

    // Tunnel 3D — corridor of rings flying toward camera
    const tunnelGroup = new THREE.Group();
    const tunMats = [];
    const TRINGS = 40;
    for (let i = 0; i < TRINGS; i++) {
      const g = new THREE.TorusGeometry(3, 0.12, 8, 40);
      const mat = new THREE.MeshStandardMaterial({
        color: new THREE.Color().setHSL(i / TRINGS, 0.9, 0.55),
        emissive: new THREE.Color().setHSL(i / TRINGS, 1, 0.5), emissiveIntensity: 1.2,
        metalness: 0.4, roughness: 0.4,
      });
      tunMats.push(mat);
      const ring = new THREE.Mesh(g, mat);
      ring.position.z = -i * 2;
      ring.userData = { baseI: i };
      tunnelGroup.add(ring);
    }
    envGroup.add(tunnelGroup);

    // === Imported models group ===
    const modelsGroup = new THREE.Group();
    scene.add(modelsGroup);

    Object.assign(objectsRef.current, {
      renderer, scene, camera, key, fill, gridHelp,
      envGroup, primGroup, primMats, points, partGeom, partMat, pVel, pLife,
      cloud, cloudMat, splats, splatMat,
      modelsGroup, THREE,
      barsGroup, barMats, helixGroup, helixMats, tunnelGroup, tunMats,
    });

    // Manual orbit controls (built-in, no extra deps)
    let dragging = false, lastX = 0, lastY = 0;
    let yaw = 0, pitch = 0.3, radius = 14, autoYaw = 0;
    const onDown = e => { dragging = true; lastX = e.clientX; lastY = e.clientY; };
    const onMove = e => {
      if (!dragging) return;
      yaw += (e.clientX - lastX) * 0.005;
      pitch = Math.max(-1.2, Math.min(1.2, pitch + (e.clientY - lastY) * 0.005));
      lastX = e.clientX; lastY = e.clientY;
    };
    const onUp = () => dragging = false;
    const onWheel = e => {
      e.preventDefault();
      radius = Math.max(2, Math.min(80, radius + e.deltaY * 0.015));
    };
    renderer.domElement.addEventListener("mousedown", onDown);
    window.addEventListener("mousemove", onMove);
    window.addEventListener("mouseup", onUp);
    renderer.domElement.addEventListener("wheel", onWheel, { passive: false });

    let rafId;
    const tick = (tNow) => {
      const ms = tNow / 1000;
      const A = audioRef.current || {};
      const P = paramsRef.current || {};
      const env = envRef.current;

      if (orbitRef.current) autoYaw += 0.003 + (A.energy || 0) * 0.01;
      const cy = yaw + autoYaw;
      camera.position.set(
        Math.cos(cy) * Math.cos(pitch) * radius,
        Math.sin(pitch) * radius + 1.5,
        Math.sin(cy) * Math.cos(pitch) * radius
      );
      camera.lookAt(0, 0, 0);

      primGroup.visible = env === "primitives";
      points.visible = env === "niagara";
      cloud.visible = env === "pointcloud";
      splats.visible = env === "splat";
      barsGroup.visible = env === "bars3d";
      helixGroup.visible = env === "helix";
      tunnelGroup.visible = env === "tunnel3d";

      // Bars 3D
      if (barsGroup.visible) {
        const fr = A.freq || [];
        barsGroup.rotation.y += 0.002 + (A.energy || 0) * 0.01;
        barsGroup.children.forEach((bar, i) => {
          const fi = Math.floor((i / BARS) * (fr.length ? fr.length * 0.6 : 1));
          const v = fr.length ? fr[fi] / 255 : (A.energy || 0);
          const hgt = 0.4 + v * 10 * (P.intensity || 1);
          bar.scale.y = hgt;
          barMats[i].emissiveIntensity = 0.3 + v * 2.5;
          barMats[i].color.setHSL(((((P.hue || 0) + i * 4) % 360)) / 360, 0.85, 0.5 + v * 0.2);
        });
      }

      // DNA Helix
      if (helixGroup.visible) {
        helixGroup.rotation.y = ms * (0.4 + (A.mid || 0) * 1.5) * (P.motion || 1);
        const rad = 2.2 + (A.bass || 0) * 1.4 * (P.intensity || 1);
        helixGroup.children.forEach((sph, i) => {
          const u = sph.userData;
          sph.position.set(Math.cos(u.ang) * rad, u.y, Math.sin(u.ang) * rad);
          const sc = 1 + (A.high || 0) * 1.2;
          sph.scale.setScalar(sc);
          helixMats[i].emissiveIntensity = 0.5 + (A.energy || 0) * 2;
        });
      }

      // Tunnel 3D
      if (tunnelGroup.visible) {
        const speed = (0.06 + (A.energy || 0) * 0.4) * (P.motion || 1);
        tunnelGroup.children.forEach((ring, i) => {
          ring.position.z += speed;
          if (ring.position.z > 8) ring.position.z -= TRINGS * 2;
          const depth = (ring.position.z + TRINGS * 2) / (TRINGS * 2);
          const wob = 1 + (A.bass || 0) * 0.4 * (P.intensity || 1);
          ring.scale.setScalar(wob);
          ring.rotation.z = ms * 0.3 + i * 0.2;
          tunMats[i].emissiveIntensity = 0.8 + (A.high || 0) * 2.5;
          tunMats[i].color.setHSL(((((P.hue || 0) + i * 9 + ms * 30) % 360)) / 360, 0.95, 0.55);
        });
      }

      // Primitives
      if (primGroup.visible) {
        primGroup.rotation.y += 0.004 + (A.bass || 0) * 0.04 * (P.motion || 1);
        primGroup.children.forEach((mesh, i) => {
          const s = 1 + (A.bass || 0) * 1.2 * (P.intensity || 1) * (i % 2 ? 1 : 0.6);
          mesh.scale.setScalar(s);
          mesh.rotation.x += 0.01 + (A.mid || 0) * 0.05;
          mesh.rotation.y += 0.012;
          primMats[i].emissiveIntensity = 0.4 + (A.high || 0) * 2;
          primMats[i].color.setHSL((((P.hue || 0) + i * 60) % 360) / 360, 0.8, 0.55);
        });
      }

      // Niagara
      if (points.visible) {
        const burst = (A.beat || 0) > 0.5;
        for (let i = 0; i < NPART; i++) {
          pPos[i*3]   += pVel[i*3];
          pPos[i*3+1] += pVel[i*3+1];
          pPos[i*3+2] += pVel[i*3+2];
          pVel[i*3+1] -= 0.001;
          pLife[i] -= 0.005 + (A.high || 0) * 0.01;
          if (pLife[i] <= 0 || pPos[i*3+1] < -3) {
            pLife[i] = 1;
            pPos[i*3] = (Math.random() - 0.5) * 0.6;
            pPos[i*3+1] = 0;
            pPos[i*3+2] = (Math.random() - 0.5) * 0.6;
            const speed = (0.04 + Math.random() * 0.06) * (1 + (A.bass || 0) * 5 * (P.intensity || 1));
            const ang = Math.random() * Math.PI * 2;
            pVel[i*3]   = Math.cos(ang) * speed * (burst ? 4 : 1);
            pVel[i*3+1] = (0.05 + Math.random() * 0.15) * (1 + (A.bass || 0) * 3);
            pVel[i*3+2] = Math.sin(ang) * speed * (burst ? 4 : 1);
          }
        }
        partGeom.attributes.position.needsUpdate = true;
        partMat.size = 0.08 + (A.energy || 0) * 0.4;
      }

      // Cloud
      if (cloud.visible) {
        cloud.rotation.y += 0.001 + (A.energy || 0) * 0.01;
        const sc = 1 + (A.bass || 0) * 0.18 * (P.intensity || 1);
        cloud.scale.setScalar(sc);
        cloudMat.size = 0.04 + (A.high || 0) * 0.2;
      }

      // Splats
      if (splats.visible) {
        splats.rotation.y -= 0.002 + (A.mid || 0) * 0.008;
        splatMat.size = 0.8 + (A.bass || 0) * 1.6 * (P.intensity || 1);
        splatMat.opacity = 0.3 + (A.energy || 0) * 0.5;
      }

      // Imported models — animate based on per-model audio reactivity
      const mList = modelsRef.current;
      modelsGroup.children.forEach((obj, i) => {
        const m = mList.find(mm => mm._objId === obj.userData._objId);
        if (!m) return;
        obj.visible = m.visible;
        obj.scale.setScalar(m.scale * (1 + (m.audioReactive ? (A[m.audioBand] || 0) * 0.5 : 0)));
        obj.rotation.y = (m.rotY || 0) * Math.PI / 180 + (m.spin ? ms * 0.3 : 0);
        obj.position.x = m.x || 0;
        obj.position.y = (m.y || 0);
        obj.position.z = (m.z || 0);
      });

      key.intensity = 3 + (A.bass || 0) * 12;
      fill.intensity = 2 + (A.high || 0) * 8;
      gridHelp.visible = grid;

      renderer.render(scene, camera);
      rafId = requestAnimationFrame(tick);
    };
    rafId = requestAnimationFrame(tick);

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

    return () => {
      cancelAnimationFrame(rafId);
      ro.disconnect();
      renderer.domElement.removeEventListener("mousedown", onDown);
      window.removeEventListener("mousemove", onMove);
      window.removeEventListener("mouseup", onUp);
      renderer.domElement.removeEventListener("wheel", onWheel);
      renderer.dispose();
      try { mount.removeChild(renderer.domElement); } catch (e) {}
    };
  }, []);

  // Update grid visibility
  useEffect3(() => {
    if (objectsRef.current.gridHelp) objectsRef.current.gridHelp.visible = grid;
  }, [grid]);

  /* ====== Model loaders ====== */
  const fitModel = (obj) => {
    const THREE = window.THREE;
    const box = new THREE.Box3().setFromObject(obj);
    const size = box.getSize(new THREE.Vector3());
    const center = box.getCenter(new THREE.Vector3());
    const maxDim = Math.max(size.x, size.y, size.z, 0.001);
    const s = 4 / maxDim;
    obj.scale.setScalar(s);
    obj.position.sub(center.multiplyScalar(s));
  };

  const addToScene = (obj, name, kind) => {
    const id = newId();
    const objId = id;
    obj.userData._objId = objId;
    fitModel(obj);
    // place models on a small grid so they don't overlap
    const i = modelsRef.current.length;
    const r = 4;
    const ang = (i / 6) * Math.PI * 2;
    obj.position.x = Math.cos(ang) * (i > 0 ? r : 0);
    obj.position.z = Math.sin(ang) * (i > 0 ? r : 0);
    objectsRef.current.modelsGroup.add(obj);
    const newModel = {
      id, _objId: objId, name, kind,
      visible: true, scale: 1, rotY: 0,
      x: obj.position.x, y: obj.position.y, z: obj.position.z,
      audioReactive: true, audioBand: "bass", spin: false,
    };
    setModels(arr => [...arr, newModel]);
    setSelectedId(id);
    setBusy(null);
  };

  const addShape = (kind) => {
    const THREE = window.THREE;
    let geom;
    if (kind === "cube") geom = new THREE.BoxGeometry(1, 1, 1);
    else if (kind === "sphere") geom = new THREE.SphereGeometry(0.7, 32, 24);
    else if (kind === "torus") geom = new THREE.TorusGeometry(0.7, 0.25, 16, 60);
    else if (kind === "torusknot") geom = new THREE.TorusKnotGeometry(0.7, 0.22, 80, 12);
    else if (kind === "cone") geom = new THREE.ConeGeometry(0.7, 1.4, 32);
    else if (kind === "icosa") geom = new THREE.IcosahedronGeometry(1, 0);
    else if (kind === "dodeca") geom = new THREE.DodecahedronGeometry(1, 0);
    const hue = Math.random();
    const mat = new THREE.MeshStandardMaterial({
      color: new THREE.Color().setHSL(hue, 0.7, 0.55),
      metalness: 0.4, roughness: 0.3,
      emissive: new THREE.Color().setHSL(hue, 0.9, 0.3), emissiveIntensity: 0.4,
    });
    const mesh = new THREE.Mesh(geom, mat);
    addToScene(mesh, kind.charAt(0).toUpperCase() + kind.slice(1), "shape");
  };

  const loadFile = async (file) => {
    const THREE = window.THREE;
    const ext = file.name.split(".").pop().toLowerCase();
    setBusy(file.name);
    const url = URL.createObjectURL(file);
    try {
      if (ext === "glb" || ext === "gltf") {
        if (!THREE.GLTFLoader) throw new Error("GLTFLoader not loaded");
        new THREE.GLTFLoader().load(url, (g) => {
          addToScene(g.scene, file.name, "glb");
        }, undefined, (err) => { console.warn(err); setBusy(null); });
      } else if (ext === "obj") {
        if (!THREE.OBJLoader) throw new Error("OBJLoader not loaded");
        new THREE.OBJLoader().load(url, (obj) => {
          obj.traverse(c => {
            if (c.isMesh && (!c.material || c.material.type === "MeshBasicMaterial")) {
              c.material = new THREE.MeshStandardMaterial({
                color: new THREE.Color().setHSL(Math.random(), 0.7, 0.55),
                metalness: 0.3, roughness: 0.4,
              });
            }
          });
          addToScene(obj, file.name, "obj");
        }, undefined, (err) => { console.warn(err); setBusy(null); });
      } else if (ext === "ply") {
        if (!THREE.PLYLoader) throw new Error("PLYLoader not loaded");
        new THREE.PLYLoader().load(url, (geo) => {
          let obj;
          if (geo.attributes.normal) {
            geo.computeVertexNormals();
            const mat = new THREE.MeshStandardMaterial({
              vertexColors: !!geo.attributes.color,
              color: geo.attributes.color ? 0xffffff : 0xb785ff,
              side: THREE.DoubleSide, metalness: 0.3, roughness: 0.6,
            });
            obj = new THREE.Mesh(geo, mat);
          } else {
            const mat = new THREE.PointsMaterial({
              vertexColors: !!geo.attributes.color,
              color: geo.attributes.color ? 0xffffff : 0x4be6ff,
              size: 0.05, sizeAttenuation: true,
            });
            obj = new THREE.Points(geo, mat);
          }
          addToScene(obj, file.name, "ply");
        }, undefined, (err) => { console.warn(err); setBusy(null); });
      } else if (ext === "stl") {
        if (!THREE.STLLoader) throw new Error("STLLoader not loaded");
        new THREE.STLLoader().load(url, (geo) => {
          const mat = new THREE.MeshStandardMaterial({
            color: new THREE.Color().setHSL(Math.random(), 0.7, 0.55),
            metalness: 0.6, roughness: 0.3,
          });
          const mesh = new THREE.Mesh(geo, mat);
          addToScene(mesh, file.name, "stl");
        }, undefined, (err) => { console.warn(err); setBusy(null); });
      } else {
        alert(`Unsupported format: .${ext}\nUse: ${SUPPORTED_FORMATS.join(", ")}`);
        setBusy(null);
      }
    } catch (e) {
      console.error(e); alert(`Failed: ${e.message}`); setBusy(null);
    }
  };

  const removeModel = (id) => {
    const m = models.find(mm => mm.id === id);
    if (!m) return;
    const objGroup = objectsRef.current.modelsGroup;
    const obj = objGroup.children.find(c => c.userData._objId === m._objId);
    if (obj) {
      obj.traverse(c => {
        if (c.geometry) c.geometry.dispose();
        if (c.material) {
          if (Array.isArray(c.material)) c.material.forEach(mat => mat.dispose());
          else c.material.dispose();
        }
      });
      objGroup.remove(obj);
    }
    setModels(arr => arr.filter(mm => mm.id !== id));
    if (selectedId === id) setSelectedId(null);
  };

  const updateModel = (id, patch) => {
    setModels(arr => arr.map(m => m.id === id ? { ...m, ...patch } : m));
  };

  const findObj = (model) => {
    const grp = objectsRef.current.modelsGroup;
    if (!grp) return null;
    return grp.children.find(c => c.userData._objId === model._objId);
  };
  const setModelWireframe = (model, on) => {
    const obj = findObj(model); if (!obj) return;
    obj.traverse(c => {
      if (c.material) {
        const mats = Array.isArray(c.material) ? c.material : [c.material];
        mats.forEach(m => { if ("wireframe" in m) m.wireframe = on; });
      }
    });
  };
  const setModelColor = (model, hex) => {
    const obj = findObj(model); if (!obj || !window.THREE) return;
    const col = new window.THREE.Color(hex);
    obj.traverse(c => {
      if (c.material) {
        const mats = Array.isArray(c.material) ? c.material : [c.material];
        mats.forEach(m => {
          if (m.color) m.color.copy(col);
          if (m.emissive) m.emissive.copy(col).multiplyScalar(0.5);
        });
      }
    });
  };

  /* ====== Drag-drop handlers ====== */
  const onDrop = async (e) => {
    e.preventDefault();
    setDragOver(false);
    for (const f of e.dataTransfer.files) await loadFile(f);
  };
  const onDragOver = (e) => { e.preventDefault(); setDragOver(true); };
  const onDragLeave = () => setDragOver(false);
  const onFiles = async (e) => {
    for (const f of e.target.files) await loadFile(f);
    e.target.value = "";
  };

  const selected = models.find(m => m.id === selectedId);

  return (
    <div className="panel stage-bezel" style={{ padding: 0, display: "flex", flexDirection: "column", overflow: "hidden" }}>
      <Screws />

      {/* Top toolbar */}
      <div className="stage-toolbar">
        <div className="stage-tabs"><div className="stage-tab active">3D Scene</div></div>
        <div style={{ flex: 1 }} />
        <span className="cap">Environment:</span>
        <div className="chips" style={{ padding: 3 }}>
          {ENVIRONMENTS.map(e => (
            <button key={e.id}
              className={`chip ${environment === e.id ? "active" : ""}`}
              onClick={() => setEnvironment(e.id)} title={e.desc}>
              {e.label}
            </button>
          ))}
        </div>
        <button className={`nbtn compact ${orbit ? "glow pressed" : ""}`} onClick={() => setOrbit(o => !o)}>Orbit</button>
        <button className={`nbtn compact ${grid ? "glow pressed" : ""}`} onClick={() => setGrid(g => !g)}>Grid</button>
      </div>

      {/* MAIN GRID */}
      <div className="lum-wsgrid" style={{ flex: 1, display: "grid", gridTemplateColumns: "240px 1fr 260px", gap: 0, minHeight: 0 }}>
        {/* LEFT: Scene Outliner */}
        <div style={{
          background: "linear-gradient(180deg, #14141a, #0a0a0c)",
          borderRight: "1px solid rgba(255,255,255,0.04)",
          padding: 10, display: "flex", flexDirection: "column", gap: 10,
          overflow: "hidden",
        }}>
          <div style={{
            display: "flex", alignItems: "center", gap: 6, padding: "6px 8px",
            background: "linear-gradient(180deg, var(--panel-hi), var(--panel))",
            borderRadius: 6, boxShadow: "var(--nshadow-out-sm)",
          }}>
            <span style={{ width: 6, height: 6, borderRadius: "50%",
              background: "var(--led-cyan)", boxShadow: "0 0 6px var(--led-cyan-glow)" }} />
            <span className="cap" style={{ flex: 1 }}>Scene Outliner</span>
            <span style={{ fontFamily: "var(--font-mono)", fontSize: 9, color: "var(--led-cyan)",
              textShadow: "0 0 4px var(--led-cyan-glow)" }}>{models.length} obj</span>
          </div>

          {/* + Import button (big & prominent) */}
          <input ref={fileInputRef} type="file" hidden multiple
            accept={SUPPORTED_FORMATS.join(",")} onChange={onFiles} />
          <button
            onClick={() => fileInputRef.current.click()}
            style={{
              padding: "12px 10px",
              background: "radial-gradient(circle at 50% 0%, #ffe39a 0%, #ffb02a 40%, #c87016 100%)",
              color: "#2a1606",
              border: "none", borderRadius: 8,
              boxShadow: "inset 0 1px 0 rgba(255,255,255,0.4), 0 0 14px rgba(255,122,26,0.5)",
              fontFamily: "var(--font-display)", fontWeight: 800, fontSize: 12,
              letterSpacing: "0.12em", textTransform: "uppercase",
              cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", gap: 6,
            }}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
              <path d="M12 5v14M5 12h14"/></svg>
            Import 3D Model
          </button>
          <div className="cap" style={{ fontSize: 8, textAlign: "center", color: "var(--ink-faint)", marginTop: -4 }}>
            {SUPPORTED_FORMATS.join(" · ")}
          </div>

          {/* Sample shapes (quick test) */}
          <div className="cap" style={{ marginTop: 4 }}>Quick Shapes</div>
          <div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 4 }}>
            {SAMPLE_SHAPES.map(s => (
              <button key={s.id} className="nbtn compact"
                onClick={() => addShape(s.id)}
                title={`Add ${s.label}`}
                style={{ padding: "8px 0", flexDirection: "column", gap: 1, fontSize: 7 }}>
                <span style={{ fontSize: 14 }}>{s.icon}</span>
                {s.label}
              </button>
            ))}
          </div>

          {/* Model list */}
          <div className="cap" style={{ marginTop: 6 }}>Models in Scene</div>
          <div style={{ flex: 1, overflowY: "auto", display: "flex", flexDirection: "column", gap: 4, paddingRight: 4 }}>
            {models.length === 0 && (
              <div style={{
                padding: "16px 12px", textAlign: "center", color: "var(--ink-faint)",
                fontSize: 10, lineHeight: 1.6,
                border: "1.5px dashed rgba(255,255,255,0.08)", borderRadius: 8,
              }}>
                Nothing imported yet.<br/>
                <span style={{ fontSize: 9, opacity: 0.8 }}>
                  Click <b style={{ color: "var(--accent)" }}>Import</b>,<br/>
                  drag a file onto the viewport,<br/>
                  or add a <b>Quick Shape</b>.
                </span>
              </div>
            )}
            {busy && (
              <div style={{
                padding: 6, borderRadius: 6,
                background: "rgba(75,230,255,0.08)",
                border: "1px solid rgba(75,230,255,0.3)",
                fontSize: 10, color: "var(--led-cyan)",
                display: "flex", alignItems: "center", gap: 6,
              }}>
                <Led on color="cyan" size={6} />
                Loading {busy}…
              </div>
            )}
            {models.map(m => (
              <div key={m.id}
                onClick={() => setSelectedId(m.id)}
                className={`preset-card ${m.id === selectedId ? "active" : ""}`}
                style={{ padding: 6, cursor: "pointer" }}>
                <div className="preset-icon" style={{ width: 28, height: 28, fontSize: 12 }}>
                  {m.kind === "shape" ? "■" :
                   m.kind === "glb" ? "▣" :
                   m.kind === "obj" ? "◇" :
                   m.kind === "ply" ? "✦" :
                   m.kind === "stl" ? "▲" : "◯"}
                </div>
                <div style={{ flex: 1, minWidth: 0, textAlign: "left" }}>
                  <div className="preset-name" style={{ fontSize: 10, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
                    {m.name}
                  </div>
                  <div className="preset-tag" style={{ fontSize: 8 }}>
                    {m.kind.toUpperCase()} · {m.audioReactive ? `react ${m.audioBand}` : "static"}
                  </div>
                </div>
                <button className="nbtn compact" style={{ padding: 3, minWidth: 20 }}
                  onClick={(e) => { e.stopPropagation(); updateModel(m.id, { visible: !m.visible }); }}>
                  <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4"
                    style={{ opacity: m.visible ? 1 : 0.4 }}>
                    <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8S1 12 1 12z"/>
                    <circle cx="12" cy="12" r="3"/>
                  </svg>
                </button>
                <button className="nbtn compact" style={{ padding: 3, minWidth: 20 }}
                  onClick={(e) => { e.stopPropagation(); removeModel(m.id); }}>
                  <svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4">
                    <path d="M6 6l12 12M18 6L6 18"/></svg>
                </button>
              </div>
            ))}
          </div>
        </div>

        {/* CENTER: viewport */}
        <div style={{ position: "relative" }}
          onDrop={onDrop} onDragOver={onDragOver} onDragLeave={onDragLeave}>
          <div className="screen-frame" style={{ margin: 0, height: "100%", borderRadius: 0 }}>
            <div ref={mountRef} style={{ width: "100%", height: "100%", cursor: "grab" }} />
            <div className="screen-corner" style={{ top: 10, left: 14 }}>
              ● 3D · {environment === "none" ? "Stage" : ENVIRONMENTS.find(e => e.id === environment)?.label}
            </div>
            <div className="screen-corner" style={{ top: 10, right: 14 }}>
              DRAG ROTATE · WHEEL ZOOM
            </div>
            <div className="screen-corner" style={{ bottom: 10, left: 14 }}>
              {models.length} model{models.length === 1 ? "" : "s"} loaded
            </div>
            <div className="screen-corner" style={{ bottom: 10, right: 14 }}>
              {audio?.energy > 0.05 ? "● AUDIO LOCKED" : "○ NO AUDIO"}
            </div>
            {!window.THREE && (
              <div style={{
                position: "absolute", inset: 0, display: "grid", placeItems: "center",
                color: "var(--ink-dim)", fontFamily: "var(--font-mono)", fontSize: 12
              }}>
                Loading Three.js…
              </div>
            )}
            {/* Drop overlay */}
            {dragOver && (
              <div style={{
                position: "absolute", inset: 18,
                border: "3px dashed var(--accent)",
                borderRadius: 8,
                background: "rgba(255,122,26,0.12)",
                backdropFilter: "blur(4px)",
                display: "grid", placeItems: "center",
                pointerEvents: "none", zIndex: 10,
              }}>
                <div style={{ textAlign: "center" }}>
                  <div style={{ fontSize: 60, color: "var(--accent)",
                    textShadow: "0 0 20px var(--accent-glow)" }}>↓</div>
                  <div style={{
                    fontFamily: "var(--font-display)", fontWeight: 700,
                    fontSize: 18, letterSpacing: "0.16em", textTransform: "uppercase",
                    color: "var(--accent)", textShadow: "0 0 10px var(--accent-glow)",
                  }}>Drop 3D Models</div>
                  <div style={{
                    fontFamily: "var(--font-mono)", fontSize: 10,
                    color: "var(--ink-dim)", marginTop: 6, letterSpacing: "0.1em",
                  }}>
                    {SUPPORTED_FORMATS.join(" · ")}
                  </div>
                </div>
              </div>
            )}
            {/* Empty-state hint */}
            {!dragOver && models.length === 0 && (
              <div style={{
                position: "absolute", left: "50%", top: "50%",
                transform: "translate(-50%, -50%)",
                pointerEvents: "none", textAlign: "center",
                opacity: 0.6,
              }}>
                <div style={{
                  fontFamily: "var(--font-display)", fontSize: 16, fontWeight: 700,
                  color: "var(--ink-dim)", letterSpacing: "0.16em", textTransform: "uppercase",
                }}>Drop model here</div>
                <div style={{ fontFamily: "var(--font-mono)", fontSize: 9, color: "var(--ink-faint)", marginTop: 4 }}>
                  or use ← left panel
                </div>
              </div>
            )}
          </div>
        </div>

        {/* RIGHT: Inspector */}
        <div style={{
          background: "linear-gradient(180deg, #14141a, #0a0a0c)",
          borderLeft: "1px solid rgba(255,255,255,0.04)",
          padding: 10, display: "flex", flexDirection: "column", gap: 10,
          overflowY: "auto",
        }}>
          {!selected ? (
            <div style={{
              padding: "20px 12px", textAlign: "center",
              color: "var(--ink-faint)", fontSize: 11, lineHeight: 1.6,
            }}>
              <div style={{ fontFamily: "var(--font-display)", fontSize: 11, fontWeight: 700,
                letterSpacing: "0.16em", textTransform: "uppercase", color: "var(--ink-dim)", marginBottom: 8 }}>
                Inspector
              </div>
              Select an object to edit its position, scale, and audio reactivity.
            </div>
          ) : (
            <>
              <div className="dock-section" style={{ padding: 8 }}>
                <div className="dock-label" style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
                  <span>{selected.kind.toUpperCase()}</span>
                  <Led on={selected.visible} color="green" />
                </div>
                <div style={{ fontFamily: "var(--font-mono)", fontSize: 10, color: "var(--accent)",
                  textShadow: "0 0 3px var(--accent-glow)", marginTop: 4,
                  whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
                }}>{selected.name}</div>
              </div>

              <div className="dock-section" style={{ padding: 8 }}>
                <div className="dock-label">Transform</div>
                <Slider3 label="Scale"   value={selected.scale} onChange={v => updateModel(selected.id, { scale: v })} min={0.05} max={5} step={0.01} />
                <Slider3 label="Rot Y"   value={selected.rotY}  onChange={v => updateModel(selected.id, { rotY:  v })} min={-180} max={180} step={1} unit="°" />
                <Slider3 label="Pos X"   value={selected.x || 0} onChange={v => updateModel(selected.id, { x: v })} min={-15} max={15} step={0.05} />
                <Slider3 label="Pos Y"   value={selected.y || 0} onChange={v => updateModel(selected.id, { y: v })} min={-8} max={8} step={0.05} />
                <Slider3 label="Pos Z"   value={selected.z || 0} onChange={v => updateModel(selected.id, { z: v })} min={-15} max={15} step={0.05} />
                <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginTop: 8 }}>
                  <span className="cap" style={{ fontSize: 9 }}>Auto-spin</span>
                  <Flip value={!!selected.spin} onChange={v => updateModel(selected.id, { spin: v })} />
                </div>
                <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginTop: 8 }}>
                  <span className="cap" style={{ fontSize: 9 }}>Wireframe</span>
                  <Flip value={!!selected.wireframe} onChange={v => { updateModel(selected.id, { wireframe: v }); setModelWireframe(selected, v); }} />
                </div>
                <div style={{ marginTop: 8 }}>
                  <div className="cap" style={{ fontSize: 9, marginBottom: 4 }}>Material Color</div>
                  <input type="color" value={selected.matColor || "#b785ff"}
                    onChange={e => { updateModel(selected.id, { matColor: e.target.value }); setModelColor(selected, e.target.value); }}
                    style={{ width: "100%", height: 26, border: "none", borderRadius: 6, background: "transparent",
                      cursor: "pointer", boxShadow: "var(--nshadow-in-sm)" }} />
                </div>
              </div>

              <div className="dock-section" style={{ padding: 8 }}>
                <div className="dock-label" style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
                  Audio Reactive
                  <Flip value={selected.audioReactive} onChange={v => updateModel(selected.id, { audioReactive: v })} />
                </div>
                {selected.audioReactive && (
                  <div style={{ marginTop: 6 }}>
                    <div className="cap" style={{ fontSize: 9, marginBottom: 4 }}>Band</div>
                    <div style={{ display: "flex", background: "linear-gradient(180deg,#0a0a0c,#16161a)",
                      borderRadius: 6, padding: 2, boxShadow: "var(--nshadow-in-sm)" }}>
                      {["bass","mid","high","energy"].map(b => (
                        <button key={b}
                          onClick={() => updateModel(selected.id, { audioBand: b })}
                          style={{
                            flex: 1, padding: "3px 4px", border: "none",
                            background: selected.audioBand === b ? "linear-gradient(180deg, var(--panel-hi), var(--panel))" : "transparent",
                            color: selected.audioBand === b ? "var(--accent)" : "var(--ink-faint)",
                            textShadow: selected.audioBand === b ? "0 0 4px var(--accent-glow)" : "none",
                            borderRadius: 4, fontSize: 8.5, cursor: "pointer",
                            fontFamily: "var(--font-display)", letterSpacing: "0.1em", textTransform: "uppercase",
                            fontWeight: 700,
                          }}>{b}</button>
                      ))}
                    </div>
                  </div>
                )}
              </div>

              <button className="nbtn compact danger" onClick={() => removeModel(selected.id)}>
                Remove from Scene
              </button>
            </>
          )}
        </div>
      </div>

      <div className="stage-strip" style={{ gap: 16 }}>
        <span className="cap">FORMATS:</span>
        {SUPPORTED_FORMATS.map(f => (
          <span key={f} className="chip" style={{ fontSize: 9 }}>{f.toUpperCase()}</span>
        ))}
        <div style={{ flex: 1 }} />
        <span className="cap" style={{ color: "var(--ink-faint)" }}>
          Tip: drag files onto the viewport to import quickly.
        </span>
      </div>
    </div>
  );
}

function Slider3({ label, value, onChange, min, max, step, unit = "" }) {
  return (
    <div style={{ display: "grid", gridTemplateColumns: "40px 1fr 50px", gap: 6, alignItems: "center", marginTop: 4 }}>
      <span className="cap" style={{ fontSize: 8 }}>{label}</span>
      <HSlider value={value} onChange={onChange} min={min} max={max} />
      <span style={{ fontFamily: "var(--font-mono)", fontSize: 9, color: "var(--accent)",
        textShadow: "0 0 3px var(--accent-glow)", textAlign: "right" }}>
        {step >= 1 ? value.toFixed(0) : value.toFixed(2)}{unit}
      </span>
    </div>
  );
}

window.Scene3D = Scene3D;
