///|
pub struct TraceDiagnostics {
  steps : Int
  mean_change_probability : Double
  maximum_change_probability : Double
  maximum_change_step : Int
  shortest_map_run_length : Int
  longest_map_run_length : Int
  mean_posterior_mean : Double
}

///|
pub fn diagnose_trace(
  updates : Array[Update],
) -> Result[TraceDiagnostics, DetectError] {
  if updates.length() == 0 {
    return Err(InvalidRunLengthLimit)
  }
  let mut total_probability = 0.0
  let mut total_mean = 0.0
  let mut maximum_probability = updates[0].change_probability
  let mut maximum_step = updates[0].step
  let mut shortest_run = updates[0].most_likely_run_length
  let mut longest_run = updates[0].most_likely_run_length
  for update in updates {
    total_probability = total_probability + update.change_probability
    total_mean = total_mean + update.posterior_mean
    if update.change_probability > maximum_probability {
      maximum_probability = update.change_probability
      maximum_step = update.step
    }
    if update.most_likely_run_length < shortest_run {
      shortest_run = update.most_likely_run_length
    }
    if update.most_likely_run_length > longest_run {
      longest_run = update.most_likely_run_length
    }
  }
  Ok({
    steps: updates.length(),
    mean_change_probability: total_probability / updates.length().to_double(),
    maximum_change_probability: maximum_probability,
    maximum_change_step: maximum_step,
    shortest_map_run_length: shortest_run,
    longest_map_run_length: longest_run,
    mean_posterior_mean: total_mean / updates.length().to_double(),
  })
}

///|
pub fn TraceDiagnostics::is_spiky(
  self : TraceDiagnostics,
  threshold : Double,
) -> Result[Bool, DetectError] {
  if !finite(threshold) || threshold < 0.0 || threshold > 1.0 {
    Err(InvalidHazard)
  } else {
    Ok(self.maximum_change_probability >= threshold)
  }
}

///|
pub fn TraceDiagnostics::probability_ratio(self : TraceDiagnostics) -> Double {
  if self.mean_change_probability == 0.0 {
    0.0
  } else {
    self.maximum_change_probability / self.mean_change_probability
  }
}

///|
pub struct ProbabilityHistogram {
  edges : Array[Double]
  counts : Array[Int]
}

///|
pub fn ProbabilityHistogram::edges(
  self : ProbabilityHistogram,
) -> Array[Double] {
  self.edges.copy()
}

///|
pub fn ProbabilityHistogram::counts(self : ProbabilityHistogram) -> Array[Int] {
  self.counts.copy()
}

///|
pub fn ProbabilityHistogram::total(self : ProbabilityHistogram) -> Int {
  let mut total = 0
  for count in self.counts {
    total = total + count
  }
  total
}

///|
pub fn probability_histogram(
  updates : Array[Update],
  bins : Int,
) -> Result[ProbabilityHistogram, DetectError] {
  if updates.length() == 0 || bins <= 0 {
    return Err(InvalidRunLengthLimit)
  }
  let edges = []
  let counts = Array::make(bins, 0)
  for index in 0..<=bins {
    edges.push(index.to_double() / bins.to_double())
  }
  for update in updates {
    let mut bucket = (update.change_probability * bins.to_double()).to_int()
    if bucket >= bins {
      bucket = bins - 1
    }
    if bucket < 0 {
      bucket = 0
    }
    counts[bucket] = counts[bucket] + 1
  }
  Ok({ edges, counts, })
}

///|
pub fn format_probability_histogram(
  histogram : ProbabilityHistogram,
  width : Int,
) -> Result[String, DetectError] {
  if width <= 0 {
    return Err(InvalidRunLengthLimit)
  }
  let mut maximum = 0
  for count in histogram.counts {
    if count > maximum {
      maximum = count
    }
  }
  if maximum == 0 {
    return Ok("")
  }
  let mut text = ""
  for index in 0.. DetectedSegment {
  let mut total_observation = 0.0
  let mut total_probability = 0.0
  for index in start.. Result[Array[DetectedSegment], DetectError] {
  if updates.length() == 0 {
    return Err(InvalidRunLengthLimit)
  }
  let result = []
  let mut start = 0
  for boundary in boundaries {
    if boundary.step <= start + 1 || boundary.step > updates.length() + 1 {
      return Err(InvalidRunLengthLimit)
    }
    result.push(make_segment(updates, start, boundary.step - 1))
    start = boundary.step - 1
  }
  if start < updates.length() {
    result.push(make_segment(updates, start, updates.length()))
  }
  Ok(result)
}

///|
pub fn segment_boundaries_from_updates(
  updates : Array[Update],
  threshold : Double,
  cooldown : Int,
) -> Result[Array[Boundary], DetectError] {
  if cooldown < 0 {
    return Err(InvalidRunLengthLimit)
  }
  let candidates = match predicted_boundaries(updates, threshold) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let result = []
  let mut last = -cooldown - 1
  for candidate in candidates {
    if candidate.step - last > cooldown {
      result.push(candidate)
      last = candidate.step
    }
  }
  Ok(result)
}

///|
pub struct BoundaryDelay {
  truth_step : Int
  predicted_step : Int?
  delay : Int?
}

///|
pub fn boundary_delays(
  truth : Array[Boundary],
  predicted : Array[Boundary],
  tolerance : Int,
) -> Result[Array[BoundaryDelay], DetectError] {
  if tolerance < 0 {
    return Err(InvalidRunLengthLimit)
  }
  let used = Array::make(predicted.length(), false)
  let result = []
  for expected in truth {
    let mut match_index : Int? = None
    let mut best_distance = tolerance + 1
    for index in 0..
        result.push({
          truth_step: expected.step,
          predicted_step: None,
          delay: None,
        })
      Some(index) => {
        used[index] = true
        result.push({
          truth_step: expected.step,
          predicted_step: Some(predicted[index].step),
          delay: Some(predicted[index].step - expected.step),
        })
      }
    }
  }
  Ok(result)
}

///|
pub fn mean_absolute_delay(delays : Array[BoundaryDelay]) -> Int? {
  let mut count = 0
  let mut total = 0
  for item in delays {
    match item.delay {
      None => ()
      Some(value) => {
        if value < 0 {
          total = total - value
        } else {
          total = total + value
        }
        count = count + 1
      }
    }
  }
  if count == 0 {
    None
  } else {
    Some(total / count)
  }
}

///|
pub fn format_diagnostics(value : TraceDiagnostics) -> String {
  "steps=" +
  value.steps.to_string() +
  ",mean_change_probability=" +
  value.mean_change_probability.to_string() +
  ",maximum_change_probability=" +
  value.maximum_change_probability.to_string() +
  ",maximum_change_step=" +
  value.maximum_change_step.to_string() +
  ",shortest_map_run_length=" +
  value.shortest_map_run_length.to_string() +
  ",longest_map_run_length=" +
  value.longest_map_run_length.to_string()
}