// A rigid body, as an aspect on a Node2D, backed by Box2D v3 in creator-2d. Mirrors the 3D Physics
// aspect. Requires a Shape2D aspect on the same node (its geometry becomes the body's solid fixture).
//
//   Physics2D.configure({ gravity: [0, -980] })          // once, before creating bodies
//   ground.aspect(Shape2D, { segment: { from: [-500,0], to: [500,0] } }).aspect(Physics2D, { motion: 'static' })
//   hero.aspect(Shape2D, { box: [12, 20] }).aspect(Physics2D, { motion: 'dynamic', fixedRotation: true })
//   hero.physics.applyImpulse([0, 400])                   // jump
//   hero.addEventListener('enter', other => …)            // contact / sensor overlap began
//
// For a `dynamic` body, physics OWNS the transform — don't set node.position per frame; drive it via
// velocity / applyImpulse. Reading node.position works: the node is flagged _physicsDriven and lazily
// syncs its cache from native on read. Kinematic bodies are driven with moveTo. Best practice: keep
// bodies on root nodes (see creator-2d PLAN.md).

import { Aspect } from "../core/Aspect"
import { Vec2, cx, cy, type Vec2Like } from "../math/vec"
import { node2dRegistry, type Node2D } from "./Node2D"
import { Shape2D } from "./Shape2D"

export type MotionType = "static" | "kinematic" | "dynamic"
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, -980]. */
  gravity?: Vec2Like
  /** Pixels per meter — tunes Box2D's internal tolerances; you still author in world units. Default 64. */
  pixelsPerMeter?: number
  /** Solver sub-steps per fixed step. Default 4. */
  subSteps?: number
}

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

// Resolve a native entity id to its Node2D (0 = none).
const node2dById = (id: number): Node2D | null => (id ? node2dRegistry.get(id) ?? null : null)

export class Physics2D extends Aspect<"physics", Node2D> {
  static readonly aspect = "physics"

  /** Default "dynamic". */
  motion: MotionType = "dynamic"
  density = 1
  friction = 0.3
  restitution = 0
  /** Lock rotation (essential for platformer characters). */
  fixedRotation = false
  /** Continuous collision for fast-moving bodies (projectiles). */
  bullet = false

  private _gravityScale = 1
  private _h = 0   // native body id (0 = none / no physics support)

  /** Native physics body id. 0 until attached, or if the build has no physics support. Mirrors 3D `Physics.id`. */
  get id(): number { return this._h }

  onAttach(): void {
    if (!_creator2d.physicsHasSupport || !_creator2d.physicsHasSupport()) return
    const shape = this.node.get(Shape2D)
    if (!shape) {
      throw new Error("Physics2D requires a Shape2D aspect — add it first: node.aspect(Shape2D, {…}).aspect(Physics2D, {…})")
    }
    this._h = _creator2d.physicsCreateBody(this.node.id, MOTION[this.motion])
    if (!this._h) return
    shape._addTo(this._h, false /* solid */, this.density, this.friction, this.restitution)
    // dynamic/kinematic bodies are moved by the native step → the node must re-sync its transform
    // cache from native on read. Static bodies never move, so they keep the free cache.
    if (this.motion !== "static") this.node._physicsDriven = true
    if (this.fixedRotation) _creator2d.physicsSetFixedRotation(this._h, true)
    if (this._gravityScale !== 1) _creator2d.physicsSetGravityScale(this._h, this._gravityScale)
    if (this.bullet) _creator2d.physicsSetBullet(this._h, true)
  }

  onDetach(): void {
    if (this._h) { _creator2d.physicsRemoveBody(this._h); this._h = 0 }
    this.node._physicsDriven = false
  }

  /** Linear velocity in world units/second (fresh Vec2 on read). */
  get velocity(): Vec2 {
    const o = this._h ? _creator2d.physicsGetLinearVelocity(this._h) : null
    return o ? new Vec2(o[0], o[1]) : new Vec2(0, 0)
  }
  set velocity(v: Vec2Like) { if (this._h) _creator2d.physicsSetLinearVelocity(this._h, cx(v), cy(v)) }

  /** Angular velocity in degrees/second. */
  set angularVelocity(degPerSec: number) { if (this._h) _creator2d.physicsSetAngularVelocity(this._h, degPerSec) }

  /** Per-body gravity multiplier: 1 = full world gravity, 0 = floats, <0 = repelled. Set at attach
   *  time (as an option) or live. */
  get gravityScale(): number { return this._gravityScale }
  set gravityScale(scale: number) { this._gravityScale = scale; if (this._h) _creator2d.physicsSetGravityScale(this._h, scale) }
  set linearDamping(d: number) { if (this._h) _creator2d.physicsSetLinearDamping(this._h, d) }
  set enabled(v: boolean) { if (this._h) _creator2d.physicsSetEnabled(this._h, v) }

  /** Apply an instantaneous impulse (world units) and wake the body. */
  applyImpulse(v: Vec2Like): this { if (this._h) _creator2d.physicsApplyLinearImpulse(this._h, cx(v), cy(v)); return this }
  /** Apply a continuous force (world units) and wake the body. */
  applyForce(v: Vec2Like): this { if (this._h) _creator2d.physicsApplyForce(this._h, cx(v), cy(v)); return this }
  /** Teleport / drive a kinematic (or dynamic) body to a world position (+ optional rotation degrees). */
  moveTo(p: Vec2Like, deg = 0): this { if (this._h) _creator2d.physicsSetTransform(this._h, cx(p), cy(p), deg); return this }

  // --- world (static) -------------------------------------------------------------------------
  /** Create / reset the physics world. Call once before creating bodies. */
  static configure(config: PhysicsConfig = {}): void {
    const g = config.gravity ?? [0, -980]
    _creator2d.physicsConfigure(cx(g), cy(g), config.pixelsPerMeter ?? 64, config.subSteps ?? 4)
  }

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

  /** 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 Physics2D._interpolation }
  static set interpolation(on: boolean) { Physics2D._interpolation = on; _creator2d.setInterpolation(on) }

  /** Closest body hit by the segment from→to, or null. `node` resolves the hit entity. */
  static raycast(from: Vec2Like, to: Vec2Like): RayHit | null {
    const o = _creator2d.physicsRaycastClosest(cx(from), cy(from), cx(to), cy(to))
    if (o[0] < 0.5) return null
    return { node: node2dById(o[1]), point: new Vec2(o[2], o[3]), normal: new Vec2(o[4], o[5]), fraction: o[6] }
  }

  /** Topmost body (solid or sensor) whose shape contains the world point, or null. */
  static overlapPoint(p: Vec2Like): Node2D | null {
    return node2dById(_creator2d.physicsOverlapPoint(cx(p), cy(p)))
  }
}
