///|
const MAX_DEBUG_ERRORS : Int = 32

///|
pub(all) enum GraphValidationError {
  InvalidInput0(Int, Int, Int)
  InvalidInput1(Int, Int, Int)
  InvalidStateIndex(Int, String)
} derive(Debug, Eq)

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

///|
/// Check that node's input0 and input1 are within buffer bounds.
/// Called per node in debug mode only. Returns true if valid.
fn validate_node_inputs(
  node : DspNode,
  node_index : Int,
  buffer_count : Int,
  errors : Array[GraphValidationError],
) -> Bool {
  let max_valid = buffer_count - 1
  let mut valid = true
  if node.input0 >= 0 && node.input0 >= buffer_count {
    if errors.length() < MAX_DEBUG_ERRORS {
      errors.push(
        GraphValidationError::InvalidInput0(node_index, node.input0, max_valid),
      )
    }
    valid = false
  }
  if node.input1 >= 0 && node.input1 >= buffer_count {
    if errors.length() < MAX_DEBUG_ERRORS {
      errors.push(
        GraphValidationError::InvalidInput1(node_index, node.input1, max_valid),
      )
    }
    valid = false
  }
  valid
}

///|
/// Check that stateful node has its required state present.
/// Called per node in debug mode only. Returns true if valid.
fn validate_node_state(
  graph : CompiledGraph,
  node : DspNode,
  node_index : Int,
  errors : Array[GraphValidationError],
) -> Bool {
  let missing = match node.kind {
    Oscillator =>
      if graph.osc_states[node_index] is None {
        Some("osc")
      } else {
        None
      }
    Noise =>
      if graph.noise_states[node_index] is None {
        Some("noise")
      } else {
        None
      }
    Adsr =>
      if graph.env_states[node_index] is None {
        Some("env")
      } else {
        None
      }
    Biquad =>
      if graph.biquad_states[node_index] is None {
        Some("biquad")
      } else {
        None
      }
    Delay =>
      if graph.delay_states[node_index] is None {
        Some("delay")
      } else {
        None
      }
    StereoBiquad =>
      if graph.stereo_biquad_left_states[node_index] is None ||
        graph.stereo_biquad_right_states[node_index] is None {
        Some("stereo_biquad")
      } else {
        None
      }
    StereoDelay =>
      if graph.stereo_delay_left_states[node_index] is None ||
        graph.stereo_delay_right_states[node_index] is None {
        Some("stereo_delay")
      } else {
        None
      }
    _ => None
  }
  match missing {
    Some(kind) => {
      if errors.length() < MAX_DEBUG_ERRORS {
        errors.push(GraphValidationError::InvalidStateIndex(node_index, kind))
      }
      false
    }
    None => true
  }
}

///|
/// Shared debug process loop for both mono and stereo graphs.
/// Validates each node's inputs and state before processing.
/// On violation, fills output with silence and skips to next node.
///
/// Delegates per-node processing to process_node_block (graph_process.mbt),
/// the single source of truth for block-at-a-time node dispatch. Debug mode
/// adds validation before each node; the processing itself is identical.
fn process_debug_loop(
  graph : CompiledGraph,
  context : DspContext,
  sample_count : Int,
) -> Unit {
  graph.last_validation_errors = []
  let buffer_count = graph.buffers.length()
  let pan = Pan::new()
  for index = 0; index < graph.nodes.length(); index = index + 1 {
    let node = graph.nodes[index]
    if !validate_node_inputs(
        node,
        index,
        buffer_count,
        graph.last_validation_errors,
      ) ||
      !validate_node_state(graph, node, index, graph.last_validation_errors) {
      // WHY zero all three buffers: stereo nodes (Pan, StereoGain, etc.) write
      // to left_buffers/right_buffers, not the mono buffer. Without zeroing all
      // three, downstream stereo consumers would see stale data from the
      // previous block instead of silence.
      graph.buffers[index].fill(0.0)
      graph.left_buffers[index].fill(0.0)
      graph.right_buffers[index].fill(0.0)
      continue index + 1
    }
    process_node_block(graph, index, context, sample_count, pan)
  }
}

///|
/// Validate all nodes once (inputs in bounds, required state present).
/// Resets `last_validation_errors` and returns true when every node passes.
fn validate_all_nodes(graph : CompiledGraph) -> Bool {
  graph.last_validation_errors = []
  let buffer_count = graph.buffers.length()
  for index = 0; index < graph.nodes.length(); index = index + 1 {
    let node = graph.nodes[index]
    ignore(
      validate_node_inputs(
        node,
        index,
        buffer_count,
        graph.last_validation_errors,
      ),
    )
    ignore(
      validate_node_state(graph, node, index, graph.last_validation_errors),
    )
  }
  graph.last_validation_errors.length() == 0
}

///|
/// Debug variant of mono process. Validates each node before processing.
/// For feedback graphs, validates once per block then delegates to the
/// production per-sample feedback path for bit-identical output.
fn CompiledDsp::process_mono_debug(
  self : CompiledDsp,
  context : DspContext,
  output : AudioBuffer,
  sample_count : Int,
) -> Unit {
  if self.0.feedback_edges.length() > 0 {
    if validate_all_nodes(self.0) {
      self.process_feedback_graph(context, output, sample_count)
    } else {
      output.fill(0.0)
    }
    self.0.last_sanitized_count = @dsp.sanitize_buffer(output, sample_count)
    return
  }
  process_debug_loop(self.0, 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)
}

///|
/// Debug variant of stereo process. Validates each node before processing.
/// For feedback graphs, validates once per block then delegates to the
/// production per-sample feedback path for bit-identical output.
fn CompiledStereoDsp::process_stereo_debug(
  self : CompiledStereoDsp,
  context : DspContext,
  left_output : AudioBuffer,
  right_output : AudioBuffer,
  sample_count : Int,
) -> Unit {
  if self.0.feedback_edges.length() > 0 {
    if validate_all_nodes(self.0) {
      self.process_feedback_graph(
        context, left_output, right_output, sample_count,
      )
    } else {
      left_output.fill(0.0)
      right_output.fill(0.0)
    }
    self.0.last_sanitized_count = @dsp.sanitize_buffer(
        left_output, sample_count,
      ) +
      @dsp.sanitize_buffer(right_output, sample_count)
    return
  }
  process_debug_loop(self.0, 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)
}