///|
/// Features extracted from a bounded window for dashboards and model input.
pub struct WindowFeatures {
  count : Int
  mean : Double
  standard_deviation : Double
  median : Double
  mad : Double
  minimum : Double
  maximum : Double
  range : Double
  slope : Double
  autocorrelation : Double
  change_rate : Double
}

///|
pub fn WindowFeatures::empty() -> WindowFeatures {
  {
    count: 0,
    mean: 0.0,
    standard_deviation: 0.0,
    median: 0.0,
    mad: 0.0,
    minimum: 0.0,
    maximum: 0.0,
    range: 0.0,
    slope: 0.0,
    autocorrelation: 0.0,
    change_rate: 0.0,
  }
}

///|
pub fn extract_features(values : Array[Double]) -> WindowFeatures {
  if values.length() == 0 {
    return WindowFeatures::empty()
  }
  {
    count: values.length(),
    mean: mean(values),
    standard_deviation: standard_deviation(values),
    median: median(values),
    mad: median_absolute_deviation(values),
    minimum: array_minimum(values),
    maximum: array_maximum(values),
    range: array_maximum(values) - array_minimum(values),
    slope: linear_slope(values),
    autocorrelation: autocorrelation(values, 1),
    change_rate: sign_change_rate(values),
  }
}

///|
pub fn feature_distance(
  left : WindowFeatures,
  right : WindowFeatures,
) -> Double {
  let mean_scale = 1.0 + absolute(left.mean) + absolute(right.mean)
  let scale_scale = 1.0 + left.standard_deviation + right.standard_deviation
  let slope_scale = 1.0 + absolute(left.slope) + absolute(right.slope)
  let a = absolute(left.mean - right.mean) / mean_scale
  let b = absolute(left.standard_deviation - right.standard_deviation) /
    scale_scale
  let c = absolute(left.median - right.median) / mean_scale
  let d = absolute(left.slope - right.slope) / slope_scale
  (a + b + c + d) / 4.0
}

///|
pub fn feature_change_score(
  left : Array[Double],
  right : Array[Double],
) -> Double {
  feature_distance(extract_features(left), extract_features(right))
}

///|
pub struct FeatureExtractor {
  left : DoubleWindow
  right : DoubleWindow
  threshold : Double
  mut index : Int
}

///|
pub fn FeatureExtractor::new(
  window_size? : Int = 16,
  threshold? : Double = 0.25,
) -> FeatureExtractor {
  let size = if window_size < 2 { 2 } else { window_size }
  {
    left: DoubleWindow::new(size),
    right: DoubleWindow::new(size),
    threshold: if threshold <= 0.0 {
      0.25
    } else {
      threshold
    },
    index: 0,
  }
}

///|
pub fn FeatureExtractor::update(
  self : FeatureExtractor,
  value : Double,
) -> DetectionResult {
  self.index += 1
  if !is_finite(value) {
    return DetectionResult::quiet(index=self.index)
  }
  if self.right.is_full() {
    self.left.clear()
    for item in self.right.to_array() {
      ignore(self.left.push(item))
    }
    self.right.clear()
  }
  ignore(self.right.push(value))
  if !self.left.is_full() || !self.right.is_full() {
    return DetectionResult::quiet(index=self.index)
  }
  let score = feature_change_score(self.left.to_array(), self.right.to_array())
  DetectionResult::new(
    score >= self.threshold,
    score / self.threshold,
    clamp_probability(score),
    DistributionShift,
    self.index,
    evidence=score,
  )
}

///|
pub fn feature_names() -> Array[String] {
  [
    "count", "mean", "standard_deviation", "median", "mad", "minimum", "maximum",
    "range", "slope", "autocorrelation", "change_rate",
  ]
}