///|
priv struct GraphTopologyEditFailure {
  edit_index : Int
  reason : GraphTopologyEditError
}

///|
fn apply_topology_edits_result(
  nodes : Array[DspNode],
  edits : Array[GraphTopologyEdit],
) -> Result[Unit, GraphTopologyEditFailure] {
  for index = 0; index < edits.length(); index = index + 1 {
    let edit = edits[index]
    match apply_topology_edit_result(nodes, edit) {
      Ok(_) => ()
      Err(reason) => return Err({ edit_index: index, reason })
    }
  }
  Ok(())
}

///|
fn apply_topology_edit_result(
  nodes : Array[DspNode],
  edit : GraphTopologyEdit,
) -> Result[Unit, GraphTopologyEditError] {
  match edit {
    ReplaceNode(node_index, replacement) =>
      if node_index < 0 || node_index >= nodes.length() {
        Err(GraphTopologyEditError::InvalidNodeIndex(node_index))
      } else {
        nodes[node_index] = replacement
        Ok(())
      }
    RewireInput(node_index, input_slot, source_index) =>
      if node_index < 0 || node_index >= nodes.length() {
        Err(GraphTopologyEditError::InvalidNodeIndex(node_index))
      } else if source_index < 0 || source_index >= nodes.length() {
        Err(GraphTopologyEditError::InvalidSourceIndex(source_index))
      } else {
        apply_topology_rewire_input_result(
          nodes, node_index, input_slot, source_index,
        )
      }
    InsertNode(retarget_node_index, retarget_input_slot, inserted_node) =>
      apply_topology_insert_node(
        nodes, retarget_node_index, inserted_node, retarget_input_slot,
      )
    DeleteNode(
      delete_node_index,
      retarget_node_index,
      retarget_input_slot,
      replacement_source_index
    ) =>
      if delete_node_index < 0 ||
        delete_node_index >= nodes.length() ||
        replacement_source_index < 0 ||
        replacement_source_index >= nodes.length() {
        if delete_node_index < 0 || delete_node_index >= nodes.length() {
          Err(GraphTopologyEditError::InvalidNodeIndex(delete_node_index))
        } else {
          Err(
            GraphTopologyEditError::InvalidSourceIndex(replacement_source_index),
          )
        }
      } else {
        apply_topology_delete_node(
          nodes, delete_node_index, retarget_node_index, retarget_input_slot, replacement_source_index,
        )
      }
    InsertChain(retarget_node, input_slot, chain_nodes) =>
      apply_topology_insert_chain(nodes, retarget_node, input_slot, chain_nodes)
    DeleteChain(
      start_node,
      end_node,
      retarget_node,
      input_slot,
      replacement_source
    ) =>
      apply_topology_delete_chain(
        nodes, start_node, end_node, retarget_node, input_slot, replacement_source,
      )
  }
}

///|
fn apply_topology_insert_node(
  nodes : Array[DspNode],
  retarget_node_index : Int,
  inserted_template : DspNode,
  retarget_input_slot : GraphTopologyInputSlot,
) -> Result[Unit, GraphTopologyEditError] {
  if retarget_node_index < 0 || retarget_node_index >= nodes.length() {
    return Err(GraphTopologyEditError::InvalidNodeIndex(retarget_node_index))
  }
  let old_source = match
    topology_node_input(nodes[retarget_node_index], retarget_input_slot) {
    Some(old_source) => old_source
    None =>
      return Err(
        GraphTopologyEditError::UnsupportedInputSlot(
          retarget_node_index,
          retarget_input_slot,
          nodes[retarget_node_index].kind,
        ),
      )
  }
  let inserted = match topology_inserted_node(inserted_template, old_source) {
    Some(inserted) => inserted
    None =>
      return Err(
        GraphTopologyEditError::UnsupportedInsertTemplate(
          inserted_template.kind,
        ),
      )
  }
  let inserted_index = nodes.length()
  nodes.push(inserted)
  match
    apply_topology_rewire_input_result(
      nodes, retarget_node_index, retarget_input_slot, inserted_index,
    ) {
    Ok(_) => Ok(())
    Err(error) => {
      ignore(nodes.pop())
      Err(error)
    }
  }
}

///|
fn apply_topology_delete_node(
  nodes : Array[DspNode],
  delete_node_index : Int,
  retarget_node_index : Int,
  retarget_input_slot : GraphTopologyInputSlot,
  replacement_source_index : Int,
) -> Result[Unit, GraphTopologyEditError] {
  let unary_input = match
    topology_node_input(
      nodes[delete_node_index],
      GraphTopologyInputSlot::Input0,
    ) {
    Some(unary_input) => unary_input
    None =>
      return Err(
        GraphTopologyEditError::DeleteNodeRequiresUnary(
          delete_node_index,
          nodes[delete_node_index].kind,
        ),
      )
  }
  if retarget_node_index < 0 || retarget_node_index >= nodes.length() {
    return Err(GraphTopologyEditError::InvalidNodeIndex(retarget_node_index))
  }
  let consumer_index = match
    topology_single_consumer_for_input(nodes, delete_node_index) {
    Some(consumer_index) => consumer_index
    _ =>
      return Err(
        GraphTopologyEditError::DeleteNodeRequiresSingleConsumer(
          delete_node_index,
        ),
      )
  }
  if consumer_index != retarget_node_index {
    return Err(
      GraphTopologyEditError::DeleteNodeConsumerMismatch(
        consumer_index, retarget_node_index,
      ),
    )
  }
  // If the caller specified the deleted node itself as the replacement source,
  // substitute its upstream input instead — the deleted node won't exist.
  let expected_replacement = if replacement_source_index == delete_node_index {
    unary_input
  } else {
    replacement_source_index
  }
  match
    apply_topology_rewire_input_result(
      nodes, consumer_index, retarget_input_slot, expected_replacement,
    ) {
    Ok(_) => ()
    Err(error) => return Err(error)
  }
  let compacted = Array::new(capacity=nodes.length() - 1)
  for index in 0.. Result[Unit, GraphTopologyEditError] {
  if chain_nodes.length() == 0 {
    return Err(GraphTopologyEditError::EmptyInsertChain)
  }
  if retarget_node_index < 0 || retarget_node_index >= nodes.length() {
    return Err(GraphTopologyEditError::InvalidNodeIndex(retarget_node_index))
  }
  // Validate all chain nodes are unary (no input1)
  for index = 0; index < chain_nodes.length(); index = index + 1 {
    let node = chain_nodes[index]
    if node.input1 >= 0 {
      return Err(GraphTopologyEditError::UnsupportedChainNode(index, node.kind))
    }
  }
  // Get original source of the retarget node's input
  let old_source = match
    topology_node_input(nodes[retarget_node_index], retarget_input_slot) {
    Some(old_source) => old_source
    None =>
      return Err(
        GraphTopologyEditError::UnsupportedInputSlot(
          retarget_node_index,
          retarget_input_slot,
          nodes[retarget_node_index].kind,
        ),
      )
  }
  // For each node in the chain: append to authoring array
  let mut prev_source = old_source
  let mut last_index = -1
  let first_chain_index = nodes.length()
  for chain_node in chain_nodes {
    let inserted = match topology_inserted_node(chain_node, prev_source) {
      Some(inserted) => inserted
      None => {
        // Roll back: remove all chain nodes appended so far
        while nodes.length() > first_chain_index {
          ignore(nodes.pop())
        }
        return Err(
          GraphTopologyEditError::UnsupportedInsertTemplate(chain_node.kind),
        )
      }
    }
    last_index = nodes.length()
    nodes.push(inserted)
    prev_source = last_index
  }
  // Retarget the downstream node's input to the last chain node
  match
    apply_topology_rewire_input_result(
      nodes, retarget_node_index, retarget_input_slot, last_index,
    ) {
    Ok(_) => Ok(())
    Err(error) => {
      // Roll back
      while nodes.length() > first_chain_index {
        ignore(nodes.pop())
      }
      Err(error)
    }
  }
}

///|
fn apply_topology_delete_chain(
  nodes : Array[DspNode],
  start_node_index : Int,
  end_node_index : Int,
  retarget_node_index : Int,
  retarget_input_slot : GraphTopologyInputSlot,
  replacement_source_index : Int,
) -> Result[Unit, GraphTopologyEditError] {
  // Basic bounds check
  if start_node_index < 0 || start_node_index >= nodes.length() {
    return Err(GraphTopologyEditError::InvalidNodeIndex(start_node_index))
  }
  if end_node_index < 0 || end_node_index >= nodes.length() {
    return Err(GraphTopologyEditError::InvalidNodeIndex(end_node_index))
  }
  if start_node_index > end_node_index {
    return Err(
      GraphTopologyEditError::InvalidDeleteRange(
        start_node_index, end_node_index,
      ),
    )
  }
  if retarget_node_index < 0 || retarget_node_index >= nodes.length() {
    return Err(GraphTopologyEditError::InvalidNodeIndex(retarget_node_index))
  }
  if replacement_source_index < 0 || replacement_source_index >= nodes.length() {
    return Err(
      GraphTopologyEditError::InvalidSourceIndex(replacement_source_index),
    )
  }
  // Reject if replacement_source is inside the deleted span
  if replacement_source_index >= start_node_index &&
    replacement_source_index <= end_node_index {
    return Err(
      GraphTopologyEditError::ReplacementSourceInDeletedRange(
        replacement_source_index,
      ),
    )
  }
  // Validate: each node in the chain must be unary (input1 < 0) and not
  // Output/StereoOutput, and each must feed exactly one downstream consumer
  for index = start_node_index; index <= end_node_index; index = index + 1 {
    let node = nodes[index]
    if node.input1 >= 0 {
      return Err(
        GraphTopologyEditError::DeleteChainRequiresUnary(index, node.kind),
      )
    }
    if node.kind is Output || node.kind is StereoOutput {
      return Err(
        GraphTopologyEditError::DeleteChainRequiresUnary(index, node.kind),
      )
    }
    let consumer = match topology_single_consumer_for_input(nodes, index) {
      Some(consumer) => consumer
      None =>
        return Err(
          GraphTopologyEditError::DeleteChainRequiresSingleConsumer(index),
        )
    }
    // Each chain node except the last must feed the next node in the chain
    if index < end_node_index {
      if consumer != index + 1 {
        return Err(
          GraphTopologyEditError::DeleteChainConsumerMismatch(
            index,
            consumer,
            index + 1,
          ),
        )
      }
      // Last chain node must feed the retarget node
    } else if consumer != retarget_node_index {
      return Err(
        GraphTopologyEditError::DeleteChainConsumerMismatch(
          index, consumer, retarget_node_index,
        ),
      )
    }
  }
  // Retarget the downstream node's input to the replacement source
  match
    apply_topology_rewire_input_result(
      nodes, retarget_node_index, retarget_input_slot, replacement_source_index,
    ) {
    Ok(_) => ()
    Err(error) => return Err(error)
  }
  // Build compacted array excluding all chain nodes, adjusting indices
  // We need to delete nodes from start_node_index to end_node_index inclusive
  let chain_count = end_node_index - start_node_index + 1
  let compacted = Array::new(capacity=nodes.length() - chain_count)
  for index in 0.. end_node_index {
      let node = topology_chain_compacted_node_inputs(
        nodes[index],
        start_node_index,
        end_node_index,
      )
      compacted.push(node)
    }
  }
  nodes.clear()
  for node in compacted {
    nodes.push(node)
  }
  Ok(())
}

///|
fn topology_chain_compacted_node_inputs(
  node : DspNode,
  start_deleted : Int,
  end_deleted : Int,
) -> DspNode {
  DspNode::new(
    node.kind,
    topology_chain_compacted_input(node.input0, start_deleted, end_deleted),
    topology_chain_compacted_input(node.input1, start_deleted, end_deleted),
    node.value0,
    node.value1,
    node.value2,
    node.value3,
    node.waveform,
    node.filter_mode,
    node.delay_max_samples,
    node.delay_samples,
    node.seed,
  )
}

///|
fn topology_chain_compacted_input(
  input : Int,
  start_deleted : Int,
  end_deleted : Int,
) -> Int {
  if input < 0 {
    input
  } else if input > end_deleted {
    input - (end_deleted - start_deleted + 1)
  } else if input >= start_deleted {
    // This should not happen for valid graphs (we already rewired away from
    // deleted nodes), but return -1 as a safety measure
    -1
  } else {
    input
  }
}

///|
fn topology_single_consumer_for_input(
  nodes : Array[DspNode],
  source_index : Int,
) -> Int? {
  let mut consumer = -1
  for index in 0..= 0 {
        return None
      }
      consumer = index
    }
  }
  if consumer >= 0 {
    Some(consumer)
  } else {
    None
  }
}

///|
fn topology_compacted_node_inputs(
  node : DspNode,
  deleted_index : Int,
) -> DspNode {
  DspNode::new(
    node.kind,
    topology_compacted_input(node.input0, deleted_index),
    topology_compacted_input(node.input1, deleted_index),
    node.value0,
    node.value1,
    node.value2,
    node.value3,
    node.waveform,
    node.filter_mode,
    node.delay_max_samples,
    node.delay_samples,
    node.seed,
  )
}

///|
fn topology_compacted_input(input : Int, deleted_index : Int) -> Int {
  if input >= 0 && input > deleted_index {
    input - 1
  } else {
    input
  }
}

///|
fn topology_node_input(
  node : DspNode,
  input_slot : GraphTopologyInputSlot,
) -> Int? {
  match (node.kind, input_slot) {
    (DspNodeKind::Mul, GraphTopologyInputSlot::Input0) => Some(node.input0)
    (DspNodeKind::Mul, GraphTopologyInputSlot::Input1) => Some(node.input1)
    (DspNodeKind::Mix, GraphTopologyInputSlot::Input0) => Some(node.input0)
    (DspNodeKind::Mix, GraphTopologyInputSlot::Input1) => Some(node.input1)
    (DspNodeKind::Gain, GraphTopologyInputSlot::Input0) => Some(node.input0)
    (DspNodeKind::Clip, GraphTopologyInputSlot::Input0) => Some(node.input0)
    (DspNodeKind::Output, GraphTopologyInputSlot::Input0) => Some(node.input0)
    (DspNodeKind::Pan, GraphTopologyInputSlot::Input0) => Some(node.input0)
    (DspNodeKind::StereoGain, GraphTopologyInputSlot::Input0) =>
      Some(node.input0)
    (DspNodeKind::StereoClip, GraphTopologyInputSlot::Input0) =>
      Some(node.input0)
    (DspNodeKind::StereoMixDown, GraphTopologyInputSlot::Input0) =>
      Some(node.input0)
    (DspNodeKind::StereoOutput, GraphTopologyInputSlot::Input0) =>
      Some(node.input0)
    (DspNodeKind::Biquad, GraphTopologyInputSlot::Input0) => Some(node.input0)
    (DspNodeKind::Delay, GraphTopologyInputSlot::Input0) => Some(node.input0)
    (DspNodeKind::StereoBiquad, GraphTopologyInputSlot::Input0) =>
      Some(node.input0)
    (DspNodeKind::StereoDelay, GraphTopologyInputSlot::Input0) =>
      Some(node.input0)
    _ => None
  }
}

///|
fn topology_inserted_node(template : DspNode, input_source : Int) -> DspNode? {
  match template.kind {
    DspNodeKind::Gain => Some(DspNode::gain(input_source, template.value0))
    DspNodeKind::Clip => Some(DspNode::clip(input_source, template.value0))
    DspNodeKind::Pan => Some(DspNode::pan(input_source, template.value0))
    DspNodeKind::StereoGain =>
      Some(DspNode::stereo_gain(input=input_source, amount=template.value0))
    DspNodeKind::StereoClip =>
      Some(DspNode::stereo_clip(input=input_source, threshold=template.value0))
    DspNodeKind::StereoMixDown => Some(DspNode::stereo_mixdown(input_source))
    DspNodeKind::Output => Some(DspNode::output(input_source))
    DspNodeKind::StereoOutput => Some(DspNode::stereo_output(input_source))
    DspNodeKind::Biquad =>
      Some(
        DspNode::biquad(
          input=input_source,
          mode=template.filter_mode,
          cutoff_hz=template.value0,
          q=template.value1,
        ),
      )
    DspNodeKind::Delay =>
      Some(
        DspNode::delay(
          input=input_source,
          max_delay_samples=template.delay_max_samples,
          delay_samples=template.delay_samples,
          feedback=template.value0,
        ),
      )
    DspNodeKind::StereoBiquad =>
      Some(
        DspNode::stereo_biquad(
          input=input_source,
          mode=template.filter_mode,
          cutoff_hz=template.value0,
          q=template.value1,
        ),
      )
    DspNodeKind::StereoDelay =>
      Some(
        DspNode::stereo_delay(
          input=input_source,
          max_delay_samples=template.delay_max_samples,
          delay_samples=template.delay_samples,
          feedback=template.value0,
        ),
      )
    _ => None
  }
}

///|
fn apply_topology_rewire_input_result(
  nodes : Array[DspNode],
  node_index : Int,
  input_slot : GraphTopologyInputSlot,
  source_index : Int,
) -> Result[Unit, GraphTopologyEditError] {
  let node = nodes[node_index]
  let rewired = match (node.kind, input_slot) {
    (DspNodeKind::Gain, GraphTopologyInputSlot::Input0) =>
      Some(DspNode::gain(source_index, node.value0))
    (DspNodeKind::Mul, GraphTopologyInputSlot::Input0) =>
      Some(DspNode::mul(source_index, node.input1))
    (DspNodeKind::Mul, GraphTopologyInputSlot::Input1) =>
      Some(DspNode::mul(node.input0, source_index))
    (DspNodeKind::Mix, GraphTopologyInputSlot::Input0) =>
      Some(DspNode::mix(source_index, node.input1))
    (DspNodeKind::Mix, GraphTopologyInputSlot::Input1) =>
      Some(DspNode::mix(node.input0, source_index))
    (DspNodeKind::Clip, GraphTopologyInputSlot::Input0) =>
      Some(DspNode::clip(source_index, node.value0))
    (DspNodeKind::Output, GraphTopologyInputSlot::Input0) =>
      Some(DspNode::output(source_index))
    (DspNodeKind::Pan, GraphTopologyInputSlot::Input0) =>
      Some(DspNode::pan(source_index, node.value0))
    (DspNodeKind::StereoGain, GraphTopologyInputSlot::Input0) =>
      Some(DspNode::stereo_gain(input=source_index, amount=node.value0))
    (DspNodeKind::StereoClip, GraphTopologyInputSlot::Input0) =>
      Some(DspNode::stereo_clip(input=source_index, threshold=node.value0))
    (DspNodeKind::StereoMixDown, GraphTopologyInputSlot::Input0) =>
      Some(DspNode::stereo_mixdown(source_index))
    (DspNodeKind::StereoOutput, GraphTopologyInputSlot::Input0) =>
      Some(DspNode::stereo_output(source_index))
    (DspNodeKind::Biquad, GraphTopologyInputSlot::Input0) =>
      Some(
        DspNode::biquad(
          input=source_index,
          mode=node.filter_mode,
          cutoff_hz=node.value0,
          q=node.value1,
        ),
      )
    (DspNodeKind::Delay, GraphTopologyInputSlot::Input0) =>
      Some(
        DspNode::delay(
          input=source_index,
          max_delay_samples=node.delay_max_samples,
          delay_samples=node.delay_samples,
          feedback=node.value0,
        ),
      )
    (DspNodeKind::StereoBiquad, GraphTopologyInputSlot::Input0) =>
      Some(
        DspNode::stereo_biquad(
          input=source_index,
          mode=node.filter_mode,
          cutoff_hz=node.value0,
          q=node.value1,
        ),
      )
    (DspNodeKind::StereoDelay, GraphTopologyInputSlot::Input0) =>
      Some(
        DspNode::stereo_delay(
          input=source_index,
          max_delay_samples=node.delay_max_samples,
          delay_samples=node.delay_samples,
          feedback=node.value0,
        ),
      )
    _ => None
  }
  match rewired {
    Some(rewired) => {
      nodes[node_index] = rewired
      Ok(())
    }
    None =>
      Err(
        GraphTopologyEditError::UnsupportedInputSlot(
          node_index,
          input_slot,
          node.kind,
        ),
      )
  }
}