// Sprite-sheet animation as an aspect on a Sprite. Configured as DATA (frame size + named clips) in
// the attach call, then driven by name:
//   sprite.aspect(SpriteAnimation, { size: [32, 48], fps: 10, clips: { idle: [0,1], walk: [4,5,6,7] } })
//   sprite.anim.play('walk')
// Once defined, clips advance entirely in native code; the aspect just translates clip ids back to
// names for the node's 'loopReached' / 'completed' events.

import { Aspect } from "../core/Aspect"
import { ensureAnimEvents } from "./loop"
import type { Sprite } from "./Sprite"

/** Per-clip frame list, or an object for per-clip fps/loop overrides. */
export type Clip = number[] | { frames: number[], fps?: number, loop?: boolean }

export class SpriteAnimation extends Aspect<"anim", Sprite> {
  static readonly aspect = "anim"

  /** Grid cell size in texture pixels (defaults to the full texture). */
  size?: [number, number]
  /** Default fps for clips that don't override it. */
  fps = 12
  /** Default loop for clips that don't override it. */
  loop = true
  /** Named clips; frame indices are row-major into the grid. Set via the attach opts. */
  clips: Record<string, Clip> = {}

  private _clipIds: Record<string, number> = {}
  private _clipNames: Record<number, string> = {}
  private _current: string | null = null

  onAttach(): void {
    this._define()
    // route native animation events for this entity to named 'loopReached'/'completed' events.
    ;(this.node as { _dispatchAnimEvent: (clipId: number, type: number) => void })._dispatchAnimEvent =
      (clipId, type) => this.node._emitAnim(type === 1 ? "completed" : "loopReached", this._clipNames[clipId] ?? "")
    ensureAnimEvents()
  }

  /** (Re)build native clips from `this.clips`. Called on attach; call again after changing clips. */
  define(): this { this._define(); return this }

  private _define(): void {
    const node = this.node
    const tex = node.texture
    if (!tex) throw new Error("SpriteAnimation: set a texture on the Sprite before adding the aspect")
    const [ fw, fh ] = this.size ?? [ tex.width, tex.height ]
    const cols = Math.max(1, Math.floor(tex.width / fw))

    for (const name of Object.keys(this.clips)) {
      const value = this.clips[name]
      const indices = Array.isArray(value) ? value : value.frames
      const fps = Array.isArray(value) ? this.fps : (value.fps ?? this.fps)
      const loop = Array.isArray(value) ? this.loop : (value.loop ?? this.loop)

      const frames = new Float32Array(indices.length * 4)
      indices.forEach((idx, i) => {
        const px = (idx % cols) * fw
        const py = Math.floor(idx / cols) * fh
        frames[i * 4 + 0] = px / tex.width
        frames[i * 4 + 1] = py / tex.height
        frames[i * 4 + 2] = (px + fw) / tex.width
        frames[i * 4 + 3] = (py + fh) / tex.height
      })
      const clipId = _creator2d.defineAnimation(node.id, frames, fps, loop)
      this._clipIds[name] = clipId
      this._clipNames[clipId] = name
    }
    if (this.size) _creator2d.setSpriteSize(node.id, fw, fh)
  }

  /** Play a clip by name. Re-playing the active clip is a no-op (keeps it running smoothly). */
  play(name: string): this {
    const id = this._clipIds[name]
    if (id === undefined) throw new Error(`SpriteAnimation.play(): unknown clip "${name}"`)
    if (this._current === name) return this
    this._current = name
    _creator2d.playAnimation(this.node.id, id)
    return this
  }

  stop(): this {
    this._current = null
    _creator2d.stopAnimation(this.node.id)
    return this
  }

  set speed(value: number) { _creator2d.setAnimationSpeed(this.node.id, value) }
  get current(): string | null { return this._current }
  get frame(): number { return _creator2d.getAnimationFrame(this.node.id) }
}
