///|
/// Configuration for production-style event detection.
pub struct EventDetectionConfig {
  sta_samples : Int
  lta_samples : Int
  onset_threshold : Double
  offset_threshold : Double
  minimum_samples : Int
  merge_gap_samples : Int
  dt : Double
  start_time : Double
}

///|
/// A detected event with timing and signal quality measurements.
pub struct EventCandidate {
  on_index : Int
  off_index : Int
  peak_index : Int
  on_time : Double
  off_time : Double
  peak_time : Double
  peak_ratio : Double
  energy : Double
  signal_to_noise : Double
}

///|
/// Return a conservative default detector configuration.
pub fn default_event_config(
  dt : Double,
  start_time : Double,
) -> EventDetectionConfig {
  {
    sta_samples: 5,
    lta_samples: 20,
    onset_threshold: 2.0,
    offset_threshold: 1.2,
    minimum_samples: 3,
    merge_gap_samples: 2,
    dt,
    start_time,
  }
}

///|
/// Compute the short-to-long energy ratio with an O(N) prefix sum.
pub fn energy_ratio(
  data : Array[Double],
  short_samples : Int,
  long_samples : Int,
) -> Array[Double] {
  let result = Array::make(data.length(), 1.0)
  if data.is_empty() || short_samples <= 0 || long_samples <= short_samples {
    return result
  }
  let prefix = Array::make(data.length() + 1, 0.0)
  for index = 0; index < data.length(); index = index + 1 {
    prefix[index + 1] = prefix[index] + data[index] * data[index]
  }
  for index = long_samples - 1; index < data.length(); index = index + 1 {
    let long_start = index + 1 - long_samples
    let short_start = index + 1 - short_samples
    let long_energy = prefix[index + 1] - prefix[long_start]
    let short_energy = prefix[index + 1] - prefix[short_start]
    let long_mean = long_energy / long_samples.to_double()
    let short_mean = short_energy / short_samples.to_double()
    result[index] = if long_mean <= 0.0 { 1.0 } else { short_mean / long_mean }
  }
  result
}

///|
/// Compute a recursive STA/LTA ratio for streaming-friendly detection.
pub fn recursive_sta_lta(
  data : Array[Double],
  sta_samples : Int,
  lta_samples : Int,
) -> Array[Double] {
  let result = Array::make(data.length(), 1.0)
  if data.is_empty() || sta_samples <= 0 || lta_samples <= sta_samples {
    return result
  }
  let sta_decay = @math.exp(-1.0 / sta_samples.to_double())
  let lta_decay = @math.exp(-1.0 / lta_samples.to_double())
  let mut sta = 0.0
  let mut lta = 0.0
  for index = 0; index < data.length(); index = index + 1 {
    let amplitude = data[index].abs()
    sta = sta_decay * sta + (1.0 - sta_decay) * amplitude
    lta = lta_decay * lta + (1.0 - lta_decay) * amplitude
    result[index] = if lta <= 0.0 { 1.0 } else { sta / lta }
  }
  result
}

///|
/// Compute a short-time characteristic function from first differences.
pub fn difference_characteristic(data : Array[Double]) -> Array[Double] {
  let result = Array::make(data.length(), 0.0)
  if data.length() < 2 {
    return result
  }
  for index = 1; index < data.length(); index = index + 1 {
    result[index] = (data[index] - data[index - 1]).abs()
  }
  result
}

///|
/// Return the first index crossing a threshold after a quiet pre-event window.
pub fn first_arrival_index(
  data : Array[Double],
  threshold : Double,
  quiet_samples : Int,
) -> Int? {
  if data.is_empty() {
    return None
  }
  let quiet = if quiet_samples < 1 { 1 } else { quiet_samples }
  let baseline_data = slice_samples(data, 0, quiet)
  let baseline = mean_samples(baseline_data) +
    3.0 * standard_deviation(baseline_data)
  let effective = if threshold > baseline { threshold } else { baseline }
  for index = quiet; index < data.length(); index = index + 1 {
    if data[index].abs() >= effective {
      return Some(index)
    }
  }
  None
}

///|
/// Find the peak index and value within an inclusive event range.
fn event_peak(data : Array[Double], start : Int, end : Int) -> (Int, Double) {
  let first = if start < 0 { 0 } else { start }
  let last = if end >= data.length() { data.length() - 1 } else { end }
  if data.is_empty() || first > last {
    return (0, 0.0)
  }
  let mut peak_index = first
  let mut peak = data[first].abs()
  for index = first + 1; index <= last; index = index + 1 {
    if data[index].abs() > peak {
      peak = data[index].abs()
      peak_index = index
    }
  }
  (peak_index, peak)
}

///|
/// Merge adjacent trigger windows separated by at most `gap_samples`.
pub fn merge_triggers(
  triggers : Array[Trigger],
  gap_samples : Int,
) -> Array[Trigger] {
  let result = Array::new()
  if triggers.is_empty() {
    return result
  }
  let gap = if gap_samples < 0 { 0 } else { gap_samples }
  let mut current = triggers[0]
  for index = 1; index < triggers.length(); index = index + 1 {
    let next = triggers[index]
    if next.on_index - current.off_index <= gap + 1 {
      current = {
        on_index: current.on_index,
        off_index: if next.off_index > current.off_index {
          next.off_index
        } else {
          current.off_index
        },
        on_time: current.on_time,
        off_time: if next.off_time > current.off_time {
          next.off_time
        } else {
          current.off_time
        },
      }
    } else {
      result.push(current)
      current = next
    }
  }
  result.push(current)
  result
}

///|
/// Convert a trigger into a feature-rich event candidate.
fn candidate_from_trigger(
  trigger : Trigger,
  data : Array[Double],
  ratio : Array[Double],
  dt : Double,
) -> EventCandidate {
  let (peak_index, peak) = event_peak(data, trigger.on_index, trigger.off_index)
  let values = slice_samples(data, trigger.on_index, trigger.off_index + 1)
  let noise_end = if trigger.on_index < 20 { trigger.on_index } else { 20 }
  let noise = slice_samples(data, 0, noise_end)
  let noise_rms = rms_samples(noise)
  let snr = if noise_rms <= 0.0 {
    0.0
  } else {
    20.0 * @math.log10(peak / noise_rms)
  }
  let peak_ratio = if peak_index < ratio.length() {
    ratio[peak_index]
  } else {
    0.0
  }
  {
    on_index: trigger.on_index,
    off_index: trigger.off_index,
    peak_index,
    on_time: trigger.on_time,
    off_time: trigger.off_time,
    peak_time: trigger.on_time +
    (peak_index - trigger.on_index).to_double() * dt,
    peak_ratio,
    energy: signal_energy(values),
    signal_to_noise: snr,
  }
}

///|
/// Detect, merge, and characterize event windows.
pub fn detect_events(
  data : Array[Double],
  config : EventDetectionConfig,
) -> Array[EventCandidate] {
  let ratio = classic_sta_lta(data, config.sta_samples, config.lta_samples)
  let raw = trigger_onset(
    ratio,
    config.onset_threshold,
    config.offset_threshold,
    config.dt,
    config.start_time,
  )
  let merged = merge_triggers(raw, config.merge_gap_samples)
  let result = Array::new()
  for trigger in merged {
    if trigger.off_index - trigger.on_index + 1 >= config.minimum_samples {
      result.push(candidate_from_trigger(trigger, data, ratio, config.dt))
    }
  }
  result
}

///|
/// Estimate the energy in a physical time interval.
pub fn event_energy(
  trace : Trace,
  start_time : Double,
  end_time : Double,
) -> Double {
  if trace.sampling_rate <= 0.0 || end_time <= start_time {
    return 0.0
  }
  let first = ((start_time - trace.start_time) * trace.sampling_rate).to_int()
  let last = ((end_time - trace.start_time) * trace.sampling_rate).to_int() + 1
  signal_energy(slice_samples(trace.data, first, last)) / trace.sampling_rate
}

///|
/// Return events whose signal-to-noise estimate passes a minimum in dB.
pub fn filter_events_by_snr(
  events : Array[EventCandidate],
  minimum_db : Double,
) -> Array[EventCandidate] {
  events.filter(event => event.signal_to_noise >= minimum_db)
}

///|
/// Return the largest candidate by energy.
pub fn strongest_event(events : Array[EventCandidate]) -> EventCandidate? {
  if events.is_empty() {
    return None
  }
  let mut best = events[0]
  for index = 1; index < events.length(); index = index + 1 {
    if events[index].energy > best.energy {
      best = events[index]
    }
  }
  Some(best)
}

///|
/// Compute the duration of an event in seconds.
pub fn event_duration(event : EventCandidate) -> Double {
  event.off_time - event.on_time
}