// ── geometry.jsx ──────────────────────────────────────────────────────────────
// v3 GEOMETRY ENGINE — construction-aware, piece-summed sweater estimation.
//
// WHY THIS EXISTS
// The v2 engine summed pieces too, and its AREAS were already correct: validation
// showed model_area × flat-swatch density reproduces published pattern yardage to
// within the maker's buffer. The apparent "−30% on oversized" was NOT a geometry
// error — it was a density-BASIS mismatch (the engine reported finished-garment
// CONSUMPTION while pattern figures are quoted at flat-swatch PURCHASE density).
//
// This module fixes two real things:
//   1. Makes the pieces explicit and CONSTRUCTION-AWARE (a cropped circular-yoke
//      sweater puts its yarn in the yoke, a drop-shoulder in a full body + wide
//      sleeves) — the single-rectangle body could not express that. Each piece can
//      carry its own stitch pattern (colourwork yoke, cabled body, …).
//   2. Fixes the density basis: the curve is FLAT-SWATCH fabric density, and the
//      single user-facing figure is "yarn to have on hand":
//          yarn_flat = Σ(piece_area × stitch_mod) × junction × metres_per_cm²(flat)
//          toBuy     = yarn_flat × (1 + safety_buffer)
//
// CALIBRATION (June 2026, against the audit validation set):
//   • metres_per_cm² flat curve: K=0.01245, P=0.8098, fit to weighed swatches
//     (Empirical + Matavimai), independent of the old regression.
//   • safety_buffer DEFAULTS TO 0 (headline = exactly enough to knit at gauge).
//     The user dials in spare with a 0–25% slider; ≈15% is where the figure lines
//     up with published pattern yardage (MAPE 4.8% over 13 oversized / bulky /
//     held-double pattern points).
//
// CALIBRATION NOTE (not surfaced to users): weighed finished garments come in ~12–33%
// below their flat-swatch yarn — a gauge/blocking relaxation effect, fabric-dependent
// and shaky on n=3. It is a calibration cross-check only. The engine deliberately does
// NOT report a "consumption / weight" figure: yarn is conserved (a garment does not
// lose weight by being worn) and a second, lower number only confuses customers.

// ── density: FLAT-SWATCH fabric, the physical yarn-per-area at a given gauge ─────
// Replaces the regression-cloned metresPerCm2New. Row gauge enters as a linear
// correction vs the standard-tension fabric (a row-dense swatch costs more yarn).
function stdRowsForGeo(sts) { return Math.max(6, 1.195 * (sts || 22) + 2.052) }
function metresPerCm2Flat(sts, rows) {
  const base = 0.01245 * Math.pow(Math.max(2, sts || 22), 0.8098)
  return (rows && rows > 0) ? base * (rows / stdRowsForGeo(sts)) : base
}

// ── per-construction geometry + basis factors ───────────────────────────────────
//   junction   small allowance for shared/shaped fabric where body meets sleeves
//   yokeFrac   yoke (underarm→neck) depth as a fraction of finished chest
//   sleeveFrac upper-arm circumference as a fraction of finished chest
const SWEATER_GEO = {
  set_in:        { junction: 1.04, yokeFrac: 0.17, sleeveFrac: 0.34 },
  raglan:        { junction: 1.05, yokeFrac: 0.18, sleeveFrac: 0.34 },
  yoke:          { junction: 1.06, yokeFrac: 0.27, sleeveFrac: 0.34 },
  modified_drop: { junction: 1.06, yokeFrac: 0.13, sleeveFrac: 0.38 },
  drop:          { junction: 1.08, yokeFrac: 0.00, sleeveFrac: 0.42 },
}

// stitch-pattern yarn multipliers (same values as the app's CONSTRUCTION table)
// Stitch-pattern yarn multipliers: read from data.jsx's CONSTRUCTION table (the
// single source of truth) so sweater and non-sweater paths always agree.
// CONSTRUCTION is available here because data.jsx loads before geometry.jsx.

// neckline opening area (cm²) cut out of the yoke — manipulable negative shape
const GEO_NECK = {
  crew:  (C, w, d) => 0.5 * Math.PI * ((w > 0 ? w : 0.20 * C) / 2) * (d > 0 ? d : 4),
  scoop: (C, w, d) => 0.5 * Math.PI * ((w > 0 ? w : 0.24 * C) / 2) * (d > 0 ? d : 9),
  v:     (C, w, d) => 0.5 * (w > 0 ? w : 0.20 * C) * (d > 0 ? d : 14),
  boat:  (C, w, d) => 0.7 * (w > 0 ? w : 0.34 * C) * (d > 0 ? d : 2),
  none:  () => 0,
}

// ── decompose a sweater into pieces (cm² each), construction-aware ───────────────
// spec: { chest, bodyLen, construction, sleeveLen?, upperArm?, cuff?, neckType?,
//         hemDepth?, cuffDepth?, neckbandDepth? }  — all lengths in cm; chest, hem
//         & sleeve circumferences are full circumference. upperArm / cuff / hemDepth
//         come from the editable blocks when present, else construction defaults.
function sweaterPieces(spec) {
  const C = spec.chest, L = spec.bodyLen
  const k = SWEATER_GEO[spec.construction] ? spec.construction : "raglan"
  const G = SWEATER_GEO[k]
  const Ls = (spec.sleeveLen != null && spec.sleeveLen > 0) ? spec.sleeveLen : Math.round(44 + (C - 95) * 0.06)
  const hemDepth = spec.hemDepth ?? 6, cuffDepth = spec.cuffDepth ?? 6, neckbandDepth = spec.neckbandDepth ?? 3
  const neckCirc = Math.max(34, 0.34 * C)
  const neckFn = GEO_NECK[spec.neckType] || GEO_NECK.crew
  const neckOpen = neckFn(C)
  const yd = G.yokeFrac > 0 ? (G.yokeFrac * C + 3) : 0
  // sleeve width: the block's value when supplied, else a construction default (a
  // fraction of chest, so it still scales with size). cuff = the narrow (wrist) end.
  const upperArm = (spec.upperArm > 0) ? spec.upperArm : G.sleeveFrac * C
  const cuff = (spec.cuff > 0) ? spec.cuff : 0.20 * C

  const pieces = []
  const add = (name, role, area, stitch, group) =>
    pieces.push({ name, role, area: Math.max(0, area), stitch, group })

  if (k === "drop") {
    add("Body", "sweater-body", C * L - neckOpen, "stockinette", "struct")           // full straight tube
  } else {
    add("Body", "sweater-body", C * Math.max(2, L - yd), "stockinette", "struct")     // hem → underarm
    add("Yoke", "sweater-yoke", ((C + neckCirc) / 2) * yd - neckOpen, "stockinette", "struct") // underarm → neck frustum
  }
  add("Sleeves", "sweater-sleeve", 2 * ((upperArm + cuff) / 2) * Ls, "stockinette", "struct")  // 2 tapered tubes
  add("Hem", "sweater-hem", C * hemDepth, "rib_elastic", "rib")
  add("Cuffs", "sweater-cuff", 2 * cuff * cuffDepth, "rib_elastic", "rib")
  add("Neckband", "sweater-neckband", neckCirc * neckbandDepth, "rib_elastic", "rib")

  return { pieces, derived: { yokeDepth: yd, sleeveLen: Ls, upperArm, cuff, neckCirc } }
}

// ── estimate yarn for a sweater ──────────────────────────────────────────────────
// spec additionally takes: gauge (sts/10cm), rows? (sts-gauge row count), strands (1
// or 2 for held-double), buffer (safety fraction, default 0.15), and optional
// per-piece stitch overrides: stitchByRole = { "sweater-yoke": "colorwork", ... }.
// Returns metres figures (per-strand) plus held-double totals and a piece breakdown.
function estimateSweater(spec) {
  const gauge = spec.gauge || 22, rows = spec.rows || null
  const strands = spec.strands || 1, buffer = spec.buffer != null ? spec.buffer : 0
  const k = SWEATER_GEO[spec.construction] ? spec.construction : "raglan"
  const G = SWEATER_GEO[k]
  const overrides = spec.stitchByRole || {}
  const { pieces } = sweaterPieces(spec)
  // precision mode: a weighed swatch gives metres_per_cm² directly (the knitter's
  // real tension, row gauge & fibre). Otherwise fall back to the FLAT-SWATCH curve.
  const m = (spec.mPerCm2 && spec.mPerCm2 > 0) ? spec.mPerCm2 : metresPerCm2Flat(gauge, rows)

  let structMetres = 0, ribMetres = 0
  const breakdown = pieces.map(p => {
    const stitch = overrides[p.role] || p.stitch
    const mod = (CONSTRUCTION[stitch] && CONSTRUCTION[stitch].mod) || 1
    let metres = p.area * mod * m
    if (p.group === "struct") { metres *= G.junction; structMetres += metres }
    else ribMetres += metres
    return { name: p.name, role: p.role, area: Math.round(p.area), stitch, metres: Math.round(metres) }
  })

  const yarnFlat = structMetres + ribMetres          // per strand, flat-swatch basis
  const toBuy = Math.round(yarnFlat * (1 + buffer))  // 0 spare by default; slider adds margin
  return {
    construction: k, gauge, rows, strands, buffer, measured: !!(spec.mPerCm2 && spec.mPerCm2 > 0),
    metresPerCm2: +m.toFixed(4), pieces: breakdown,
    yarnFlat: Math.round(yarnFlat),                   // per strand, 0-spare base the slider scales
    toBuy,                                            // per strand
    toBuyTotal: toBuy * strands,                      // single-strand metres to purchase
  }
}

if (typeof window !== "undefined") {
  Object.assign(window, {
    metresPerCm2Flat, SWEATER_GEO, GEO_NECK,
    sweaterPieces, estimateSweater,
  })
}
if (typeof module !== "undefined" && module.exports) {
  module.exports = { metresPerCm2Flat, SWEATER_GEO, sweaterPieces, estimateSweater }
}
