/**
 * Canonical skeleton — GDD §10.2.
 *
 * Mixamo-compatible core set. The harness builds joints under these names and
 * binds skin weights to them, so every generated character plays the same
 * procedural clips (and any future shared animation library).
 */

export const CANONICAL_BONES = [
  "Hips",
  "Spine",
  "Spine1",
  "Spine2",
  "Neck",
  "Head",
  "LeftShoulder",
  "LeftArm",
  "LeftForeArm",
  "LeftHand",
  "RightShoulder",
  "RightArm",
  "RightForeArm",
  "RightHand",
  "LeftUpLeg",
  "LeftLeg",
  "LeftFoot",
  "LeftToeBase",
  "RightUpLeg",
  "RightLeg",
  "RightFoot",
  "RightToeBase",
] as const;

export type CanonicalBone = (typeof CANONICAL_BONES)[number];

/** Parent of each bone; `null` marks the skeleton root. */
export const CANONICAL_HIERARCHY: Record<CanonicalBone, CanonicalBone | null> = {
  Hips: null,
  Spine: "Hips",
  Spine1: "Spine",
  Spine2: "Spine1",
  Neck: "Spine2",
  Head: "Neck",
  LeftShoulder: "Spine2",
  LeftArm: "LeftShoulder",
  LeftForeArm: "LeftArm",
  LeftHand: "LeftForeArm",
  RightShoulder: "Spine2",
  RightArm: "RightShoulder",
  RightForeArm: "RightArm",
  RightHand: "RightForeArm",
  LeftUpLeg: "Hips",
  LeftLeg: "LeftUpLeg",
  LeftFoot: "LeftLeg",
  LeftToeBase: "LeftFoot",
  RightUpLeg: "Hips",
  RightLeg: "RightUpLeg",
  RightFoot: "RightLeg",
  RightToeBase: "RightFoot",
};

/** Bones a rigger must produce for a clip to be playable at all. */
export const REQUIRED_BONES: readonly CanonicalBone[] = [
  "Hips",
  "Spine",
  "Head",
  "LeftArm",
  "RightArm",
  "LeftUpLeg",
  "LeftLeg",
  "LeftFoot",
  "RightUpLeg",
  "RightLeg",
  "RightFoot",
];

const CANONICAL_SET: ReadonlySet<string> = new Set(CANONICAL_BONES);

export function isCanonicalBone(name: string): name is CanonicalBone {
  return CANONICAL_SET.has(name);
}

/** Depth-first walk order, parents always before children. */
export function canonicalWalkOrder(): 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;
}

/**
 * Common aliases emitted by third-party riggers, normalised to canonical names.
 * Mixamo's `mixamorig:` prefix is stripped before lookup.
 */
export const BONE_ALIASES: Record<string, CanonicalBone> = {
  hip: "Hips",
  pelvis: "Hips",
  root: "Hips",
  spine_01: "Spine",
  spine_02: "Spine1",
  spine_03: "Spine2",
  chest: "Spine2",
  upperchest: "Spine2",
  head_end: "Head",
  clavicle_l: "LeftShoulder",
  clavicle_r: "RightShoulder",
  upperarm_l: "LeftArm",
  upperarm_r: "RightArm",
  lowerarm_l: "LeftForeArm",
  lowerarm_r: "RightForeArm",
  hand_l: "LeftHand",
  hand_r: "RightHand",
  thigh_l: "LeftUpLeg",
  thigh_r: "RightUpLeg",
  calf_l: "LeftLeg",
  calf_r: "RightLeg",
  foot_l: "LeftFoot",
  foot_r: "RightFoot",
  ball_l: "LeftToeBase",
  ball_r: "RightToeBase",
};

/** Best-effort mapping of an arbitrary rigger bone name onto the canonical set. */
export function normaliseBoneName(raw: string): CanonicalBone | null {
  const stripped = raw.replace(/^mixamorig[:_]?/i, "").trim();
  if (isCanonicalBone(stripped)) return stripped;

  const key = stripped.toLowerCase().replace(/[\s.-]+/g, "_");
  const alias = BONE_ALIASES[key];
  if (alias) return alias;

  // `LeftArm` vs `leftarm` vs `Left_Arm`
  const squashed = key.replace(/_/g, "");
  for (const bone of CANONICAL_BONES) {
    if (bone.toLowerCase() === squashed) return bone;
  }
  return null;
}
