import type { CanonicalBone } from "@psx/shared";
import type { Rgb } from "../image/Raster.js";
import {
  add,
  computeFlatNormals,
  createMesh,
  cross,
  domeShell,
  dot,
  lerp,
  loft,
  normalise,
  orientedBox,
  scale,
  sub,
  tube,
  tubePath,
  type DomeShellOptions,
  type LoftKey,
  type MeshData,
  type Vec3Tuple,
} from "../mesh/MeshBuilder.js";
import { jointWorld, type Skeleton } from "../rig/skeleton.js";
import { headLandmarks } from "./body.js";
import { PART, type CoverageBand } from "./coverage.js";
import { bodyMetrics, cutAbove, cutBelow, padded, torsoPlan, withHem } from "./metrics.js";
import type { PartContext, PartDefinition } from "./types.js";

/**
 * Outfit part builders. Each returns a mesh in bind pose on the shared
 * skeleton; the assembler merges them with the base body and re-skins once.
 *
 * Clothing is lofted like the body (ludus keyframes) — slightly larger, 4–6
 * sides for a PS1 / FF8 angular read.
 */

const TORSO_SIDES = 8;
const LIMB_SIDES = 6;

/** Ring frames pinned across the body, matching the skin loft. */
const ACROSS: Vec3Tuple = [1, 0, 0];

const at = (skeleton: Skeleton, bone: CanonicalBone): Vec3Tuple => jointWorld(skeleton, bone);
const finish = (mesh: MeshData): MeshData => {
  computeFlatNormals(mesh);
  return mesh;
};

// --- hair ------------------------------------------------------------------

function hairLandmarks(ctx: PartContext) {
  const { skeleton, body, headStyle } = ctx;
  const hl = headLandmarks(skeleton, body, headStyle);
  const { head, neck, crown, brow, browFwd, occiput, headW, headD, height: h } = hl;
  const pad = 1.14;
  const skullAxis = normalise(sub(crown, head));
  const face: Vec3Tuple = [0, 0, 1];
  const hairFrom = add(
    lerp(head, crown, 0.5),
    add(scale(skullAxis, headW * 0.04), scale(face, headW * 0.03)),
  );
  const hairTo = add(crown, scale(skullAxis, headW * 0.12));
  const halfW = headW * pad;
  // Depth tracks the real skull rather than breadth: the face sliders trade one
  // against the other, so a long head is narrower *and* deeper, and a cap sized
  // off width alone would let the occiput through the back of the hair.
  const halfD = headD * pad * 0.778;
  return {
    h,
    head,
    neck,
    headHalf: headW,
    crown,
    brow,
    browFwd,
    occiput,
    hairFrom,
    hairTo,
    halfW,
    halfD,
    centreZ: (brow[2] + occiput[2]) * 0.5,
    hairlineY: hairFrom[1],
    crownY: hairTo[1],
    browFwdZ: browFwd[2],
    capHeight: Math.hypot(hairTo[0] - hairFrom[0], hairTo[1] - hairFrom[1], hairTo[2] - hairFrom[2]),
  };
}

function baseCapOptions(style: "short" | "long" | "bun" | "undercut" | "messy"): DomeShellOptions {
  const faceDir: Vec3Tuple = [0, 0, 1];
  const faceCut = { height: 0.62, halfWidth: 0.62, depth: 0.12 };
  const common = {
    sections: 4,
    sides: 8,
    faceDir,
    faceCut,
    frontScale: 0.9,
    frontScaleTop: 0.68,
    backScale: 1.12,
    topScale: 0.82,
  };
  if (style === "long") return { ...common, backScale: 1.2, topScale: 0.84, faceCut: { ...faceCut, halfWidth: 0.58 } };
  if (style === "bun") return { ...common, topScale: 0.78 };
  if (style === "undercut") return { ...common, faceCut: { ...faceCut, height: 0.68 }, topScale: 0.8 };
  return common;
}

type BackScalpLength = "crop" | "nape" | "shoulder";

function capFrame(lm: ReturnType<typeof hairLandmarks>, cap: DomeShellOptions) {
  const from = lm.hairFrom;
  const to = lm.hairTo;
  const axisN = normalise(sub(to, from));
  const faceWanted = cap.faceDir ?? ([0, 0, 1] as Vec3Tuple);
  let face = sub(faceWanted, scale(axisN, dot(faceWanted, axisN)));
  if (Math.hypot(face[0], face[1], face[2]) < 1e-5) {
    const alt: Vec3Tuple = Math.abs(axisN[1]) < 0.9 ? [0, 1, 0] : [1, 0, 0];
    face = sub(alt, scale(axisN, dot(alt, axisN)));
  }
  face = normalise(face);
  const side = normalise(cross(axisN, face));
  const hw = lm.halfW;
  const hd = lm.halfD;
  const backScale = cap.backScale ?? 1.12;
  const ringPoint = (cos: number, sin: number): Vec3Tuple => {
    const depthScale = sin >= 0 ? (cap.frontScale ?? 0.9) : backScale;
    return add(from, add(scale(side, -cos * hw), scale(face, sin * hd * depthScale)));
  };
  const inv = Math.SQRT1_2;
  return {
    axisN,
    face,
    side,
    ringCentre: from,
    backCentre: ringPoint(0, -1),
    backLeft: ringPoint(inv, -inv),
    backRight: ringPoint(-inv, -inv),
    hw,
    hd,
    backScale,
  };
}

function addBackScalp(
  mesh: MeshData,
  ctx: PartContext,
  lm: ReturnType<typeof hairLandmarks>,
  color: Rgb,
  length: BackScalpLength,
  cap: DomeShellOptions,
): void {
  const { headHalf: skinW, neck } = lm;
  const spine2 = at(ctx.skeleton, "Spine2");
  const spine1 = at(ctx.skeleton, "Spine1");
  const frame = capFrame(lm, cap);
  const { axisN, face, backCentre, backLeft, backRight } = frame;
  const seamHalfW =
    0.5 *
    Math.hypot(backLeft[0] - backRight[0], backLeft[1] - backRight[1], backLeft[2] - backRight[2]);
  const depthRatio = length === "crop" ? 0.4 : length === "nape" ? 0.42 : 0.45;
  const tubeHalfD = seamHalfW * depthRatio;
  const sinkIn = scale(face, tubeHalfD * 0.92);
  const seamOverlap = scale(axisN, skinW * 0.04);
  const backTop = add(add(backCentre, seamOverlap), sinkIn);
  const seamLeft = add(add(backLeft, seamOverlap), sinkIn);
  const seamRight = add(add(backRight, seamOverlap), sinkIn);
  const backMid = add(backTop, add(scale(axisN, -skinW * 0.55), scale(face, skinW * 0.05)));

  let end: Vec3Tuple;
  let widths: number[];
  if (length === "crop") {
    end = add(backMid, add(scale(axisN, -skinW * 0.4), scale(face, skinW * 0.03)));
    widths = [seamHalfW, seamHalfW * 0.92, seamHalfW * 0.72];
  } else if (length === "nape") {
    end = add([0, neck[1] - skinW * 0.1, neck[2] - skinW * 0.12] as Vec3Tuple, sinkIn);
    widths = [seamHalfW, seamHalfW * 0.88, seamHalfW * 0.58];
  } else {
    const shoulderY = Math.min(spine2[1], spine1[1] + (spine2[1] - spine1[1]) * 0.35);
    end = add(
      [0, shoulderY - skinW * 0.05, spine2[2] - skinW * 0.22] as Vec3Tuple,
      scale(face, tubeHalfD * 0.5),
    );
    widths = [seamHalfW, seamHalfW * 0.9, seamHalfW * 0.65, seamHalfW * 0.48];
  }

  if (length === "shoulder") {
    const nape = add(
      [backTop[0], neck[1] - skinW * 0.05, neck[2] - skinW * 0.16] as Vec3Tuple,
      scale(face, tubeHalfD * 0.7),
    );
    tubePath(mesh, [backTop, backMid, nape, end], widths, depthRatio, color, {
      sides: 6,
      sectionsPerSegment: 2,
    });
    for (const [s, corner] of [[-1, seamLeft], [1, seamRight]] as const) {
      tubePath(
        mesh,
        [
          corner,
          [s * seamHalfW * 0.9, nape[1], nape[2] + skinW * 0.04],
          [s * seamHalfW * 0.65, end[1] + skinW * 0.05, end[2] + skinW * 0.05],
        ],
        [skinW * 0.24, skinW * 0.22, skinW * 0.15],
        0.7,
        color,
        { sides: 5, sectionsPerSegment: 2 },
      );
    }
  } else {
    tubePath(mesh, [backTop, backMid, end], widths, depthRatio, color, {
      sides: 6,
      sectionsPerSegment: 2,
    });
  }
  orientedBox(
    mesh,
    backTop,
    seamHalfW * 0.95,
    skinW * 0.08,
    tubeHalfD * 0.9,
    normalise(add(scale(face, -1), scale(axisN, -0.12))),
    color,
  );
}

function addSideburns(
  mesh: MeshData,
  lm: ReturnType<typeof hairLandmarks>,
  color: Rgb,
  lengthScale: number,
  thicknessScale = 1,
): void {
  const drop = lm.headHalf * (0.55 + lengthScale * 0.45);
  const yTop = lm.hairlineY - lm.headHalf * 0.02;
  const yBot = yTop - drop;
  const z = lm.centreZ - lm.headHalf * 0.05;
  const x = lm.halfW * 0.9;
  const tw = lm.headHalf * 0.16 * thicknessScale;
  const td = lm.headHalf * 0.2 * thicknessScale;
  for (const side of [-1, 1] as const) {
    tube(
      mesh,
      [side * x, yTop, z],
      [side * x * 0.95, yBot, z - lm.headHalf * 0.05],
      tw,
      tw * 0.85,
      td / Math.max(tw, 1e-6),
      color,
      { sides: 5, sections: 1, caps: true },
    );
  }
}

function hairCap(ctx: PartContext, style: "short" | "long" | "bun" | "undercut" | "messy"): MeshData {
  const mesh = createMesh();
  const color = ctx.color ?? ctx.palette.hair;
  const lm = hairLandmarks(ctx);
  const cap = baseCapOptions(style);

  domeShell(mesh, lm.hairFrom, lm.hairTo, lm.halfW, lm.halfD, color, cap);

  const cutTop = lerp(lm.hairFrom, lm.hairTo, 0.55);
  orientedBox(
    mesh,
    add(cutTop, [0, lm.headHalf * 0.02, lm.headHalf * 0.05]),
    lm.halfW * 0.55,
    lm.headHalf * 0.06,
    lm.headHalf * 0.08,
    [0, 0.35, 1],
    color,
  );
  for (const s of [-1, 1] as const) {
    orientedBox(
      mesh,
      add(lm.hairFrom, [s * lm.halfW * 0.55, lm.headHalf * 0.04, lm.headHalf * 0.08]),
      lm.headHalf * 0.14,
      lm.headHalf * 0.16,
      lm.headHalf * 0.1,
      [s * 0.15, 0.2, 1],
      color,
    );
  }

  const backLength: BackScalpLength =
    style === "long" ? "shoulder" : style === "messy" || style === "bun" ? "nape" : "crop";
  addBackScalp(mesh, ctx, lm, color, backLength, cap);

  if (style === "short") addSideburns(mesh, lm, color, 0.35, 0.9);
  if (style === "long") addSideburns(mesh, lm, color, 0.55, 1.05);
  if (style === "messy") {
    addSideburns(mesh, lm, color, 0.45, 1.0);
    for (const t of [
      { c: add(lm.crown, [0, lm.headHalf * 0.06, lm.headHalf * 0.04]), hr: lm.headHalf * 0.38, hu: lm.headHalf * 0.2, hf: lm.headHalf * 0.28, dir: [0, 1, 0.2] as Vec3Tuple },
      { c: add(lm.crown, [-lm.halfW * 0.35, -lm.headHalf * 0.02, -lm.headHalf * 0.08]), hr: lm.headHalf * 0.28, hu: lm.headHalf * 0.2, hf: lm.headHalf * 0.22, dir: [-0.4, 0.9, -0.1] as Vec3Tuple },
      { c: add(lm.crown, [lm.halfW * 0.32, 0, -lm.headHalf * 0.12]), hr: lm.headHalf * 0.26, hu: lm.headHalf * 0.22, hf: lm.headHalf * 0.2, dir: [0.35, 1, -0.2] as Vec3Tuple },
    ]) {
      orientedBox(mesh, t.c, t.hr, t.hu, t.hf, t.dir, color);
    }
  }
  if (style === "bun") {
    const bunCentre: Vec3Tuple = [0, lm.crown[1] - lm.headHalf * 0.02, lm.occiput[2] - lm.headHalf * 0.1];
    tube(
      mesh,
      [0, lm.crown[1] - lm.headHalf * 0.1, lm.centreZ - lm.halfD * 0.2],
      bunCentre,
      lm.headHalf * 0.22,
      lm.headHalf * 0.32,
      1,
      color,
      { sides: 6, sections: 1 },
    );
    orientedBox(mesh, bunCentre, lm.headHalf * 0.42, lm.headHalf * 0.35, lm.headHalf * 0.38, [0, 0.4, -1], color);
  }
  if (style === "undercut") {
    orientedBox(
      mesh,
      add(lm.crown, [0, -lm.headHalf * 0.02, lm.headHalf * 0.02]),
      lm.halfW * 0.5,
      lm.headHalf * 0.12,
      lm.headHalf * 0.26,
      [0, 0.55, 0.3],
      color,
    );
  }

  return finish(mesh);
}

// --- upper body clothing ---------------------------------------------------

function upperGarment(
  ctx: PartContext,
  kind: "shirt" | "jacket" | "coat" | "labcoat" | "uniform" | "workcoat",
): MeshData {
  const mesh = createMesh();
  const { skeleton, palette, body, bodyStyle } = ctx;
  const h = skeleton.height;
  const color = ctx.color ?? palette.primary;
  const trim = palette.secondary;
  const long = kind === "coat" || kind === "labcoat";
  const heavy = kind === "jacket" || kind === "workcoat" || kind === "uniform";

  // Cloth thickness in metres, not a percentage: a jacket clears the skin by
  // the same 2 cm on a heavy body as on a lean one.
  const cloth = h * (long ? 0.014 : heavy ? 0.011 : 0.007);
  const skin = bodyMetrics(skeleton, body, bodyStyle);
  const m = padded(skin, cloth);

  // Body's ring plan, grown by cloth and cut at the hem — a garment that
  // invents its own key list eventually skips a section the body has.
  const plan = torsoPlan(m);
  const torsoKeys = long
    ? withHem(
        plan,
        m.crotchY - h * 0.075,
        // Wide enough to hang clear of a flared skirt underneath.
        { rx: m.hip.rx * 1.28, rz: m.hip.rz * 1.22 },
        m.at("Hips")[2] + m.buttZ * 0.3,
      )
    : cutBelow(plan, m.waistY - h * 0.05);

  // Ring count matters more here than on the skin: the garment only clears the
  // body if its chords sit outside the body's, so the shoulder slope needs
  // rings on both sides of the trapezius key, not a single chord across it.
  loft(mesh, torsoKeys, color, { sides: TORSO_SIDES, rings: 17, caps: true, startRight: ACROSS });

  // Sleeves — the skin arm loft, stopped at the cuff.
  for (const prefix of ["Left", "Right"] as const) {
    const side = prefix === "Left" ? 1 : -1;
    const shoulder = m.at(`${prefix}Shoulder` as CanonicalBone);
    const arm = m.at(`${prefix}Arm` as CanonicalBone);
    const foreArm = m.at(`${prefix}ForeArm` as CanonicalBone);
    const hand = m.at(`${prefix}Hand` as CanonicalBone);
    const sleeveEnd = kind === "shirt" ? lerp(foreArm, hand, 0.55) : lerp(foreArm, hand, 0.92);
    const down = normalise(sub(hand, arm));

    const sleeveKeys: LoftKey[] = [
      {
        t: 0.0,
        c: [side * m.clavX, shoulder[1] - h * 0.012, shoulder[2]],
        rx: m.deltoidR * 0.94,
        rz: m.deltoidR,
      },
      {
        // Same t as the skin's deltoid key so both lofts peak on a ring.
        t: 0.1,
        c: [side * m.socketX, arm[1] + h * 0.002, arm[2]],
        rx: m.deltoidR,
        rz: m.deltoidR * 1.04,
      },
      {
        t: 0.22,
        c: add(arm, scale(down, h * 0.045)),
        rx: m.upperArm.rx * 1.06,
        rz: m.upperArm.rz * 1.06,
      },
      { t: 0.45, c: lerp(arm, foreArm, 0.62), rx: m.upperArm.rx * 0.94, rz: m.upperArm.rz * 0.94 },
      { t: 0.6, c: foreArm, rx: m.elbow.rx, rz: m.elbow.rz },
      { t: 0.8, c: lerp(foreArm, sleeveEnd, 0.6), rx: m.foreArm.rx * 0.95, rz: m.foreArm.rz * 0.95 },
      { t: 1.0, c: sleeveEnd, rx: m.wrist.rx * 1.3, rz: m.wrist.rz * 1.2 },
    ];
    loft(mesh, sleeveKeys, color, {
      sides: LIMB_SIDES,
      rings: 11,
      capStart: false,
      capEnd: true,
      startRight: ACROSS,
    });
    // Cuff accent
    loft(
      mesh,
      [
        { t: 0, c: lerp(sleeveEnd, foreArm, 0.1), rx: m.wrist.rx * 1.36, rz: m.wrist.rz * 1.26 },
        { t: 1, c: sleeveEnd, rx: m.wrist.rx * 1.38, rz: m.wrist.rz * 1.28 },
      ],
      trim,
      { sides: LIMB_SIDES, rings: 2, caps: true, startRight: ACROSS },
    );
  }

  // Low stand collar (not a brick tower)
  if (kind !== "shirt") {
    const neck = m.at("Neck");
    loft(
      mesh,
      [
        { t: 0, c: [0, m.collarY, neck[2] + h * 0.005], rx: m.neck.rx * 1.2, rz: m.neck.rz * 1.18 },
        {
          t: 1,
          c: [0, m.collarY + h * 0.03, neck[2] + h * 0.008],
          rx: m.neck.rx * 1.26,
          rz: m.neck.rz * 1.22,
        },
      ],
      trim,
      { sides: 6, rings: 2, caps: true, startRight: ACROSS },
    );
  }

  return finish(mesh);
}

function lowerGarment(ctx: PartContext, kind: "pants" | "cargo" | "skirt"): MeshData {
  const mesh = createMesh();
  const { skeleton, palette, body, bodyStyle } = ctx;
  const h = skeleton.height;
  const color = ctx.color ?? palette.secondary;
  const cloth = h * (kind === "cargo" ? 0.011 : 0.008);
  const m = padded(bodyMetrics(skeleton, body, bodyStyle), cloth);
  const hips = at(skeleton, "Hips");
  const spine = at(skeleton, "Spine");
  const waistbandY = m.waistY + h * 0.01;

  if (kind === "skirt") {
    // Full A-line: waistband → hip shelf → past the crotch → knee hem. It must
    // wrap the whole pelvis, or the uncapped leg roots show as open tubes.
    const kneeY = at(skeleton, "LeftLeg")[1] + h * 0.02;
    loft(
      mesh,
      [
        { t: 0.0, c: [0, waistbandY, spine[2] + m.bellyZ], rx: m.waist.rx, rz: m.waist.rz },
        {
          t: 0.24,
          c: [0, m.hipY, hips[2] + m.buttZ * 0.4],
          rx: m.hip.rx * 1.04,
          rz: m.hip.rz * 1.06,
        },
        // Below the crotch — the hem is already clear of the leg roots here.
        {
          t: 0.52,
          c: [0, m.crotchY - h * 0.05, hips[2]],
          rx: m.hip.rx * 1.16,
          rz: m.hip.rz * 1.12,
        },
        { t: 1.0, c: [0, kneeY, hips[2]], rx: m.hip.rx * 1.34, rz: m.hip.rz * 1.24 },
      ],
      color,
      { sides: TORSO_SIDES, rings: 8, caps: true, startRight: ACROSS },
    );
    // Waist sash
    loft(
      mesh,
      [
        { t: 0, c: [0, waistbandY - h * 0.012, spine[2] + m.bellyZ], rx: m.waist.rx * 1.03, rz: m.waist.rz * 1.03 },
        { t: 1, c: [0, waistbandY + h * 0.018, spine[2] + m.bellyZ], rx: m.waist.rx * 0.99, rz: m.waist.rz * 0.99 },
      ],
      palette.primary,
      { sides: TORSO_SIDES, rings: 3, caps: true, startRight: ACROSS },
    );
    return finish(mesh);
  }

  // Pants: a hip yoke on the body's own ring plan, cut at the waistband, then
  // two legs whose roots stay buried inside it.
  loft(
    mesh,
    cutAbove(torsoPlan(m), waistbandY),
    color,
    // Rings no denser than the layer above: an inner layer that samples a
    // curve more finely than an outer one bulges through it at the glutes.
    { sides: TORSO_SIDES, rings: 6, caps: true, startRight: ACROSS },
  );

  for (const prefix of ["Left", "Right"] as const) {
    const side = prefix === "Left" ? 1 : -1;
    const upLeg = at(skeleton, `${prefix}UpLeg` as CanonicalBone);
    const leg = at(skeleton, `${prefix}Leg` as CanonicalBone);
    const foot = at(skeleton, `${prefix}Foot` as CanonicalBone);
    const legX = Math.abs(upLeg[0]);
    const hemY = lerp(leg, foot, 0.9);

    const legKeys: LoftKey[] = [
      // Buried in the yoke.
      {
        t: 0.0,
        c: [side * legX * 0.6, m.hipY + h * 0.012, hips[2] + m.buttZ * 0.5],
        rx: m.hipSocket.rx,
        rz: m.hipSocket.rz,
      },
      {
        t: 0.1,
        c: [side * legX * 0.95, m.crotchY + h * 0.01, upLeg[2] - h * 0.004],
        rx: m.thigh.rx * 1.1,
        rz: m.thigh.rz * 1.12,
      },
      { t: 0.26, c: [side * legX, upLeg[1] - h * 0.03, upLeg[2]], rx: m.thigh.rx, rz: m.thigh.rz },
      { t: 0.45, c: lerp(upLeg, leg, 0.78), rx: m.thigh.rx * 0.84, rz: m.thigh.rz * 0.86 },
      { t: 0.56, c: leg, rx: m.knee.rx * 1.12, rz: m.knee.rz * 1.12 },
      { t: 0.72, c: add(lerp(leg, foot, 0.26), [0, 0, m.calfZ]), rx: m.calf.rx, rz: m.calf.rz },
      // Hem breaks over the boot rather than shrink-wrapping the ankle.
      { t: 1.0, c: hemY, rx: m.calf.rx * 0.86, rz: m.calf.rz * 0.86 },
    ];
    loft(mesh, legKeys, color, {
      sides: LIMB_SIDES,
      rings: 11,
      capStart: false,
      capEnd: true,
      startRight: ACROSS,
    });
  }

  return finish(mesh);
}

function footwear(ctx: PartContext, kind: "boots" | "shoes"): MeshData {
  const mesh = createMesh();
  const { skeleton, palette, body, bodyStyle } = ctx;
  const h = skeleton.height;
  const color = ctx.color ?? palette.leather;
  const sole = palette.secondary;
  const m = padded(bodyMetrics(skeleton, body, bodyStyle), h * 0.008);
  const soleLift = h * 0.012;

  for (const prefix of ["Left", "Right"] as const) {
    const ankleJoint = at(skeleton, `${prefix}Foot` as CanonicalBone);
    const leg = at(skeleton, `${prefix}Leg` as CanonicalBone);
    const x = ankleJoint[0];
    const z = ankleJoint[2];
    const hw = m.footHalfW;
    const heelZ = z - m.heelBack;
    const toeZ = z + m.footLength - m.heelBack + h * 0.006;

    // Shaft: from the cuff down over the ankle, on the skin leg sections.
    const top = kind === "boots" ? lerp(leg, ankleJoint, 0.55) : lerp(leg, ankleJoint, 0.86);
    loft(
      mesh,
      [
        { t: 0, c: top, rx: m.calf.rx * 0.9, rz: m.calf.rz * 0.9 },
        { t: 0.6, c: [x, ankleJoint[1] + h * 0.01, z], rx: m.ankle.rx * 1.15, rz: m.ankle.rz * 1.1 },
        { t: 1, c: [x, m.ankleHeight * 0.7, z + h * 0.004], rx: hw * 0.86, rz: m.ankleHeight * 0.66 },
      ],
      color,
      { sides: LIMB_SIDES, rings: kind === "boots" ? 5 : 4, capStart: true, capEnd: false, startRight: ACROSS },
    );

    // Shell over the foot — the skin foot's ring plan, lifted onto a sole.
    loft(
      mesh,
      [
        { t: 0.0, c: [x, soleLift + m.ankleHeight * 0.62, heelZ], rx: hw * 0.66, rz: m.ankleHeight * 0.62 },
        { t: 0.18, c: [x, soleLift + m.ankleHeight * 0.56, heelZ + m.footLength * 0.1], rx: hw * 0.82, rz: m.ankleHeight * 0.56 },
        { t: 0.4, c: [x, soleLift + m.ankleHeight * 0.66, z + m.footLength * 0.06], rx: hw * 0.9, rz: m.ankleHeight * 0.6 },
        { t: 0.62, c: [x, soleLift + m.ankleHeight * 0.5, z + m.footLength * 0.28], rx: hw * 0.97, rz: m.ankleHeight * 0.48 },
        { t: 0.85, c: [x, soleLift + m.ankleHeight * 0.4, z + m.footLength * 0.5], rx: hw * 1.02, rz: m.ankleHeight * 0.4 },
        { t: 1.0, c: [x, soleLift + m.ankleHeight * 0.32, toeZ], rx: hw * 0.88, rz: m.ankleHeight * 0.32 },
      ],
      color,
      { sides: LIMB_SIDES, rings: 6, caps: true, startRight: ACROSS },
    );

    // Sole slab, sitting on the floor. Spans heel to toe exactly: run it off
    // its own length and it juts out behind the heel, where the shin has a
    // claim on the weights and a stride shears it into a blade.
    orientedBox(
      mesh,
      [x, soleLift * 0.5, (heelZ + toeZ) * 0.5],
      hw * 1.02,
      soleLift * 0.5,
      (toeZ - heelZ) * 0.5,
      [0, 0, 1],
      sole,
    );
  }
  return finish(mesh);
}

function accessory(ctx: PartContext, kind: "backpack" | "belt" | "glasses" | "satchel"): MeshData {
  const mesh = createMesh();
  const { skeleton, palette } = ctx;
  const h = skeleton.height;
  const s = h / 1.72;
  const color = ctx.color ?? palette.accent;

  if (kind === "backpack") {
    const spine2 = at(skeleton, "Spine2");
    const spine1 = at(skeleton, "Spine1");
    const centre = lerp(spine1, spine2, 0.5);
    loft(
      mesh,
      [
        {
          t: 0,
          c: [centre[0], centre[1] - h * 0.06, centre[2] - h * 0.065],
          rx: 0.065 * s,
          rz: 0.05 * s,
        },
        {
          t: 0.5,
          c: [centre[0], centre[1], centre[2] - h * 0.07],
          rx: 0.072 * s,
          rz: 0.055 * s,
        },
        {
          t: 1,
          c: [centre[0], centre[1] + h * 0.08, centre[2] - h * 0.06],
          rx: 0.068 * s,
          rz: 0.052 * s,
        },
      ],
      color,
      { sides: 5, rings: 4, caps: true },
    );
    for (const side of [-1, 1]) {
      loft(
        mesh,
        [
          {
            t: 0,
            c: [side * h * 0.07, spine2[1] + h * 0.01, spine2[2]],
            rx: 0.008 * s,
            rz: 0.008 * s,
          },
          {
            t: 1,
            c: [side * h * 0.07, spine1[1] - h * 0.01, spine1[2] - h * 0.04],
            rx: 0.008 * s,
            rz: 0.008 * s,
          },
        ],
        palette.leather,
        { sides: 4, rings: 2, caps: true },
      );
    }
  }
  if (kind === "belt") {
    // Rides on the hip shelf, over whatever the lower slot put there.
    const m = padded(bodyMetrics(skeleton, ctx.body, ctx.bodyStyle), h * 0.014);
    const beltY = m.hipY - h * 0.005;
    loft(
      mesh,
      [
        { t: 0, c: [0, beltY, m.at("Hips")[2] + m.buttZ * 0.4], rx: m.hip.rx, rz: m.hip.rz },
        {
          t: 1,
          c: [0, beltY + h * 0.026, m.at("Hips")[2] + m.buttZ * 0.35],
          rx: m.hip.rx * 0.98,
          rz: m.hip.rz * 0.98,
        },
      ],
      color,
      { sides: TORSO_SIDES, rings: 2, caps: true, startRight: ACROSS },
    );
    const buckleZ = m.at("Hips")[2] + m.hip.rz + h * 0.004;
    loft(
      mesh,
      [
        { t: 0, c: [0, beltY + h * 0.004, buckleZ], rx: h * 0.018, rz: h * 0.008 },
        { t: 1, c: [0, beltY + h * 0.022, buckleZ], rx: h * 0.016, rz: h * 0.008 },
      ],
      palette.metal,
      { sides: 4, rings: 2, caps: true, startRight: ACROSS },
    );
  }
  if (kind === "glasses") {
    const hl = headLandmarks(ctx.skeleton, ctx.body, ctx.headStyle);
    const y = hl.brow[1] - hl.headW * 0.12;
    const z = hl.browFwd[2] + hl.headW * 0.05;
    const eyeX = hl.headW * 0.32;
    for (const side of [-1, 1]) {
      orientedBox(
        mesh,
        [side * eyeX, y, z],
        hl.headW * 0.2,
        hl.headW * 0.14,
        hl.headW * 0.07,
        [0, 0, 1],
        palette.metal,
      );
    }
    tube(
      mesh,
      [-eyeX * 0.55, y, z],
      [eyeX * 0.55, y, z],
      h * 0.004,
      h * 0.004,
      1,
      palette.metal,
      { sides: 4, sections: 1 },
    );
  }
  if (kind === "satchel") {
    const hip = at(skeleton, "Hips");
    loft(
      mesh,
      [
        {
          t: 0,
          c: [h * 0.12, hip[1] - h * 0.02, 0],
          rx: 0.04 * s,
          rz: 0.025 * s,
        },
        {
          t: 1,
          c: [h * 0.12, hip[1] - h * 0.09, 0],
          rx: 0.045 * s,
          rz: 0.028 * s,
        },
      ],
      color,
      { sides: 5, rings: 3, caps: true },
    );
    loft(
      mesh,
      [
        { t: 0, c: [0, hip[1] + h * 0.12, 0], rx: 0.007 * s, rz: 0.007 * s },
        { t: 1, c: [h * 0.12, hip[1], 0], rx: 0.007 * s, rz: 0.007 * s },
      ],
      palette.leather,
      { sides: 4, rings: 2, caps: true },
    );
  }
  return finish(mesh);
}

// --- coverage ---------------------------------------------------------------

/**
 * Skin each closed garment encloses, in body loft parameters.
 *
 * The numbers are read off the key lists in `body.ts` — the torso loft runs
 * crotch 0 → waist 0.4 → chest 0.7 → collar 1, arms deltoid 0.1 → elbow 0.52 →
 * wrist 0.86 → fingertip 1, legs hip 0 → knee 0.52 → ankle 1. Bands stop a
 * little short of each opening so a rim of skin recesses inside the cuff or
 * collar instead of ending on a hard cloth edge.
 *
 * Hair claims nothing: the caps are open-fronted, and hiding the scalp under
 * one shows straight through the face cut.
 */
const bothArms = (tMin: number, tMax: number): CoverageBand[] => [
  { part: PART.ARM_L, tMin, tMax },
  { part: PART.ARM_R, tMin, tMax },
];
const bothLegs = (tMin: number, tMax: number): CoverageBand[] => [
  { part: PART.LEG_L, tMin, tMax },
  { part: PART.LEG_R, tMin, tMax },
];
const bothFeet: CoverageBand[] = [
  { part: PART.FOOT_L, tMin: -0.01, tMax: 1.01 },
  { part: PART.FOOT_R, tMin: -0.01, tMax: 1.01 },
];

/** Waist-hem tops: torso from below the hem up to the collar, sleeve to cuff. */
const TORSO_TO_COLLAR = (hemT: number): CoverageBand[] => [
  { part: PART.TORSO, tMin: hemT, tMax: 0.97 },
];
const SHIRT_COVER: CoverageBand[] = [...TORSO_TO_COLLAR(0.3), ...bothArms(-0.01, 0.62)];
const JACKET_COVER: CoverageBand[] = [...TORSO_TO_COLLAR(0.28), ...bothArms(-0.01, 0.76)];
// Hem falls past the crotch, so the whole trunk goes.
const COAT_COVER: CoverageBand[] = [
  { part: PART.TORSO, tMin: -0.01, tMax: 0.97 },
  ...bothArms(-0.01, 0.76),
  ...bothLegs(-0.01, 0.3),
];
const PANTS_COVER: CoverageBand[] = [
  { part: PART.TORSO, tMin: -0.01, tMax: 0.38 },
  ...bothLegs(-0.01, 0.9),
];
const SKIRT_COVER: CoverageBand[] = [
  { part: PART.TORSO, tMin: -0.01, tMax: 0.38 },
  ...bothLegs(-0.01, 0.24),
];
const BOOTS_COVER: CoverageBand[] = [...bothFeet, ...bothLegs(0.86, 1.01)];
const SHOES_COVER: CoverageBand[] = [...bothFeet, ...bothLegs(0.97, 1.01)];

// --- catalog registration --------------------------------------------------

export const PART_DEFINITIONS: PartDefinition[] = [
  // Hair
  { id: "hair.none", slot: "hair", name: "None", build: () => createMesh() },
  { id: "hair.short", slot: "hair", name: "Short", build: (c) => hairCap(c, "short") },
  { id: "hair.long", slot: "hair", name: "Long", build: (c) => hairCap(c, "long") },
  { id: "hair.bun", slot: "hair", name: "Bun", build: (c) => hairCap(c, "bun") },
  { id: "hair.undercut", slot: "hair", name: "Undercut", build: (c) => hairCap(c, "undercut") },
  { id: "hair.messy", slot: "hair", name: "Messy", build: (c) => hairCap(c, "messy") },

  // Upper
  { id: "upper.none", slot: "upper", name: "None", build: () => createMesh() },
  { id: "upper.shirt", slot: "upper", name: "Shirt", build: (c) => upperGarment(c, "shirt"), coverage: SHIRT_COVER },
  { id: "upper.jacket", slot: "upper", name: "Jacket", build: (c) => upperGarment(c, "jacket"), coverage: JACKET_COVER },
  { id: "upper.coat", slot: "upper", name: "Coat", build: (c) => upperGarment(c, "coat"), coverage: COAT_COVER },
  { id: "upper.labcoat", slot: "upper", name: "Lab Coat", build: (c) => upperGarment(c, "labcoat"), coverage: COAT_COVER },
  { id: "upper.uniform", slot: "upper", name: "Uniform", build: (c) => upperGarment(c, "uniform"), coverage: JACKET_COVER },
  { id: "upper.workcoat", slot: "upper", name: "Work Coat", build: (c) => upperGarment(c, "workcoat"), coverage: JACKET_COVER },

  // Lower
  { id: "lower.none", slot: "lower", name: "None", build: () => createMesh() },
  { id: "lower.pants", slot: "lower", name: "Pants", build: (c) => lowerGarment(c, "pants"), coverage: PANTS_COVER },
  { id: "lower.cargo", slot: "lower", name: "Cargo Pants", build: (c) => lowerGarment(c, "cargo"), coverage: PANTS_COVER },
  { id: "lower.skirt", slot: "lower", name: "Skirt", body: "female", build: (c) => lowerGarment(c, "skirt"), coverage: SKIRT_COVER },

  // Feet
  { id: "feet.none", slot: "feet", name: "None", build: () => createMesh() },
  { id: "feet.boots", slot: "feet", name: "Boots", build: (c) => footwear(c, "boots"), coverage: BOOTS_COVER },
  { id: "feet.shoes", slot: "feet", name: "Shoes", build: (c) => footwear(c, "shoes"), coverage: SHOES_COVER },

  // Accessories
  { id: "accessory.none", slot: "accessory", name: "None", build: () => createMesh() },
  { id: "accessory.backpack", slot: "accessory", name: "Backpack", build: (c) => accessory(c, "backpack") },
  { id: "accessory.belt", slot: "accessory", name: "Belt", build: (c) => accessory(c, "belt") },
  { id: "accessory.glasses", slot: "accessory", name: "Glasses", build: (c) => accessory(c, "glasses") },
  { id: "accessory.satchel", slot: "accessory", name: "Satchel", build: (c) => accessory(c, "satchel") },
];

export const PART_CATALOGUE: ReadonlyMap<string, PartDefinition> = new Map(
  PART_DEFINITIONS.map((part) => [part.id, part]),
);

export function listParts(slot?: PartDefinition["slot"]): PartDefinition[] {
  return PART_DEFINITIONS.filter((part) => (slot ? part.slot === slot : true) && part.id !== `${part.slot}.none`);
}
