///|
pub fn sanitize_image(
  image : GrayImage,
  lower : Double,
  upper : Double,
) -> GrayImage {
  image_clamp_values(normalize_minmax(image), lower, upper)
}

///|
pub fn replace_non_finite_image(
  image : GrayImage,
  replacement : Double,
) -> GrayImage {
  let result = image.copy()
  for i in 0.. Bool,
) -> Int {
  let mut result = 0
  for value in image.pixels {
    if predicate(value) {
      result += 1
    }
  }
  result
}

///|
pub fn image_fraction(
  image : GrayImage,
  predicate : (Double) -> Bool,
) -> Double {
  if image.pixels.is_empty() {
    0.0
  } else {
    Double::from_int(image_value_count(image, predicate)) /
    Double::from_int(image.pixels.length())
  }
}

///|
pub fn image_histogram_probability(
  image : GrayImage,
  bins : Int,
) -> Array[Double] {
  let counts = histogram(image, bins).bins
  let total = image.pixels.length()
  if total == 0 {
    counts.map(fn(_) { 0.0 })
  } else {
    counts.map(fn(value) { Double::from_int(value) / Double::from_int(total) })
  }
}

///|
pub fn histogram_cumulative(histogram_value : Histogram) -> Array[Int] {
  let result : Array[Int] = []
  let mut total = 0
  for value in histogram_value.bins {
    total += value
    result.push(total)
  }
  result
}

///|
pub fn histogram_quantile(histogram_value : Histogram, q : Double) -> Double {
  let cumulative = histogram_cumulative(histogram_value)
  if cumulative.is_empty() || cumulative[cumulative.length() - 1] == 0 {
    histogram_value.minimum
  } else {
    let target = clamp_double(q, 0.0, 1.0) *
      Double::from_int(cumulative[cumulative.length() - 1])
    let mut index = 0
    while index < cumulative.length() &&
          Double::from_int(cumulative[index]) < target {
      index += 1
    }
    histogram_value.minimum +
    (histogram_value.maximum - histogram_value.minimum) *
    Double::from_int(index) /
    Double::from_int(cumulative.length())
  }
}

///|
pub fn image_quality_flags(image : GrayImage) -> Array[String] {
  let flags : Array[String] = []
  if image.width < 16 || image.height < 16 {
    flags.push("small-frame")
  }
  if saturation_fraction(image, 0.001, 0.999) > 0.1 {
    flags.push("saturated")
  }
  if image_dynamic_range(image) < 0.05 {
    flags.push("low-contrast")
  }
  if texture_noise(image) > 0.5 {
    flags.push("noisy")
  }
  flags
}

///|
pub fn image_quality_score(image : GrayImage) -> Double {
  let flags = image_quality_flags(image)
  clamp_double(1.0 - Double::from_int(flags.length()) / 4.0, 0.0, 1.0)
}

///|
pub fn profile_quality_flags(profile : ProfileSummary) -> Array[String] {
  let result : Array[String] = []
  if profile.samples.length() < 2 {
    result.push("short-profile")
  }
  if profile.edges.is_empty() {
    result.push("no-edges")
  }
  if profile.noise > 0.2 {
    result.push("noisy-profile")
  }
  if profile.signal_to_noise < 2.0 {
    result.push("weak-edge")
  }
  result
}

///|
pub fn measurement_quality_flags(
  measurement : Measurement,
  tolerance : Tolerance,
) -> Array[String] {
  let result : Array[String] = []
  if measurement.decision == Warn {
    result.push("near-limit")
  }
  if measurement.decision == Fail {
    result.push("out-of-tolerance")
  }
  if measurement.unit != tolerance.unit {
    result.push("unit-mismatch")
  }
  if !is_finite(measurement.value) {
    result.push("non-finite")
  }
  result
}

///|
pub fn normalize_measurements(
  measurements : Array[Measurement],
) -> Array[Measurement] {
  let values = measurement_values(measurements)
  let summary = describe_samples(values)
  if summary.standard_deviation <= 0.0000001 {
    measurements
  } else {
    measurements.map(fn(measurement) {
      {
        ..measurement,
        deviation: (measurement.value - summary.mean) /
        summary.standard_deviation,
      }
    })
  }
}

///|
pub fn measurements_within(
  measurements : Array[Measurement],
  lower : Double,
  upper : Double,
) -> Array[Measurement] {
  measurements.filter(fn(measurement) {
    measurement.value >= lower && measurement.value <= upper
  })
}

///|
pub fn measurement_bounds(measurements : Array[Measurement]) -> DoubleBounds {
  {
    min: minimum_value(measurement_values(measurements)),
    max: maximum_value(measurement_values(measurements)),
  }
}

///|
pub fn measurement_quantiles(
  measurements : Array[Measurement],
) -> Array[Double] {
  let values = measurement_values(measurements)
  [
    percentile_value(values, 0.05),
    percentile_value(values, 0.5),
    percentile_value(values, 0.95),
  ]
}

///|
pub fn quality_score_report(
  image : GrayImage,
  profile : ProfileSummary,
  measurements : Array[Measurement],
) -> String {
  let batch = BatchRun::new("quality")
  for measurement in measurements {
    batch.add(measurement)
  }
  let out = StringBuilder()
  out.write_string("image=")
  out.write_string(image_quality_score(image).to_string())
  out.write_string("; profile=")
  out.write_string(profile_quality(profile, 2.0).confidence.to_string())
  out.write_string("; measurements=")
  out.write_string(batch_quality(batch).to_string())
  out.to_string()
}