/**
 * GhostRecorder — records the blob root's position + uniform scale at 15Hz
 * between `race:started` and `race:finished`, and on a new best writes a
 * quantised, base64 track to localStorage `blobbo:ghost`.
 *
 * It is a fixed-step System (added via installGhost) so the 15Hz grid is
 * deterministic (every 4th 60Hz step) rather than tied to render cadence.
 *
 * Consumes: race:started, race:finished {time,best}.  Emits: nothing.
 */
import type { Blob, System, World } from '../contracts'
import {
  FLOATS_PER_SAMPLE,
  MAX_SAMPLES,
  RECORD_HZ,
  quantize,
  saveGhost,
  type GhostTrack,
} from './format'

export class GhostRecorder implements System {
  private readonly buf = new Float32Array(MAX_SAMPLES * FLOATS_PER_SAMPLE)
  private count = 0
  private acc = 0
  private recording = false
  private readonly period = 1 / RECORD_HZ

  constructor(
    private readonly world: World,
    private readonly blob: Blob,
  ) {
    world.events.on('race:started', () => this.begin())
    world.events.on('race:finished', (p) => this.finish(p))
  }

  private begin(): void {
    this.count = 0
    this.acc = 0
    this.recording = true
    this.capture() // anchor sample at t = 0
  }

  private capture(): void {
    if (this.count >= MAX_SAMPLES) {
      this.recording = false // 5-minute cap reached; keep what we have
      return
    }
    // The blob root (group) carries world position (synced to the rigid body
    // each fixed step) and the `size` uniform scale — exactly what we replay.
    const g = this.blob.group
    const o = this.count * FLOATS_PER_SAMPLE
    this.buf[o] = g.position.x
    this.buf[o + 1] = g.position.y
    this.buf[o + 2] = g.position.z
    this.buf[o + 3] = g.scale.x
    this.count++
  }

  update(dt: number): void {
    if (!this.recording) return
    this.acc += dt
    while (this.acc >= this.period) {
      this.acc -= this.period
      this.capture()
      if (!this.recording) break
    }
  }

  private finish(p?: { time?: number; best?: number }): void {
    if (!this.recording) return
    this.recording = false
    this.capture() // pin the exact finish pose

    const time = p?.time ?? this.count / RECORD_HZ
    const best = p?.best
    // game.ts sets `best` to the new time BEFORE emitting on a record, so a new
    // best arrives as time === best. `<=` catches that; a slower run has
    // best < time and is skipped.
    const isBest = best == null || time <= best + 1e-6
    if (!isBest || this.count < 2) return

    const track: GhostTrack = {
      time,
      hz: RECORD_HZ,
      count: this.count,
      data: quantize(this.buf, this.count),
    }
    saveGhost(track)
  }
}
