// ── schematic.jsx ──────────────────────────────────────────────────────────
// Renders the garment as an anatomical schematic in cm-space. Shapes react to
// dimension changes; tapping a shape selects its block. Falls back to a tidy
// vertical stack for freeform (preset-less) builds.

const { useMemo: useMemoS } = React

function hexA(hex, a) {
  const h = hex.replace("#", "")
  const n = parseInt(h.length === 3 ? h.split("").map(c => c + c).join("") : h, 16)
  return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${a})`
}

// ── Tip — a soft, paper-themed hover balloon ────────────────────────────────
// Wrap any control: <Tip text="…">{child}</Tip>. Pure CSS show/hide (see
// index.html .tip styles). `below` flips the balloon under the element for
// items near the top edge. Renders children untouched when text is empty.
function Tip({ text, children, below = false, grow = false, style }) {
  if (!text) return children
  return (
    <span className="tip" style={{ ...(grow ? { flex: 1, width: "100%" } : null), ...style }}>
      {children}
      <span className={"tip-bub" + (below ? " below" : "")} role="tooltip">{text}</span>
    </span>
  )
}

// Which real dimensions sit along the x (horizontal wall) and y (vertical wall)
// of each drawn shape. Keyed by anatomical role first (orientation depends on
// how the layout engine flattens each piece — e.g. a sleeve is drawn on its
// side), then by primitive type for freeform / preset-less pieces.
const DIM_ORIENT_ROLE = {
  "sweater-body": ["width", "length"],
  "sweater-sleeve": ["depth", "circumference"],
  "sweater-hem": ["circumference", "depth"],
  "hat-body": ["circumference", "depth"],
  "hat-brim": ["circumference", "depth"],
  "hat-crown": ["circumference", "depth"],
  "scarf-body": ["width", "length"],
  "sock-cuff": ["circumference", "depth"],
  "sock-leg": ["circumference", "depth"],
  "sock-heel": [null, "circumference"],
  "sock-foot": ["depth", "circumference"],
  "sock-toe": [null, "circumference"],
  "shawl-body": ["base", "height"],
  "shawl-border": ["circumference", "depth"],
}
const DIM_ORIENT_TYPE = {
  rectangle: ["width", "length"],
  tube: ["circumference", "depth"],
  cone: ["depth", "circumference"],
  dome: ["circumference", "depth"],
  crown: ["circumference", null],
  triangle: ["base", "height"],
  ribband: ["circumference", "depth"],
  heel: ["circumference", null],
  toe: ["circumference", null],
  overhead: [null, null],
}
function dimInfoFor(block, dims, key) {
  if (!block || !key || !dims) return null
  const prim = PRIMITIVES[block.type]
  const def = prim.dims.find(d => d.key === key)
  const v = dims[key]
  if (v == null) return null
  return { value: Math.round(v), unit: def?.unit || "cm" }
}

// Build representative boxes for a freeform (preset-less) build: a centred stack.
function freeformShapes(blocks, effDims) {
  const rows = blocks.map((b, i) => {
    const d = effDims[i]
    const prim = PRIMITIVES[b.type]
    let w, h, kind = "rect"
    if (b.type === "rectangle")      { w = d.width; h = Math.min(d.length, d.width * 4) }
    else if (b.type === "tube")      { w = d.circumference / 2; h = d.depth }
    else if (b.type === "cone")      { w = d.depth; h = d.circumference / 2; kind = "trap"; }
    else if (b.type === "dome")      { w = d.circumference / 2; h = (d.depth || d.circumference / 8); kind = "dome" }
    else if (b.type === "crown")     { w = d.circumference / 2; h = d.circumference / 4; kind = "dome" }
    else if (b.type === "heel" || b.type === "toe") { w = d.circumference / 2; h = d.circumference / 2.5; kind = "trap"; }
    else if (b.type === "triangle")  { w = d.base; h = d.height; kind = "tri" }
    else if (b.type === "ribband")   { w = d.circumference / 2; h = d.depth }
    else                             { w = 10; h = 8 }
    return { b, w, h, kind, color: prim.color, label: b.label || prim.label }
  })
  const maxW = Math.max(...rows.map(r => r.w), 10)
  const gap = maxW * 0.06
  let y = 0
  return rows.map(r => {
    const s = { blockId: r.b.id, kind: r.kind, x: (maxW - r.w) / 2, y, w: r.w, h: r.h, color: r.color, label: r.label }
    y += r.h + gap
    return s
  })
}

// Lay a subset of blocks out in a horizontal row (used to append custom/added
// pieces below a preset garment so they're visible, not just in the breakdown).
function freeformRow(blocks, effDims) {
  const items = blocks.map((b, i) => {
    const d = effDims[i]
    const prim = PRIMITIVES[b.type]
    let w, h, kind = "rect"
    if (b.type === "rectangle")      { w = d.width; h = Math.min(d.length, d.width * 4) }
    else if (b.type === "tube")      { w = d.circumference / 2; h = d.depth }
    else if (b.type === "cone")      { w = d.depth; h = d.circumference / 2; kind = "trap"; }
    else if (b.type === "dome")      { w = d.circumference / 2; h = (d.depth || d.circumference / 8); kind = "dome" }
    else if (b.type === "crown")     { w = d.circumference / 2; h = d.circumference / 4; kind = "dome" }
    else if (b.type === "heel" || b.type === "toe") { w = d.circumference / 2; h = d.circumference / 2.5; kind = "trap"; }
    else if (b.type === "triangle")  { w = d.base; h = d.height; kind = "tri" }
    else if (b.type === "ribband")   { w = d.circumference / 2; h = d.depth }
    else                             { w = 9; h = 8 }   // fixed overhead (pompom etc.)
    return { b, w, h, kind, color: prim.color, label: b.label || prim.label }
  })
  const maxH = Math.max(...items.map(r => r.h), 6)
  const gap = maxH * 0.18 + 1
  let x = 0
  return items.map(r => {
    const s = { blockId: r.b.id, kind: r.kind, x, y: maxH - r.h, w: r.w, h: r.h, color: r.color, label: r.label }
    x += r.w + gap
    return s
  })
}

function ShapeNode({ s, selected, onPointerDownShape, showLabel, showDims, density, pxPerCm, badge, dragging }) {
  const fill = hexA(s.color, selected ? 0.30 : 0.15)
  const stroke = selected ? s.color : hexA(s.color, 0.55)
  const sw = selected ? Math.max(s.w, s.h) * 0.018 + 0.4 : 0.4
  const down = (e) => { e.stopPropagation(); onPointerDownShape(s.blockId, e) }
  const common = {
    fill, stroke, strokeWidth: sw,
    onPointerDown: down,
    style: { cursor: dragging ? "grabbing" : "grab", touchAction: "none",
      transition: dragging ? "none" : "fill .15s, stroke-width .15s" },
  }
  let node
  if (s.kind === "rect") {
    node = <rect x={s.x} y={s.y} width={s.w} height={s.h} rx={Math.min(s.w, s.h) * 0.07} {...common} />
  } else if (s.kind === "dome") {
    const d = `M ${s.x},${s.y + s.h} A ${s.w / 2} ${s.h} 0 0 1 ${s.x + s.w},${s.y + s.h} Z`
    node = <path d={d} {...common} />
  } else if (s.kind === "trap") {
    // tapered shape (sleeve / heel / toe). taper = narrow-end / wide-end ratio.
    const tp = Math.max(0.05, Math.min(1, s.taper == null ? 0.6 : s.taper))
    const cx = s.x + s.w / 2, cy = s.y + s.h / 2
    let pts
    if (s.dir === "down") {       // wide at top, narrow at bottom (heel)
      pts = `${s.x},${s.y} ${s.x + s.w},${s.y} ${cx + s.w * tp / 2},${s.y + s.h} ${cx - s.w * tp / 2},${s.y + s.h}`
    } else if (s.dir === "up") {
      pts = `${cx - s.w * tp / 2},${s.y} ${cx + s.w * tp / 2},${s.y} ${s.x + s.w},${s.y + s.h} ${s.x},${s.y + s.h}`
    } else if (s.dir === "left") { // wide at right (attached to body), narrow at left (cuff)
      pts = `${s.x + s.w},${s.y} ${s.x + s.w},${s.y + s.h} ${s.x},${cy + s.h * tp / 2} ${s.x},${cy - s.h * tp / 2}`
    } else {                       // dir "right" — wide at left, narrow at right (cuff / toe)
      pts = `${s.x},${s.y} ${s.x},${s.y + s.h} ${s.x + s.w},${cy + s.h * tp / 2} ${s.x + s.w},${cy - s.h * tp / 2}`
    }
    node = <polygon points={pts} {...common} />
  } else if (s.kind === "poly") {
    node = <polygon points={(s.pts || []).map(p => `${p[0]},${p[1]}`).join(" ")} {...common} />
  } else { // tri (point down)
    node = <polygon points={`${s.x},${s.y} ${s.x + s.w},${s.y} ${s.x + s.w / 2},${s.y + s.h}`} {...common} />
  }
  // guarantee a ≥44px tap target even for very thin bands (hem/brim/border)
  const minCm = pxPerCm > 0 ? 44 / pxPerCm : 0
  const hitW = Math.max(s.w, minCm), hitH = Math.max(s.h, minCm)
  const needHit = hitW > s.w + 0.01 || hitH > s.h + 0.01
  const isPoly = s.kind === "poly" && s.pts && s.pts.length
  const polyCx = isPoly ? s.pts.reduce((a, p) => a + p[0], 0) / s.pts.length : 0
  const polyCy = isPoly ? s.pts.reduce((a, p) => a + p[1], 0) / s.pts.length : 0
  const cx = isPoly ? polyCx : s.x + s.w / 2
  const cy = isPoly ? polyCy : (s.kind === "tri" ? s.y + s.h * 0.34 : s.kind === "dome" ? s.y + s.h * 0.62 : s.y + s.h / 2)
  // size the label to the shape: limited by height AND by the label's width
  // (~0.55cm per char per font-unit) so long names never spill past the edge
  const usableW = (s.kind === "tri" || isPoly ? s.w * 0.5 : s.w * 0.86)
  const label = s.tLabel != null ? s.tLabel : s.label
  const byHeight = Math.min(s.w * 0.95, s.h) * 0.34
  const byWidth = usableW / Math.max(4, (label || "").length * 0.56)
  const fs = Math.max(2.2, Math.min(byHeight, byWidth))
  // optional rotation about the shape's true centre (angled sock foot/toe). The
  // label, dims, hit-target and badge all rotate with it.
  const rotT = s.rot ? `rotate(${s.rot} ${s.x + s.w / 2} ${s.y + s.h / 2})` : undefined
  return (
    <g transform={rotT}>
      {node}
      {needHit && (
        <rect x={cx - hitW / 2} y={cy - hitH / 2} width={hitW} height={hitH}
          fill="transparent" style={{ cursor: dragging ? "grabbing" : "grab", touchAction: "none" }}
          onPointerDown={down} />
      )}
      {showLabel && (
        <text x={cx} y={cy} textAnchor="middle" dominantBaseline="central"
          style={{ fontSize: fs, fill: selected ? s.color : hexA(s.color, 0.85),
            fontWeight: 600, letterSpacing: ".01em", pointerEvents: "none",
            fontFamily: "var(--font-ui)" }}>
          {label}
        </text>
      )}
      {showDims && pxPerCm > 0 && (() => {
        const dimFs = Math.max(2, Math.min(9 / pxPerCm, s.w * 0.42, s.h * 0.42, 6))
        const wpx = s.w * pxPerCm, hpx = s.h * pxPerCm
        const col = hexA(s.color, selected ? 0.95 : 0.82)
        const txt = (key, x, y, rot) => {
          const dm = s[key]
          if (!dm) return null
          return (
            <text x={x} y={y} textAnchor="middle" dominantBaseline="central"
              transform={rot ? `rotate(${rot} ${x} ${y})` : undefined}
              style={{ fontSize: dimFs, fill: col, fontWeight: 600, pointerEvents: "none",
                fontFamily: "var(--font-ui)", fontVariantNumeric: "tabular-nums",
                letterSpacing: ".01em" }}>
              {dm.value}
            </text>
          )
        }
        const showX = s.dimX && wpx > 40 && hpx > 26
        const showY = s.dimY && hpx > 40 && wpx > 26
        const yx = s.kind === "tri" ? s.x + s.w * 0.17 : s.x + dimFs * 0.95
        const yy = s.kind === "tri" ? s.y + s.h * 0.42 : s.y + s.h / 2
        return (
          <g>
            {showX && txt("dimX", cx, s.y + dimFs * 0.95, 0)}
            {showY && txt("dimY", yx, yy, -90)}
          </g>
        )
      })()}
      {badge && (() => {
        const r = pxPerCm > 0 ? Math.min(9 / pxPerCm, Math.min(s.w, s.h) * 0.32) : Math.min(s.w, s.h) * 0.18
        const bx = s.kind === "tri" ? s.x + s.w / 2 : s.x + r + Math.min(s.w, s.h) * 0.06
        const by = s.y + r + Math.min(s.w, s.h) * 0.06
        return (
          <g pointerEvents="none">
            <circle cx={bx} cy={by} r={r} fill={badge.color} stroke="var(--card)" strokeWidth={r * 0.18} />
            <text x={bx} y={by} textAnchor="middle" dominantBaseline="central"
              style={{ fontSize: r * 1.15, fill: "#fff", fontFamily: "var(--font-ui)" }}>⛓</text>
          </g>
        )
      })()}
    </g>
  )
}

function GarmentSchematic({ presetKey, blocks, effDims, selectedId, onSelect, density, height = 320,
                           badges = true, showDims = true, draggable = false, onMove, onBeginMove }) {
  const t = useT()
  const svgRef = React.useRef(null)
  const dragRef = React.useRef(null)
  const partsRef = React.useRef(null)
  const [dragId, setDragId] = React.useState(null)
  const [frozenVB, setFrozenVB] = React.useState(null)

  // Link groups: connected components of pieces that share a measurement, so
  // related pieces (e.g. mirrored sleeves) get a matching badge at a glance.
  const linkBadge = useMemoS(() => {
    if (!badges) return {}
    const COLORS = ["#C07A2E", "#3E5C86", "#6E8B5B", "#9C5468", "#7A6CA8"]
    const parent = {}
    const find = (x) => { while (parent[x] !== x) { parent[x] = parent[parent[x]]; x = parent[x] } return x }
    blocks.forEach(b => { parent[b.id] = b.id })
    const anchor = blocks.find(b => b.isAnchor)
    const union = (a, b) => { if (a == null || b == null || !(a in parent) || !(b in parent)) return; const ra = find(a), rb = find(b); if (ra !== rb) parent[ra] = rb }
    blocks.forEach(b => {
      Object.values(b.links || {}).forEach(tid => union(b.id, tid))
      if (b.followsAnchorDim && anchor) union(b.id, anchor.id)
    })
    const comp = {}
    blocks.forEach(b => { const r = find(b.id); (comp[r] = comp[r] || []).push(b.id) })
    const map = {}; let gi = 0
    Object.values(comp).forEach(ids => {
      if (ids.length > 1) { const color = COLORS[gi % COLORS.length]; ids.forEach(id => { map[id] = { color, idx: gi + 1 } }); gi++ }
    })
    return map
  }, [blocks, badges])

  const shapes = useMemoS(() => {
    let base
    const preset = presetKey ? shapesForGarment(presetKey, blocks, effDims) : null
    if (preset && preset.length) {
      base = preset
      // append any custom/added pieces not represented in the preset layout
      const ids = new Set(base.map(s => s.blockId))
      const exB = [], exD = []
      blocks.forEach((b, i) => { if (!ids.has(b.id)) { exB.push(b); exD.push(effDims[i]) } })
      if (exB.length) {
        let bMaxY = -Infinity, bMinX = Infinity, bMaxX = -Infinity
        base.forEach(s => { bMaxY = Math.max(bMaxY, s.y + s.h); bMinX = Math.min(bMinX, s.x); bMaxX = Math.max(bMaxX, s.x + s.w) })
        const ex = freeformRow(exB, exD)
        const exW = Math.max(...ex.map(s => s.x + s.w), 0)
        const bW = bMaxX - bMinX
        const gapY = Math.max(2, (bMaxY) * 0.1)
        ex.forEach(s => { s.y += bMaxY + gapY; s.x += bMinX + (bW - exW) / 2 })
        base = base.concat(ex)
      }
    } else {
      base = freeformShapes(blocks, effDims)
    }
    const posById = {}
    blocks.forEach(b => { if (b.pos) posById[b.id] = b.pos })
    const blockById = {}, dimsById = {}
    blocks.forEach((b, i) => { blockById[b.id] = b; dimsById[b.id] = effDims[i] })
    return base.map(s => {
      const p = posById[s.blockId]
      const b = blockById[s.blockId]
      const orient = b ? (DIM_ORIENT_ROLE[b.role] || DIM_ORIENT_TYPE[b.type] || [null, null]) : [null, null]
      return { ...s, x: s.x + (p ? p.dx : 0), y: s.y + (p ? p.dy : 0), tLabel: t(s.label),
        dimX: dimInfoFor(b, dimsById[s.blockId], orient[0]),
        dimY: dimInfoFor(b, dimsById[s.blockId], orient[1]) }
    })
  }, [presetKey, blocks, effDims, t])

  if (!shapes.length) return null

  // bounding box — account for rotated shapes (angled sock foot/toe) by including
  // their rotated corners, so nothing clips.
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity
  shapes.forEach(s => {
    const corners = [[s.x, s.y], [s.x + s.w, s.y], [s.x + s.w, s.y + s.h], [s.x, s.y + s.h]]
    if (s.rot) {
      const cx = s.x + s.w / 2, cy = s.y + s.h / 2, a = s.rot * Math.PI / 180
      const ca = Math.cos(a), sa = Math.sin(a)
      corners.forEach(p => {
        const ox = p[0] - cx, oy = p[1] - cy
        const rx = cx + ox * ca - oy * sa, ry = cy + ox * sa + oy * ca
        minX = Math.min(minX, rx); minY = Math.min(minY, ry)
        maxX = Math.max(maxX, rx); maxY = Math.max(maxY, ry)
      })
    } else {
      minX = Math.min(minX, s.x); minY = Math.min(minY, s.y)
      maxX = Math.max(maxX, s.x + s.w); maxY = Math.max(maxY, s.y + s.h)
    }
  })
  const w = maxX - minX || 1, h = maxY - minY || 1
  const pad = Math.max(w, h) * 0.08
  const liveVB = `${minX - pad} ${minY - pad} ${w + pad * 2} ${h + pad * 2}`
  partsRef.current = { minX, minY, w, h, pad }
  const vb = (dragId != null && frozenVB) ? frozenVB : liveVB
  const labelThreshold = Math.max(w, h) * 0.07
  const pxPerCm = height / (h + pad * 2)

  const clientToUser = (clientX, clientY) => {
    const svg = svgRef.current
    if (!svg || !svg.getScreenCTM) return null
    const ctm = svg.getScreenCTM()
    if (!ctm) return null
    const pt = svg.createSVGPoint(); pt.x = clientX; pt.y = clientY
    const p = pt.matrixTransform(ctm.inverse())
    return { x: p.x, y: p.y }
  }

  const onPointerDownShape = (blockId, e) => {
    if (!draggable) { onSelect(blockId); return }
    const start = clientToUser(e.clientX, e.clientY)
    if (!start) { onSelect(blockId); return }
    const b = blocks.find(x => x.id === blockId)
    const startPos = b && b.pos ? { ...b.pos } : { dx: 0, dy: 0 }
    const threshold = Math.max(w, h) * 0.012
    dragRef.current = { blockId, start, startPos, moved: false, threshold }
    setFrozenVB(liveVB)
    setDragId(blockId)

    const onWinMove = (ev) => {
      const dr = dragRef.current
      if (!dr) return
      const cur = clientToUser(ev.clientX, ev.clientY)
      if (!cur) return
      const dx = dr.startPos.dx + (cur.x - dr.start.x)
      const dy = dr.startPos.dy + (cur.y - dr.start.y)
      if (!dr.moved && Math.hypot(cur.x - dr.start.x, cur.y - dr.start.y) > dr.threshold) dr.moved = true
      if (dr.moved && onMove) onMove(dr.blockId, { dx, dy })
    }
    const onWinUp = () => {
      const dr = dragRef.current
      window.removeEventListener("pointermove", onWinMove)
      window.removeEventListener("pointerup", onWinUp)
      if (dr && !dr.moved) onSelect(dr.blockId)   // a tap, not a drag → select
      dragRef.current = null
      setDragId(null); setFrozenVB(null)
    }
    window.addEventListener("pointermove", onWinMove)
    window.addEventListener("pointerup", onWinUp)
    if (onBeginMove) onBeginMove(blockId)
  }

  return (
    <svg ref={svgRef} viewBox={vb} width="100%" height={height}
      preserveAspectRatio="xMidYMid meet"
      style={{ display: "block", overflow: "visible", touchAction: dragId != null ? "none" : "pan-y" }}
      onPointerDown={(e) => { if (e.target === svgRef.current) onSelect(null) }}>
      {shapes.map((s, i) => (
        <ShapeNode key={i} s={s}
          selected={s.blockId === selectedId}
          onPointerDownShape={onPointerDownShape}
          dragging={dragId === s.blockId}
          density={density}
          pxPerCm={pxPerCm}
          badge={linkBadge[s.blockId]}
          showDims={showDims}
          showLabel={Math.min(s.w, s.h) > labelThreshold || s.blockId === selectedId} />
      ))}
    </svg>
  )
}

// Compact, non-interactive preview for the onboarding preset cards.
function MiniSchematic({ presetKey, size = 96 }) {
  const blocks = useMemoS(() => PRESETS[presetKey].blocks.map(makeBlock), [presetKey])
  const effDims = blocks.map(b => b.dims)
  return (
    <div style={{ pointerEvents: "none" }}>
      <GarmentSchematic presetKey={presetKey} blocks={blocks} effDims={effDims}
        selectedId={null} onSelect={() => {}} density="regular" height={size} badges={false} showDims={false} />
    </div>
  )
}

Object.assign(window, { GarmentSchematic, MiniSchematic, hexA, Tip })
