/**
 * Body style — three appearance sliders from ludus (dreamfall-style morphs).
 *
 *   mass   −1 lean … +1 heavy
 *   muscle −1 soft … +1 muscular
 *   fat     0 base … +1 fat
 *
 * Maps onto loft physique scalars used by the body + clothing builders.
 */

import type { BodyType } from "./types.js";

export interface BodyStyle {
  /** −1 lean … +1 heavy */
  mass: number;
  /** −1 soft … +1 muscular */
  muscle: number;
  /** 0 base … +1 fat */
  fat: number;
}

export const BODY_STYLE_DEFAULTS: BodyStyle = Object.freeze({
  mass: 0,
  muscle: 0,
  fat: 0,
});

/** Slider metadata for the creator UI. */
export const BODY_STYLE_SLIDERS = Object.freeze([
  {
    id: "mass" as const,
    label: "Mass",
    min: -1,
    max: 1,
    step: 0.01,
    left: "Lean",
    right: "Heavy",
  },
  {
    id: "muscle" as const,
    label: "Muscle",
    min: -1,
    max: 1,
    step: 0.01,
    left: "Soft",
    right: "Muscular",
  },
  {
    id: "fat" as const,
    label: "Fat",
    min: 0,
    max: 1,
    step: 0.01,
    left: "Base",
    right: "Heavy",
  },
]);

function clamp(v: number, lo: number, hi: number): number {
  if (!Number.isFinite(v)) return lo;
  return Math.max(lo, Math.min(hi, v));
}

/** Normalize a partial body bag. Missing keys become neutral. */
export function normalizeBodyStyle(raw: unknown): BodyStyle {
  if (!raw || typeof raw !== "object") {
    return { ...BODY_STYLE_DEFAULTS };
  }
  const o = raw as Record<string, unknown>;
  // Accept packed wire form { m, u, f } as well as full names.
  const mass = o.mass ?? o.m;
  const muscle = o.muscle ?? o.u;
  const fat = o.fat ?? o.f;
  return {
    mass: clamp(Number(mass ?? 0), -1, 1),
    muscle: clamp(Number(muscle ?? 0), -1, 1),
    fat: clamp(Number(fat ?? 0), 0, 1),
  };
}

export function isDefaultBodyStyle(style: BodyStyle): boolean {
  const s = normalizeBodyStyle(style);
  return s.mass === 0 && s.muscle === 0 && s.fat === 0;
}

/**
 * Ease unit strength so mid-slider stays mild and the last third punches
 * harder into caricature (leaner / bulkier / more cut / fatter).
 */
function easeExtreme(t: number): number {
  const a = clamp(t, 0, 1);
  return a * 0.35 + a * a * 0.25 + a * a * a * 0.4;
}

function shapedSigned(v: number): number {
  if (v === 0) return 0;
  return Math.sign(v) * easeExtreme(Math.abs(v));
}

/** Multipliers applied to loft half-widths (≈1 at neutral). */
export interface Physique {
  bulk: number;
  waistF: number;
  shoulderF: number;
  chestF: number;
  armF: number;
  legF: number;
  headF: number;
  style: BodyStyle;
}

/**
 * Map mass/muscle/fat onto loft physique scalars (ludus physiqueFromBodyStyle).
 * Deterministic — no rng jitter so the creator UI is stable.
 */
export function physiqueFromBodyStyle(style: BodyStyle | null | undefined): Physique {
  const s = normalizeBodyStyle(style);
  const mass = shapedSigned(s.mass);
  const muscle = shapedSigned(s.muscle);
  const fat = easeExtreme(s.fat);

  // Peak gains at shaped = ±1 — readable under clothing at 240p.
  const bulkBase = 1 + mass * 0.32 + fat * 0.26 + muscle * 0.1;
  const waistBase = 1 + fat * 0.48 + mass * 0.2 - muscle * 0.18;
  const shoulderBase = 1 + muscle * 0.42 + mass * 0.16 + fat * 0.1;
  const chestBase = 1 + muscle * 0.28 + mass * 0.14 + fat * 0.18;
  const armBase = 1 + muscle * 0.48 + mass * 0.14 + fat * 0.12;
  const legBase = 1 + fat * 0.36 + mass * 0.2 + muscle * 0.16;
  const headBase = 1 + mass * 0.06 + fat * 0.05;

  return {
    bulk: bulkBase,
    waistF: Math.max(0.52, waistBase),
    shoulderF: Math.max(0.62, shoulderBase),
    chestF: Math.max(0.65, chestBase),
    armF: Math.max(0.58, armBase),
    legF: Math.max(0.6, legBase),
    headF: Math.max(0.85, headBase),
    style: s,
  };
}

/**
 * Sex base (per sex) × slider multipliers.
 *
 * These sit near 1.0 on purpose. Breadth differences between the male and
 * female bases live in the width tables in `proportions.ts`, which
 * `bodyMetrics` reads directly; anything but ~1 here would discount those a
 * second time — which is exactly what the earlier 0.78–0.86 "slender" bases
 * did, taking a third off every limb. What is left here is the girth
 * difference breadth alone doesn't capture: limbs and neck.
 */
export function physiqueFor(
  body: BodyType,
  style: BodyStyle | null | undefined = null,
): Physique {
  const female = body === "female";
  const base = {
    bulk: female ? 0.97 : 1.0,
    waistF: female ? 0.98 : 1.0,
    shoulderF: female ? 0.96 : 1.0,
    chestF: female ? 0.98 : 1.0,
    armF: female ? 0.9 : 1.0,
    legF: female ? 0.95 : 1.0,
    headF: female ? 1.01 : 1.0,
  };
  const slider = physiqueFromBodyStyle(style);
  return {
    bulk: base.bulk * slider.bulk,
    waistF: base.waistF * slider.waistF,
    shoulderF: base.shoulderF * slider.shoulderF,
    chestF: base.chestF * slider.chestF,
    armF: base.armF * slider.armF,
    legF: base.legF * slider.legF,
    headF: base.headF * slider.headF,
    style: slider.style,
  };
}
