// Camera: ties world, view, clip, NDC and window coordinates together.
//
//   local --Model--> world --View--> view --Projection--> clip --/w--> NDC --Viewport--> window

///|
pub(all) struct Camera {
  /// Camera -> world transform (position + orientation). Looks down its local -Z.
  pose : Rigid
  projection : Projection
  viewport : Viewport
} derive(Eq, Debug)

///|
pub fn Camera::new(
  pose : Rigid,
  projection : Projection,
  viewport : Viewport,
) -> Camera {
  { pose, projection, viewport, }
}

///|
pub fn Camera::look_at(
  eye : Vec3,
  target : Vec3,
  up : Vec3,
  projection : Projection,
  viewport : Viewport,
) -> Camera {
  { pose: Rigid::look_at(eye, target, up), projection, viewport, }
}

///|
/// Orbit / FPS style: `yaw`, `pitch` give the viewing direction as a `Spherical` direction
/// (yaw 0, pitch 0 looks down -Z; +yaw turns left about +Y; +pitch looks up). The eye sits
/// `distance` behind `target` along that direction.
pub fn Camera::orbit(
  target : Vec3,
  distance : Double,
  yaw : Double,
  pitch : Double,
  projection : Projection,
  viewport : Viewport,
) -> Camera {
  let f = Spherical::new(1.0, yaw, pitch).to_cartesian()
  let eye = target - f.scale(distance)
  { pose: Rigid::look_at(eye, target, Vec3::unit_y()), projection, viewport, }
}

///|
pub fn Camera::eye(self : Camera) -> Vec3 {
  self.pose.translation
}

///|
pub fn Camera::forward(self : Camera) -> Vec3 {
  self.pose.rotation.rotate(Vec3::new(0.0, 0.0, -1.0))
}

///|
pub fn Camera::right(self : Camera) -> Vec3 {
  self.pose.rotation.rotate(Vec3::unit_x())
}

///|
pub fn Camera::up(self : Camera) -> Vec3 {
  self.pose.rotation.rotate(Vec3::unit_y())
}

///|
/// World -> view matrix.
pub fn Camera::view_matrix(self : Camera) -> Mat4 {
  self.pose.to_mat4().rigid_inverse()
}

///|
pub fn Camera::proj_matrix(self : Camera) -> Mat4 {
  self.projection.to_mat4()
}

///|
/// World -> clip matrix.
pub fn Camera::view_proj(self : Camera) -> Mat4 {
  self.projection.to_mat4() * self.view_matrix()
}

///|
/// World -> clip with the eye moved to the origin ("camera-relative / floating origin"
/// rendering). Pair with model matrices whose translation has `eye` subtracted in DOUBLE
/// precision (`Mat4::relative_to`) before the single rounding to float: this removes the
/// jitter of huge world coordinates.
pub fn Camera::view_proj_rte(self : Camera) -> Mat4 {
  let inv_rot = Mat4::from_mat3(Mat3::from_quat(self.pose.rotation).transpose())
  self.projection.to_mat4() * inv_rot
}

///|
pub fn Camera::world_to_view(self : Camera, p : Vec3) -> Vec3 {
  self.pose.inverse().mul_point(p)
}

///|
pub fn Camera::view_to_world(self : Camera, p : Vec3) -> Vec3 {
  self.pose.mul_point(p)
}

///|
pub fn Camera::world_to_clip(self : Camera, p : Vec3) -> Vec4 {
  self.view_proj().mul_vec4(p.to_vec4(1.0))
}

///|
/// World -> NDC. `None` if behind the eye plane (perspective).
pub fn Camera::world_to_ndc(self : Camera, p : Vec3) -> Vec3? {
  self.projection.project_view(self.world_to_view(p))
}

///|
/// World -> window: (x px, y px, window depth). `None` if behind the camera.
pub fn Camera::world_to_screen(self : Camera, p : Vec3) -> Vec3? {
  match self.world_to_ndc(p) {
    Some(n) => Some(self.viewport.ndc_to_screen(n, self.projection.depth))
    None => None
  }
}

///|
/// Window position + window depth (as read back from the depth buffer) -> world.
pub fn Camera::screen_to_world(
  self : Camera,
  sx : Double,
  sy : Double,
  depth : Double,
) -> Vec3 {
  let ndc = self.viewport.screen_to_ndc(sx, sy, depth, self.projection.depth)
  self.pose.mul_point(self.projection.unproject_ndc(ndc))
}

///|
/// World point under window position (sx, sy) at `distance` in front of the camera plane.
pub fn Camera::screen_to_world_at_distance(
  self : Camera,
  sx : Double,
  sy : Double,
  distance : Double,
) -> Vec3 {
  let ray = self.pick_ray(sx, sy)
  match self.projection.kind {
    Perspective => {
      // point on the ray whose depth along the camera axis equals `distance`
      let cosang = ray.dir.dot(self.forward())
      ray.at(distance / cosang)
    }
    Orthographic =>
      // origin is on the near plane (depth = near)
      ray.at(distance - self.projection.near)
  }
}

///|
/// World-space picking ray through a window position (unit direction).
pub fn Camera::pick_ray(self : Camera, sx : Double, sy : Double) -> Ray {
  let ndc = self.viewport.screen_to_ndc_xy({ x: sx, y: sy, })
  let (o, d) = self.projection.view_ray(ndc.x, ndc.y)
  { origin: self.pose.mul_point(o), dir: self.pose.mul_dir(d), }
}

///|
pub fn Camera::frustum(self : Camera) -> Frustum {
  Frustum::from_view_proj(self.view_proj(), self.projection.depth)
}

///|
/// Change the viewport and keep the picture undistorted by updating the projection aspect.
pub fn Camera::resize(self : Camera, width : Double, height : Double) -> Camera {
  let vp = Viewport::new(self.viewport.x, self.viewport.y, width, height)
  {
    ..self,
    viewport: vp,
    projection: self.projection.with_aspect(width / height),
  }
}

///|
/// Model matrix expressed relative to `origin` (translation reduced in double precision).
/// Equivalent to `translation(-origin) * m` but without a full matrix multiply.
pub fn Mat4::relative_to(self : Mat4, origin : Vec3) -> Mat4 {
  {
    ..self,
    m03: self.m03 - origin.x,
    m13: self.m13 - origin.y,
    m23: self.m23 - origin.z,
  }
}