///|
pub fn calibrate_anisotropic(
  pixel_x : Double,
  pixel_y : Double,
  physical_x : Double,
  physical_y : Double,
) -> AffineTransform {
  match try_calibrate_anisotropic(pixel_x, pixel_y, physical_x, physical_y) {
    Some(transform) => transform
    None => AffineTransform::identity()
  }
}

///|
pub fn try_calibrate_anisotropic(
  pixel_x : Double,
  pixel_y : Double,
  physical_x : Double,
  physical_y : Double,
) -> AffineTransform? {
  if pixel_x > 0.0 &&
    pixel_y > 0.0 &&
    is_finite(pixel_x) &&
    is_finite(pixel_y) &&
    is_finite(physical_x) &&
    is_finite(physical_y) {
    Some(
      AffineTransform::scaling(sx=physical_x / pixel_x, sy=physical_y / pixel_y),
    )
  } else {
    None
  }
}

///|
pub fn calibration_fit(
  source : Array[Point],
  target : Array[Point],
) -> CalibrationFit {
  fit_affine(source, target)
}

///|
pub fn calibration_scale(fit : CalibrationFit) -> Vector2 {
  {
    x: (fit.transform.a * fit.transform.a + fit.transform.c * fit.transform.c).sqrt(),
    y: (fit.transform.b * fit.transform.b + fit.transform.d * fit.transform.d).sqrt(),
  }
}

///|
pub fn calibration_is_usable(
  fit : CalibrationFit,
  maximum_rms_error : Double,
) -> Bool {
  is_finite(fit.rms_error) && fit.rms_error <= maximum_rms_error.max(0.0)
}

///|
pub fn calibrated_distance(
  fit : AffineTransform,
  first : Point,
  second : Point,
) -> Double {
  distance(fit.apply(first), fit.apply(second))
}

///|
pub fn calibrated_area(fit : AffineTransform, pixel_area : Double) -> Double {
  fit.determinant().abs() * pixel_area
}

///|
pub fn calibrated_circle(fit : AffineTransform, circle : Circle) -> Circle {
  let center = fit.apply(circle.center)
  let scale = calibration_scale({
    transform: fit,
    residuals: [],
    rms_error: 0.0,
    max_error: 0.0,
  })
  let radius = circle.radius * (scale.x + scale.y) / 2.0
  { center, radius, rms_error: circle.rms_error * (scale.x + scale.y) / 2.0 }
}

///|
pub fn residual_summary(fit : CalibrationFit) -> StatsSummary {
  describe_samples(fit.residuals)
}

///|
pub fn calibration_report(fit : CalibrationFit) -> String {
  let out = StringBuilder()
  out.write_string("rms_error=")
  out.write_string(fit.rms_error.to_string())
  out.write_string("; max_error=")
  out.write_string(fit.max_error.to_string())
  out.write_string("; samples=")
  out.write_string(fit.residuals.length().to_string())
  out.to_string()
}

///|
pub fn map_reference_grid(
  fit : AffineTransform,
  columns : Int,
  rows : Int,
  spacing_x : Double,
  spacing_y : Double,
) -> Array[Point] {
  let result : Array[Point] = []
  for y in 0.. Point {
  if references.is_empty() {
    point
  } else {
    let mut result = references[0]
    let mut best = distance(point, result)
    for reference in references[1:] {
      let current = distance(point, reference)
      if current < best {
        best = current
        result = reference
      }
    }
    result
  }
}

///|
pub fn calibration_quality_score(
  fit : CalibrationFit,
  sample_count : Int,
) -> Double {
  let count_factor = (1.0 - 1.0 / Double::from_int(sample_count.max(1))).max(
    0.0,
  )
  count_factor / (1.0 + fit.rms_error.abs())
}