///|
/// Robust aggregate for one window or segment.
pub struct AggregateSummary {
  count : Int
  mean_value : Double
  median_value : Double
  mad_value : Double
  minimum : Double
  maximum : Double
  trimmed_value : Double
  outlier_count : Int
  quality : Double
}

///|
/// A collection of aligned segment aggregates.
pub struct AggregateTable {
  starts : Array[Int]
  ends : Array[Int]
  summaries : Array[AggregateSummary]
}

///|
pub fn aggregate_summary(
  data : Array[Double],
  trim_percent : Double,
  threshold : Double,
) -> AggregateSummary {
  {
    count: data.length(),
    mean_value: mean(data),
    median_value: median(data),
    mad_value: mad(data),
    minimum: min_value(data),
    maximum: max_value(data),
    trimmed_value: trimmed_mean(data, trim_percent),
    outlier_count: outlier_indices_z(data, threshold~).length(),
    quality: robust_signal_quality(data),
  }
}

///|
pub fn aggregate_summary_vector(summary : AggregateSummary) -> Array[Double] {
  [
    summary.count.to_double(),
    summary.mean_value,
    summary.median_value,
    summary.mad_value,
    summary.minimum,
    summary.maximum,
    summary.trimmed_value,
    summary.outlier_count.to_double(),
    summary.quality,
  ]
}

///|
pub fn aggregate_summary_lines(summary : AggregateSummary) -> Array[String] {
  [
    "count=" + summary.count.to_string(),
    "mean=" + summary.mean_value.to_string(),
    "median=" + summary.median_value.to_string(),
    "mad=" + summary.mad_value.to_string(),
    "minimum=" + summary.minimum.to_string(),
    "maximum=" + summary.maximum.to_string(),
    "trimmed=" + summary.trimmed_value.to_string(),
    "outliers=" + summary.outlier_count.to_string(),
    "quality=" + summary.quality.to_string(),
  ]
}

///|
pub fn aggregate_summary_string(summary : AggregateSummary) -> String {
  aggregate_summary_lines(summary).join("\n")
}

///|
pub fn aggregate_blocks(
  data : Array[Double],
  block_size : Int,
  trim_percent : Double,
  threshold : Double,
) -> AggregateTable {
  let starts = []
  let ends = []
  let summaries = []
  let width = if block_size < 1 { 1 } else { block_size }
  let mut start = 0
  while start < data.length() {
    let end = if start + width > data.length() {
      data.length()
    } else {
      start + width
    }
    let values = []
    for index = start; index < end; index = index + 1 {
      values.push(data[index])
    }
    starts.push(start)
    ends.push(end)
    summaries.push(aggregate_summary(values, trim_percent, threshold))
    start = end
  }
  { starts, ends, summaries }
}

///|
pub fn aggregate_table_means(table : AggregateTable) -> Array[Double] {
  let result = []
  for summary in table.summaries {
    result.push(summary.mean_value)
  }
  result
}

///|
pub fn aggregate_table_medians(table : AggregateTable) -> Array[Double] {
  let result = []
  for summary in table.summaries {
    result.push(summary.median_value)
  }
  result
}

///|
pub fn aggregate_table_scales(table : AggregateTable) -> Array[Double] {
  let result = []
  for summary in table.summaries {
    result.push(summary.mad_value)
  }
  result
}

///|
pub fn aggregate_table_quality(table : AggregateTable) -> Double {
  let values = []
  for summary in table.summaries {
    values.push(summary.quality)
  }
  if values.length() == 0 {
    0.0
  } else {
    mean(values)
  }
}

///|
pub fn aggregate_table_outlier_counts(table : AggregateTable) -> Array[Int] {
  let result = []
  for summary in table.summaries {
    result.push(summary.outlier_count)
  }
  result
}

///|
pub fn aggregate_block_mean(
  data : Array[Double],
  block_size : Int,
) -> Array[Double] {
  aggregate_table_means(aggregate_blocks(data, block_size, 0.1, 3.5))
}

///|
pub fn aggregate_block_median(
  data : Array[Double],
  block_size : Int,
) -> Array[Double] {
  aggregate_table_medians(aggregate_blocks(data, block_size, 0.1, 3.5))
}

///|
pub fn aggregate_block_mad(
  data : Array[Double],
  block_size : Int,
) -> Array[Double] {
  aggregate_table_scales(aggregate_blocks(data, block_size, 0.1, 3.5))
}

///|
pub fn aggregate_block_trimmed(
  data : Array[Double],
  block_size : Int,
  trim_percent : Double,
) -> Array[Double] {
  let table = aggregate_blocks(data, block_size, trim_percent, 3.5)
  let result = []
  for summary in table.summaries {
    result.push(summary.trimmed_value)
  }
  result
}

///|
pub fn aggregate_block_quality(
  data : Array[Double],
  block_size : Int,
) -> Array[Double] {
  let table = aggregate_blocks(data, block_size, 0.1, 3.5)
  let result = []
  for summary in table.summaries {
    result.push(summary.quality)
  }
  result
}

///|
pub fn aggregate_block_ranges(
  data : Array[Double],
  block_size : Int,
) -> Array[Double] {
  let table = aggregate_blocks(data, block_size, 0.1, 3.5)
  let result = []
  for summary in table.summaries {
    result.push(summary.maximum - summary.minimum)
  }
  result
}

///|
pub fn aggregate_block_shift(
  data : Array[Double],
  block_size : Int,
) -> Array[Double] {
  let values = aggregate_block_median(data, block_size)
  let result = []
  for index = 1; index < values.length(); index = index + 1 {
    result.push(values[index] - values[index - 1])
  }
  result
}

///|
pub fn aggregate_block_outlier_rate(
  data : Array[Double],
  block_size : Int,
) -> Array[Double] {
  let table = aggregate_blocks(data, block_size, 0.1, 3.5)
  let result = []
  for summary in table.summaries {
    result.push(
      if summary.count == 0 {
        0.0
      } else {
        summary.outlier_count.to_double() / summary.count.to_double()
      },
    )
  }
  result
}

///|
pub fn aggregate_equal_width(
  data : Array[Double],
  bins : Int,
) -> Array[Array[Double]] {
  let edges = histogram_edges(data, if bins < 1 { 1 } else { bins })
  let result = []
  for _ in 0..<(edges.length() - 1) {
    result.push([])
  }
  for value in data {
    if result.length() > 0 {
      let index = drift_bin_index(value, edges)
      result[index].push(value)
    }
  }
  result
}

///|
pub fn aggregate_bin_means(data : Array[Double], bins : Int) -> Array[Double] {
  let result = []
  for group in aggregate_equal_width(data, bins) {
    result.push(mean(group))
  }
  result
}

///|
pub fn aggregate_bin_medians(data : Array[Double], bins : Int) -> Array[Double] {
  let result = []
  for group in aggregate_equal_width(data, bins) {
    result.push(median(group))
  }
  result
}

///|
pub fn aggregate_bin_scales(data : Array[Double], bins : Int) -> Array[Double] {
  let result = []
  for group in aggregate_equal_width(data, bins) {
    result.push(mad(group))
  }
  result
}

///|
pub fn aggregate_bin_counts(data : Array[Double], bins : Int) -> Array[Int] {
  let result = []
  for group in aggregate_equal_width(data, bins) {
    result.push(group.length())
  }
  result
}

///|
pub fn aggregate_quantile_bins(
  data : Array[Double],
  bins : Int,
) -> Array[Array[Double]] {
  aggregate_by_quantile(data, if bins < 1 { 1 } else { bins })
}

///|
pub fn aggregate_quantile_means(
  data : Array[Double],
  bins : Int,
) -> Array[Double] {
  quantile_group_means(data, if bins < 1 { 1 } else { bins })
}

///|
pub fn aggregate_quantile_scales(
  data : Array[Double],
  bins : Int,
) -> Array[Double] {
  quantile_group_mads(data, if bins < 1 { 1 } else { bins })
}

///|
pub fn aggregate_weighted_summary(
  data : Array[Double],
  weights : Array[Double],
  trim_percent : Double,
) -> Array[Double] {
  [
    weighted_mean(data, weights),
    weighted_median(data, weights),
    weighted_variance(data, weights),
    weighted_iqr(data, weights),
    trimmed_mean(data, trim_percent),
  ]
}

///|
pub fn aggregate_cumulative_summary(
  data : Array[Double],
) -> Array[Array[Double]] {
  [
    cumulative_mean(data),
    cumulative_median(data),
    cumulative_mad(data),
    cumulative_outlier_rate(data),
  ]
}

///|
pub fn aggregate_exponential_summary(
  data : Array[Double],
  alpha : Double,
) -> Array[Array[Double]] {
  [
    exponentially_weighted_mean(data, alpha),
    robust_exponentially_weighted_mean(data, alpha, 3.5),
  ]
}

///|
pub fn aggregate_running_summary(data : Array[Double]) -> Array[Array[Double]] {
  [
    running_minimum(data),
    running_maximum(data),
    running_range(data),
    cumulative_sum(data),
  ]
}

///|
pub fn aggregate_downsample_mean(
  data : Array[Double],
  target_size : Int,
) -> Array[Double] {
  let count = if target_size < 1 { 1 } else { target_size }
  if data.length() <= count {
    return data.copy()
  }
  let result = []
  for bucket = 0; bucket < count; bucket = bucket + 1 {
    let start = bucket * data.length() / count
    let end = (bucket + 1) * data.length() / count
    let values = []
    for index = start; index < end; index = index + 1 {
      values.push(data[index])
    }
    result.push(mean(values))
  }
  result
}

///|
pub fn aggregate_downsample_median(
  data : Array[Double],
  target_size : Int,
) -> Array[Double] {
  let count = if target_size < 1 { 1 } else { target_size }
  if data.length() <= count {
    return data.copy()
  }
  let result = []
  for bucket = 0; bucket < count; bucket = bucket + 1 {
    let start = bucket * data.length() / count
    let end = (bucket + 1) * data.length() / count
    let values = []
    for index = start; index < end; index = index + 1 {
      values.push(data[index])
    }
    result.push(median(values))
  }
  result
}

///|
pub fn aggregate_downsample_trimmed(
  data : Array[Double],
  target_size : Int,
  trim_percent : Double,
) -> Array[Double] {
  let count = if target_size < 1 { 1 } else { target_size }
  if data.length() <= count {
    return data.copy()
  }
  let result = []
  for bucket = 0; bucket < count; bucket = bucket + 1 {
    let start = bucket * data.length() / count
    let end = (bucket + 1) * data.length() / count
    let values = []
    for index = start; index < end; index = index + 1 {
      values.push(data[index])
    }
    result.push(trimmed_mean(values, trim_percent))
  }
  result
}

///|
pub fn aggregate_downsample_quality(
  data : Array[Double],
  target_size : Int,
) -> Array[Double] {
  let count = if target_size < 1 { 1 } else { target_size }
  let result = []
  for bucket = 0; bucket < count && bucket < data.length(); bucket = bucket + 1 {
    let start = bucket * data.length() / count
    let end = (bucket + 1) * data.length() / count
    let values = []
    for index = start; index < end; index = index + 1 {
      values.push(data[index])
    }
    result.push(robust_signal_quality(values))
  }
  result
}

///|
pub fn aggregate_window_summary(
  data : Array[Double],
  window : Int,
  trim_percent : Double,
  threshold : Double,
) -> Array[AggregateSummary] {
  let result = []
  let width = if window < 1 { 1 } else { window }
  for end = 1; end <= data.length(); end = end + 1 {
    let start = if end < width { 0 } else { end - width }
    let values = []
    for index = start; index < end; index = index + 1 {
      values.push(data[index])
    }
    result.push(aggregate_summary(values, trim_percent, threshold))
  }
  result
}

///|
pub fn aggregate_window_quality(
  data : Array[Double],
  window : Int,
) -> Array[Double] {
  let result = []
  for summary in aggregate_window_summary(data, window, 0.1, 3.5) {
    result.push(summary.quality)
  }
  result
}

///|
pub fn aggregate_window_outliers(
  data : Array[Double],
  window : Int,
) -> Array[Int] {
  let result = []
  for summary in aggregate_window_summary(data, window, 0.1, 3.5) {
    result.push(summary.outlier_count)
  }
  result
}

///|
pub fn aggregate_window_center_shift(
  data : Array[Double],
  window : Int,
) -> Array[Double] {
  let result = []
  let summaries = aggregate_window_summary(data, window, 0.1, 3.5)
  for index = 1; index < summaries.length(); index = index + 1 {
    result.push(
      summaries[index].median_value - summaries[index - 1].median_value,
    )
  }
  result
}

///|
pub fn aggregate_window_scale_shift(
  data : Array[Double],
  window : Int,
) -> Array[Double] {
  let result = []
  let summaries = aggregate_window_summary(data, window, 0.1, 3.5)
  for index = 1; index < summaries.length(); index = index + 1 {
    result.push(summaries[index].mad_value - summaries[index - 1].mad_value)
  }
  result
}

///|
pub fn aggregate_window_alerts(
  data : Array[Double],
  window : Int,
  threshold : Double,
) -> Array[Bool] {
  let result = []
  for summary in aggregate_window_summary(data, window, 0.1, threshold) {
    result.push(summary.outlier_count > 0)
  }
  result
}

///|
pub fn aggregate_compare_blocks(
  data : Array[Double],
  block_size : Int,
) -> Array[Array[Double]] {
  let table = aggregate_blocks(data, block_size, 0.1, 3.5)
  let result = []
  for summary in table.summaries {
    result.push([
      summary.mean_value,
      summary.median_value,
      summary.mad_value,
      summary.trimmed_value,
      summary.quality,
    ])
  }
  result
}