//! Cameras. A [`Camera`] supplies a single `view_projection` matrix.
//!
//! This is the linchpin 2D->3D->VR seam:
//! - 2D is just [`OrthographicCamera`].
//! - 3D is [`PerspectiveCamera`], swapped in without touching drawables.
//! - VR is "two cameras, one per eye." Because the renderer takes a `&dyn Camera`
//!   per draw (see [`crate::gfx::RenderBackend::draw`]), per-eye rendering is a
//!   loop over cameras, not an architectural change.
//!
//! Projection note: Mawida targets wgpu, whose normalized device coordinates use
//! a `0.0..=1.0` depth range (like Vulkan/Metal/DX12, unlike OpenGL's
//! `-1.0..=1.0`). We therefore use glam's `*_rh` constructors, **not** the
//! `*_rh_gl` ones. Using the GL variants would push everything to the wrong
//! depth half and is a classic wgpu bug.

use crate::math::{Mat4, Vec2, Vec3};

pub trait Camera {
    /// The combined view * projection matrix to upload to the shader.
    fn view_projection(&self) -> Mat4;
}

/// 2D / orthographic camera.
///
/// By default it maps a pixel-space rectangle of `width x height` with the
/// origin at the top-left (y down), which is the convention most 2D game code
/// expects. Flip `y_down` to `false` for a y-up math convention.
#[derive(Debug, Clone, Copy)]
pub struct OrthographicCamera {
    /// Top-left of the visible region, in world units.
    pub position: Vec3,
    pub width: f32,
    pub height: f32,
    pub near: f32,
    pub far: f32,
    /// If true, +y points downward (screen convention). If false, +y is up.
    pub y_down: bool,
}

impl OrthographicCamera {
    /// A camera that shows exactly `width x height` pixels, origin top-left.
    pub fn new(width: f32, height: f32) -> Self {
        Self {
            position: Vec3::ZERO,
            width,
            height,
            near: -1.0,
            far: 1.0,
            y_down: true,
        }
    }
}

impl Camera for OrthographicCamera {
    fn view_projection(&self) -> Mat4 {
        let l = self.position.x;
        let r = self.position.x + self.width;
        let (b, t) = if self.y_down {
            // top-left origin: top = position.y, bottom = position.y + height
            (self.position.y + self.height, self.position.y)
        } else {
            (self.position.y, self.position.y + self.height)
        };
        // wgpu depth range 0..1 -> use orthographic_rh (NOT _rh_gl).
        Mat4::orthographic_rh(l, r, b, t, self.near, self.far)
    }
}

/// 3D / perspective camera. Present from day one so the 3D pivot needs no new
/// camera type, only a swap.
#[derive(Debug, Clone, Copy)]
pub struct PerspectiveCamera {
    pub eye: Vec3,
    pub target: Vec3,
    pub up: Vec3,
    /// Vertical field of view, radians.
    pub fov_y: f32,
    pub aspect: f32,
    pub near: f32,
    pub far: f32,
}

impl PerspectiveCamera {
    pub fn new(aspect: f32) -> Self {
        Self {
            eye: Vec3::new(0.0, 0.0, 5.0),
            target: Vec3::ZERO,
            up: Vec3::Y,
            fov_y: 60.0 * crate::math::DEG_TO_RAD,
            aspect,
            near: 0.1,
            far: 1000.0,
        }
    }
}

impl Camera for PerspectiveCamera {
    fn view_projection(&self) -> Mat4 {
        let view = Mat4::look_at_rh(self.eye, self.target, self.up);
        // wgpu depth range 0..1 -> perspective_rh (NOT _rh_gl).
        let proj = Mat4::perspective_rh(self.fov_y, self.aspect, self.near, self.far);
        proj * view
    }
}

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

    #[test]
    fn ortho_top_left_maps_to_ndc_top_left() {
        // 800x600, top-left origin. World (0,0) should land at NDC (-1, +1).
        let cam = OrthographicCamera::new(800.0, 600.0);
        let vp = cam.view_projection();
        let clip = vp * Vec4::new(0.0, 0.0, 0.0, 1.0);
        assert!((clip.x - -1.0).abs() < 1e-4, "x was {}", clip.x);
        assert!((clip.y - 1.0).abs() < 1e-4, "y was {}", clip.y);
    }

    #[test]
    fn ortho_bottom_right_maps_to_ndc_bottom_right() {
        let cam = OrthographicCamera::new(800.0, 600.0);
        let vp = cam.view_projection();
        let clip = vp * Vec4::new(800.0, 600.0, 0.0, 1.0);
        assert!((clip.x - 1.0).abs() < 1e-4, "x was {}", clip.x);
        assert!((clip.y - -1.0).abs() < 1e-4, "y was {}", clip.y);
    }

    #[test]
    fn perspective_keeps_target_centered() {
        let cam = PerspectiveCamera::new(1.0);
        let vp = cam.view_projection();
        let clip = vp * Vec4::new(0.0, 0.0, 0.0, 1.0); // target at origin
        // After perspective divide, the target should be near screen center.
        let ndc_x = clip.x / clip.w;
        let ndc_y = clip.y / clip.w;
        assert!(ndc_x.abs() < 1e-4);
        assert!(ndc_y.abs() < 1e-4);
    }
}

/// A perspective camera built from four RAW field-of-view half-angles: the
/// shape every OpenXR runtime hands you per eye (left/right/up/down angles
/// off the view axis, usually asymmetric). The symmetric PerspectiveCamera
/// is the special case where left = -right and up = -down. Element-wise
/// verified against the symmetric projection during the CCF VR bring-up.
#[derive(Debug, Clone, Copy)]
pub struct PerspectiveFovCamera {
    pub eye: Vec3,
    pub target: Vec3,
    pub up: Vec3,
    /// Half-angles in radians, signed: left/down are typically negative.
    pub angle_left: f32,
    pub angle_right: f32,
    pub angle_up: f32,
    pub angle_down: f32,
    pub near: f32,
    pub far: f32,
}

impl Camera for PerspectiveFovCamera {
    fn view_projection(&self) -> Mat4 {
        let view = Mat4::look_at_rh(self.eye, self.target, self.up);
        // The asymmetric frustum, wgpu depth 0..1. tan of the four angles
        // gives the near-plane extents; the off-center terms (a, b) shear
        // the frustum when the eye's fov is not symmetric (it never is).
        let (l, r) = (self.angle_left.tan(), self.angle_right.tan());
        let (d, u) = (self.angle_down.tan(), self.angle_up.tan());
        let (w, h) = (r - l, u - d);
        let a = (r + l) / w;
        let b = (u + d) / h;
        let proj = Mat4::from_cols_array(&[
            2.0 / w, 0.0, 0.0, 0.0, //
            0.0, 2.0 / h, 0.0, 0.0, //
            a, b, self.far / (self.near - self.far), -1.0, //
            0.0, 0.0, (self.far * self.near) / (self.near - self.far), 0.0,
        ]);
        proj * view
    }
}

// ---------------------------------------------------------------------------
// Camera controls (item 13 of the v0.4 sweep)
// ---------------------------------------------------------------------------

/// An orbit camera: spherical coordinates around a target, the day-one 3D
/// inspection camera (Blender/Godot viewport style). It IS a Camera: the
/// eye position falls out of (yaw, pitch, distance) each frame, so game
/// code steers three scalars instead of managing an eye point.
#[derive(Debug, Clone, Copy)]
pub struct OrbitCamera {
    pub target: Vec3,
    /// Radians around +Y (0 looks down -Z, the convention's forward).
    pub yaw: f32,
    /// Radians above the horizon; clamped shy of the poles so `up` and the
    /// view direction can never go parallel (the classic NaN at the pole).
    pub pitch: f32,
    pub distance: f32,
    pub fov_y: f32,
    pub aspect: f32,
    pub near: f32,
    pub far: f32,
}

impl OrbitCamera {
    pub fn new(target: Vec3, distance: f32, aspect: f32) -> Self {
        Self {
            target,
            yaw: 0.0,
            pitch: 0.4,
            distance,
            fov_y: 60.0 * crate::math::DEG_TO_RAD,
            aspect,
            near: 0.1,
            far: 1000.0,
        }
    }

    /// Steer by deltas (mouse drag, arrow keys): yaw wraps freely, pitch
    /// clamps shy of straight up/down, distance clamps positive.
    pub fn orbit(&mut self, d_yaw: f32, d_pitch: f32) {
        self.yaw += d_yaw;
        self.pitch = (self.pitch + d_pitch).clamp(-1.45, 1.45); // ~83 degrees
    }

    pub fn zoom(&mut self, delta: f32) {
        self.distance = (self.distance - delta).max(0.05);
    }

    /// The eye point, computed from the spherical coordinates.
    pub fn eye(&self) -> Vec3 {
        let (sy, cy) = self.yaw.sin_cos();
        let (sp, cp) = self.pitch.sin_cos();
        // yaw 0, pitch 0 puts the eye at +Z looking down -Z (the decided
        // convention: right-handed, +Y up, -Z forward).
        self.target + Vec3::new(sy * cp, sp, cy * cp) * self.distance
    }
}

impl Camera for OrbitCamera {
    fn view_projection(&self) -> Mat4 {
        let view = Mat4::look_at_rh(self.eye(), self.target, Vec3::Y);
        let proj = Mat4::perspective_rh(self.fov_y, self.aspect, self.near, self.far);
        proj * view
    }
}

/// A smoothed follow camera for 2D: wraps the orthographic projection and
/// pans it toward a target with exponential smoothing (frame-rate
/// independent: the classic 1 - exp(-stiffness * dt) lerp factor). The
/// world moves under a fixed window, which is all a 2D "camera" ever is.
#[derive(Debug, Clone, Copy)]
pub struct FollowCamera2d {
    ortho: OrthographicCamera,
    /// The current view center offset, in world pixels (0,0 = top-left
    /// origin exactly like the plain ortho camera).
    pub offset: Vec2,
    /// How eagerly the camera closes the gap: ~5 is loose, ~15 is snappy.
    pub stiffness: f32,
}

impl FollowCamera2d {
    pub fn new(width: f32, height: f32, stiffness: f32) -> Self {
        Self {
            ortho: OrthographicCamera::new(width, height),
            offset: Vec2::ZERO,
            stiffness,
        }
    }

    /// Move the view so `target` tends toward the CENTER of the window.
    /// Call once per update with the thing to follow (the player).
    pub fn follow(&mut self, target: Vec2, dt: f32) {
        let want = target - Vec2::new(self.ortho.width, self.ortho.height) * 0.5;
        // exponential smoothing: the same fraction of the remaining gap
        // closes every second, whatever the frame rate does
        let t = 1.0 - (-self.stiffness * dt).exp();
        self.offset += (want - self.offset) * t;
    }

    /// Snap instantly (level start, teleports).
    pub fn snap_to(&mut self, target: Vec2) {
        self.offset = target - Vec2::new(self.ortho.width, self.ortho.height) * 0.5;
    }
}

impl Camera for FollowCamera2d {
    fn view_projection(&self) -> Mat4 {
        // The pan is a translation BEFORE the ortho projection: world ->
        // shifted world -> the same top-left pixel mapping as ever.
        self.ortho.view_projection()
            * Mat4::from_translation(Vec3::new(-self.offset.x, -self.offset.y, 0.0))
    }
}

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

    #[test]
    fn orbit_eye_respects_the_convention() {
        // yaw 0, pitch 0: the eye sits on +Z looking down -Z at the target.
        let mut cam = OrbitCamera::new(Vec3::ZERO, 5.0, 1.0);
        cam.pitch = 0.0;
        assert!(cam.eye().abs_diff_eq(Vec3::new(0.0, 0.0, 5.0), 1e-5));
        // pitch clamps shy of the pole: no NaN view matrix, ever
        cam.orbit(0.0, 10.0);
        assert!(cam.pitch < 1.5);
        assert!(cam.view_projection().is_finite());
    }

    #[test]
    fn follow_centers_the_target() {
        let mut cam = FollowCamera2d::new(800.0, 600.0, 10.0);
        // long enough: the smoothing converges wherever it started
        for _ in 0..300 {
            cam.follow(Vec2::new(1000.0, 700.0), 1.0 / 60.0);
        }
        // the followed point now projects to NDC (0, 0): screen center
        let vp = cam.view_projection();
        let p = vp * Vec4::new(1000.0, 700.0, 0.0, 1.0);
        assert!(p.x.abs() < 1e-3 && p.y.abs() < 1e-3);
    }

    #[test]
    fn perspective_fov_symmetric_matches_perspective_rh() {
        // With symmetric angles the asymmetric matrix must reduce to the
        // classic one (the CCF bring-up proof, reproduced here).
        let half = 30.0_f32.to_radians();
        let cam = PerspectiveFovCamera {
            eye: Vec3::new(0.0, 0.0, 5.0),
            target: Vec3::ZERO,
            up: Vec3::Y,
            angle_left: -half,
            angle_right: half,
            angle_up: half,
            angle_down: -half,
            near: 0.1,
            far: 100.0,
        };
        let classic = Mat4::perspective_rh(2.0 * half, 1.0, 0.1, 100.0)
            * Mat4::look_at_rh(Vec3::new(0.0, 0.0, 5.0), Vec3::ZERO, Vec3::Y);
        let got = cam.view_projection();
        for i in 0..4 {
            assert!(got.col(i).abs_diff_eq(classic.col(i), 1e-5), "column {i}");
        }
    }

    #[test]
    fn follow_is_frame_rate_independent() {
        // the same second of smoothing in 6 steps or 60 lands together
        let mut a = FollowCamera2d::new(800.0, 600.0, 8.0);
        let mut b = FollowCamera2d::new(800.0, 600.0, 8.0);
        for _ in 0..6 {
            a.follow(Vec2::new(500.0, 500.0), 1.0 / 6.0);
        }
        for _ in 0..60 {
            b.follow(Vec2::new(500.0, 500.0), 1.0 / 60.0);
        }
        assert!((a.offset - b.offset).length() < 12.0); // same neighborhood
    }
}
