// stl-hero.jsx — Upload-your-own-STL hero variant
// Parses binary or ASCII STL into a Three.js BufferGeometry, auto-frames the
// camera to the model's bounding box, persists the file in localStorage so it
// survives a reload, and reports volume / triangle count / bounding box.

const STL_STORAGE_KEY = "sync-hero-stl-v1";
const STL_META_KEY = "sync-hero-stl-meta-v1";
const STL_MAX_PERSIST_BYTES = 3 * 1024 * 1024; // 3MB cap for localStorage persistence

const STL_PRINT_COLORS = [
  { id: "graphite", hex: "#2a2a2a", name: "Graphite PLA" },
  { id: "bone",     hex: "#e8e3d8", name: "Bone PLA" },
  { id: "stone",    hex: "#8a8478", name: "Stone PLA" },
  { id: "slate",    hex: "#3d4148", name: "Slate PETG" },
  { id: "fog",      hex: "#b7b5ae", name: "Fog PLA" },
  { id: "rust",     hex: "#8a4a36", name: "Rust PLA" },
];

const STLTick = ({ pos }) => {
  const base = { position: "absolute", width: 12, height: 12, color: "var(--ink-soft)" };
  const map = {
    tl: { top: 0, left: 0, borderTop: "1px solid currentColor", borderLeft: "1px solid currentColor" },
    tr: { top: 0, right: 0, borderTop: "1px solid currentColor", borderRight: "1px solid currentColor" },
    bl: { bottom: 0, left: 0, borderBottom: "1px solid currentColor", borderLeft: "1px solid currentColor" },
    br: { bottom: 0, right: 0, borderBottom: "1px solid currentColor", borderRight: "1px solid currentColor" },
  };
  return <span style={{ ...base, ...map[pos] }} />;
};

// ─── STL parser ─────────────────────────────────────────────────────────────
function parseSTL(buffer) {
  const view = new DataView(buffer);

  // Detect binary vs ASCII. Header may start with "solid" in either case
  // (some exporters write that into binary headers), so use the size check.
  let isBinary = false;
  if (buffer.byteLength >= 84) {
    const triCount = view.getUint32(80, true);
    if (80 + 4 + triCount * 50 === buffer.byteLength) isBinary = true;
  }
  if (!isBinary && buffer.byteLength >= 5) {
    const head = new Uint8Array(buffer, 0, Math.min(5, buffer.byteLength));
    const headStr = String.fromCharCode(...head).toLowerCase();
    if (headStr !== "solid") isBinary = true;
  }

  if (isBinary) {
    const n = view.getUint32(80, true);
    const positions = new Float32Array(n * 9);
    const normals = new Float32Array(n * 9);
    let offset = 84;
    for (let i = 0; i < n; i++) {
      const nx = view.getFloat32(offset, true);
      const ny = view.getFloat32(offset + 4, true);
      const nz = view.getFloat32(offset + 8, true);
      offset += 12;
      for (let v = 0; v < 3; v++) {
        const k = i * 9 + v * 3;
        positions[k]     = view.getFloat32(offset, true);
        positions[k + 1] = view.getFloat32(offset + 4, true);
        positions[k + 2] = view.getFloat32(offset + 8, true);
        normals[k]     = nx;
        normals[k + 1] = ny;
        normals[k + 2] = nz;
        offset += 12;
      }
      offset += 2; // attribute byte count
    }
    const geo = new THREE.BufferGeometry();
    geo.setAttribute("position", new THREE.BufferAttribute(positions, 3));
    geo.setAttribute("normal",   new THREE.BufferAttribute(normals, 3));
    return geo;
  }

  // ASCII parser
  const text = new TextDecoder().decode(buffer);
  const positions = [];
  const normals = [];
  let curN = [0, 0, 1];
  const facetRe = /facet\s+normal\s+(\S+)\s+(\S+)\s+(\S+)/g;
  const vertRe = /vertex\s+(\S+)\s+(\S+)\s+(\S+)/g;
  // Walk linearly to preserve normal-vs-vertex ordering
  const lines = text.split(/\r?\n/);
  for (const raw of lines) {
    const t = raw.trim();
    if (!t) continue;
    if (t.startsWith("facet normal")) {
      const p = t.split(/\s+/);
      curN = [parseFloat(p[2]), parseFloat(p[3]), parseFloat(p[4])];
    } else if (t.startsWith("vertex")) {
      const p = t.split(/\s+/);
      positions.push(parseFloat(p[1]), parseFloat(p[2]), parseFloat(p[3]));
      normals.push(curN[0], curN[1], curN[2]);
    }
  }
  if (!positions.length) throw new Error("No vertices found in STL");
  const geo = new THREE.BufferGeometry();
  geo.setAttribute("position", new THREE.BufferAttribute(new Float32Array(positions), 3));
  geo.setAttribute("normal",   new THREE.BufferAttribute(new Float32Array(normals), 3));
  return geo;
}

// Closed-mesh volume via signed tetrahedra from origin. Units = (input units)³.
function computeMeshVolume(geo) {
  const pos = geo.attributes.position.array;
  let v = 0;
  for (let i = 0; i < pos.length; i += 9) {
    const ax = pos[i],     ay = pos[i+1], az = pos[i+2];
    const bx = pos[i+3],   by = pos[i+4], bz = pos[i+5];
    const cx = pos[i+6],   cy = pos[i+7], cz = pos[i+8];
    v += (ax * (by * cz - bz * cy)
        + ay * (bz * cx - bx * cz)
        + az * (bx * cy - by * cx)) / 6;
  }
  return Math.abs(v);
}

// ─── Persistence helpers (chunked btoa to avoid call-stack overflow) ────────
function arrayBufferToBase64(buffer) {
  const bytes = new Uint8Array(buffer);
  let bin = "";
  const chunk = 0x8000;
  for (let i = 0; i < bytes.length; i += chunk) {
    bin += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
  }
  return btoa(bin);
}
function base64ToArrayBuffer(b64) {
  const bin = atob(b64);
  const bytes = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
  return bytes.buffer;
}

// ─── The viewer: mounts Three.js with the parsed geometry, auto-frames camera ─
function STLViewer({ arrayBuffer, color, interactive = true, autoRotate = true }) {
  const mountRef = React.useRef(null);
  const stateRef = React.useRef({});

  React.useEffect(() => {
    if (!arrayBuffer) return;
    const mount = mountRef.current;
    if (!mount) return;

    let w = mount.clientWidth, h = mount.clientHeight;
    if (!w || !h) { w = 480; h = 480; }

    // Parse FIRST → recenter → re-orient (STL convention is Z-up; Three.js is
    // Y-up). Do this before allocating a WebGLRenderer so an unreadable file
    // bails out without leaking a GPU context (browsers cap live contexts).
    let geo;
    try {
      geo = parseSTL(arrayBuffer);
    } catch (e) {
      console.error("STL parse failed", e);
      return;
    }
    // STLs may have missing normals — recompute from triangles
    geo.computeVertexNormals();
    geo.computeBoundingBox();
    const bb = geo.boundingBox;
    const center = new THREE.Vector3();
    bb.getCenter(center);
    geo.translate(-center.x, -center.y, -center.z);
    // Reorient: STL Z-up → Three Y-up
    geo.rotateX(-Math.PI / 2);
    geo.computeBoundingSphere();
    const radius = geo.boundingSphere.radius || 1;

    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(34, w / h, 0.01, 10000);

    const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.setSize(w, h);
    renderer.outputColorSpace = THREE.SRGBColorSpace;
    mount.appendChild(renderer.domElement);

    // Lights — match the rest of the site
    const key = new THREE.DirectionalLight(0xffffff, 1.6); key.position.set(3, 4, 3); scene.add(key);
    const fill = new THREE.DirectionalLight(0xffeed8, 0.45); fill.position.set(-3.5, 1.2, 2); scene.add(fill);
    const rim = new THREE.DirectionalLight(0xcfd9ff, 0.9); rim.position.set(-1, -2, -3.5); scene.add(rim);
    const top = new THREE.DirectionalLight(0xffffff, 0.6); top.position.set(0, 6, 0.5); scene.add(top);
    scene.add(new THREE.AmbientLight(0xffffff, 0.35));

    const mat = new THREE.MeshStandardMaterial({ color, roughness: 0.72, metalness: 0.0 });
    const mesh = new THREE.Mesh(geo, mat);
    mesh.rotation.x = 0.12;
    scene.add(mesh);

    // Auto-frame: fit the bounding sphere within the camera FOV with padding
    const fov = camera.fov * Math.PI / 180;
    const distance = (radius / Math.tan(fov / 2)) * 1.4;
    camera.position.set(distance * 0.25, distance * 0.25, distance);
    camera.near = Math.max(0.01, distance * 0.001);
    camera.far  = distance * 100;
    camera.updateProjectionMatrix();
    camera.lookAt(0, 0, 0);

    // Ground shadow disc proportional to the model
    const discGeo = new THREE.CircleGeometry(radius * 1.4, 64);
    const discMat = new THREE.MeshBasicMaterial({ color: 0x000000, transparent: true, opacity: 0.07 });
    const disc = new THREE.Mesh(discGeo, discMat);
    disc.rotation.x = -Math.PI / 2;
    disc.position.y = -radius * 0.95;
    scene.add(disc);

    stateRef.current = { scene, camera, renderer, mesh, mat, mount };

    // Pointer interaction
    let dragging = false, lastX = 0, lastY = 0, velX = 0.005, targetVelX = 0.005;
    function onDown(e) {
      if (!interactive) return;
      dragging = true;
      lastX = e.clientX ?? e.touches?.[0]?.clientX ?? 0;
      lastY = e.clientY ?? e.touches?.[0]?.clientY ?? 0;
      mount.style.cursor = "grabbing";
    }
    function onMove(e) {
      if (!dragging) return;
      const x = e.clientX ?? e.touches?.[0]?.clientX ?? 0;
      const y = e.clientY ?? e.touches?.[0]?.clientY ?? 0;
      const dx = x - lastX, dy = y - lastY;
      mesh.rotation.y += dx * 0.01;
      mesh.rotation.x += dy * 0.008;
      mesh.rotation.x = Math.max(-1.2, Math.min(1.2, mesh.rotation.x));
      lastX = x; lastY = y;
      targetVelX = 0;
    }
    function onUp() {
      dragging = false;
      mount.style.cursor = interactive ? "grab" : "default";
      if (autoRotate) targetVelX = 0.005;
    }
    if (interactive) {
      mount.style.cursor = "grab";
      mount.addEventListener("mousedown", onDown);
      window.addEventListener("mousemove", onMove);
      window.addEventListener("mouseup", onUp);
      mount.addEventListener("touchstart", onDown, { passive: true });
      mount.addEventListener("touchmove", onMove, { passive: true });
      mount.addEventListener("touchend", onUp);
    }

    let raf;
    function tick() {
      velX += (targetVelX - velX) * 0.04;
      if (!dragging && autoRotate) mesh.rotation.y += velX;
      renderer.render(scene, camera);
      raf = requestAnimationFrame(tick);
    }
    tick();

    const ro = new ResizeObserver(() => {
      const W = mount.clientWidth, H = mount.clientHeight;
      if (!W || !H) return;
      camera.aspect = W / H;
      camera.updateProjectionMatrix();
      renderer.setSize(W, H);
    });
    ro.observe(mount);

    return () => {
      cancelAnimationFrame(raf);
      ro.disconnect();
      if (interactive) {
        mount.removeEventListener("mousedown", onDown);
        window.removeEventListener("mousemove", onMove);
        window.removeEventListener("mouseup", onUp);
        mount.removeEventListener("touchstart", onDown);
        mount.removeEventListener("touchmove", onMove);
        mount.removeEventListener("touchend", onUp);
      }
      geo.dispose(); mat.dispose();
      discGeo.dispose(); discMat.dispose();
      renderer.dispose();
      if (renderer.domElement.parentNode === mount) mount.removeChild(renderer.domElement);
    };
  }, [arrayBuffer]);

  // live color update
  React.useEffect(() => {
    const s = stateRef.current;
    if (s && s.mat) s.mat.color.set(color);
  }, [color]);

  return <div ref={mountRef} style={{ width: "100%", height: "100%" }} />;
}

// ─── The hero variant — drag/drop or pick, persist, show stats ──────────────
function HeroSTL({ swatch, setSwatch, onUpload }) {
  const [file, setFile] = React.useState(null);
  const [loading, setLoading] = React.useState(false);
  const [error, setError] = React.useState("");
  const [dragOver, setDragOver] = React.useState(false);
  const fileInputRef = React.useRef(null);

  // restore on mount
  React.useEffect(() => {
    try {
      const b64 = localStorage.getItem(STL_STORAGE_KEY);
      const metaStr = localStorage.getItem(STL_META_KEY);
      if (b64 && metaStr) {
        const arrayBuffer = base64ToArrayBuffer(b64);
        const meta = JSON.parse(metaStr);
        setFile({ arrayBuffer, ...meta });
      }
    } catch (e) {
      // ignore
    }
  }, []);

  function handleFile(f) {
    if (!f) return;
    if (!/\.stl$/i.test(f.name)) {
      setError("That doesn't look like an .stl file.");
      return;
    }
    setError(""); setLoading(true);
    const reader = new FileReader();
    reader.onerror = () => { setError("Couldn't read that file."); setLoading(false); };
    reader.onload = () => {
      try {
        const buffer = reader.result;
        const geo = parseSTL(buffer);
        geo.computeBoundingBox();
        const bb = geo.boundingBox;
        const dims = [
          +(bb.max.x - bb.min.x).toFixed(1),
          +(bb.max.y - bb.min.y).toFixed(1),
          +(bb.max.z - bb.min.z).toFixed(1),
        ];
        const triangles = Math.round(geo.attributes.position.count / 3);
        // Volume in mm³ → cm³
        const volMm3 = computeMeshVolume(geo);
        const vol = +(volMm3 / 1000).toFixed(2);
        geo.dispose();

        const meta = { name: f.name, dims, triangles, vol, size: buffer.byteLength };

        // persist if small enough
        try {
          if (buffer.byteLength <= STL_MAX_PERSIST_BYTES) {
            localStorage.setItem(STL_STORAGE_KEY, arrayBufferToBase64(buffer));
            localStorage.setItem(STL_META_KEY, JSON.stringify(meta));
          } else {
            localStorage.removeItem(STL_STORAGE_KEY);
            localStorage.removeItem(STL_META_KEY);
          }
        } catch (e) {
          // quota; ignore
        }

        setFile({ arrayBuffer: buffer, ...meta });
      } catch (e) {
        console.error(e);
        setError("Couldn't parse that as an STL.");
      } finally {
        setLoading(false);
      }
    };
    reader.readAsArrayBuffer(f);
  }

  function clearFile() {
    try {
      localStorage.removeItem(STL_STORAGE_KEY);
      localStorage.removeItem(STL_META_KEY);
    } catch (e) {}
    setFile(null);
  }

  // Estimated mass at PLA density (1.24 g/cm³) for shell+infill ~ 50%
  const massEst = file ? (file.vol * 1.24 * 0.55) : 0;

  return (
    <section style={{
      padding: "0 var(--pad-x) var(--pad-y)",
      display: "grid",
      gridTemplateColumns: "1.05fr 1fr",
      gap: "clamp(24px, 4vw, 56px)",
      alignItems: "stretch",
      minHeight: "calc(100vh - 60px)",
    }}>
      {/* Left: editorial copy */}
      <div style={{ display: "flex", flexDirection: "column", justifyContent: "space-between", paddingTop: "clamp(20px,4vw,48px)" }}>
        <div>
          <div className="mono upper" style={{ color: "var(--muted)", marginBottom: 18 }}>
            <span style={{ color: "var(--accent)" }}>●</span>&nbsp; 02 printers online · queue 14m
          </div>
          <h1 className="serif" style={{
            fontSize: "clamp(56px, 8.4vw, 132px)",
            lineHeight: 0.92, margin: 0, letterSpacing: "-0.035em", textWrap: "balance",
          }}>
            See <span style={{ fontStyle: "italic" }}>your part</span><br />
            before you print.
          </h1>
          <p style={{ maxWidth: 460, marginTop: 28, fontSize: 17, lineHeight: 1.5, color: "var(--ink-soft)" }}>
            Drop your own STL on the right. We parse it in the browser —
            no upload required to preview — and show you triangle count,
            bounding box and estimated mass.
          </p>
        </div>
        <div style={{ marginTop: 40 }}>
          <div style={{ display: "flex", gap: 12, alignItems: "center", flexWrap: "wrap" }}>
            <button onClick={onUpload} style={{
              all: "unset", cursor: "pointer", padding: "16px 26px", borderRadius: 999,
              background: "var(--ink)", color: "var(--bg)", fontSize: 15, fontWeight: 500,
              display: "inline-flex", alignItems: "center", gap: 10,
            }}>
              <span>Continue to quote</span>
              <span style={{ fontSize: 18, lineHeight: 1 }}>→</span>
            </button>
            <button className="mono" onClick={() => fileInputRef.current?.click()} style={{
              all: "unset", cursor: "pointer", padding: "16px 22px", borderRadius: 999,
              border: "1px solid var(--line)", fontSize: 12, color: "var(--ink-soft)",
            }}>
              {file ? "Replace STL" : "Browse for STL"}
            </button>
          </div>
          {file ? (
            <div className="mono upper" style={{ marginTop: 36, color: "var(--muted)", display: "grid", gridTemplateColumns: "repeat(4, auto)", gap: 28, justifyContent: "flex-start" }}>
              <span><b style={{ color: "var(--ink)", fontWeight: 500 }}>{file.dims.join(" × ")}</b><br/>mm bounding box</span>
              <span><b style={{ color: "var(--ink)", fontWeight: 500 }}>{file.triangles.toLocaleString()}</b><br/>triangles</span>
              <span><b style={{ color: "var(--ink)", fontWeight: 500 }}>{file.vol.toFixed(1)}</b><br/>cm³ volume</span>
              <span><b style={{ color: "var(--ink)", fontWeight: 500 }}>~{massEst.toFixed(1)}g</b><br/>est. mass · PLA</span>
            </div>
          ) : (
            <div className="mono upper" style={{ marginTop: 36, color: "var(--muted)", display: "flex", gap: 28, flexWrap: "wrap" }}>
              <span><b style={{ color: "var(--ink)", fontWeight: 500 }}>£0.18</b> · /gram PLA</span>
              <span><b style={{ color: "var(--ink)", fontWeight: 500 }}>48hr</b> · turnaround</span>
              <span><b style={{ color: "var(--ink)", fontWeight: 500 }}>0.08mm</b> · finest layer</span>
            </div>
          )}
        </div>
      </div>

      {/* Right: dropzone / viewer */}
      <div
        onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
        onDragLeave={() => setDragOver(false)}
        onDrop={(e) => {
          e.preventDefault();
          setDragOver(false);
          const f = e.dataTransfer.files?.[0];
          if (f) handleFile(f);
        }}
        style={{
          position: "relative",
          minHeight: 480, display: "flex", flexDirection: "column",
          borderRadius: 8,
          background: file ? "transparent" : "color-mix(in srgb, var(--ink) 3%, transparent)",
          outline: dragOver ? "1.5px dashed var(--ink)" : (file ? "none" : "1.5px dashed var(--ink)"),
          outlineOffset: -1,
          transition: "background 0.15s",
        }}
      >
        {/* hidden file picker */}
        <input ref={fileInputRef} type="file" accept=".stl" style={{ display: "none" }}
          onChange={(e) => handleFile(e.target.files?.[0])} />

        {/* Filament swatches when a file is loaded */}
        {file && (
          <div style={{ position: "absolute", top: 16, right: 16, zIndex: 2, display: "flex", alignItems: "center", gap: 10 }}>
            <span className="mono upper" style={{ color: "var(--muted)" }}>filament</span>
            <div style={{ display: "flex", gap: 6 }}>
              {STL_PRINT_COLORS.map((c) =>
                <button key={c.id} onClick={() => setSwatch(c.hex)} title={c.name} style={{
                  all: "unset", cursor: "pointer",
                  width: 22, height: 22, borderRadius: 999,
                  background: c.hex,
                  boxShadow: swatch === c.hex ? "0 0 0 1.5px var(--ink), 0 0 0 4px var(--bg)" : "inset 0 0 0 1px rgba(0,0,0,0.08)",
                }} />
              )}
            </div>
          </div>
        )}

        {file ? (
          <div style={{ flex: 1, position: "relative" }}>
            <STLViewer arrayBuffer={file.arrayBuffer} color={swatch} />
            <STLTick pos="tl" /><STLTick pos="tr" /><STLTick pos="bl" /><STLTick pos="br" />
            <div className="mono upper" style={{ position: "absolute", bottom: 16, left: 14, color: "var(--muted)" }}>
              {file.name} · drag to rotate
            </div>
            <div className="mono" style={{ position: "absolute", bottom: 16, right: 14, color: "var(--muted)", fontSize: 11, display: "flex", gap: 12 }}>
              <button onClick={clearFile} className="mono" style={{
                all: "unset", cursor: "pointer", color: "var(--ink-soft)", textDecoration: "underline",
              }}>clear</button>
              <span>{file.triangles.toLocaleString()} tri · {(file.size / 1024).toFixed(0)} kB</span>
            </div>
          </div>
        ) : (
          <div style={{ flex: 1, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", textAlign: "center", padding: 24, gap: 16 }}>
            <svg width="52" height="52" viewBox="0 0 52 52" fill="none" stroke="currentColor" strokeWidth="1.5" style={{ color: "var(--ink-soft)" }}>
              <path d="M26 36V14M16 25l10-9 10 9" strokeLinecap="round" strokeLinejoin="round" />
              <path d="M8 41h36" strokeLinecap="round" />
            </svg>
            <div className="serif" style={{ fontSize: 38, letterSpacing: "-0.025em" }}>
              {loading ? "Parsing…" : "Drop your STL here"}
            </div>
            <div className="mono upper" style={{ color: "var(--muted)" }}>
              Binary or ASCII · parsed locally · ≤ 50MB
            </div>
            <button onClick={() => fileInputRef.current?.click()} className="mono" style={{
              all: "unset", cursor: "pointer", marginTop: 6,
              padding: "10px 18px", borderRadius: 999,
              border: "1px solid var(--ink)", fontSize: 12,
            }}>or browse</button>
            {error && (
              <div className="mono" style={{ color: "#b3261e", marginTop: 4, fontSize: 11 }}>{error}</div>
            )}
          </div>
        )}
      </div>
    </section>
  );
}

window.HeroSTL = HeroSTL;
