///|
/// Projection model used by a media camera.
pub enum ProjectionModel {
  Perspective
  Orthographic
} derive(Debug)

///|
/// A viewport in physical pixels. The camera keeps this separate from the
/// animation timeline so a composition can be rendered at several sizes.
pub struct Viewport {
  width : Double
  height : Double
  pixel_ratio : Double
} derive(Debug)

///|
pub fn viewport(
  width : Double,
  height : Double,
  pixel_ratio? : Double = 1.0,
) -> Viewport {
  {
    width: width.max(1.0),
    height: height.max(1.0),
    pixel_ratio: pixel_ratio.max(0.01),
  }
}

///|
pub fn Viewport::width(self : Viewport) -> Double {
  self.width
}

///|
pub fn Viewport::height(self : Viewport) -> Double {
  self.height
}

///|
pub fn Viewport::pixel_ratio(self : Viewport) -> Double {
  self.pixel_ratio
}

///|
pub fn Viewport::aspect(self : Viewport) -> Double {
  self.width / self.height
}

///|
pub fn Viewport::physical_width(self : Viewport) -> Double {
  self.width * self.pixel_ratio
}

///|
pub fn Viewport::physical_height(self : Viewport) -> Double {
  self.height * self.pixel_ratio
}

///|
pub fn Viewport::center(self : Viewport) -> Point2 {
  point2(x=self.width * 0.5, y=self.height * 0.5)
}

///|
/// Lens parameters shared by 2D and 3D camera tracks.
pub struct CameraLens {
  model : ProjectionModel
  field_of_view : Double
  orthographic_height : Double
  near_plane : Double
  far_plane : Double
  aperture : Double
  focus_distance : Double
} derive(Debug)

///|
pub fn perspective_lens(
  field_of_view : Double,
  near_plane? : Double = 0.01,
  far_plane? : Double = 10000.0,
) -> CameraLens {
  {
    model: Perspective,
    field_of_view: clamp(field_of_view, 0.01, 3.13),
    orthographic_height: 10.0,
    near_plane: near_plane.max(0.0001),
    far_plane: far_plane.max(near_plane.max(0.0001) + 0.001),
    aperture: 0.0,
    focus_distance: 10.0,
  }
}

///|
pub fn orthographic_lens(
  height : Double,
  near_plane? : Double = -10000.0,
  far_plane? : Double = 10000.0,
) -> CameraLens {
  {
    model: Orthographic,
    field_of_view: 1.0,
    orthographic_height: height.max(0.0001),
    near_plane,
    far_plane: far_plane.max(near_plane + 0.001),
    aperture: 0.0,
    focus_distance: 10.0,
  }
}

///|
pub fn CameraLens::model(self : CameraLens) -> ProjectionModel {
  self.model
}

///|
pub fn CameraLens::field_of_view(self : CameraLens) -> Double {
  self.field_of_view
}

///|
pub fn CameraLens::orthographic_height(self : CameraLens) -> Double {
  self.orthographic_height
}

///|
pub fn CameraLens::near_plane(self : CameraLens) -> Double {
  self.near_plane
}

///|
pub fn CameraLens::far_plane(self : CameraLens) -> Double {
  self.far_plane
}

///|
pub fn CameraLens::aperture(self : CameraLens) -> Double {
  self.aperture
}

///|
pub fn CameraLens::focus_distance(self : CameraLens) -> Double {
  self.focus_distance
}

///|
pub fn CameraLens::with_aperture(
  self : CameraLens,
  aperture : Double,
) -> CameraLens {
  {
    model: self.model,
    field_of_view: self.field_of_view,
    orthographic_height: self.orthographic_height,
    near_plane: self.near_plane,
    far_plane: self.far_plane,
    aperture: aperture.max(0.0),
    focus_distance: self.focus_distance,
  }
}

///|
pub fn CameraLens::with_focus_distance(
  self : CameraLens,
  distance : Double,
) -> CameraLens {
  {
    model: self.model,
    field_of_view: self.field_of_view,
    orthographic_height: self.orthographic_height,
    near_plane: self.near_plane,
    far_plane: self.far_plane,
    aperture: self.aperture,
    focus_distance: distance.max(self.near_plane),
  }
}

///|
/// A camera pose with an explicit target. Target-based poses are convenient
/// for editorial tools because camera orientation remains stable when the
/// tracked object moves.
pub struct CameraPose {
  position : Point3
  target : Point3
  up : Point3
  roll : Double
} derive(Debug)

///|
pub fn camera_pose(
  position : Point3,
  target : Point3,
  up? : Point3 = point3(x=0.0, y=1.0, z=0.0),
  roll? : Double = 0.0,
) -> CameraPose {
  { position, target, up, roll }
}

///|
pub fn CameraPose::position(self : CameraPose) -> Point3 {
  self.position
}

///|
pub fn CameraPose::target(self : CameraPose) -> Point3 {
  self.target
}

///|
pub fn CameraPose::up(self : CameraPose) -> Point3 {
  self.up
}

///|
pub fn CameraPose::roll(self : CameraPose) -> Double {
  self.roll
}

///|
pub fn CameraPose::forward(self : CameraPose) -> Point3 {
  point3(
    x=self.target.x() - self.position.x(),
    y=self.target.y() - self.position.y(),
    z=self.target.z() - self.position.z(),
  )
  |> point3_normalized()
}

///|
pub fn CameraPose::right(self : CameraPose) -> Point3 {
  point3_cross(self.forward(), point3_normalized(self.up))
  |> point3_normalized()
}

///|
pub fn CameraPose::true_up(self : CameraPose) -> Point3 {
  point3_cross(self.right(), self.forward()) |> point3_normalized()
}

///|
pub fn CameraPose::look_at(self : CameraPose, target : Point3) -> CameraPose {
  { position: self.position, target, up: self.up, roll: self.roll }
}

///|
pub fn CameraPose::translated(self : CameraPose, offset : Point3) -> CameraPose {
  {
    position: point3(
      x=self.position.x() + offset.x(),
      y=self.position.y() + offset.y(),
      z=self.position.z() + offset.z(),
    ),
    target: point3(
      x=self.target.x() + offset.x(),
      y=self.target.y() + offset.y(),
      z=self.target.z() + offset.z(),
    ),
    up: self.up,
    roll: self.roll,
  }
}

///|
pub fn CameraPose::lerp(
  self : CameraPose,
  other : CameraPose,
  ratio : Double,
) -> CameraPose {
  let t = clamp01(ratio)
  {
    position: self.position.lerp(other.position, t),
    target: self.target.lerp(other.target, t),
    up: self.up.lerp(other.up, t),
    roll: self.roll + (other.roll - self.roll) * t,
  }
}

///|
/// A camera rig combines animated pose, lens, and viewport policies.
pub struct CameraRig {
  pose : CameraPose
  lens : CameraLens
  viewport : Viewport
  jitter : Point2
} derive(Debug)

///|
pub fn camera_rig(
  pose : CameraPose,
  lens : CameraLens,
  viewport : Viewport,
) -> CameraRig {
  { pose, lens, viewport, jitter: point2(x=0.0, y=0.0) }
}

///|
pub fn CameraRig::pose(self : CameraRig) -> CameraPose {
  self.pose
}

///|
pub fn CameraRig::lens(self : CameraRig) -> CameraLens {
  self.lens
}

///|
pub fn CameraRig::viewport(self : CameraRig) -> Viewport {
  self.viewport
}

///|
pub fn CameraRig::jitter(self : CameraRig) -> Point2 {
  self.jitter
}

///|
pub fn CameraRig::with_jitter(self : CameraRig, jitter : Point2) -> CameraRig {
  { pose: self.pose, lens: self.lens, viewport: self.viewport, jitter }
}

///|
fn CameraRig::camera_coordinates(self : CameraRig, world : Point3) -> Point3 {
  let delta = point3(
    x=world.x() - self.pose.position.x(),
    y=world.y() - self.pose.position.y(),
    z=world.z() - self.pose.position.z(),
  )
  let right = self.pose.right()
  let up = self.pose.true_up()
  let forward = self.pose.forward()
  point3(
    x=point3_dot(delta, right),
    y=point3_dot(delta, up),
    z=point3_dot(delta, forward),
  )
}

///|
pub fn CameraRig::project(self : CameraRig, world : Point3) -> Point2 {
  let camera = self.camera_coordinates(world)
  let aspect = self.viewport.aspect()
  match self.lens.model {
    Perspective => {
      let depth = camera.z().max(self.lens.near_plane)
      let scale = @math.tan(self.lens.field_of_view * 0.5)
      let screen_x = (camera.x() / (depth * scale * aspect) * 0.5 + 0.5) *
        self.viewport.width +
        self.jitter.x()
      let screen_y = (0.5 - camera.y() / (depth * scale) * 0.5) *
        self.viewport.height +
        self.jitter.y()
      point2(x=screen_x, y=screen_y)
    }
    Orthographic => {
      let half_height = self.lens.orthographic_height * 0.5
      let half_width = half_height * aspect
      point2(
        x=(camera.x() / half_width * 0.5 + 0.5) * self.viewport.width +
          self.jitter.x(),
        y=(0.5 - camera.y() / half_height * 0.5) * self.viewport.height +
          self.jitter.y(),
      )
    }
  }
}

///|
pub fn CameraRig::visible(self : CameraRig, world : Point3) -> Bool {
  let camera = self.camera_coordinates(world)
  camera.z() >= self.lens.near_plane && camera.z() <= self.lens.far_plane
}

///|
pub fn CameraRig::unproject(
  self : CameraRig,
  screen : Point2,
  depth : Double,
) -> Point3 {
  let aspect = self.viewport.aspect()
  let x = (screen.x() - self.jitter.x()) / self.viewport.width * 2.0 - 1.0
  let y = 1.0 - (screen.y() - self.jitter.y()) / self.viewport.height * 2.0
  let camera = match self.lens.model {
    Perspective => {
      let scale = @math.tan(self.lens.field_of_view * 0.5)
      point3(x=x * depth * scale * aspect, y=y * depth * scale, z=depth)
    }
    Orthographic => {
      let half_height = self.lens.orthographic_height * 0.5
      let half_width = half_height * aspect
      point3(x=x * half_width, y=y * half_height, z=depth)
    }
  }
  let right = self.pose.right()
  let up = self.pose.true_up()
  let forward = self.pose.forward()
  point3(
    x=self.pose.position.x() +
      right.x() * camera.x() +
      up.x() * camera.y() +
      forward.x() * camera.z(),
    y=self.pose.position.y() +
      right.y() * camera.x() +
      up.y() * camera.y() +
      forward.y() * camera.z(),
    z=self.pose.position.z() +
      right.z() * camera.x() +
      up.z() * camera.y() +
      forward.z() * camera.z(),
  )
}

///|
pub fn CameraRig::ray(self : CameraRig, screen : Point2) -> (Point3, Point3) {
  let origin = match self.lens.model {
    Perspective => self.pose.position
    Orthographic => self.unproject(screen, self.lens.near_plane)
  }
  let far = self.unproject(screen, self.lens.far_plane)
  let direction = point3_normalized(
    point3(
      x=far.x() - origin.x(),
      y=far.y() - origin.y(),
      z=far.z() - origin.z(),
    ),
  )
  (origin, direction)
}

///|
pub fn CameraRig::fit_distance(self : CameraRig, radius : Double) -> Double {
  match self.lens.model {
    Perspective => radius.max(0.0) / @math.tan(self.lens.field_of_view * 0.5)
    Orthographic => radius.max(0.0)
  }
}

///|
pub fn CameraRig::dolly_to_fit(
  self : CameraRig,
  center : Point3,
  radius : Double,
) -> CameraRig {
  let direction = self.pose.forward()
  let distance = self.fit_distance(radius)
  let position = point3(
    x=center.x() - direction.x() * distance,
    y=center.y() - direction.y() * distance,
    z=center.z() - direction.z() * distance,
  )
  camera_rig(
    camera_pose(position, center, up=self.pose.up, roll=self.pose.roll),
    self.lens,
    self.viewport,
  ).with_jitter(self.jitter)
}

///|
/// A camera keyframe carries pose and lens changes as one editorial unit.
pub struct CameraKeyframe {
  time : Double
  pose : CameraPose
  lens : CameraLens
  curve : Curve
} derive(Debug)

///|
pub fn camera_keyframe(
  time : Double,
  pose : CameraPose,
  lens : CameraLens,
  curve? : Curve = Curve::builtin(Linear),
) -> CameraKeyframe raise MotionError {
  ensure_finite(time)
  { time, pose, lens, curve }
}

///|
pub fn CameraKeyframe::time(self : CameraKeyframe) -> Double {
  self.time
}

///|
pub fn CameraKeyframe::pose(self : CameraKeyframe) -> CameraPose {
  self.pose
}

///|
pub fn CameraKeyframe::lens(self : CameraKeyframe) -> CameraLens {
  self.lens
}

///|
pub fn CameraKeyframe::curve(self : CameraKeyframe) -> Curve {
  self.curve
}

///|
pub struct CameraTrack {
  frames : Array[CameraKeyframe]
  viewport : Viewport
} derive(Debug)

///|
pub fn CameraTrack::new(
  frames : Array[CameraKeyframe],
  viewport : Viewport,
) -> CameraTrack raise MotionError {
  if frames.length() == 0 {
    raise MotionError::EmptyTrack
  }
  for index in 1.. Int {
  self.frames.length()
}

///|
pub fn CameraTrack::start_time(self : CameraTrack) -> Double {
  self.frames[0].time
}

///|
pub fn CameraTrack::end_time(self : CameraTrack) -> Double {
  self.frames[self.frames.length() - 1].time
}

///|
pub fn CameraTrack::duration(self : CameraTrack) -> Double {
  self.end_time() - self.start_time()
}

///|
pub fn CameraTrack::keyframes(self : CameraTrack) -> Array[CameraKeyframe] {
  self.frames.copy()
}

///|
fn CameraTrack::segment(
  self : CameraTrack,
  time : Double,
) -> (CameraKeyframe, CameraKeyframe, Double) {
  let last = self.frames.length() - 1
  if time <= self.frames[0].time {
    return (self.frames[0], self.frames[0], 0.0)
  }
  if time >= self.frames[last].time {
    return (self.frames[last], self.frames[last], 0.0)
  }
  for index in 0.. CameraRig {
  let (left, right, ratio) = self.segment(time)
  let pose = left.pose.lerp(right.pose, ratio)
  let lens = if ratio < 0.5 { left.lens } else { right.lens }
  camera_rig(pose, lens, self.viewport)
}

///|
pub fn CameraTrack::sample_many(
  self : CameraTrack,
  count : Int,
) -> Array[CameraRig] raise MotionError {
  if count < 2 {
    raise MotionError::InvalidSampleCount(count)
  }
  let values : Array[CameraRig] = []
  for index in 0.. CameraTrack raise MotionError {
  let frames : Array[CameraKeyframe] = []
  for frame in self.frames {
    frames.push({
      time: frame.time + delta,
      pose: frame.pose,
      lens: frame.lens,
      curve: frame.curve,
    })
  }
  CameraTrack::new(frames, self.viewport)
}