/**
 * Fixed-timestep loop with a render interpolation factor.
 *
 * The simulation ticks at a constant 60 Hz regardless of display refresh. That
 * matters for two reasons beyond the usual determinism argument: the character
 * mover's ray-probe budget is tuned per tick, and the host-authoritative
 * networking in Milestone 2 needs a stable tick index to tag state snapshots.
 */

export const TICK_RATE = 60;
export const FIXED_DT = 1 / TICK_RATE;

/** Beyond this, we stop trying to catch up and let time slip (tab was hidden). */
const MAX_FRAME_TIME = 0.25;

export interface LoopCallbacks {
  /** Advance the simulation exactly one fixed step. */
  fixedUpdate(dt: number, tick: number): void;
  /** Draw. `alpha` is the 0..1 blend between the previous and current tick. */
  render(alpha: number, frameDt: number): void;
}

export class GameLoop {
  private running = false;
  private rafHandle = 0;
  private lastTime = 0;
  private accumulator = 0;
  private tick = 0;

  /** Smoothed frames per second, for the debug overlay. */
  fps = 0;

  constructor(private readonly callbacks: LoopCallbacks) {}

  start(): void {
    if (this.running) return;
    this.running = true;
    this.lastTime = performance.now();
    this.accumulator = 0;
    this.rafHandle = requestAnimationFrame(this.frame);
  }

  stop(): void {
    this.running = false;
    cancelAnimationFrame(this.rafHandle);
  }

  private readonly frame = (now: number): void => {
    if (!this.running) return;
    this.rafHandle = requestAnimationFrame(this.frame);

    const frameDt = Math.min((now - this.lastTime) / 1000, MAX_FRAME_TIME);
    this.lastTime = now;

    if (frameDt > 0) this.fps = this.fps * 0.9 + (1 / frameDt) * 0.1;

    this.accumulator += frameDt;
    while (this.accumulator >= FIXED_DT) {
      this.callbacks.fixedUpdate(FIXED_DT, this.tick++);
      this.accumulator -= FIXED_DT;
    }

    this.callbacks.render(this.accumulator / FIXED_DT, frameDt);
  };

  get currentTick(): number {
    return this.tick;
  }
}
