///|
/// A numeric summary of a timestamp sequence.
pub struct TimeStatistics {
  count : Int
  minimum : Int64?
  maximum : Int64?
  mean : Double?
  median : Double?
  spread : Double?
} derive(Debug, Eq)

///|
/// Summarize timestamp values using deterministic sorting.
pub fn time_statistics(values : Array[Int64]) -> TimeStatistics {
  let sorted = values.copy()
  sorted.sort()
  let count = sorted.length()
  let total : Int64 = sorted.fold(init=0L, fn(sum, value) { sum + value })
  let mean = if count == 0 {
    None
  } else {
    Some(total.to_double() / count.to_double())
  }
  let median = if count == 0 {
    None
  } else if count % 2 == 1 {
    Some(sorted[count / 2].to_double())
  } else {
    Some(
      (sorted[count / 2 - 1].to_double() + sorted[count / 2].to_double()) / 2.0,
    )
  }
  let spread = match (mean, count) {
    (Some(center), value) if value > 0 =>
      Some(
        sorted.fold(init=0.0, fn(sum, item) {
          let distance = item.to_double() - center
          sum + distance * distance
        }) /
        value.to_double(),
      )
    _ => None
  }
  {
    count,
    minimum: sorted.get(0),
    maximum: sorted.last(),
    mean,
    median,
    spread,
  }
}

///|
/// Return a signal's change timestamp statistics.
pub fn signal_time_statistics(
  trace : Trace,
  signal_id : String,
) -> TimeStatistics {
  time_statistics(change_times(trace, signal_id))
}

///|
/// Return a signal's transition timestamp statistics.
pub fn transition_time_statistics(
  trace : Trace,
  signal_id : String,
) -> TimeStatistics {
  time_statistics(transition_times(trace, signal_id))
}

///|
/// Compute an empirical transition histogram by integer bucket.
pub fn transition_histogram(
  trace : Trace,
  signal_id : String,
  bucket : Int64,
) -> Array[(Int64, Int)] {
  let width = Int64::max(1L, bucket)
  let result : Array[(Int64, Int)] = []
  for time in transition_times(trace, signal_id) {
    let key = time / width * width
    match result.search_by(fn(item) { item.0 == key }) {
      Some(index) => result[index] = (key, result[index].1 + 1)
      None => result.push((key, 1))
    }
  }
  result.sort_by(fn(left, right) {
    if left.0 < right.0 {
      -1
    } else if left.0 > right.0 {
      1
    } else {
      0
    }
  })
  result
}

///|
/// Compute Shannon-style entropy over observed values.
pub fn value_entropy(trace : Trace, signal_id : String) -> Double {
  let values = distinct_values(trace, signal_id)
  let total = signal_changes(trace, signal_id).length()
  if total == 0 || values.length() == 0 {
    return 0.0
  }
  let mut entropy = 0.0
  for value in values {
    let count = signal_changes(trace, signal_id).count_if(fn(change) {
      change.value == value
    })
    let probability = count.to_double() / total.to_double()
    entropy -= probability * log2_approx(probability)
  }
  entropy
}

///|
/// Return the average distance between changes, if at least two exist.
pub fn mean_change_interval(trace : Trace, signal_id : String) -> Double? {
  let times = change_times(trace, signal_id)
  if times.length() < 2 {
    None
  } else {
    let mut total : Int64 = 0L
    for i in 1.. Double {
  if value <= 0.0 {
    return 0.0
  }
  let mut x = value
  let mut exponent = 0
  while x < 1.0 {
    x *= 2.0
    exponent -= 1
  }
  while x >= 2.0 {
    x /= 2.0
    exponent += 1
  }
  let y = (x - 1.0) / (x + 1.0)
  let y2 = y * y
  let mut term = y
  let mut sum = 0.0
  for n in 0..<12 {
    sum += term / (2.0 * n.to_double() + 1.0)
    term *= y2
  }
  2.0 * sum / 0.6931471805599453 + exponent.to_double()
}