// Camera2D: 24 bytes (offset, target as Vector2; rotation, zoom as float)

///|
/// Camera2D, defines position/orientation in 2D space.
pub struct Camera2D {
  offset : Vector2
  target : Vector2
  rotation : Float
  zoom : Float
} derive(Eq, Debug)

///|
pub extend Camera2D with Eq::{equal, not_equal}

///|
pub extend Camera2D with @debug.Debug::{to_repr}

///|
/// Create a new Camera2D.
pub fn Camera2D::new(
  offset : Vector2,
  target : Vector2,
  rotation : Float,
  zoom : Float,
) -> Camera2D {
  { offset, target, rotation, zoom, }
}

///|
/// Serialize Camera2D to bytes.
pub fn Camera2D::to_bytes(c : Camera2D) -> Bytes {
  let buf = Buffer(size_hint=24)
  buf.write_float_le(c.offset.x)
  buf.write_float_le(c.offset.y)
  buf.write_float_le(c.target.x)
  buf.write_float_le(c.target.y)
  buf.write_float_le(c.rotation)
  buf.write_float_le(c.zoom)
  buf.to_bytes()
}

///|
/// Deserialize Camera2D from bytes.
pub fn Camera2D::from_bytes(b : Bytes) -> Camera2D {
  {
    offset: { x: read_float(b, 0), y: read_float(b, 4), },
    target: { x: read_float(b, 8), y: read_float(b, 12), },
    rotation: read_float(b, 16),
    zoom: read_float(b, 20),
  }
}

// Camera3D: 44 bytes (position, target, up as Vector3; fovy as float; projection as int)

///|
/// Camera3D, defines position/orientation in 3D space.
pub struct Camera3D {
  position : Vector3
  target : Vector3
  up : Vector3
  fovy : Float
  projection : Int32
} derive(Eq, Debug)

///|
pub extend Camera3D with Eq::{equal, not_equal}

///|
pub extend Camera3D with @debug.Debug::{to_repr}

///|
/// Create a new Camera3D.
pub fn Camera3D::new(
  position : Vector3,
  target : Vector3,
  up : Vector3,
  fovy : Float,
  projection : Int32,
) -> Camera3D {
  { position, target, up, fovy, projection, }
}

///|
/// Serialize Camera3D to bytes.
pub fn Camera3D::to_bytes(c : Camera3D) -> Bytes {
  let buf = Buffer(size_hint=44)
  buf.write_float_le(c.position.x)
  buf.write_float_le(c.position.y)
  buf.write_float_le(c.position.z)
  buf.write_float_le(c.target.x)
  buf.write_float_le(c.target.y)
  buf.write_float_le(c.target.z)
  buf.write_float_le(c.up.x)
  buf.write_float_le(c.up.y)
  buf.write_float_le(c.up.z)
  buf.write_float_le(c.fovy)
  buf.write_int_le(c.projection)
  buf.to_bytes()
}

///|
/// Deserialize Camera3D from bytes.
pub fn Camera3D::from_bytes(b : Bytes) -> Camera3D {
  {
    position: { x: read_float(b, 0), y: read_float(b, 4), z: read_float(b, 8), },
    target: {
      x: read_float(b, 12),
      y: read_float(b, 16),
      z: read_float(b, 20),
    },
    up: { x: read_float(b, 24), y: read_float(b, 28), z: read_float(b, 32), },
    fovy: read_float(b, 36),
    projection: read_int(b, 40),
  }
}

// Camera mode constants

///|
/// Custom camera mode, controlled by user (UpdateCamera() does nothing).
pub const CameraCustom : Int32 = 0

///|
/// Camera free mode.
pub const CameraFree : Int32 = 1

///|
/// Camera orbital mode, around target, zoom supported.
pub const CameraOrbital : Int32 = 2

///|
/// Camera first person mode.
pub const CameraFirstPerson : Int32 = 3

///|
/// Camera third person mode.
pub const CameraThirdPerson : Int32 = 4

// Camera projection constants

///|
/// Perspective projection.
pub const CameraPerspective : Int32 = 0

///|
/// Orthographic projection.
pub const CameraOrthographic : Int32 = 1

// Re-export passthrough camera functions

///|
pub using @ffi {end_mode_2d, end_mode_3d}

// Camera wrappers

///|
/// Begin 2D mode with custom camera (2D).
pub fn begin_mode_2d(camera : Camera2D) -> Unit {
  @ffi.begin_mode_2d(camera.to_bytes())
}

///|
/// Begin 3D mode with custom camera (3D).
pub fn begin_mode_3d(camera : Camera3D) -> Unit {
  @ffi.begin_mode_3d(camera.to_bytes())
}

///|
/// Update camera position for selected mode.
pub fn update_camera(camera : Camera3D, mode : Int32) -> Camera3D {
  Camera3D::from_bytes(@ffi.update_camera(camera.to_bytes(), mode))
}

///|
/// Update camera movement/rotation.
pub fn update_camera_pro(
  camera : Camera3D,
  movement : Vector3,
  rotation : Vector3,
  zoom : Float,
) -> Camera3D {
  Camera3D::from_bytes(
    @ffi.update_camera_pro(
      camera.to_bytes(),
      movement.to_bytes(),
      rotation.to_bytes(),
      zoom,
    ),
  )
}

///|
/// Get the screen space position for a 3D world space position.
pub fn get_world_to_screen(position : Vector3, camera : Camera3D) -> Vector2 {
  Vector2::from_bytes(
    @ffi.get_world_to_screen(position.to_bytes(), camera.to_bytes()),
  )
}

///|
/// Get the world space position for a 2D camera screen space position.
pub fn get_screen_to_world_2d(position : Vector2, camera : Camera2D) -> Vector2 {
  let inv = Matrix::invert(get_camera_matrix_2d(camera))
  let v = Vector3::transform({ x: position.x, y: position.y, z: 0.0, }, inv)
  { x: v.x, y: v.y, }
}

///|
/// Get the screen space position for a 2D camera world space position.
pub fn get_world_to_screen_2d(position : Vector2, camera : Camera2D) -> Vector2 {
  let mat = get_camera_matrix_2d(camera)
  let v = Vector3::transform({ x: position.x, y: position.y, z: 0.0, }, mat)
  { x: v.x, y: v.y, }
}

// ============================================================================
// Camera matrix
// ============================================================================

///|
/// Get camera transform matrix (view matrix).
pub fn get_camera_matrix(camera : Camera3D) -> Matrix {
  Matrix::look_at(camera.position, camera.target, camera.up)
}

///|
/// Get camera 2D transform matrix.
pub fn get_camera_matrix_2d(camera : Camera2D) -> Matrix {
  let mat_origin = Matrix::translate(-camera.target.x, -camera.target.y, 0.0)
  let mat_rotation = Matrix::rotate(
    { x: 0.0, y: 0.0, z: 1.0, },
    camera.rotation * 0.017453292519943295,
  )
  let mat_scale = Matrix::scale(camera.zoom, camera.zoom, 1.0)
  let mat_translation = Matrix::translate(camera.offset.x, camera.offset.y, 0.0)
  Matrix::multiply(
    Matrix::multiply(mat_origin, Matrix::multiply(mat_scale, mat_rotation)),
    mat_translation,
  )
}

///|
/// Get a ray trace from screen position (i.e mouse).
pub fn get_screen_to_world_ray(position : Vector2, camera : Camera3D) -> Ray {
  Ray::from_bytes(
    @ffi.get_screen_to_world_ray(position.to_bytes(), camera.to_bytes()),
  )
}

///|
/// Get a ray trace from screen position (i.e mouse) in a viewport.
pub fn get_screen_to_world_ray_ex(
  position : Vector2,
  camera : Camera3D,
  width : Int32,
  height : Int32,
) -> Ray {
  Ray::from_bytes(
    @ffi.get_screen_to_world_ray_ex(
      position.to_bytes(),
      camera.to_bytes(),
      width,
      height,
    ),
  )
}

///|
/// Get size position for a 3D world space position.
pub fn get_world_to_screen_ex(
  position : Vector3,
  camera : Camera3D,
  width : Int32,
  height : Int32,
) -> Vector2 {
  Vector2::from_bytes(
    @ffi.get_world_to_screen_ex(
      position.to_bytes(),
      camera.to_bytes(),
      width,
      height,
    ),
  )
}