///|
/// Normalization mode for Chroma feature vectors across time frames.
pub(all) enum ChromaNorm {
  None
  L1
  L2
  Max
} derive(Eq, Debug)

///|
/// Returns standard pitch class name for index 0..=11 (0 = C, 9 = A, etc.).
pub fn pitch_class_name(idx : Int) -> String {
  let note_idx = (idx % 12 + 12) % 12
  match note_idx {
    0 => "C"
    1 => "C#"
    2 => "D"
    3 => "D#"
    4 => "E"
    5 => "F"
    6 => "F#"
    7 => "G"
    8 => "G#"
    9 => "A"
    10 => "A#"
    11 => "B"
    _ => "?"
  }
}

///|
/// Generates a 12-bin Chroma filterbank projecting frequency bins to 12 semitones.
/// Returns a FeatureMatrix of shape [12, n_bins].
pub fn chroma_filterbank(
  sample_rate : Int,
  n_fft : Int,
  tuning : Double,
  f_min : Double,
) -> @types.FeatureMatrix {
  if sample_rate <= 0 ||
    n_fft <= 0 ||
    !@types.is_finite(tuning) ||
    !@types.is_finite(f_min) {
    return @types.FeatureMatrix::zeros(12, 0)
  }
  let n_bins = n_fft / 2 + 1
  let n_chroma = 12
  let filterbank = @types.FeatureMatrix::zeros(n_chroma, n_bins)
  let delta_f = sample_rate.to_double() / n_fft.to_double()
  let ln2 = 0.6931471805599453

  for k in 1..= f_min && freq >= 1.0 {
      // Fractional MIDI note: A440 = 69
      let midi = 12.0 * (@math.ln(freq / 440.0) / ln2) + 69.0 - tuning
      let mut pitch_class = midi % 12.0
      if pitch_class < 0.0 {
        pitch_class = pitch_class + 12.0
      }

      // Project onto 12 circular pitch classes using Gaussian octave profiles
      let sigma = 0.5
      let mut col_sum = 0.0
      for c in 0..<12 {
        let mut diff = (pitch_class - c.to_double()).abs()
        if diff > 6.0 {
          diff = 12.0 - diff
        }
        let weight = @math.exp(-0.5 * (diff / sigma) * (diff / sigma))
        filterbank.set(c, k, weight)
        col_sum = col_sum + weight
      }

      // Column-normalize across the 12 chroma bins
      if col_sum > 1.0e-12 {
        let inv_sum = 1.0 / col_sum
        for c in 0..<12 {
          filterbank.set(c, k, filterbank.at(c, k) * inv_sum)
        }
      }
    }
  }

  filterbank
}

///|
/// Extracts 12-bin Chroma representation from a 2D power/magnitude spectrogram.
/// tuning must be finite; f_min must be finite and in [0, sample_rate / 2].
pub fn chroma_from_spectrogram(
  spectrogram : @types.FeatureMatrix,
  sample_rate : Int,
  n_fft : Int,
  norm : ChromaNorm,
  tuning : Double,
  f_min : Double,
) -> Result[@types.FeatureMatrix, @types.MelError] {
  if sample_rate <= 0 {
    return Err(InvalidSampleRate(sample_rate))
  }
  if n_fft <= 0 {
    return Err(InvalidFftSize(n_fft))
  }
  if !@types.is_finite(tuning) {
    return Err(DimensionMismatch("Chroma tuning must be finite"))
  }
  let nyquist = sample_rate.to_double() / 2.0
  if !@types.is_finite(f_min) || f_min < 0.0 || f_min > nyquist {
    return Err(InvalidFrequencyRange(f_min, nyquist))
  }
  match spectrogram.validate() {
    Ok(_) => ()
    Err(e) => return Err(e)
  }
  let n_bins = n_fft / 2 + 1
  if spectrogram.n_rows != n_bins {
    return Err(
      DimensionMismatch(
        "Spectrogram frequency bins (\{spectrogram.n_rows}) do not match n_fft/2+1 (\{n_bins})",
      ),
    )
  }
  let n_frames = spectrogram.n_cols
  let n_chroma = 12
  let filterbank = chroma_filterbank(sample_rate, n_fft, tuning, f_min)
  let result = @types.FeatureMatrix::zeros(n_chroma, n_frames)

  // Matrix multiplication: [12, n_bins] x [n_bins, n_frames] -> [12, n_frames]
  for frame_idx in 0.. ()
      Max => {
        let mut max_val = 0.0
        for c in 0.. max_val {
            max_val = v
          }
        }
        if max_val > 1.0e-12 {
          let inv = 1.0 / max_val
          for c in 0.. {
        let mut sum_val = 0.0
        for c in 0.. 1.0e-12 {
          let inv = 1.0 / sum_val
          for c in 0.. {
        let mut sum_sq = 0.0
        for c in 0.. 1.0e-12 {
          let inv = 1.0 / norm_val
          for c in 0.. Result[@types.FeatureMatrix, @types.MelError] {
  match @stft.stft_spectrogram(samples, config) {
    Ok(spec) =>
      chroma_from_spectrogram(
        spec,
        config.sample_rate,
        config.n_fft,
        norm,
        0.0,
        config.f_min,
      )
    Err(e) => Err(e)
  }
}

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

///|
pub extend ChromaNorm with Eq::{not_equal, equal}