/* eslint-disable */
// ============================================================
// ASTEM — 3D tooth chart (Three.js)
// Orbit + zoom + pan controls; camera presets; click to select.
// ============================================================

const { useRef: use3DRef, useEffect: use3DEffect, useState: use3DState } = React;

// --- shared dimensions per tooth type ---
const TOOTH_DIMS_3D = {
  incisor: { w: 0.34, d: 0.26, cH: 0.42, rH: 0.66 },
  canine: { w: 0.34, d: 0.36, cH: 0.52, rH: 0.82 },
  premolar: { w: 0.46, d: 0.50, cH: 0.42, rH: 0.66 },
  molar: { w: 0.66, d: 0.58, cH: 0.46, rH: 0.62 }
};

const ARCH = { X: 2.05, Z: 2.6, Y: 0.65 };

function stateColors3D(p) {
  if (!p) return { kind: "natural" };
  const pr = p.prothese || {};
  if (pr.implant) return { kind: "implant" };
  if (pr.couronne) return { kind: "crown" };
  if (pr.bridge) return { kind: "bridge", role: p.bridge_role || "anchor" };
  if (p.programme === "absente") return { kind: "absent" };
  if (p.programme === "extraction") return { kind: "extraction" };
  return { kind: "natural" };
}

function makeTooth3DGeometry(type) {
  const d = TOOTH_DIMS_3D[type];
  // Crown — squished sphere with type-specific shaping
  const crownGeo = new THREE.SphereGeometry(0.5, 24, 18);
  // Anatomical shaping per type (cusps, ridges, taper)
  const pos = crownGeo.attributes.position;
  for (let i = 0; i < pos.count; i++) {
    let x = pos.getX(i),y = pos.getY(i),z = pos.getZ(i);
    if (type === "molar") {
      // Flatten the occlusal table and carve 4 cusps with a central fossa
      if (y > 0.25) {
        const tFlat = (y - 0.25) / 0.25;
        y = 0.25 + tFlat * 0.18;
        // 4 cusps via cos(2x) * cos(2z) bumps
        const cuspW = Math.cos(x * Math.PI * 2.4) * Math.cos(z * Math.PI * 2.4);
        y += 0.07 * cuspW * tFlat;
        // central groove
        const groove = Math.exp(-((x * x + z * z) * 30));
        y -= 0.05 * groove * tFlat;
      }
    } else if (type === "premolar") {
      // 2 cusps buccal+lingual (along z axis), small central groove
      if (y > 0.25) {
        const tFlat = (y - 0.25) / 0.25;
        y = 0.25 + tFlat * 0.22;
        const cusp = Math.cos(z * Math.PI * 1.8);
        y += 0.09 * cusp * tFlat;
        const groove = Math.exp(-(x * x * 80));
        y -= 0.04 * groove * tFlat;
      }
    } else if (type === "canine") {
      // Single sharp cusp at the tip
      if (y > 0.30) {
        const tip = (y - 0.30) / 0.20;
        x *= 1 - 0.45 * tip;
        z *= 1 - 0.30 * tip;
        y = 0.30 + tip * 0.22;
      }
    } else if (type === "incisor") {
      // Flat blade tapering to a slightly mamelonated edge
      if (y > 0.30) {
        const tip = (y - 0.30) / 0.20;
        x *= 1 - 0.25 * tip;
        z *= 1 - 0.55 * tip;
        // tiny mamelons along the incisal edge
        const mam = Math.sin(x * Math.PI * 6) * 0.015 * tip;
        y += mam;
      }
    }
    pos.setXYZ(i, x, y, z);
  }
  crownGeo.computeVertexNormals();
  crownGeo.scale(d.w, d.cH, d.d);
  // Lift crown so base is at y=0
  crownGeo.translate(0, d.cH * 0.5, 0);

  // Crown vertex colors — realistic enamel gradient: amber-ivory at cervical → bright white-grey at incisal
  const colors = [];
  const colCervical = new THREE.Color(0xD4AA6E); // riche et ambré à la jonction email-cément
  const colMid = new THREE.Color(0xEDD9A8); // ivoire moyen
  const colIncisal = new THREE.Color(0xF8F2E8); // blanc cassé à la pointe
  const colorPos = crownGeo.attributes.position;
  const yMin = 0;
  const yMax = d.cH;
  for (let i = 0; i < colorPos.count; i++) {
    const y = colorPos.getY(i);
    const t = Math.max(0, Math.min(1, (y - yMin) / (yMax - yMin)));
    const c = t < 0.5 ?
    colCervical.clone().lerp(colMid, t * 2) :
    colMid.clone().lerp(colIncisal, (t - 0.5) * 2);
    colors.push(c.r, c.g, c.b);
  }
  crownGeo.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3));

  return { crownGeo, d };
}

function makeRoot3DGeometry(d, type) {
  // Tapered cone root. Molars get a wider, more pronounced taper.
  const topR = type === "molar" ? d.w * 0.45 : d.w * 0.42;
  const botR = type === "molar" ? d.w * 0.12 : d.w * 0.18;
  const segs = type === "molar" ? 14 : 12;
  const rootGeo = new THREE.CylinderGeometry(topR, botR, d.rH, segs);
  // Add a subtle waist / cervical concavity by displacing vertices near the top
  const pos = rootGeo.attributes.position;
  for (let i = 0; i < pos.count; i++) {
    const x = pos.getX(i),y = pos.getY(i),z = pos.getZ(i);
    // Cervical line is at y=0 (top of cylinder before translate)
    if (Math.abs(y - d.rH * 0.5) < 0.04) {
      // pinch a tiny bit at the cervical join
      pos.setXYZ(i, x * 0.96, y, z * 0.96);
    }
  }
  rootGeo.computeVertexNormals();
  rootGeo.translate(0, -d.rH * 0.5, 0);
  return rootGeo;
}

function buildTooth3D(num, upper) {
  const type = toothType(num);
  const { crownGeo, d } = makeTooth3DGeometry(type);
  const rootGeo = makeRoot3DGeometry(d, type);

  const group = new THREE.Group();
  group.userData.num = num;
  group.userData.upper = upper;
  group.userData.type = type;

  const crownMat = new THREE.MeshPhysicalMaterial({
    color: 0xF0DDB0, // ivoire chaud, plus saturé
    vertexColors: true,
    roughness: 0.18, // surface lisse (émail poli)
    metalness: 0.0,
    clearcoat: 0.92, // couche vitreuse de l'émail
    clearcoatRoughness: 0.06, // très lisse
    sheen: 0.22,
    sheenRoughness: 0.48,
    sheenColor: new THREE.Color(0xFFF8F0),
    envMapIntensity: 1.4, // reflections plus prononcées
    emissive: 0x000000,
    emissiveIntensity: 0
  });
  const rootMat = new THREE.MeshPhysicalMaterial({
    color: 0xF2E8D5, // ivoire blanc — racine
    roughness: 0.55,
    metalness: 0.0,
    clearcoat: 0.12,
    clearcoatRoughness: 0.50
  });

  const crown = new THREE.Mesh(crownGeo, crownMat);
  const root = new THREE.Mesh(rootGeo, rootMat);
  group.add(root);
  group.add(crown);

  // Outline overlay — shown when tooth is "absent" (ghost contour).
  // Builds edges for BOTH crown and root, in a single LineSegments mesh.
  const crownEdges = new THREE.EdgesGeometry(crownGeo, 14);
  const rootEdges = new THREE.EdgesGeometry(rootGeo, 14);
  // Merge by collecting both edge arrays manually
  const ce = crownEdges.attributes.position.array;
  const re = rootEdges.attributes.position.array;
  const all = new Float32Array(ce.length + re.length);
  all.set(ce, 0);
  all.set(re, ce.length);
  const mergedEdges = new THREE.BufferGeometry();
  mergedEdges.setAttribute("position", new THREE.BufferAttribute(all, 3));
  const outlineMat = new THREE.LineBasicMaterial({
    color: 0x3D2EB8,
    transparent: true,
    opacity: 1.0
  });
  const outline = new THREE.LineSegments(mergedEdges, outlineMat);
  outline.visible = false;
  outline.renderOrder = 5;
  group.add(outline);

  // 2nd halo outline — slightly bigger to thicken the silhouette
  const outline2 = new THREE.LineSegments(mergedEdges.clone(), new THREE.LineBasicMaterial({
    color: 0x6A5FCC, transparent: true, opacity: 0.55
  }));
  outline2.visible = false;
  outline2.scale.set(1.08, 1.08, 1.08);
  outline2.renderOrder = 4;
  group.add(outline2);

  // 3rd halo — outermost soft glow band
  const outline3 = new THREE.LineSegments(mergedEdges.clone(), new THREE.LineBasicMaterial({
    color: 0x9D95D0, transparent: true, opacity: 0.32
  }));
  outline3.visible = false;
  outline3.scale.set(1.16, 1.16, 1.16);
  outline3.renderOrder = 3;
  group.add(outline3);

  // Implant screw — built but hidden by default
  const screwGroup = new THREE.Group();
  // tapered body
  const body = new THREE.Mesh(
    new THREE.CylinderGeometry(d.w * 0.34, d.w * 0.22, d.rH * 0.95, 16),
    new THREE.MeshPhysicalMaterial({ color: 0xB6B6C0, roughness: 0.28, metalness: 0.72, clearcoat: 0.3 })
  );
  body.position.y = -d.rH * 0.475;
  screwGroup.add(body);
  // Threads — small tori at intervals
  for (let i = 1; i <= 6; i++) {
    const ringY = -d.rH * 0.95 * (i / 7);
    const radius = d.w * (0.34 - (0.34 - 0.22) * (i / 7));
    const t = new THREE.Mesh(
      new THREE.TorusGeometry(radius * 1.05, d.w * 0.04, 6, 18),
      new THREE.MeshPhysicalMaterial({ color: 0xA0A0AC, roughness: 0.32, metalness: 0.75, clearcoat: 0.25 })
    );
    t.rotation.x = Math.PI / 2;
    t.position.y = ringY;
    screwGroup.add(t);
  }
  // abutment
  const abut = new THREE.Mesh(
    new THREE.CylinderGeometry(d.w * 0.22, d.w * 0.28, d.cH * 0.4, 12),
    new THREE.MeshPhysicalMaterial({ color: 0x999AA6, roughness: 0.25, metalness: 0.78, clearcoat: 0.35 })
  );
  abut.position.y = d.cH * 0.2;
  screwGroup.add(abut);

  screwGroup.visible = false;
  group.add(screwGroup);

  // ── Halo de sélection — deux meshes FrontSide bleus légèrement agrandis ──
  // Rendus avant la vraie dent (renderOrder 9 < 10) : la dent couvre le centre
  // et ne laisse visible que le liseré bleu en bordure. Pas de BackSide, pas
  // de conflits Z.
  const haloMatBlue = new THREE.MeshBasicMaterial({
    color: 0x3B82F6,
    depthTest: true,   // respecte le Z des autres objets
    depthWrite: false, // n'écrit pas dans le depth buffer → la dent (renderOrder 0) peint par-dessus
  });
  const haloCrown = new THREE.Mesh(crownGeo.clone(), haloMatBlue);
  haloCrown.scale.set(1.13, 1.13, 1.13);
  haloCrown.position.y = -d.cH * 0.065;
  haloCrown.renderOrder = 1; // après les autres dents (0) → leur depth est écrit, halo respecte l'occlusion
  haloCrown.visible = false;
  group.add(haloCrown);

  const haloRoot = new THREE.Mesh(rootGeo.clone(), haloMatBlue.clone());
  haloRoot.scale.set(1.13, 1.13, 1.13);
  haloRoot.position.y = d.rH * 0.065;
  haloRoot.renderOrder = 1;
  haloRoot.visible = false;
  group.add(haloRoot);

  group.userData.parts = { crown, root, screwGroup, outline, outline2, outline3, outlineMat, d, type, crownMat, rootMat, haloCrown, haloRoot };

  // ── Guide chirurgical ring — cervical junction ──
  const ringR = (d.w + d.d) * 0.17;
  const ringGeoG = new THREE.TorusGeometry(ringR, ringR * 0.20, 10, 28);
  ringGeoG.rotateX(Math.PI / 2);
  const ringMatG = new THREE.MeshPhysicalMaterial({
    color: 0x3B82F6, emissive: 0x3B82F6, emissiveIntensity: 0.6,
    roughness: 0.22, metalness: 0.05, transparent: true, opacity: 0.92
  });
  const guideRing = new THREE.Mesh(ringGeoG, ringMatG);
  guideRing.position.y = 0;
  guideRing.visible = false;
  guideRing.renderOrder = 3;
  group.add(guideRing);
  group.userData.parts.guideRing = guideRing;
  group.userData.parts.ringMat = ringMatG;

  // Invisible hit sphere — much more reliable for raycasting than complex geometry
  const hitGeo = new THREE.SphereGeometry(0.45, 8, 6);
  const hitMat = new THREE.MeshBasicMaterial({ transparent: true, opacity: 0, depthWrite: false });
  const hitSphere = new THREE.Mesh(hitGeo, hitMat);
  hitSphere.position.y = d.cH * 0.2;
  hitSphere.userData.num = num;
  hitSphere.userData.isHitSphere = true;
  group.add(hitSphere);

  if (upper) group.scale.y = -1;
  return group;
}

function applyCrownStyle(parts, style) {
  // style: { color, vertexColors, roughness, metalness, clearcoat, sheen, emissive, emissiveIntensity, opacity }
  const m = parts.crownMat;
  m.color.setHex(style.color);
  m.vertexColors = !!style.vertexColors;
  m.roughness = style.roughness ?? 0.30;
  m.metalness = style.metalness ?? 0.0;
  m.clearcoat = style.clearcoat ?? 0.65;
  m.clearcoatRoughness = style.clearcoatRoughness ?? 0.16;
  m.sheen = style.sheen ?? 0.18;
  m.sheenColor.setHex(style.sheenColor ?? 0xFFF6E6);
  m.transparent = !!style.transparent;
  m.opacity = style.opacity ?? 1;
  m.emissive.setHex(style.emissive ?? 0x000000);
  m.emissiveIntensity = style.emissiveIntensity ?? 0;
  m.needsUpdate = true;
}

function applyRootStyle(parts, color) {
  const m = parts.rootMat;
  m.color.setHex(color);
  m.transparent = false;
  m.opacity = 1;
  m.needsUpdate = true;
}

function updateTooth3D(mesh, p, isSelected, isHovered) {
  const { parts } = mesh.userData;
  if (!parts) return;
  const c = stateColors3D(p);

  // Reset visibility defaults
  parts.crown.visible = true;
  parts.root.visible = true;
  parts.screwGroup.visible = false;
  parts.outline.visible = false;
  if (parts.outline2) parts.outline2.visible = false;
  if (parts.outline3) parts.outline3.visible = false;
  if (parts.haloCrown) parts.haloCrown.visible = false;
  if (parts.haloRoot)  parts.haloRoot.visible  = false;
  parts.crown.renderOrder = 0;
  parts.root.renderOrder  = 0;
  mesh.visible = true;

  const yDir = mesh.userData.upper ? -1 : 1;
  const s = isSelected ? 1.04 : 1.0;
  mesh.scale.set(s, yDir * s, s);

  // Determine base style per kind
  switch (c.kind) {
    case "absent":{
        // Translucent ghost body + crisp 3-layer outline (crown AND root)
        parts.crown.visible = true;
        parts.root.visible = true;
        applyCrownStyle(parts, {
          color: 0x6A5FCC,
          vertexColors: false,
          roughness: 0.6,
          metalness: 0,
          clearcoat: 0.1,
          clearcoatRoughness: 0.5,
          sheen: 0,
          transparent: true,
          opacity: isSelected ? 0.28 : 0.18
        });
        // Root: matching translucent ghost
        parts.rootMat.color.setHex(0x6A5FCC);
        parts.rootMat.transparent = true;
        parts.rootMat.opacity = isSelected ? 0.28 : 0.18;
        parts.rootMat.needsUpdate = true;

        parts.outline.visible = true;
        parts.outlineMat.color.setHex(isSelected ? 0x3B82F6 : 0x3D2EB8);
        parts.outlineMat.opacity = 1;
        if (parts.outline2) {
          parts.outline2.visible = true;
          parts.outline2.material.color.setHex(isSelected ? 0x60A5FA : 0x6A5FCC);
          parts.outline2.material.opacity = isSelected ? 0.65 : 0.55;
        }
        if (parts.outline3) {
          parts.outline3.visible = true;
          parts.outline3.material.color.setHex(isSelected ? 0x93C5FD : 0x9D95D0);
          parts.outline3.material.opacity = isSelected ? 0.35 : 0.32;
        }
        return;
      }
    case "extraction":{
        // Solid red/orange
        applyCrownStyle(parts, {
          color: 0xE85029,
          vertexColors: false,
          roughness: 0.45,
          metalness: 0,
          clearcoat: 0.35,
          clearcoatRoughness: 0.25,
          sheen: 0.0
        });
        applyRootStyle(parts, 0xC23F22);
        break;
      }
    case "crown":{
        // Doré métallique
        applyCrownStyle(parts, {
          color: 0xD4A820, // or doré — couronne
          vertexColors: false,
          roughness: 0.14,
          metalness: 0.92,
          clearcoat: 0.50,
          clearcoatRoughness: 0.10,
          sheen: 0.0,
          emissive: 0x7A5800,
          emissiveIntensity: 0.22
        });
        applyRootStyle(parts, 0xE2D0B6);
        break;
      }
    case "implant":{
        const isNeighbor = p != null && p.implant_group_root != null;
        parts.root.visible = false; // no root cone for any implant tooth
        parts.screwGroup.visible = !isNeighbor; // screw only on pilier/unaire tooth
        mesh.userData._isImplant = true;
        applyCrownStyle(parts, {
          color: 0x22C55E, // vert vif — implant
          vertexColors: false,
          roughness: 0.20,
          metalness: 0.04,
          clearcoat: 0.78,
          clearcoatRoughness: 0.10,
          sheen: 0.08
        });
        break;
      }
    case "bridge":{
        // Silvery porcelain; pontics have no root, anchors keep them
        const isPontic = c.role === "pontic";
        if (isPontic) parts.root.visible = false;
        applyCrownStyle(parts, {
          color: 0xCFCBE3,
          vertexColors: false,
          roughness: 0.24,
          metalness: 0.55,
          clearcoat: 0.6,
          clearcoatRoughness: 0.16,
          sheen: 0.1
        });
        applyRootStyle(parts, 0xC2BAD8);
        break;
      }
    default:{
        // Natural tooth — vertex-color gradient ivory
        applyCrownStyle(parts, {
          color: 0xF0DDB0,
          vertexColors: true,
          roughness: 0.18,
          metalness: 0.0,
          clearcoat: 0.92,
          clearcoatRoughness: 0.06,
          sheen: 0.22,
          sheenColor: 0xFFF8F0
        });
        applyRootStyle(parts, 0xF2E8D5);
        break;
      }
  }

  // Selection / hover / implant emissive
  const isImp = !!mesh.userData._isImplant;
  if (!isImp) mesh.userData._isImplant = false;
  if (isSelected) {
    if (parts.haloCrown) parts.haloCrown.visible = true;
    if (parts.haloRoot)  parts.haloRoot.visible  = parts.root.visible;
    // Dent sélectionnée à renderOrder:2 → peint après le halo (1)
    parts.crown.renderOrder = 2;
    parts.root.renderOrder  = 2;
    parts.crownMat.emissive.setHex(isImp ? 0x1A4A9A : 0x2563EB);
    parts.crownMat.emissiveIntensity = 0.14;
    parts.outline.visible = false;
    if (parts.outline2) parts.outline2.visible = false;
    if (parts.outline3) parts.outline3.visible = false;
  } else if (isHovered) {
    parts.crownMat.emissive.setHex(isImp ? 0x00882E : 0x6A5FCC);
    parts.crownMat.emissiveIntensity = 0.32;
  } else if (isImp) {
    parts.crownMat.emissive.setHex(0x00882E);
    parts.crownMat.emissiveIntensity = 0.38;
  } else {
    parts.crownMat.emissive.setHex(0x000000);
    parts.crownMat.emissiveIntensity = 0;
  }

  // ── Guide chirurgical ring ──
  if (parts.guideRing) {
    const GUIDE_HEX_3D = { dentaire: 0x3B82F6, muqueux: 0xEC4899, osseux: 0xF59E0B };
    const gKey = p?.guide;
    if (gKey && GUIDE_HEX_3D[gKey] !== undefined) {
      const col = GUIDE_HEX_3D[gKey];
      parts.guideRing.visible = true;
      parts.ringMat.color.setHex(col);
      parts.ringMat.emissive.setHex(col);
      parts.ringMat.needsUpdate = true;
      // "dentaire" = appui sur la couronne → anneau au-dessus de la dent
      // autres types (muqueux, osseux) → jonction cervicale (y = 0)
      parts.guideRing.position.y = gKey === "dentaire" ? parts.d.cH * 1.12 : 0;
    } else {
      parts.guideRing.visible = false;
    }
  }
}

function makeGum3D() {
  // Curved arch built from CatmullRom curve + tube — sweeps the FRONT half
  // (where the teeth are positioned: a ∈ [-π/2, +π/2]).
  // Placed on the LINGUAL side (slightly inside the tooth row) so the
  // teeth appear in front and the pink gum line shows behind them.
  const pts = [];
  const N = 60;
  const radius = 0.86;
  for (let i = 0; i <= N; i++) {
    const a = -Math.PI / 2 - 0.06 + i / N * (Math.PI + 0.12);
    pts.push(new THREE.Vector3(
      Math.sin(a) * ARCH.X * radius,
      0,
      Math.cos(a) * ARCH.Z * radius
    ));
  }
  const curve = new THREE.CatmullRomCurve3(pts);
  const tubeGeo = new THREE.TubeGeometry(curve, 80, 0.22, 14, false);
  const mat = new THREE.MeshPhysicalMaterial({
    color: 0xD68B7E,
    roughness: 0.55,
    metalness: 0,
    clearcoat: 0.18,
    clearcoatRoughness: 0.45,
    sheen: 0.2,
    sheenColor: new THREE.Color(0xFFE0D7)
  });
  const mesh = new THREE.Mesh(tubeGeo, mat);
  return mesh;
}

function makeNumberSprite(num) {
  const c = document.createElement("canvas");
  c.width = 64;c.height = 64;
  const ctx = c.getContext("2d");
  ctx.globalAlpha = 0.38;
  ctx.fillStyle = "white";
  ctx.beginPath();ctx.arc(32, 32, 24, 0, Math.PI * 2);ctx.fill();
  ctx.globalAlpha = 0.75;
  ctx.fillStyle = "#1B0F8A";
  ctx.font = "bold 24px 'Plus Jakarta Sans', sans-serif";
  ctx.textAlign = "center";ctx.textBaseline = "middle";
  ctx.fillText(String(num), 32, 33);
  const tex = new THREE.CanvasTexture(c);
  tex.anisotropy = 4;
  const mat = new THREE.SpriteMaterial({ map: tex, depthTest: false, depthWrite: false, transparent: true });
  const sp = new THREE.Sprite(mat);
  sp.scale.set(0.19, 0.19, 1);
  sp.renderOrder = 999;
  return sp;
}

// Camera presets (target = origin)
// Camera presets: pos = camera position, target = where it looks, hide = which arch to hide
// `splay`: when true, the upper arch hinges open 180° around its posterior so both arches
//          lay flat side-by-side (panoramic-style top view).
const PRESETS_3D = {
  "frontal": { pos: [0, 0, 8.5], target: [0, 0, 0], name: "Frontale", label: "Avant", hide: null },
  "top": { pos: [0, 15, 1.2], target: [0, 0, 1.2], name: "Vue d'ensemble", label: "Dessus", hide: null, splay: true },

  "occlusal-haut": { pos: [0, -7, 0], target: [0, 0.65, 0], name: "Maxillaire seul", label: "Maxill.", hide: "lower", cameraUp: [0, 0, 1] },
  "occlusal-bas": { pos: [0, 5.2, 9.6], target: [0, -1.0, 0], name: "Mandibulaire seul", label: "Mandib.", hide: "upper" },
  "lateral-d": { pos: [-8, 0.5, 0.5], target: [0, 0, 0], name: "Latérale droite", label: "Droite", hide: null },
  "lateral-g": { pos: [8, 0.5, 0.5], target: [0, 0, 0], name: "Latérale gauche", label: "Gauche", hide: null }
};

function ToothChart3D({ planning, selected, onSelect, hovered, onHover }) {
  const mountRef = use3DRef(null);
  const stateRef = use3DRef({});
  const [activePreset, setActivePreset] = use3DState("frontal");
  const [showGum, setShowGum] = use3DState(false);
  const [showLabels, setShowLabels] = use3DState(true);
  const [showTeeth, setShowTeeth] = use3DState(true);
  const [showRoots, setShowRoots] = use3DState(true);
  const [showRings, setShowRings] = use3DState(true);
  const [showProstheses, setShowProstheses] = use3DState(true);

  // mount three scene once
  use3DEffect(() => {
    const mount = mountRef.current;
    if (!mount || !window.THREE) return;
    const THREE = window.THREE;

    const w = mount.clientWidth;
    const h = mount.clientHeight;

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

    const camera = new THREE.PerspectiveCamera(38, w / h, 0.1, 100);
    const p = PRESETS_3D["frontal"].pos;
    camera.position.set(p[0], p[1], p[2]);
    camera.lookAt(0, 0, 0);

    const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, premultipliedAlpha: false });
    renderer.setSize(w, h);
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.outputEncoding = THREE.sRGBEncoding;
    mount.appendChild(renderer.domElement);
    renderer.domElement.style.touchAction = "none";

    const controls = new THREE.OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.dampingFactor = 0.08;
    controls.minDistance = 3.5;
    controls.maxDistance = 18;
    controls.target.set(0, 0, 0);

    // ── Lighting — dental photography setup ──────────────────────
    scene.add(new THREE.AmbientLight(0xffffff, 0.18));
    // Key light: warm, high & slightly front
    const key = new THREE.DirectionalLight(0xFFF8F0, 0.40);
    key.position.set(2, 10, 7);
    scene.add(key);
    // Rim / back: cool blue-white for enamel translucency effect
    const rim = new THREE.DirectionalLight(0xC8E0FF, 0.20);
    rim.position.set(-6, 2, -5);
    scene.add(rim);
    // Front fill: warm, low, soft
    const fill = new THREE.DirectionalLight(0xFFF3DC, 0.13);
    fill.position.set(0, -3, 8);
    scene.add(fill);
    // Top overhead: sharp specular on crown surfaces
    const top = new THREE.DirectionalLight(0xFFFFFF, 0.15);
    top.position.set(0, 14, 2);
    scene.add(top);
    // Hemisphere: sky/ground for base ambience
    const hemi = new THREE.HemisphereLight(0xF5EFFF, 0xD4C8B0, 0.13);
    scene.add(hemi);

    // Procedurally-generated environment map for subtle reflections
    const envScene = new THREE.Scene();
    envScene.background = new THREE.Color(0x2a2540);
    // 6 colored quads for a quick env probe
    const envColors = [0x5a4d80, 0x4b3f70, 0x6c5d96, 0x3b3358, 0xffffff, 0x2a2540];
    const cubeRT = new THREE.WebGLCubeRenderTarget(64, { format: THREE.RGBAFormat, generateMipmaps: true, minFilter: THREE.LinearMipmapLinearFilter });
    const cubeCam = new THREE.CubeCamera(0.1, 100, cubeRT);
    envColors.forEach((c, i) => {
      const m = new THREE.Mesh(
        new THREE.PlaneGeometry(10, 10),
        new THREE.MeshBasicMaterial({ color: c, side: THREE.DoubleSide })
      );
      const positions = [
      [0, 0, 5], [5, 0, 0], [0, 5, 0], [-5, 0, 0], [0, -5, 0], [0, 0, -5]];

      const rotations = [
      [0, 0, 0], [0, Math.PI / 2, 0], [Math.PI / 2, 0, 0],
      [0, -Math.PI / 2, 0], [-Math.PI / 2, 0, 0], [0, Math.PI, 0]];

      m.position.set(...positions[i]);
      m.rotation.set(...rotations[i]);
      envScene.add(m);
    });
    cubeCam.update(renderer, envScene);
    scene.environment = cubeRT.texture;

    // Arch groups — used for the "open mouth" splay in top view
    const upperGroup = new THREE.Group();
    const lowerGroup = new THREE.Group();
    scene.add(upperGroup);
    scene.add(lowerGroup);

    // gums
    const upperGum = makeGum3D();
    upperGum.position.y = ARCH.Y - 0.05;
    upperGum.visible = false;
    upperGroup.add(upperGum);
    const lowerGum = makeGum3D();
    lowerGum.position.y = -(ARCH.Y - 0.05);
    lowerGum.visible = false;
    lowerGroup.add(lowerGum);

    // teeth
    const teethMeshes = {};
    const upperTeeth = [];
    const lowerTeeth = [];
    const labelSprites = {};
    window.TEETH.forEach((t) => {
      const upper = t.q <= 2;
      // In 3D, both arches must share the same "front" (z = +Z). The FDI
      // angles in TEETH put lower teeth (Q3/Q4) at angles 105°–255° which
      // would land them at z<0 (back of mouth). Mirror them to 180°−angle
      // so 41 lands directly under 11, 31 under 21, 48 under 18, etc.
      const angleDeg3D = upper ? t.angle : 180 - t.angle;
      const aRad = angleDeg3D * Math.PI / 180;
      const mesh = buildTooth3D(t.num, upper);
      mesh.position.set(
        Math.sin(aRad) * ARCH.X,
        upper ? ARCH.Y : -ARCH.Y,
        Math.cos(aRad) * ARCH.Z
      );
      mesh.userData.baseAngle = aRad;
      mesh.rotation.y = aRad;
      (upper ? upperGroup : lowerGroup).add(mesh);
      teethMeshes[t.num] = mesh;
      (upper ? upperTeeth : lowerTeeth).push(mesh);

      // number sprite — on the crown surface, small + transparent
      const sp = makeNumberSprite(t.num);
      const d = TOOTH_DIMS_3D[toothType(t.num)];
      const labelY = upper ? ARCH.Y - d.cH * 0.55 : -ARCH.Y + d.cH * 0.55;
      sp.position.set(
        Math.sin(aRad) * ARCH.X,
        labelY,
        Math.cos(aRad) * ARCH.Z
      );
      sp.userData.num = t.num;
      sp.userData.upper = upper;
      sp.userData.basePos = sp.position.clone();
      sp.visible = false;
      (upper ? upperGroup : lowerGroup).add(sp);
      labelSprites[t.num] = sp;
    });

    // raycaster
    const raycaster = new THREE.Raycaster();
    const mouse = new THREE.Vector2();
    let lastHovered = null;
    let dragging = false;
    let downAt = null;

    function picksFromEvent(e) {
      const rect = renderer.domElement.getBoundingClientRect();
      mouse.x = (e.clientX - rect.left) / rect.width * 2 - 1;
      mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
      raycaster.setFromCamera(mouse, camera);
      // Prefer hit spheres — fast, reliable, unaffected by geometry complexity
      const hitSpheres = Object.values(teethMeshes).
      map((g) => g.children.find((c) => c.userData && c.userData.isHitSphere)).
      filter((c) => c && !c.parent.userData._hidden);
      const sphereHits = raycaster.intersectObjects(hitSpheres, false);
      if (sphereHits.length > 0) return sphereHits[0].object.userData.num;
      // Fallback: full recursive mesh intersection
      const intersects = raycaster.intersectObjects(
        Object.values(teethMeshes).filter((m) => !m.userData._hidden), true);
      for (const it of intersects) {
        let o = it.object;
        while (o && o.userData.num == null) o = o.parent;
        if (o) return o.userData.num;
      }
      return null;
    }

    function onMove(e) {
      const num = picksFromEvent(e);
      renderer.domElement.style.cursor = num ? "pointer" : "grab";
      if (num !== lastHovered) {
        lastHovered = num;
        if (stateRef.current.onHover) stateRef.current.onHover(num);
      }
    }
    function onDown(e) {
      dragging = false;
      downAt = { x: e.clientX, y: e.clientY };
    }
    function onUp(e) {
      if (downAt && Math.hypot(e.clientX - downAt.x, e.clientY - downAt.y) < 5) {
        const num = picksFromEvent(e);
        if (num && stateRef.current.onSelect) stateRef.current.onSelect(num);
      }
      downAt = null;
    }

    renderer.domElement.addEventListener("pointermove", onMove);
    renderer.domElement.addEventListener("pointerdown", onDown);
    renderer.domElement.addEventListener("pointerup", onUp);
    renderer.domElement.style.cursor = "grab";

    // resize
    function onResize() {
      const w2 = mount.clientWidth,h2 = mount.clientHeight;
      if (w2 === 0 || h2 === 0) return;
      camera.aspect = w2 / h2;
      camera.updateProjectionMatrix();
      renderer.setSize(w2, h2);
    }
    const ro = new ResizeObserver(onResize);
    ro.observe(mount);

    // anim loop
    let raf;
    function animate() {
      raf = requestAnimationFrame(animate);
      controls.update();
      renderer.render(scene, camera);
    }
    animate();

    stateRef.current = {
      scene, camera, renderer, controls, teethMeshes, labelSprites,
      upperTeeth, lowerTeeth, upperGum, lowerGum,
      upperGroup, lowerGroup,
      onSelect, onHover, showGum: false, showLabels: false
    };

    return () => {
      cancelAnimationFrame(raf);
      ro.disconnect();
      renderer.domElement.removeEventListener("pointermove", onMove);
      renderer.domElement.removeEventListener("pointerdown", onDown);
      renderer.domElement.removeEventListener("pointerup", onUp);
      controls.dispose();
      mount.removeChild(renderer.domElement);
      renderer.dispose();
      Object.values(teethMeshes).forEach((m) => {
        m.traverse((c) => {
          if (c.isMesh) {
            c.geometry?.dispose();
            if (Array.isArray(c.material)) c.material.forEach((mm) => mm.dispose());else
            c.material?.dispose();
          }
        });
      });
    };
    // eslint-disable-next-line
  }, []);

  // keep callbacks fresh
  use3DEffect(() => {
    stateRef.current.onSelect = onSelect;
    stateRef.current.onHover = onHover;
  });

  // gum visibility
  use3DEffect(() => {
    const s = stateRef.current;
    if (!s.upperGum) return;
    stateRef.current.showGum = showGum;
    const preset = PRESETS_3D[activePreset];
    s.upperGum.visible = showGum && preset?.hide !== "upper";
    s.lowerGum.visible = showGum && preset?.hide !== "lower";
  }, [showGum, activePreset]);

  // update tooth appearance on state change
  use3DEffect(() => {
    const meshes = stateRef.current.teethMeshes;
    if (!meshes) return;
    Object.entries(meshes).forEach(([numS, mesh]) => {
      const num = parseInt(numS);
      updateTooth3D(mesh, planning[num], selected === num, hovered === num);
      // honor arch hiding from preset
      if (mesh.userData._hidden) { mesh.visible = false; return; }
      // Global layer visibility overrides
      const parts = mesh.userData.parts;
      if (parts) {
        if (parts.crown) parts.crown.visible = parts.crown.visible && showTeeth;
        if (parts.root) parts.root.visible = parts.root.visible && showRoots;
        if (parts.screwGroup) parts.screwGroup.visible = parts.screwGroup.visible && showProstheses;
        if (parts.guideRing) parts.guideRing.visible = parts.guideRing.visible && showRings;
      }
    });
  }, [planning, selected, hovered, showTeeth, showRoots, showRings, showProstheses]);

  // animate camera to preset + arch visibility + splay (top view = open mouth)
  use3DEffect(() => {
    const s = stateRef.current;
    if (!s.camera || !s.controls) return;
    const preset = PRESETS_3D[activePreset];
    if (!preset) return;

    // toggle arch visibility based on preset.hide
    const showLower = preset.hide !== "lower";
    const showUpper = preset.hide !== "upper";
    if (s.upperTeeth) s.upperTeeth.forEach((m) => {m.userData._hidden = !showUpper;});
    if (s.lowerTeeth) s.lowerTeeth.forEach((m) => {m.userData._hidden = !showLower;});
    const showGumNow = stateRef.current.showGum !== false;
    if (s.upperGum) s.upperGum.visible = showGumNow && showUpper;
    if (s.lowerGum) s.lowerGum.visible = showGumNow && showLower;
    Object.entries(s.teethMeshes).forEach(([n, mesh]) => {
      if (mesh.userData._hidden) mesh.visible = false;else
      if (!mesh.userData._absent) mesh.visible = true;
    });

    // ── Flip upper teeth 180° pour la vue splay (dessus) uniquement ──
    const flipUpper = !!preset.splay;
    if (s.teethMeshes) {
      Object.values(s.teethMeshes).forEach((mesh) => {
        if (mesh.userData.upper && mesh.userData.baseAngle !== undefined) {
          mesh.rotation.y = mesh.userData.baseAngle + (flipUpper ? Math.PI : 0);
        }
      });
    }

    // === Open-mouth splay: rotate upperGroup 180° around an X-axis at the
    //     posterior of the mouth (z = -ARCH.Z). This unfolds the maxilla
    //     so both arches lie on the same plane in panoramic-style layout.
    const startUpper = {
      px: s.upperGroup.position.x, py: s.upperGroup.position.y, pz: s.upperGroup.position.z,
      rx: s.upperGroup.rotation.x, ry: s.upperGroup.rotation.y, rz: s.upperGroup.rotation.z
    };
    const targetUpper = preset.splay ?
    { px: 0, py: 0, pz: -2.0, rx: 0, ry: 0, rz: 0 } :
    { px: 0, py: 0, pz: 0, rx: 0, ry: 0, rz: 0 };
    const startLower = {
      px: s.lowerGroup.position.x, py: s.lowerGroup.position.y, pz: s.lowerGroup.position.z
    };
    const targetLower = preset.splay ?
    { px: 0, py: 0, pz: 2.0 } :
    { px: 0, py: 0, pz: 0 };

    const targetPos = new THREE.Vector3(...preset.pos);
    const targetLook = new THREE.Vector3(...(preset.target || [0, 0, 0]));
    const startPos = s.camera.position.clone();
    const startLook = s.controls.target.clone();

    // Orientation caméra (up vector) — important pour la vue occlusal maxillaire
    const upVec = preset.cameraUp || [0, 1, 0];
    s.camera.up.set(upVec[0], upVec[1], upVec[2]);

    const startTime = performance.now();
    const dur = 700;

    // Disable damping during programmatic animation
    const prevDamping = s.controls.enableDamping;
    s.controls.enableDamping = false;
    s.controls.enabled = false; // disable user input during anim

    let raf;
    function step() {
      const elapsed = performance.now() - startTime;
      const t = Math.min(1, elapsed / dur);
      const e = 1 - Math.pow(1 - t, 3);
      s.camera.position.lerpVectors(startPos, targetPos, e);
      s.controls.target.lerpVectors(startLook, targetLook, e);
      s.camera.lookAt(s.controls.target);
      // splay tween
      s.upperGroup.position.set(
        startUpper.px + (targetUpper.px - startUpper.px) * e,
        startUpper.py + (targetUpper.py - startUpper.py) * e,
        startUpper.pz + (targetUpper.pz - startUpper.pz) * e
      );
      s.upperGroup.rotation.set(
        startUpper.rx + (targetUpper.rx - startUpper.rx) * e,
        startUpper.ry + (targetUpper.ry - startUpper.ry) * e,
        startUpper.rz + (targetUpper.rz - startUpper.rz) * e
      );
      s.lowerGroup.position.set(
        startLower.px + (targetLower.px - startLower.px) * e,
        startLower.py + (targetLower.py - startLower.py) * e,
        startLower.pz + (targetLower.pz - startLower.pz) * e
      );
      if (t < 1) {
        raf = requestAnimationFrame(step);
      } else {
        s.controls.enableDamping = prevDamping;
        s.controls.enabled = true;
        s.controls.update();
      }
    }
    step();
    return () => {
      if (raf) cancelAnimationFrame(raf);
      s.controls.enableDamping = prevDamping;
      s.controls.enabled = true;
    };
  }, [activePreset]);

  // tooth-number labels toggle
  use3DEffect(() => {
    const s = stateRef.current;
    if (!s.labelSprites) return;
    s.showLabels = showLabels;
    Object.values(s.labelSprites).forEach((sp) => {
      // a label is visible when toggle is on AND its tooth's arch is visible
      const num = sp.userData.num;
      const mesh = s.teethMeshes[num];
      const archVisible = mesh && !mesh.userData._hidden;
      sp.visible = showLabels && archVisible;
    });
  }, [showLabels, activePreset, planning]);

  return (
    <div className="chart3d-wrap">
      <div ref={mountRef} className="tooth-chart-3d" />
      <div className="chart3d-toolbar">
        <div className="chart3d-presets">
          {Object.entries(PRESETS_3D).map(([k, v]) =>
          <button
            key={k}
            className={`preset-btn ${activePreset === k ? "active" : ""}`}
            onClick={() => setActivePreset(k)}
            title={v.name}>
            
              <PresetIcon kind={k} />
              <span>{v.label}</span>
            </button>
          )}
        </div>
      </div>
      <div className="chart3d-actions">
        <button
          className={`preset-btn icon ${showTeeth ? "active" : ""}`}
          onClick={() => setShowTeeth(!showTeeth)}
          title={showTeeth ? "Masquer les dents" : "Afficher les dents"}>
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
            <path d="M7 5 Q12 2 17 5 Q18 11 16 17 Q14 21 12 21 Q10 21 8 17 Q6 11 7 5Z"/>
          </svg>
        </button>
        <button
          className={`preset-btn icon ${showRoots ? "active" : ""}`}
          onClick={() => setShowRoots(!showRoots)}
          title={showRoots ? "Masquer les racines" : "Afficher les racines"}>
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
            <path d="M9 3 Q12 2 15 3 L14 11 L12 22 L10 11 Z"/>
          </svg>
        </button>
        <button
          className={`preset-btn icon ${showRings ? "active" : ""}`}
          onClick={() => setShowRings(!showRings)}
          title={showRings ? "Masquer les anneaux guide" : "Afficher les anneaux guide"}>
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
            <ellipse cx="12" cy="13" rx="9" ry="3.5"/>
            <ellipse cx="12" cy="13" rx="5" ry="2"/>
          </svg>
        </button>
        <button
          className={`preset-btn icon ${showProstheses ? "active" : ""}`}
          onClick={() => setShowProstheses(!showProstheses)}
          title={showProstheses ? "Masquer les prothèses" : "Afficher les prothèses"}>
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
            <rect x="10" y="2" width="4" height="10" rx="2"/>
            <path d="M10 6 L7 6 M10 9 L7 9"/>
            <path d="M12 12 L12 20 M9 16 L15 16"/>
          </svg>
        </button>
        <button
          className={`preset-btn icon ${showLabels ? "active" : ""}`}
          onClick={() => setShowLabels(!showLabels)}
          title={showLabels ? "Masquer les numéros" : "Afficher les numéros"}>
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
            <circle cx="12" cy="12" r="9" />
            <text x="12" y="16" textAnchor="middle" fontSize="11" fontWeight="800" fill="currentColor" stroke="none">N°</text>
          </svg>
        </button>
        <button
          className={`preset-btn icon ${showGum ? "active" : ""}`}
          onClick={() => setShowGum(!showGum)}
          title={showGum ? "Masquer la gencive" : "Afficher la gencive"}>
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
            <path d="M3 14 Q5 8 12 8 Q19 8 21 14" strokeLinecap="round" />
            <path d="M5 14 Q6 18 8 18 Q10 18 10 16 Q10 18 12 18 Q14 18 14 16 Q14 18 16 18 Q18 18 19 14" strokeLinecap="round" strokeLinejoin="round" />
          </svg>
        </button>
      </div>
      <div className="chart3d-hint">
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M5 9l-3 3 3 3M19 9l3 3-3 3M9 5l3-3 3 3M9 19l3 3 3-3" /></svg>
        Glissez pour orbiter · Molette pour zoomer
      </div>
    </div>);

}

function PresetIcon({ kind }) {
  const props = { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2 };
  switch (kind) {
    case "3/4":
      return <svg {...props}><path d="M3 16l9-12 9 12" /><path d="M3 16l9 5 9-5" /></svg>;
    case "top":
      return <svg {...props}><circle cx="12" cy="12" r="8" /><circle cx="12" cy="12" r="3" /><path d="M12 4v2M12 18v2M4 12h2M18 12h2" /></svg>;
    case "frontal":
      return <svg {...props}><circle cx="12" cy="12" r="9" /><circle cx="9" cy="10" r="1" /><circle cx="15" cy="10" r="1" /></svg>;
    case "occlusal-haut":
      return <svg {...props}><path d="M6 8 Q12 4 18 8 L18 14 Q12 16 6 14 Z" /><path d="M6 14 L18 14" /></svg>;
    case "occlusal-bas":
      return <svg {...props}><path d="M6 16 Q12 20 18 16 L18 10 Q12 8 6 10 Z" /><path d="M6 10 L18 10" /></svg>;
    case "lateral-d":
      return <svg {...props}><path d="M5 12 L19 12" /><path d="M5 12 L9 8" /><path d="M5 12 L9 16" /></svg>;
    case "lateral-g":
      return <svg {...props}><path d="M5 12 L19 12" /><path d="M19 12 L15 8" /><path d="M19 12 L15 16" /></svg>;
    default:
      return null;
  }
}

window.AstemToothChart3D = ToothChart3D;