//! [`Camera`]: perspective projection plus view/projection matrix helpers.
//!
//! A camera holds only projection parameters; its *position* is a
//! [`Transform`] supplied at render time (so a camera can be an entity in the
//! scene). The view matrix is the inverse of that world transform.

use serde::{Deserialize, Serialize};

use crate::layer::{Layer, LayerMask};
use crate::math::{Mat4, Transform};

/// A perspective camera.
///
/// Stage 4 ships perspective projection only; orthographic and other
/// projections can be added later without changing the renderer interface.
///
/// A `Camera` is also a **reflected, addable component**: place one on an
/// entity and it becomes the scene's viewpoint, dual-editable from the editor
/// and scripts like any other component. (The runtime gathering of camera
/// entities into the render path is wired in a later stage; today the editor
/// drives its own viewport camera.)
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, crate::reflect::Reflect)]
pub struct Camera {
    /// Vertical field of view, in radians.
    pub fov_y: f32,
    /// Near clip plane distance (> 0).
    pub z_near: f32,
    /// Far clip plane distance (> `z_near`).
    pub z_far: f32,
    /// The layers this camera renders. An entity is drawn only if its
    /// [`Layer`](crate::layer::Layer) membership intersects this mask. Defaults
    /// to [`LayerMask::ALL`] (sees everything) — e.g. a minimap or first-person
    /// view-model camera narrows it. The host applies it when gathering objects.
    pub visibility: LayerMask,
}

impl Default for Camera {
    /// A 60° vertical FOV camera with a 0.1–1000 unit depth range that sees all
    /// layers.
    fn default() -> Self {
        Self {
            fov_y: 60_f32.to_radians(),
            z_near: 0.1,
            z_far: 1000.0,
            visibility: LayerMask::ALL,
        }
    }
}

impl Camera {
    /// Creates a perspective camera from a vertical FOV (radians) and clip range,
    /// seeing all layers.
    pub fn perspective(fov_y: f32, z_near: f32, z_far: f32) -> Self {
        Self {
            fov_y,
            z_near,
            z_far,
            visibility: LayerMask::ALL,
        }
    }

    /// Sets the layer-visibility mask (builder style).
    pub fn with_visibility(mut self, visibility: LayerMask) -> Self {
        self.visibility = visibility;
        self
    }

    /// Whether this camera renders an entity with the given layer membership.
    pub fn sees(&self, layer: Layer) -> bool {
        layer.matches(self.visibility)
    }

    /// The projection matrix for a viewport of the given `aspect` (width /
    /// height). Uses a reversed-Z-free, `0..1` NDC depth range (wgpu/Vulkan/
    /// DX/Metal convention).
    pub fn projection_matrix(&self, aspect: f32) -> Mat4 {
        Mat4::perspective_rh(
            self.fov_y,
            aspect.max(f32::EPSILON),
            self.z_near,
            self.z_far,
        )
    }

    /// The view matrix for a camera placed at `view_transform` — i.e. the
    /// inverse of the camera's world transform.
    pub fn view_matrix(view_transform: &Transform) -> Mat4 {
        view_transform.to_matrix().inverse()
    }

    /// The combined view-projection matrix: `projection * view`.
    pub fn view_projection(&self, aspect: f32, view_transform: &Transform) -> Mat4 {
        self.projection_matrix(aspect) * Self::view_matrix(view_transform)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::math::Vec3;

    #[test]
    fn visibility_filters_by_layer() {
        // Default camera sees every layer.
        let cam = Camera::default();
        assert!(cam.sees(Layer::on(7)));

        // A camera restricted to the "UI" layer (3) only sees layer-3 entities.
        let ui_cam = Camera::default().with_visibility(LayerMask::layer(3));
        assert!(ui_cam.sees(Layer::on(3)));
        assert!(!ui_cam.sees(Layer::on(0)));
        assert!(!ui_cam.sees(Layer::default())); // default layer 0
    }

    #[test]
    fn projection_is_finite_and_depth_mapped() {
        let cam = Camera::default();
        let proj = cam.projection_matrix(16.0 / 9.0);
        assert!(proj.is_finite());
        // A point on the near plane maps to NDC z ~ 0, the far plane to ~ 1.
        let near = proj.project_point3(Vec3::new(0.0, 0.0, -cam.z_near));
        let far = proj.project_point3(Vec3::new(0.0, 0.0, -cam.z_far));
        assert!(near.z.abs() < 1e-3, "near z = {}", near.z);
        assert!((far.z - 1.0).abs() < 1e-3, "far z = {}", far.z);
    }

    #[test]
    fn view_matrix_moves_world_into_camera_space() {
        // Camera at +Z looking at the origin: the origin should sit straight
        // ahead, down the camera's -Z axis.
        let cam_tf = Transform::looking_at(Vec3::new(0.0, 0.0, 5.0), Vec3::ZERO, Vec3::Y);
        let view = Camera::view_matrix(&cam_tf);
        let origin_in_view = view.project_point3(Vec3::ZERO);
        assert!((origin_in_view.x).abs() < 1e-5);
        assert!((origin_in_view.y).abs() < 1e-5);
        assert!(
            (origin_in_view.z + 5.0).abs() < 1e-4,
            "z = {}",
            origin_in_view.z
        );
    }
}
