///|
/// Configuration for tempogram computation and tempo estimation.
pub(all) struct TempoConfig {
  min_bpm : Double // Minimum BPM bound (default 30.0)
  max_bpm : Double // Maximum BPM bound (default 300.0)
  start_bpm : Double // Prior mean tempo in BPM (default 120.0)
  std_bpm : Double // Prior standard deviation in log2-BPM space (default 1.0)
  win_length : Int // Analysis window in onset frames (default 192)
  hop_length : Int // Hop length in onset frames (default 1)
} derive(Debug)

///|
/// Default tempo configuration.
pub fn TempoConfig::default() -> TempoConfig {
  {
    min_bpm: 30.0,
    max_bpm: 300.0,
    start_bpm: 120.0,
    std_bpm: 1.0,
    win_length: 192,
    hop_length: 1,
  }
}

///|
/// Result of tempogram analysis.
pub(all) struct TempogramResult {
  tempogram : @types.FeatureMatrix // Matrix of shape [n_lags, n_frames]
  tempi : Array[Double] // BPM frequency associated with each lag row
  bpm : Double // Estimated primary global tempo in BPM
} derive(Debug)

///|
/// Log2 helper.
fn log2(x : Double) -> Double {
  @math.ln(x) / @math.ln(2.0)
}

///|
fn empty_tempogram() -> TempogramResult {
  { tempogram: @types.FeatureMatrix::zeros(0, 0), tempi: [], bpm: 0.0, }
}

///|
/// Compute autocorrelation tempogram and estimate global tempo from onset envelope.
/// Only integer lags inside the inclusive BPM range and analysis window are used.
/// Returns an empty result with bpm=0 when no such lag exists.
pub fn tempogram_from_onset(
  onset_env : Array[Double],
  sample_rate : Int,
  hop_length : Int,
  config? : TempoConfig = TempoConfig::default(),
) -> TempogramResult {
  let len = onset_env.length()
  if len < 2 ||
    sample_rate <= 0 ||
    hop_length <= 0 ||
    config.win_length < 2 ||
    !@types.is_finite(config.min_bpm) ||
    !@types.is_finite(config.max_bpm) ||
    !@types.is_finite(config.start_bpm) ||
    !@types.is_finite(config.std_bpm) ||
    config.min_bpm <= 0.0 ||
    config.max_bpm <= config.min_bpm ||
    config.start_bpm <= 0.0 ||
    config.std_bpm <= 0.0 {
    return empty_tempogram()
  }
  let fps = sample_rate.to_double() / hop_length.to_double()
  let w_len = if config.win_length < len { config.win_length } else { len }

  // BPM decreases with lag: round the lower lag up and the upper lag down.
  // Intersect in floating point before converting so tiny BPM bounds cannot
  // overflow Int or invent a lag beyond the available window.
  let lower_lag = (60.0 * fps / config.max_bpm).ceil()
  let upper_lag = (60.0 * fps / config.min_bpm).floor()
  let max_possible = w_len - 1
  if lower_lag > max_possible.to_double() ||
    upper_lag < 1.0 ||
    upper_lag < lower_lag {
    return empty_tempogram()
  }
  let tau_min = if lower_lag < 1.0 { 1 } else { lower_lag.to_int() }
  let tau_max = if upper_lag >= max_possible.to_double() {
    max_possible
  } else {
    upper_lag.to_int()
  }

  let n_lags = tau_max - tau_min + 1
  let tempi : Array[Double] = Array::make(n_lags, 0.0)
  for i in 0.. 0 { config.hop_length } else { 1 }
  let n_frames = if len >= w_len { (len - w_len) / step + 1 } else { 1 }
  let matrix = @types.FeatureMatrix::zeros(n_lags, n_frames)

  // Precompute periodic Hann window for autocorrelation
  let pi2 = 2.0 * @math.PI
  let window : Array[Double] = Array::make(w_len, 0.0)
  for j in 0.. 1.0e-12 { 1.0 / r0 } else { 0.0 }

    for i in 0.. best_score {
      best_score = score
      best_bpm = bpm_cand
    }
  }

  { tempogram: matrix, tempi, bpm: best_bpm, }
}

///|
/// Extract tempogram and estimate tempo directly from single-channel AudioBuffer.
pub fn tempogram(
  audio : @types.AudioBuffer,
  feature_config : @types.FeatureConfig,
  tempo_config? : TempoConfig = TempoConfig::default(),
) -> Result[TempogramResult, @types.MelError] {
  match @onset.onset_strength_audio(audio, feature_config) {
    Ok(env) =>
      Ok(
        tempogram_from_onset(
          env,
          audio.sample_rate,
          feature_config.hop_length,
          config=tempo_config,
        ),
      )
    Err(e) => Err(e)
  }
}

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

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