///|
pub fn calibrate_from_grid(
  source_grid : Array[Point],
  target_grid : Array[Point],
) -> CalibrationFit {
  calibration_fit(source_grid, target_grid)
}

///|
pub fn calibrate_from_rectangle(
  source : Bounds,
  target_width : Double,
  target_height : Double,
) -> AffineTransform {
  let width = Double::from_int((source.max_x - source.min_x).max(1))
  let height = Double::from_int((source.max_y - source.min_y).max(1))
  AffineTransform::scaling(sx=target_width / width, sy=target_height / height)
}

///|
pub fn calibration_transform_at(
  fit : CalibrationFit,
  _point : Point,
) -> AffineTransform {
  fit.transform
}

///|
pub fn calibration_apply_to_components(
  fit : CalibrationFit,
  components : Array[Component],
) -> Array[Component] {
  components.map(fn(component) {
    let transformed_bounds = polygon_bounds([
      fit.transform.apply(
        Point::Point(
          x=Double::from_int(component.bounds.min_x),
          y=Double::from_int(component.bounds.min_y),
        ),
      ),
      fit.transform.apply(
        Point::Point(
          x=Double::from_int(component.bounds.max_x),
          y=Double::from_int(component.bounds.max_y),
        ),
      ),
    ])
    {
      ..component,
      bounds: transformed_bounds,
      centroid: fit.transform.apply(component.centroid),
      area: (Double::from_int(component.area) *
      fit.transform.determinant().abs())
      .round()
      .to_int(),
      perimeter: component.perimeter * calibration_scale(fit).x,
    }
  })
}

///|
pub fn calibration_apply_to_measurement(
  fit : CalibrationFit,
  measurement : Measurement,
) -> Measurement {
  let scale = (calibration_scale(fit).x + calibration_scale(fit).y) / 2.0
  {
    ..measurement,
    value: measurement.value * scale,
    deviation: measurement.deviation * scale,
  }
}

///|
pub fn calibration_apply_to_batch(
  fit : CalibrationFit,
  batch : BatchRun,
) -> BatchRun {
  let result = BatchRun::new(batch.id + "-calibrated")
  for measurement in batch.records {
    result.add(calibration_apply_to_measurement(fit, measurement))
  }
  result
}

///|
pub fn calibration_residual_plot(
  fit : CalibrationFit,
  width : Int,
  height : Int,
) -> GrayImage {
  let image = GrayImage(width=width.max(0), height=height.max(0), fill=0.0)
  if fit.residuals.is_empty() {
    image
  } else {
    let maximum = maximum_value(fit.residuals).max(0.0000001)
    for i in 0.. Double {
  let scale = calibration_scale(fit)
  (scale.x + scale.y) / 2.0
}

///|
pub fn calibration_anisotropy(fit : CalibrationFit) -> Double {
  let scale = calibration_scale(fit)
  if scale.x.max(scale.y) <= 0.0000001 {
    0.0
  } else {
    (scale.x - scale.y).abs() / scale.x.max(scale.y)
  }
}

///|
pub fn calibration_rotation(fit : CalibrationFit) -> Double {
  @math.atan2(fit.transform.c, fit.transform.a)
}

///|
pub fn calibration_validate_grid(
  source : Array[Point],
  target : Array[Point],
  maximum_error : Double,
) -> Array[PipelineDiagnostic] {
  let result : Array[PipelineDiagnostic] = []
  if source.length() != target.length() || source.length() < 3 {
    result.push(
      diagnostic_error(
        "calibration", "at least three paired points are required",
      ),
    )
  } else {
    let fit = calibration_fit(source, target)
    if fit.rms_error <= maximum_error {
      result.push(diagnostic_ok("calibration", "grid residual is within limit"))
    } else {
      result.push(
        diagnostic_error("calibration", "grid residual exceeds limit"),
      )
    }
  }
  result
}

///|
pub fn calibration_reference_distance(
  fit : CalibrationFit,
  first : Point,
  second : Point,
) -> Double {
  calibrated_distance(fit.transform, first, second)
}

///|
pub fn calibration_scale_report(fit : CalibrationFit) -> String {
  let scale = calibration_scale(fit)
  "scale_x=" +
  scale.x.to_string() +
  "; scale_y=" +
  scale.y.to_string() +
  "; rotation=" +
  calibration_rotation(fit).to_string()
}

///|
pub fn calibration_roundtrip_error(
  fit : AffineTransform,
  points : Array[Point],
) -> Double {
  let inverse = fit.inverse()
  let mut maximum = 0.0
  for point in points {
    maximum = maximum.max(distance(inverse.apply(fit.apply(point)), point))
  }
  maximum
}

///|
pub fn calibration_translate_origin(
  fit : CalibrationFit,
  origin : Point,
) -> AffineTransform {
  AffineTransform::translation(dx=-origin.x, dy=-origin.y).compose(
    fit.transform,
  )
}