///| Real-time streaming audio feature extractor.
/// Reuses FFT buffers and a filterbank; returned matrices and arbitrary-size transforms allocate.
/// Streaming uses left-aligned complete frames, independent of config.center.

///|
pub(all) struct StreamingExtractor {
  config : @types.FeatureConfig
  window : Array[Double]
  filterbank : @types.FeatureMatrix
  mut buffer : Array[Double]
  mut buffer_start : Int
  fft_re : Array[Double]
  fft_im : Array[Double]
  spec_buffer : Array[Double]
}

///|
/// Create a new streaming extractor with precomputed window and filterbank.
pub fn StreamingExtractor::new(
  config? : @types.FeatureConfig = @types.FeatureConfig::default(),
) -> Result[StreamingExtractor, @types.MelError] {
  match config.validate() {
    Ok(_) => ()
    Err(e) => return Err(e)
  }

  let n_fft = config.n_fft
  let n_bins = n_fft / 2 + 1
  let win = @window.create_window(config.window, config.win_length)
  let fb = match @mel.mel_filterbank(config) {
    Ok(f) => f
    Err(e) => return Err(e)
  }

  Ok({
    config,
    window: win,
    filterbank: fb,
    buffer: [],
    buffer_start: 0,
    fft_re: Array::make(n_fft, 0.0),
    fft_im: Array::make(n_fft, 0.0),
    spec_buffer: Array::make(n_bins, 0.0),
  })
}

///|
/// Push newly arrived audio samples to the streaming buffer.
pub fn StreamingExtractor::push_samples(
  self : StreamingExtractor,
  chunk : Array[Double],
) -> Unit {
  for sample in chunk {
    self.buffer.push(sample)
  }
}

///|
/// Returns the number of ready full frames available for extraction.
pub fn StreamingExtractor::available_frames(self : StreamingExtractor) -> Int {
  let n_fft = self.config.n_fft
  let hop = self.config.hop_length
  let len = self.buffer.length() - self.buffer_start
  if len < n_fft {
    0
  } else {
    (len - n_fft) / hop + 1
  }
}

///|
/// Process and consume ready frames, returning a Mel Spectrogram matrix [n_mels][n_frames].
pub fn StreamingExtractor::pop_mel_frames(
  self : StreamingExtractor,
) -> @types.FeatureMatrix {
  let n_frames = self.available_frames()
  let n_mels = self.config.n_mels
  if n_frames == 0 {
    return @types.FeatureMatrix::zeros(n_mels, 0)
  }

  let n_fft = self.config.n_fft
  let hop = self.config.hop_length
  let n_bins = n_fft / 2 + 1
  let out = @types.FeatureMatrix::zeros(n_mels, n_frames)

  for frame = 0; frame < n_frames; frame = frame + 1 {
    let offset = self.buffer_start + frame * hop

    @framing.window_frame(self.buffer, offset, self.window, self.fft_re)
    self.fft_im.fill(0.0)
    match @fft.transform_in_place(self.fft_re, self.fft_im, n_fft, false) {
      Ok(_) => ()
      Err(_) => abort("Validated streaming FFT buffers are inconsistent")
    }
    for i in 0.. 0.0 {
          mel_energy = mel_energy + weight * self.spec_buffer[k]
        }
      }
      out.set(m, frame, mel_energy)
    }
  }

  // The cursor can pass the current buffer for hop > n_fft. Preserve that
  // gap so the next arriving chunks resume on the original sample timeline.
  self.buffer_start = self.buffer_start + n_frames * hop
  if self.buffer_start >= self.buffer.length() {
    self.buffer_start = self.buffer_start - self.buffer.length()
    self.buffer = []
  } else if self.buffer_start >= self.buffer.length() / 2 {
    let remaining = self.buffer.length() - self.buffer_start
    let compacted = Array::make(remaining, 0.0)
    for i in 0.. @types.FeatureMatrix {
  let mel_mat = self.pop_mel_frames()
  let log_mel = match @mfcc.log_mel_from_mel(mel_mat, self.config) {
    Ok(value) => value
    Err(_) => abort("Validated streaming Mel matrix is inconsistent")
  }
  match @mfcc.mfcc_from_log_mel(log_mel, self.config) {
    Ok(value) => value
    Err(_) => abort("Validated streaming Log-Mel matrix is inconsistent")
  }
}

///|
/// Clear remaining samples in buffer.
pub fn StreamingExtractor::reset(self : StreamingExtractor) -> Unit {
  self.buffer = []
  self.buffer_start = 0
}