///|
/// A uniformly sampled tachogram used by spectral analysis.
pub(all) struct Tachogram {
  sample_rate_hz : Double
  timestamps_s : Array[Double]
  values_ms : Array[Double]
} derive(FromJson, ToJson, Debug, Eq)

///|
/// Window functions for periodogram input.
pub(all) enum WindowFunction {
  Rectangular
  Hann
  Hamming
  Blackman
} derive(FromJson, ToJson, Debug, Eq)

///|
/// Convert RR durations into cumulative beat timestamps.
pub fn rr_timestamps(intervals : Array[Double]) -> Array[Double] {
  let result = []
  let mut elapsed = 0.0
  for interval in intervals {
    elapsed += interval / 1000.0
    result.push(elapsed)
  }
  result
}

///|
/// Linearly interpolate a value at a timestamp, clamping at the endpoints.
pub fn interpolate_at(
  timestamps : Array[Double],
  values : Array[Double],
  timestamp : Double,
) -> Double {
  let n = if timestamps.length() < values.length() {
    timestamps.length()
  } else {
    values.length()
  }
  if n == 0 {
    return 0.0
  }
  if n == 1 || timestamp <= timestamps[0] {
    return values[0]
  }
  if timestamp >= timestamps[n - 1] {
    return values[n - 1]
  }
  let mut right = 1
  while right < n && timestamps[right] < timestamp {
    right += 1
  }
  let left = right - 1
  let width = timestamps[right] - timestamps[left]
  if width <= 0.0 {
    values[left]
  } else {
    let fraction = (timestamp - timestamps[left]) / width
    values[left] + fraction * (values[right] - values[left])
  }
}

///|
/// Resample the instantaneous RR series onto an evenly spaced time grid.
pub fn resample_rr(
  intervals : Array[Double],
  sample_rate_hz : Double,
) -> Tachogram {
  if intervals.length() == 0 || sample_rate_hz <= 0.0 {
    return { sample_rate_hz, timestamps_s: [], values_ms: [] }
  }
  let timestamps = rr_timestamps(intervals)
  let duration = timestamps[timestamps.length() - 1]
  let count = (duration * sample_rate_hz).floor().to_int() + 1
  let output_timestamps = []
  let output_values = []
  for i in 0.. Array[Double] {
  let trend = fit_linear_trend(values)
  let result = []
  for i in 0.. Double {
  if length <= 1 {
    return 1.0
  }
  let phase = 2.0 * @math.PI * index.to_double() / (length - 1).to_double()
  match function {
    Rectangular => 1.0
    Hann => 0.5 * (1.0 - @math.cos(phase))
    Hamming => 0.54 - 0.46 * @math.cos(phase)
    Blackman => 0.42 - 0.5 * @math.cos(phase) + 0.08 * @math.cos(2.0 * phase)
  }
}

///|
/// Apply a window without changing the input array.
pub fn apply_window(
  values : Array[Double],
  function : WindowFunction,
) -> Array[Double] {
  let result = []
  for i in 0.. Array[Double] {
  let centered = []
  let average = mean_value(values)
  for value in values {
    centered.push(value - average)
  }
  let detrended = if remove_trend { detrend_linear(centered) } else { centered }
  apply_window(detrended, function)
}

///|
/// Split a sequence into overlapping frames for streaming or spectral work.
pub fn frame_values(
  values : Array[Double],
  frame_size : Int,
  hop_size : Int,
) -> Array[Array[Double]] {
  let result = []
  if frame_size <= 0 || hop_size <= 0 || values.length() < frame_size {
    return result
  }
  let mut start = 0
  while start + frame_size <= values.length() {
    let frame = []
    for i in start..<(start + frame_size) {
      frame.push(values[i])
    }
    result.push(frame)
    start += hop_size
  }
  result
}

///|
/// Resample, center, detrend, and window an RR series in one call.
pub fn prepare_tachogram(
  intervals : Array[Double],
  sample_rate_hz : Double,
  remove_trend : Bool,
  function : WindowFunction,
) -> Tachogram {
  let tachogram = resample_rr(intervals, sample_rate_hz)
  let values = prepare_spectral_values(
    tachogram.values_ms,
    remove_trend,
    function,
  )
  {
    sample_rate_hz: tachogram.sample_rate_hz,
    timestamps_s: tachogram.timestamps_s,
    values_ms: values,
  }
}