///|
/// Onset strength computation method.
pub(all) enum OnsetMethod {
  SpectralFlux
  SuperFlux(Int) // Frequency filter window size (radius = size / 2)
  Energy
} derive(Eq, Debug)

///|
/// Configuration for onset strength envelope extraction.
pub(all) struct OnsetConfig {
  mode : OnsetMethod
  normalize : Bool // Normalize envelope to [0.0, 1.0]
} derive(Eq, Debug)

///|
/// Default configuration: Spectral flux with normalization.
pub fn OnsetConfig::default() -> OnsetConfig {
  { mode: SpectralFlux, normalize: true, }
}

///|
/// Standard spectral flux: half-wave rectified difference across successive frames.
pub fn spectral_flux(spec : @types.FeatureMatrix) -> Array[Double] {
  let n_rows = spec.n_rows
  let n_cols = spec.n_cols
  let flux : Array[Double] = Array::make(n_cols, 0.0)
  if n_cols <= 1 {
    return flux
  }

  for c in 1.. 0.0 {
        sum = sum + diff
      }
    }
    flux[c] = sum
  }
  flux
}

///|
/// SuperFlux: spectral difference relative to maximum-filtered previous frame (Boeck & Widmer 2013).
/// Filter sizes <= 1 disable frequency spreading and use ordinary spectral flux.
pub fn super_flux(
  spec : @types.FeatureMatrix,
  filter_size : Int,
) -> Array[Double] {
  if filter_size <= 1 {
    return spectral_flux(spec)
  }
  let n_rows = spec.n_rows
  let n_cols = spec.n_cols
  let flux : Array[Double] = Array::make(n_cols, 0.0)
  if n_cols <= 1 {
    return flux
  }

  let radius = filter_size / 2

  for c in 1.. 0 { r - radius } else { 0 }
      let r_max = if r + radius < n_rows - 1 { r + radius } else { n_rows - 1 }
      let mut max_prev = spec.at(r_min, c - 1)
      for k in (r_min + 1)..<=r_max {
        let v = spec.at(k, c - 1)
        if v > max_prev {
          max_prev = v
        }
      }
      let diff = spec.at(r, c) - max_prev
      if diff > 0.0 {
        sum = sum + diff
      }
    }
    flux[c] = sum
  }
  flux
}

///|
/// Compute onset strength envelope from spectrogram.
pub fn onset_strength(
  spec : @types.FeatureMatrix,
  config : OnsetConfig,
) -> Array[Double] {
  let raw = match config.mode {
    SpectralFlux => spectral_flux(spec)
    SuperFlux(size) => super_flux(spec, size)
    Energy => {
      let n_cols = spec.n_cols
      let n_rows = spec.n_rows
      let out : Array[Double] = Array::make(n_cols, 0.0)
      if n_cols > 1 {
        let mut prev_energy = 0.0
        for r in 0.. 0.0 { diff } else { 0.0 }
          prev_energy = curr_energy
        }
      }
      out
    }
  }

  if config.normalize {
    let mut max_val = 0.0
    for v in raw {
      if v > max_val {
        max_val = v
      }
    }
    if max_val > 1.0e-12 {
      let inv = 1.0 / max_val
      for i in 0.. Array[Int] {
  let len = envelope.length()
  let peaks : Array[Int] = []
  if len == 0 {
    return peaks
  }

  let mut last_onset = -wait - 1

  for i in 0.. 0 { i - pre_max } else { 0 }
    let end_max = if i + post_max < len - 1 { i + post_max } else { len - 1 }
    let mut is_max = true
    for j in start_max..<=end_max {
      if j != i && envelope[j] >= val {
        is_max = false
        break
      }
    }
    if !is_max {
      continue
    }

    // 2. Check local average threshold in [i - pre_avg, i + post_avg]
    let start_avg = if i - pre_avg > 0 { i - pre_avg } else { 0 }
    let end_avg = if i + post_avg < len - 1 { i + post_avg } else { len - 1 }
    let count = end_avg - start_avg + 1
    let mut sum = 0.0
    for j in start_avg..<=end_avg {
      sum = sum + envelope[j]
    }
    let local_mean = sum / count.to_double()

    if val < local_mean + delta {
      continue
    }

    // 3. Check wait interval
    if i - last_onset >= wait {
      peaks.push(i)
      last_onset = i
    }
  }

  peaks
}

///|
/// Extract onset strength envelope directly from AudioBuffer.
pub fn onset_strength_audio(
  audio : @types.AudioBuffer,
  config : @types.FeatureConfig,
  onset_config? : OnsetConfig = OnsetConfig::default(),
) -> Result[Array[Double], @types.MelError] {
  match @stft.stft_spectrogram(audio.samples, config) {
    Ok(spec) => Ok(onset_strength(spec, onset_config))
    Err(e) => Err(e)
  }
}

///|
/// Detect onset event frame indices directly from AudioBuffer.
pub fn onset_detect_audio(
  audio : @types.AudioBuffer,
  config : @types.FeatureConfig,
  onset_config? : OnsetConfig = OnsetConfig::default(),
  delta? : Double = 0.05,
  wait? : Int = 3,
) -> Result[Array[Int], @types.MelError] {
  match onset_strength_audio(audio, config, onset_config~) {
    Ok(env) => Ok(peak_pick(env, delta~, wait~))
    Err(e) => Err(e)
  }
}

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

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

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

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