// A ragdoll: the model's skeleton handed to physics. One rigid body per listed bone — a capsule from
// the bone's origin to the next joint down the chain — joined to its parent part by a swing-twist
// constraint (a cone + a twist range, or a one-way hinge for knees and elbows). Built once at
// attach from the pose the model is in (that pose is the joints' neutral for the limits), and
// switched on and off at will:
//
//   const rag = hero.aspect(Ragdoll).get(Ragdoll)!            // 'humanoid': the parts by bone name
//   rag.activate({ velocity: capsule.controller.velocity })   // drops from its CURRENT pose, keeps its speed
//   …
//   rag.deactivate()                                           // the animator owns the bones again
//
// While active the engine writes the bodies' poses onto the bones every frame AFTER the animator, so
// the listed bones hide whatever plays underneath; bones NOT listed (fingers, toes, the rest of a
// spine chain between two parts) keep their animated local pose under the ragdolled parents, and
// anything mounted on a bone (a weapon in the hand) rides along. Raycasts hit the parts and report
// the bone node, so a shot knows which limb it struck — `impulse(bone, …)` pushes that part.
//
// The character CONTROLLER is yours to switch off (`node.removeAspect(CharacterController)`): a
// capsule left standing would keep the fallen body from settling. `root` (the hips) tells you where
// the body ended up when it is time to get back up.
//
// A fallen body is scenery, not an obstacle: by default (`collide: 'static'`) only the static world
// and other ragdolls stop it — dynamic bodies, character controllers and vehicles pass through, so it
// never shoves or blocks anything and the solver never pairs it with them — and once every part has
// come to rest (`freeze`, or `freezeAfter` seconds at the latest) the parts turn static where they
// lie: the pose holds, nothing can wake them, the solver skips them. Raycasts hit it either way, and
// a frozen `'all'` body is an obstacle you can stand on.

import { Aspect } from "../core/Aspect"
import type { FieldMeta } from "../core/fields"
import { cx, cy, cz, type Vec3Like } from "../math/vec"
import { Node } from "./Node"
import { DEFAULT_FRICTION } from "./Physics"

export interface RagdollHinge {
  /** The hinge axis in the MODEL's space (its rest pose: +Z forward, +Y up, +X the model's left in
   *  glTF). Perpendicular to the bone; the engine drops any component along it. */
  axis: Vec3Like
  /** The bend range about `axis` in degrees, relative to the pose the ragdoll was built in — a knee
   *  is `{ axis: [1, 0, 0], min: 0, max: 140 }`: it folds one way only. */
  min: number
  max: number
}

export interface RagdollPart {
  /** The bone the body sits on — its origin is the joint. */
  bone: string
  /** The bone the capsule points at (the next joint down). Omitted = a leaf: `length` along the line
   *  from the parent part's origin (a head, a hand, a foot without toes). */
  to?: string
  /** The bone of the part this one hangs from (default: the previous part in the list). The root
   *  has none. */
  parent?: string
  /** Capsule radius in metres (default 0.05). */
  radius?: number
  /** Length in metres: a leaf's, or an override of the distance to `to`. */
  length?: number
  /** This part's share of the ragdoll's `mass` (relative weight, default 1). */
  mass?: number
  /** Cone half-angle in degrees the part may swing off its parent (default 30). Ignored with `hinge`. */
  swing?: number
  /** Twist half-angle in degrees about its own axis (default 20). */
  twist?: number
  /** A one-way joint instead of the cone. */
  hinge?: RagdollHinge
}

const VERSION = 2
const HEADER = 12
const STRIDE = 12

// Bone names per humanoid role, tried in order: Mixamo / MotusMan / Unity-style first, then UE-style.
// `node.bone()` already matches the part after a `:` / `|` prefix (`mixamorig:Hips`).
const HUMANOID: Record<string, string[]> = {
  hips: [ "Hips", "pelvis", "Pelvis", "hip" ],
  spine: [ "Spine", "spine_01", "spine", "Spine1" ],
  neck: [ "Neck", "neck_01", "neck" ],
  head: [ "Head", "head" ],
  upperArmL: [ "LeftArm", "upperarm_l", "LeftUpperArm", "UpperArm_L", "L_UpperArm" ],
  foreArmL: [ "LeftForeArm", "lowerarm_l", "LeftLowerArm", "LowerArm_L", "L_Forearm" ],
  handL: [ "LeftHand", "hand_l", "Hand_L", "L_Hand" ],
  upLegL: [ "LeftUpLeg", "thigh_l", "LeftUpperLeg", "UpperLeg_L", "L_Thigh" ],
  legL: [ "LeftLeg", "calf_l", "LeftLowerLeg", "LowerLeg_L", "L_Calf" ],
  footL: [ "LeftFoot", "foot_l", "Foot_L", "L_Foot" ],
  toeL: [ "LeftToeBase", "ball_l", "LeftToe", "Toe_L", "L_Toe" ],
  upperArmR: [ "RightArm", "upperarm_r", "RightUpperArm", "UpperArm_R", "R_UpperArm" ],
  foreArmR: [ "RightForeArm", "lowerarm_r", "RightLowerArm", "LowerArm_R", "R_Forearm" ],
  handR: [ "RightHand", "hand_r", "Hand_R", "R_Hand" ],
  upLegR: [ "RightUpLeg", "thigh_r", "RightUpperLeg", "UpperLeg_R", "R_Thigh" ],
  legR: [ "RightLeg", "calf_r", "RightLowerLeg", "LowerLeg_R", "R_Calf" ],
  footR: [ "RightFoot", "foot_r", "Foot_R", "R_Foot" ],
  toeR: [ "RightToeBase", "ball_r", "RightToe", "Toe_R", "R_Toe" ],
}

/** The standard humanoid layout (hips, one spine capsule up to the neck, head, two-segment arms and
 *  legs, hands and feet where the rig has them): sizes for a ~1.8 m human, mass shares from body
 *  segment tables, knees and elbows as hinges that fold the natural way for a model standing in a
 *  T- or A-pose facing +Z. */
const humanoidParts = (find: (role: string) => string | null): RagdollPart[] => {
  const hips = find("hips"), spine = find("spine"), neck = find("neck"), head = find("head")
  if (!hips) throw new Error("Ragdoll 'humanoid': no hips bone (Hips / pelvis) on this model — list the parts by name")
  const parts: RagdollPart[] = []
  parts.push({ bone: hips, to: spine ?? undefined, length: spine ? 0 : 0.2, radius: 0.12, mass: 15 })
  const torso = spine ?? hips
  if (spine) parts.push({ bone: spine, to: neck ?? head ?? undefined, length: neck || head ? 0 : 0.4, parent: hips, radius: 0.11, mass: 25, swing: 25, twist: 25 })
  if (head) parts.push({ bone: head, parent: torso, radius: 0.1, length: 0.2, mass: 6, swing: 35, twist: 45 })
  for (const side of [ "L", "R" ] as const) {
    const upper = find(`upperArm${side}`), fore = find(`foreArm${side}`), hand = find(`hand${side}`)
    if (upper) parts.push({ bone: upper, to: fore ?? undefined, length: fore ? 0 : 0.3, parent: torso, radius: 0.05, mass: 3, swing: 80, twist: 45 })
    // the elbow: a T-pose arm along ±X folds its hand forward (+Z) — a negative turn about +Y on the
    // left, a positive one on the right
    if (upper && fore) parts.push({ bone: fore, to: hand ?? undefined, length: hand ? 0 : 0.28, parent: upper, radius: 0.04, mass: 2, twist: 30, hinge: { axis: [ 0, 1, 0 ], min: side === "L" ? -140 : 0, max: side === "L" ? 0 : 140 } })
    if (upper && fore && hand) parts.push({ bone: hand, parent: fore, radius: 0.035, length: 0.16, mass: 1, swing: 30, twist: 20 })
    const up = find(`upLeg${side}`), leg = find(`leg${side}`), foot = find(`foot${side}`), toe = find(`toe${side}`)
    if (up) parts.push({ bone: up, to: leg ?? undefined, length: leg ? 0 : 0.45, parent: hips, radius: 0.08, mass: 10, swing: 60, twist: 30 })
    // the knee: the shin (pointing down) swings its foot backward (−Z) for a positive turn about +X
    if (up && leg) parts.push({ bone: leg, to: foot ?? undefined, length: foot ? 0 : 0.42, parent: up, radius: 0.06, mass: 5, twist: 10, hinge: { axis: [ 1, 0, 0 ], min: 0, max: 140 } })
    if (up && leg && foot) parts.push({ bone: foot, to: toe ?? undefined, length: toe ? 0 : 0.2, parent: leg, radius: 0.04, mass: 2, swing: 30, twist: 15 })
  }
  return parts
}

export class Ragdoll extends Aspect<"ragdoll", Node> {
  static readonly aspect = "ragdoll"

  /** The parts: `'humanoid'` (default) finds the standard bones by name; a list places bodies on any
   *  bones — parents before children. */
  parts: RagdollPart[] | "humanoid" = "humanoid"
  /** Total mass in kg (default 70), split between the parts by their `mass` shares. */
  mass = 70
  /** Friction of every part (default 0.6 — the physics default). */
  friction = DEFAULT_FRICTION
  /** Linear / angular damping of every part; 0 = Jolt's own 0.05. More angular damping settles a
   *  flailing body sooner. */
  linearDamping = 0
  angularDamping = 0
  /** What the parts collide with. `'static'` (default): the static geometry and other such ragdolls
   *  only — dynamic bodies, character controllers and vehicles pass through a fallen body, which then
   *  never shoves or blocks anything and costs nothing when they meet. `'all'`: a regular dynamic
   *  body that bumps into everything (and gets kicked awake by everything). */
  collide: "static" | "all" = "static"
  /** Once every part has come to rest the parts turn static where they lie (default true): the pose
   *  holds, nothing can wake a settled body and it costs the solver nothing — `deactivate()` /
   *  `activate()` still work (activate makes it dynamic again). */
  freeze = true
  /** Seconds after `activate()` at which the body freezes whatever it is doing — a twitch on a slope
   *  or a pile never sleeps on its own. 0 (default) = no cap. */
  freezeAfter = 0

  static fields: FieldMeta<Ragdoll> = {
    mass: { min: 1, step: 5 },
    friction: { min: 0, max: 2, step: 0.05 },
    linearDamping: { label: "Linear damping", min: 0, max: 5, step: 0.05 },
    angularDamping: { label: "Angular damping", min: 0, max: 5, step: 0.05 },
    collide: { options: [ "static", "all" ] },
    freezeAfter: { label: "Freeze after (s)", min: 0, max: 60, step: 0.5 },
    parts: { hidden: true },
  }

  private _id = 0
  private _active = false
  private _bones: { name: string; node: Node }[] = []

  onAttach(): void {
    if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return
    if (!_creator.ragdollCreate) {
      console.warn("Ragdoll needs a newer host (no ragdollCreate) — the body will not fall")
      return
    }
    const find = (role: string): string | null => {
      for (const name of HUMANOID[role] ?? []) if (this.node.bone(name)) return name
      return null
    }
    const parts = this.parts === "humanoid" ? humanoidParts(find) : this.parts
    if (parts.length === 0) throw new Error("Ragdoll: no parts")

    const n = parts.length
    const bones = new Uint32Array(n * 2)
    const s = new Float32Array(HEADER + n * STRIDE)
    s[0] = VERSION; s[1] = n; s[2] = STRIDE; s[3] = this.friction; s[4] = this.linearDamping; s[5] = this.angularDamping
    s[6] = this.collide === "all" ? 0 : 1; s[7] = this.freeze ? 1 : 0; s[8] = Math.max(0, this.freezeAfter)
    let shares = 0
    for (const p of parts) shares += p.mass ?? 1
    this._bones = []
    for (let i = 0; i < n; i++) {
      const p = parts[i]!
      const bone = this.node.bone(p.bone)
      if (!bone) throw new Error(`Ragdoll: no bone '${p.bone}' on '${this.node.name}'`)
      const to = p.to ? this.node.bone(p.to) : null
      if (p.to && !to) throw new Error(`Ragdoll: no bone '${p.to}' (the 'to' of '${p.bone}') on '${this.node.name}'`)
      let parent = -1
      if (i > 0) {
        parent = p.parent ? parts.findIndex((q, j) => j < i && q.bone === p.parent) : i - 1
        if (parent < 0) throw new Error(`Ragdoll: the parent '${p.parent}' of '${p.bone}' must be listed before it`)
      }
      bones[i * 2] = bone.id
      bones[i * 2 + 1] = to ? to.id : 0
      const o = HEADER + i * STRIDE
      s[o] = parent
      s[o + 1] = p.radius ?? 0.05
      s[o + 2] = p.length ?? 0
      s[o + 3] = this.mass * (p.mass ?? 1) / shares
      s[o + 4] = p.swing ?? 30
      s[o + 5] = p.twist ?? 20
      if (p.hinge) {
        s[o + 6] = 1
        s[o + 7] = cx(p.hinge.axis); s[o + 8] = cy(p.hinge.axis); s[o + 9] = cz(p.hinge.axis)
        s[o + 10] = p.hinge.min; s[o + 11] = p.hinge.max
      }
      this._bones.push({ name: p.bone, node: bone })
    }
    this._id = _creator.ragdollCreate(this.node.id, bones, s)
    if (!this._id) console.warn(`Ragdoll: the host refused the parts on '${this.node.name}'`)
  }

  onDetach(): void {
    if (this._id) {
      if (this._active) _creator.ragdollDeactivate!(this._id)
      _creator.ragdollDestroy!(this._id)
      this._id = 0
    }
    this._active = false
    this._bones = []
  }

  /** Hand the bones to physics from the pose they are in right now. `velocity` (world m/s) is given
   *  to every part — pass the character's, so a running body keeps travelling. */
  activate(opts: { velocity?: Vec3Like } = {}): boolean {
    if (!this._id) return false
    const v = opts.velocity ?? [ 0, 0, 0 ]
    this._active = _creator.ragdollActivate!(this._id, cx(v), cy(v), cz(v))
    return this._active
  }

  /** Take the bones back: the animator's pose shows again from the next frame. */
  deactivate(): void {
    if (!this._id || !this._active) return
    _creator.ragdollDeactivate!(this._id)
    this._active = false
  }

  /** Physics owns the bones right now. */
  get active(): boolean { return this._active }

  /** Active and every part asleep (or frozen) — the body has come to rest. */
  get settled(): boolean {
    return this._active && !!this._id && !_creator.ragdollActive!(this._id)
  }

  /** The root part's bone (the hips): where the body is. */
  get root(): Node | null { return this._bones[0]?.node ?? null }

  /** The bones that carry a part, in order. */
  get bones(): string[] { return this._bones.map((b) => b.name) }

  /** The rigid-body id of a part (0 if none) — for the plain body calls. */
  bodyOf(bone: string): number {
    const i = this._bones.findIndex((b) => b.name === bone)
    return i < 0 || !this._id ? 0 : _creator.ragdollBodyId!(this._id, i)
  }

  /** Push one part: an impulse in N·s, at a world point (a hit) or through its centre. Wakes the body. */
  impulse(bone: string, v: Vec3Like, at?: Vec3Like): boolean {
    const id = this.bodyOf(bone)
    if (!id) return false
    if (at && _creator.physicsApplyImpulseAt) _creator.physicsApplyImpulseAt(id, cx(v), cy(v), cz(v), cx(at), cy(at), cz(at))
    else _creator.physicsApplyImpulse(id, cx(v), cy(v), cz(v))
    return true
  }
}
