///|
/// A bounded sample buffer for online waveform algorithms.
pub struct StreamingBuffer {
  capacity : Int
  data : Array[Double]
  total_samples : Int
}

///|
/// Online first and second moments using Welford's method.
pub struct RunningMoments {
  count : Int
  mean : Double
  m2 : Double
  minimum : Double
  maximum : Double
}

///|
/// Stateful STA/LTA trigger values for a streaming detector.
pub struct StreamingTriggerState {
  sta : Double
  lta : Double
  sample_index : Int
  triggered : Bool
  trigger_start : Int
  peak_ratio : Double
}

///|
/// Summary produced for one streaming block.
pub struct StreamingBlockSummary {
  start_index : Int
  end_index : Int
  sample_count : Int
  mean : Double
  rms : Double
  peak : Double
  ratio_peak : Double
  triggered : Bool
}

///|
/// Create an empty bounded buffer.
pub fn StreamingBuffer::new(capacity : Int) -> StreamingBuffer {
  { capacity: capacity.max(1), data: [], total_samples: 0 }
}

///|
/// Append a sample and retain only the newest capacity values.
pub fn StreamingBuffer::push(
  self : StreamingBuffer,
  value : Double,
) -> StreamingBuffer {
  let data = self.data.copy()
  data.push(value)
  if data.length() > self.capacity {
    ignore(data.remove(0))
  }
  { capacity: self.capacity, data, total_samples: self.total_samples + 1 }
}

///|
/// Append a block of samples to a bounded buffer.
pub fn StreamingBuffer::push_block(
  self : StreamingBuffer,
  values : Array[Double],
) -> StreamingBuffer {
  let mut result = self
  for value in values {
    result = result.push(value)
  }
  result
}

///|
/// Return the latest buffer values.
pub fn StreamingBuffer::values(self : StreamingBuffer) -> Array[Double] {
  self.data.copy()
}

///|
/// Return an empty online moment accumulator.
pub fn RunningMoments::new() -> RunningMoments {
  { count: 0, mean: 0.0, m2: 0.0, minimum: 0.0, maximum: 0.0 }
}

///|
/// Update moments with one value.
pub fn RunningMoments::update(
  self : RunningMoments,
  value : Double,
) -> RunningMoments {
  let count = self.count + 1
  let difference = value - self.mean
  let mean = self.mean + difference / count.to_double()
  let m2 = self.m2 + difference * (value - mean)
  {
    count,
    mean,
    m2,
    minimum: if self.count == 0 {
      value
    } else {
      self.minimum.min(value)
    },
    maximum: if self.count == 0 {
      value
    } else {
      self.maximum.max(value)
    },
  }
}

///|
/// Update moments with a complete block.
pub fn RunningMoments::update_block(
  self : RunningMoments,
  values : Array[Double],
) -> RunningMoments {
  let mut result = self
  for value in values {
    result = result.update(value)
  }
  result
}

///|
/// Return online population variance.
pub fn RunningMoments::variance(self : RunningMoments) -> Double {
  if self.count == 0 {
    0.0
  } else {
    self.m2 / self.count.to_double()
  }
}

///|
/// Return online standard deviation.
pub fn RunningMoments::standard_deviation(self : RunningMoments) -> Double {
  self.variance().sqrt()
}

///|
/// Initialize an online STA/LTA trigger.
pub fn StreamingTriggerState::new() -> StreamingTriggerState {
  {
    sta: 0.0,
    lta: 0.0,
    sample_index: 0,
    triggered: false,
    trigger_start: -1,
    peak_ratio: 0.0,
  }
}

///|
/// Advance a streaming STA/LTA state by one absolute sample.
pub fn StreamingTriggerState::update(
  self : StreamingTriggerState,
  value : Double,
  sta_samples : Int,
  lta_samples : Int,
  onset_threshold : Double,
  offset_threshold : Double,
) -> StreamingTriggerState {
  let sta_decay = @math.exp(-1.0 / sta_samples.max(1).to_double())
  let lta_decay = @math.exp(
    -1.0 / lta_samples.max(sta_samples.max(1)).to_double(),
  )
  let amplitude = value.abs()
  let sta = sta_decay * self.sta + (1.0 - sta_decay) * amplitude
  let lta = lta_decay * self.lta + (1.0 - lta_decay) * amplitude
  let ratio = if lta <= 0.0 { 1.0 } else { sta / lta }
  let triggered = if self.triggered {
    ratio >= offset_threshold
  } else {
    ratio >= onset_threshold
  }
  {
    sta,
    lta,
    sample_index: self.sample_index + 1,
    triggered,
    trigger_start: if !self.triggered && triggered {
      self.sample_index
    } else {
      self.trigger_start
    },
    peak_ratio: self.peak_ratio.max(ratio),
  }
}

///|
/// Consume a block and return the final state plus a block summary.
pub fn process_streaming_block(
  state : StreamingTriggerState,
  values : Array[Double],
  sta_samples : Int,
  lta_samples : Int,
  onset_threshold : Double,
  offset_threshold : Double,
) -> (StreamingTriggerState, StreamingBlockSummary) {
  let mut current = state
  let start = state.sample_index
  for value in values {
    current = current.update(
      value, sta_samples, lta_samples, onset_threshold, offset_threshold,
    )
  }
  (
    current,
    {
      start_index: start,
      end_index: current.sample_index,
      sample_count: values.length(),
      mean: mean_samples(values),
      rms: rms_samples(values),
      peak: absolute_peak(values),
      ratio_peak: current.peak_ratio,
      triggered: current.triggered,
    },
  )
}

///|
/// Compute a rolling RMS stream with a bounded history.
pub fn rolling_rms_stream(
  values : Array[Double],
  window : Int,
) -> Array[Double] {
  let result = Array::make(values.length(), 0.0)
  let size = window.max(1)
  let mut energy = 0.0
  for index = 0; index < values.length(); index = index + 1 {
    energy += values[index] * values[index]
    if index >= size {
      energy -= values[index - size] * values[index - size]
    }
    let count = (index + 1).min(size)
    result[index] = (energy / count.to_double()).sqrt()
  }
  result
}

///|
/// Compute a rolling mean stream using a prefix accumulator.
pub fn rolling_mean_stream(
  values : Array[Double],
  window : Int,
) -> Array[Double] {
  let result = Array::make(values.length(), 0.0)
  let size = window.max(1)
  let mut total = 0.0
  for index = 0; index < values.length(); index = index + 1 {
    total += values[index]
    if index >= size {
      total -= values[index - size]
    }
    result[index] = total / (index + 1).min(size).to_double()
  }
  result
}

///|
/// Return a block summary for every complete block.
pub fn summarize_stream_blocks(
  values : Array[Double],
  block_size : Int,
) -> Array[StreamingBlockSummary] {
  let result = Array::new()
  if block_size <= 0 {
    return result
  }
  let mut start = 0
  while start < values.length() {
    let end = (start + block_size).min(values.length())
    let block = slice_samples(values, start, end)
    result.push({
      start_index: start,
      end_index: end,
      sample_count: block.length(),
      mean: mean_samples(block),
      rms: rms_samples(block),
      peak: absolute_peak(block),
      ratio_peak: 0.0,
      triggered: false,
    })
    start = end
  }
  result
}

///|
/// Calculate a block-level drift from online moments.
pub fn running_drift(values : Array[Double], block_size : Int) -> Array[Double] {
  let result = Array::new()
  if block_size <= 0 {
    return result
  }
  let blocks = summarize_stream_blocks(values, block_size)
  if blocks.length() < 2 {
    return result
  }
  for index = 1; index < blocks.length(); index = index + 1 {
    result.push(blocks[index].mean - blocks[index - 1].mean)
  }
  result
}

///|
/// Return the fraction of blocks whose RMS exceeds a threshold.
pub fn active_block_fraction(
  values : Array[Double],
  block_size : Int,
  threshold : Double,
) -> Double {
  let blocks = summarize_stream_blocks(values, block_size)
  if blocks.is_empty() {
    0.0
  } else {
    blocks.filter(block => block.rms >= threshold).length().to_double() /
    blocks.length().to_double()
  }
}

///|
/// Format an online moment record.
pub fn running_moments_text(moments : RunningMoments) -> String {
  "count=" +
  moments.count.to_string() +
  " mean=" +
  moments.mean.to_string() +
  " variance=" +
  moments.variance().to_string() +
  " minimum=" +
  moments.minimum.to_string() +
  " maximum=" +
  moments.maximum.to_string()
}

///|
/// Format a streaming trigger state.
pub fn streaming_trigger_text(state : StreamingTriggerState) -> String {
  "index=" +
  state.sample_index.to_string() +
  " sta=" +
  state.sta.to_string() +
  " lta=" +
  state.lta.to_string() +
  " triggered=" +
  state.triggered.to_string() +
  " start=" +
  state.trigger_start.to_string() +
  " peak_ratio=" +
  state.peak_ratio.to_string()
}