/**
 * A friendly, original humanoid avatar built entirely from Three.js primitives.
 *
 * Dimensions are metres, the origin is between the soles, and -Z is forward.
 * The face is a slightly curved dark display with an optional caller-owned
 * texture. A deployment can build one prototype per appearance/webcam stream,
 * then cheaply clone that prototype for repeated views: clones share immutable
 * geometry and materials while all animation joints remain independent.
 */
import * as THREE from "three";
import {
  actorMesh,
  disposeActor,
  namedGroup,
  requireGroup,
  type ActorRigBase,
} from "./common.ts";

export const HUMANOID_METRICS = {
  height: 1.76,
  shoulderWidth: 0.46,
  hipY: 0.94,
  eyeY: 1.65,
} as const;

export interface HumanoidMaterials {
  skin: THREE.Material;
  outfit: THREE.Material;
  accent: THREE.Material;
  sole: THREE.Material;
  hair: THREE.Material;
  face: THREE.Material;
}

export interface HumanoidBuildOptions {
  materials?: HumanoidMaterials;
  skinTone?: THREE.ColorRepresentation;
  outfitColor?: THREE.ColorRepresentation;
  accentColor?: THREE.ColorRepresentation;
  hairColor?: THREE.ColorRepresentation;
  /** Caller-owned and never disposed by this asset. */
  faceTexture?: THREE.Texture;
  bodyShape?: "slim" | "average" | "broad";
}

export interface HumanoidJoints {
  pelvis: THREE.Group;
  torso: THREE.Group;
  head: THREE.Group;
  shoulderLeft: THREE.Group;
  shoulderRight: THREE.Group;
  elbowLeft: THREE.Group;
  elbowRight: THREE.Group;
  hipLeft: THREE.Group;
  hipRight: THREE.Group;
  kneeLeft: THREE.Group;
  kneeRight: THREE.Group;
}

export interface HumanoidRig extends ActorRigBase {
  joints: HumanoidJoints;
  /** Front-facing surface for a webcam or generated profile face. */
  face: THREE.Mesh;
}

export interface HumanoidPose {
  walkPhase?: number;
  stride?: number;
  armSwing?: number;
  headYaw?: number;
  headPitch?: number;
}

export function createHumanoidMaterials(options: HumanoidBuildOptions = {}): HumanoidMaterials {
  return {
    skin: new THREE.MeshStandardMaterial({
      name: "humanoid.skin",
      color: options.skinTone ?? 0x9b6246,
      roughness: 0.68,
    }),
    outfit: new THREE.MeshStandardMaterial({
      name: "humanoid.outfit",
      color: options.outfitColor ?? 0x26364b,
      roughness: 0.72,
    }),
    accent: new THREE.MeshStandardMaterial({
      name: "humanoid.accent",
      color: options.accentColor ?? 0x4fa9c8,
      roughness: 0.55,
    }),
    sole: new THREE.MeshStandardMaterial({
      name: "humanoid.sole",
      color: 0x181b1e,
      roughness: 0.9,
    }),
    hair: new THREE.MeshStandardMaterial({
      name: "humanoid.hair",
      color: options.hairColor ?? 0x241b18,
      roughness: 0.86,
    }),
    face: new THREE.MeshBasicMaterial({
      name: "humanoid.face",
      color: options.faceTexture ? 0xffffff : 0x18242b,
      map: options.faceTexture ?? null,
      toneMapped: false,
    }),
  };
}

function limb(
  joint: THREE.Group,
  name: string,
  material: THREE.Material,
  radius: number,
  length: number,
): void {
  joint.add(
    actorMesh(name, new THREE.CapsuleGeometry(radius, Math.max(0.01, length - radius * 2), 4, 8), material, {
      position: [0, -length / 2, 0],
    }),
  );
}

function resolveHumanoid(root: THREE.Group, ownsMaterials: boolean): HumanoidRig {
  const face = root.getObjectByName("humanoid.face");
  if (!(face instanceof THREE.Mesh)) throw new Error("humanoid: missing face surface");
  return {
    root,
    ownsMaterials,
    face,
    joints: {
      pelvis: requireGroup(root, "humanoid.pelvis"),
      torso: requireGroup(root, "humanoid.torso"),
      head: requireGroup(root, "humanoid.head"),
      shoulderLeft: requireGroup(root, "humanoid.shoulder.left"),
      shoulderRight: requireGroup(root, "humanoid.shoulder.right"),
      elbowLeft: requireGroup(root, "humanoid.elbow.left"),
      elbowRight: requireGroup(root, "humanoid.elbow.right"),
      hipLeft: requireGroup(root, "humanoid.hip.left"),
      hipRight: requireGroup(root, "humanoid.hip.right"),
      kneeLeft: requireGroup(root, "humanoid.knee.left"),
      kneeRight: requireGroup(root, "humanoid.knee.right"),
    },
  };
}

export function buildHumanoid(options: HumanoidBuildOptions = {}): HumanoidRig {
  const materials = options.materials ?? createHumanoidMaterials(options);
  const shape = options.bodyShape ?? "average";
  const widthScale = shape === "slim" ? 0.86 : shape === "broad" ? 1.14 : 1;
  const root = namedGroup("humanoid");
  root.userData.kind = "actor";
  root.userData.actorType = "humanoid";
  root.userData.forwardAxis = "-Z";

  const pelvis = namedGroup("humanoid.pelvis", [0, HUMANOID_METRICS.hipY, 0]);
  root.add(pelvis);
  pelvis.add(
    actorMesh("humanoid.pelvis.shell", new THREE.CapsuleGeometry(0.12, 0.12, 4, 8), materials.outfit, {
      scale: [1.25 * widthScale, 0.75, 0.9],
      position: [0, 0.02, 0],
    }),
  );

  const torso = namedGroup("humanoid.torso", [0, 0.11, 0]);
  pelvis.add(torso);
  torso.add(
    actorMesh("humanoid.chest", new THREE.CapsuleGeometry(0.18, 0.25, 5, 10), materials.outfit, {
      position: [0, 0.24, 0],
      scale: [1.08 * widthScale, 1, 0.72],
    }),
    actorMesh("humanoid.chest.accent", new THREE.BoxGeometry(0.2 * widthScale, 0.065, 0.018), materials.accent, {
      position: [0, 0.32, -0.134],
    }),
  );

  const head = namedGroup("humanoid.head", [0, 0.58, 0]);
  torso.add(head);
  head.add(
    actorMesh("humanoid.head.shell", new THREE.SphereGeometry(0.12, 16, 12), materials.skin, {
      position: [0, 0.1, 0],
      scale: [0.86, 1.05, 0.86],
    }),
    actorMesh("humanoid.hair", new THREE.SphereGeometry(0.122, 14, 8, 0, Math.PI * 2, 0, Math.PI * 0.47), materials.hair, {
      position: [0, 0.115, 0.004],
      scale: [0.88, 1.06, 0.88],
    }),
  );
  const face = actorMesh("humanoid.face", new THREE.PlaneGeometry(0.125, 0.105, 2, 2), materials.face, {
    position: [0, 0.09, -0.105],
    rotation: [0, Math.PI, 0],
    receiveShadow: false,
  });
  head.add(face);

  const shoulderY = 0.47;
  for (const side of [-1, 1] as const) {
    const word = side < 0 ? "left" : "right";
    const shoulder = namedGroup(`humanoid.shoulder.${word}`, [side * 0.23 * widthScale, shoulderY, 0]);
    torso.add(shoulder);
    limb(shoulder, `humanoid.upper-arm.${word}`, materials.outfit, 0.066, 0.3);
    const elbow = namedGroup(`humanoid.elbow.${word}`, [0, -0.3, 0]);
    shoulder.add(elbow);
    limb(elbow, `humanoid.forearm.${word}`, materials.skin, 0.055, 0.27);
    elbow.add(
      actorMesh(`humanoid.hand.${word}`, new THREE.SphereGeometry(0.065, 10, 8), materials.skin, {
        position: [0, -0.295, -0.012],
        scale: [0.72, 1.08, 0.55],
      }),
    );
  }

  for (const side of [-1, 1] as const) {
    const word = side < 0 ? "left" : "right";
    const hip = namedGroup(`humanoid.hip.${word}`, [side * 0.105 * widthScale, -0.02, 0]);
    pelvis.add(hip);
    limb(hip, `humanoid.thigh.${word}`, materials.outfit, 0.085, 0.43);
    const knee = namedGroup(`humanoid.knee.${word}`, [0, -0.43, 0]);
    hip.add(knee);
    limb(knee, `humanoid.shin.${word}`, materials.outfit, 0.072, 0.4);
    knee.add(
      actorMesh(`humanoid.shoe.${word}`, new THREE.BoxGeometry(0.15, 0.09, 0.25), materials.sole, {
        position: [0, -0.405, -0.055],
      }),
    );
  }

  return resolveHumanoid(root, !options.materials);
}

/** Clone the hierarchy while sharing geometry and materials. */
export function cloneHumanoid(source: HumanoidRig): HumanoidRig {
  return resolveHumanoid(source.root.clone(true), false);
}

export function poseHumanoid(rig: HumanoidRig, pose: HumanoidPose = {}): void {
  const phase = pose.walkPhase ?? 0;
  const stride = THREE.MathUtils.clamp(pose.stride ?? 0, 0, 0.75);
  const arm = THREE.MathUtils.clamp(pose.armSwing ?? stride * 0.85, 0, 0.7);
  const swing = Math.sin(phase);
  const bendLeft = Math.max(0, -swing) * stride * 0.7;
  const bendRight = Math.max(0, swing) * stride * 0.7;
  rig.joints.hipLeft.rotation.x = swing * stride;
  rig.joints.hipRight.rotation.x = -swing * stride;
  rig.joints.kneeLeft.rotation.x = -bendLeft;
  rig.joints.kneeRight.rotation.x = -bendRight;
  rig.joints.shoulderLeft.rotation.x = -swing * arm;
  rig.joints.shoulderRight.rotation.x = swing * arm;
  rig.joints.elbowLeft.rotation.x = 0.1 + Math.max(0, swing) * 0.18;
  rig.joints.elbowRight.rotation.x = 0.1 + Math.max(0, -swing) * 0.18;
  rig.joints.pelvis.position.y = HUMANOID_METRICS.hipY + Math.abs(Math.cos(phase)) * stride * 0.018;
  rig.joints.head.rotation.y = THREE.MathUtils.clamp(pose.headYaw ?? 0, -0.9, 0.9);
  rig.joints.head.rotation.x = THREE.MathUtils.clamp(pose.headPitch ?? 0, -0.45, 0.45);
}

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