// The game clock: one place that owns "how fast does the game run" — `Time.scale` (slow-motion, 0.5),
// `Time.paused` (a menu over a frozen world) — and what a frame is worth (`Time.dt`).
//
// The aspect dispatcher (core/Aspect.ts) calls `_beginFrame(rawDt)` once per frame with the host's
// wall-clock delta; `update*(dt)` callbacks then receive the SCALED delta (0 while paused). The same
// scale is pushed to every engine that registered a sink (creator-gl / creator-2d `setTimeScale`),
// so physics, animators and particles slow down and freeze together with the game code — a paused
// aspect and a paused rigid body agree. `Time.unscaledDt` is the wall-clock delta for things that
// must keep moving while the game is paused (a HUD fade, a pause-menu animation): those aspects set
// `updateWhilePaused = true` and read it themselves, exactly like Unity's unscaledDeltaTime.

type ScaleSink = (scale: number) => void
const sinks: ScaleSink[] = []

class TimeClock {
  private _scale = 1
  private _paused = false
  /** The fixed step: what `updateFixed(dt)` receives, every time, on every machine (1/60). Under
   *  `scale` the NUMBER of fixed steps per frame changes, never this value. */
  readonly fixedDt = 1 / 60
  /** Seconds of GAME time since the app started (scaled; stops while paused). */
  now = 0
  /** Seconds of wall-clock time since the app started (never stops). */
  unscaledNow = 0
  /** This frame's delta in game seconds — what `update(dt)` receives. 0 while paused. */
  dt = 0
  /** This frame's wall-clock delta in seconds (the host's frame time, clamped by the engine). */
  unscaledDt = 0
  /** Frames dispatched since the app started (counts paused frames too). */
  frame = 0

  /** Speed of the game relative to wall-clock: 1 normal, 0.5 half speed, 2 double. Applied to
   *  `dt`, `now` AND the engines' physics / animation / particle clocks. Negative or NaN → 0. */
  get scale(): number { return this._scale }
  set scale(v: number) {
    const s = Number.isFinite(v) && v > 0 ? v : 0
    if (s === this._scale) return
    this._scale = s
    this._push()
  }

  /** Freeze the game: `dt` reads 0, aspects without `updateWhilePaused` are skipped, the engines'
   *  simulations stop. `scale` is remembered and restored on resume. */
  get paused(): boolean { return this._paused }
  set paused(v: boolean) {
    if (v === this._paused) return
    this._paused = v
    this._push()
  }

  /** The scale the engines currently run at (0 while paused). */
  get effectiveScale(): number { return this._paused ? 0 : this._scale }

  private _push(): void {
    const s = this.effectiveScale
    for (const sink of sinks) sink(s)
  }

  /** @internal Called by the aspect dispatcher at the top of every frame with the host's raw delta. */
  _beginFrame(rawDt: number): void {
    const raw = Number.isFinite(rawDt) && rawDt > 0 ? rawDt : 0
    this.frame++
    this.unscaledNow += raw
    this.now += this._phaseDt(raw)
  }
  /** @internal A phase's scaled delta from its raw one; records both as this frame's `dt`. */
  _phaseDt(rawDt: number): number {
    const raw = Number.isFinite(rawDt) && rawDt > 0 ? rawDt : 0
    this.unscaledDt = raw
    this.dt = raw * this.effectiveScale
    return this.dt
  }
}

/** The game clock — scale / pause the whole game, read `dt` / `now` anywhere. */
export const Time = new TimeClock()

/** @internal Engine layers (Scene / Scene2D) register the bridge call that forwards the effective
 *  scale to their native clocks. Pushed immediately with the current value so a scene created after
 *  `Time.paused = true` starts frozen. */
export const _registerTimeScaleSink = (sink: ScaleSink): void => {
  sinks.push(sink)
  sink(Time.effectiveScale)
}
