///|
/// Shared hot-swap state for both mono and stereo compiled graphs.
///
/// Mono uses `old_output`/`new_output` (stereo buffers are zero-length).
/// Stereo uses `old_left`/`old_right`/`new_left`/`new_right` (mono buffers
/// are zero-length). Zero-length rather than Option so crossfade code can
/// call buffer methods unconditionally without branching per sample.
priv struct HotSwapGraph {
  mut active : CompiledGraph
  mut pending : CompiledGraph?
  crossfade_samples : Int
  mut crossfade_position : Int
  old_output : AudioBuffer
  new_output : AudioBuffer
  old_left : AudioBuffer
  old_right : AudioBuffer
  new_left : AudioBuffer
  new_right : AudioBuffer
}

///|
/// Block-boundary mono graph hot-swap wrapper for `CompiledDsp`.
///
/// This first Phase 2 slice supports swapping between already-compiled mono
/// graphs with an optional equal-power crossfade. It does not migrate internal
/// node state between graphs.
struct CompiledDspHotSwap(HotSwapGraph)

///|
/// Block-boundary stereo graph hot-swap wrapper for `CompiledStereoDsp`.
///
/// This first stereo parity slice supports swapping between already-compiled
/// terminal-stereo graphs with an optional equal-power crossfade. It does not
/// migrate internal node state between graphs.
struct CompiledStereoDspHotSwap(HotSwapGraph)

///|
/// Failure reason for result-typed hot-swap queue APIs.
pub(all) enum HotSwapQueueError {
  SampleRateMismatch(Double, Double)
  BlockCapacityMismatch(Int, Int)
} derive(Debug)

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

///|
/// Queue a new compiled graph, resetting the crossfade position.
fn HotSwapGraph::queue_swap_impl(
  self : HotSwapGraph,
  next : CompiledGraph,
) -> Result[Unit, HotSwapQueueError] {
  if self.active.compile_sample_rate != next.compile_sample_rate {
    return Err(
      HotSwapQueueError::SampleRateMismatch(
        self.active.compile_sample_rate,
        next.compile_sample_rate,
      ),
    )
  }
  let active_capacity = self.active.compiled_buffer_capacity()
  let next_capacity = next.compiled_buffer_capacity()
  if active_capacity != next_capacity {
    return Err(
      HotSwapQueueError::BlockCapacityMismatch(active_capacity, next_capacity),
    )
  }
  self.pending = Some(next)
  self.crossfade_position = 0
  Ok(())
}

///|
/// Apply one runtime control message to the active (and pending) graph.
fn HotSwapGraph::apply_control_impl(
  self : HotSwapGraph,
  control : GraphControl,
) -> Result[Unit, GraphControlError] {
  match self.pending {
    None => self.active.apply_control_impl(control)
    Some(next) => {
      match valid_hot_swap_control_graph_result(self.active, control) {
        Ok(_) => ()
        Err(error) => return Err(error)
      }
      match valid_hot_swap_control_graph_result(next, control) {
        Ok(_) => ()
        Err(error) => return Err(error)
      }
      match self.active.apply_control_impl(control) {
        Ok(_) => ()
        Err(error) => return Err(error)
      }
      match next.apply_control_impl(control) {
        Ok(_) => ()
        Err(error) => return Err(error)
      }
      Ok(())
    }
  }
}

///|
/// Apply a batch of runtime control messages to the active (and pending) graph.
fn HotSwapGraph::apply_controls_impl(
  self : HotSwapGraph,
  controls : Array[GraphControl],
) -> Result[Unit, GraphControlError] {
  match self.pending {
    None => self.active.apply_controls_impl(controls)
    Some(next) => {
      match valid_hot_swap_controls_graph_result(self.active, controls) {
        Ok(_) => ()
        Err(error) => return Err(error)
      }
      match valid_hot_swap_controls_graph_result(next, controls) {
        Ok(_) => ()
        Err(error) => return Err(error)
      }
      match self.active.apply_controls_impl(controls) {
        Ok(_) => ()
        Err(error) => return Err(error)
      }
      match next.apply_controls_impl(controls) {
        Ok(_) => ()
        Err(error) => return Err(error)
      }
      Ok(())
    }
  }
}

///|
/// Create a hot-swap wrapper around an active mono compiled graph.
pub fn CompiledDspHotSwap::from_graph(
  active : CompiledDsp,
  crossfade_samples? : Int = 0,
) -> CompiledDspHotSwap {
  let clamped_crossfade = clamp_crossfade_samples(crossfade_samples)
  let capacity = active.compiled_buffer_capacity()
  CompiledDspHotSwap({
    active: active.0,
    pending: None,
    crossfade_samples: clamped_crossfade,
    crossfade_position: 0,
    old_output: AudioBuffer::filled(capacity),
    new_output: AudioBuffer::filled(capacity),
    old_left: AudioBuffer::filled(0),
    old_right: AudioBuffer::filled(0),
    new_left: AudioBuffer::filled(0),
    new_right: AudioBuffer::filled(0),
  })
}

///|
/// Queue a replacement graph for the next `process(...)` call.
///
/// Returns an error when the replacement graph is incompatible with the active
/// graph's compile-time sample rate or block capacity.
pub fn CompiledDspHotSwap::queue_swap(
  self : CompiledDspHotSwap,
  next : CompiledDsp,
) -> Result[Unit, HotSwapQueueError] {
  self.0.queue_swap_impl(next.0)
}

///|
/// Shared crossfade loop for both mono and stereo hot-swap.
///
/// Applies equal-power crossfade sample-by-sample between old and new graphs'
/// scratch buffers, writing into the output buffers. The `is_stereo` flag
/// selects which scratch buffer set to read from (mono: old_output/new_output;
/// stereo: old_left/old_right/new_left/new_right).
fn HotSwapGraph::crossfade_block(
  self : HotSwapGraph,
  output : AudioBuffer,
  left_output : AudioBuffer,
  right_output : AudioBuffer,
  is_stereo : Bool,
  sample_count : Int,
) -> Unit {
  if sample_count <= 0 {
    output.fill(0.0)
    if is_stereo {
      left_output.fill(0.0)
      right_output.fill(0.0)
    }
    return
  }
  for index in 0.. Unit {
  match self.0.pending {
    None => CompiledDsp(self.0.active).process(context, output)
    Some(next) =>
      if self.0.crossfade_samples <= 0 {
        self.0.active = next
        self.0.pending = None
        self.0.crossfade_position = 0
        CompiledDsp(self.0.active).process(context, output)
      } else {
        CompiledDsp(self.0.active).process(context, self.0.old_output)
        CompiledDsp(next).process(context, self.0.new_output)
        let sample_count = compiled_hot_swap_sample_count(
          context,
          output,
          self.0.old_output,
          self.0.new_output,
        )
        self.0.crossfade_block(output, output, output, false, sample_count)
        if self.0.crossfade_position >= self.0.crossfade_samples {
          self.0.active = next
          self.0.pending = None
          self.0.crossfade_position = 0
        }
      }
  }
}

///|
/// Create a hot-swap wrapper around an active stereo compiled graph.
pub fn CompiledStereoDspHotSwap::from_graph(
  active : CompiledStereoDsp,
  crossfade_samples? : Int = 0,
) -> CompiledStereoDspHotSwap {
  let clamped_crossfade = clamp_crossfade_samples(crossfade_samples)
  let capacity = active.compiled_buffer_capacity()
  CompiledStereoDspHotSwap({
    active: active.0,
    pending: None,
    crossfade_samples: clamped_crossfade,
    crossfade_position: 0,
    old_output: AudioBuffer::filled(0),
    new_output: AudioBuffer::filled(0),
    old_left: AudioBuffer::filled(capacity),
    old_right: AudioBuffer::filled(capacity),
    new_left: AudioBuffer::filled(capacity),
    new_right: AudioBuffer::filled(capacity),
  })
}

///|
/// Queue a replacement stereo graph for the next `process(...)` call.
///
/// Returns an error when the replacement graph is incompatible with the active
/// graph's compile-time sample rate or block capacity.
pub fn CompiledStereoDspHotSwap::queue_swap(
  self : CompiledStereoDspHotSwap,
  next : CompiledStereoDsp,
) -> Result[Unit, HotSwapQueueError] {
  self.0.queue_swap_impl(next.0)
}

///|
/// Process one block, crossfading between the active and pending stereo graphs
/// when a swap is in flight.
pub fn CompiledStereoDspHotSwap::process(
  self : CompiledStereoDspHotSwap,
  context : DspContext,
  left_output : AudioBuffer,
  right_output : AudioBuffer,
) -> Unit {
  match self.0.pending {
    None =>
      CompiledStereoDsp(self.0.active).process(
        context, left_output, right_output,
      )
    Some(next) =>
      if self.0.crossfade_samples <= 0 {
        self.0.active = next
        self.0.pending = None
        self.0.crossfade_position = 0
        CompiledStereoDsp(self.0.active).process(
          context, left_output, right_output,
        )
      } else {
        CompiledStereoDsp(self.0.active).process(
          context,
          self.0.old_left,
          self.0.old_right,
        )
        CompiledStereoDsp(next).process(
          context,
          self.0.new_left,
          self.0.new_right,
        )
        let sample_count = compiled_stereo_hot_swap_sample_count(
          context,
          left_output,
          right_output,
          self.0.old_left,
          self.0.old_right,
          self.0.new_left,
          self.0.new_right,
        )
        self.0.crossfade_block(
          left_output, left_output, right_output, true, sample_count,
        )
        if self.0.crossfade_position >= self.0.crossfade_samples {
          self.0.active = next
          self.0.pending = None
          self.0.crossfade_position = 0
        }
      }
  }
}

///|
fn clamp_crossfade_samples(crossfade_samples : Int) -> Int {
  if crossfade_samples > 0 {
    crossfade_samples
  } else {
    0
  }
}

///|
/// WHY a named function for a one-liner: the equal-power crossfade blend
/// formula is used in both mono and stereo mix_hot_swap_outputs. Naming it
/// makes the intent legible at both call sites and keeps the crossfade
/// math in one place if the blend curve ever changes.
fn apply_crossfade_sample(
  old_val : Double,
  new_val : Double,
  old_gain : Double,
  new_gain : Double,
) -> Double {
  old_val * old_gain + new_val * new_gain
}

///|
fn compiled_hot_swap_sample_count(
  context : DspContext,
  output : AudioBuffer,
  old_output : AudioBuffer,
  new_output : AudioBuffer,
) -> Int {
  let bounded = @dsp.effective_sample_count(context, output)
  if bounded < old_output.length() {
    if bounded < new_output.length() {
      bounded
    } else {
      new_output.length()
    }
  } else if old_output.length() < new_output.length() {
    old_output.length()
  } else {
    new_output.length()
  }
}

///|
fn hot_swap_progress(position : Int, crossfade_samples : Int) -> Double {
  if crossfade_samples <= 0 {
    1.0
  } else if position <= 0 {
    0.0
  } else if position >= crossfade_samples {
    1.0
  } else {
    position.to_double() / crossfade_samples.to_double()
  }
}

///|
fn equal_power_old_gain(progress : Double) -> Double {
  @math.cos(progress * 0.5 * @math.PI)
}

///|
fn equal_power_new_gain(progress : Double) -> Double {
  @math.sin(progress * 0.5 * @math.PI)
}

///|
fn valid_hot_swap_control_graph_result(
  compiled : CompiledGraph,
  control : GraphControl,
) -> Result[Unit, GraphControlError] {
  let simulated_nodes = FixedArray::makei(compiled.nodes.length(), index => {
    compiled.nodes[index]
  })
  valid_graph_control_result(compiled, simulated_nodes, control)
}

///|
fn valid_hot_swap_controls_graph_result(
  compiled : CompiledGraph,
  controls : Array[GraphControl],
) -> Result[Unit, GraphControlError] {
  let simulated_nodes = FixedArray::makei(compiled.nodes.length(), index => {
    compiled.nodes[index]
  })
  for index = 0; index < controls.length(); index = index + 1 {
    match
      valid_graph_control_result(compiled, simulated_nodes, controls[index]) {
      Ok(_) => ()
      Err(error) => return Err(error)
    }
  }
  Ok(())
}

///|
fn compiled_stereo_hot_swap_sample_count(
  context : DspContext,
  left_output : AudioBuffer,
  right_output : AudioBuffer,
  old_left : AudioBuffer,
  old_right : AudioBuffer,
  new_left : AudioBuffer,
  new_right : AudioBuffer,
) -> Int {
  let bounded = compiled_stereo_sample_count(context, left_output, right_output)
  let old_length = if old_left.length() < old_right.length() {
    old_left.length()
  } else {
    old_right.length()
  }
  let new_length = if new_left.length() < new_right.length() {
    new_left.length()
  } else {
    new_right.length()
  }
  let bounded_old = if bounded < old_length { bounded } else { old_length }
  if bounded_old < new_length {
    bounded_old
  } else {
    new_length
  }
}