///|
pub struct DiagnosticResult {
  sample_count : Int
  mean : Double
  deviation : Double
  runs : Int
  increasing_pairs : Int
  decreasing_pairs : Int
  change_index : Int
  shift : Double
  variance_ratio : Double
  autocorrelation : Double
  stable : Bool
} derive(Debug, ToJson)

///|
fn diag_mean(values : Array[Double]) -> Double {
  if values.length() == 0 {
    0.0
  } else {
    let mut total = 0.0
    for value in values {
      total += value
    }
    total / values.length().to_double()
  }
}

///|
fn diag_variance(values : Array[Double], mean : Double) -> Double {
  if values.length() < 2 {
    0.0
  } else {
    let mut total = 0.0
    for value in values {
      let d = value - mean
      total += d * d
    }
    total / (values.length() - 1).to_double()
  }
}

///|
pub fn runs_count(values : Array[Double], center : Double) -> Int {
  if values.length() == 0 {
    return 0
  }
  let mut runs = 1
  let mut previous = values[0] >= center
  for value in values[1:] {
    let current = value >= center
    if current != previous {
      runs += 1
    }
    previous = current
  }
  runs
}

///|
pub fn increasing_pair_count(values : Array[Double]) -> Int {
  let mut count = 0
  for i in 1.. values[i - 1] {
      count += 1
    }
  }
  count
}

///|
pub fn decreasing_pair_count(values : Array[Double]) -> Int {
  let mut count = 0
  for i in 1.. Int {
  for i, value in values {
    if (value - baseline).abs() > tolerance {
      return i
    }
  }
  -1
}

///|
pub fn variance_ratio(values : Array[Double], split : Int) -> Double {
  if split <= 0 || split >= values.length() {
    return 0.0
  }
  let left = values[0:split].to_owned()
  let right = values[split:].to_owned()
  let left_var = diag_variance(left, diag_mean(left))
  let right_var = diag_variance(right, diag_mean(right))
  if left_var <= 0.0 {
    if right_var <= 0.0 {
      1.0
    } else {
      999999.0
    }
  } else {
    right_var / left_var
  }
}

///|
pub fn lag_autocorrelation(values : Array[Double], lag : Int) -> Double {
  if lag <= 0 || lag >= values.length() {
    return 0.0
  }
  let mean = diag_mean(values)
  let mut numerator = 0.0
  let mut denominator = 0.0
  for value in values {
    let d = value - mean
    denominator += d * d
  }
  for i in lag.. DiagnosticResult {
  let mean = diag_mean(values)
  let deviation = diag_variance(values, mean).sqrt()
  let split = values.length() / 2
  let ratio = variance_ratio(values, split)
  let shift = mean - baseline
  {
    sample_count: values.length(),
    mean,
    deviation,
    runs: runs_count(values, baseline),
    increasing_pairs: increasing_pair_count(values),
    decreasing_pairs: decreasing_pair_count(values),
    change_index: first_mean_shift(values, baseline, shift_tolerance),
    shift,
    variance_ratio: ratio,
    autocorrelation: lag_autocorrelation(values, 1),
    stable: shift.abs() <= shift_tolerance &&
    ratio <= variance_limit &&
    ratio >= 1.0 / variance_limit,
  }
}

///|
pub fn diagnostic_alarm_count(result : DiagnosticResult) -> Int {
  let mut count = 0
  if result.change_index >= 0 {
    count += 1
  }
  if result.variance_ratio > 2.0 || result.variance_ratio < 0.5 {
    count += 1
  }
  if result.autocorrelation.abs() > 0.8 {
    count += 1
  }
  count
}

///|
pub fn trend_direction(values : Array[Double]) -> Int {
  let up = increasing_pair_count(values)
  let down = decreasing_pair_count(values)
  if up > down {
    1
  } else if down > up {
    -1
  } else {
    0
  }
}

///|
pub fn plateau_count(values : Array[Double], tolerance : Double) -> Int {
  let mut count = 0
  for i in 1.. Array[Int] {
  let result : Array[Int] = []
  for i, value in values {
    if (value - center).abs() > limit {
      result.push(i)
    }
  }
  result
}

///|
pub fn diagnostic_score(result : DiagnosticResult) -> Double {
  let shift_score = result.shift.abs()
  let variance_score = if result.variance_ratio <= 0.0 {
    10.0
  } else {
    (result.variance_ratio - 1.0).abs()
  }
  shift_score + variance_score + result.autocorrelation.abs()
}

///|
pub fn diagnostic_label(result : DiagnosticResult) -> String {
  if result.stable {
    "stable"
  } else if diagnostic_alarm_count(result) > 1 {
    "investigate"
  } else {
    "watch"
  }
}