///|
/// Process one node for a full block. Shared dispatch for mono, stereo, and
/// debug process loops. Both Output and StereoOutput arms are present —
/// compile-time validation guarantees only the appropriate one fires.
fn process_node_block(
  graph : CompiledGraph,
  index : Int,
  context : DspContext,
  sample_count : Int,
  pan : Pan,
) -> Unit {
  let node = graph.nodes[index]
  let buffer = graph.buffers[index]
  match node.kind {
    Constant => fill_constant_buffer(buffer, sample_count, node.value0)
    Oscillator =>
      if node.input0 >= 0 {
        let osc = graph.osc_states[index].unwrap()
        let freq_buf = graph.buffers[node.input0]
        let sr = context.sample_rate()
        for i in 0.. graph.noise_states[index].unwrap().process(context, buffer)
    Adsr => graph.env_states[index].unwrap().process(context, buffer)
    Biquad => {
      copy_buffer(graph.buffers[node.input0], buffer, sample_count)
      process_graph_biquad(
        graph.biquad_states[index].unwrap(),
        context,
        buffer,
        node.filter_mode,
        node.value0,
        node.value1,
      )
    }
    Delay => {
      copy_buffer(graph.buffers[node.input0], buffer, sample_count)
      graph.delay_states[index].unwrap().process(context, buffer)
    }
    Gain => {
      copy_buffer(graph.buffers[node.input0], buffer, sample_count)
      if node.input1 >= 0 {
        // Envelope modulation: multiply by input1 buffer (e.g. ADSR), then scale
        apply_envelope_gain_buffer(
          buffer,
          graph.buffers[node.input1],
          sample_count,
          node.value0,
        )
      } else {
        apply_gain_buffer(buffer, sample_count, node.value0)
      }
    }
    Mul =>
      multiply_buffers(
        graph.buffers[node.input0],
        graph.buffers[node.input1],
        buffer,
        sample_count,
      )
    Mix =>
      mix_buffers(
        graph.buffers[node.input0],
        graph.buffers[node.input1],
        buffer,
        sample_count,
      )
    Clip => {
      copy_buffer(graph.buffers[node.input0], buffer, sample_count)
      clip_buffer(buffer, sample_count, node.value0)
    }
    Output => copy_buffer(graph.buffers[node.input0], buffer, sample_count)
    Pan =>
      pan.process(
        context~,
        input=graph.buffers[node.input0],
        left_output=graph.left_buffers[index],
        right_output=graph.right_buffers[index],
        position=node.value0,
      )
    StereoGain => {
      copy_stereo_buffers(
        graph.left_buffers[node.input0],
        graph.right_buffers[node.input0],
        graph.left_buffers[index],
        graph.right_buffers[index],
        sample_count,
      )
      apply_gain_buffer(graph.left_buffers[index], sample_count, node.value0)
      apply_gain_buffer(graph.right_buffers[index], sample_count, node.value0)
    }
    StereoClip => {
      copy_stereo_buffers(
        graph.left_buffers[node.input0],
        graph.right_buffers[node.input0],
        graph.left_buffers[index],
        graph.right_buffers[index],
        sample_count,
      )
      clip_buffer(graph.left_buffers[index], sample_count, node.value0)
      clip_buffer(graph.right_buffers[index], sample_count, node.value0)
    }
    StereoBiquad => {
      copy_stereo_buffers(
        graph.left_buffers[node.input0],
        graph.right_buffers[node.input0],
        graph.left_buffers[index],
        graph.right_buffers[index],
        sample_count,
      )
      process_graph_biquad(
        graph.stereo_biquad_left_states[index].unwrap(),
        context,
        graph.left_buffers[index],
        node.filter_mode,
        node.value0,
        node.value1,
      )
      process_graph_biquad(
        graph.stereo_biquad_right_states[index].unwrap(),
        context,
        graph.right_buffers[index],
        node.filter_mode,
        node.value0,
        node.value1,
      )
    }
    StereoDelay => {
      copy_stereo_buffers(
        graph.left_buffers[node.input0],
        graph.right_buffers[node.input0],
        graph.left_buffers[index],
        graph.right_buffers[index],
        sample_count,
      )
      graph.stereo_delay_left_states[index]
      .unwrap()
      .process(context, graph.left_buffers[index])
      graph.stereo_delay_right_states[index]
      .unwrap()
      .process(context, graph.right_buffers[index])
    }
    StereoMixDown =>
      mixdown_stereo_buffers(
        graph.left_buffers[node.input0],
        graph.right_buffers[node.input0],
        buffer,
        sample_count,
      )
    StereoOutput =>
      copy_stereo_buffers(
        graph.left_buffers[node.input0],
        graph.right_buffers[node.input0],
        graph.left_buffers[index],
        graph.right_buffers[index],
        sample_count,
      )
  }
}

///|
/// Run every node through `process_node_block` for one block.
///
/// Shared by both mono and stereo process paths. Callers handle the
/// variant-specific output copy and sanitization themselves.
fn CompiledGraph::run_node_loop(
  self : CompiledGraph,
  context : DspContext,
  sample_count : Int,
) -> Unit {
  let pan = Pan::new()
  for index in 0.. Unit {
  let sample_rate = context.sample_rate()
  let sample_count = @dsp.effective_sample_count(context, output)
  if !@dsp.is_finite_positive(sample_rate) {
    clear_compiled_buffers(self.0.buffers)
    clear_compiled_buffers(self.0.left_buffers)
    clear_compiled_buffers(self.0.right_buffers)
    reset_graph_env_states(self.0.env_states)
    output.fill(0.0)
    self.0.last_sanitized_count = 0
    return
  }
  if sample_count <= 0 || sample_count > self.compiled_buffer_capacity() {
    clear_compiled_buffers(self.0.buffers)
    clear_compiled_buffers(self.0.left_buffers)
    clear_compiled_buffers(self.0.right_buffers)
    output.fill(0.0)
    self.0.last_sanitized_count = 0
    return
  }
  if self.0.debug_validate {
    self.process_mono_debug(context, output, sample_count)
    return
  }
  if self.0.feedback_edges.length() > 0 {
    self.process_feedback_graph(context, output, sample_count)
    self.0.last_sanitized_count = @dsp.sanitize_buffer(output, sample_count)
    return
  }
  self.0.run_node_loop(context, sample_count)
  copy_buffer(self.0.buffers[self.0.buffers.length() - 1], output, sample_count)
  self.0.last_sanitized_count = @dsp.sanitize_buffer(output, sample_count)
}

///|
/// Run the compiled stereo graph for one buffer and write the final output into
/// explicit left and right buffers.
pub fn CompiledStereoDsp::process(
  self : CompiledStereoDsp,
  context : DspContext,
  left_output : AudioBuffer,
  right_output : AudioBuffer,
) -> Unit {
  let sample_rate = context.sample_rate()
  let sample_count = compiled_stereo_sample_count(
    context, left_output, right_output,
  )
  if !@dsp.is_finite_positive(sample_rate) {
    clear_compiled_buffers(self.0.buffers)
    clear_compiled_buffers(self.0.left_buffers)
    clear_compiled_buffers(self.0.right_buffers)
    reset_graph_env_states(self.0.env_states)
    left_output.fill(0.0)
    right_output.fill(0.0)
    self.0.last_sanitized_count = 0
    return
  }
  if sample_count <= 0 || sample_count > self.compiled_buffer_capacity() {
    clear_compiled_buffers(self.0.buffers)
    clear_compiled_buffers(self.0.left_buffers)
    clear_compiled_buffers(self.0.right_buffers)
    left_output.fill(0.0)
    right_output.fill(0.0)
    self.0.last_sanitized_count = 0
    return
  }
  if self.0.debug_validate {
    self.process_stereo_debug(context, left_output, right_output, sample_count)
    return
  }
  if self.0.feedback_edges.length() > 0 {
    self.process_feedback_graph(
      context, left_output, right_output, sample_count,
    )
    self.0.last_sanitized_count = @dsp.sanitize_buffer(
        left_output, sample_count,
      ) +
      @dsp.sanitize_buffer(right_output, sample_count)
    return
  }
  self.0.run_node_loop(context, sample_count)
  copy_stereo_buffers(
    self.0.left_buffers[self.0.left_buffers.length() - 1],
    self.0.right_buffers[self.0.right_buffers.length() - 1],
    left_output,
    right_output,
    sample_count,
  )
  self.0.last_sanitized_count = @dsp.sanitize_buffer(left_output, sample_count) +
    @dsp.sanitize_buffer(right_output, sample_count)
}

///|
fn compiled_stereo_sample_count(
  context : DspContext,
  left_output : AudioBuffer,
  right_output : AudioBuffer,
) -> Int {
  let output_length = if left_output.length() < right_output.length() {
    left_output.length()
  } else {
    right_output.length()
  }
  if output_length < context.block_size() {
    output_length
  } else {
    context.block_size()
  }
}

///|