///|
/// Window aggregation helpers for dashboards and batch analysis.

///|
/// One scalar window observation.
pub(all) struct ScalarWindow {
  start_index : Int
  end_index : Int
  mean : Double
  median : Double
  standard_deviation : Double
  minimum : Double
  maximum : Double
  valid : Bool
} derive(FromJson, ToJson, Debug, Eq)

///|
/// Window schedule.
pub(all) struct WindowSchedule {
  size : Int
  hop : Int
  include_partial : Bool
} derive(FromJson, ToJson, Debug, Eq)

///|
/// Default overlapping schedule for display metrics.
pub fn WindowSchedule::default() -> WindowSchedule {
  { size: 60, hop: 30, include_partial: false }
}

///|
/// Return valid windows according to a schedule.
pub fn scalar_windows(
  values : Array[Double],
  schedule : WindowSchedule,
) -> Array[ScalarWindow] {
  if schedule.size <= 0 || schedule.hop <= 0 {
    return []
  }
  let result = []
  let mut start = 0
  while start < values.length() {
    let end = if start + schedule.size > values.length() {
      values.length()
    } else {
      start + schedule.size
    }
    if end - start < schedule.size && !schedule.include_partial {
      break
    }
    let window = []
    for i in start.. 0,
    })
    start += schedule.hop
  }
  result
}

///|
/// Aggregate RMSSD from complete RR windows.
pub fn rmssd_windows(
  intervals : Array[Double],
  schedule : WindowSchedule,
) -> Array[ScalarWindow] {
  if schedule.size <= 1 || schedule.hop <= 0 {
    return []
  }
  let result = []
  let mut start = 0
  while start < intervals.length() {
    let end = if start + schedule.size > intervals.length() {
      intervals.length()
    } else {
      start + schedule.size
    }
    if end - start < schedule.size && !schedule.include_partial {
      break
    }
    let window = []
    for i in start.. 1,
    })
    start += schedule.hop
  }
  result
}

///|
/// Return the mean of a window field.
pub fn window_mean(windows : Array[ScalarWindow]) -> Double {
  let values = []
  for window in windows {
    if window.valid {
      values.push(window.mean)
    }
  }
  mean_value(values)
}

///|
/// Return the slope of window means.
pub fn window_mean_trend(windows : Array[ScalarWindow]) -> Double {
  let values = []
  for window in windows {
    if window.valid {
      values.push(window.mean)
    }
  }
  fit_linear_trend(values).slope
}

///|
/// Return the best window by a scalar field.
pub fn best_scalar_window(windows : Array[ScalarWindow]) -> ScalarWindow? {
  let mut result : ScalarWindow? = None
  for window in windows {
    if window.valid {
      match result {
        None => result = Some(window)
        Some(current) => if window.mean > current.mean { result = Some(window) }
      }
    }
  }
  result
}

///|
/// Return the worst window by a scalar field.
pub fn worst_scalar_window(windows : Array[ScalarWindow]) -> ScalarWindow? {
  let mut result : ScalarWindow? = None
  for window in windows {
    if window.valid {
      match result {
        None => result = Some(window)
        Some(current) => if window.mean < current.mean { result = Some(window) }
      }
    }
  }
  result
}

///|
/// Return windows whose values changed by at least a threshold.
pub fn changed_windows(
  windows : Array[ScalarWindow],
  threshold : Double,
) -> Array[ScalarWindow] {
  let result = []
  if windows.length() == 0 {
    return result
  }
  let bound = if threshold < 0.0 { 0.0 } else { threshold }
  let mut previous = windows[0].mean
  for i in 1..= bound {
      result.push(windows[i])
    }
    previous = windows[i].mean
  }
  result
}

///|
/// Downsample window means into a fixed number of buckets.
pub fn bucket_window_means(
  windows : Array[ScalarWindow],
  bucket_count : Int,
) -> Array[Double] {
  if bucket_count <= 0 || windows.length() == 0 {
    return []
  }
  let result = Array::make(bucket_count, 0.0)
  let counts = Array::make(bucket_count, 0)
  for i in 0.. 0 {
      result[i] /= counts[i].to_double()
    }
  }
  result
}

///|
/// Convert scalar windows to a compact feature vector.
pub fn window_feature_vector(windows : Array[ScalarWindow]) -> Array[Double] {
  let best = best_scalar_window(windows)
  let worst = worst_scalar_window(windows)
  [
    windows.length().to_double(),
    window_mean(windows),
    window_mean_trend(windows),
    match best {
      Some(value) => value.mean
      None => 0.0
    },
    match worst {
      Some(value) => value.mean
      None => 0.0
    },
    standard_deviation(windows.map(fn(window) { window.mean })),
  ]
}

///|
/// Return whether windows have monotonic, non-overlapping indices.
pub fn windows_are_ordered(windows : Array[ScalarWindow]) -> Bool {
  for i in 1..