///|
fn window_bounds(length : Int, index : Int, window : Int) -> (Int, Int) {
  let radius = window / 2
  let start = if index - radius < 0 { 0 } else { index - radius }
  let end = if start + window > length { length } else { start + window }
  let corrected_start = if end - start < window && end - window >= 0 {
    end - window
  } else {
    start
  }
  (corrected_start, end)
}

///|
fn extract_window(
  data : Array[Double],
  index : Int,
  window : Int,
) -> Array[Double] {
  if window <= 0 {
    abort("window must be positive")
  }
  let bounds = window_bounds(data.length(), index, window)
  let result = []
  for position = bounds.0; position < bounds.1; position = position + 1 {
    result.push(data[position])
  }
  result
}

///|
pub fn rolling_mean(data : Array[Double], window : Int) -> Array[Double] {
  let result = []
  for index = 0; index < data.length(); index = index + 1 {
    result.push(mean(extract_window(data, index, window)))
  }
  result
}

///|
pub fn rolling_median(data : Array[Double], window : Int) -> Array[Double] {
  let result = []
  for index = 0; index < data.length(); index = index + 1 {
    result.push(median(extract_window(data, index, window)))
  }
  result
}

///|
pub fn rolling_mad(data : Array[Double], window : Int) -> Array[Double] {
  let result = []
  for index = 0; index < data.length(); index = index + 1 {
    result.push(mad(extract_window(data, index, window)))
  }
  result
}

///|
pub fn rolling_quantile(
  data : Array[Double],
  window : Int,
  probability : Double,
) -> Array[Double] {
  let result = []
  for index = 0; index < data.length(); index = index + 1 {
    result.push(quantile(extract_window(data, index, window), probability))
  }
  result
}

///|
pub fn rolling_trimmed_mean(
  data : Array[Double],
  window : Int,
  trim_percent : Double,
) -> Array[Double] {
  let result = []
  for index = 0; index < data.length(); index = index + 1 {
    result.push(trimmed_mean(extract_window(data, index, window), trim_percent))
  }
  result
}

///|
pub fn rolling_winsorized_mean(
  data : Array[Double],
  window : Int,
  trim_percent : Double,
) -> Array[Double] {
  let result = []
  for index = 0; index < data.length(); index = index + 1 {
    result.push(
      winsorized_mean(extract_window(data, index, window), trim_percent),
    )
  }
  result
}

///|
pub fn rolling_z_scores(data : Array[Double], window : Int) -> Array[Double] {
  let result = []
  for index = 0; index < data.length(); index = index + 1 {
    let neighborhood = extract_window(data, index, window)
    result.push(
      robust_z_score(data[index], median(neighborhood), mad(neighborhood)),
    )
  }
  result
}

///|
pub fn rolling_iqr(data : Array[Double], window : Int) -> Array[Double] {
  let result = []
  for index = 0; index < data.length(); index = index + 1 {
    result.push(interquartile_range(extract_window(data, index, window)))
  }
  result
}

///|
pub fn rolling_min(data : Array[Double], window : Int) -> Array[Double] {
  let result = []
  for index = 0; index < data.length(); index = index + 1 {
    result.push(min_value(extract_window(data, index, window)))
  }
  result
}

///|
pub fn rolling_max(data : Array[Double], window : Int) -> Array[Double] {
  let result = []
  for index = 0; index < data.length(); index = index + 1 {
    result.push(max_value(extract_window(data, index, window)))
  }
  result
}

///|
pub fn rolling_range(data : Array[Double], window : Int) -> Array[Double] {
  let result = []
  for index = 0; index < data.length(); index = index + 1 {
    result.push(range(extract_window(data, index, window)))
  }
  result
}

///|
pub fn exponentially_weighted_mean(
  data : Array[Double],
  alpha : Double,
) -> Array[Double] {
  if alpha <= 0.0 || alpha > 1.0 {
    abort("alpha must be in (0, 1]")
  }
  let result = []
  if data.length() == 0 {
    return result
  }
  let mut state = data[0]
  result.push(state)
  for index = 1; index < data.length(); index = index + 1 {
    state = alpha * data[index] + (1.0 - alpha) * state
    result.push(state)
  }
  result
}

///|
pub fn robust_exponentially_weighted_mean(
  data : Array[Double],
  alpha : Double,
  clip : Double,
) -> Array[Double] {
  if clip <= 0.0 {
    abort("clip must be positive")
  }
  if alpha <= 0.0 || alpha > 1.0 {
    abort("alpha must be in (0, 1]")
  }
  let result = []
  if data.length() == 0 {
    return result
  }
  let mut state = data[0]
  result.push(state)
  for index = 1; index < data.length(); index = index + 1 {
    let innovation = clamp_double(data[index] - state, -clip, clip)
    state = state + alpha * innovation
    result.push(state)
  }
  result
}

///|
pub fn rolling_huber_location(
  data : Array[Double],
  window : Int,
) -> Array[Double] {
  let result = []
  for index = 0; index < data.length(); index = index + 1 {
    result.push(huber_location(extract_window(data, index, window)))
  }
  result
}

///|
pub fn rolling_outlier_flags(
  data : Array[Double],
  window : Int,
  threshold? : Double = 3.5,
) -> Array[Bool] {
  if threshold <= 0.0 {
    abort("threshold must be positive")
  }
  let scores = rolling_z_scores(data, window)
  let result = []
  for score in scores {
    result.push(abs_double(score) > threshold)
  }
  result
}

///|
pub fn rolling_mad_residuals(
  data : Array[Double],
  window : Int,
) -> Array[Double] {
  let centers = rolling_median(data, window)
  let scales = rolling_mad(data, window)
  let result = []
  for index = 0; index < data.length(); index = index + 1 {
    result.push(robust_z_score(data[index], centers[index], scales[index]))
  }
  result
}

///|
pub fn centered_difference(data : Array[Double]) -> Array[Double] {
  let result = []
  for index = 0; index < data.length(); index = index + 1 {
    if data.length() <= 1 {
      result.push(0.0)
    } else if index == 0 {
      result.push(data[1] - data[0])
    } else if index == data.length() - 1 {
      result.push(data[index] - data[index - 1])
    } else {
      result.push((data[index + 1] - data[index - 1]) / 2.0)
    }
  }
  result
}

///|
pub fn robust_detrend(data : Array[Double], window : Int) -> Array[Double] {
  let baseline = rolling_huber_location(data, window)
  let result = []
  for index = 0; index < data.length(); index = index + 1 {
    result.push(data[index] - baseline[index])
  }
  result
}

///|
pub fn rolling_covariance(
  x : Array[Double],
  y : Array[Double],
  window : Int,
) -> Array[Double] {
  if x.length() != y.length() {
    return []
  }
  let result = []
  for index = 0; index < x.length(); index = index + 1 {
    result.push(
      covariance(
        extract_window(x, index, window),
        extract_window(y, index, window),
      ),
    )
  }
  result
}

///|
pub fn rolling_correlation(
  x : Array[Double],
  y : Array[Double],
  window : Int,
) -> Array[Double] {
  if x.length() != y.length() {
    return []
  }
  let result = []
  for index = 0; index < x.length(); index = index + 1 {
    result.push(
      pearson_correlation(
        extract_window(x, index, window),
        extract_window(y, index, window),
      ),
    )
  }
  result
}

///|
pub fn rolling_mean_absolute_error(
  actual : Array[Double],
  predicted : Array[Double],
  window : Int,
) -> Array[Double] {
  if actual.length() != predicted.length() {
    return []
  }
  let result = []
  for index = 0; index < actual.length(); index = index + 1 {
    result.push(
      mean_absolute_error(
        extract_window(actual, index, window),
        extract_window(predicted, index, window),
      ),
    )
  }
  result
}