// A character controller, as an aspect on a 3D Node, backed by Jolt's CharacterVirtual — a kinematic
// collide-and-slide controller (NOT a rigid body: no bouncing/tipping, crisp control, built-in stair
// stepping + slope handling). Requires a Shape (a capsule is recommended) and does NOT use Physics.
//
//   const hero = new Mesh(capsuleGeometry(), material)
//     .aspect(Shape, { capsule: { halfHeight: 0.6, radius: 0.3 } })
//     .aspect(CharacterController, { speed: 6, jumpSpeed: 8 })
//   Input.on('move', (x, z) => hero.controller.move(x, z))
//   Input.on('jump', () => hero.controller.jump())
//
// You drive horizontal intent + jump; the controller integrates gravity and Jolt handles collision
// against the world. `grounded` reports whether it's on walkable ground. It runs in the EARLY update
// phase (feeds this frame's step). Note: it collides with solid bodies but PASSES THROUGH triggers,
// and (v1) is not itself detected by triggers and isn't pointer-pickable while active.

import { Aspect } from "../core/Aspect"
import { Vec3 } from "../math/vec"
import { Shape } from "./Shape"
import type { Node } from "./Node"

const clamp1 = (v: number): number => (v < -1 ? -1 : v > 1 ? 1 : v)

export class CharacterController extends Aspect<"controller", Node> {
  static readonly aspect = "controller"

  /** Horizontal move speed (world units/s). */
  speed = 5
  /** Jump take-off speed (world units/s). */
  jumpSpeed = 7
  /** Character-tuned gravity (world units/s²), separate from the world gravity — usually stronger for
   *  snappier feel. Applied to the vertical velocity each frame. */
  gravity = -20
  /** Max ground slope (degrees) the character treats as walkable. Set before attach. */
  maxSlope = 45

  updateBeforePhysics = true // EARLY phase — set velocity so THIS frame's step consumes it

  private _charId = 0
  private _mx = 0
  private _mz = 0
  private _jumpQueued = false
  private _vy = 0

  onAttach(): void {
    if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return
    const shape = this.node.get(Shape)
    if (!shape) {
      throw new Error("CharacterController requires a Shape aspect (a capsule is recommended) — add it first: node.aspect(Shape, { capsule: {…} }).aspect(CharacterController)")
    }
    const shapeId = shape._claim() // the character owns the shape; it is not a rigid body
    this._charId = _creator.characterCreate(this.node.id, shapeId, this.maxSlope)
  }

  onDetach(): void {
    if (this._charId) { _creator.characterDestroy(this._charId); this._charId = 0 }
    this.node.get(Shape)?._recreatePickBody()
  }

  /** Native character id (0 until attached / no physics support). */
  get id(): number { return this._charId }

  /** Horizontal move intent; each component in [-1, 1] (e.g. from input). Sticky — set 0 to stop. */
  move(x: number, z: number): void { this._mx = clamp1(x); this._mz = clamp1(z) }

  /** Queue a jump; consumed on the next frame if grounded. */
  jump(): void { this._jumpQueued = true }

  /** True while standing on walkable ground (OnGround). */
  get grounded(): boolean {
    return this._charId ? _creator.characterGetGroundState(this._charId) === 0 : false
  }

  /** Ground state: 0 on-ground, 1 on-steep-slope, 2 touching-but-unsupported, 3 in-air. */
  get groundState(): number {
    return this._charId ? _creator.characterGetGroundState(this._charId) : 3
  }

  /** Current velocity (world units/s, fresh Vec3). */
  get velocity(): Vec3 {
    const o = new Float32Array(3)
    if (this._charId) _creator.characterGetVelocity(this._charId, o)
    return new Vec3(o[0], o[1], o[2])
  }

  /** Teleport to a world position (clears vertical velocity). */
  teleport(x: number, y: number, z: number): this {
    if (this._charId) _creator.characterSetPosition(this._charId, x, y, z)
    this._vy = 0
    return this
  }

  update(dt: number): void {
    if (!this._charId) return
    const grounded = _creator.characterGetGroundState(this._charId) === 0
    if (grounded && this._vy < 0) this._vy = 0          // landed
    if (this._jumpQueued && grounded) this._vy = this.jumpSpeed
    this._jumpQueued = false
    this._vy += this.gravity * dt                        // integrate gravity (CharacterVirtual is kinematic)
    _creator.characterSetVelocity(this._charId, this._mx * this.speed, this._vy, this._mz * this.speed)
  }
}
