// stl-utils.jsx — Shared STL parsing, analysis, persistence, and Three.js viewer.
// Exposed on window.SyncSTL for use by both the hero variant and the upload flow.

const SyncSTL = {};

// ─── PARSER ─────────────────────────────────────────────────────────────────
SyncSTL.parse = function parseSTL(buffer) {
  const view = new DataView(buffer);

  // Detect binary vs ASCII via size check. Some binary STLs start with "solid".
  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;
    }
    const geo = new THREE.BufferGeometry();
    geo.setAttribute("position", new THREE.BufferAttribute(positions, 3));
    geo.setAttribute("normal",   new THREE.BufferAttribute(normals, 3));
    return geo;
  }

  // ASCII
  const text = new TextDecoder().decode(buffer);
  const positions = [];
  const normals = [];
  let curN = [0, 0, 1];
  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");
  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 — signed tetrahedra from origin. Input units cubed.
SyncSTL.volume = 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);
};

// ─── COST MODEL ─────────────────────────────────────────────────────────────
// Real operating costs for a single-machine UK studio. Everything downstream
// (quote engine, homepage calculator, presets) reads from here so there's one
// source of truth. No VAT — the business is not VAT registered.
SyncSTL.PRICING = {
  electricityPerKwh: 0.60,      // £/kWh
  printerWatts: 150,            // avg draw while printing (hotend+bed+motors+fans)
  dryerWatts: 300,              // filament dryer
  dryerHoursPerSpool: 12,       // dry each spool 12h before use
  spoolGrams: 1000,             // 1kg spools
  machineCost: 2000,            // RatRig V-Core 4 Hybrid build
  machinePaybackMonths: 12,     // recoup in 12 months
  machineHoursPerMonth: 180,    // ~6h/day realistic utilisation
  operatingSurchargePerHour: 0.85, // maintenance, nozzles, consumables, wear
  labourPerPart: 0.75,          // handling, plate prep, inspection, packing
  markup: 1.55,                 // gross margin over direct cost (covers failures, consumables, profit)
  minOrderTotal: 3.50,          // floor per order
  supportDensity: 0.09,         // sparse/tree supports — fraction of the swept column volume
  // Raw filament £/kg by polymer family
  filamentPerKg: { PLA: 60, PETG: 60, ABS: 70, ASA: 80, TPU: 90, Nylon: 110, PP: 90, "PLA-CF": 110, PC: 120 },
  // Max linear print speed (mm/s) by polymer family — drives run-time
  maxSpeed:      { PLA: 350, ABS: 350, ASA: 350, PETG: 220, TPU: 80, Nylon: 50, PP: 150, "PLA-CF": 300, PC: 120 },
};

// Landed filament cost per gram = raw + amortised drying energy.
SyncSTL.filamentPerGram = function (type) {
  const p = SyncSTL.PRICING;
  const perKg = p.filamentPerKg[type] ?? 35;
  const dryingPerG = (p.dryerWatts / 1000 * p.dryerHoursPerSpool * p.electricityPerKwh) / p.spoolGrams;
  return perKg / 1000 + dryingPerG;
};

// Machine cost per print-hour = amortisation + print electricity.
SyncSTL.machineHourly = function () {
  const p = SyncSTL.PRICING;
  const amort = p.machineCost / (p.machineHoursPerMonth * p.machinePaybackMonths);
  const elec = (p.printerWatts / 1000) * p.electricityPerKwh;
  return amort + elec + p.operatingSurchargePerHour;
};

// Full quote from a slice result. Support is always included silently (mass is
// already baked into slice.massG). Returns per-unit + order total, no VAT.
SyncSTL.quote = function (slice, materialType, quantity = 1) {
  const p = SyncSTL.PRICING;
  const filPerG = SyncSTL.filamentPerGram(materialType);
  const materialCost = slice.massG * filPerG;              // incl. support mass
  const machineCost = slice.printHours * SyncSTL.machineHourly();
  const direct = materialCost + machineCost;
  const unitRaw = direct * p.markup + p.labourPerPart;
  const bulk = quantity >= 50 ? 0.82 : quantity >= 25 ? 0.88 : quantity >= 10 ? 0.93 : 1.0;
  const unit = unitRaw * bulk;
  const total = Math.max(p.minOrderTotal, unit * quantity);
  return {
    filamentPerG: filPerG,
    materialCost, machineCost,
    machineHourly: SyncSTL.machineHourly(),
    unit, total, bulk,
    hitMinimum: unit * quantity < p.minOrderTotal,
  };
};

// Support (swept-column) estimate for a given build "up" direction.
// axis: 0=x 1=y 2=z, sign: +1/-1. Cheap triangle-only pass (no topology).
SyncSTL._supportForUp = function (pos, axis, sign, cosThr) {
  const nTri = pos.length / 9;
  let minUp = Infinity;
  for (let i = 0; i < pos.length; i += 3) {
    const v = sign * pos[i + axis];
    if (v < minUp) minUp = v;
  }
  const bedTol = 0.3;
  let overhangArea = 0, colVol = 0, totalArea = 0;
  for (let i = 0; i < nTri; i++) {
    const ax = pos[i*9], ay = pos[i*9+1], az = pos[i*9+2];
    const bx = pos[i*9+3], by = pos[i*9+4], bz = pos[i*9+5];
    const cx = pos[i*9+6], cy = pos[i*9+7], cz = pos[i*9+8];
    const ux = bx-ax, uy = by-ay, uz = bz-az;
    const vx = cx-ax, vy = cy-ay, vz = cz-az;
    const nx = uy*vz - uz*vy, ny = uz*vx - ux*vz, nz = ux*vy - uy*vx;
    const len = Math.hypot(nx, ny, nz);
    if (len === 0) continue;
    const area = len * 0.5;
    totalArea += area;
    const ncomp = sign * (axis === 0 ? nx : axis === 1 ? ny : nz) / len;
    if (ncomp < -cosThr) {
      const u0 = sign * (axis === 0 ? ax : axis === 1 ? ay : az);
      const u1 = sign * (axis === 0 ? bx : axis === 1 ? by : bz);
      const u2 = sign * (axis === 0 ? cx : axis === 1 ? cy : cz);
      if (Math.max(u0, u1, u2) > minUp + bedTol) {
        overhangArea += area;
        colVol += area * Math.max(0, (u0 + u1 + u2) / 3 - minUp);
      }
    }
  }
  return { overhangArea, colVol, totalArea };
};

// Test the 6 axis-aligned build orientations and return the one needing the
// least support. This models "we orient the part to minimise supports".
SyncSTL.bestOrientation = function (geo, overhangAngleDeg = 45) {
  const pos = geo.attributes.position.array;
  const cosThr = Math.cos((overhangAngleDeg * Math.PI) / 180);
  const dirs = [[0,1],[0,-1],[1,1],[1,-1],[2,1],[2,-1]];
  const labels = { "0_1": "+X up", "0_-1": "−X up", "1_1": "+Y up", "1_-1": "−Y up", "2_1": "+Z up", "2_-1": "−Z up" };
  let best = null;
  for (const [axis, sign] of dirs) {
    const r = SyncSTL._supportForUp(pos, axis, sign, cosThr);
    if (!best || r.colVol < best.colVol) best = { ...r, axis, sign };
  }
  const supportVolMm3 = best.colVol * SyncSTL.PRICING.supportDensity;
  return {
    supportVolMm3,
    overhangAreaMm2: best.overhangArea,
    overhangFraction: best.totalArea > 0 ? best.overhangArea / best.totalArea : 0,
    needsSupport: best.overhangArea > best.totalArea * 0.02,
    orientationLabel: labels[best.axis + "_" + best.sign] || "auto",
  };
};

// ─── GEOMETRY ANALYSIS ──────────────────────────────────────────────────────
// Operates on the original Z-up mesh (orient = "zup") or post-reorientation
// Y-up mesh (orient = "yup"). Returns watertight, overhang area & support
// estimate, connected component count, surface area, triangle count.
//
// Overhang threshold = 45° (typical FDM rule of thumb). Tunable.
SyncSTL.analyze = function analyzeMesh(geo, opts = {}) {
  const orient = opts.orient || "yup";
  const overhangAngleDeg = opts.overhangAngleDeg ?? 45;
  const cosThr = Math.cos((overhangAngleDeg * Math.PI) / 180);
  const pos = geo.attributes.position.array;
  const nTri = pos.length / 9;

  // Quantize vertices to ints for edge / component matching
  const Q = 1000; // 0.001 unit precision (works for any reasonable scale)
  const vmap = new Map();
  let nextId = 0;
  const vid = (x, y, z) => {
    const k = ((x * Q) | 0) + ":" + ((y * Q) | 0) + ":" + ((z * Q) | 0);
    let id = vmap.get(k);
    if (id === undefined) { id = nextId++; vmap.set(k, id); }
    return id;
  };

  const edgeCounts = new Map();
  const triVerts = new Int32Array(nTri * 3);

  let totalArea = 0;
  let overhangArea = 0;
  let overhangColumnVolume = 0; // crude support-volume estimate

  // Bounding box (lowest Y for column-height estimate) — compute up-front
  // so the bed-contact check uses the final minY rather than a running one.
  let minY = Infinity, maxY = -Infinity;
  for (let i = 0; i < pos.length; i += 3) {
    const v = orient === "yup" ? pos[i + 1] : pos[i + 2];
    if (v < minY) minY = v;
    if (v > maxY) maxY = v;
  }
  const bedTol = 0.25; // mm — approx 1 layer; faces within this of the bed are bed-supported

  for (let i = 0; i < nTri; i++) {
    const ax = pos[i*9],   ay = pos[i*9+1], az = pos[i*9+2];
    const bx = pos[i*9+3], by = pos[i*9+4], bz = pos[i*9+5];
    const cx = pos[i*9+6], cy = pos[i*9+7], cz = pos[i*9+8];

    // Face normal & area
    const ux = bx - ax, uy = by - ay, uz = bz - az;
    const vx = cx - ax, vy = cy - ay, vz = cz - az;
    let nx = uy * vz - uz * vy;
    let ny = uz * vx - ux * vz;
    let nz = ux * vy - uy * vx;
    const len = Math.hypot(nx, ny, nz);
    const area = len * 0.5;
    if (len > 0) { nx /= len; ny /= len; nz /= len; }
    totalArea += area;

    // Downward-facing? (build direction = up axis; overhang = normal pointing down)
    const downComp = orient === "yup" ? ny : nz;
    if (downComp < -cosThr) {
      // Bed-contact: skip triangles whose entire face sits on the build plate
      // (within 1 layer height of the minimum). These are supported by the bed.
      const triUp = orient === "yup"
        ? [ay, by, cy]
        : [az, bz, cz];
      const triMaxUp = Math.max(...triUp);
      const isBedContact = triMaxUp <= minY + bedTol;
      if (!isBedContact) {
        overhangArea += area;
        const centroidUp = (triUp[0] + triUp[1] + triUp[2]) / 3;
        const colH = Math.max(0, centroidUp - minY);
        overhangColumnVolume += area * colH;
      }
    }

    const v1 = vid(ax, ay, az);
    const v2 = vid(bx, by, bz);
    const v3 = vid(cx, cy, cz);
    triVerts[i*3] = v1; triVerts[i*3+1] = v2; triVerts[i*3+2] = v3;

    // Edges, sorted-pair keys
    const e = [
      v1 < v2 ? (v1 + "_" + v2) : (v2 + "_" + v1),
      v2 < v3 ? (v2 + "_" + v3) : (v3 + "_" + v2),
      v3 < v1 ? (v3 + "_" + v1) : (v1 + "_" + v3),
    ];
    for (const k of e) edgeCounts.set(k, (edgeCounts.get(k) || 0) + 1);
  }

  // Watertight: every edge shared by exactly 2 triangles
  let openEdges = 0, nonManifoldEdges = 0;
  for (const c of edgeCounts.values()) {
    if (c < 2) openEdges++;
    else if (c > 2) nonManifoldEdges++;
  }
  const watertight = openEdges === 0 && nonManifoldEdges === 0;

  // Connected components via union-find
  const parent = new Int32Array(nextId);
  for (let i = 0; i < nextId; i++) parent[i] = i;
  const find = (x) => {
    while (parent[x] !== x) { parent[x] = parent[parent[x]]; x = parent[x]; }
    return x;
  };
  for (let i = 0; i < nTri; i++) {
    const a = triVerts[i*3], b = triVerts[i*3+1], c = triVerts[i*3+2];
    const ra = find(a), rb = find(b);
    if (ra !== rb) parent[ra] = rb;
    const rb2 = find(b), rc = find(c);
    if (rb2 !== rc) parent[rb2] = rc;
  }
  const roots = new Set();
  for (let i = 0; i < nextId; i++) roots.add(find(i));
  const components = roots.size;

  // Support estimate uses the best of the 6 axis-aligned orientations — we
  // orient every part to minimise support before printing.
  const best = SyncSTL.bestOrientation(geo, overhangAngleDeg);

  return {
    watertight,
    openEdges,
    nonManifoldEdges,
    overhangAreaMm2: best.overhangAreaMm2,
    overhangFraction: best.overhangFraction,
    overhangAngleDeg,
    supportVolMm3: best.supportVolMm3,
    needsSupport: best.needsSupport,
    orientationLabel: best.orientationLabel,
    components,
    partIntegrity: components <= 1,
    surfaceAreaMm2: totalArea,
    triangles: nTri,
    // thin walls — placeholder: no robust check without raycasting
    thinWalls: false,
  };
};

// ─── PERSISTENCE ────────────────────────────────────────────────────────────
SyncSTL.bufferToBase64 = 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);
};
SyncSTL.base64ToBuffer = 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;
};

// ─── VIEWER (shared) ────────────────────────────────────────────────────────
// Renders a parsed STL with auto-framing. Used by HeroSTL and the upload flow.
SyncSTL.Viewer = function STLViewer({ arrayBuffer, color = "#2a2a2a", interactive = true, autoRotate = true, showSupports = false, supportColor = "#9ec8e2" }) {
  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; }

    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);

    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));

    let geo;
    try {
      geo = SyncSTL.parse(arrayBuffer);
    } catch (e) {
      console.error("STL parse failed", e);
      return;
    }
    geo.computeVertexNormals();
    geo.computeBoundingBox();
    const bb = geo.boundingBox;
    const center = new THREE.Vector3();
    bb.getCenter(center);
    geo.translate(-center.x, -center.y, -center.z);
    geo.rotateX(-Math.PI / 2);
    geo.computeBoundingSphere();
    const radius = geo.boundingSphere.radius || 1;

    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);

    // Overhang highlight overlay — paints downward-facing triangles in a tint
    let overlay = null;
    if (showSupports) {
      const pos = geo.attributes.position.array;
      const nTri = pos.length / 9;
      // pre-compute minY for bed-contact filter (post-rotation: Y-up)
      let minY = Infinity;
      for (let i = 1; i < pos.length; i += 3) if (pos[i] < minY) minY = pos[i];
      const bedTol = 0.25;
      const overhangPos = [];
      const COS_45 = Math.cos(Math.PI / 4);
      for (let i = 0; i < nTri; i++) {
        const ax = pos[i*9],   ay = pos[i*9+1], az = pos[i*9+2];
        const bx = pos[i*9+3], by = pos[i*9+4], bz = pos[i*9+5];
        const cx = pos[i*9+6], cy = pos[i*9+7], cz = pos[i*9+8];
        const ux = bx - ax, uy = by - ay, uz = bz - az;
        const vx = cx - ax, vy = cy - ay, vz = cz - az;
        const nx = uy * vz - uz * vy;
        const ny = uz * vx - ux * vz;
        const nz = ux * vy - uy * vx;
        const len = Math.hypot(nx, ny, nz);
        if (len === 0) continue;
        if (ny / len < -COS_45) {
          const triMaxY = Math.max(ay, by, cy);
          if (triMaxY <= minY + bedTol) continue; // bed-contact, no overhang
          overhangPos.push(ax, ay, az, bx, by, bz, cx, cy, cz);
        }
      }
      if (overhangPos.length) {
        const ogeo = new THREE.BufferGeometry();
        ogeo.setAttribute("position", new THREE.BufferAttribute(new Float32Array(overhangPos), 3));
        ogeo.computeVertexNormals();
        const omat = new THREE.MeshStandardMaterial({
          color: supportColor, roughness: 0.5, metalness: 0,
          transparent: true, opacity: 0.95,
          polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -1,
        });
        overlay = new THREE.Mesh(ogeo, omat);
        mesh.add(overlay);
      }
    }

    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);

    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, overlay };

    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);
      }
      geo.dispose(); mat.dispose();
      if (overlay) { overlay.geometry.dispose(); overlay.material.dispose(); }
      discGeo.dispose(); discMat.dispose();
      renderer.dispose();
      if (renderer.domElement.parentNode === mount) mount.removeChild(renderer.domElement);
    };
  }, [arrayBuffer, showSupports]);

  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%" }} />;
};

window.SyncSTL = SyncSTL;

// ─── SHAPE FACTORIES ───────────────────────────────────────────────────────
// Build binary STL ArrayBuffers for primitive shapes. Centred on origin,
// Z-up (STL convention), so the bed sits at z = -h/2.
SyncSTL.buildCubeSTL = function buildCubeSTL(size = 20) {
  const h = size / 2;
  const v = [
    [-h,-h,-h], [ h,-h,-h], [ h, h,-h], [-h, h,-h],
    [-h,-h, h], [ h,-h, h], [ h, h, h], [-h, h, h],
  ];
  // Each face: two CCW-from-outside triangles. Outward normal computed via
  // (v1-v0) × (v2-v0); the declared normal here is just metadata.
  const faces = [
    [[0,2,1],[0,3,2], [0,0,-1]],  // bottom (z=-h, viewed from below)
    [[4,5,6],[4,6,7], [0,0, 1]],  // top
    [[0,1,5],[0,5,4], [0,-1,0]],  // front (y=-h)
    [[3,6,2],[3,7,6], [0, 1,0]],  // back
    [[0,7,3],[0,4,7], [-1,0,0]],  // left
    [[1,6,5],[1,2,6], [ 1,0,0]],  // right
  ];
  const tris = [];
  faces.forEach(f => {
    tris.push({ n: f[2], v: f[0].map(i => v[i]) });
    tris.push({ n: f[2], v: f[1].map(i => v[i]) });
  });
  return triListToSTL(tris);
};

SyncSTL.buildSlabSTL = function buildSlabSTL(w = 80, d = 50, h = 8) {
  return buildBoxSTL(w, d, h);
};

function buildBoxSTL(w, d, h) {
  const x = w/2, y = d/2, z = h/2;
  const v = [
    [-x,-y,-z],[ x,-y,-z],[ x, y,-z],[-x, y,-z],
    [-x,-y, z],[ x,-y, z],[ x, y, z],[-x, y, z],
  ];
  const faces = [
    [[0,2,1],[0,3,2], [0,0,-1]],
    [[4,5,6],[4,6,7], [0,0, 1]],
    [[0,1,5],[0,5,4], [0,-1,0]],
    [[3,6,2],[3,7,6], [0, 1,0]],
    [[0,7,3],[0,4,7], [-1,0,0]],
    [[1,6,5],[1,2,6], [ 1,0,0]],
  ];
  const tris = [];
  faces.forEach(f => {
    tris.push({ n: f[2], v: f[0].map(i => v[i]) });
    tris.push({ n: f[2], v: f[1].map(i => v[i]) });
  });
  return triListToSTL(tris);
}

SyncSTL.buildCylinderSTL = function buildCylinderSTL(radius = 20, height = 30, segments = 48) {
  const tris = [];
  const hz = height / 2;
  for (let i = 0; i < segments; i++) {
    const a0 = (i / segments) * Math.PI * 2;
    const a1 = ((i + 1) / segments) * Math.PI * 2;
    const x0 = Math.cos(a0) * radius, y0 = Math.sin(a0) * radius;
    const x1 = Math.cos(a1) * radius, y1 = Math.sin(a1) * radius;
    // Side
    const nx = Math.cos((a0 + a1) / 2), ny = Math.sin((a0 + a1) / 2);
    tris.push({ n: [nx, ny, 0], v: [[x0,y0,-hz],[x1,y1,-hz],[x1,y1,hz]] });
    tris.push({ n: [nx, ny, 0], v: [[x0,y0,-hz],[x1,y1, hz],[x0,y0,hz]] });
    // Top fan
    tris.push({ n: [0,0, 1], v: [[0,0, hz],[x0,y0, hz],[x1,y1, hz]] });
    // Bottom fan
    tris.push({ n: [0,0,-1], v: [[0,0,-hz],[x1,y1,-hz],[x0,y0,-hz]] });
  }
  return triListToSTL(tris);
};

SyncSTL.buildTubeSTL = function buildTubeSTL(rOuter = 22, rInner = 18, height = 60, segments = 64) {
  const tris = [];
  const hz = height / 2;
  for (let i = 0; i < segments; i++) {
    const a0 = (i / segments) * Math.PI * 2;
    const a1 = ((i + 1) / segments) * Math.PI * 2;
    const xo0 = Math.cos(a0) * rOuter, yo0 = Math.sin(a0) * rOuter;
    const xo1 = Math.cos(a1) * rOuter, yo1 = Math.sin(a1) * rOuter;
    const xi0 = Math.cos(a0) * rInner, yi0 = Math.sin(a0) * rInner;
    const xi1 = Math.cos(a1) * rInner, yi1 = Math.sin(a1) * rInner;
    // outer side
    const nx = Math.cos((a0+a1)/2), ny = Math.sin((a0+a1)/2);
    tris.push({ n: [nx, ny, 0], v: [[xo0,yo0,-hz],[xo1,yo1,-hz],[xo1,yo1,hz]] });
    tris.push({ n: [nx, ny, 0], v: [[xo0,yo0,-hz],[xo1,yo1, hz],[xo0,yo0,hz]] });
    // inner side (normal points in)
    tris.push({ n: [-nx,-ny, 0], v: [[xi0,yi0,-hz],[xi1,yi1, hz],[xi1,yi1,-hz]] });
    tris.push({ n: [-nx,-ny, 0], v: [[xi0,yi0,-hz],[xi0,yi0, hz],[xi1,yi1, hz]] });
    // top ring
    tris.push({ n: [0,0,1], v: [[xi0,yi0, hz],[xo0,yo0, hz],[xo1,yo1, hz]] });
    tris.push({ n: [0,0,1], v: [[xi0,yi0, hz],[xo1,yo1, hz],[xi1,yi1, hz]] });
    // bottom ring (normal down)
    tris.push({ n: [0,0,-1], v: [[xi0,yi0,-hz],[xo1,yo1,-hz],[xo0,yo0,-hz]] });
    tris.push({ n: [0,0,-1], v: [[xi0,yi0,-hz],[xi1,yi1,-hz],[xo1,yo1,-hz]] });
  }
  return triListToSTL(tris);
};

SyncSTL.buildPhoneStandSTL = function buildPhoneStandSTL() {
  // L-shaped phone stand: base + back support
  const tris = [];
  // Base: 80 × 50 × 6
  const baseW = 80, baseD = 50, baseH = 6;
  pushBox(tris, [0, 0, baseH/2], baseW, baseD, baseH);
  // Back: 80 × 8 × 70, tilted 75° offset behind centre
  const backW = 80, backD = 8, backH = 70;
  pushBox(tris, [0, -18, baseH + backH/2], backW, backD, backH);
  return triListToSTL(tris);
};

function pushBox(tris, [cx, cy, cz], w, d, h) {
  const x = w/2, y = d/2, z = h/2;
  const v = [
    [cx-x,cy-y,cz-z],[cx+x,cy-y,cz-z],[cx+x,cy+y,cz-z],[cx-x,cy+y,cz-z],
    [cx-x,cy-y,cz+z],[cx+x,cy-y,cz+z],[cx+x,cy+y,cz+z],[cx-x,cy+y,cz+z],
  ];
  const f = [
    [[0,2,1],[0,3,2], [0,0,-1]],
    [[4,5,6],[4,6,7], [0,0, 1]],
    [[0,1,5],[0,5,4], [0,-1,0]],
    [[3,6,2],[3,7,6], [0, 1,0]],
    [[0,7,3],[0,4,7], [-1,0,0]],
    [[1,6,5],[1,2,6], [ 1,0,0]],
  ];
  f.forEach(fa => {
    tris.push({ n: fa[2], v: fa[0].map(i => v[i]) });
    tris.push({ n: fa[2], v: fa[1].map(i => v[i]) });
  });
}

function triListToSTL(tris) {
  const nTri = tris.length;
  const buffer = new ArrayBuffer(80 + 4 + nTri * 50);
  const view = new DataView(buffer);
  view.setUint32(80, nTri, true);
  let off = 84;
  for (const f of tris) {
    view.setFloat32(off, f.n[0], true); view.setFloat32(off+4, f.n[1], true); view.setFloat32(off+8, f.n[2], true);
    off += 12;
    for (const v of f.v) {
      view.setFloat32(off, v[0], true); view.setFloat32(off+4, v[1], true); view.setFloat32(off+8, v[2], true);
      off += 12;
    }
    view.setUint16(off, 0, true); off += 2;
  }
  return buffer;
}

// ─── SLICE / PRICE ESTIMATOR (shared with upload screen) ───────────────────
// Analytical FDM slice model. Splits part into shell + top/bottom + infill +
// supports, each with its own volumetric flow cap. ~10-15% of PrusaSlicer for
// typical FDM geometry.
SyncSTL.estimateSlice = function estimateSlice(file, settings, material) {
  const totalVolMm3 = file.volCm3 * 1000;
  const surfaceAreaMm2 = file.analysis.surfaceAreaMm2;
  const dimsX = file.dims[0], dimsY = file.dims[1];

  const layerHeight    = parseFloat(settings.layerHeight) || 0.20;
  const infillPct      = settings.infillPct;
  const walls          = settings.walls ?? 3;
  const nozzleWidth    = settings.nozzleWidth ?? 0.4;
  const topBottomLayers = settings.topBottomLayers ?? 4;

  const shellThickness = walls * nozzleWidth;
  let shellVol = Math.min(surfaceAreaMm2 * shellThickness, totalVolMm3 * 0.70);

  const xyArea = Math.max(0.1, dimsX * dimsY);
  const tbThickness = topBottomLayers * layerHeight;
  let tbVol = 2 * tbThickness * xyArea * 0.65;
  tbVol = Math.min(tbVol, Math.max(0, totalVolMm3 - shellVol));

  const remainingVol = Math.max(0, totalVolMm3 - shellVol - tbVol);
  const infillVol = remainingVol * (infillPct / 100);

  // Support is always included — essential for a successful print. Callers may
  // still pass supportsEnabled:false for demo primitives (cube/preset) that
  // never need it.
  const supportsEnabled = settings.supportsEnabled !== false;
  const supportVolMm3 = supportsEnabled ? (file.analysis.supportVolMm3 || 0) : 0;

  const totalExtrudedMm3 = shellVol + tbVol + infillVol + supportVolMm3;
  const massG = (totalExtrudedMm3 / 1000) * material.density;

  // ── Run-time from material-specific linear print speed ───────────────────
  // Volumetric flow = linear_speed × layer_height × line_width, capped at the
  // hot-end's physical ceiling (~30 mm³/s high-flow). Each content type runs at
  // a fraction of the material's max speed (outer walls slow for quality).
  const lineWidth = nozzleWidth * 1.125;                 // ~0.45 for a 0.4 nozzle
  const maxLinear = SyncSTL.PRICING.maxSpeed[material.type] || 200; // mm/s
  const volCap = Math.min(30, maxLinear * layerHeight * lineWidth); // mm³/s

  const outerFlow   = volCap * 0.35;
  const innerFlow   = volCap * 0.70;
  const tbFlow      = volCap * 0.55;
  const infillFlow  = volCap * 1.00;
  const supportFlow = volCap * 0.60;

  const outerVol = Math.min(surfaceAreaMm2 * nozzleWidth, shellVol);
  const innerVol = Math.max(0, shellVol - outerVol);

  const t_outer   = outerVol  / outerFlow;
  const t_inner   = innerVol  / innerFlow;
  const t_tb      = tbVol     / tbFlow;
  const t_infill  = infillVol / infillFlow;
  const t_support = supportVolMm3 / supportFlow;

  const extrusionSec = (t_outer + t_inner + t_tb + t_infill + t_support) * 1.35;
  const fixedSec = 300;
  const totalSec = extrusionSec + fixedSec;

  return {
    massG,
    shellVolMm3: shellVol, shellMassG: (shellVol / 1000) * material.density,
    topBottomVolMm3: tbVol, tbMassG: (tbVol / 1000) * material.density,
    infillVolMm3: infillVol, infillMassG: (infillVol / 1000) * material.density,
    supportVolMm3, supportMassG: (supportVolMm3 / 1000) * material.density,
    totalExtrudedMm3,
    printHours: totalSec / 3600,
    extrusionMinutes: extrusionSec / 60,
    overheadMinutes: fixedSec / 60,
  };
};

// Quick analysis for primitive shapes (closed manifold, no overhangs above bed)
SyncSTL.quickAnalysisForPrimitive = function (dims, volCm3, surfaceAreaMm2) {
  return {
    watertight: true,
    openEdges: 0,
    nonManifoldEdges: 0,
    overhangAreaMm2: 0,
    overhangFraction: 0,
    overhangAngleDeg: 45,
    supportVolMm3: 0,
    needsSupport: false,
    components: 1,
    partIntegrity: true,
    surfaceAreaMm2,
    triangles: 12,
    thinWalls: false,
  };
};

// ─── 3MF SUPPORT ────────────────────────────────────────────────────────────
// 3MF = ZIP archive with 3D/3dmodel.model XML inside. Requires window.fflate.
// Returns a non-indexed BufferGeometry in mm, Z-up (same convention as STL).

// 3MF transform: 12 numbers, row-vector convention.
// p' = [x y z 1] · M where rows are the x/y/z axes + translation.
function __parse3mfTransform(str) {
  if (!str) return null;
  const m = str.trim().split(/\s+/).map(Number);
  return m.length === 12 && m.every(Number.isFinite) ? m : null;
}
function __apply3mfTransform(m, x, y, z) {
  if (!m) return [x, y, z];
  return [
    x * m[0] + y * m[3] + z * m[6] + m[9],
    x * m[1] + y * m[4] + z * m[7] + m[10],
    x * m[2] + y * m[5] + z * m[8] + m[11],
  ];
}
// combined = child then parent (row-vector: p·C·P)
function __compose3mf(parent, child) {
  if (!parent) return child;
  if (!child) return parent;
  const C = child, P = parent, R = new Array(12);
  for (let col = 0; col < 3; col++) {
    R[0 + col] = C[0] * P[0 + col] + C[1] * P[3 + col] + C[2] * P[6 + col];
    R[3 + col] = C[3] * P[0 + col] + C[4] * P[3 + col] + C[5] * P[6 + col];
    R[6 + col] = C[6] * P[0 + col] + C[7] * P[3 + col] + C[8] * P[6 + col];
    R[9 + col] = C[9] * P[0 + col] + C[10] * P[3 + col] + C[11] * P[6 + col] + P[9 + col];
  }
  return R;
}

SyncSTL.parse3MF = function parse3MF(buffer) {
  if (!window.fflate) throw new Error("fflate not loaded");
  const files = window.fflate.unzipSync(new Uint8Array(buffer));
  const keys = Object.keys(files);
  const modelKey =
    keys.find(k => /^3D\/.*\.model$/i.test(k)) ||
    keys.find(k => /\.model$/i.test(k));
  if (!modelKey) throw new Error("No 3D model payload in 3MF");
  const xml = new TextDecoder().decode(files[modelKey]);
  const doc = new DOMParser().parseFromString(xml, "application/xml");
  if (doc.getElementsByTagName("parsererror").length) throw new Error("Bad 3MF XML");

  const modelEl = doc.getElementsByTagName("model")[0];
  const unit = ((modelEl && modelEl.getAttribute("unit")) || "millimeter").toLowerCase();
  const scale = { micron: 0.001, millimeter: 1, centimeter: 10, inch: 25.4, foot: 304.8, meter: 1000 }[unit] ?? 1;

  const objects = {};
  Array.from(doc.getElementsByTagName("object")).forEach(o => { objects[o.getAttribute("id")] = o; });

  const out = [];
  function firstChildByLocal(el, name) {
    for (let c = el.firstElementChild; c; c = c.nextElementSibling) {
      if (c.localName === name) return c;
    }
    return null;
  }
  function addObject(obj, matrix, depth) {
    if (!obj || depth > 8) return;
    const mesh = firstChildByLocal(obj, "mesh");
    if (mesh) {
      const vertsEl = firstChildByLocal(mesh, "vertices");
      const trisEl = firstChildByLocal(mesh, "triangles");
      if (!vertsEl || !trisEl) return;
      const vs = [];
      for (let v = vertsEl.firstElementChild; v; v = v.nextElementSibling) {
        if (v.localName !== "vertex") continue;
        vs.push([+v.getAttribute("x"), +v.getAttribute("y"), +v.getAttribute("z")]);
      }
      for (let t = trisEl.firstElementChild; t; t = t.nextElementSibling) {
        if (t.localName !== "triangle") continue;
        const idx = [+t.getAttribute("v1"), +t.getAttribute("v2"), +t.getAttribute("v3")];
        for (const i of idx) {
          const p = vs[i];
          if (!p) continue;
          const [x, y, z] = __apply3mfTransform(matrix, p[0], p[1], p[2]);
          out.push(x * scale, y * scale, z * scale);
        }
      }
    }
    const compsEl = firstChildByLocal(obj, "components");
    if (compsEl) {
      for (let c = compsEl.firstElementChild; c; c = c.nextElementSibling) {
        if (c.localName !== "component") continue;
        const ref = objects[c.getAttribute("objectid")];
        addObject(ref, __compose3mf(matrix, __parse3mfTransform(c.getAttribute("transform"))), depth + 1);
      }
    }
  }

  const items = Array.from(doc.getElementsByTagName("item"));
  if (items.length) {
    items.forEach(it => addObject(objects[it.getAttribute("objectid")], __parse3mfTransform(it.getAttribute("transform")), 0));
  } else {
    Object.values(objects).forEach(o => addObject(o, null, 0));
  }
  if (!out.length) throw new Error("3MF contained no triangles");

  const geo = new THREE.BufferGeometry();
  geo.setAttribute("position", new THREE.BufferAttribute(new Float32Array(out), 3));
  geo.computeVertexNormals();
  return geo;
};

// ─── STEP SUPPORT (lazy OpenCascade WASM kernel) ────────────────────────────
const OCCT_BASE = "https://unpkg.com/occt-import-js@0.0.23/dist/";
SyncSTL.loadOcct = async function loadOcct() {
  if (SyncSTL._occt) return SyncSTL._occt;
  if (SyncSTL._occtLoading) return SyncSTL._occtLoading;
  SyncSTL._occtLoading = (async () => {
    if (!window.occtimportjs) {
      await new Promise((res, rej) => {
        const s = document.createElement("script");
        s.src = OCCT_BASE + "occt-import-js.js";
        s.onload = res;
        s.onerror = () => rej(new Error("CAD kernel script failed to load"));
        document.head.appendChild(s);
      });
    }
    SyncSTL._occt = await window.occtimportjs({ locateFile: (f) => OCCT_BASE + f });
    return SyncSTL._occt;
  })();
  return SyncSTL._occtLoading;
};

SyncSTL.parseSTEP = async function parseSTEP(buffer) {
  const occt = await SyncSTL.loadOcct();
  const res = occt.ReadStepFile(new Uint8Array(buffer), null);
  if (!res || !res.success || !res.meshes || !res.meshes.length) {
    throw new Error("STEP read failed");
  }
  const positions = [];
  for (const m of res.meshes) {
    const pos = m.attributes && m.attributes.position && m.attributes.position.array;
    if (!pos) continue;
    const idx = m.index && m.index.array;
    if (idx) {
      for (let i = 0; i < idx.length; i++) {
        const j = idx[i] * 3;
        positions.push(pos[j], pos[j + 1], pos[j + 2]);
      }
    } else {
      for (let i = 0; i < pos.length; i++) positions.push(pos[i]);
    }
  }
  if (!positions.length) throw new Error("STEP produced no mesh");
  const geo = new THREE.BufferGeometry();
  geo.setAttribute("position", new THREE.BufferAttribute(new Float32Array(positions), 3));
  geo.computeVertexNormals();
  return geo;
};

// ─── Geometry → binary STL (so converted formats reuse the whole pipeline) ──
SyncSTL.geometryToSTL = function geometryToSTL(geo) {
  const pos = geo.attributes.position.array;
  const nTri = Math.floor(pos.length / 9);
  const buffer = new ArrayBuffer(84 + nTri * 50);
  const view = new DataView(buffer);
  view.setUint32(80, nTri, true);
  let off = 84;
  for (let i = 0; i < nTri; i++) {
    const ax = pos[i*9],   ay = pos[i*9+1], az = pos[i*9+2];
    const bx = pos[i*9+3], by = pos[i*9+4], bz = pos[i*9+5];
    const cx = pos[i*9+6], cy = pos[i*9+7], cz = pos[i*9+8];
    let nx = (by-ay)*(cz-az) - (bz-az)*(cy-ay);
    let ny = (bz-az)*(cx-ax) - (bx-ax)*(cz-az);
    let nz = (bx-ax)*(cy-ay) - (by-ay)*(cx-ax);
    const len = Math.hypot(nx, ny, nz) || 1;
    nx /= len; ny /= len; nz /= len;
    view.setFloat32(off, nx, true); view.setFloat32(off+4, ny, true); view.setFloat32(off+8, nz, true);
    off += 12;
    view.setFloat32(off, ax, true); view.setFloat32(off+4, ay, true); view.setFloat32(off+8, az, true); off += 12;
    view.setFloat32(off, bx, true); view.setFloat32(off+4, by, true); view.setFloat32(off+8, bz, true); off += 12;
    view.setFloat32(off, cx, true); view.setFloat32(off+4, cy, true); view.setFloat32(off+8, cz, true); off += 12;
    view.setUint16(off, 0, true); off += 2;
  }
  return buffer;
};
