// tag3d.jsx — interactive 3D preview of the keychain tag (three.js).
// Shows the plate colour + QR relief on BOTH faces (recto-verso), so the
// client sees it is double-sided. Auto-rotates, drag to spin, resumes auto
// after idle. TagStage wraps it with a 3D / 2D toggle and falls back to the
// flat PhotoTagPreview when WebGL/three is unavailable.
// Exports to window: Tag3DPreview, TagStage

(function () {
  const THREE = window.THREE;

  // ---- base STL (the real printed tag geometry) ----
  // plate+ring span x∈[0,46], y∈[140,177], z∈[0,4] (mm); ring on the +x side.
  let _stlPromise = null;
  function loadBaseGeometry() {
    if (!_stlPromise) {
      _stlPromise = fetch("assets/base-tag.stl")
        .then((r) => { if (!r.ok) throw new Error("base STL not found"); return r.arrayBuffer(); })
        .then((buf) => {
          const geo = new THREE.STLLoader().parse(buf);
          // planar UVs so the print-texture bump map has coordinates to live on
          const pos = geo.attributes.position;
          const uv = new Float32Array(pos.count * 2);
          for (let i = 0; i < pos.count; i++) {
            uv[i * 2] = pos.getX(i) * 0.045;
            uv[i * 2 + 1] = pos.getY(i) * 0.045;
          }
          geo.setAttribute("uv", new THREE.BufferAttribute(uv, 2));
          return geo;
        });
    }
    return _stlPromise;
  }

  // ---- geometry helpers -------------------------------------------------
  function qrMatrix(text) {
    try {
      const qr = window.qrcode(0, "M");
      qr.addData(text && text.length ? text : " ");
      qr.make();
      const n = qr.getModuleCount();
      const m = [];
      for (let r = 0; r < n; r++) {
        const row = [];
        for (let c = 0; c < n; c++) row.push(qr.isDark(r, c));
        m.push(row);
      }
      return m;
    } catch (e) { return null; }
  }

  // subtle diagonal cross-hatch print texture (like the real product photos)
  let _hatchTex = null;
  function hatchTexture() {
    if (_hatchTex) return _hatchTex;
    const c = document.createElement("canvas"); c.width = 512; c.height = 512;
    const x = c.getContext("2d");
    x.fillStyle = "#808080"; x.fillRect(0, 0, 512, 512);
    x.strokeStyle = "#6e6e6e"; x.lineWidth = 2;
    for (let i = -512; i < 1024; i += 7) {
      x.beginPath(); x.moveTo(i, 0); x.lineTo(i + 512, 512); x.stroke();
      x.beginPath(); x.moveTo(i + 512, 0); x.lineTo(i, 512); x.stroke();
    }
    x.strokeStyle = "#8f8f8f"; x.lineWidth = 1;
    for (let i = -512; i < 1024; i += 7) {
      x.beginPath(); x.moveTo(i + 3, 0); x.lineTo(i + 515, 512); x.stroke();
    }
    const t = new THREE.CanvasTexture(c);
    t.wrapS = t.wrapT = THREE.RepeatWrapping;
    _hatchTex = t;
    return t;
  }

  // plate colour → physical material tuning
  function plateMaterial(colorId, hex) {
    const col = new THREE.Color(hex);
    const p = { color: col, roughness: 0.62, metalness: 0.04, clearcoat: 0.25, clearcoatRoughness: 0.6,
      bumpMap: hatchTexture(), bumpScale: 0.12 };
    if (colorId === "gold") { p.metalness = 0.9; p.roughness = 0.4; p.clearcoat = 0.4; p.bumpScale = 0.16; }
    else if (colorId === "clear") { p.transmission = 0.72; p.roughness = 0.22; p.metalness = 0; p.transparent = true; p.opacity = 0.85; p.thickness = 3; p.ior = 1.45; p.clearcoat = 0.5; }
    else if (colorId === "white") { p.roughness = 0.7; }
    else if (colorId === "black") { p.roughness = 0.5; }
    return new THREE.MeshPhysicalMaterial(p);
  }

  // build the whole tag as a THREE.Group centred at origin, using the REAL
  // printed geometry (assets/base-tag.stl) + QR relief placed exactly like
  // the print generator (stl.jsx): recto on top, verso mirrored about the
  // plate centre.
  const STL = { qrX0: 3.0, qrY0: 143.0, qrSize: 31.0, zTop: 4.0, zBottom: 0.0, relief: 0.8, mirrorX: 37.0,
                cx: 23.0, cy: 158.5, cz: 2.0 };
  function buildTag(opts, baseGeo) {
    const { text, plateId, plateHex, codeHex } = opts;
    const plateMat = plateMaterial(plateId, plateHex);
    const codeMat = new THREE.MeshPhysicalMaterial({ color: new THREE.Color(codeHex), roughness: 0.55, metalness: 0.04, clearcoat: 0.2 });

    const inner = new THREE.Group();
    inner.add(new THREE.Mesh(baseGeo, plateMat));

    const m = qrMatrix(text);
    if (m) {
      const n = m.length;
      const ms = STL.qrSize / n;
      const boxGeo = new THREE.BoxGeometry(ms, ms, STL.relief);
      let dark = 0;
      for (let r = 0; r < n; r++) for (let c = 0; c < n; c++) if (m[r][c]) dark++;
      const front = new THREE.InstancedMesh(boxGeo, codeMat, dark);
      const back = new THREE.InstancedMesh(boxGeo, codeMat, dark);
      const dummy = new THREE.Object3D();
      let i = 0;
      for (let r = 0; r < n; r++) {
        for (let c = 0; c < n; c++) {
          if (!m[r][c]) continue;
          // laid out so the QR reads upright when the ring points up on screen
          const x = STL.qrX0 + STL.qrSize - (r + 0.5) * ms;
          const y = STL.qrY0 + STL.qrSize - (c + 0.5) * ms;
          dummy.position.set(x, y, STL.zTop + STL.relief / 2 - 0.02);
          dummy.updateMatrix();
          front.setMatrixAt(i, dummy.matrix);
          // verso: mirrored about the plate centre, like the printed model
          dummy.position.set(STL.mirrorX - x, y, STL.zBottom - STL.relief / 2 + 0.02);
          dummy.updateMatrix();
          back.setMatrixAt(i, dummy.matrix);
          i++;
        }
      }
      front.instanceMatrix.needsUpdate = true;
      back.instanceMatrix.needsUpdate = true;
      inner.add(front); inner.add(back);
    }

    inner.position.set(-STL.cx, -STL.cy, -STL.cz);
    const group = new THREE.Group();
    group.add(inner);
    group.rotation.z = Math.PI / 2;   // ring (+x in the STL) points up

    group.userData.dispose = function () {
      plateMat.dispose(); codeMat.dispose();
      group.traverse(function (o) { if (o.geometry && o.geometry !== baseGeo) o.geometry.dispose(); });
    };
    return group;
  }

  // ---- React component --------------------------------------------------
  function Tag3DPreview({ text, plateId, plateHex, codeHex, size = 260, brightness = 1, contrast = 1, saturation = 1, relief = 1, lightAngle = -27 }) {
    const mountRef = React.useRef(null);
    const stateRef = React.useRef(null);

    // one-time scene setup
    React.useEffect(function () {
      const mount = mountRef.current;
      if (!mount || !THREE) return;
      const W = size, H = Math.round(size * 1.12);
      const scene = new THREE.Scene();
      const camera = new THREE.PerspectiveCamera(32, W / H, 0.1, 500);
      camera.position.set(0, 2, 118);

      let renderer;
      try {
        renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, preserveDrawingBuffer: true });
      } catch (e) { return; }
      renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
      renderer.setSize(W, H);
      renderer.outputEncoding = THREE.sRGBEncoding;
      mount.appendChild(renderer.domElement);
      renderer.domElement.style.cursor = "grab";
      renderer.domElement.style.touchAction = "pan-y";

      // lights (base intensities × brightness tweak)
      const hemi = new THREE.HemisphereLight(0xffffff, 0x9099a8, 0.85); scene.add(hemi);
      const key = new THREE.DirectionalLight(0xffffff, 1.15); key.position.set(-40, 60, 80); scene.add(key);
      const fill = new THREE.DirectionalLight(0xffffff, 0.5); fill.position.set(50, 10, 40); scene.add(fill);
      const rim = new THREE.DirectionalLight(0xffffff, 0.6); rim.position.set(0, -20, -80); scene.add(rim);
      const lights = [[hemi, 0.85], [key, 1.15], [fill, 0.5], [rim, 0.6]];
      const named = { hemi, key, fill, rim };

      const pivot = new THREE.Group();
      scene.add(pivot);

      const st = {
        renderer, scene, camera, pivot, tag: null, lights, named,
        auto: true, idle: 0, velY: 0.006, dragging: false, lastX: 0, lastY: 0,
        rotX: -0.12, raf: 0, W, H,
      };
      stateRef.current = st;

      // interaction
      const el = renderer.domElement;
      function down(e) {
        st.dragging = true; st.auto = false; st.idle = 0;
        el.style.cursor = "grabbing";
        const p = e.touches ? e.touches[0] : e;
        st.lastX = p.clientX; st.lastY = p.clientY;
      }
      function move(e) {
        if (!st.dragging) return;
        const p = e.touches ? e.touches[0] : e;
        const dx = p.clientX - st.lastX, dy = p.clientY - st.lastY;
        st.lastX = p.clientX; st.lastY = p.clientY;
        pivot.rotation.y += dx * 0.01;
        st.rotX = Math.max(-0.9, Math.min(0.9, st.rotX + dy * 0.008));
        st.velY = dx * 0.01;
        if (e.cancelable) e.preventDefault();
      }
      function up() { st.dragging = false; st.idle = 0; el.style.cursor = "grab"; }
      el.addEventListener("mousedown", down);
      window.addEventListener("mousemove", move);
      window.addEventListener("mouseup", up);
      el.addEventListener("touchstart", down, { passive: true });
      el.addEventListener("touchmove", move, { passive: false });
      window.addEventListener("touchend", up);

      function loop() {
        st.raf = requestAnimationFrame(loop);
        if (st.auto) {
          pivot.rotation.y += st.velY;
        } else {
          st.idle += 1;
          if (!st.dragging && st.idle > 150) { st.auto = true; st.velY = 0.006; }
          // ease residual momentum
          if (!st.dragging) { pivot.rotation.y += st.velY; st.velY *= 0.94; }
        }
        pivot.rotation.x += (st.rotX - pivot.rotation.x) * 0.1;
        renderer.render(scene, camera);
      }
      loop();

      st._cleanup = function () {
        cancelAnimationFrame(st.raf);
        el.removeEventListener("mousedown", down);
        window.removeEventListener("mousemove", move);
        window.removeEventListener("mouseup", up);
        el.removeEventListener("touchstart", down);
        el.removeEventListener("touchmove", move);
        window.removeEventListener("touchend", up);
        if (st.tag && st.tag.userData.dispose) st.tag.userData.dispose();
        renderer.dispose();
        if (el.parentNode) el.parentNode.removeChild(el);
      };
      return function () { st._cleanup && st._cleanup(); stateRef.current = null; };
    }, [size]);

    // rebuild tag when content/colours change (base geometry loads once, then is reused)
    React.useEffect(function () {
      let stale = false;
      loadBaseGeometry().then(function (geo) {
        const st = stateRef.current;
        if (stale || !st) return;
        if (st.tag) { st.pivot.remove(st.tag); if (st.tag.userData.dispose) st.tag.userData.dispose(); }
        const tag = buildTag({ text, plateId, plateHex, codeHex }, geo);
        st.pivot.add(tag);
        st.tag = tag;
      }).catch(function () {});
      return function () { stale = true; };
    }, [text, plateId, plateHex, codeHex]);

    // brightness + relief tweaks → light intensities.
    // relief > 1: stronger key/rim, softer ambient → marked shadows, shape
    // reads better. relief < 1: flatter, softer lighting.
    React.useEffect(function () {
      const st = stateRef.current;
      if (!st || !st.named) return;
      const b = Math.max(0.1, brightness || 1);
      const r = Math.max(0, Math.min(2, relief == null ? 1 : relief));
      st.named.hemi.intensity = 0.85 * b * Math.max(0.3, 1.3 - 0.3 * r);
      st.named.key.intensity  = 1.15 * b * r;
      st.named.fill.intensity = 0.5  * b * Math.max(0.3, 1.4 - 0.4 * r);
      st.named.rim.intensity  = 0.6  * b * r;
    }, [brightness, relief]);

    // light angle tweak → orbit the key light around the tag
    React.useEffect(function () {
      const st = stateRef.current;
      if (!st || !st.named) return;
      const rad = ((lightAngle == null ? -27 : lightAngle) * Math.PI) / 180;
      st.named.key.position.set(Math.sin(rad) * 89, 60, Math.cos(rad) * 89);
    }, [lightAngle]);

    // contrast + saturation tweaks → CSS filter on the canvas
    React.useEffect(function () {
      const st = stateRef.current;
      if (!st) return;
      const c = Math.max(0.1, contrast || 1);
      const s = Math.max(0, saturation == null ? 1 : saturation);
      const parts = [];
      if (c !== 1) parts.push("contrast(" + c + ")");
      if (s !== 1) parts.push("saturate(" + s + ")");
      st.renderer.domElement.style.filter = parts.join(" ");
    }, [contrast, saturation]);

    return React.createElement("div", { ref: mountRef, style: { width: size, height: Math.round(size * 1.12), display: "grid", placeItems: "center" } });
  }

  // ---- toggle wrapper ---------------------------------------------------
  const canWebGL = (function () {
    try { const c = document.createElement("canvas"); return !!(window.THREE && (c.getContext("webgl") || c.getContext("experimental-webgl"))); }
    catch (e) { return false; }
  })();

  function TagStage({ text, plateId, plateHex, codeHex, size = 230, lang, brightness, contrast, saturation, relief, lightAngle }) {
    const [mode, setMode] = React.useState(canWebGL ? "3d" : "2d");
    const is3d = mode === "3d" && canWebGL;
    const L = (fr, en) => (lang === "en" ? en : fr);

    return React.createElement("div", { className: "tagstage" },
      is3d
        ? React.createElement(React.Fragment, null,
            React.createElement(window.Tag3DPreview, { text, plateId, plateHex, codeHex, size: size + 40, brightness, contrast, saturation, relief, lightAngle }),
            React.createElement("div", { className: "tagstage__hint" },
              React.createElement("span", { className: "tagstage__rv" }, L("Recto-verso", "Double-sided")),
              React.createElement("span", null, L("Glissez pour tourner", "Drag to rotate"))),
          )
        : React.createElement(window.PhotoTagPreview, { text, colorId: plateId, code: codeHex, size }),
    );
  }

  Object.assign(window, { Tag3DPreview, TagStage });
})();
