import type { CanonicalBone } from "@psx/shared";

/**
 * Procedural animation clips authored against the canonical skeleton.
 *
 * Because every channel targets a canonical bone *name* rather than a joint
 * index, a clip generated for one character plays on any other character the
 * harness produces (GDD §10.2). Nothing here depends on the figure's
 * proportions — only on its topology.
 *
 * Rotation-only, and deliberately so: root translation would fight the
 * runtime's `CharacterMover`, which owns the character's position. The mover
 * drives where the body goes; these clips only say what the limbs do.
 *
 * ## Axis convention
 *
 * The harness skeleton uses **identity rest rotations** and local translations
 * only (Y-up, Z-forward, right-handed — same as the runtime). Under that setup:
 *
 *  - A limb hanging down the −Y axis swings **backward (−Z)** under **+X** pitch.
 *  - Forward swing (toward +Z, the facing direction) is therefore **−X**.
 *  - Knee flex (shin folds back toward −Z when the thigh is vertical) is **+X**.
 *  - Elbow flex (hand comes forward toward +Z when the upper arm hangs) is **−X**.
 *  - Spine lean toward +Z (forward) is **+X** (children sit along +Y).
 */

export interface Keyframe {
  time: number;
  /** Euler XYZ in radians, converted to quaternions at export. */
  rotation: [number, number, number];
}

export interface AnimationChannel {
  bone: CanonicalBone;
  keyframes: Keyframe[];
}

export interface AnimationClip {
  name: string;
  duration: number;
  loop: boolean;
  channels: AnimationChannel[];
}

/** Samples per cycle. Low on purpose — PSX animation was chunky. */
const SAMPLES = 8;

/**
 * Builds a channel by sampling a function over one cycle.
 * The final key repeats the first so looping playback has no seam.
 */
function cycle(
  bone: CanonicalBone,
  duration: number,
  fn: (phase: number) => [number, number, number],
): AnimationChannel {
  const keyframes: Keyframe[] = [];
  for (let i = 0; i <= SAMPLES; i++) {
    const phase = i / SAMPLES;
    keyframes.push({ time: phase * duration, rotation: fn(phase % 1) });
  }
  return { bone, keyframes };
}

const wave = (phase: number, offset = 0): number => Math.sin((phase + offset) * Math.PI * 2);

/** Neutral pose — every bone at rest. Used as the bind/reset clip. */
export function buildIdlePose(): AnimationClip {
  return {
    name: "TPose",
    duration: 0.001,
    loop: false,
    channels: [{ bone: "Hips", keyframes: [{ time: 0, rotation: [0, 0, 0] }] }],
  };
}

/**
 * Breathing idle. Survival horror characters stand tense, so the motion is
 * small and slow — big idle sway reads as comedic.
 */
export function buildIdleClip(duration = 4): AnimationClip {
  return {
    name: "Idle",
    duration,
    loop: true,
    channels: [
      // Spine children sit along +Y: +X leans forward (+Z). Tiny breath.
      cycle("Spine", duration, (p) => [wave(p) * 0.022, 0, 0]),
      cycle("Spine1", duration, (p) => [wave(p, 0.08) * 0.018, 0, 0]),
      cycle("Neck", duration, (p) => [wave(p, 0.15) * -0.03, wave(p, 0.3) * 0.04, 0]),
      // Soft arm hang: slight −X is forward; elbows rest slightly flexed (−X).
      cycle("LeftArm", duration, (p) => [wave(p, 0.1) * -0.03, 0, 0.05]),
      cycle("RightArm", duration, (p) => [wave(p, 0.1) * -0.03, 0, -0.05]),
      cycle("LeftForeArm", duration, (p) => [-0.2 + wave(p, 0.2) * -0.02, 0, 0]),
      cycle("RightForeArm", duration, (p) => [-0.2 + wave(p, 0.2) * -0.02, 0, 0]),
    ],
  };
}

/**
 * Walk cycle. Arms swing in opposition to the legs — the single detail that
 * makes a walk read as a walk rather than a shuffle.
 */
export function buildWalkClip(duration = 1, stride = 0.55): AnimationClip {
  return {
    name: "Walk",
    duration,
    loop: true,
    channels: [
      // Counter-rotation through the torso, small enough not to look like a strut.
      cycle("Hips", duration, (p) => [0, wave(p) * 0.06, 0]),
      // Slight forward lean (+X with +Y spine).
      cycle("Spine", duration, (p) => [0.05, wave(p) * -0.05, 0]),
      cycle("Spine2", duration, (p) => [0.02, wave(p) * -0.07, 0]),

      // Hips: −X is forward (+Z). Opposite legs are half a cycle apart.
      cycle("LeftUpLeg", duration, (p) => [-wave(p) * stride, 0, 0]),
      cycle("RightUpLeg", duration, (p) => [-wave(p, 0.5) * stride, 0, 0]),
      // Knees only flex one way: +X folds the shin back (foot toward −Z when vertical).
      // Peak flex just after the hip starts the swing (phase offset 0.25 / 0.75).
      cycle("LeftLeg", duration, (p) => [Math.max(0, wave(p, 0.25)) * stride * 1.15, 0, 0]),
      cycle("RightLeg", duration, (p) => [Math.max(0, wave(p, 0.75)) * stride * 1.15, 0, 0]),
      // Plantar flex follows the stride so the sole doesn't skate.
      cycle("LeftFoot", duration, (p) => [-wave(p, 0.15) * 0.22, 0, 0]),
      cycle("RightFoot", duration, (p) => [-wave(p, 0.65) * 0.22, 0, 0]),

      // Arms opposite the legs (0.5 phase), −X forward.
      cycle("LeftArm", duration, (p) => [-wave(p, 0.5) * stride * 0.55, 0, 0.08]),
      cycle("RightArm", duration, (p) => [-wave(p) * stride * 0.55, 0, -0.08]),
      // Elbows: −X flexes the hand forward. Extra bend on the forward swing.
      cycle("LeftForeArm", duration, (p) => [-0.22 - Math.max(0, wave(p, 0.5)) * 0.32, 0, 0]),
      cycle("RightForeArm", duration, (p) => [-0.22 - Math.max(0, wave(p)) * 0.32, 0, 0]),
    ],
  };
}

/** Run cycle: longer stride, deeper forward lean, faster loop. */
export function buildRunClip(duration = 0.62): AnimationClip {
  const walk = buildWalkClip(duration, 0.95);
  return {
    ...walk,
    name: "Run",
    channels: walk.channels.map((channel) =>
      channel.bone === "Spine"
        ? {
            ...channel,
            keyframes: channel.keyframes.map((key) => ({
              ...key,
              // Deeper forward lean for the run.
              rotation: [0.18, key.rotation[1], key.rotation[2]] as [number, number, number],
            })),
          }
        : channel,
    ),
  };
}

/**
 * Quick-turn: the 180° spin from GDD §6.2. Body-only — the runtime rotates the
 * character's yaw itself, so this adds the lean and arm swing that sell it.
 */
export function buildQuickTurnClip(duration = 0.22): AnimationClip {
  const arc = (phase: number): number => Math.sin(phase * Math.PI);
  const keys = (fn: (phase: number) => [number, number, number]): Keyframe[] =>
    Array.from({ length: 5 }, (_, i) => {
      const phase = i / 4;
      return { time: phase * duration, rotation: fn(phase) };
    });

  return {
    name: "QuickTurn",
    duration,
    loop: false,
    channels: [
      { bone: "Spine", keyframes: keys((p) => [0, arc(p) * -0.4, 0]) },
      { bone: "Spine2", keyframes: keys((p) => [0, arc(p) * -0.3, 0]) },
      // −X is forward; arms brace into the spin.
      { bone: "LeftArm", keyframes: keys((p) => [arc(p) * -0.5, 0, 0.25]) },
      { bone: "RightArm", keyframes: keys((p) => [arc(p) * 0.35, 0, -0.25]) },
      { bone: "LeftUpLeg", keyframes: keys((p) => [arc(p) * -0.35, 0, 0]) },
      { bone: "RightUpLeg", keyframes: keys((p) => [arc(p) * 0.35, 0, 0]) },
    ],
  };
}

/**
 * Corpse / ambient body pose — not a locomotion clip.
 *
 * Weight settles into the hips, spine folds, head hangs, arms go limp. Used by
 * room NPCs so they don't stand in the breathing Idle like living party members.
 */
export function buildSlumpedClip(): AnimationClip {
  const hold = (bone: CanonicalBone, rotation: [number, number, number]): AnimationChannel => ({
    bone,
    keyframes: [
      { time: 0, rotation },
      { time: 0.001, rotation },
    ],
  });

  return {
    name: "Slumped",
    duration: 0.001,
    loop: false,
    channels: [
      // Collapse into a sit-lean against a wall: pelvis tucks, spine curls forward.
      hold("Hips", [0.12, 0.08, 0.04]),
      hold("Spine", [0.45, 0.18, 0.1]),
      hold("Spine1", [0.35, 0.12, 0.06]),
      hold("Spine2", [0.2, 0.08, 0.04]),
      hold("Neck", [0.55, 0.25, 0.12]),
      hold("Head", [0.4, 0.2, 0.08]),
      // Arms hang heavy — −X brings hands forward/down rather than locking behind.
      hold("LeftShoulder", [0.1, 0, 0.15]),
      hold("RightShoulder", [0.1, 0, -0.15]),
      hold("LeftArm", [0.35, 0.1, 0.55]),
      hold("RightArm", [0.55, -0.15, -0.65]),
      hold("LeftForeArm", [-0.85, 0.1, 0.05]),
      hold("RightForeArm", [-0.7, -0.1, -0.05]),
      hold("LeftHand", [0.1, 0, 0.15]),
      hold("RightHand", [0.15, 0, -0.2]),
      // Legs splay loosely; +X on the knee folds the shin back.
      hold("LeftUpLeg", [-0.35, 0.2, 0.12]),
      hold("RightUpLeg", [-0.25, -0.25, -0.15]),
      hold("LeftLeg", [0.85, 0, 0]),
      hold("RightLeg", [1.05, 0, 0]),
      hold("LeftFoot", [-0.2, 0.05, 0]),
      hold("RightFoot", [-0.15, -0.05, 0]),
    ],
  };
}

export function buildDefaultClips(): AnimationClip[] {
  return [
    buildIdleClip(),
    buildWalkClip(),
    buildRunClip(),
    buildQuickTurnClip(),
    buildSlumpedClip(),
  ];
}

/** Euler XYZ to quaternion, matching three.js's default order and glTF XYZW. */
export function eulerToQuaternion(
  x: number,
  y: number,
  z: number,
): [number, number, number, number] {
  const cx = Math.cos(x / 2);
  const sx = Math.sin(x / 2);
  const cy = Math.cos(y / 2);
  const sy = Math.sin(y / 2);
  const cz = Math.cos(z / 2);
  const sz = Math.sin(z / 2);

  return [
    sx * cy * cz + cx * sy * sz,
    cx * sy * cz - sx * cy * sz,
    cx * cy * sz + sx * sy * cz,
    cx * cy * cz - sx * sy * sz,
  ];
}
