///|
/// Configuration for Octave Spectral Contrast feature extraction.
pub(all) struct ContrastConfig {
  n_bands : Int // Number of frequency octave sub-bands (default 6)
  f_min : Double // Minimum base frequency boundary in Hz (default 200.0)
  quantile : Double // Quantile fraction for valley and peak (default 0.02)
  linear : Bool // Output peak-minus-valley instead of dB difference
} derive(Debug)

///|
/// Default Spectral Contrast configuration.
pub fn ContrastConfig::default() -> ContrastConfig {
  { n_bands: 6, f_min: 200.0, quantile: 0.02, linear: false, }
}

///|
/// In-place insertion sort for Double arrays.
fn sort_doubles(arr : Array[Double]) -> Unit {
  let len = arr.length()
  for i in 1..= 0 && arr[j] > key {
      arr[j + 1] = arr[j]
      j = j - 1
    }
    arr[j + 1] = key
  }
}

///|
/// Empirical quantile estimation on sorted double array.
pub fn quantile_sorted(sorted : Array[Double], q : Double) -> Double {
  let len = sorted.length()
  if len == 0 {
    return 0.0
  }
  if len == 1 {
    return sorted[0]
  }
  let safe_q = if q < 0.0 {
    0.0
  } else if q > 1.0 {
    1.0
  } else if !@types.is_finite(q) {
    0.0
  } else {
    q
  }
  let pos = (len - 1).to_double() * safe_q
  let idx = pos.to_int()
  let frac = pos - idx.to_double()
  if idx >= len - 1 {
    sorted[len - 1]
  } else {
    sorted[idx] * (1.0 - frac) + sorted[idx + 1] * frac
  }
}

///|
/// Compute Octave Spectral Contrast matrix from magnitude spectrogram.
pub fn spectral_contrast(
  spec : @types.FeatureMatrix,
  sample_rate : Int,
  n_fft : Int,
  config? : ContrastConfig = ContrastConfig::default(),
) -> @types.FeatureMatrix {
  let n_rows = spec.n_rows
  let n_cols = spec.n_cols
  if sample_rate <= 0 ||
    n_fft <= 0 ||
    config.n_bands < 0 ||
    !@types.is_finite(config.f_min) ||
    config.f_min <= 0.0 ||
    !@types.is_finite(config.quantile) ||
    config.quantile < 0.0 ||
    config.quantile > 0.5 {
    return @types.FeatureMatrix::zeros(0, 0)
  }
  let n_bands = config.n_bands
  let num_features = n_bands + 1
  let result = @types.FeatureMatrix::zeros(num_features, n_cols)
  if n_rows == 0 || n_cols == 0 {
    return result
  }

  let nyquist = sample_rate.to_double() / 2.0
  let bin_hz = sample_rate.to_double() / n_fft.to_double()

  // Calculate octave frequency band edges
  let band_edges : Array[Int] = Array::make(num_features + 1, 0)
  band_edges[0] = 0

  let mut curr_f = config.f_min
  for b in 1..<=n_bands {
    let edge_f = if curr_f < nyquist { curr_f } else { nyquist }
    let bin = (edge_f / bin_hz + 0.5).to_int()
    let clamped_bin = if bin < n_rows { bin } else { n_rows - 1 }
    band_edges[b] = clamped_bin
    curr_f = curr_f * 2.0
  }
  band_edges[num_features] = n_rows - 1

  // Ensure monotonically non-decreasing band edges
  for b in 1..<=num_features {
    if band_edges[b] < band_edges[b - 1] {
      band_edges[b] = band_edges[b - 1]
    }
  }

  let eps = 1.0e-12
  let q_low = config.quantile
  let q_high = 1.0 - config.quantile

  for c in 0.. Result[@types.FeatureMatrix, @types.MelError] {
  match @stft.stft_spectrogram(audio.samples, feature_config) {
    Ok(spec) =>
      Ok(
        spectral_contrast(
          spec,
          audio.sample_rate,
          feature_config.n_fft,
          config=contrast_config,
        ),
      )
    Err(e) => Err(e)
  }
}

///|
pub extend ContrastConfig with @moonbitlang/core/debug.Debug::{to_repr}