/**
 * A metre-scale, original procedural dog built for readable third-person motion.
 *
 * The hierarchy intentionally separates shoulder/hip, upper leg, lower leg and
 * paw. Presentation can therefore distinguish a planted walk, diagonal trot and
 * gathered run without changing renderer-independent actor or replay state.
 */
import * as THREE from "three";
import {
  actorMesh,
  disposeActor,
  namedGroup,
  requireGroup,
  type ActorRigBase,
} from "./common.ts";

export const DOG_METRICS = {
  length: 1.25,
  shoulderHeight: 0.54,
  height: 0.94,
} as const;

export type DogPoseState = "idle" | "attention" | "walk" | "trot" | "run";

/** Renderer-only values. All numbers are clamped and the pose is deterministic. */
export interface DogPose {
  state: DogPoseState;
  phase?: number;
  /** Overall pose intensity, from still (0) to full expression (1). */
  amount?: number;
  /** Signed turn/lean request, -1 left to 1 right. */
  turn?: number;
  /** Signed start/stop impulse, -1 braking to 1 accelerating. */
  acceleration?: number;
  attentionYaw?: number;
  attentionPitch?: number;
}

export interface DogMaterials {
  coat: THREE.Material;
  markings: THREE.Material;
  nose: THREE.Material;
  collar: THREE.Material;
}

export interface DogBuildOptions {
  materials?: DogMaterials;
  coatColor?: THREE.ColorRepresentation;
  markingsColor?: THREE.ColorRepresentation;
  collarColor?: THREE.ColorRepresentation;
}

export interface DogLegJoints {
  upper: THREE.Group;
  lower: THREE.Group;
  paw: THREE.Group;
}

export interface DogJoints {
  body: THREE.Group;
  chest: THREE.Group;
  neck: THREE.Group;
  head: THREE.Group;
  earLeft: THREE.Group;
  earRight: THREE.Group;
  tail: THREE.Group;
  tailTip: THREE.Group;
  frontLeft: DogLegJoints;
  frontRight: DogLegJoints;
  rearLeft: DogLegJoints;
  rearRight: DogLegJoints;
  /** Backwards-compatible aliases for the four original one-piece leg joints. */
  legFrontLeft: THREE.Group;
  legFrontRight: THREE.Group;
  legRearLeft: THREE.Group;
  legRearRight: THREE.Group;
}

export interface DogRig extends ActorRigBase {
  joints: DogJoints;
}

export function createDogMaterials(options: DogBuildOptions = {}): DogMaterials {
  return {
    coat: new THREE.MeshStandardMaterial({
      name: "dog.coat",
      color: options.coatColor ?? 0x282b2d,
      roughness: 0.84,
      metalness: 0.02,
    }),
    markings: new THREE.MeshStandardMaterial({
      name: "dog.markings",
      color: options.markingsColor ?? 0xc7ad88,
      roughness: 0.92,
    }),
    nose: new THREE.MeshStandardMaterial({ name: "dog.nose", color: 0x0b0d0e, roughness: 0.62 }),
    collar: new THREE.MeshStandardMaterial({
      name: "dog.collar",
      color: options.collarColor ?? 0xd69a35,
      roughness: 0.48,
      metalness: 0.08,
    }),
  };
}

function leg(root: THREE.Group, name: string): DogLegJoints {
  return {
    upper: requireGroup(root, `dog.leg.${name}.upper`),
    lower: requireGroup(root, `dog.leg.${name}.lower`),
    paw: requireGroup(root, `dog.leg.${name}.paw`),
  };
}

function resolveDog(root: THREE.Group, ownsMaterials: boolean): DogRig {
  const frontLeft = leg(root, "front.left");
  const frontRight = leg(root, "front.right");
  const rearLeft = leg(root, "rear.left");
  const rearRight = leg(root, "rear.right");
  return {
    root,
    ownsMaterials,
    joints: {
      body: requireGroup(root, "dog.body"),
      chest: requireGroup(root, "dog.chest"),
      neck: requireGroup(root, "dog.neck"),
      head: requireGroup(root, "dog.head"),
      earLeft: requireGroup(root, "dog.ear.left"),
      earRight: requireGroup(root, "dog.ear.right"),
      tail: requireGroup(root, "dog.tail"),
      tailTip: requireGroup(root, "dog.tail.tip"),
      frontLeft,
      frontRight,
      rearLeft,
      rearRight,
      legFrontLeft: frontLeft.upper,
      legFrontRight: frontRight.upper,
      legRearLeft: rearLeft.upper,
      legRearRight: rearRight.upper,
    },
  };
}

function addLeg(
  body: THREE.Group,
  m: DogMaterials,
  fore: "front" | "rear",
  side: "left" | "right",
  x: number,
  z: number,
): void {
  const name = `${fore}.${side}`;
  const upperLength = fore === "front" ? 0.16 : 0.17;
  const lowerLength = fore === "front" ? 0.18 : 0.17;
  const upper = namedGroup(`dog.leg.${name}.upper`, [x, fore === "front" ? -0.055 : -0.035, z]);
  body.add(upper);
  upper.add(actorMesh(`dog.leg.${name}.upper-mesh`, new THREE.CapsuleGeometry(0.047, upperLength - 0.08, 3, 7), m.coat, {
    position: [0, -upperLength * 0.5, 0],
    scale: fore === "front" ? [0.9, 1, 0.9] : [1.15, 1.04, 1.08],
  }));

  const lower = namedGroup(`dog.leg.${name}.lower`, [0, -upperLength, 0]);
  upper.add(lower);
  lower.add(actorMesh(`dog.leg.${name}.lower-mesh`, new THREE.CapsuleGeometry(0.039, lowerLength - 0.07, 3, 7), m.coat, {
    position: [0, -lowerLength * 0.5, 0],
    scale: fore === "front" ? [0.9, 1, 0.88] : [1, 1, 0.9],
  }));

  const paw = namedGroup(`dog.leg.${name}.paw`, [0, -lowerLength, 0]);
  lower.add(paw);
  paw.add(actorMesh(`dog.paw.${name}`, new THREE.SphereGeometry(0.058, 8, 6), m.markings, {
    position: [0, -0.018, -0.026],
    scale: [0.88, 0.46, 1.38],
  }));
}

export function buildDog(options: DogBuildOptions = {}): DogRig {
  const m = options.materials ?? createDogMaterials(options);
  const root = namedGroup("dog");
  root.userData.kind = "actor";
  root.userData.actorType = "anonymous-dog";
  root.userData.forwardAxis = "-Z";
  root.userData.rigVersion = 2;
  root.userData.pose = "idle" satisfies DogPoseState;

  const body = namedGroup("dog.body", [0, 0.425, 0.045]);
  root.add(body);
  body.add(
    actorMesh("dog.torso", new THREE.CapsuleGeometry(0.205, 0.43, 5, 11), m.coat, {
      rotation: [Math.PI / 2, 0, 0],
      scale: [0.82, 1, 0.9],
    }),
    actorMesh("dog.loin", new THREE.SphereGeometry(0.19, 11, 8), m.coat, {
      position: [0, -0.025, 0.245],
      scale: [0.78, 0.8, 1.05],
    }),
    actorMesh("dog.shoulder-mass", new THREE.SphereGeometry(0.205, 11, 8), m.coat, {
      position: [0, 0.015, -0.205],
      scale: [0.92, 1.12, 0.78],
    }),
    actorMesh("dog.hip-mass", new THREE.SphereGeometry(0.195, 11, 8), m.coat, {
      position: [0, 0.01, 0.255],
      scale: [0.9, 1.04, 0.82],
    }),
  );

  const chest = namedGroup("dog.chest", [0, 0.01, -0.225]);
  body.add(chest);
  chest.add(actorMesh("dog.chest-marking", new THREE.SphereGeometry(0.175, 11, 8), m.markings, {
    position: [0, -0.035, -0.045],
    scale: [0.62, 1.04, 0.52],
  }));

  const neck = namedGroup("dog.neck", [0, 0.105, -0.31]);
  body.add(neck);
  neck.rotation.x = -0.24;
  neck.add(
    actorMesh("dog.neck-mesh", new THREE.CapsuleGeometry(0.125, 0.13, 4, 9), m.coat, {
      position: [0, 0.085, -0.018],
      scale: [0.92, 1, 0.92],
    }),
    actorMesh("dog.collar", new THREE.TorusGeometry(0.128, 0.014, 6, 18), m.collar, {
      position: [0, 0.035, -0.005],
      rotation: [Math.PI / 2, 0, 0],
      scale: [1, 0.9, 1],
    }),
    actorMesh("dog.tag", new THREE.OctahedronGeometry(0.032, 0), m.collar, {
      position: [0, -0.005, -0.135],
      rotation: [0.2, 0, Math.PI / 4],
    }),
  );

  const head = namedGroup("dog.head", [0, 0.17, -0.045]);
  neck.add(head);
  head.add(
    actorMesh("dog.skull", new THREE.SphereGeometry(0.155, 12, 9), m.coat, {
      scale: [0.8, 0.94, 0.92],
    }),
    actorMesh("dog.brow", new THREE.SphereGeometry(0.12, 10, 7), m.coat, {
      position: [0, 0.035, -0.1],
      scale: [0.9, 0.55, 0.72],
    }),
    actorMesh("dog.muzzle", new THREE.SphereGeometry(0.105, 11, 8), m.markings, {
      position: [0, -0.05, -0.145],
      scale: [0.8, 0.62, 1.15],
    }),
    actorMesh("dog.nose", new THREE.SphereGeometry(0.044, 9, 6), m.nose, {
      position: [0, -0.035, -0.247],
      scale: [1.18, 0.72, 0.72],
    }),
  );
  for (const side of [-1, 1] as const) {
    const word = side < 0 ? "left" : "right";
    head.add(
      actorMesh(`dog.eye.${word}`, new THREE.SphereGeometry(0.022, 8, 6), m.nose, {
        position: [side * 0.073, 0.025, -0.128],
        scale: [0.82, 1, 0.62],
        receiveShadow: false,
      }),
      actorMesh(`dog.eye-catchlight.${word}`, new THREE.SphereGeometry(0.0055, 6, 4), m.markings, {
        position: [side * 0.076, 0.031, -0.145],
        receiveShadow: false,
      }),
    );
    const ear = namedGroup(`dog.ear.${word}`, [side * 0.105, 0.095, -0.005]);
    head.add(ear);
    ear.add(actorMesh(`dog.ear-flap.${word}`, new THREE.ConeGeometry(0.073, 0.19, 5), m.coat, {
      position: [side * 0.012, 0.06, 0.015],
      rotation: [0.08, 0, side * -0.18],
      scale: [0.88, 1, 0.72],
    }));
  }

  addLeg(body, m, "front", "left", -0.14, -0.22);
  addLeg(body, m, "front", "right", 0.14, -0.22);
  addLeg(body, m, "rear", "left", -0.145, 0.245);
  addLeg(body, m, "rear", "right", 0.145, 0.245);

  const tail = namedGroup("dog.tail", [0, 0.065, 0.37]);
  body.add(tail);
  tail.add(actorMesh("dog.tail.base-mesh", new THREE.CapsuleGeometry(0.046, 0.17, 4, 8), m.coat, {
    position: [0, 0.115, 0],
    scale: [1.05, 1, 1.05],
  }));
  const tailTip = namedGroup("dog.tail.tip", [0, 0.21, 0]);
  tail.add(tailTip);
  tailTip.add(actorMesh("dog.tail.tip-mesh", new THREE.CapsuleGeometry(0.034, 0.15, 4, 8), m.coat, {
    position: [0, 0.095, 0],
    scale: [0.9, 1, 0.9],
  }));

  const rig = resolveDog(root, !options.materials);
  poseDog(rig, { state: "idle", phase: 0, amount: 1 });
  return rig;
}

export function cloneDog(source: DogRig): DogRig {
  return resolveDog(source.root.clone(true), false);
}

/** A consistent state choice for local and network presentation adapters. */
export function dogPoseStateForSpeed(speedMps: number): DogPoseState {
  const speed = Number.isFinite(speedMps) ? Math.max(0, speedMps) : 0;
  if (speed < 0.08) return "idle";
  if (speed < 1.15) return "walk";
  if (speed < 2.65) return "trot";
  return "run";
}

function finite(value: number | undefined, fallback = 0): number {
  return value === undefined || !Number.isFinite(value) ? fallback : value;
}

function setLeg(leg: DogLegJoints, upper: number, bend: number, toeLift: number): void {
  leg.upper.rotation.set(upper, 0, 0);
  leg.lower.rotation.set(bend, 0, 0);
  leg.paw.rotation.set(-(upper + bend) * 0.64 - toeLift, 0, 0);
}

/** Apply one complete pose. Repeated identical inputs produce identical transforms. */
export function poseDog(rig: DogRig, pose: DogPose): void {
  const state: DogPoseState = ["idle", "attention", "walk", "trot", "run"].includes(pose.state)
    ? pose.state
    : "idle";
  const phase = finite(pose.phase);
  const amount = THREE.MathUtils.clamp(finite(pose.amount, 1), 0, 1);
  const turn = THREE.MathUtils.clamp(finite(pose.turn), -1, 1);
  const acceleration = THREE.MathUtils.clamp(finite(pose.acceleration), -1, 1);
  const lookYaw = THREE.MathUtils.clamp(finite(pose.attentionYaw), -0.9, 0.9);
  const lookPitch = THREE.MathUtils.clamp(finite(pose.attentionPitch), -0.45, 0.4);
  const locomoting = state === "walk" || state === "trot" || state === "run";
  const breath = Math.sin(phase * 0.48);
  const cycle = Math.sin(phase);
  const opposite = Math.sin(phase + Math.PI);
  const liftLeft = locomoting ? Math.max(0, Math.sin(phase + Math.PI * 0.18)) : 0;
  const liftRight = locomoting ? Math.max(0, Math.sin(phase + Math.PI * 1.18)) : 0;

  let stride = 0;
  let bend = 0;
  let bob = breath * 0.003;
  if (state === "walk") {
    stride = 0.38 * amount;
    bend = 0.3 * amount;
    bob += Math.abs(Math.cos(phase)) * 0.006 * amount;
  } else if (state === "trot") {
    stride = 0.58 * amount;
    bend = 0.46 * amount;
    bob += Math.abs(Math.cos(phase)) * 0.015 * amount;
  } else if (state === "run") {
    stride = 0.76 * amount;
    bend = 0.66 * amount;
    bob += Math.sin(phase * 2) * 0.018 * amount;
  }

  if (state === "run") {
    const frontLeft = Math.sin(phase + 0.18);
    const frontRight = Math.sin(phase - 0.18);
    const rearLeft = Math.sin(phase + Math.PI + 0.2);
    const rearRight = Math.sin(phase + Math.PI - 0.2);
    setLeg(rig.joints.frontLeft, frontLeft * stride, -Math.max(0, -frontLeft) * bend, liftLeft * 0.16);
    setLeg(rig.joints.frontRight, frontRight * stride, -Math.max(0, -frontRight) * bend, liftRight * 0.16);
    setLeg(rig.joints.rearLeft, rearLeft * stride, Math.max(0, rearLeft) * bend, liftRight * 0.13);
    setLeg(rig.joints.rearRight, rearRight * stride, Math.max(0, rearRight) * bend, liftLeft * 0.13);
  } else {
    setLeg(rig.joints.frontLeft, cycle * stride, -liftLeft * bend, liftLeft * 0.1);
    setLeg(rig.joints.frontRight, opposite * stride, -liftRight * bend, liftRight * 0.1);
    setLeg(rig.joints.rearLeft, opposite * stride, liftRight * bend * 0.84, liftRight * 0.08);
    setLeg(rig.joints.rearRight, cycle * stride, liftLeft * bend * 0.84, liftLeft * 0.08);
  }

  const locomotionAmount = locomoting ? amount : 0;
  rig.joints.body.position.set(0, 0.425 + bob, 0.045);
  rig.joints.body.rotation.set(-acceleration * 0.09 + (state === "run" ? -0.035 : 0), 0, -turn * 0.12 * locomotionAmount);
  rig.joints.chest.scale.set(1, 1 + breath * (locomoting ? 0.012 : 0.026), 1);
  rig.joints.neck.rotation.set(-0.24 + acceleration * 0.035, turn * 0.035, turn * 0.04);
  rig.joints.head.rotation.set(
    -0.055 + lookPitch - Math.abs(turn) * 0.035 + (state === "attention" ? -0.08 : 0),
    lookYaw - turn * 0.08,
    turn * 0.045,
  );

  const attention = state === "attention" ? 1 : 0;
  rig.joints.earLeft.rotation.set(0.02 + attention * 0.08, 0, 0.07 + attention * 0.08);
  rig.joints.earRight.rotation.set(-0.01, 0, -0.07 + attention * 0.035);
  const wag = Math.sin(phase * (locomoting ? 0.85 : 1.3)) * (locomoting ? 0.16 : 0.38) * amount;
  rig.joints.tail.rotation.set(0.72 - locomotionAmount * 0.12, 0, wag - turn * 0.12);
  rig.joints.tailTip.rotation.set(-0.16, 0, wag * 0.7);

  rig.root.userData.pose = state;
  rig.root.userData.posePhase = phase;
  rig.root.userData.poseAmount = amount;
  rig.root.userData.turnLean = turn;
  rig.root.userData.acceleration = acceleration;
}

/** Legacy gait API retained for existing integrations and downstream users. */
export function poseDogWalk(rig: DogRig, phase: number, amount = 0.55): void {
  const intensity = THREE.MathUtils.clamp(finite(amount), 0, 1);
  poseDog(rig, {
    state: intensity < 0.04 ? "idle" : intensity < 0.52 ? "walk" : "trot",
    phase,
    amount: intensity,
  });
}

/** Legacy additive attention API: deliberately leaves the active gait untouched. */
export function poseDogAttention(rig: DogRig, lookYaw: number, tailPhase: number): void {
  const yaw = THREE.MathUtils.clamp(finite(lookYaw), -0.9, 0.9);
  const phase = finite(tailPhase);
  rig.joints.head.rotation.y = yaw;
  rig.joints.head.rotation.x = -0.08;
  rig.joints.earLeft.rotation.z = 0.13;
  rig.joints.earRight.rotation.z = -0.105;
  rig.joints.tail.rotation.z = Math.sin(phase) * 0.34;
  rig.joints.tailTip.rotation.z = Math.sin(phase + 0.35) * 0.22;
}

export function disposeDog(rig: DogRig, options: { disposeMaterials?: boolean } = {}): void {
  disposeActor(rig, options);
}
