///|
/// Framing utilities for adapting animated cameras to an output viewport.
pub enum FramingPolicy2 {
  FitWidthFraming
  FitHeightFraming
  FitBoundsFraming
  FillBoundsFraming
} derive(Debug)

///|
pub fn framing_policies2() -> Array[FramingPolicy2] {
  [FitWidthFraming, FitHeightFraming, FitBoundsFraming, FillBoundsFraming]
}

///|
pub struct CameraFraming2 {
  center : Point3
  radius : Double
  policy : FramingPolicy2
  margin : Double
} derive(Debug)

///|
pub fn camera_framing2(
  center : Point3,
  radius : Double,
  policy : FramingPolicy2,
  margin? : Double = 0.0,
) -> CameraFraming2 {
  { center, radius: radius.max(0.0), policy, margin: margin.max(0.0) }
}

///|
pub fn CameraFraming2::center(self : CameraFraming2) -> Point3 {
  self.center
}

///|
pub fn CameraFraming2::radius(self : CameraFraming2) -> Double {
  self.radius
}

///|
pub fn CameraFraming2::policy(self : CameraFraming2) -> FramingPolicy2 {
  self.policy
}

///|
pub fn CameraFraming2::margin(self : CameraFraming2) -> Double {
  self.margin
}

///|
pub fn CameraFraming2::effective_radius(self : CameraFraming2) -> Double {
  self.radius * (1.0 + self.margin)
}

///|
pub fn CameraRig::frame_sphere(
  self : CameraRig,
  framing : CameraFraming2,
) -> CameraRig {
  self.dolly_to_fit(framing.center, framing.effective_radius())
}

///|
pub fn project_bounds3d(camera : CameraRig, bounds : Bounds3D) -> Bounds2D {
  let minimum = bounds.minimum()
  let maximum = bounds.maximum()
  let corners = [
    point3(x=minimum.x(), y=minimum.y(), z=minimum.z()),
    point3(x=maximum.x(), y=minimum.y(), z=minimum.z()),
    point3(x=minimum.x(), y=maximum.y(), z=minimum.z()),
    point3(x=maximum.x(), y=maximum.y(), z=minimum.z()),
    point3(x=minimum.x(), y=minimum.y(), z=maximum.z()),
    point3(x=maximum.x(), y=minimum.y(), z=maximum.z()),
    point3(x=minimum.x(), y=maximum.y(), z=maximum.z()),
    point3(x=maximum.x(), y=maximum.y(), z=maximum.z()),
  ]
  let first = camera.project(corners[0])
  let mut min_x = first.x()
  let mut min_y = first.y()
  let mut max_x = first.x()
  let mut max_y = first.y()
  for corner in corners {
    let point = camera.project(corner)
    min_x = min_x.min(point.x())
    min_y = min_y.min(point.y())
    max_x = max_x.max(point.x())
    max_y = max_y.max(point.y())
  }
  { minimum: point2(x=min_x, y=min_y), maximum: point2(x=max_x, y=max_y) }
}

///|
pub fn camera_framing_error(
  camera : CameraRig,
  bounds : Bounds3D,
  margin : Double,
) -> Double {
  let projected = project_bounds3d(camera, bounds)
  let viewport = camera.viewport()
  let left = projected.minimum().x()
  let right = projected.maximum().x()
  let top = projected.minimum().y()
  let bottom = projected.maximum().y()
  let margin_x = viewport.width() * margin.max(0.0)
  let margin_y = viewport.height() * margin.max(0.0)
  (margin_x - left)
  .max(0.0)
  .max(right - viewport.width() + margin_x)
  .max((margin_y - top).max(0.0))
  .max(bottom - viewport.height() + margin_y)
}

///|
pub fn solve_orthographic_height(
  camera : CameraRig,
  bounds : Bounds3D,
  margin : Double,
) -> Double {
  let center = bounds.center()
  let size = bounds.size()
  let aspect = camera.viewport().aspect()
  let required_height = size.y().max(size.x() / aspect) *
    (1.0 + margin.max(0.0) * 2.0)
  let _ = center
  required_height
}

///|
pub fn camera_center_of_bounds(bounds : Bounds3D) -> Point3 {
  bounds.center()
}

///|
pub fn camera_radius_of_bounds(bounds : Bounds3D) -> Double {
  point3_length(bounds.size()) * 0.5
}

///|
pub struct ProjectionSample2 {
  world : Point3
  screen : Point2
  visible : Bool
  depth : Double
} derive(Debug)

///|
pub fn ProjectionSample2::world(self : ProjectionSample2) -> Point3 {
  self.world
}

///|
pub fn ProjectionSample2::screen(self : ProjectionSample2) -> Point2 {
  self.screen
}

///|
pub fn ProjectionSample2::visible(self : ProjectionSample2) -> Bool {
  self.visible
}

///|
pub fn ProjectionSample2::depth(self : ProjectionSample2) -> Double {
  self.depth
}

///|
pub fn project_samples2(
  camera : CameraRig,
  points : Array[Point3],
) -> Array[ProjectionSample2] {
  let result : Array[ProjectionSample2] = []
  for world in points {
    let screen = camera.project(world)
    let ray = camera.ray(screen)
    let delta = point3(
      x=world.x() - ray.0.x(),
      y=world.y() - ray.0.y(),
      z=world.z() - ray.0.z(),
    )
    result.push({
      world,
      screen,
      visible: camera.visible(world),
      depth: point3_dot(delta, ray.1),
    })
  }
  result
}

///|
pub fn reprojection_error(camera : CameraRig, world : Point3) -> Double {
  let screen = camera.project(world)
  let reconstructed = camera.unproject(
    screen,
    point3_length(
      point3(
        x=world.x() - camera.pose().position().x(),
        y=world.y() - camera.pose().position().y(),
        z=world.z() - camera.pose().position().z(),
      ),
    ),
  )
  point3_distance(world, reconstructed)
}