///|
/// High-level audio runtime that integrates the mixer with backend hooks.
pub(all) struct AudioRuntime {
  mixer : Mixer
  output_buffer : FixedArray[Float]
  frames_per_tick : Int
  mut tick_count : Int
  mut initialized : Bool
  command_queue : CommandQueue
  latency_monitor : LatencyMonitor
}

///|
/// Create a new audio runtime.
/// sample_rate: output sample rate (e.g. 44100)
/// tps: ticks per second (e.g. 60 for 60fps game loop)
pub fn new_audio_runtime(sample_rate : Int, tps : Int) -> AudioRuntime {
  let frames_per_tick = sample_rate / tps
  let buffer_size = frames_per_tick * 2 // stereo
  AudioRuntime::{
    mixer: new_mixer(sample_rate),
    output_buffer: FixedArray::make(buffer_size, 0.0),
    frames_per_tick,
    tick_count: 0,
    initialized: false,
    command_queue: new_command_queue(),
    latency_monitor: new_latency_monitor(),
  }
}

///|
/// Run one game tick: flush commands, mix audio, write to backend, track latency.
pub fn audio_tick(rt : AudioRuntime) -> Unit {
  if not(rt.initialized) {
    let hooks = get_audio_backend_hooks()
    (hooks.initialize)(rt.mixer.sample_rate, rt.mixer.channels)
    rt.initialized = true
  }
  // Flush pending commands
  flush_commands(rt.command_queue, rt.mixer)
  // Mix audio
  tick(rt.mixer, rt.frames_per_tick, rt.output_buffer)
  let hooks = get_audio_backend_hooks()
  (hooks.write)(rt.output_buffer, rt.frames_per_tick)
  // Track latency
  let consumed = (hooks.report_consumed)()
  update_latency(rt.latency_monitor, rt.frames_per_tick, consumed)
  rt.tick_count = rt.tick_count + 1
  collect_stopped(rt.mixer)
}

///|
/// Play a sound buffer through the runtime. Returns voice ID.
pub fn play_sound(
  rt : AudioRuntime,
  buffer : AudioBuffer,
  gain? : Float = 1.0,
  pan? : Float = 0.0,
  looping? : Bool = false,
) -> VoiceId {
  let voice = new_voice_from_buffer(rt.mixer, buffer, gain~, pan~, looping~)
  add_voice(rt.mixer, voice)
}