// The Aspect system: attachable capabilities (animation, collider, physics, and user behaviors)
// that hang off a node WITHOUT bloating the node class. Composition with the terseness of methods.
//
//   const hero = new Sprite({ texture })
//     .aspect(SpriteAnimation, { fps: 8, clips: { walk: [1, 2, 3] } })   // attach + configure
//     .aspect(Shape2D, { box: [16, 8] })                                 // chains: returns the node
//   hero.anim.play('walk')          // access by name (the chain result is typed as having it)
//   if (hero.has(Physics)) hero.physics.velocity = [0, 0]                // has() is a type guard
//
// A user aspect declares its name + target node in the generic and reads `this.node`:
//   class Health extends Aspect<'health', Sprite> { hp = 100; hurt(n: number) { this.hp -= n } }
//   sprite.aspect(Health, { hp: 80 }).health.hurt(10)
//
// The accessor name lives in the generic (Aspect<'name', Node>). The `chisel` bundler extracts it at
// compile time and injects the runtime `static aspect` on user aspects + the virtual type accessor;
// the SDK's own built-in aspects declare `static readonly aspect` directly (see SpriteAnimation etc.).

import { Emitter, type EventMap } from "./events"
import type { Node } from "../gl/Node"

// ---- type helpers -----------------------------------------------------------
/** The class object of an aspect — accepted even when its constructor is protected/abstract. */
export type AspectCtor<A extends Aspect<any, any>> = abstract new () => A
/** The accessor name carried in the aspect's generic. */
export type KeyOf<A> = A extends Aspect<infer K, any> ? K : never
/** The node kind an aspect targets. */
export type TargetOf<A> = A extends Aspect<any, infer P> ? P : never
/** `{ name: Aspect }` — the field an aspect contributes to its node. */
export type FieldOf<A extends Aspect<any, any>> = { [P in KeyOf<A>]: A }

type UnionToIntersection<U> =
  (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never

/**
 * A variable typed as a node KNOWN to carry the given aspect(s). Union, not a tuple — reads as
 * English and mirrors the runtime guard `node.has(Health)`:
 *   let boss: With<Sprite, Health | Physics>
 */
export type With<N, A extends Aspect<any, any>> =
  N & UnionToIntersection<A extends any ? FieldOf<A> : never>

// ---- per-frame update dispatch (two phases) ---------------------------------
// Aspects that define update(dt) are ticked every frame in ONE of two phases, chosen by the aspect's
// `updateBeforePhysics` field:
//   • LATE (default): after the physics step + transform sync, right before the frame draws. Reads of
//     node.worldPosition are the FINAL drawn position — cameras/followers land exactly, no 1-frame lag.
//   • EARLY (updateBeforePhysics = true): before the physics step, so writing velocity/force/kinematic
//     transforms is consumed by the SAME frame's step (zero input latency).
// The FRAME SOURCE is pluggable so core never names a specific engine bridge (a 3D-only bundle must not
// pull in the 2D one): the 2D layer installs a render-synced source via _installAspectFrames (the native
// c2dRender early/late hooks), so dispatch is in lockstep with the draw. With none installed (3D-only,
// or a runtime without the hooks) it falls back to a single host setLoop, early-then-late — no true
// post-physics slot. Both lists stay sorted by the aspect's `order` field (default 0; ties keep
// insertion order). Lazy: the first update-bearing aspect installs the dispatch; a project with none
// starts nothing. No visibility culling yet — every registered aspect ticks regardless of on-screen state.
type Updatable = Aspect<any, any>
type PhaseFn = (dt: number) => void
const earlyUpdaters: Updatable[] = []
const lateUpdaters: Updatable[] = []
let dispatchInstalled = false

// A render-synced frame source: given the two phase runners, wire them to the engine and return true;
// return false if unavailable (→ setLoop fallback). Installed by the 2D layer (see Scene2D) so core
// never references a concrete engine bridge — that keeps the 2D bridge out of a 3D-only bundle.
type FrameInstaller = (early: PhaseFn, late: PhaseFn) => boolean
let frameInstaller: FrameInstaller | undefined
/** @internal Install a render-synced frame source for aspect update(dt). Called from the 2D layer. */
export const _installAspectFrames = (fn: FrameInstaller): void => { frameInstaller = fn }

const policyOf = (a: Updatable): { early: boolean; order: number } =>
  ({ early: a.updateBeforePhysics === true, order: a.order ?? 0 })

// Stable insert by ascending order: place after the last element whose order is <= this one's.
const addSorted = (list: Updatable[], a: Updatable, order: number): void => {
  let i = list.length
  while (i > 0 && list[i - 1].order > order) i--
  list.splice(i, 0, a)
}

const runPhase = (list: Updatable[], dt: number): void => {
  // iterate a snapshot so an update() that attaches/detaches aspects can't corrupt this frame's pass
  for (const a of list.slice()) a.update?.(dt)
}

const ensureDispatch = (): void => {
  if (dispatchInstalled) return
  dispatchInstalled = true
  const early: PhaseFn = (dt) => runPhase(earlyUpdaters, dt)
  const late: PhaseFn = (dt) => runPhase(lateUpdaters, dt)
  // render-synced source if the engine layer installed one; else one host loop (early then late).
  if (!(frameInstaller && frameInstaller(early, late))) {
    setLoop((dt) => { early(dt); late(dt) })
  }
}

const registerUpdater = (a: Updatable): void => {
  const { early, order } = policyOf(a)
  addSorted(early ? earlyUpdaters : lateUpdaters, a, order)
  ensureDispatch()
}
const unregisterUpdater = (a: Updatable): void => {
  let i = earlyUpdaters.indexOf(a)
  if (i >= 0) { earlyUpdaters.splice(i, 1); return }
  i = lateUpdaters.indexOf(a)
  if (i >= 0) lateUpdaters.splice(i, 1)
}

// ---- the host: mixed into every node kind (Node2D, Node) --------------------
/**
 * Base for anything that can carry aspects. Extends Emitter, so node kinds get events too. Provides
 * the four verbs; the named accessor (node.physics, node.health, …) is set as an own property at
 * attach time, keyed by the aspect class's runtime `aspect` name.
 */
export abstract class AspectHost<E extends EventMap = EventMap> extends Emitter<E> {
  /** @internal class → instance, the authoritative store (named accessors mirror this). */
  readonly _aspects = new Map<Function, Aspect<any, any>>()

  /** Attach (and configure) an aspect, or reconfigure it if already present. Returns the node typed
   *  as now-having that aspect (so no guard/`?.` is needed afterwards). Rejects a wrong target node. */
  aspect<Self extends TargetOf<A>, A extends Aspect<any, any>>(
    this: Self,
    ctor: AspectCtor<A>,
    opts?: Partial<A>,
  ): Self & FieldOf<A> {
    const host = this as unknown as AspectHost
    let inst = host._aspects.get(ctor) as A | undefined
    const fresh = inst === undefined
    if (!inst) {
      inst = new (ctor as unknown as new () => A)()
      ;(inst as { node: unknown }).node = this
      host._aspects.set(ctor, inst)
      const name = (ctor as { aspect?: string }).aspect
      if (name) (host as unknown as Record<string, unknown>)[name] = inst
    }
    if (opts) Object.assign(inst, opts)
    if (fresh) {
      inst.onAttach?.()
      if (typeof inst.update === "function") registerUpdater(inst)
    }
    return this as Self & FieldOf<A>
  }

  /** Safe access — undefined if the aspect isn't attached. */
  get<A extends Aspect<any, any>>(ctor: AspectCtor<A>): A | undefined {
    return this._aspects.get(ctor) as A | undefined
  }

  /** Existence check AND type guard: inside `if (node.has(Physics))`, `node.physics` is present. */
  has<A extends Aspect<any, any>>(ctor: AspectCtor<A>): this is this & FieldOf<A> {
    return this._aspects.has(ctor)
  }

  /** Detach an aspect (runs its onDetach). Named `removeAspect` so it never collides with the
   *  child-management `add`/`remove` semantics some node kinds expose. */
  removeAspect<A extends Aspect<any, any>>(ctor: AspectCtor<A>): this {
    const inst = this._aspects.get(ctor)
    if (inst) {
      inst.onDetach?.()
      unregisterUpdater(inst)
      this._aspects.delete(ctor)
      const name = (ctor as { aspect?: string }).aspect
      if (name) delete (this as unknown as Record<string, unknown>)[name]
    }
    return this
  }
}

// ---- the aspect base --------------------------------------------------------
/**
 * Base for everything you attach. `K` = accessor name (extracted by chisel for the runtime). `P` =
 * the target node kind: `this.node` is typed to it, AND attaching to a wrong node is a compile error.
 * Initialize in `onAttach` (the node is set by then) — not a constructor; aspects are created by the
 * engine via `node.aspect()`, never `new`.
 */
export abstract class Aspect<K extends string, P = Node> {
  // NOTE: P has no `extends AspectHost` constraint on purpose — Emitter<E>'s generic
  // addEventListener makes a specific-event node (AspectHost<NodeEvents>) not assignable to
  // AspectHost<any>, which would reject every real node. P only types `this.node`; target
  // enforcement happens in aspect() via `this: Self extends TargetOf<A>`. Defaults to Node
  // (3D is the default engine) — a 2D aspect declares its own P, e.g. Aspect<'x', Sprite>.
  /** The node this aspect is attached to (set at attach time). */
  readonly node!: P
  /** @internal phantom — lets the type system recover the accessor name `K`. Never read at runtime. */
  declare protected readonly __key__?: K
  onAttach?(): void
  onDetach?(): void
  /**
   * Called every frame while attached (dt = seconds since the last frame). Render-synced: runs in the
   * LATE phase by default — after the physics step + transform sync, right before the frame draws — so
   * reads of `node.worldPosition` are the final drawn position (cameras/followers have no 1-frame lag).
   * Set `updateBeforePhysics = true` (an instance field) to run in the EARLY phase (before the step)
   * instead — for aspects that WRITE velocity/force/kinematic transforms and want the same frame's
   * step to consume them. Cameras/followers READ, so they stay in the default LATE phase.
   */
  update?(dt: number): void

  /** Run update(dt) in the EARLY phase (before the physics step) rather than the default LATE phase.
   *  An instance field (like `updateWhenVisible`) — set it as a class field: `updateBeforePhysics = true`.
   *  Use only when you FEED the simulation (velocity/force/kinematic transform). Read once, at attach. */
  updateBeforePhysics = false
  /** Tick order within a phase — ascending; default 0, ties keep attach order. Read once, at attach. */
  order = 0

  /** Opt-in: only run update(dt) while the node is on-screen. NOOP for now — visibility culling isn't
   *  wired yet, so every updater ticks regardless; declared so aspects can opt in ahead of it. */
  updateWhenVisible = false
}
