///|
/// Window family used by short-time spectral analysis.
pub enum AnalysisWindow {
  Rectangular
  Hann
  Hamming
  Blackman
}

///|
/// Return all supported windows in a stable order for configuration UIs.
pub fn supported_analysis_windows() -> Array[AnalysisWindow] {
  [Rectangular, Hann, Hamming, Blackman]
}

///|
/// Return the Hann analysis-window tag.
pub fn analysis_window_hann() -> AnalysisWindow {
  Hann
}

///|
/// A single short-time analysis frame.
pub struct TimeFrequencyFrame {
  index : Int
  start_index : Int
  start_time : Double
  values : Array[Double]
  rms : Double
  peak : Double
  zero_crossings : Int
}

///|
/// A compact spectrogram whose rows are consecutive time frames.
pub struct Spectrogram {
  sample_rate : Double
  frame_size : Int
  hop_size : Int
  window : AnalysisWindow
  frames : Array[TimeFrequencyFrame]
  frequencies : Array[Double]
  powers : Array[Array[Double]]
}

///|
/// A time-frequency ridge point selected from a spectrogram row.
pub struct RidgePoint {
  frame_index : Int
  time : Double
  frequency : Double
  power : Double
  confidence : Double
}

///|
/// Summary statistics for a spectrogram or a selected band.
pub struct SpectrogramSummary {
  frame_count : Int
  frequency_bins : Int
  duration : Double
  total_energy : Double
  mean_energy : Double
  peak_frequency : Double
  peak_power : Double
  dominant_time : Double
  active_fraction : Double
}

///|
/// Return a normalized analysis window of the requested type.
pub fn analysis_window(kind : AnalysisWindow, length : Int) -> Array[Double] {
  if length <= 0 {
    return []
  }
  match kind {
    Rectangular => Array::make(length, 1.0)
    Hann => hann_window(length)
    Hamming => hamming_window(length)
    Blackman => blackman_window(length)
  }
}

///|
/// Clamp a frame size to a usable positive value.
pub fn safe_frame_size(requested : Int, sample_count : Int) -> Int {
  if requested <= 0 || sample_count <= 0 {
    0
  } else if requested > sample_count {
    sample_count
  } else {
    requested
  }
}

///|
/// Clamp a hop size to a usable positive value.
pub fn safe_hop_size(requested : Int, frame_size : Int) -> Int {
  if frame_size <= 0 {
    0
  } else if requested <= 0 {
    frame_size
  } else {
    requested
  }
}

///|
/// Count complete frames in a signal using a hop size.
pub fn complete_frame_count(
  sample_count : Int,
  frame_size : Int,
  hop_size : Int,
) -> Int {
  if sample_count <= 0 ||
    frame_size <= 0 ||
    hop_size <= 0 ||
    sample_count < frame_size {
    0
  } else {
    (sample_count - frame_size) / hop_size + 1
  }
}

///|
/// Return frame start indexes, optionally including a zero-padded tail.
pub fn frame_start_indexes(
  sample_count : Int,
  frame_size : Int,
  hop_size : Int,
  include_partial : Bool,
) -> Array[Int] {
  let result = Array::new()
  if sample_count <= 0 || frame_size <= 0 || hop_size <= 0 {
    return result
  }
  let mut start = 0
  while start < sample_count {
    if !include_partial && start + frame_size > sample_count {
      break
    }
    result.push(start)
    start += hop_size
  }
  result
}

///|
/// Calculate frame RMS with an explicit empty-frame policy.
pub fn frame_rms(values : Array[Double]) -> Double {
  rms_samples(values)
}

///|
/// Calculate the crest factor of one frame.
pub fn frame_crest_factor(values : Array[Double]) -> Double {
  let rms = rms_samples(values)
  if rms <= 0.0 {
    0.0
  } else {
    let peak = maximum_sample(values).abs().max(minimum_sample(values).abs())
    peak / rms
  }
}

///|
/// Estimate the mean absolute first difference of a frame.
pub fn frame_activity(values : Array[Double]) -> Double {
  if values.length() < 2 {
    0.0
  } else {
    let mut total = 0.0
    for index = 1; index < values.length(); index = index + 1 {
      total += (values[index] - values[index - 1]).abs()
    }
    total / (values.length() - 1).to_double()
  }
}

///|
/// Compute the power spectrum of one frame using the selected window.
pub fn frame_periodogram(
  values : Array[Double],
  sample_rate : Double,
  window : AnalysisWindow,
) -> Spectrum {
  periodogram(values, sample_rate, analysis_window(window, values.length()))
}

///|
/// Build a spectrogram from a signal. Short final frames are omitted.
pub fn make_spectrogram(
  data : Array[Double],
  sample_rate : Double,
  frame_size : Int,
  hop_size : Int,
  window : AnalysisWindow,
) -> Spectrogram {
  let safe_size = safe_frame_size(frame_size, data.length())
  let safe_hop = safe_hop_size(hop_size, safe_size)
  let starts = frame_start_indexes(data.length(), safe_size, safe_hop, false)
  let frames = Array::new(capacity=starts.length())
  let powers = Array::new(capacity=starts.length())
  let mut frequencies = []
  if safe_size == 0 {
    return {
      sample_rate: if sample_rate > 0.0 {
        sample_rate
      } else {
        0.0
      },
      frame_size: 0,
      hop_size: 0,
      window,
      frames,
      frequencies,
      powers,
    }
  }
  for frame_index = 0
      frame_index < starts.length()
      frame_index = frame_index + 1 {
    let start = starts[frame_index]
    let values = extract_window(data, start, safe_size, 0.0)
    let spectrum = frame_periodogram(values, sample_rate, window)
    if frame_index == 0 {
      frequencies = spectrum.frequencies.copy()
    }
    frames.push({
      index: frame_index,
      start_index: start,
      start_time: if sample_rate > 0.0 {
        start.to_double() / sample_rate
      } else {
        0.0
      },
      values,
      rms: frame_rms(values),
      peak: maximum_sample(values).abs().max(minimum_sample(values).abs()),
      zero_crossings: zero_crossing_count(values),
    })
    powers.push(spectrum.powers)
  }
  {
    sample_rate: if sample_rate > 0.0 {
      sample_rate
    } else {
      0.0
    },
    frame_size: safe_size,
    hop_size: safe_hop,
    window,
    frames,
    frequencies,
    powers,
  }
}

///|
/// Build a spectrogram from a trace and preserve its absolute start time.
pub fn trace_spectrogram(
  trace : Trace,
  frame_size : Int,
  hop_size : Int,
  window : AnalysisWindow,
) -> Spectrogram {
  let result = make_spectrogram(
    trace.data,
    trace.sampling_rate,
    frame_size,
    hop_size,
    window,
  )
  let frames = Array::new(capacity=result.frames.length())
  for frame in result.frames {
    frames.push({
      index: frame.index,
      start_index: frame.start_index,
      start_time: frame.start_time + trace.start_time,
      values: frame.values,
      rms: frame.rms,
      peak: frame.peak,
      zero_crossings: frame.zero_crossings,
    })
  }
  {
    sample_rate: result.sample_rate,
    frame_size: result.frame_size,
    hop_size: result.hop_size,
    window: result.window,
    frames,
    frequencies: result.frequencies,
    powers: result.powers,
  }
}

///|
/// Return a copy of a spectrogram with each frequency row normalized.
pub fn normalize_spectrogram(spectrogram : Spectrogram) -> Spectrogram {
  let normalized = Array::new(capacity=spectrogram.powers.length())
  for row in spectrogram.powers {
    normalized.push(normalize_spectral_power(row))
  }
  {
    sample_rate: spectrogram.sample_rate,
    frame_size: spectrogram.frame_size,
    hop_size: spectrogram.hop_size,
    window: spectrogram.window,
    frames: spectrogram.frames.copy(),
    frequencies: spectrogram.frequencies.copy(),
    powers: normalized,
  }
}

///|
/// Return the energy of each spectrogram frame in a frequency band.
pub fn spectrogram_band_energy(
  spectrogram : Spectrogram,
  band : SpectralBand,
) -> Array[Double] {
  let result = Array::make(spectrogram.powers.length(), 0.0)
  for row_index = 0
      row_index < spectrogram.powers.length()
      row_index = row_index + 1 {
    let row = spectrogram.powers[row_index]
    let count = if row.length() < spectrogram.frequencies.length() {
      row.length()
    } else {
      spectrogram.frequencies.length()
    }
    for bin = 0; bin < count; bin = bin + 1 {
      if spectral_band_contains(band, spectrogram.frequencies[bin]) {
        result[row_index] += row[bin].max(0.0)
      }
    }
  }
  result
}

///|
/// Return the dominant frequency in every frame.
pub fn spectrogram_dominant_frequencies(
  spectrogram : Spectrogram,
) -> Array[Double] {
  let result = Array::make(spectrogram.powers.length(), 0.0)
  for row_index = 0
      row_index < spectrogram.powers.length()
      row_index = row_index + 1 {
    let row = spectrogram.powers[row_index]
    if row.length() > 1 && spectrogram.frequencies.length() > 1 {
      let limit = if row.length() < spectrogram.frequencies.length() {
        row.length()
      } else {
        spectrogram.frequencies.length()
      }
      let mut best = 1
      for bin = 2; bin < limit; bin = bin + 1 {
        if row[bin] > row[best] {
          best = bin
        }
      }
      result[row_index] = spectrogram.frequencies[best]
    }
  }
  result
}

///|
/// Select the maximum-power bin from each spectrogram row.
pub fn spectrogram_ridge(
  spectrogram : Spectrogram,
  minimum_power : Double,
) -> Array[RidgePoint] {
  let result = Array::new()
  for row_index = 0
      row_index < spectrogram.powers.length()
      row_index = row_index + 1 {
    let row = spectrogram.powers[row_index]
    let limit = if row.length() < spectrogram.frequencies.length() {
      row.length()
    } else {
      spectrogram.frequencies.length()
    }
    if limit == 0 {
      continue
    }
    let mut best = 0
    for bin = 1; bin < limit; bin = bin + 1 {
      if row[bin] > row[best] {
        best = bin
      }
    }
    let total = sum_samples(row).max(1.0e-20)
    let power = row[best].max(0.0)
    if power >= minimum_power {
      result.push({
        frame_index: row_index,
        time: if row_index < spectrogram.frames.length() {
          spectrogram.frames[row_index].start_time
        } else {
          row_index.to_double() *
          spectrogram.hop_size.to_double() /
          spectrogram.sample_rate.max(1.0)
        },
        frequency: spectrogram.frequencies[best],
        power,
        confidence: power / total,
      })
    }
  }
  result
}

///|
/// Compute a stable summary, including active-frame occupancy.
pub fn summarize_spectrogram(
  spectrogram : Spectrogram,
  active_threshold : Double,
) -> SpectrogramSummary {
  let energies = spectrogram_band_energy(
    spectrogram,
    SpectralBand::new(0.0, 1.0e300),
  )
  let mut total = 0.0
  let mut active = 0
  let mut peak_power = 0.0
  let mut peak_frequency = 0.0
  let mut dominant_time = 0.0
  for energy in energies {
    total += energy
    if energy >= active_threshold {
      active += 1
    }
  }
  for row_index = 0
      row_index < spectrogram.powers.length()
      row_index = row_index + 1 {
    let row = spectrogram.powers[row_index]
    for bin = 0
        bin < row.length() && bin < spectrogram.frequencies.length()
        bin = bin + 1 {
      if row[bin] > peak_power {
        peak_power = row[bin]
        peak_frequency = spectrogram.frequencies[bin]
        if row_index < spectrogram.frames.length() {
          dominant_time = spectrogram.frames[row_index].start_time
        }
      }
    }
  }
  let mean_energy = if energies.is_empty() {
    0.0
  } else {
    total / energies.length().to_double()
  }
  {
    frame_count: spectrogram.powers.length(),
    frequency_bins: spectrogram.frequencies.length(),
    duration: if spectrogram.frames.is_empty() {
      0.0
    } else {
      let last = spectrogram.frames[spectrogram.frames.length() - 1]
      let frame_duration = if spectrogram.sample_rate > 0.0 {
        spectrogram.frame_size.to_double() / spectrogram.sample_rate
      } else {
        0.0
      }
      last.start_time + frame_duration
    },
    total_energy: total,
    mean_energy,
    peak_frequency,
    peak_power,
    dominant_time,
    active_fraction: if energies.is_empty() {
      0.0
    } else {
      active.to_double() / energies.length().to_double()
    },
  }
}

///|
/// Compute positive spectral flux between consecutive spectrogram rows.
pub fn spectrogram_flux(spectrogram : Spectrogram) -> Array[Double] {
  let result = Array::make(spectrogram.powers.length(), 0.0)
  if spectrogram.powers.length() < 2 {
    return result
  }
  for index = 1; index < spectrogram.powers.length(); index = index + 1 {
    let left = normalize_spectral_power(spectrogram.powers[index - 1])
    let right = normalize_spectral_power(spectrogram.powers[index])
    let count = if left.length() < right.length() {
      left.length()
    } else {
      right.length()
    }
    let mut energy = 0.0
    for bin = 0; bin < count; bin = bin + 1 {
      let change = right[bin] - left[bin]
      if change > 0.0 {
        energy += change * change
      }
    }
    result[index] = energy.sqrt()
  }
  result
}

///|
/// Compute frame-to-frame spectral similarity in `[0, 1]`.
pub fn spectrogram_similarity(
  left : Spectrogram,
  right : Spectrogram,
) -> Array[Double] {
  let count = if left.powers.length() < right.powers.length() {
    left.powers.length()
  } else {
    right.powers.length()
  }
  let result = Array::make(count, 0.0)
  for row = 0; row < count; row = row + 1 {
    let a = normalize_spectral_power(left.powers[row])
    let b = normalize_spectral_power(right.powers[row])
    let bins = if a.length() < b.length() { a.length() } else { b.length() }
    let mut dot = 0.0
    let mut norm_a = 0.0
    let mut norm_b = 0.0
    for bin = 0; bin < bins; bin = bin + 1 {
      dot += a[bin] * b[bin]
      norm_a += a[bin] * a[bin]
      norm_b += b[bin] * b[bin]
    }
    let denominator = (norm_a * norm_b).sqrt()
    result[row] = if denominator <= 0.0 { 0.0 } else { dot / denominator }
  }
  result
}

///|
/// Smooth a ridge with a moving median in the frequency direction.
pub fn smooth_ridge(
  points : Array[RidgePoint],
  radius : Int,
) -> Array[RidgePoint] {
  let result = Array::new(capacity=points.length())
  let window = if radius < 0 { 0 } else { radius }
  for index = 0; index < points.length(); index = index + 1 {
    let point = points[index]
    let values = Array::new()
    let start = if index < window { 0 } else { index - window }
    let end = if index + window + 1 > points.length() {
      points.length()
    } else {
      index + window + 1
    }
    for cursor = start; cursor < end; cursor = cursor + 1 {
      values.push(points[cursor].frequency)
    }
    result.push({
      frame_index: point.frame_index,
      time: point.time,
      frequency: median_samples(values),
      power: point.power,
      confidence: point.confidence,
    })
  }
  result
}

///|
/// Serialize a spectrogram ridge for downstream plotting tools.
pub fn ridge_to_csv(points : Array[RidgePoint]) -> String {
  let mut output = "frame,time,frequency,power,confidence\n"
  for point in points {
    output = output +
      point.frame_index.to_string() +
      "," +
      point.time.to_string() +
      "," +
      point.frequency.to_string() +
      "," +
      point.power.to_string() +
      "," +
      point.confidence.to_string() +
      "\n"
  }
  output
}

///|
/// Compute the ratio of high-band to low-band energy for every frame.
pub fn spectrogram_band_ratio(
  spectrogram : Spectrogram,
  low_band : SpectralBand,
  high_band : SpectralBand,
) -> Array[Double] {
  let low = spectrogram_band_energy(spectrogram, low_band)
  let high = spectrogram_band_energy(spectrogram, high_band)
  let result = Array::make(low.length().min(high.length()), 0.0)
  for index = 0; index < result.length(); index = index + 1 {
    result[index] = if low[index] <= 0.0 {
      0.0
    } else {
      high[index] / low[index]
    }
  }
  result
}

///|
/// Return the frame with the greatest RMS, if one exists.
pub fn loudest_time_frequency_frame(
  spectrogram : Spectrogram,
) -> TimeFrequencyFrame? {
  if spectrogram.frames.is_empty() {
    return None
  }
  let mut best = 0
  for index = 1; index < spectrogram.frames.length(); index = index + 1 {
    if spectrogram.frames[index].rms > spectrogram.frames[best].rms {
      best = index
    }
  }
  Some(spectrogram.frames[best])
}

///|
/// Estimate the time span whose frame energies exceed a threshold.
pub fn active_time_span(
  spectrogram : Spectrogram,
  threshold : Double,
) -> (Double, Double)? {
  let energies = spectrogram_band_energy(
    spectrogram,
    SpectralBand::new(0.0, 1.0e300),
  )
  let mut first = -1
  let mut last = -1
  for index = 0; index < energies.length(); index = index + 1 {
    if energies[index] >= threshold {
      if first < 0 {
        first = index
      }
      last = index
    }
  }
  if first < 0 || last < 0 {
    None
  } else {
    let start = if first < spectrogram.frames.length() {
      spectrogram.frames[first].start_time
    } else {
      0.0
    }
    let end = if last < spectrogram.frames.length() {
      spectrogram.frames[last].start_time +
      spectrogram.frame_size.to_double() / spectrogram.sample_rate.max(1.0)
    } else {
      start
    }
    Some((start, end))
  }
}

///|
/// Create a compact, deterministic summary line for a spectrogram.
pub fn spectrogram_summary_text(summary : SpectrogramSummary) -> String {
  "frames=" +
  summary.frame_count.to_string() +
  " bins=" +
  summary.frequency_bins.to_string() +
  " duration=" +
  summary.duration.to_string() +
  " energy=" +
  summary.total_energy.to_string() +
  " mean_energy=" +
  summary.mean_energy.to_string() +
  " peak_hz=" +
  summary.peak_frequency.to_string() +
  " peak_power=" +
  summary.peak_power.to_string() +
  " active_fraction=" +
  summary.active_fraction.to_string()
}