import type { CanonicalBone } from "@psx/shared";
import { jointWorld, type Skeleton } from "../rig/skeleton.js";
import type { LoftKey, Vec3Tuple } from "../mesh/MeshBuilder.js";
import { physiqueFor, type BodyStyle, type Physique } from "./bodyStyle.js";
import { craniumFor, type Cranium, type HeadStyle } from "./headStyle.js";
import { measurementsFor } from "./proportions.js";
import type { BodyType } from "./types.js";

/**
 * Every dimension of a figure, in metres, derived once.
 *
 * Before this existed, `body.ts` and `parts.ts` each carried their own magic
 * half-widths scaled off `height / 1.72`, and clothing fitted the body only
 * because both files had been nudged in tandem. Sizes now come from one place:
 * skin builds at the metrics, garments build at `padded(metrics, thickness)`.
 * A jacket is the body plus 12 mm of cloth, which is what a jacket is.
 *
 * Widths come from {@link measurementsFor} (fractions of height); depths and
 * the offsets a front-view photo cannot show — how far the glutes sit aft of
 * the hip axis, how far the calf sits behind the shin — are anatomical
 * constants applied here.
 */

/** Elliptical cross-section: half-width across the ring, half-depth through it. */
export interface Section {
  rx: number;
  rz: number;
}

const sec = (rx: number, rz: number): Section => ({ rx, rz });

export interface BodyMetrics {
  height: number;
  female: boolean;
  physique: Physique;

  /** Joint lookup, so callers don't re-import the skeleton helpers. */
  at: (bone: CanonicalBone) => Vec3Tuple;

  // --- torso levels (world Y) ---
  crotchY: number;
  gluteY: number;
  hipY: number;
  waistY: number;
  ribY: number;
  chestY: number;
  girdleY: number;
  collarY: number;
  neckTopY: number;

  // --- torso sections ---
  crotch: Section;
  glute: Section;
  hip: Section;
  waist: Section;
  rib: Section;
  chest: Section;
  girdle: Section;
  collar: Section;
  neck: Section;

  /** Z offsets from the spine line: glutes aft, belly and sternum forward. */
  buttZ: number;
  bellyZ: number;
  chestZ: number;

  /** Breast form (female). `out` is projection past the chest wall. */
  bust: { r: number; x: number; y: number; z: number; out: number };
  /** Pectoral shelf (male) — a flat pad, not a muscle. */
  pec: { rx: number; rz: number; x: number; y: number; z: number };

  // --- arm ---
  clavX: number;
  socketX: number;
  deltoidR: number;
  upperArm: Section;
  elbow: Section;
  foreArm: Section;
  wrist: Section;
  /** Palm: rx is thickness (knuckle to palm), rz is breadth. */
  palm: Section;
  handLength: number;
  thumbR: number;

  // --- leg ---
  hipSocket: Section;
  thigh: Section;
  knee: Section;
  calf: Section;
  ankle: Section;
  /** Calf mass sits behind the shin axis. */
  calfZ: number;

  // --- foot ---
  footHalfW: number;
  footLength: number;
  heelBack: number;
  ankleHeight: number;

  // --- head ---
  headHalfW: number;
  headHalfD: number;
  chinY: number;
  jawY: number;
  mouthY: number;
  browY: number;
  crownY: number;
  headZ: number;
  /**
   * Face shape scalars — sex base × head sliders. The dimensions above already
   * have the size terms baked in; this carries the *proportions* the head loft
   * needs (how wide the jaw is against the cheekbones, how far the brow juts).
   */
  cranium: Cranium;
}

export function bodyMetrics(
  skeleton: Skeleton,
  body: BodyType,
  bodyStyle: BodyStyle | null = null,
  headStyle: HeadStyle | null = null,
): BodyMetrics {
  const h = skeleton.height;
  const female = body === "female";
  const phy = physiqueFor(body, bodyStyle);
  const cranium = craniumFor(body, headStyle);
  const { bulk, waistF, shoulderF, chestF, armF, legF, headF } = phy;
  const w = measurementsFor(body).widths;
  const at = (bone: CanonicalBone): Vec3Tuple => jointWorld(skeleton, bone);

  // Half-breadths in metres before physique.
  const halfShoulder = (w.shoulder / 2) * h;
  const halfChest = (w.chest / 2) * h;
  const halfWaist = (w.waist / 2) * h;
  const halfHip = (w.hip / 2) * h;
  const halfHead = (w.head / 2) * h;
  const halfNeck = (w.neck / 2) * h;

  const hips = at("Hips");
  const spine = at("Spine");
  const spine1 = at("Spine1");
  const spine2 = at("Spine2");
  const neckJoint = at("Neck");
  const headJoint = at("Head");
  const shoulderJoint = at("LeftArm");

  // Levels. The skeleton already carries the landmark heights; these name the
  // ones the loft needs and that no bone sits on.
  const crotchY = hips[1] - h * (female ? 0.033 : 0.031);
  const gluteY = hips[1] - h * 0.012;
  const hipY = hips[1] + h * 0.012;
  const waistY = spine[1] + (spine1[1] - spine[1]) * 0.35;
  const ribY = spine1[1] + (spine2[1] - spine1[1]) * 0.45;
  const chestY = spine2[1] - h * 0.005;
  // The Neck bone is C7 — the base of the neck, level with the shoulders — so
  // the collar ring sits *above* the girdle and the trapezius slopes between.
  const girdleY = shoulderJoint[1];
  const collarY = neckJoint[1] + h * 0.008;

  // Head size and proportion, needed here because the neck ends inside the jaw.
  const headHalfW = halfHead * headF * cranium.widthF;
  const chinY = h - h * 0.132 * headF * cranium.faceLengthF;
  // Root ring of the head loft — under the jaw, buried in the neck.
  const headBottomY = chinY - headHalfW * 0.16;

  // The neck stops just inside that ring. Pinning it to the Head joint instead
  // (as this did before the face sliders) leaves it fixed while a round face
  // lifts the chin: past about −0.5 on the length slider the head floats off
  // and the neck's end cap shows as a disk under a detached jaw.
  const neckTopY = headBottomY + h * 0.011;

  // Trunk at shoulder level stops short of the acromion; the deltoids carry
  // the silhouette out the rest of the way.
  const girdleRx = halfShoulder * 0.78 * bulk * shoulderF;
  const deltoidR = h * 0.031 * Math.sqrt(bulk) * (0.72 + 0.34 * armF);

  const chestRx = halfChest * bulk * chestF;
  const waistRx = halfWaist * bulk * waistF;
  const hipRx = halfHip * bulk * (female ? 1 : 0.98);

  return {
    height: h,
    female,
    physique: phy,
    at,

    crotchY,
    gluteY,
    hipY,
    waistY,
    ribY,
    chestY,
    girdleY,
    collarY,
    neckTopY,

    // The crotch ring is the saddle between the thighs, not a hip section.
    crotch: sec(hipRx * 0.4, hipRx * 0.5),
    glute: sec(hipRx * 0.94, hipRx * (female ? 0.86 : 0.8)),
    hip: sec(hipRx, hipRx * 0.78),
    waist: sec(waistRx, waistRx * 0.76),
    rib: sec(chestRx * 0.93, chestRx * 0.74),
    chest: sec(chestRx, chestRx * (female ? 0.76 : 0.79)),
    girdle: sec(girdleRx, girdleRx * 0.62),
    // Tight to the neck: this is the torso's top ring, and its end cap is only
    // hidden if the neck loft is wider than it at that height.
    collar: sec(halfNeck * 1.12 * bulk, halfNeck * 1.24 * bulk),
    neck: sec(halfNeck * bulk, halfNeck * 1.14 * bulk),

    buttZ: -h * (female ? 0.02 : 0.016),
    bellyZ: h * (0.004 + phy.style.fat * 0.016),
    chestZ: h * 0.008,

    bust: {
      r: h * (female ? 0.032 : 0) * (0.88 + 0.24 * chestF),
      x: halfChest * 0.46,
      y: chestY + h * 0.006,
      z: spine2[2] + chestRx * 0.6,
      out: h * 0.026,
    },
    // Only emerges past the chest wall on a muscular slider; at rest it stays
    // buried and the chest keeps its flat PSX plane.
    pec: {
      rx: chestRx * 0.56,
      rz: h * 0.03 * Math.max(0, phy.style.muscle),
      x: chestRx * 0.42,
      y: chestY + h * 0.016,
      z: spine2[2] + chestRx * 0.66,
    },

    clavX: halfShoulder * 0.3,
    socketX: Math.abs(shoulderJoint[0]),
    deltoidR,
    upperArm: sec((w.upperArm / 2) * h * armF * bulk, (w.upperArm / 2) * h * armF * bulk * 0.94),
    elbow: sec((w.forearm / 2) * h * armF * bulk * 1.02, (w.forearm / 2) * h * armF * bulk * 0.98),
    foreArm: sec((w.forearm / 2) * h * armF * bulk, (w.forearm / 2) * h * armF * bulk * 0.9),
    wrist: sec(h * 0.016 * armF, h * 0.02 * armF),
    palm: sec(h * 0.011 * armF, h * 0.026 * armF),
    handLength: h * 0.095,
    thumbR: h * 0.011,

    hipSocket: sec(hipRx * 0.58, hipRx * 0.6),
    thigh: sec((w.thigh / 2) * h * legF * bulk, (w.thigh / 2) * h * legF * bulk * 0.92),
    knee: sec(h * 0.031 * legF, h * 0.033 * legF),
    calf: sec((w.calf / 2) * h * legF * bulk, (w.calf / 2) * h * legF * bulk * 1.08),
    ankle: sec(h * 0.021 * legF, h * 0.026 * legF),
    calfZ: -h * 0.009,

    footHalfW: (w.foot / 2) * h,
    footLength: h * 0.152,
    heelBack: h * 0.038,
    ankleHeight: h * 0.045,

    // Size comes from the body (`headF`), proportion from the face sliders.
    // Levels are measured down from the crown, so a longer face drops the chin
    // rather than lifting the skull off the neck.
    headHalfW,
    headHalfD: halfHead * headF * cranium.depthF * 1.26,
    chinY,
    jawY: h - h * 0.105 * headF * cranium.faceLengthF,
    mouthY: h - h * 0.098 * headF * cranium.faceLengthF,
    browY: h - h * 0.056 * headF * cranium.faceLengthF,
    crownY: h,
    headZ: headJoint[2],
    cranium,
  };
}

/**
 * The torso ring plan, crotch (t=0) to collar (t=1).
 *
 * Shared by the skin and by every torso garment. A garment that invents its own
 * key list will sooner or later skip a section the body has — the labcoat that
 * went hem → hip skipped the glutes, and the trousers underneath poked through
 * the back of it. Take the body's plan, grow it by cloth, cut it at the hem.
 *
 * The `t` values here are the coverage parameters garments cite in
 * `parts.ts`: waist 0.4, chest 0.7, girdle 0.87.
 */
export function torsoPlan(m: BodyMetrics): LoftKey[] {
  const h = m.height;
  const hips = m.at("Hips");
  const spine = m.at("Spine");
  const spine1 = m.at("Spine1");
  const spine2 = m.at("Spine2");
  const neck = m.at("Neck");

  return [
    // Saddle between the thighs. Narrow and tucked forward so the crotch closes
    // instead of showing daylight between two leg tubes.
    { t: 0.0, c: [0, m.crotchY, hips[2] + h * 0.008], rx: m.crotch.rx, rz: m.crotch.rz },
    // Glutes — the widest part of the figure from behind, set aft of the axis.
    { t: 0.08, c: [0, m.gluteY, hips[2] + m.buttZ], rx: m.glute.rx, rz: m.glute.rz },
    // Hip shelf / iliac crest.
    { t: 0.17, c: [0, m.hipY, hips[2] + m.buttZ * 0.4], rx: m.hip.rx, rz: m.hip.rz },
    // Lower abdomen.
    {
      t: 0.28,
      c: [0, (m.hipY + m.waistY) * 0.5, spine[2] + m.bellyZ],
      rx: (m.hip.rx + m.waist.rx) * 0.5,
      rz: (m.hip.rz + m.waist.rz) * 0.52,
    },
    // Waist — narrowest, and on the female base the whole silhouette turns here.
    { t: 0.4, c: [0, m.waistY, spine[2] + m.bellyZ * 0.8], rx: m.waist.rx, rz: m.waist.rz },
    // Lower ribs.
    { t: 0.55, c: [0, m.ribY, spine1[2] + m.chestZ * 0.7], rx: m.rib.rx, rz: m.rib.rz },
    // Chest at nipple line.
    { t: 0.7, c: [0, m.chestY, spine2[2] + m.chestZ], rx: m.chest.rx, rz: m.chest.rz },
    // Clavicle shelf: flattens front-to-back before the shoulders.
    {
      t: 0.83,
      c: [0, m.chestY + (m.girdleY - m.chestY) * 0.72, spine2[2] + m.chestZ * 0.6],
      rx: (m.chest.rx + m.girdle.rx) * 0.5,
      rz: (m.chest.rz + m.girdle.rz) * 0.5,
    },
    // Shoulder girdle — trunk only; the deltoids extend past this.
    { t: 0.87, c: [0, m.girdleY, neck[2] + h * 0.004], rx: m.girdle.rx, rz: m.girdle.rz },
    // Trapezius. Two keys close together, or the slope from acromion to neck
    // reads as one hard step — a square coat hanger instead of a shoulder.
    {
      t: 0.94,
      c: [0, m.girdleY + (m.collarY - m.girdleY) * 0.45, neck[2] + h * 0.005],
      rx: m.girdle.rx * 0.7,
      rz: m.girdle.rz * 0.82,
    },
    { t: 1.0, c: [0, m.collarY, neck[2] + h * 0.006], rx: m.collar.rx, rz: m.collar.rz },
  ];
}

/**
 * Cut a plan off below `hemY`, replacing everything under it with one ring
 * interpolated at that height. `t` is renormalised so the result still runs
 * 0..1 — coverage bands are quoted against the *body's* plan, so they are read
 * before this is applied.
 */
export function cutBelow(keys: LoftKey[], hemY: number): LoftKey[] {
  const above = keys.filter((k) => k.c[1] > hemY);
  if (above.length === keys.length || above.length < 2) return keys;

  const first = above[0]!;
  const below = keys[keys.length - above.length - 1]!;
  const span = first.c[1] - below.c[1];
  const f = span > 1e-6 ? (hemY - below.c[1]) / span : 0;
  const mix = (a: number, b: number) => a + (b - a) * f;
  const hem: LoftKey = {
    t: 0,
    c: [0, hemY, mix(below.c[2], first.c[2])],
    rx: mix(below.rx, first.rx),
    rz: mix(below.rz, first.rz),
  };

  const t0 = first.t;
  return [
    hem,
    ...above.map((k) => ({ ...k, t: (k.t - t0) / (1 - t0) })),
  ];
}

/**
 * Replace the bottom of a plan with an open, flared hem.
 *
 * A coat is not a body: taking the torso plan all the way down inherits the
 * crotch saddle, and the coat tapers to a point between the legs instead of
 * hanging as a tube. Cut at the hips, then drop one wide ring to the hem.
 */
export function withHem(
  keys: LoftKey[],
  hemY: number,
  hem: Section,
  hemZ: number,
  share = 0.2,
): LoftKey[] {
  const above = keys.filter((k) => k.c[1] > hemY);
  if (above.length < 2) return keys;
  const t0 = above[0]!.t;
  const rescale = (t: number) => share + ((t - t0) / (1 - t0)) * (1 - share);
  return [
    { t: 0, c: [0, hemY, hemZ], rx: hem.rx, rz: hem.rz },
    ...above.map((k) => ({ ...k, t: rescale(k.t) })),
  ];
}

/** As {@link cutBelow}, from the other end: keep everything under `waistY`. */
export function cutAbove(keys: LoftKey[], waistY: number): LoftKey[] {
  const below = keys.filter((k) => k.c[1] < waistY);
  if (below.length === keys.length || below.length < 2) return keys;

  const last = below[below.length - 1]!;
  const above = keys[below.length]!;
  const span = above.c[1] - last.c[1];
  const f = span > 1e-6 ? (waistY - last.c[1]) / span : 0;
  const mix = (a: number, b: number) => a + (b - a) * f;
  const band: LoftKey = {
    t: 1,
    c: [0, waistY, mix(last.c[2], above.c[2])],
    rx: mix(last.rx, above.rx),
    rz: mix(last.rz, above.rz),
  };

  const t1 = last.t + (above.t - last.t) * f;
  return [...below.map((k) => ({ ...k, t: k.t / t1 })), band];
}

/**
 * The same figure, grown by `pad` metres of cloth.
 *
 * Garments loft on these so they clear the skin by a known thickness instead of
 * a percentage — a 12 mm jacket is 12 mm at every scale, and a heavy coat over
 * a heavy body still clears it.
 */
export function padded(metrics: BodyMetrics, pad: number): BodyMetrics {
  const grow = (s: Section): Section => sec(s.rx + pad, s.rz + pad);
  return {
    ...metrics,
    crotch: grow(metrics.crotch),
    glute: grow(metrics.glute),
    hip: grow(metrics.hip),
    waist: grow(metrics.waist),
    rib: grow(metrics.rib),
    chest: grow(metrics.chest),
    girdle: grow(metrics.girdle),
    collar: grow(metrics.collar),
    neck: grow(metrics.neck),
    deltoidR: metrics.deltoidR + pad,
    upperArm: grow(metrics.upperArm),
    elbow: grow(metrics.elbow),
    foreArm: grow(metrics.foreArm),
    wrist: grow(metrics.wrist),
    hipSocket: grow(metrics.hipSocket),
    thigh: grow(metrics.thigh),
    knee: grow(metrics.knee),
    calf: grow(metrics.calf),
    ankle: grow(metrics.ankle),
    footHalfW: metrics.footHalfW + pad,
  };
}
