// Rigid body as an aspect on a 3D Node, backed by a JoltPhysics body in creator-gl. Requires a Shape
// aspect on the same node (its geometry becomes the body's collision shape).
//
//   Physics.configure({ gravity: [0, -9.81, 0] })          // once, before creating bodies
//   ground.aspect(Shape, { box: [20, 0.5, 20] }).aspect(Physics, { motion: 'static' })
//   crate.aspect(Shape, {}).aspect(Physics, { mass: 2 })   // auto box from the mesh
//   crate.physics.applyImpulse([0, 6, 0])
//   crate.addEventListener('enter', other => …)            // contact / trigger overlap began
//
// For a `dynamic` body, physics OWNS the transform — do NOT set node.position per frame; drive it via
// velocity / applyImpulse. Reading node.position is free (native already wrote it). Kinematic bodies
// are driven with moveTo.

import { Aspect } from "../core/Aspect"
import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
import { nodeRegistry, type Node } from "./Node"
import { Shape } from "./Shape"

export type MotionType = "static" | "dynamic" | "kinematic"
const MOTION: Record<MotionType, number> = { static: 0, kinematic: 1, dynamic: 2 }

export interface PhysicsConfig {
  /** Gravity in world units/s² (Y-up: down is negative). Default [0, -9.81, 0]. */
  gravity?: Vec3Like
  /** Max simultaneous bodies (resizes the world; only before any body exists). Default 4096. */
  maxBodies?: number
}

/** Closest raycast hit. */
export interface RayHit {
  node: Node | null
  point: Vec3
  normal: Vec3
  fraction: number
}

export class Physics extends Aspect<"physics", Node> {
  static readonly aspect = "physics"

  /** Render interpolation of body transforms between the fixed 60 Hz steps (global; default on). Turn
   *  off to save per-frame transform writes when you have many moving bodies / don't need >60 Hz
   *  smoothness — bodies then advance in discrete steps. */
  private static _interpolation = true
  static get interpolation(): boolean { return Physics._interpolation }
  static set interpolation(on: boolean) { Physics._interpolation = on; _creator.setInterpolation(on) }

  /** Default "dynamic". */
  motion: MotionType = "dynamic"
  /** Mass in kg for dynamic bodies (default 1). Ignored for static/kinematic. */
  mass = 1

  private _bodyId = 0

  onAttach(): void {
    if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return
    const shape = this.node.get(Shape)
    if (!shape) {
      throw new Error("Physics requires a Shape aspect — add it first: node.aspect(Shape, {…}).aspect(Physics, {…})")
    }
    const shapeId = shape._claim() // drop the pick-only body; reuse its shape
    this._bodyId = _creator.physicsCreateBody(this.node.id, shapeId, MOTION[this.motion], this.mass, false, false)
    shape._ownBody(this._bodyId)
  }

  onDetach(): void {
    if (this._bodyId) { _creator.physicsRemoveBody(this._bodyId); this._bodyId = 0 }
    this.node.get(Shape)?._recreatePickBody() // keep the node pickable if the Shape stays
  }

  /** Native Jolt body id. 0 until attached, or if the build has no physics support. */
  get id(): number { return this._bodyId }

  /** Linear velocity in world units/second (fresh Vec3 on read). */
  get velocity(): Vec3 {
    const out = new Float32Array(3)
    if (this._bodyId) _creator.physicsGetLinearVelocity(this._bodyId, out)
    return new Vec3(out[0], out[1], out[2])
  }
  set velocity(v: Vec3Like) {
    if (this._bodyId) _creator.physicsSetLinearVelocity(this._bodyId, cx(v), cy(v), cz(v))
  }

  /** Apply an instantaneous impulse (kg·m/s) and wake the body. */
  applyImpulse(v: Vec3Like): this {
    if (this._bodyId) _creator.physicsApplyImpulse(this._bodyId, cx(v), cy(v), cz(v))
    return this
  }

  /** Teleport / drive a kinematic (or dynamic) body to a world position. */
  moveTo(p: Vec3Like): this {
    if (this._bodyId) _creator.physicsSetBodyPosition(this._bodyId, cx(p), cy(p), cz(p))
    return this
  }

  // --- world (static) -------------------------------------------------------------------------
  /** Configure gravity / limits. Call once before creating bodies. */
  static configure(config: PhysicsConfig = {}): void {
    const g = config.gravity ?? [0, -9.81, 0]
    _creator.physicsConfigure(cx(g), cy(g), cz(g), config.maxBodies ?? 0)
  }

  /** Whether this build has physics support (CREATOR_GL_PHYSICS). */
  static get supported(): boolean {
    return !!_creator.physicsHasSupport && _creator.physicsHasSupport()
  }

  /** Closest pickable body hit by the ray from `origin` along `dir` (up to `maxDist`), or null. */
  static raycast(origin: Vec3Like, dir: Vec3Like, maxDist = 1000): RayHit | null {
    const out = new Float32Array(7)
    const id = _creator.physicsRaycast(cx(origin), cy(origin), cz(origin), cx(dir), cy(dir), cz(dir), maxDist, out)
    if (!id) return null
    return {
      node: nodeRegistry.get(id) ?? null,
      point: new Vec3(out[0], out[1], out[2]),
      normal: new Vec3(out[3], out[4], out[5]),
      fraction: out[6],
    }
  }
}
