///|
pub(all) struct CalibrationGrid {
  columns : Int
  rows : Int
  spacing : Double
} derive(Debug, Eq)

///|
pub fn CalibrationGrid::new(
  columns~ : Int,
  rows~ : Int,
  spacing~ : Double,
) -> CalibrationGrid raise @core.GeometryError {
  if columns <= 0 || rows <= 0 || spacing <= 0.0 {
    raise @core.GeometryError::DegenerateInput(
      "calibration grid dimensions are invalid",
    )
  }
  { columns, rows, spacing }
}

///|
pub fn CalibrationGrid::length(grid : CalibrationGrid) -> Int {
  grid.columns * grid.rows
}

///|
pub fn CalibrationGrid::points(grid : CalibrationGrid) -> Array[@core.Point3] {
  let result : Array[@core.Point3] = []
  for row in 0.. @core.Rect2 raise @core.GeometryError {
  @core.rect2_from_points(
    grid.points().map(fn(p) { @core.Point2::new(x=p.x, y=p.y) }),
  )
}

///|
pub fn CalibrationGrid::coverage(
  grid : CalibrationGrid,
  size : ImageSize,
  intrinsics : CameraIntrinsics,
) -> Double raise @core.GeometryError {
  let bounds = grid.bounds()
  let pixel_min = normalized_to_pixel(bounds.min, intrinsics)
  let pixel_max = normalized_to_pixel(bounds.max, intrinsics)
  let image_area = Double::from_int(size.width * size.height)
  let width = if pixel_max.x > pixel_min.x {
    pixel_max.x - pixel_min.x
  } else {
    pixel_min.x - pixel_max.x
  }
  let height = if pixel_max.y > pixel_min.y {
    pixel_max.y - pixel_min.y
  } else {
    pixel_min.y - pixel_max.y
  }
  let area = width * height
  if image_area <= 0.0 {
    0.0
  } else if area > image_area {
    1.0
  } else {
    area / image_area
  }
}

///|
pub fn CalibrationGrid::world_point(
  grid : CalibrationGrid,
  index : Int,
) -> @core.Point3 raise @core.GeometryError {
  if index < 0 || index >= grid.length() {
    raise @core.GeometryError::DegenerateInput(
      "calibration grid index out of range",
    )
  }
  let row = index / grid.columns
  let column = index % grid.columns
  @core.Point3::new(
    x=Double::from_int(column) * grid.spacing,
    y=Double::from_int(row) * grid.spacing,
    z=0.0,
  )
}

///|
pub fn ray_bundle(
  pixels : ArrayView[@core.Point2],
  intrinsics : CameraIntrinsics,
) -> Array[@core.Vec3] raise @core.GeometryError {
  let result : Array[@core.Vec3] = []
  for pixel in pixels {
    result.push(bearing_from_pixel(pixel, intrinsics))
  }
  result
}