// Base 3D scene node — an empty transform. Mesh and Light extend it, so every node operation works
// on them. Native (creator-gl) owns the authoritative transform; reads decompose a cached local
// matrix (re-synced lazily, since physics can write the transform from native each frame).
//
// Capabilities are aspects: node.aspect(Physics, …) (→ node.physics), node.aspect(Shape, …).
// A GLB import is the Model node kind (with the Animator aspect at model.anim). Hierarchy lives here.

import { AspectHost } from "../core/Aspect"
import { Registry } from "../core/registry"
import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
import { Quat, cw, type QuatLike } from "../math/quat"
import { Mat4, type Mat4Like } from "../math/mat4"
import type { Geometry } from "./Geometry"
import { registerTouchEndEvent, registerTouchStartEvent } from "./touch"
import { ensurePhysicsEvents } from "./physicsEvents"
import { Material } from "./Material"
import type { CompAxis, CompWriter } from "../core/compWrite"
import type { ClickEvent, TouchStartEvent } from "../runtime/touch"

/** id → Node, so host callbacks (touch hits, animation events) route back to the owning object. */
export const nodeRegistry = new Registry<Node>()

export type NodeEvents = {
  click: (ev: ClickEvent<Node | null>) => void
  touchstart: (ev: TouchStartEvent<Node | null>) => void
  /** AR anchors only (`scene.root` / `scene.createAnchor()`): the anchor began tracking. */
  track: () => void
  /** AR anchors only: the anchor lost tracking. */
  untrack: () => void
  /** A physics contact/trigger overlap began (the other body's node). Needs a Shape + Physics/Trigger. */
  enter: (other: Node) => void
  /** A physics contact/trigger overlap ended. */
  exit: (other: Node) => void
}

const shortName = (s: string): string => s.slice(s.search(/[^:|]*$/))

/** Fallback for hosts without `_creator.findNode` (exact name, then short name, first in tree order —
 *  no skin-joint preference: that needs the host's skin knowledge). 0 when absent. */
const findNodeByWalk = (rootId: number, name: string): number => {
  let exact = 0
  let loose = 0
  const want = shortName(name)
  _creator.traverse(rootId, (id) => {
    if (exact) return
    const n = _creator.getName(id)
    if (n === name) exact = id
    else if (!loose && shortName(n) === want) loose = id
  })
  return exact || loose
}

// Scratch for the world-position read-back in _setOwnedPosition (one per module, never handed out).
const worldScratch = new Float32Array(3)

export class Node extends AspectHost<NodeEvents> implements CompWriter {
  /** Native entity handle. */
  readonly id: number

  /** AR anchors only — whether the anchor is currently being tracked. Kept in step with the
   *  'track' / 'untrack' events; `false` for any non-anchor node. */
  isTracked = false

  private _matrix?: Float32Array
  private _worldMatrix?: Float32Array
  private _boneCache?: Map<string, Node | null>
  /** Materials assigned through setMaterial, by primitive slot (a Mesh fills slot 0 itself). */
  protected _materials?: Material[]

  constructor(internalId?: number) {
    super()
    this.id = internalId ?? _creator.createEntity()
    nodeRegistry.set(this.id, this)
  }

  /** Geometry, if this node is a Mesh (used by physics auto-shape). Overridden by Mesh. */
  get geometry(): Geometry | undefined { return undefined }

  // --- name / visibility ---
  get name(): string { return _creator.getName(this.id) }
  set name(value: string) { _creator.setName(this.id, value) }
  get visible(): boolean { return _creator.isVisible(this.id) }
  set visible(value: boolean) { _creator.setVisible(this.id, value) }

  // --- local transform ---
  // Native owns the authoritative matrix. `_sync()` reads it into a per-node scratch buffer on EVERY
  // call: a bridge read is ~40 ns on QuickJS — what the Date.now() of a staleness check cost — so a
  // cache bought nothing and needed invalidation hooks (physics / controllers / animation rewrite
  // transforms natively between frames). The PUBLIC `matrix` getter hands back a fresh Mat4 *copy*
  // (value semantics — mutating it never touches the node until you assign it back); `_sync()` is
  // the internal hot path for component reads.
  private _sync(): Float32Array {
    const m = this._matrix ?? (this._matrix = new Float32Array(16))
    _creator.getMatrix(this.id, m)
    return m
  }

  get matrix(): Mat4 { return new Mat4(this._sync()) }
  set matrix(m: Mat4Like) {
    const buf = this._matrix ?? (this._matrix = new Float32Array(16))
    buf.set(matArr(m))
    _creator.setMatrix(this.id, buf)
    // A matrix write on a physics-owned node must move the engine object too, or it is silently
    // overwritten by the next sync. Position always; rotation only for a BODY (see _setOwnedRotation).
    // Scale is never routed — a Jolt shape is built at a fixed size, that is `Shape`'s job.
    // `worldMatrix` composes into this setter, so it is covered here too.
    if (this._xf) {
      this._setOwnedPosition(buf[12], buf[13], buf[14])
      this._setOwnedRotation()
    }
  }

  get worldMatrix(): Mat4 {
    if (this.parent === null) return this.matrix
    if (!this._worldMatrix) this._worldMatrix = new Float32Array(16)
    _creator.getWorldMatrix(this.id, this._worldMatrix)
    return new Mat4(this._worldMatrix)
  }
  set worldMatrix(m: Mat4Like) {
    const p = this.parent
    if (p === null) { this.matrix = m; return }
    // local = parentWorld⁻¹ · world, composed here: the native setWorldMatrix ignores the parent (it
    // wrote the world matrix as the local one — a bone under a rotated Model wrapper read back with the
    // ancestors' rotation applied twice)
    this.matrix = p.worldMatrix.invert().mul(m)
  }

  /** @internal The engine object that mirrors this node's transform, if any — a physics body or a
   *  character (0 = none). `position` writes are routed to it as well, because the engine is the
   *  authority: on a dynamic body a plain setPosition is overwritten by the next sync, and on a
   *  static / pick / trigger body it would leave the COLLIDER behind while the node moved. Set by
   *  Shape (bodies) and CharacterController. */
  _xf = 0
  /** @internal What `_xf` is: 1 = character, 2 = physics body. */
  _xfKind = 0

  // Route a position write to the engine object that owns this node. Kept out of the setters' hot path
  // (they only pay one truthiness check). `position` stays a LOCAL coordinate like on every node, while
  // physics works in world space — so the node is written first and the world point read back natively
  // (one bridge call for any parent depth, no allocation; the native transform manager updates the
  // world matrix synchronously).
  private _setOwnedPosition(x: number, y: number, z: number): void {
    _creator.setPosition(this.id, x, y, z)   // keep the node itself right, so a read-back is immediate
    _creator.getWorldPosition(this.id, worldScratch)
    if (this._xfKind === 1) _creator.characterSetPosition(this._xf, worldScratch[0], worldScratch[1], worldScratch[2])
    else _creator.physicsSetBodyPosition(this._xf, worldScratch[0], worldScratch[1], worldScratch[2])
  }

  // The rotation half of the same routing. BODIES ONLY: a character's rotation is node-owned by design
  // (the engine writes its position and nothing else — a capsule is symmetric about its up axis, so its
  // yaw is physically meaningless), and pushing one into Jolt would invent an authority that isn't there.
  // The node has already been written by the caller, so the world rotation is read back from it — that
  // way the engine's own euler convention decides, instead of this file re-deriving it.
  private _setOwnedRotation(): void {
    if (this._xfKind !== 2 || !_creator.physicsSetBodyRotation) return
    const q = this.worldMatrix.rotation
    _creator.physicsSetBodyRotation(this._xf, q.x, q.y, q.z, q.w)
  }

  get position(): Vec3 { const m = this._sync(); return new Vec3(m[12], m[13], m[14]) }
  set position(v: Vec3Like) {
    if (this._xf) this._setOwnedPosition(cx(v), cy(v), cz(v))
    else _creator.setPosition(this.id, cx(v), cy(v), cz(v))
  }

  // Scalar position accessors — one axis without touching the others (`node.x = 3`). The direct spelling
  // `node.position.x = 3` compiles to the same thing (chisel's comp_write pass routes it through
  // `_writeComp` below); only a STORED copy of `position` is still a copy.
  get x(): number { return this._sync()[12] }
  set x(v: number) { const m = this._sync(); if (this._xf) this._setOwnedPosition(v, m[13], m[14]); else _creator.setPosition(this.id, v, m[13], m[14]) }
  get y(): number { return this._sync()[13] }
  set y(v: number) { const m = this._sync(); if (this._xf) this._setOwnedPosition(m[12], v, m[14]); else _creator.setPosition(this.id, m[12], v, m[14]) }
  get z(): number { return this._sync()[14] }
  set z(v: number) { const m = this._sync(); if (this._xf) this._setOwnedPosition(m[12], m[13], v); else _creator.setPosition(this.id, m[12], m[13], v) }

  /** @internal chisel `comp_write` (see core/compWrite.ts): `node.position.<axis> = v` is compiled to a
   *  call here; the scalar setters already carry the physics routing. The list is compile-time data. */
  static _comps = [ "position" ]
  _writeComp(_prop: string, axis: CompAxis, v: number): void {
    if (axis === "x") this.x = v
    else if (axis === "y") this.y = v
    else if (axis === "z") this.z = v
  }

  get scale(): Vec3 {
    const m = this._sync()
    return new Vec3(Math.hypot(m[0], m[1], m[2]), Math.hypot(m[4], m[5], m[6]), Math.hypot(m[8], m[9], m[10]))
  }
  set scale(v: Vec3Like | number) {
    if (typeof v === "number") _creator.setScale(this.id, v, v, v)
    else _creator.setScale(this.id, cx(v), cy(v), cz(v))
  }

  get quaternion(): Quat { return new Mat4(this._sync()).rotation }
  set quaternion(v: QuatLike) {
    _creator.setQuaternion(this.id, cx(v), cy(v), cz(v), cw(v))
    if (this._xf) this._setOwnedRotation()
  }

  get eulerAngles(): Vec3 { return new Mat4(this._sync()).eulerAngles }
  // order 1 = YXZ — the SDK's euler convention (math/quat.ts); the getter also extracts YXZ, so
  // the pair round-trips. (Historically this passed 0/XYZ AND the engine stored the euler matrix
  // transposed — the setter applied the INVERSE rotation. Both fixed 2026-07-10.)
  set eulerAngles(v: Vec3Like) {
    _creator.setEulerAngles(this.id, cx(v), cy(v), cz(v), 1)
    if (this._xf) this._setOwnedRotation()
  }

  // --- world-space reads ---
  get forward(): Vec3 {
    const v = new Float32Array(3)
    _creator.getWorldDirection(this.id, 2, v)
    return new Vec3(-v[0], -v[1], -v[2])
  }
  get worldPosition(): Vec3 {
    const v = new Float32Array(3)
    _creator.getWorldPosition(this.id, v)
    return new Vec3(v[0], v[1], v[2])
  }
  get worldScale(): Vec3 { return this.worldMatrix.scaling }
  get worldQuaternion(): Quat { return this.worldMatrix.rotation }
  get worldEulerAngles(): Vec3 { return this.worldMatrix.eulerAngles }

  // --- hierarchy ---
  get parent(): Node | null {
    const id = _creator.getParent(this.id)
    return id === 0 ? null : (nodeRegistry.get(id) ?? new Node(id))
  }
  get children(): Node[] {
    return _creator.getChildren(this.id).map((id) => nodeRegistry.get(id) ?? new Node(id))
  }
  get childCount(): number { return _creator.getChildCount(this.id) }
  getChild(index: number): Node | null {
    const id = _creator.getChild(this.id, index)
    return id === 0 ? null : (nodeRegistry.get(id) ?? new Node(id))
  }
  /** Parent the given nodes under this one. */
  add(...children: Node[]): this {
    for (const c of children) _creator.setParent(c.id, this.id, false)
    return this
  }
  setParent(parent: Node | null, worldPositionStays = false): this {
    if (parent === null) _creator.setParentNull(this.id, worldPositionStays)
    else _creator.setParent(this.id, parent.id, worldPositionStays)
    return this
  }
  traverse(callback: (node: Node) => void): void {
    callback(this)
    _creator.traverse(this.id, (id) => callback(nodeRegistry.get(id) ?? new Node(id)))
  }
  // --- materials (any renderable: a Mesh, or an internal node of a loaded Model) ---
  /** The material assigned to slot 0 through this API (null before one is set — a GLB part's own
   *  glTF material stays native-side). */
  get material(): Material | null { return this.getMaterial(0) }
  set material(m: Material) { this.setMaterial(m, 0) }
  /** Replace the material of one primitive slot (`index` = the primitive's order in the glTF
   *  mesh; a primitive-shape Mesh has one slot). */
  setMaterial(material: Material, index = 0): this {
    (this._materials ??= [])[index] = material
    _creator.setMaterial(this.id, Material.idOf(material), index)
    return this
  }
  /** The material previously assigned to `index` (null when none was — see Mesh for the probe). */
  getMaterial(index = 0): Material | null {
    return this._materials?.[index] ?? null
  }

  /** Find a descendant by name — bones of a loaded Model included (`hero.bone('RightHand').add(sword)`).
   *  Same rule the Animator binds clips with: exact name first, then the part after the last `:` / `|`
   *  (Mixamo `mixamorig:Hips` matches `Hips`); a skinned joint beats a plain node of the same name.
   *  Cached per name; null if absent. */
  bone(name: string): Node | null {
    const cache = (this._boneCache ??= new Map<string, Node | null>())
    const hit = cache.get(name)
    if (hit !== undefined) return hit
    const id = _creator.findNode ? _creator.findNode(this.id, name) : findNodeByWalk(this.id, name)
    const found = id ? nodeRegistry.get(id) ?? new Node(id) : null
    cache.set(name, found)
    return found
  }

  // --- behaviors are aspects: node.aspect(Shape, …) / node.aspect(Physics, …) ---

  /** Orient the node so the given axis (default "-z", "forward") points at a world point. */
  lookAt(point: Vec3Like, mode: "z" | "-z" | "x" | "-x" | "y" | "-y" = "-z", ortho: Vec3Like = [ 0, 1, 0 ]): this {
    const world = this.worldMatrix
    const pos = world.position
    let dir = new Vec3(point).sub(pos)
    const len = dir.length()
    if (len === 0) return this
    let axis: string = mode
    if (mode.startsWith("-")) { dir = dir.scale(-1 / len); axis = mode.slice(1) }
    else dir = dir.scale(1 / len)

    const o = new Vec3(ortho)
    let forward: Vec3, right: Vec3, up: Vec3
    if (axis === "x") {
      right = dir; up = o.cross(right).normalize(); forward = up.cross(right).normalize()
    } else if (axis === "y") {
      up = dir; right = o.cross(up).normalize(); forward = up.cross(right).normalize()
    } else {
      forward = dir; right = o.cross(dir).normalize(); up = dir.cross(right)
    }
    const s = world.scaling, w = world.m
    this.worldMatrix = Float32Array.of(
      right.x * s.x, right.y * s.x, right.z * s.x, 0,
      up.x * s.y, up.y * s.y, up.z * s.y, 0,
      forward.x * s.z, forward.y * s.z, forward.z * s.z, 0,
      w[12], w[13], w[14], w[15],
    )
    return this
  }

  // --- events ---
  override addEventListener<K extends keyof NodeEvents>(channel: K, callback: NodeEvents[K]): void {
    if (channel === "click") registerTouchEndEvent()
    else if (channel === "touchstart") registerTouchStartEvent()
    else if (channel === "enter" || channel === "exit") ensurePhysicsEvents()
    super.addEventListener(channel, callback)
  }

  /** @internal — called by the touch system to deliver a hit. */
  _dispatchEvent<K extends keyof NodeEvents>(channel: K, ...args: Parameters<NodeEvents[K]>): void {
    this.dispatch(channel, ...args)
  }

  /** @internal — emit a physics contact/trigger event (the physics-event router can't reach dispatch). */
  _emitCollision(channel: "enter" | "exit", other: Node): void { this.dispatch(channel, other) }


  /** Destroy this node and its whole subtree. Every aspect in the subtree is detached first (its
   *  `onDetach` runs: physics bodies released, updaters unregistered, listeners dropped) — native
   *  `destroyEntity` frees the entity tree but knows nothing about JS-side aspects, and a destroyed
   *  node's `update()` must not keep ticking. Deepest nodes go first, then this one. */
  destroy(): void {
    const subtree: Node[] = [this]
    _creator.traverse(this.id, (id) => { const n = nodeRegistry.get(id); if (n) subtree.push(n) })
    for (let i = subtree.length - 1; i >= 0; i--) {
      const n = subtree[i]
      n._detachAll()
      n._xf = 0
      n._xfKind = 0
      nodeRegistry.delete(n.id)
    }
    _creator.destroyEntity(this.id)
  }
}

/** Read the raw column-major elements out of a Mat4 wrapper or a plain length-16 array. */
const matArr = (m: Mat4Like): ArrayLike<number> => (m as { m?: ArrayLike<number> }).m ?? (m as ArrayLike<number>)
