///|
pub fn standard_uncertainty(values : Array[Double]) -> UncertaintyEstimate {
  let summary = describe_samples(values)
  let standard = if summary.count == 0 {
    0.0
  } else {
    summary.standard_deviation / Double::from_int(summary.count).sqrt()
  }
  {
    standard,
    expanded: standard * 2.0,
    coverage: 0.95,
    source: "repeatability",
  }
}

///|
pub fn uncertainty_from_components(
  components : Array[UncertaintyEstimate],
) -> UncertaintyEstimate {
  let mut variance = 0.0
  let mut coverage = 1.0
  for component in components {
    variance += component.standard * component.standard
    coverage = coverage.min(component.coverage)
  }
  let standard = variance.sqrt()
  { standard, expanded: standard * 2.0, coverage, source: "combined" }
}

///|
pub fn propagate_scale_uncertainty(
  value : Double,
  scale : Double,
  scale_uncertainty : Double,
) -> UncertaintyEstimate {
  let standard = value.abs() * scale.abs() * scale_uncertainty.abs()
  { standard, expanded: standard * 2.0, coverage: 0.95, source: "scale" }
}

///|
pub fn propagate_addition(
  first : UncertaintyEstimate,
  second : UncertaintyEstimate,
) -> UncertaintyEstimate {
  uncertainty_from_components([first, second])
}

///|
pub fn propagate_product(
  value : Double,
  uncertainty : UncertaintyEstimate,
  factor : Double,
) -> UncertaintyEstimate {
  let standard = value.abs() * uncertainty.standard * factor.abs()
  {
    standard,
    expanded: standard * 2.0,
    coverage: uncertainty.coverage,
    source: "scaled",
  }
}

///|
pub fn tolerance_guard_band(
  tolerance : Tolerance,
  uncertainty : UncertaintyEstimate,
) -> Tolerance {
  {
    ..tolerance,
    lower: tolerance.lower + uncertainty.expanded,
    upper: tolerance.upper - uncertainty.expanded,
  }
}

///|
pub fn measurement_with_uncertainty(
  measurement : Measurement,
  uncertainty : UncertaintyEstimate,
) -> String {
  measurement.value.to_string() +
  " +/- " +
  uncertainty.expanded.to_string() +
  " " +
  measurement.unit
}

///|
pub fn confidence_from_uncertainty(
  measurement : Measurement,
  uncertainty : UncertaintyEstimate,
  tolerance : Tolerance,
) -> Double {
  let margin = (tolerance.upper - measurement.value)
    .abs()
    .min((measurement.value - tolerance.lower).abs())
  if uncertainty.expanded <= 0.0000001 {
    1.0
  } else {
    clamp_double(margin / uncertainty.expanded, 0.0, 1.0)
  }
}

///|
pub fn repeatability_limit(
  values : Array[Double],
  multiplier : Double,
) -> Double {
  multiplier.abs() * repeatability(values)
}

///|
pub fn reproducibility_between_groups(groups : Array[Array[Double]]) -> Double {
  let means : Array[Double] = []
  for group in groups {
    means.push(mean_value(group))
  }
  standard_uncertainty(means).standard
}

///|
pub fn pooled_standard_deviation(groups : Array[Array[Double]]) -> Double {
  let total_count = groups.fold(init=0, fn(total, group) {
    total + group.length()
  })
  if total_count <= groups.length() {
    0.0
  } else {
    let mut sum_squares = 0.0
    for group in groups {
      sum_squares += variance_value(group) *
        Double::from_int((group.length() - 1).max(0))
    }
    (sum_squares / Double::from_int(total_count - groups.length())).sqrt()
  }
}

///|
pub fn uncertainty_budget_report(
  components : Array[UncertaintyEstimate],
) -> String {
  let combined = uncertainty_from_components(components)
  let out = StringBuilder()
  out.write_string("source,standard,expanded,coverage\n")
  for component in components {
    out.write_string(component.source)
    out.write_string(",")
    out.write_string(component.standard.to_string())
    out.write_string(",")
    out.write_string(component.expanded.to_string())
    out.write_string(",")
    out.write_string(component.coverage.to_string())
    out.write_string("\n")
  }
  out.write_string("combined,")
  out.write_string(combined.standard.to_string())
  out.write_string(",")
  out.write_string(combined.expanded.to_string())
  out.write_string(",")
  out.write_string(combined.coverage.to_string())
  out.write_string("\n")
  out.to_string()
}

///|
pub fn guard_band_decision(
  value : Double,
  tolerance : Tolerance,
  uncertainty : UncertaintyEstimate,
) -> MeasurementDecision {
  let guarded = tolerance_guard_band(tolerance, uncertainty)
  guarded.judge(value)
}

///|
pub fn uncertainty_ratio(
  uncertainty : UncertaintyEstimate,
  tolerance : Tolerance,
) -> Double {
  let span = (tolerance.upper - tolerance.lower).abs()
  if span <= 0.0000001 {
    0.0
  } else {
    uncertainty.expanded / span
  }
}

///|
pub fn uncertainty_is_acceptable(
  uncertainty : UncertaintyEstimate,
  tolerance : Tolerance,
  maximum_ratio : Double,
) -> Bool {
  uncertainty_ratio(uncertainty, tolerance) <= maximum_ratio.max(0.0)
}