/**
 * Procedural American-crow-scale actor with a readable rear flight silhouette.
 *
 * The rig remains code-only and cheap to clone, but the wing is no longer one
 * flat polygon. Shoulder, elbow and wrist joints carry overlapping covert,
 * secondary and primary feathers, so the renderer can describe a flap, glide,
 * bank, tuck or perch without replacing geometry.
 */
import * as THREE from "three";
import { mergeGeometries } from "three/examples/jsm/utils/BufferGeometryUtils.js";
import {
  actorMesh,
  disposeActor,
  namedGroup,
  requireGroup,
  type ActorRigBase,
} from "./common.ts";

export const CROW_METRICS = {
  bodyLength: 0.48,
  perchedHeight: 0.44,
  wingspan: 1.02,
} as const;

export type CrowPoseState = "flap" | "glide" | "bank" | "tuck" | "perch";

export interface CrowPose {
  state: CrowPoseState;
  /** Cyclic flap phase in radians. */
  phase?: number;
  /** Pose strength in [0, 1]. */
  amount?: number;
  /** Signed bank request in [-1, 1]; positive banks right. */
  bank?: number;
  /** Tucks the feet as this reaches one. */
  flight?: number;
}

export interface CrowMaterials {
  feather: THREE.Material;
  sheen: THREE.Material;
  beak: THREE.Material;
  eye: THREE.Material;
  foot: THREE.Material;
}

export interface CrowBuildOptions {
  materials?: CrowMaterials;
  featherColor?: THREE.ColorRepresentation;
  sheenColor?: THREE.ColorRepresentation;
}

export interface CrowJoints {
  body: THREE.Group;
  head: THREE.Group;
  /** Backwards-compatible aliases for the shoulder joints. */
  wingLeft: THREE.Group;
  wingRight: THREE.Group;
  shoulderLeft: THREE.Group;
  shoulderRight: THREE.Group;
  elbowLeft: THREE.Group;
  elbowRight: THREE.Group;
  wristLeft: THREE.Group;
  wristRight: THREE.Group;
  tail: THREE.Group;
  legLeft: THREE.Group;
  legRight: THREE.Group;
}

export interface CrowRig extends ActorRigBase {
  joints: CrowJoints;
}

export function createCrowMaterials(options: CrowBuildOptions = {}): CrowMaterials {
  return {
    // A blue-violet highlight keeps the bird readable against black terrain
    // without painting a naturally black animal grey.
    feather: new THREE.MeshStandardMaterial({
      name: "crow.feather",
      color: options.featherColor ?? 0x111821,
      roughness: 0.68,
      metalness: 0.08,
      emissive: 0x07111d,
      emissiveIntensity: 0.24,
      side: THREE.DoubleSide,
    }),
    sheen: new THREE.MeshPhysicalMaterial({
      name: "crow.sheen",
      color: options.sheenColor ?? 0x253547,
      roughness: 0.34,
      metalness: 0.22,
      clearcoat: 0.28,
      clearcoatRoughness: 0.42,
      emissive: 0x0b1729,
      emissiveIntensity: 0.32,
    }),
    beak: new THREE.MeshStandardMaterial({
      name: "crow.beak",
      color: 0x252a30,
      roughness: 0.72,
      metalness: 0.08,
    }),
    eye: new THREE.MeshBasicMaterial({ name: "crow.eye", color: 0xd9b86c, toneMapped: false }),
    foot: new THREE.MeshStandardMaterial({ name: "crow.foot", color: 0x2b2f35, roughness: 0.9 }),
  };
}

/** A small double-sided feather prism, rooted at z=0 and tapered along +Z. */
function featherGeometry(): THREE.BufferGeometry {
  const outline: readonly [number, number][] = [
    [-0.28, 0],
    [-0.5, 0.46],
    [-0.42, 0.76],
    [-0.18, 0.95],
    [0, 1],
    [0.18, 0.95],
    [0.42, 0.76],
    [0.5, 0.46],
    [0.28, 0],
  ];
  const halfThickness = 0.012;
  const positions: number[] = [];
  for (const y of [halfThickness, -halfThickness]) {
    for (const [x, z] of outline) positions.push(x, y, z);
  }
  const count = outline.length;
  const indices: number[] = [];
  // Top and bottom are triangle fans around the root-side midpoint.
  for (let index = 1; index < count - 1; index += 1) {
    indices.push(0, index, index + 1);
    indices.push(count, count + index + 1, count + index);
  }
  for (let index = 0; index < count; index += 1) {
    const next = (index + 1) % count;
    indices.push(index, count + index, next, next, count + index, count + next);
  }
  const geometry = new THREE.BufferGeometry();
  geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
  geometry.setIndex(indices);
  geometry.computeVertexNormals();
  geometry.computeBoundingBox();
  geometry.computeBoundingSphere();
  geometry.name = "crow.feather-blade";
  return geometry;
}

function beakGeometry(): THREE.BufferGeometry {
  const geometry = new THREE.BufferGeometry();
  geometry.setAttribute("position", new THREE.Float32BufferAttribute([
    -0.054, 0.025, 0,
    0.054, 0.025, 0,
    -0.043, -0.035, 0,
    0.043, -0.035, 0,
    0, -0.018, -0.19,
  ], 3));
  geometry.setIndex([
    0, 1, 4,
    1, 3, 4,
    3, 2, 4,
    2, 0, 4,
    0, 2, 3,
    0, 3, 1,
  ]);
  geometry.computeVertexNormals();
  geometry.name = "crow.beak-wedge";
  return geometry;
}

interface FeatherPlacement {
  position: readonly [number, number, number];
  scale: readonly [number, number, number];
  rotation?: readonly [number, number, number];
}

/** Merge one articulated feather layer into one draw call. */
function featherBatch(
  name: string,
  material: THREE.Material,
  placements: readonly FeatherPlacement[],
  blade: THREE.BufferGeometry,
): THREE.Mesh {
  const parts = placements.map((placement) => {
    const matrix = new THREE.Matrix4().compose(
      new THREE.Vector3(...placement.position),
      new THREE.Quaternion().setFromEuler(new THREE.Euler(...(placement.rotation ?? [0, 0, 0]), "XYZ")),
      new THREE.Vector3(...placement.scale),
    );
    return blade.clone().applyMatrix4(matrix);
  });
  const geometry = mergeGeometries(parts, false);
  for (const part of parts) part.dispose();
  if (!geometry) throw new Error(`crow: could not merge feather layer "${name}"`);
  geometry.name = `${name}.geometry`;
  geometry.computeBoundingBox();
  geometry.computeBoundingSphere();
  const mesh = actorMesh(name, geometry, material, { receiveShadow: false });
  mesh.userData.featherCount = placements.length;
  return mesh;
}

function addWing(
  body: THREE.Group,
  side: -1 | 1,
  materials: CrowMaterials,
  featherBlade: THREE.BufferGeometry,
): { shoulder: THREE.Group; elbow: THREE.Group; wrist: THREE.Group } {
  const word = side < 0 ? "left" : "right";
  const shoulder = namedGroup(`crow.wing.${word}`, [side * 0.09, 0.055, -0.035]);
  const elbow = namedGroup(`crow.elbow.${word}`, [side * 0.175, 0, 0.012]);
  const wrist = namedGroup(`crow.wrist.${word}`, [side * 0.165, -0.004, 0.045]);
  shoulder.add(elbow);
  elbow.add(wrist);
  body.add(shoulder);

  // Rounded scapular coverts bridge the torso to the articulated arm.
  const coverts: FeatherPlacement[] = [];
  for (let index = 0; index < 4; index += 1) {
    coverts.push({
      position: [side * (0.025 + index * 0.035), 0.018 - index * 0.004, -0.035 + index * 0.018],
      scale: [0.09, 0.72, 0.18 + index * 0.012],
      rotation: [0.06, side * (-0.05 - index * 0.035), 0],
    });
  }
  shoulder.add(featherBatch(`crow.coverts.${word}`, materials.sheen, coverts, featherBlade));

  // The secondaries overlap from elbow to wrist and create the broad inner
  // trailing edge visible in the accepted rear-view direction.
  const innerSecondaries: FeatherPlacement[] = [];
  const outerSecondaries: FeatherPlacement[] = [];
  for (let index = 0; index < 7; index += 1) {
    const along = index / 6;
    (index < 3 ? innerSecondaries : outerSecondaries).push({
      position: [side * (0.015 + along * 0.155), -0.006 - along * 0.005, 0.018 + along * 0.014],
      scale: [0.095 - along * 0.014, 0.72, 0.26 + along * 0.055],
      rotation: [0.02 + along * 0.05, side * (-0.1 - along * 0.16), 0],
    });
  }
  elbow.add(
    featherBatch(`crow.secondaries-inner.${word}`, materials.sheen, innerSecondaries, featherBlade),
    featherBatch(`crow.secondaries-outer.${word}`, materials.feather, outerSecondaries, featherBlade),
  );

  // Long, individually separated primaries fan from the wrist. Their small
  // angular and length progression gives a clear fingertip silhouette.
  const primaries: FeatherPlacement[] = [];
  for (let index = 0; index < 9; index += 1) {
    const along = index / 8;
    primaries.push({
      position: [side * (0.01 + along * 0.095), -0.014 - along * 0.003, 0.025 + along * 0.01],
      scale: [0.074 - along * 0.014, 0.78, 0.34 - along * 0.075],
      rotation: [0.015 + along * 0.035, side * (-0.2 - along * 0.42), side * (along - 0.5) * 0.035],
    });
  }
  wrist.add(featherBatch(`crow.primaries.${word}`, materials.feather, primaries, featherBlade));
  return { shoulder, elbow, wrist };
}

function addFoot(body: THREE.Group, side: -1 | 1, material: THREE.Material): THREE.Group {
  const word = side < 0 ? "left" : "right";
  const leg = namedGroup(`crow.leg.${word}`, [side * 0.052, -0.115, 0.015]);
  leg.add(actorMesh(
    `crow.shank.${word}`,
    new THREE.CylinderGeometry(0.009, 0.007, 0.12, 7),
    material,
    { position: [0, -0.052, 0.015], rotation: [0.2, 0, 0] },
  ));
  const foot = namedGroup(`crow.foot.${word}`, [0, -0.108, -0.002]);
  leg.add(foot);
  for (let index = 0; index < 3; index += 1) {
    const angle = (index - 1) * 0.42;
    foot.add(actorMesh(
      `crow.toe.${word}.${index}`,
      new THREE.CylinderGeometry(0.0045, 0.003, 0.075, 6),
      material,
      {
        position: [Math.sin(angle) * 0.025, -0.004, -0.028 - Math.cos(angle) * 0.006],
        rotation: [Math.PI / 2, 0, -angle],
      },
    ));
  }
  foot.add(actorMesh(
    `crow.toe.${word}.rear`,
    new THREE.CylinderGeometry(0.004, 0.003, 0.055, 6),
    material,
    { position: [0, -0.005, 0.022], rotation: [Math.PI / 2, 0, 0] },
  ));
  body.add(leg);
  return leg;
}

function resolveCrow(root: THREE.Group, ownsMaterials: boolean): CrowRig {
  const wingLeft = requireGroup(root, "crow.wing.left");
  const wingRight = requireGroup(root, "crow.wing.right");
  return {
    root,
    ownsMaterials,
    joints: {
      body: requireGroup(root, "crow.body"),
      head: requireGroup(root, "crow.head"),
      wingLeft,
      wingRight,
      shoulderLeft: wingLeft,
      shoulderRight: wingRight,
      elbowLeft: requireGroup(root, "crow.elbow.left"),
      elbowRight: requireGroup(root, "crow.elbow.right"),
      wristLeft: requireGroup(root, "crow.wrist.left"),
      wristRight: requireGroup(root, "crow.wrist.right"),
      tail: requireGroup(root, "crow.tail"),
      legLeft: requireGroup(root, "crow.leg.left"),
      legRight: requireGroup(root, "crow.leg.right"),
    },
  };
}

export function buildCrow(options: CrowBuildOptions = {}): CrowRig {
  const materials = options.materials ?? createCrowMaterials(options);
  const featherBlade = featherGeometry();
  const root = namedGroup("crow");
  root.userData.kind = "actor";
  root.userData.actorType = "anonymous-crow";
  root.userData.forwardAxis = "-Z";
  root.userData.rigVersion = 2;

  const body = namedGroup("crow.body", [0, 0.225, 0]);
  root.add(body);
  body.add(
    actorMesh("crow.torso", new THREE.SphereGeometry(0.14, 18, 12), materials.feather, {
      rotation: [-0.12, 0, 0],
      scale: [0.82, 0.94, 1.58],
    }),
    actorMesh("crow.breast", new THREE.SphereGeometry(0.112, 16, 10), materials.sheen, {
      position: [0, -0.006, -0.112],
      rotation: [-0.18, 0, 0],
      scale: [0.76, 1.03, 0.72],
    }),
    actorMesh("crow.mantle", new THREE.SphereGeometry(0.116, 16, 10), materials.sheen, {
      position: [0, 0.057, 0.028],
      scale: [0.82, 0.62, 1.12],
    }),
    actorMesh("crow.nape", new THREE.SphereGeometry(0.096, 16, 10), materials.sheen, {
      position: [0, 0.086, -0.088],
      rotation: [-0.18, 0, 0],
      scale: [0.86, 0.74, 1.2],
    }),
  );

  // Small overlapping mantle feathers break up the old featureless pawn back.
  const crownMantle: FeatherPlacement[] = [];
  const lowerMantle: FeatherPlacement[] = [];
  for (let row = 0; row < 3; row += 1) {
    for (let column = -1; column <= 1; column += 1) {
      (row === 0 ? crownMantle : lowerMantle).push({
        position: [column * (0.036 - row * 0.004), 0.105 - row * 0.026, 0.02 + row * 0.045],
        scale: [0.07 - row * 0.006, 0.65, 0.1 + row * 0.014],
        rotation: [0.28, column * -0.08, 0],
      });
    }
  }
  body.add(
    featherBatch("crow.mantle-feathers.crown", materials.sheen, crownMantle, featherBlade),
    featherBatch("crow.mantle-feathers.lower", materials.feather, lowerMantle, featherBlade),
  );

  const head = namedGroup("crow.head", [0, 0.108, -0.15]);
  body.add(head);
  head.add(
    actorMesh("crow.skull", new THREE.SphereGeometry(0.098, 18, 12), materials.feather, {
      scale: [0.88, 0.84, 1.14],
    }),
    actorMesh("crow.crown-sheen", new THREE.SphereGeometry(0.09, 16, 10, 0, Math.PI * 2, 0, Math.PI * 0.52), materials.sheen, {
      position: [0, 0.012, -0.003],
      scale: [0.92, 0.62, 0.96],
    }),
    actorMesh("crow.beak", beakGeometry(), materials.beak, { position: [0, -0.018, -0.073] }),
  );
  for (const side of [-1, 1] as const) {
    const word = side < 0 ? "left" : "right";
    head.add(
      actorMesh(`crow.eye-rim.${word}`, new THREE.SphereGeometry(0.016, 8, 6), materials.beak, {
        position: [side * 0.068, 0.022, -0.058],
        scale: [0.55, 1, 0.82],
      }),
      actorMesh(`crow.eye.${word}`, new THREE.SphereGeometry(0.008, 8, 6), materials.eye, {
        position: [side * 0.076, 0.023, -0.063],
      }),
    );
  }

  addWing(body, -1, materials, featherBlade);
  addWing(body, 1, materials, featherBlade);

  const tail = namedGroup("crow.tail", [0, -0.025, 0.132]);
  body.add(tail);
  const centralTail: FeatherPlacement[] = [];
  const outerTail: FeatherPlacement[] = [];
  for (let index = 0; index < 9; index += 1) {
    const fan = (index - 4) / 4;
    (Math.abs(fan) < 0.5 ? centralTail : outerTail).push({
      position: [fan * 0.052, -Math.abs(fan) * 0.004, 0],
      scale: [0.082, 0.8, 0.285 - Math.abs(fan) * 0.035],
      rotation: [0.04, fan * -0.44, fan * -0.045],
    });
  }
  tail.add(
    featherBatch("crow.tail-feathers.central", materials.sheen, centralTail, featherBlade),
    featherBatch("crow.tail-feathers.outer", materials.feather, outerTail, featherBlade),
  );

  addFoot(body, -1, materials.foot);
  addFoot(body, 1, materials.foot);
  featherBlade.dispose();

  const rig = resolveCrow(root, options.materials === undefined);
  poseCrow(rig, { state: "perch", amount: 1, flight: 0 });
  return rig;
}

export function cloneCrow(source: CrowRig): CrowRig {
  return resolveCrow(source.root.clone(true), false);
}

function poseWing(
  shoulder: THREE.Group,
  elbow: THREE.Group,
  wrist: THREE.Group,
  side: -1 | 1,
  pose: Required<CrowPose>,
): void {
  const stroke = Math.sin(pose.phase);
  const bankLift = pose.bank * side;
  if (pose.state === "perch") {
    elbow.position.x = side * 0.035;
    wrist.position.x = side * 0.018;
    shoulder.rotation.set(0.08, 0, side * 0.06);
    elbow.rotation.set(0.08, 0, side * -0.1);
    wrist.rotation.set(-0.06, 0, side * -0.06);
    return;
  }
  if (pose.state === "tuck") {
    elbow.position.x = side * 0.065;
    wrist.position.x = side * 0.035;
    shoulder.rotation.set(-0.05, 0, side * 0.16);
    elbow.rotation.set(0.08, 0, side * -0.3);
    wrist.rotation.set(-0.08, 0, side * -0.2);
    return;
  }

  elbow.position.x = side * 0.175;
  wrist.position.x = side * 0.165;
  const flap = pose.state === "flap" ? stroke * 0.72 * pose.amount : 0;
  const glideDihedral = pose.state === "glide" || pose.state === "bank" ? 0.09 : 0.15;
  shoulder.rotation.set(-0.06 - Math.max(0, stroke) * 0.05, side * -0.08, side * (glideDihedral + flap + bankLift * 0.2));
  elbow.rotation.set(0.025, side * (-0.1 - Math.max(0, -stroke) * 0.1), side * (-0.05 + flap * 0.22 + bankLift * 0.12));
  wrist.rotation.set(-0.025, side * (-0.16 - pose.amount * 0.06), side * (-0.035 + flap * 0.12 + bankLift * 0.08));
}

/** Apply an explicit production pose while preserving the legacy rig contract. */
export function poseCrow(rig: CrowRig, value: CrowPose): void {
  const pose: Required<CrowPose> = {
    state: value.state,
    phase: Number.isFinite(value.phase) ? value.phase ?? 0 : 0,
    amount: THREE.MathUtils.clamp(Number.isFinite(value.amount) ? value.amount ?? 1 : 1, 0, 1),
    bank: THREE.MathUtils.clamp(Number.isFinite(value.bank) ? value.bank ?? 0 : 0, -1, 1),
    flight: THREE.MathUtils.clamp(Number.isFinite(value.flight) ? value.flight ?? 1 : 1, 0, 1),
  };
  poseWing(rig.joints.shoulderLeft, rig.joints.elbowLeft, rig.joints.wristLeft, -1, pose);
  poseWing(rig.joints.shoulderRight, rig.joints.elbowRight, rig.joints.wristRight, 1, pose);

  const flapBob = pose.state === "flap" ? Math.cos(pose.phase) * 0.045 * pose.amount : 0;
  rig.joints.body.rotation.set(flapBob, 0, -pose.bank * 0.12);
  rig.joints.head.rotation.set(-flapBob * 0.6, pose.bank * -0.08, pose.bank * 0.08);
  rig.joints.tail.rotation.set(
    pose.state === "tuck" ? -0.24 : pose.state === "perch" ? 0.18 : -0.03 - flapBob,
    0,
    -pose.bank * 0.16,
  );
  const tailSpread = pose.state === "perch"
    ? 0.78
    : pose.state === "tuck"
      ? 0.62
      : pose.state === "flap"
        ? 1.2
        : 1.48 + Math.abs(pose.bank) * 0.18;
  rig.joints.tail.scale.x = tailSpread;

  const legTuck = pose.state === "perch" ? 0 : 0.22 + pose.flight * 0.62;
  rig.joints.legLeft.rotation.x = legTuck;
  rig.joints.legRight.rotation.x = legTuck;
  rig.joints.legLeft.rotation.z = pose.state === "perch" ? -0.035 : 0.08;
  rig.joints.legRight.rotation.z = pose.state === "perch" ? 0.035 : -0.08;
  rig.root.userData.pose = pose.state;
}

/**
 * Backwards-compatible flap helper. New adapters should prefer `poseCrow` so a
 * glide, bank, tuck and perch is semantic rather than inferred from amplitude.
 */
export function poseCrowFlight(rig: CrowRig, phase: number, amount = 1): void {
  poseCrow(rig, { state: amount <= 0.25 ? "glide" : "flap", phase, amount, flight: 1 });
}

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