///|
/// Shared topology-edit state for both mono and stereo compiled graphs.
///
/// Holds the authoring-order nodes, compile-time parameters, and the
/// hot-swap graph used for block-boundary graph replacement.
priv struct TopologyGraph {
  mut authoring_nodes : Array[DspNode]
  compile_sample_rate : Double
  compile_block_size : Int
  hot_swap : HotSwapGraph
}

///|
/// Failure reason for result-typed topology queue APIs.
pub(all) enum GraphTopologyEditError {
  InvalidNodeIndex(Int)
  InvalidSourceIndex(Int)
  UnsupportedInputSlot(Int, GraphTopologyInputSlot, DspNodeKind)
  UnsupportedInsertTemplate(DspNodeKind)
  EmptyInsertChain
  UnsupportedChainNode(Int, DspNodeKind)
  InvalidDeleteRange(Int, Int)
  ReplacementSourceInDeletedRange(Int)
  DeleteNodeRequiresUnary(Int, DspNodeKind)
  DeleteNodeRequiresSingleConsumer(Int)
  DeleteNodeConsumerMismatch(Int, Int)
  DeleteChainRequiresUnary(Int, DspNodeKind)
  DeleteChainRequiresSingleConsumer(Int)
  DeleteChainConsumerMismatch(Int, Int, Int)
} derive(Debug)

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

///|
/// Failure reason for result-typed topology queue APIs.
pub(all) enum GraphTopologyQueueError {
  PendingSwap
  InvalidEdit(Int, GraphTopologyEditError)
  RecompileRejected
  HotSwap(HotSwapQueueError)
} derive(Debug)

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

///|
/// Mono topology-edit wrapper that recompiles authoring nodes and stages the
/// replacement through `HotSwapGraph`.
///
/// This first slice keeps edits narrow and deterministic: only `ReplaceNode`
/// and `RewireInput` frames are supported, plus narrow unary `InsertNode` and
/// `DeleteNode` frames on the mono path. Only one staged topology replacement
/// may be pending at a time.
struct CompiledDspTopologyController(TopologyGraph)

///|
/// Build a `TopologyGraph` from a compile+wrap callback.
///
/// WHY a callback instead of an enum: the mono and stereo paths produce
/// different concrete wrappers (`CompiledDspHotSwap` vs `CompiledStereoDspHotSwap`)
/// that allocate different buffer layouts (mono: old_output/new_output at
/// capacity; stereo: old_left/old_right/new_left/new_right). A callback lets
/// each `from_nodes` caller supply its variant-specific compile+unwrap logic
/// without a `GraphKind` enum and a match inside this function.
fn build_topology_graph(
  nodes : Array[DspNode],
  context : DspContext,
  crossfade_samples : Int,
  compile_and_wrap : (Array[DspNode], DspContext, Int) -> HotSwapGraph?,
) -> TopologyGraph? {
  let hot_swap = match compile_and_wrap(nodes, context, crossfade_samples) {
    Some(hs) => hs
    None => return None
  }
  Some({
    authoring_nodes: nodes.copy(),
    compile_sample_rate: context.sample_rate(),
    compile_block_size: context.block_size(),
    hot_swap,
  })
}

///|
/// Build a topology-edit wrapper from authoring-order mono nodes.
pub fn CompiledDspTopologyController::from_nodes(
  nodes : Array[DspNode],
  context : DspContext,
  crossfade_samples? : Int = 0,
) -> CompiledDspTopologyController? {
  build_topology_graph(nodes, context, crossfade_samples, fn(ns, ctx, xf) {
    match CompiledDsp::compile_raw(ns, ctx) {
      Some(compiled) =>
        Some(CompiledDspHotSwap::from_graph(compiled, crossfade_samples=xf).0)
      None => None
    }
  }).map(fn(tg) { CompiledDspTopologyController(tg) })
}

///|
/// Apply one runtime control message to the active or in-flight graph.
fn TopologyGraph::apply_control_impl(
  self : TopologyGraph,
  control : GraphControl,
) -> Result[Unit, GraphControlError] {
  let updated_authoring_nodes = match
    topology_control_updated_nodes_result(
      self.authoring_nodes,
      self.compile_sample_rate,
      [control],
    ) {
    Ok(updated_authoring_nodes) => updated_authoring_nodes
    Err(error) => return Err(error)
  }
  match self.hot_swap.apply_control_impl(control) {
    Ok(_) => ()
    Err(error) => return Err(error)
  }
  self.authoring_nodes = updated_authoring_nodes
  Ok(())
}

///|
/// Apply a batch of runtime control messages to the active or in-flight graph.
fn TopologyGraph::apply_controls_impl(
  self : TopologyGraph,
  controls : Array[GraphControl],
) -> Result[Unit, GraphControlError] {
  let updated_authoring_nodes = match
    topology_control_updated_nodes_result(
      self.authoring_nodes,
      self.compile_sample_rate,
      controls,
    ) {
    Ok(updated_authoring_nodes) => updated_authoring_nodes
    Err(error) => return Err(error)
  }
  match self.hot_swap.apply_controls_impl(controls) {
    Ok(_) => ()
    Err(error) => return Err(error)
  }
  self.authoring_nodes = updated_authoring_nodes
  Ok(())
}

///|
/// Queue one topology edit for the next hot-swap recompilation.
fn TopologyGraph::queue_topology_edit_impl(
  self : TopologyGraph,
  edit : GraphTopologyEdit,
  compile_fn : (Array[DspNode], DspContext) -> CompiledGraph?,
) -> Result[Unit, GraphTopologyQueueError] {
  self.queue_topology_edits_impl([edit], compile_fn)
}

///|
/// Queue an ordered topology-edit batch.
///
/// The batch is transactional: invalid node indices or a recompilation failure
/// reject the whole edit set and leave the current graph unchanged.
fn TopologyGraph::queue_topology_edits_impl(
  self : TopologyGraph,
  edits : Array[GraphTopologyEdit],
  compile_fn : (Array[DspNode], DspContext) -> CompiledGraph?,
) -> Result[Unit, GraphTopologyQueueError] {
  if self.hot_swap.pending is Some(_) {
    return Err(GraphTopologyQueueError::PendingSwap)
  }
  let edited_nodes = self.authoring_nodes.copy()
  match apply_topology_edits_result(edited_nodes, edits) {
    Ok(_) => ()
    Err(failure) =>
      return Err(
        GraphTopologyQueueError::InvalidEdit(failure.edit_index, failure.reason),
      )
  }
  let compile_context = DspContext::new(
    sample_rate=self.compile_sample_rate,
    block_size=self.compile_block_size,
  )
  let replacement = match compile_fn(edited_nodes, compile_context) {
    Some(replacement) => replacement
    None => return Err(GraphTopologyQueueError::RecompileRejected)
  }
  // Skip state copy after delete operations — authoring indices shift,
  // so position-based matching may copy state from the wrong node.
  // The crossfade smooths the transition with fresh state.
  let has_delete = edits
    .iter()
    .any(fn(e) { e is DeleteNode(_) || e is DeleteChain(_) })
  if !has_delete {
    copy_compiled_graph_state(self.hot_swap.active, replacement)
  }
  match self.hot_swap.queue_swap_impl(replacement) {
    Ok(_) => ()
    Err(error) => return Err(GraphTopologyQueueError::HotSwap(error))
  }
  self.authoring_nodes = edited_nodes
  Ok(())
}

///|
/// Queue one topology edit for the next hot-swap recompilation.
pub fn CompiledDspTopologyController::queue_topology_edit(
  self : CompiledDspTopologyController,
  edit : GraphTopologyEdit,
) -> Result[Unit, GraphTopologyQueueError] {
  self.0.queue_topology_edit_impl(edit, compile_mono_graph)
}

///|
/// Queue an ordered topology-edit batch.
///
/// The batch is transactional: invalid node indices or a recompilation failure
/// reject the whole edit set and leave the current graph unchanged.
pub fn CompiledDspTopologyController::queue_topology_edits(
  self : CompiledDspTopologyController,
  edits : Array[GraphTopologyEdit],
) -> Result[Unit, GraphTopologyQueueError] {
  self.0.queue_topology_edits_impl(edits, compile_mono_graph)
}

///|
/// Process one block through the active or crossfading mono graph.
pub fn CompiledDspTopologyController::process(
  self : CompiledDspTopologyController,
  context : DspContext,
  output : AudioBuffer,
) -> Unit {
  CompiledDspHotSwap(self.0.hot_swap).process(context, output)
}

///|
/// Terminal-stereo topology-edit wrapper that recompiles authoring nodes and
/// stages the replacement through `HotSwapGraph`.
///
/// Supports the same edit kinds as the mono controller (all six variants
/// of `GraphTopologyEdit`). Only one staged topology replacement may be
/// pending at a time.
struct CompiledStereoDspTopologyController(TopologyGraph)

///|
/// Build a topology-edit wrapper from authoring-order terminal-stereo nodes.
pub fn CompiledStereoDspTopologyController::from_nodes(
  nodes : Array[DspNode],
  context : DspContext,
  crossfade_samples? : Int = 0,
) -> CompiledStereoDspTopologyController? {
  build_topology_graph(nodes, context, crossfade_samples, fn(ns, ctx, xf) {
    match CompiledStereoDsp::compile_raw(ns, ctx) {
      Some(compiled) =>
        Some(
          CompiledStereoDspHotSwap::from_graph(compiled, crossfade_samples=xf).0,
        )
      None => None
    }
  }).map(fn(tg) { CompiledStereoDspTopologyController(tg) })
}

///|
/// Queue one topology edit for the next stereo hot-swap recompilation.
pub fn CompiledStereoDspTopologyController::queue_topology_edit(
  self : CompiledStereoDspTopologyController,
  edit : GraphTopologyEdit,
) -> Result[Unit, GraphTopologyQueueError] {
  self.0.queue_topology_edit_impl(edit, compile_stereo_graph)
}

///|
/// Queue an ordered stereo topology-edit batch.
///
/// The batch is transactional: invalid node indices or a recompilation failure
/// reject the whole edit set and leave the current graph unchanged.
pub fn CompiledStereoDspTopologyController::queue_topology_edits(
  self : CompiledStereoDspTopologyController,
  edits : Array[GraphTopologyEdit],
) -> Result[Unit, GraphTopologyQueueError] {
  self.0.queue_topology_edits_impl(edits, compile_stereo_graph)
}

///|
/// Process one block through the active or crossfading stereo graph.
pub fn CompiledStereoDspTopologyController::process(
  self : CompiledStereoDspTopologyController,
  context : DspContext,
  left_output : AudioBuffer,
  right_output : AudioBuffer,
) -> Unit {
  CompiledStereoDspHotSwap(self.0.hot_swap).process(
    context, left_output, right_output,
  )
}

///|
fn topology_control_updated_nodes_result(
  authoring_nodes : Array[DspNode],
  compile_sample_rate : Double,
  controls : Array[GraphControl],
) -> Result[Array[DspNode], GraphControlError] {
  let updated_nodes = authoring_nodes.copy()
  for control in controls {
    match
      apply_topology_runtime_control_result(
        updated_nodes, compile_sample_rate, control,
      ) {
      Ok(_) => ()
      Err(error) => return Err(error)
    }
  }
  Ok(updated_nodes)
}

///|
fn apply_topology_runtime_control_result(
  nodes : Array[DspNode],
  compile_sample_rate : Double,
  control : GraphControl,
) -> Result[Unit, GraphControlError] {
  match control.kind {
    SetParam => {
      if control.node_index < 0 || control.node_index >= nodes.length() {
        return Err(GraphControlError::InvalidNodeIndex(control.node_index))
      }
      let updated = match
        updated_node_param_result(
          nodes[control.node_index],
          control.node_index,
          control.slot,
          control.value,
          compile_sample_rate,
        ) {
        Ok(updated) => updated
        Err(error) => return Err(error)
      }
      nodes[control.node_index] = updated
      Ok(())
    }
    _ => Ok(())
  }
}

///|
/// Compile helper for mono graphs, returning `CompiledGraph` directly.
fn compile_mono_graph(
  nodes : Array[DspNode],
  context : DspContext,
) -> CompiledGraph? {
  CompiledDsp::compile_raw(nodes, context).map(fn(c) { c.0 })
}

///|
/// Compile helper for stereo graphs, returning `CompiledGraph` directly.
fn compile_stereo_graph(
  nodes : Array[DspNode],
  context : DspContext,
) -> CompiledGraph? {
  CompiledStereoDsp::compile_raw(nodes, context).map(fn(c) { c.0 })
}

///|
/// Copy runtime state from an old compiled graph to a new one for nodes
/// that share the same authoring index and node kind.
fn copy_compiled_graph_state(
  old_graph : CompiledGraph,
  new_graph : CompiledGraph,
) -> Unit {
  let old_map = old_graph.index_map
  let new_map = new_graph.index_map
  let shared_count = if old_map.length() < new_map.length() {
    old_map.length()
  } else {
    new_map.length()
  }
  for authoring_idx = 0
      authoring_idx < shared_count
      authoring_idx = authoring_idx + 1 {
    let old_compiled = old_map[authoring_idx]
    let new_compiled = new_map[authoring_idx]
    if old_compiled >= 0 &&
      new_compiled >= 0 &&
      old_compiled < old_graph.nodes.length() &&
      new_compiled < new_graph.nodes.length() &&
      old_graph.nodes[old_compiled].kind == new_graph.nodes[new_compiled].kind {
      new_graph.osc_states[new_compiled] = old_graph.osc_states[old_compiled]
      new_graph.noise_states[new_compiled] = old_graph.noise_states[old_compiled]
      new_graph.env_states[new_compiled] = old_graph.env_states[old_compiled]
      new_graph.biquad_states[new_compiled] = old_graph.biquad_states[old_compiled]
      new_graph.stereo_biquad_left_states[new_compiled] = old_graph.stereo_biquad_left_states[old_compiled]
      new_graph.stereo_biquad_right_states[new_compiled] = old_graph.stereo_biquad_right_states[old_compiled]
      // Only copy delay state if buffer capacity matches
      match
        (
          old_graph.delay_states[old_compiled],
          new_graph.delay_states[new_compiled],
        ) {
        (Some(old_delay), Some(new_delay)) =>
          if old_delay.max_delay_samples() == new_delay.max_delay_samples() {
            new_graph.delay_states[new_compiled] = old_graph.delay_states[old_compiled]
          }
        _ => ()
      }
      match
        (
          old_graph.stereo_delay_left_states[old_compiled],
          new_graph.stereo_delay_left_states[new_compiled],
        ) {
        (Some(old_delay), Some(new_delay)) =>
          if old_delay.max_delay_samples() == new_delay.max_delay_samples() {
            new_graph.stereo_delay_left_states[new_compiled] = old_graph.stereo_delay_left_states[old_compiled]
          }
        _ => ()
      }
      match
        (
          old_graph.stereo_delay_right_states[old_compiled],
          new_graph.stereo_delay_right_states[new_compiled],
        ) {
        (Some(old_delay), Some(new_delay)) =>
          if old_delay.max_delay_samples() == new_delay.max_delay_samples() {
            new_graph.stereo_delay_right_states[new_compiled] = old_graph.stereo_delay_right_states[old_compiled]
          }
        _ => ()
      }
      new_graph.self_values[new_compiled] = old_graph.self_values[old_compiled]
      new_graph.self_left_values[new_compiled] = old_graph.self_left_values[old_compiled]
      new_graph.self_right_values[new_compiled] = old_graph.self_right_values[old_compiled]
    }
  }
}