import { CANONICAL_BONES, CANONICAL_HIERARCHY, type CanonicalBone } from "@psx/shared";
import type { Measurements } from "../image/analyze.js";

/**
 * Builds a canonical skeleton scaled to a measured figure.
 *
 * Bone *positions* come from the reference wherever the analyzer measured
 * something; the ones a front-view silhouette cannot show — how far forward the
 * shoulder joint sits inside the deltoid, the depth of the hip socket — come
 * from fixed anatomical ratios. Those are the same for every character, so
 * clips authored against the canonical skeleton stay portable (GDD §10.2).
 *
 * Coordinates are metres, Y up, Z forward, origin between the feet — matching
 * the runtime's world space, so an exported GLB drops straight in.
 */

export interface Joint {
  name: CanonicalBone;
  parent: CanonicalBone | null;
  /** Rest position in world space. */
  world: [number, number, number];
  /** Rest position relative to the parent joint; what the GLB node stores. */
  local: [number, number, number];
}

export interface Skeleton {
  joints: Joint[];
  index: Map<CanonicalBone, number>;
  /** Total height in metres, feet to crown. */
  height: number;
}

export interface SkeletonOptions {
  /** Target world height in metres. PSX protagonists sit around 1.7. */
  height?: number;
}

/** Depth of the body, as a fraction of height. Not observable from the front. */
const BODY_DEPTH = 0.06;

export function buildSkeleton(
  measurements: Measurements,
  options: SkeletonOptions = {},
): Skeleton {
  const height = options.height ?? 1.7;
  const { landmarks, widths } = measurements;

  // The analyzer measures downward from the crown; the skeleton is built upward
  // from the floor, so every landmark is flipped here once.
  const up = (fromCrown: number): number => (1 - fromCrown) * height;
  const across = (fraction: number): number => fraction * height;

  const shoulderY = up(landmarks.shoulder.y);
  const neckY = up(landmarks.neck.y);
  const hipY = up(landmarks.hip.y);
  const crotchY = up(landmarks.crotch.y);
  const kneeY = up(landmarks.knee.y);
  const ankleY = up(landmarks.ankle.y);
  const chestY = up(landmarks.chest.y);
  const waistY = up(landmarks.waist.y);

  // Hips sit slightly above the crotch: the joint is inside the pelvis, not at
  // the gap between the legs.
  const hipsRootY = crotchY + (hipY - crotchY) * 0.4 + height * 0.015;

  const halfHip = across(widths.hip) / 2;
  const halfShoulder = across(widths.shoulder) / 2;
  // Slightly wider stance than pure silhouette so legs clear the coat hem.
  const legX = Math.max(halfHip * 0.48, across(Math.max(measurements.stance, 0.045)));

  // Deltoid sits inboard of the silhouette edge.
  const shoulderJointX = halfShoulder * 0.82;
  // Shoulder joint to fingertip, split on standard segment ratios
  // (humerus 42% / radius 33% / hand 25%). The hand length is left to the mesh:
  // the Hand joint is the wrist.
  const armLength = Math.max(across(measurements.armReach) - shoulderJointX, height * 0.32);
  const upperArmLength = armLength * 0.423;
  const forearmLength = armLength * 0.332;
  const handLength = armLength * 0.245;

  // Soft A-pose (~22° from the body) — SH1 rest pose, not a hard T or 45° V.
  const armAngle = (22 * Math.PI) / 180;
  const armDrop = Math.cos(armAngle);
  const armOut = Math.sin(armAngle);

  // Mild chest forward offset so the torso has a front, not a flat slab.
  const chestZ = across(BODY_DEPTH) * 0.35;

  const world = new Map<CanonicalBone, [number, number, number]>();
  const set = (bone: CanonicalBone, x: number, y: number, z: number) =>
    world.set(bone, [x, y, z]);

  set("Hips", 0, hipsRootY, 0);
  set("Spine", 0, hipsRootY + (waistY - hipsRootY) * 0.5, chestZ * 0.15);
  set("Spine1", 0, waistY + (chestY - waistY) * 0.45, chestZ * 0.55);
  set("Spine2", 0, chestY + (shoulderY - chestY) * 0.15, chestZ * 0.75);
  set("Neck", 0, neckY - height * 0.01, chestZ * 0.4);
  set("Head", 0, neckY + height * 0.035, chestZ * 0.35);

  for (const side of [-1, 1] as const) {
    const prefix = side < 0 ? "Right" : "Left";

    // Clavicle sits near the neck; shoulder joint out at the deltoid.
    set(
      `${prefix}Shoulder` as CanonicalBone,
      side * halfShoulder * 0.28,
      shoulderY + height * 0.005,
      chestZ * 0.5,
    );
    set(
      `${prefix}Arm` as CanonicalBone,
      side * shoulderJointX,
      shoulderY - height * 0.01,
      chestZ * 0.25,
    );
    set(
      `${prefix}ForeArm` as CanonicalBone,
      side * (shoulderJointX + upperArmLength * armOut),
      shoulderY - upperArmLength * armDrop,
      chestZ * 0.1,
    );
    set(
      `${prefix}Hand` as CanonicalBone,
      side * (shoulderJointX + (upperArmLength + forearmLength) * armOut),
      shoulderY - (upperArmLength + forearmLength) * armDrop,
      chestZ * 0.05,
    );

    set(`${prefix}UpLeg` as CanonicalBone, side * legX, hipsRootY - height * 0.01, 0);
    set(`${prefix}Leg` as CanonicalBone, side * legX, kneeY, 0);
    set(`${prefix}Foot` as CanonicalBone, side * legX, ankleY, 0);
    set(
      `${prefix}ToeBase` as CanonicalBone,
      side * legX,
      across(0.02),
      across(BODY_DEPTH) * 2.1,
    );
  }

  void handLength;

  const joints: Joint[] = [];
  const index = new Map<CanonicalBone, number>();

  // Parents must precede children so local offsets can be resolved in one pass.
  for (const bone of orderedBones()) {
    const position = world.get(bone);
    if (!position) throw new Error(`skeleton is missing bone ${bone}`);

    const parent = CANONICAL_HIERARCHY[bone];
    const parentPosition = parent ? world.get(parent) : null;
    const local: [number, number, number] = parentPosition
      ? [
          position[0] - parentPosition[0],
          position[1] - parentPosition[1],
          position[2] - parentPosition[2],
        ]
      : [...position];

    index.set(bone, joints.length);
    joints.push({ name: bone, parent, world: position, local });
  }

  return { joints, index, height };
}

/** Depth-first order, parents before children. */
function orderedBones(): CanonicalBone[] {
  const out: CanonicalBone[] = [];
  const visit = (bone: CanonicalBone) => {
    out.push(bone);
    for (const child of CANONICAL_BONES) {
      if (CANONICAL_HIERARCHY[child] === bone) visit(child);
    }
  };
  visit("Hips");
  return out;
}

/** Rest-pose world position of a bone. */
export function jointWorld(skeleton: Skeleton, bone: CanonicalBone): [number, number, number] {
  const joint = skeleton.joints[skeleton.index.get(bone)!];
  if (!joint) throw new Error(`unknown bone ${bone}`);
  return joint.world;
}
