///|
/// Unified feedback graph processing for both mono and stereo graphs.
///
/// Both `Output` and `StereoOutput` arms are present in the match — validation
/// at compile time guarantees a mono graph never contains `StereoOutput` and
/// vice-versa, so the "wrong" arm is a no-op that never fires at runtime.
///
/// WHY `is_stereo` flag instead of inspecting the last node's kind: the flag
/// is precomputed and branch-predictable (constant for a given graph type),
/// avoiding an index lookup on the hot path. It controls only the final
/// output copy (once per block, not per sample):
/// - mono:   copies last mono buffer to `output`
/// - stereo: copies last stereo buffers to `left_output` / `right_output`
fn CompiledGraph::process_feedback_graph_impl(
  self : CompiledGraph,
  context : DspContext,
  output : AudioBuffer,
  left_output : AudioBuffer,
  right_output : AudioBuffer,
  is_stereo : Bool,
  sample_count : Int,
) -> Unit {
  clear_compiled_buffers(self.buffers)
  clear_compiled_buffers(self.left_buffers)
  clear_compiled_buffers(self.right_buffers)
  self.prepare_feedback_biquads(context)
  // Use pre-allocated scratch arrays instead of allocating per call
  let sample_values = self.sample_values
  let left_sv = self.left_sample_values
  let right_sv = self.right_sample_values
  for i = 0; i < sample_values.length(); i = i + 1 {
    sample_values[i] = 0.0
    left_sv[i] = 0.0
    right_sv[i] = 0.0
  }
  let sample_rate = context.sample_rate()
  // Pre-compute pan trig gains once per block (position is constant within block)
  for i = 0; i < self.nodes.length(); i = i + 1 {
    if self.nodes[i].kind is Pan {
      self.pan_left_gains[i] = @dsp.pan_left_gain(self.nodes[i].value0)
      self.pan_right_gains[i] = @dsp.pan_right_gain(self.nodes[i].value0)
    }
  }

  for sample_index = 0
      sample_index < sample_count
      sample_index = sample_index + 1 {
    for node_index = 0
        node_index < self.nodes.length()
        node_index = node_index + 1 {
      let node = self.nodes[node_index]
      match node.kind {
        Constant => {
          let value = node.value0
          sample_values[node_index] = value
          self.buffers[node_index].set(sample_index, value)
        }
        Oscillator => {
          let freq = if node.input0 >= 0 {
            self_register_input_sample(
              sample_values,
              self.self_values,
              node.input0,
              self.back_edge_input0_source[node_index],
            )
          } else {
            node.value0
          }
          let value = self.osc_states[node_index]
            .unwrap()
            .tick_waveform(waveform=node.waveform, freq_hz=freq, sample_rate~)
          sample_values[node_index] = value
          self.buffers[node_index].set(sample_index, value)
        }
        Noise => {
          let value = self.noise_states[node_index].unwrap().tick()
          sample_values[node_index] = value
          self.buffers[node_index].set(sample_index, value)
        }
        Adsr => {
          let value = self.env_states[node_index].unwrap().tick(context)
          sample_values[node_index] = value
          self.buffers[node_index].set(sample_index, value)
        }
        Biquad => {
          let input0 = self_register_input_sample(
            sample_values,
            self.self_values,
            node.input0,
            self.back_edge_input0_source[node_index],
          )
          let value = self.biquad_states[node_index].unwrap().tick(input0)
          sample_values[node_index] = value
          self.buffers[node_index].set(sample_index, value)
        }
        Delay => {
          let input0 = self_register_input_sample(
            sample_values,
            self.self_values,
            node.input0,
            self.back_edge_input0_source[node_index],
          )
          let value = self.delay_states[node_index].unwrap().tick(input0)
          sample_values[node_index] = value
          self.buffers[node_index].set(sample_index, value)
        }
        Gain => {
          let input0 = self_register_input_sample(
            sample_values,
            self.self_values,
            node.input0,
            self.back_edge_input0_source[node_index],
          )
          let value = input0 * node.value0
          sample_values[node_index] = value
          self.buffers[node_index].set(sample_index, value)
        }
        Mul => {
          let input0 = self_register_input_sample(
            sample_values,
            self.self_values,
            node.input0,
            self.back_edge_input0_source[node_index],
          )
          let input1 = self_register_input_sample(
            sample_values,
            self.self_values,
            node.input1,
            self.back_edge_input1_source[node_index],
          )
          let value = input0 * input1
          sample_values[node_index] = value
          self.buffers[node_index].set(sample_index, value)
        }
        Mix => {
          let input0 = self_register_input_sample(
            sample_values,
            self.self_values,
            node.input0,
            self.back_edge_input0_source[node_index],
          )
          let input1 = self_register_input_sample(
            sample_values,
            self.self_values,
            node.input1,
            self.back_edge_input1_source[node_index],
          )
          let value = input0 + input1
          sample_values[node_index] = value
          self.buffers[node_index].set(sample_index, value)
        }
        Clip => {
          let input0 = self_register_input_sample(
            sample_values,
            self.self_values,
            node.input0,
            self.back_edge_input0_source[node_index],
          )
          let value = clip_feedback_sample(input0, node.value0)
          sample_values[node_index] = value
          self.buffers[node_index].set(sample_index, value)
        }
        Output => {
          // Mono terminal: reads input into mono buffer.
          // In stereo graphs this arm never matches (no Output node present).
          let input0 = self_register_input_sample(
            sample_values,
            self.self_values,
            node.input0,
            self.back_edge_input0_source[node_index],
          )
          sample_values[node_index] = input0
          self.buffers[node_index].set(sample_index, input0)
        }
        Pan => {
          let input0 = self_register_input_sample(
            sample_values,
            self.self_values,
            node.input0,
            self.back_edge_input0_source[node_index],
          )
          let pan_left = input0 * self.pan_left_gains[node_index]
          let pan_right = input0 * self.pan_right_gains[node_index]
          left_sv[node_index] = pan_left
          right_sv[node_index] = pan_right
          self.left_buffers[node_index].set(sample_index, pan_left)
          self.right_buffers[node_index].set(sample_index, pan_right)
        }
        StereoGain => {
          let input_left = self_register_stereo_input_left(
            left_sv,
            self.self_left_values,
            node.input0,
            self.back_edge_input0_source[node_index],
          )
          let input_right = self_register_stereo_input_right(
            right_sv,
            self.self_right_values,
            node.input0,
            self.back_edge_input0_source[node_index],
          )
          let left = input_left * node.value0
          let right = input_right * node.value0
          left_sv[node_index] = left
          right_sv[node_index] = right
          self.left_buffers[node_index].set(sample_index, left)
          self.right_buffers[node_index].set(sample_index, right)
        }
        StereoClip => {
          let input_left = self_register_stereo_input_left(
            left_sv,
            self.self_left_values,
            node.input0,
            self.back_edge_input0_source[node_index],
          )
          let input_right = self_register_stereo_input_right(
            right_sv,
            self.self_right_values,
            node.input0,
            self.back_edge_input0_source[node_index],
          )
          let left = clip_feedback_sample(input_left, node.value0)
          let right = clip_feedback_sample(input_right, node.value0)
          left_sv[node_index] = left
          right_sv[node_index] = right
          self.left_buffers[node_index].set(sample_index, left)
          self.right_buffers[node_index].set(sample_index, right)
        }
        StereoBiquad => {
          let input_left = self_register_stereo_input_left(
            left_sv,
            self.self_left_values,
            node.input0,
            self.back_edge_input0_source[node_index],
          )
          let input_right = self_register_stereo_input_right(
            right_sv,
            self.self_right_values,
            node.input0,
            self.back_edge_input0_source[node_index],
          )
          let left = self.stereo_biquad_left_states[node_index]
            .unwrap()
            .tick(input_left)
          let right = self.stereo_biquad_right_states[node_index]
            .unwrap()
            .tick(input_right)
          left_sv[node_index] = left
          right_sv[node_index] = right
          self.left_buffers[node_index].set(sample_index, left)
          self.right_buffers[node_index].set(sample_index, right)
        }
        StereoDelay => {
          let input_left = self_register_stereo_input_left(
            left_sv,
            self.self_left_values,
            node.input0,
            self.back_edge_input0_source[node_index],
          )
          let input_right = self_register_stereo_input_right(
            right_sv,
            self.self_right_values,
            node.input0,
            self.back_edge_input0_source[node_index],
          )
          let left = self.stereo_delay_left_states[node_index]
            .unwrap()
            .tick(input_left)
          let right = self.stereo_delay_right_states[node_index]
            .unwrap()
            .tick(input_right)
          left_sv[node_index] = left
          right_sv[node_index] = right
          self.left_buffers[node_index].set(sample_index, left)
          self.right_buffers[node_index].set(sample_index, right)
        }
        StereoMixDown => {
          let input_left = self_register_stereo_input_left(
            left_sv,
            self.self_left_values,
            node.input0,
            self.back_edge_input0_source[node_index],
          )
          let input_right = self_register_stereo_input_right(
            right_sv,
            self.self_right_values,
            node.input0,
            self.back_edge_input0_source[node_index],
          )
          let value = (input_left + input_right) * 0.5
          sample_values[node_index] = value
          self.buffers[node_index].set(sample_index, value)
        }
        StereoOutput => {
          // Stereo terminal: reads stereo input into left/right buffers.
          // In mono graphs this arm never matches (no StereoOutput node present).
          let input_left = self_register_stereo_input_left(
            left_sv,
            self.self_left_values,
            node.input0,
            self.back_edge_input0_source[node_index],
          )
          let input_right = self_register_stereo_input_right(
            right_sv,
            self.self_right_values,
            node.input0,
            self.back_edge_input0_source[node_index],
          )
          left_sv[node_index] = input_left
          right_sv[node_index] = input_right
          self.left_buffers[node_index].set(sample_index, input_left)
          self.right_buffers[node_index].set(sample_index, input_right)
        }
      }
      // Update self-register for feedback sources
      if self.self_enabled[node_index] {
        self.self_values[node_index] = sample_values[node_index]
        self.self_left_values[node_index] = left_sv[node_index]
        self.self_right_values[node_index] = right_sv[node_index]
      }
    }
  }

  // Final output copy: mono copies one buffer, stereo copies two
  if is_stereo {
    copy_stereo_buffers(
      self.left_buffers[self.left_buffers.length() - 1],
      self.right_buffers[self.right_buffers.length() - 1],
      left_output,
      right_output,
      sample_count,
    )
  } else {
    copy_buffer(self.buffers[self.buffers.length() - 1], output, sample_count)
  }
}

///|
fn CompiledDsp::process_feedback_graph(
  self : CompiledDsp,
  context : DspContext,
  output : AudioBuffer,
  sample_count : Int,
) -> Unit {
  // Mono wrapper: left_output/right_output are unused — pass output as a
  // harmless placeholder (is_stereo=false means only the mono copy fires).
  self.0.process_feedback_graph_impl(
    context, output, output, output, false, sample_count,
  )
}

///|
fn CompiledStereoDsp::process_feedback_graph(
  self : CompiledStereoDsp,
  context : DspContext,
  left_output : AudioBuffer,
  right_output : AudioBuffer,
  sample_count : Int,
) -> Unit {
  // Stereo wrapper: output is unused — pass left_output as a harmless
  // placeholder (is_stereo=true means only the stereo copy fires).
  self.0.process_feedback_graph_impl(
    context, left_output, left_output, right_output, true, sample_count,
  )
}

///|
fn process_graph_biquad(
  biquad : Biquad,
  context : DspContext,
  buffer : AudioBuffer,
  mode : BiquadMode,
  cutoff : Double,
  q : Double,
) -> Unit {
  if !biquad.update(context, mode, cutoff, q) {
    buffer.fill(0.0)
    return
  }
  biquad.process(context, buffer)
}

///|
/// Re-derive biquad coefficients for all filter nodes before feedback processing.
/// Called once per block — coefficients depend on sample rate and cutoff/Q which
/// may have changed via runtime parameter control.
fn CompiledGraph::prepare_feedback_biquads(
  self : CompiledGraph,
  context : DspContext,
) -> Unit {
  for index = 0; index < self.nodes.length(); index = index + 1 {
    let node = self.nodes[index]
    match node.kind {
      Biquad =>
        ignore(
          self.biquad_states[index]
          .unwrap()
          .update(context, node.filter_mode, node.value0, node.value1),
        )
      StereoBiquad => {
        ignore(
          self.stereo_biquad_left_states[index]
          .unwrap()
          .update(context, node.filter_mode, node.value0, node.value1),
        )
        ignore(
          self.stereo_biquad_right_states[index]
          .unwrap()
          .update(context, node.filter_mode, node.value0, node.value1),
        )
      }
      _ => ()
    }
  }
}

///|
fn self_register_input_sample(
  sample_values : FixedArray[Double],
  self_values : FixedArray[Double],
  input_index : Int,
  back_edge_source : Int,
) -> Double {
  if back_edge_source >= 0 {
    self_values[back_edge_source]
  } else if input_index >= 0 {
    sample_values[input_index]
  } else {
    0.0
  }
}

///|
fn self_register_stereo_input_left(
  left_sv : FixedArray[Double],
  self_left_values : FixedArray[Double],
  input_index : Int,
  back_edge_source : Int,
) -> Double {
  if back_edge_source >= 0 {
    self_left_values[back_edge_source]
  } else if input_index >= 0 {
    left_sv[input_index]
  } else {
    0.0
  }
}

///|
fn self_register_stereo_input_right(
  right_sv : FixedArray[Double],
  self_right_values : FixedArray[Double],
  input_index : Int,
  back_edge_source : Int,
) -> Double {
  if back_edge_source >= 0 {
    self_right_values[back_edge_source]
  } else if input_index >= 0 {
    right_sv[input_index]
  } else {
    0.0
  }
}

///|
fn clip_feedback_sample(value : Double, threshold : Double) -> Double {
  if value > threshold {
    threshold
  } else if value < -threshold {
    -threshold
  } else {
    value
  }
}

///|
fn reset_graph_env_states(env_states : FixedArray[Adsr?]) -> Unit {
  for index = 0; index < env_states.length(); index = index + 1 {
    match env_states[index] {
      Some(env) => env.reset()
      None => ()
    }
  }
}

///|
fn clear_compiled_buffers(buffers : FixedArray[AudioBuffer]) -> Unit {
  for index = 0; index < buffers.length(); index = index + 1 {
    buffers[index].fill(0.0)
  }
}

///|
fn is_mono_shape(shape : Int?) -> Bool {
  match shape {
    Some(MONO_SIGNAL_SHAPE) => true
    _ => false
  }
}