///|
using @dsp {type AudioBuffer, type DspContext}

///|
using @graph {
  type ControlBindingBuilder,
  type ControlBindingError,
  type ControlBindingMap,
  type CompiledDsp,
  type CompiledTemplate,
  type GraphControl,
  type GraphControlError,
}

///|
pub enum VoiceState {
  Idle
  Active
  Releasing
} derive(Eq, Debug)

///|
pub impl Show for VoiceState with output(self, logger) {
  logger.write_string(@debug.to_string(self))
}

///|
/// Generation-tagged voice handle.
/// WHY generation counter: slot indices are reused after voice stealing.
/// Without a generation tag, a stale handle from a stolen note could
/// accidentally gate_off or pan the wrong voice occupying that slot.
/// Every note_on increments the slot's generation; all operations compare
/// the handle's generation against the slot's current generation.
pub struct VoiceHandle {
  slot : Int
  generation : Int
} derive(Eq, Debug)

///|
pub impl Show for VoiceHandle with output(self, logger) {
  logger.write_string(@debug.to_string(self))
}

///|
pub(all) enum BoundVoicePoolError {
  InvalidMaxVoices
  OrphanAdsr
  CompileRejected
  Binding(ControlBindingError)
} derive(Eq, Debug)

///|
pub impl Show for BoundVoicePoolError with output(self, logger) {
  logger.write_string(@debug.to_string(self))
}

///|
/// Failure modes for VoicePool construction and template replacement.
/// Mirrors BoundVoicePoolError minus the Binding(...) variant
/// (VoicePool has no bindings).
pub(all) enum VoicePoolError {
  InvalidMaxVoices
  OrphanAdsr
  CompileRejected
} derive(Eq, Debug)

///|
pub impl Show for VoicePoolError with output(self, logger) {
  logger.write_string(@debug.to_string(self))
}

///|
/// Lift a VoicePoolError to a BoundVoicePoolError. Used by
/// BoundVoicePool's constructors after delegating to
/// validate_voice_template (which now returns VoicePoolError after
/// the boundary-type migration per ADR-0010).
pub fn BoundVoicePoolError::from_voice_pool(
  e : VoicePoolError,
) -> BoundVoicePoolError {
  match e {
    VoicePoolError::InvalidMaxVoices => BoundVoicePoolError::InvalidMaxVoices
    VoicePoolError::OrphanAdsr => BoundVoicePoolError::OrphanAdsr
    VoicePoolError::CompileRejected => BoundVoicePoolError::CompileRejected
  }
}

///|
pub(all) enum VoiceControlError {
  InvalidVoiceHandle(VoiceHandle)
  Graph(GraphControlError)
} derive(Debug)

///|
pub impl Show for VoiceControlError with output(self, logger) {
  logger.write_string(@debug.to_string(self))
}

///|
priv struct VoiceSlot {
  mut compiled : CompiledDsp?
  /// WHY per-slot snapshot: after set_template, the pool's
  /// adsr_authoring_indices changes. note_off must iterate the SAME
  /// indices used at note_on to gate the right ADSRs. Without this,
  /// gate_off would target wrong nodes (or miss freshly-orphaned ADSRs)
  /// if the new template differs.
  mut adsr_authoring_indices_snapshot : FixedArray[Int]
  mut state : VoiceState
  mut generation : Int
  mut allocation_order : Int
  /// WHY cached gains instead of a pan field: avoids 2 trig calls (cos + sin)
  /// per voice per block in process(). Updated only when pan changes
  /// (control-rate), not per block. The pan position is fully encoded by the
  /// gain pair — no separate pan field needed.
  mut pan_left_gain : Double
  mut pan_right_gain : Double
  mono_buffer : AudioBuffer
}

///|
#alias(new)
fn VoiceSlot::VoiceSlot(block_size : Int) -> VoiceSlot {
  {
    compiled: None,
    adsr_authoring_indices_snapshot: FixedArray::make(0, 0),
    state: VoiceState::Idle,
    generation: 0,
    allocation_order: 0,
    // Center pan: cos(π/4) = sin(π/4) ≈ 0.7071
    pan_left_gain: @dsp.pan_left_gain(0.0),
    pan_right_gain: @dsp.pan_right_gain(0.0),
    mono_buffer: AudioBuffer::filled(block_size),
  }
}

///|
fn VoiceSlot::is_handle_valid(self : VoiceSlot, handle : VoiceHandle) -> Bool {
  handle.generation == self.generation
}

///|
fn VoiceSlot::update_pan(self : VoiceSlot, pan : Double) -> Unit {
  let clamped = pan.clamp(min=-1.0, max=1.0)
  self.pan_left_gain = @dsp.pan_left_gain(clamped)
  self.pan_right_gain = @dsp.pan_right_gain(clamped)
}

///|
/// Gate on or off all surviving ADSR nodes using authoring indices.
/// WHY authoring indices: CompiledDsp::gate_on/gate_off accept original
/// authoring indices and internally map through index_map (which
/// accounts for optimizer elimination + topological reordering).
/// WHY unwrap: VoicePool construction rejects orphan ADSRs (see
/// validate_voice_template) and adsr_authoring_indices is a snapshot of
/// surviving ADSR indices only. Gate operations on those indices must
/// succeed; an Err here means an invariant has been violated.
fn gate_adsrs_at(
  compiled : CompiledDsp,
  authoring_indices : FixedArray[Int],
  gate_on : Bool,
) -> Unit {
  for n in 0.. VoiceSlot? {
  if handle.slot < 0 || handle.slot >= self.max_voices {
    return None
  }
  let slot = self.slots[handle.slot]
  if !slot.is_handle_valid(handle) {
    return None
  }
  Some(slot)
}

///|
pub struct VoicePool {
  priv slots : FixedArray[VoiceSlot]
  priv mut adsr_authoring_indices : FixedArray[Int]
  priv mut compiled_template : CompiledTemplate
  priv compile_context : DspContext
  priv mut next_allocation_order : Int
  priv max_voices : Int
  priv mut last_sanitized_count : Int
}

///|
fn validate_voice_template(
  compiled_template : CompiledTemplate,
  context : DspContext,
) -> Result[Unit, VoicePoolError] {
  if compiled_template.orphan_adsr_count() > 0 {
    return Err(VoicePoolError::OrphanAdsr)
  }
  // Sanity-compile to catch non-orphan problems (feedback cycle validation
  // etc.). Result discarded — per-voice slots compile their own on note_on.
  if CompiledDsp::compile(compiled_template, context) is None {
    return Err(VoicePoolError::CompileRejected)
  }
  Ok(())
}

///|
/// WHY test_compile: validates the template structurally (reachable Output
/// node, no invalid cycles, valid node indices) before accepting it. This
/// is a correctness gate so that per-voice note_on compiles can assume the
/// template is well-formed. The compiled result is discarded.
/// WHY orphan-ADSR reject: `VoicePool::note_on` calls `gate_on` for every
/// ADSR index in the template. If an ADSR is authored into the template
/// but not wired to Output, optimize_graph eliminates it and `gate_on`
/// would now return `Err(OrphanNode)`. Rejecting at pool-construction
/// time keeps the per-note `gate_adsrs_at(...)` loop infallible (it can
/// `.unwrap()`) and surfaces the misconfiguration at the earliest
/// observable point rather than at every note trigger.
/// WHY adsr_authoring_indices snapshot: the pool's long-lived snapshot
/// (used by note_on to gate ADSRs, and refreshed by set_template) stores
/// only the indices needed for gating — not the full template — so the
/// caller's CompiledTemplate can be freely discarded after VoicePool::new
/// returns. optimize_graph does not mutate its input, so neither
/// CompiledTemplate::analyze nor CompiledDsp::compile disturbs the
/// caller's array.
pub fn VoicePool::new(
  compiled_template : CompiledTemplate,
  context : DspContext,
  max_voices? : Int = 32,
) -> Result[VoicePool, VoicePoolError] {
  if max_voices <= 0 {
    return Err(VoicePoolError::InvalidMaxVoices)
  }
  match validate_voice_template(compiled_template, context) {
    Err(error) => Err(error)
    Ok(_) => {
      let block_size = context.block_size()
      let slots = FixedArray::makei(max_voices, _ => VoiceSlot::new(block_size))
      Ok({
        slots,
        adsr_authoring_indices: compiled_template.adsr_authoring_indices(),
        compiled_template,
        compile_context: context,
        next_allocation_order: 0,
        max_voices,
        last_sanitized_count: 0,
      })
    }
  }
}

///|
pub fn VoicePool::set_template(
  self : VoicePool,
  compiled_template : CompiledTemplate,
) -> Result[Unit, VoicePoolError] {
  match validate_voice_template(compiled_template, self.compile_context) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  self.compiled_template = compiled_template
  self.adsr_authoring_indices = compiled_template.adsr_authoring_indices()
  Ok(())
}

///|
pub struct BoundVoicePool {
  priv pool : VoicePool
  priv mut bindings : ControlBindingMap
}

///|
pub fn BoundVoicePool::new(
  compiled_template : CompiledTemplate,
  context : DspContext,
  bindings : ControlBindingBuilder,
  max_voices? : Int = 32,
) -> Result[BoundVoicePool, BoundVoicePoolError] {
  let pool = match VoicePool::new(compiled_template, context, max_voices~) {
    Ok(pool) => pool
    Err(error) => return Err(BoundVoicePoolError::from_voice_pool(error))
  }
  match bindings.build(compiled_template) {
    Ok(bindings) => Ok({ pool, bindings })
    Err(error) => Err(BoundVoicePoolError::Binding(error))
  }
}

///|
pub fn BoundVoicePool::set_template(
  self : BoundVoicePool,
  compiled_template : CompiledTemplate,
  bindings : ControlBindingBuilder,
) -> Result[Unit, BoundVoicePoolError] {
  match validate_voice_template(compiled_template, self.pool.compile_context) {
    Err(error) => return Err(BoundVoicePoolError::from_voice_pool(error))
    Ok(_) => ()
  }
  let next_bindings = match bindings.build(compiled_template) {
    Ok(bindings) => bindings
    Err(error) => return Err(BoundVoicePoolError::Binding(error))
  }
  self.pool.adsr_authoring_indices = compiled_template.adsr_authoring_indices()
  self.pool.compiled_template = compiled_template
  self.bindings = next_bindings
  Ok(())
}

///|
pub fn BoundVoicePool::note_on_controls(
  self : BoundVoicePool,
  controls : Map[String, Double],
) -> VoiceHandle? {
  self.pool.note_on(self.bindings.resolve_controls(controls))
}

///|
pub fn BoundVoicePool::note_off(
  self : BoundVoicePool,
  handle : VoiceHandle,
) -> Bool {
  self.pool.note_off(handle)
}

///|
pub fn BoundVoicePool::kill(
  self : BoundVoicePool,
  handle : VoiceHandle,
) -> Bool {
  self.pool.kill(handle)
}

///|
pub fn BoundVoicePool::note_off_all(self : BoundVoicePool) -> Unit {
  self.pool.note_off_all()
}

///|
pub fn BoundVoicePool::set_voice_pan(
  self : BoundVoicePool,
  handle : VoiceHandle,
  pan : Double,
) -> Bool {
  self.pool.set_voice_pan(handle, pan)
}

///|
/// Validate a live control batch against an active voice without mutating it.
pub fn BoundVoicePool::validate_voice_controls_result(
  self : BoundVoicePool,
  handle : VoiceHandle,
  controls : Array[GraphControl],
) -> Result[Unit, VoiceControlError] {
  self.pool.validate_voice_controls_result(handle, controls)
}

///|
/// Apply one validated graph control to an already-sounding voice.
pub fn BoundVoicePool::apply_voice_control_result(
  self : BoundVoicePool,
  handle : VoiceHandle,
  control : GraphControl,
) -> Result[Unit, VoiceControlError] {
  self.pool.apply_voice_controls_result(handle, [control])
}

///|
/// Apply a graph-control batch transactionally to an already-sounding voice.
pub fn BoundVoicePool::apply_voice_controls_result(
  self : BoundVoicePool,
  handle : VoiceHandle,
  controls : Array[GraphControl],
) -> Result[Unit, VoiceControlError] {
  self.pool.apply_voice_controls_result(handle, controls)
}

///|
pub fn BoundVoicePool::voice_state(
  self : BoundVoicePool,
  handle : VoiceHandle,
) -> VoiceState {
  self.pool.voice_state(handle)
}

///|
pub fn BoundVoicePool::active_voice_count(self : BoundVoicePool) -> Int {
  self.pool.active_voice_count()
}

///|
pub fn BoundVoicePool::last_sanitized_count(self : BoundVoicePool) -> Int {
  self.pool.last_sanitized_count()
}

///|
pub fn BoundVoicePool::process(
  self : BoundVoicePool,
  context : DspContext,
  left : AudioBuffer,
  right : AudioBuffer,
) -> Unit {
  self.pool.process(context, left, right)
}

///|
pub fn VoicePool::active_voice_count(self : VoicePool) -> Int {
  let mut count = 0
  for i = 0; i < self.max_voices; i = i + 1 {
    if !(self.slots[i].state is VoiceState::Idle) {
      count = count + 1
    }
  }
  count
}

///|
/// Number of non-finite samples replaced with 0.0 during the most recent
/// process() call, across both L and R output channels after mixdown.
pub fn VoicePool::last_sanitized_count(self : VoicePool) -> Int {
  self.last_sanitized_count
}

///|
/// WHY three-tier priority: Idle slots are free — no audible cost.
/// Releasing voices are already fading out, so stealing them causes minimal
/// pop. Active voices are a last resort — cutting them is audible but
/// unavoidable at full polyphony. Within each tier, oldest-first ensures
/// the most recently played notes survive longest.
fn VoicePool::find_slot(self : VoicePool) -> Int {
  // Priority 1: any idle slot (free)
  for i = 0; i < self.max_voices; i = i + 1 {
    if self.slots[i].state is VoiceState::Idle {
      return i
    }
  }
  // Priority 2: oldest releasing voice (least audible pop)
  let mut best_releasing = -1
  let mut best_releasing_order = @int.MAX_VALUE
  for i = 0; i < self.max_voices; i = i + 1 {
    if self.slots[i].state is VoiceState::Releasing &&
      self.slots[i].allocation_order < best_releasing_order {
      best_releasing = i
      best_releasing_order = self.slots[i].allocation_order
    }
  }
  if best_releasing >= 0 {
    return best_releasing
  }
  // Priority 3: oldest active voice (audible cut, last resort)
  let mut best_active = 0
  let mut best_active_order = @int.MAX_VALUE
  for i = 0; i < self.max_voices; i = i + 1 {
    if self.slots[i].allocation_order < best_active_order {
      best_active = i
      best_active_order = self.slots[i].allocation_order
    }
  }
  best_active
}

///|
/// Reassign allocation_order values to a dense sequential range (0, 1, 2, ...)
/// preserving relative ordering. Called when next_allocation_order is about to
/// overflow. This keeps the oldest-first voice-stealing heuristic correct.
fn VoicePool::compact_allocation_orders(self : VoicePool) -> Unit {
  // Collect (slot_index, allocation_order) pairs for non-idle slots
  let pairs : Array[(Int, Int)] = []
  for i = 0; i < self.max_voices; i = i + 1 {
    if !(self.slots[i].state is VoiceState::Idle) {
      pairs.push((i, self.slots[i].allocation_order))
    }
  }
  // Sort by allocation_order ascending to preserve relative age
  pairs.sort_by(fn(a, b) { a.1.compare(b.1) })
  // Reassign sequential orders starting from 0
  for j = 0; j < pairs.length(); j = j + 1 {
    self.slots[pairs[j].0].allocation_order = j
  }
  // Set pool counter to next available value
  self.next_allocation_order = pairs.length()
}

///|
pub fn VoicePool::note_on(
  self : VoicePool,
  params : Array[GraphControl],
) -> VoiceHandle? {
  let slot_index = self.find_slot()
  let slot = self.slots[slot_index]
  let compiled = match
    CompiledDsp::compile(self.compiled_template, self.compile_context) {
    Some(c) => c
    None => return None
  }
  // WHY transactional: if any param is invalid, the voice is not activated.
  // This prevents partially-configured voices with default/stale settings.
  if params.length() > 0 {
    if compiled.apply_controls(params) is Err(_) {
      return None
    }
  }
  gate_adsrs_at(compiled, self.adsr_authoring_indices, true)
  slot.compiled = Some(compiled)
  // WHY snapshot + copy: note_off needs the SAME indices that were used at
  // note_on to gate the right ADSRs. .copy() is load-bearing — without it,
  // a subsequent set_template would mutate this slot's snapshot and the
  // already-sounding voice would gate_off against the wrong index set.
  // See VoiceSlot.adsr_authoring_indices_snapshot doc comment.
  slot.adsr_authoring_indices_snapshot = self.adsr_authoring_indices.copy()
  slot.state = VoiceState::Active
  // WHY overflow guard on generation: after 2^31 increments the counter
  // wraps negative, which could revalidate stale VoiceHandles. Resetting
  // to 0 safely invalidates all outstanding handles for this slot.
  slot.generation = if slot.generation >= @int.MAX_VALUE - 1 {
    0
  } else {
    slot.generation + 1
  }
  slot.allocation_order = self.next_allocation_order
  // WHY overflow guard on allocation_order: if the pool-wide counter wraps
  // negative, the oldest-first stealing heuristic breaks. Compact all slot
  // orders into a dense sequential range preserving relative order.
  if self.next_allocation_order >= @int.MAX_VALUE - 1 {
    self.compact_allocation_orders()
  } else {
    self.next_allocation_order = self.next_allocation_order + 1
  }
  Some({ slot: slot_index, generation: slot.generation })
}

///|
pub fn VoicePool::note_off(self : VoicePool, handle : VoiceHandle) -> Bool {
  match self.resolve_slot(handle) {
    None => false
    Some(slot) => {
      if slot.state is VoiceState::Idle {
        return false
      }
      match slot.compiled {
        // WHY adsr_authoring_indices_snapshot: uses the indices from
        // note_on time, not the current self.adsr_authoring_indices which
        // may have changed via set_template.
        Some(compiled) =>
          gate_adsrs_at(compiled, slot.adsr_authoring_indices_snapshot, false)
        None => ()
      }
      slot.state = VoiceState::Releasing
      true
    }
  }
}

///|
fn VoicePool::kill(self : VoicePool, handle : VoiceHandle) -> Bool {
  match self.resolve_slot(handle) {
    None => false
    Some(slot) => {
      if slot.state is VoiceState::Idle {
        return false
      }
      slot.compiled = None
      slot.state = VoiceState::Idle
      true
    }
  }
}

///|
pub fn VoicePool::note_off_all(self : VoicePool) -> Unit {
  for i = 0; i < self.max_voices; i = i + 1 {
    let slot = self.slots[i]
    if slot.state is VoiceState::Active {
      match slot.compiled {
        Some(compiled) =>
          gate_adsrs_at(compiled, slot.adsr_authoring_indices_snapshot, false)
        None => ()
      }
      slot.state = VoiceState::Releasing
    }
  }
}

///|
pub fn VoicePool::set_voice_pan(
  self : VoicePool,
  handle : VoiceHandle,
  pan : Double,
) -> Bool {
  match self.resolve_slot(handle) {
    None => false
    Some(slot) => {
      slot.update_pan(pan)
      true
    }
  }
}

///|
fn VoicePool::compiled_voice_result(
  self : VoicePool,
  handle : VoiceHandle,
) -> Result[CompiledDsp, VoiceControlError] {
  match self.resolve_slot(handle) {
    None => Err(VoiceControlError::InvalidVoiceHandle(handle))
    Some(slot) =>
      if slot.state is VoiceState::Idle {
        Err(VoiceControlError::InvalidVoiceHandle(handle))
      } else {
        match slot.compiled {
          Some(compiled) => Ok(compiled)
          None => Err(VoiceControlError::InvalidVoiceHandle(handle))
        }
      }
  }
}

///|
fn VoicePool::validate_voice_controls_result(
  self : VoicePool,
  handle : VoiceHandle,
  controls : Array[GraphControl],
) -> Result[Unit, VoiceControlError] {
  let compiled = match self.compiled_voice_result(handle) {
    Ok(compiled) => compiled
    Err(error) => return Err(error)
  }
  match compiled.validate_controls(controls) {
    Ok(_) => Ok(())
    Err(error) => Err(VoiceControlError::Graph(error))
  }
}

///|
fn VoicePool::apply_voice_controls_result(
  self : VoicePool,
  handle : VoiceHandle,
  controls : Array[GraphControl],
) -> Result[Unit, VoiceControlError] {
  let compiled = match self.compiled_voice_result(handle) {
    Ok(compiled) => compiled
    Err(error) => return Err(error)
  }
  match compiled.apply_controls(controls) {
    Ok(_) => Ok(())
    Err(error) => Err(VoiceControlError::Graph(error))
  }
}

///|
pub fn VoicePool::voice_state(
  self : VoicePool,
  handle : VoiceHandle,
) -> VoiceState {
  match self.resolve_slot(handle) {
    None => VoiceState::Idle
    Some(slot) => slot.state
  }
}

///|
pub fn VoicePool::process(
  self : VoicePool,
  context : DspContext,
  left_output : AudioBuffer,
  right_output : AudioBuffer,
) -> Unit {
  let sample_count = if context.block_size() < left_output.length() {
    context.block_size()
  } else {
    left_output.length()
  }
  for i = 0; i < self.max_voices; i = i + 1 {
    let slot = self.slots[i]
    if slot.state is VoiceState::Idle {
      continue i + 1
    }
    match slot.compiled {
      None => {
        slot.state = VoiceState::Idle
        continue i + 1
      }
      Some(compiled) => {
        compiled.process(context, slot.mono_buffer)
        // WHY cached gains: pan_left_gain/pan_right_gain are updated only
        // when set_voice_pan is called (control-rate), not here (audio-rate).
        // This avoids 2 trig calls per voice per block.
        let left_gain = slot.pan_left_gain
        let right_gain = slot.pan_right_gain
        for s = 0; s < sample_count; s = s + 1 {
          let mono = slot.mono_buffer.get(s)
          left_output.set(s, left_output.get(s) + mono * left_gain)
          right_output.set(s, right_output.get(s) + mono * right_gain)
        }
        if slot.state is VoiceState::Releasing && compiled.is_voice_finished() {
          slot.state = VoiceState::Idle
          slot.compiled = None
        }
      }
    }
  }
  self.last_sanitized_count = @dsp.sanitize_buffer(left_output, sample_count) +
    @dsp.sanitize_buffer(right_output, sample_count)
}