///|
pub fn edge_quality(
  samples : Array[ProfileSample],
  threshold : Double,
) -> QualitySummary {
  let profile = summarize_profile(samples, threshold)
  profile_quality(profile, 2.0)
}

///|
pub fn contrast_quality(image : GrayImage, bounds : Bounds) -> Double {
  let crop = crop_image(image, bounds)
  let values = crop.pixels
  if values.is_empty() {
    0.0
  } else {
    (maximum_value(values) - minimum_value(values)) /
    (variance_value(values).sqrt() + 0.0000001)
  }
}

///|
pub fn texture_noise(image : GrayImage) -> Double {
  let horizontal : Array[Double] = []
  let vertical : Array[Double] = []
  for y in 0.. Double {
  let gradient = gradient_magnitude(image)
  mean_value(gradient.pixels)
}

///|
pub fn sharpness_score(image : GrayImage) -> Double {
  let gradient = gradient_magnitude(image)
  variance_value(gradient.pixels)
}

///|
pub fn illumination_uniformity(image : GrayImage) -> Double {
  let summary = describe_samples(image.pixels)
  if summary.mean.abs() <= 0.0000001 {
    0.0
  } else {
    1.0 - summary.standard_deviation / summary.mean.abs()
  }
}

///|
pub fn saturation_fraction(
  image : GrayImage,
  lower : Double,
  upper : Double,
) -> Double {
  if image.pixels.is_empty() {
    0.0
  } else {
    let saturated = image.pixels
      .filter(fn(value) { value <= lower || value >= upper })
      .length()
    Double::from_int(saturated) / Double::from_int(image.pixels.length())
  }
}

///|
pub fn measurement_confidence(
  measurement : Measurement,
  tolerance : Tolerance,
  quality : Double,
) -> Double {
  let span = (tolerance.upper - tolerance.lower).abs()
  let deviation = if span <= 0.0 {
    1.0
  } else {
    (measurement.value - tolerance.nominal).abs() / span
  }
  clamp_double((1.0 - deviation) * quality, 0.0, 1.0)
}

///|
pub fn decision_penalty(decision : MeasurementDecision) -> Double {
  match decision {
    Pass => 0.0
    Warn => 0.25
    Fail => 1.0
  }
}

///|
pub fn batch_quality(batch : BatchRun) -> Double {
  let summary = batch.summary()
  if summary.total == 0 {
    0.0
  } else {
    let mut penalty = 0.0
    for measurement in batch.records {
      penalty += decision_penalty(measurement.decision)
    }
    clamp_double(1.0 - penalty / Double::from_int(summary.total), 0.0, 1.0)
  }
}

///|
pub fn quality_diagnostics(
  image : GrayImage,
  profile : ProfileSummary,
) -> Array[DiagnosticEvent] {
  let result : Array[DiagnosticEvent] = []
  result.push({
    code: "FOCUS",
    stage: "image",
    detail: "mean gradient magnitude",
    value: focus_score(image),
  })
  result.push({
    code: "SHARPNESS",
    stage: "image",
    detail: "gradient variance",
    value: sharpness_score(image),
  })
  result.push({
    code: "SNR",
    stage: "profile",
    detail: "strongest edge to MAD",
    value: profile.signal_to_noise,
  })
  result.push({
    code: "NOISE",
    stage: "profile",
    detail: "median absolute deviation",
    value: profile.noise,
  })
  result
}

///|
pub fn quality_summary_text(summary : QualitySummary) -> String {
  let out = StringBuilder()
  out.write_string("signal_to_noise=")
  out.write_string(summary.signal_to_noise.to_string())
  out.write_string("; edge_count=")
  out.write_string(summary.edge_count.to_string())
  out.write_string("; confidence=")
  out.write_string(summary.confidence.to_string())
  out.write_string("; stable=")
  out.write_string(summary.stable.to_string())
  out.to_string()
}

///|
pub fn stability_from_repetitions(
  values : Array[Double],
  maximum_deviation : Double,
) -> Bool {
  mean_absolute_deviation(values) <= maximum_deviation.max(0.0)
}

///|
pub fn outlier_flags(values : Array[Double], z_limit : Double) -> Array[Bool] {
  let summary = describe_samples(values)
  let result : Array[Bool] = []
  for value in values {
    let z = if summary.standard_deviation <= 0.0000001 {
      0.0
    } else {
      (value - summary.mean).abs() / summary.standard_deviation
    }
    result.push(z > z_limit.max(0.0))
  }
  result
}

///|
pub fn reject_outliers(
  values : Array[Double],
  z_limit : Double,
) -> Array[Double] {
  let flags = outlier_flags(values, z_limit)
  let result : Array[Double] = []
  for i in 0.. DoubleBounds {
  let summary = describe_samples(values)
  let margin = if summary.count == 0 {
    0.0
  } else {
    z_score.abs() *
    summary.standard_deviation /
    Double::from_int(summary.count).sqrt()
  }
  { min: summary.mean - margin, max: summary.mean + margin }
}

///|
pub fn gauge_risk_score(
  profile : ProfileSummary,
  tolerance : Tolerance,
  measurement : Double,
) -> Double {
  let distance_to_limit = (tolerance.upper - measurement)
    .abs()
    .min((measurement - tolerance.lower).abs())
  let signal_penalty = if profile.signal_to_noise <= 0.0 {
    1.0
  } else {
    1.0 / (1.0 + profile.signal_to_noise)
  }
  clamp_double(signal_penalty + 1.0 / (1.0 + distance_to_limit), 0.0, 2.0)
}

///|
pub fn quality_gate(
  summary : QualitySummary,
  minimum_confidence : Double,
) -> Bool {
  summary.stable &&
  summary.confidence >= minimum_confidence.clamp(min=0.0, max=1.0)
}

///|
pub fn normalized_quality(
  value : Double,
  lower : Double,
  upper : Double,
) -> Double {
  if upper <= lower {
    0.0
  } else {
    clamp_double((value - lower) / (upper - lower), 0.0, 1.0)
  }
}

///|
pub fn compare_quality(first : QualitySummary, second : QualitySummary) -> Int {
  if first.confidence > second.confidence {
    1
  } else if first.confidence < second.confidence {
    -1
  } else {
    0
  }
}