// One wheel of a `Vehicle`, as an aspect on the wheel's own node. The node's position IS the wheel's
// position (where its centre rests on flat ground), and everything else about it lives here too:
//
//   const car = new Node('car')
//     .add(body, wheelFL.aspect(Wheel), wheelFR.aspect(Wheel),
//                wheelRL.aspect(Wheel), wheelRR.aspect(Wheel))
//     .aspect(Shape, { box: [0.9, 0.4, 2.1] })
//     .aspect(Vehicle, { mass: 1200 })
//
// That is the whole minimum: `Vehicle` collects every `Wheel` under itself, groups them into axles by
// Z (front first), and the defaults fall out — the front axle steers, the rear one drives and carries
// the handbrake. Override any of it per wheel.
//
// This aspect is also what POSES the wheel: it runs in the LATE phase (after the physics step, before
// the frame draws) and writes the node's transform from the suspension length, steer angle and spin
// the engine reports. Nothing else writes that transform — the engine deliberately does not, so a
// wheel model's own conventions (which way its axle points, how big it looks) are settings here
// rather than constants in C++, and a part that should steer but NOT spin is simply a sibling node.
//
// Chassis space is forward −Z / up +Y, the SDK's own convention (`node.forward`).

import { Aspect } from "../core/Aspect"
import type { FieldMeta } from "../core/fields"
import { Mat4 } from "../math/mat4"
import { Quat } from "../math/quat"
import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
import { Gizmos } from "../scene/gizmos"
import { Node } from "./Node"
import { Shape } from "./Shape"
import type { Vehicle } from "./Vehicle"

/** Floats `vehicleGetState` writes before the per-wheel block: speed, rpm, contacts, vx, vy, vz. */
export const STATE_HEADER = 6
/** Floats per wheel: contact, slip, slipAngle, suspensionLength, steerDeg, spin. */
export const WHEEL_STATE = 6

/** How a tire behaves past its grip peak. `road` is Jolt's own curve — grip falls off after the peak,
 *  so the car can be made to let go. `arcade` holds the peak all the way out: much harder to spin. */
export type TireCurve = "road" | "arcade"

export interface WheelSuspension {
  /** Total travel in metres, full droop to fully compressed (default 0.4). */
  travel?: number
  /** Spring frequency in Hz — bigger is stiffer (default 1.4; a race car ~2.5, a truck ~1). It also
   *  sets the ride-height sag, and the sag is how far the wheel can still extend DOWN. */
  stiffness?: number
  /** Damping ratio 0..1 (default 0.5). Low values pogo. */
  damping?: number
}

export interface WheelTire {
  /** Grip multiplier on the friction curves (default 1). The grip a wheel actually gets is combined
   *  with the ground it stands on as `sqrt(tire × groundFriction)`, and a body's friction defaults to
   *  0.6 — so a road a car should really corner on wants `Physics { friction: 1 }`. */
  grip?: number
  /** Which friction curve (default `road`). */
  curve?: TireCurve
}

export interface WheelVisual {
  /** The MODEL's axle direction in its own space (default `[1, 0, 0]`). Set it instead of parenting
   *  the mesh under a corrective node when the art points another way. */
  axis?: Vec3Like
  /** Drawn radius, if the art is not exactly the physical `radius` — sinks a wheel into its arch or
   *  lets a fat tire overhang, with no effect on the simulation. Default: the physical radius. */
  radius?: number
  /** Let the model spin (default true). `false` keeps the steer angle and suspension travel but not
   *  the rotation — a brake caliper. */
  spin?: boolean
}

const DEG2RAD = Math.PI / 180
const UP = new Vec3(0, 1, 0)

export class Wheel extends Aspect<"wheel", Node> {
  static readonly aspect = "wheel"

  /** Wheel radius in metres. Default: measured from the node's rendered bounds, else 0.35. Too small
   *  and the car scrapes, too large and it floats. */
  radius?: number
  /** Wheel width in metres. Default: measured, else 0.25. */
  width?: number
  /** Does this wheel steer? Default: the wheels on the front axle. */
  steer?: boolean
  /** Does the engine drive it? Default: from the vehicle's `drive` layout. */
  drive?: boolean
  /** Foot brake on this wheel. */
  brake = true
  /** Handbrake on this wheel. Default: every non-steering wheel (locking the front wheels of a moving
   *  car is not a handbrake turn, it is a crash). */
  handbrake?: boolean
  /** Axle group, front to back. Default: derived from Z. Set it for a car whose wheels are not in neat
   *  left/right pairs — an axle's wheels are paired for the differential and the anti-roll bar. */
  axle?: number
  /** Foot-brake torque in N·m (default 1500); the handbrake pulls 3× on the wheels that have it. */
  brakeTorque = 1500

  suspension: WheelSuspension = { travel: 0.4, stiffness: 1.4, damping: 0.5 }
  tire: WheelTire = { grip: 1, curve: "road" }
  visual: WheelVisual = {}

  /** Scene-editor inspector. `steer` / `drive` / `handbrake` are deliberately absent: they are
   *  tri-state (undefined = derive it from the axle), which a switch cannot express. */
  static fields: FieldMeta<Wheel> = {
    radius: { min: 0.05, step: 0.01 },
    width: { min: 0.02, step: 0.01 },
    axle: { min: 0, step: 1 },
    brake: { label: "Foot brake" },
    brakeTorque: { label: "Brake torque", min: 0, step: 50 },
    suspension: { hidden: true },
    tire: { hidden: true },
    visual: { hidden: true },
  }
  static editor = { rebuild: true }

  /** @internal Set by `Vehicle` at attach — the car this wheel belongs to, and its index in the blob. */
  _vehicle: Vehicle | null = null
  /** @internal */
  _index = -1
  /** @internal Suspension attachment point in chassis space; the wheel hangs below it. */
  _attach = new Vec3()
  /** @internal Resting centre in chassis space (what the author placed). */
  _rest = new Vec3()

  private _angle = 0                       // integrated spin, radians — the engine reports rate, not angle
  private _scale = new Vec3(1, 1, 1)       // the node's own scale, preserved through the pose write
  private _local = true                    // is the node a direct child of the chassis?

  onAttach(): void {
    this._scale = this.node.scale
  }

  onDetach(): void {
    this._vehicle = null
    this._index = -1
  }

  /** @internal Called by Vehicle once the car exists, with the geometry it resolved. */
  _bind(vehicle: Vehicle, index: number, rest: Vec3, attach: Vec3): void {
    this._vehicle = vehicle
    this._index = index
    this._rest = rest
    this._attach = attach
    this._scale = this.node.scale
    this._local = this.node.parent === vehicle.node
  }

  // ---- live state ------------------------------------------------------------------------------
  // Every getter reads the car's ONE shared state buffer, which is refreshed once per frame.

  private _state(k: number): number {
    const v = this._vehicle
    if (!v || this._index < 0) return 0
    return v._wheelState(this._index, k)
  }

  /** Touching the ground this step. */
  get contact(): boolean { return this._state(0) !== 0 }
  /** Longitudinal slip RATIO: 0 = rolling in sync with the ground, ~1 = spinning up or locked.
   *  Unsigned — smoke, tire marks and wheelspin sound key off this. */
  get slip(): number { return this._state(1) }
  /** Slip ANGLE in degrees: how far the tire's travel direction is off from where it points.
   *  Unsigned, 0..90 — past the curve's peak (~3°) the tire is sliding sideways. Drift particles. */
  get slipAngle(): number { return this._state(2) }
  /** Steer angle in degrees, positive to the right. */
  get steerAngle(): number { return this._state(4) }
  /** Wheel spin in rad/s (× radius ≈ the speed the tire is laying down). */
  get spin(): number { return this._state(5) }
  /** Suspension compression, 0 = fully extended (drooping) … 1 = fully compressed. */
  get compression(): number {
    const travel = this.suspension.travel ?? 0.4
    if (travel <= 0) return 0
    const len = this._state(3)
    const min = travel * 0.4
    return Math.max(0, Math.min(1, (travel - len) / (travel - min)))
  }

  // ---- the visual ------------------------------------------------------------------------------

  /** LATE phase: after the step and the transform sync, right before the frame draws. */
  update(dt: number): void {
    const v = this._vehicle
    if (!v || this._index < 0 || !v.id) return

    if (this.visual.spin !== false) this._angle += this._state(5) * dt

    // Down the suspension from the attachment point by however far it currently hangs.
    const length = this._state(3)
    const centre = new Vec3(this._attach.x, this._attach.y - length, this._attach.z)

    // Steer about the chassis' up axis, then spin about the MODEL's own axle in the steered frame.
    // Both reports are negated, for the same reason: the engine describes them in the DRIVER's terms
    // and the scene is right-handed with forward on −Z. Steer is reported right-positive while a yaw
    // about +Y turns left. Spin is reported positive-means-forward, but a positive turn about the
    // right-hand axle carries the top of the wheel to +Z — which rolls the car backwards.
    const steer = Quat.fromAxisAngle(UP, -this._state(4) * DEG2RAD)
    const axis = this.visual.axis ?? [1, 0, 0]
    const rotation = this.visual.spin === false ? steer : steer.mul(Quat.fromAxisAngle(axis, -this._angle))

    // A model that draws bigger or smaller than it collides scales on top of the node's own scale.
    const vr = this.visual.radius
    const k = vr !== undefined && this.radius ? vr / this.radius : 1
    const scale = k === 1 ? this._scale : new Vec3(this._scale.x * k, this._scale.y * k, this._scale.z * k)

    const m = Mat4.compose(centre, rotation, scale)
    // The common case is a direct child of the chassis, where chassis space IS the local space.
    if (this._local) this.node.matrix = m
    else this.node.worldMatrix = new Mat4(v.node.worldMatrix).mul(m)
  }

  /** Edit mode: this wheel's own circle at its resting height, its travel range, and a mark when the
   *  engine drives it — enough to place a wheel without running the game. */
  rebuild(): void {
    const travel = this.suspension.travel ?? 0.4
    const r = this.radius ?? this._measure().radius
    const world = new Mat4(this.node.worldMatrix)
    const P = (x: number, y: number, z: number): Vec3 => world.transformPoint([ x, y, z ])
    const ring: Vec3[] = []
    for (let i = 0; i < 24; i++) {
      const t = (i / 24) * Math.PI * 2
      ring.push(P(0, Math.sin(t) * r, Math.cos(t) * r))
    }
    Gizmos.polyline(ring, { color: this.steer === false ? "#9aa3b2" : "#5b8ef0", closed: true })
    Gizmos.line(P(0, travel * 0.6, 0), P(0, -travel * 0.4, 0), { color: "#59606d" })
    if (this.drive) Gizmos.cross(P(0, 0, 0), r * 0.35, { color: "#e0b25a" })
  }

  /** @internal Radius/width from the node's rendered bounds, so a placed model needs no numbers. */
  _measure(): { radius: number, width: number } {
    const b = Shape.boundsOf(this.node)
    if (!b) return { radius: 0.35, width: 0.25 }
    const size = new Vec3(cx(b.max) - cx(b.min), cy(b.max) - cy(b.min), cz(b.max) - cz(b.min))
    // A wheel is a disc: its diameter is the larger of the two axes across the axle, and the width is
    // the axle direction itself.
    const axis = this.visual.axis ?? [1, 0, 0]
    const along = Math.abs(cx(axis)) >= Math.abs(cz(axis)) ? size.x : size.z
    const across = Math.max(Math.abs(cx(axis)) >= Math.abs(cz(axis)) ? size.z : size.x, size.y)
    return { radius: across > 0 ? across * 0.5 : 0.35, width: along > 0 ? along : 0.25 }
  }
}
