use nalgebra::{Matrix4, Point3 as NPoint3, Vector3 as NVec3, Vector4 as NVec4};
use uom::si::angle::radian;
use uom::si::f64::Length;
use uom::si::length::millimeter;

use bone_types::{
    AxisAngle, Camera3, Point3, Projection, ProjectionKind, Result, Tolerance, TypesError,
    UnitVec3, Vec3, ZoomFactor,
};

use crate::camera::ViewportExtent;

const ORTHO_DEPTH_FACTOR: f64 = 8.0;
const RAY_TOLERANCE: Tolerance = Tolerance::new(1.0e-12);
const FOCAL_DENOM_FLOOR: f64 = 1.0e-9;

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct ViewportPoint {
    x: f64,
    y: f64,
}

impl ViewportPoint {
    pub fn new(x: f64, y: f64) -> Result<Self> {
        if !x.is_finite() || !y.is_finite() {
            return Err(TypesError::NonFiniteViewportPixel { x, y });
        }
        Ok(Self { x, y })
    }

    #[must_use]
    pub const fn x(self) -> f64 {
        self.x
    }

    #[must_use]
    pub const fn y(self) -> f64 {
        self.y
    }
}

impl core::fmt::Display for ViewportPoint {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "({}, {}) px", self.x, self.y)
    }
}

pub fn clip_from_world(camera: Camera3, extent: ViewportExtent) -> Result<[f32; 16]> {
    Ok(to_gpu(&clip_from_world_mat(camera, extent)?))
}

pub fn world_from_clip(camera: Camera3, extent: ViewportExtent) -> Result<[f32; 16]> {
    Ok(to_gpu(&world_from_clip_mat(camera, extent)?))
}

pub fn world_ray(
    camera: Camera3,
    extent: ViewportExtent,
    pixel: ViewportPoint,
) -> Result<(Point3, UnitVec3)> {
    let inverse = world_from_clip_mat(camera, extent)?;
    let near = unproject(&inverse, extent, pixel, 0.0);
    let far = unproject(&inverse, extent, pixel, 1.0);
    let direction = (far - near).try_normalize(RAY_TOLERANCE)?;
    Ok((near, direction))
}

pub fn world_on_focal_plane(
    camera: Camera3,
    extent: ViewportExtent,
    pixel: ViewportPoint,
) -> Result<Point3> {
    let (origin, direction) = world_ray(camera, extent, pixel)?;
    let normal = view_direction(camera)?;
    let denom = direction.dot(normal);
    if denom.abs() < FOCAL_DENOM_FLOOR {
        return Err(TypesError::ZeroLengthAxis);
    }
    let to_target = camera.target() - origin;
    let t = dot_vec_unit(to_target, normal) / denom;
    Ok(origin + direction.into_vec(Length::new::<millimeter>(t)))
}

pub fn pan_pixels(
    camera: Camera3,
    extent: ViewportExtent,
    from: ViewportPoint,
    to: ViewportPoint,
) -> Result<Camera3> {
    let grabbed = world_on_focal_plane(camera, extent, from)?;
    let released = world_on_focal_plane(camera, extent, to)?;
    translate(camera, grabbed - released)
}

pub fn zoom_about_pixel(
    camera: Camera3,
    extent: ViewportExtent,
    pixel: ViewportPoint,
    factor: ZoomFactor,
) -> Result<Camera3> {
    let pivot = world_on_focal_plane(camera, extent, pixel)?;
    let zoomed = apply_zoom(camera, factor)?;
    let drifted = world_on_focal_plane(zoomed, extent, pixel)?;
    translate(zoomed, pivot - drifted)
}

pub fn orbit_about_pixel(
    camera: Camera3,
    extent: ViewportExtent,
    pixel: ViewportPoint,
    rotation: AxisAngle,
) -> Result<Camera3> {
    let pivot = world_on_focal_plane(camera, extent, pixel)?;
    orbit_about_point(camera, pivot, rotation)
}

pub fn orbit_about_point(camera: Camera3, pivot: Point3, rotation: AxisAngle) -> Result<Camera3> {
    Camera3::new(
        camera.eye().rotated_about(pivot, rotation),
        camera.target().rotated_about(pivot, rotation),
        camera.up().rotated(rotation),
        camera.projection(),
    )
}

pub fn frame_isometric(aabb: bone_types::Aabb3, extent: ViewportExtent) -> Result<Camera3> {
    let center = aabb.center();
    let span = 0.5 * aabb.extent().norm_mm();
    let direction = UnitVec3::try_from_components(1.0, 1.0, 1.0, RAY_TOLERANCE)?;
    let eye = center + direction.into_vec(Length::new::<millimeter>(span * 3.0));
    let fit = (1.0 / aspect_ratio(extent)).max(1.0);
    let half_height = Length::new::<millimeter>(span * 1.2 * fit);
    Camera3::new(
        eye,
        center,
        UnitVec3::z_axis(),
        Projection::orthographic(half_height)?,
    )
}

fn clip_from_world_mat(camera: Camera3, extent: ViewportExtent) -> Result<Matrix4<f64>> {
    let (width, height) = (extent.width().value(), extent.height().value());
    if width == 0 || height == 0 {
        return Err(TypesError::ZeroViewportExtent { width, height });
    }
    Ok(projection_matrix(camera, extent) * view_matrix(camera))
}

fn world_from_clip_mat(camera: Camera3, extent: ViewportExtent) -> Result<Matrix4<f64>> {
    clip_from_world_mat(camera, extent)?
        .try_inverse()
        .ok_or(TypesError::NonInvertibleViewProjection)
}

fn view_matrix(camera: Camera3) -> Matrix4<f64> {
    let (ex, ey, ez) = camera.eye().coords_mm();
    let (tx, ty, tz) = camera.target().coords_mm();
    let (ux, uy, uz) = camera.up().components();
    Matrix4::look_at_rh(
        &NPoint3::new(ex, ey, ez),
        &NPoint3::new(tx, ty, tz),
        &NVec3::new(ux, uy, uz),
    )
}

fn projection_matrix(camera: Camera3, extent: ViewportExtent) -> Matrix4<f64> {
    let aspect = aspect_ratio(extent);
    match camera.projection().kind() {
        ProjectionKind::Perspective { fov, near, far } => perspective_wgpu(
            aspect,
            fov.get::<radian>(),
            near.get::<millimeter>(),
            far.get::<millimeter>(),
        ),
        ProjectionKind::Orthographic { half_height } => {
            let top = half_height.get::<millimeter>();
            let distance = (camera.target() - camera.eye()).norm_mm();
            let depth = ORTHO_DEPTH_FACTOR * top.max(distance);
            orthographic_wgpu(top * aspect, top, distance - depth, distance + depth)
        }
    }
}

fn perspective_wgpu(aspect: f64, fovy: f64, near: f64, far: f64) -> Matrix4<f64> {
    let focal = 1.0 / (fovy * 0.5).tan();
    let mut m = Matrix4::zeros();
    m[(0, 0)] = focal / aspect;
    m[(1, 1)] = focal;
    m[(2, 2)] = far / (near - far);
    m[(2, 3)] = near * far / (near - far);
    m[(3, 2)] = -1.0;
    m
}

fn orthographic_wgpu(right: f64, top: f64, near: f64, far: f64) -> Matrix4<f64> {
    let mut m = Matrix4::identity();
    m[(0, 0)] = 1.0 / right;
    m[(1, 1)] = 1.0 / top;
    m[(2, 2)] = 1.0 / (near - far);
    m[(2, 3)] = near / (near - far);
    m
}

fn unproject(
    inverse: &Matrix4<f64>,
    extent: ViewportExtent,
    pixel: ViewportPoint,
    ndc_z: f64,
) -> Point3 {
    let (ndc_x, ndc_y) = ndc_of(pixel, extent);
    let clip = NVec4::new(ndc_x, ndc_y, ndc_z, 1.0);
    let world = inverse * clip;
    Point3::from_mm(world.x / world.w, world.y / world.w, world.z / world.w)
}

fn ndc_of(pixel: ViewportPoint, extent: ViewportExtent) -> (f64, f64) {
    let width = f64::from(extent.width().value());
    let height = f64::from(extent.height().value());
    (
        2.0 * pixel.x() / width - 1.0,
        1.0 - 2.0 * pixel.y() / height,
    )
}

fn aspect_ratio(extent: ViewportExtent) -> f64 {
    f64::from(extent.width().value()) / f64::from(extent.height().value())
}

fn view_direction(camera: Camera3) -> Result<UnitVec3> {
    (camera.target() - camera.eye()).try_normalize(RAY_TOLERANCE)
}

fn translate(camera: Camera3, delta: Vec3) -> Result<Camera3> {
    Camera3::new(
        camera.eye() + delta,
        camera.target() + delta,
        camera.up(),
        camera.projection(),
    )
}

fn apply_zoom(camera: Camera3, factor: ZoomFactor) -> Result<Camera3> {
    match camera.projection().kind() {
        ProjectionKind::Orthographic { half_height } => {
            let scaled =
                Length::new::<millimeter>(half_height.get::<millimeter>() / factor.value());
            Camera3::new(
                camera.eye(),
                camera.target(),
                camera.up(),
                Projection::orthographic(scaled)?,
            )
        }
        ProjectionKind::Perspective { .. } => {
            let offset = camera.eye() - camera.target();
            Camera3::new(
                camera.target() + offset * (1.0 / factor.value()),
                camera.target(),
                camera.up(),
                camera.projection(),
            )
        }
    }
}

fn dot_vec_unit(a: Vec3, b: UnitVec3) -> f64 {
    let (ax, ay, az) = a.coords_mm();
    let (bx, by, bz) = b.components();
    ax * bx + ay * by + az * bz
}

fn to_gpu(matrix: &Matrix4<f64>) -> [f32; 16] {
    let slice = matrix.as_slice();
    core::array::from_fn(|index| crate::lower_f32(slice[index]))
}

#[cfg(test)]
mod tests {
    use super::*;
    use bone_types::{Aabb3, Angle};
    use uom::si::angle::degree;

    fn extent() -> ViewportExtent {
        ViewportExtent::square(crate::camera::ViewportPx::new(256))
    }

    fn ortho_camera() -> Camera3 {
        let Ok(projection) = Projection::orthographic(Length::new::<millimeter>(2.0)) else {
            panic!("half height is positive");
        };
        let Ok(camera) = Camera3::new(
            Point3::from_mm(4.0, 3.0, 5.0),
            Point3::origin(),
            UnitVec3::z_axis(),
            projection,
        ) else {
            panic!("camera is non-degenerate");
        };
        camera
    }

    fn perspective_camera() -> Camera3 {
        let Ok(projection) = Projection::perspective(
            Angle::new::<degree>(45.0),
            Length::new::<millimeter>(0.5),
            Length::new::<millimeter>(100.0),
        ) else {
            panic!("projection is valid");
        };
        let Ok(camera) = Camera3::new(
            Point3::from_mm(6.0, 4.0, 8.0),
            Point3::origin(),
            UnitVec3::z_axis(),
            projection,
        ) else {
            panic!("camera is non-degenerate");
        };
        camera
    }

    fn vp(x: f64, y: f64) -> ViewportPoint {
        let Ok(point) = ViewportPoint::new(x, y) else {
            panic!("pixel coordinates are finite");
        };
        point
    }

    fn zf(value: f64) -> ZoomFactor {
        let Ok(factor) = ZoomFactor::new(value) else {
            panic!("zoom factor is positive and finite");
        };
        factor
    }

    fn center() -> ViewportPoint {
        vp(128.0, 128.0)
    }

    fn focal(camera: Camera3, pixel: ViewportPoint) -> Point3 {
        let Ok(point) = world_on_focal_plane(camera, extent(), pixel) else {
            panic!("pixel has a focal point");
        };
        point
    }

    fn close(a: Point3, b: Point3, tol: f64) -> bool {
        let (ax, ay, az) = a.coords_mm();
        let (bx, by, bz) = b.coords_mm();
        (ax - bx).abs() < tol && (ay - by).abs() < tol && (az - bz).abs() < tol
    }

    fn mul(a: &Matrix4<f64>, b: &Matrix4<f64>) -> Matrix4<f64> {
        a * b
    }

    fn mul16(a: &[f32; 16], b: &[f32; 16]) -> [f32; 16] {
        core::array::from_fn(|index| {
            let (col, row) = (index / 4, index % 4);
            (0..4).map(|k| a[k * 4 + row] * b[col * 4 + k]).sum()
        })
    }

    #[test]
    fn ortho_lowering_is_column_major_affine() {
        let Ok(m) = clip_from_world(ortho_camera(), extent()) else {
            panic!("a nonzero extent lowers");
        };
        assert!(m[3].abs() < 1e-6);
        assert!(m[7].abs() < 1e-6);
        assert!(m[11].abs() < 1e-6);
        assert!((m[15] - 1.0).abs() < 1e-6);
    }

    #[test]
    fn perspective_lowering_has_projective_w_row() {
        let Ok(m) = clip_from_world(perspective_camera(), extent()) else {
            panic!("a nonzero extent lowers");
        };
        let affine = m[3].abs() < 1e-6
            && m[7].abs() < 1e-6
            && m[11].abs() < 1e-6
            && (m[15] - 1.0).abs() < 1e-6;
        assert!(
            !affine,
            "perspective w-row should be projective, not affine"
        );
    }

    #[test]
    fn ortho_inverse_round_trips_to_identity() {
        let camera = ortho_camera();
        let Ok(forward) = clip_from_world_mat(camera, extent()) else {
            panic!("a nonzero extent lowers");
        };
        let Ok(inverse) = world_from_clip_mat(camera, extent()) else {
            panic!("orthographic view-projection is invertible");
        };
        let product = mul(&forward, &inverse);
        (0..16).for_each(|index| {
            let (col, row) = (index / 4, index % 4);
            let want = f64::from(u8::from(col == row));
            assert!(
                (product.as_slice()[index] - want).abs() < 1e-9,
                "round trip broke at col={col} row={row}"
            );
        });
    }

    #[test]
    fn perspective_inverse_round_trips_to_identity() {
        let camera = perspective_camera();
        let Ok(forward) = clip_from_world_mat(camera, extent()) else {
            panic!("a nonzero extent lowers");
        };
        let Ok(inverse) = world_from_clip_mat(camera, extent()) else {
            panic!("perspective view-projection is invertible");
        };
        let product = mul(&forward, &inverse);
        (0..16).for_each(|index| {
            let (col, row) = (index / 4, index % 4);
            let want = f64::from(u8::from(col == row));
            assert!(
                (product.as_slice()[index] - want).abs() < 1e-9,
                "round trip broke at col={col} row={row}"
            );
        });
    }

    #[test]
    fn public_forward_and_inverse_round_trip() {
        [ortho_camera(), perspective_camera()]
            .into_iter()
            .for_each(|camera| {
                let Ok(forward) = clip_from_world(camera, extent()) else {
                    panic!("a nonzero extent lowers");
                };
                let Ok(inverse) = world_from_clip(camera, extent()) else {
                    panic!("view-projection is invertible");
                };
                let product = mul16(&forward, &inverse);
                (0..16).for_each(|index| {
                    let (col, row) = (index / 4, index % 4);
                    let want = f32::from(u8::from(col == row));
                    assert!(
                        (product[index] - want).abs() < 1e-3,
                        "public round trip broke at col={col} row={row}: {}",
                        product[index]
                    );
                });
            });
    }

    #[test]
    fn center_pixel_focuses_on_target() {
        [ortho_camera(), perspective_camera()]
            .into_iter()
            .for_each(|camera| {
                let focus = focal(camera, center());
                assert!(
                    close(focus, camera.target(), 1e-6),
                    "center pixel should focus on the target"
                );
            });
    }

    #[test]
    fn pan_keeps_grabbed_point_under_cursor() {
        [ortho_camera(), perspective_camera()]
            .into_iter()
            .for_each(|camera| {
                let from = vp(80.0, 96.0);
                let to = vp(170.0, 150.0);
                let grabbed = focal(camera, from);
                let Ok(panned) = pan_pixels(camera, extent(), from, to) else {
                    panic!("pan succeeds");
                };
                let landed = focal(panned, to);
                assert!(
                    close(grabbed, landed, 1e-6),
                    "grabbed world point should sit under the release cursor"
                );
            });
    }

    #[test]
    fn zoom_keeps_pivot_under_cursor() {
        [ortho_camera(), perspective_camera()]
            .into_iter()
            .for_each(|camera| {
                let pixel = vp(60.0, 200.0);
                let pivot = focal(camera, pixel);
                let Ok(zoomed) = zoom_about_pixel(camera, extent(), pixel, zf(1.6)) else {
                    panic!("zoom succeeds");
                };
                let after = focal(zoomed, pixel);
                assert!(
                    close(pivot, after, 1e-6),
                    "world point under the cursor should survive a zoom"
                );
            });
    }

    #[test]
    fn orbit_keeps_pivot_under_cursor() {
        [ortho_camera(), perspective_camera()]
            .into_iter()
            .for_each(|camera| {
                let pixel = vp(150.0, 110.0);
                let pivot = focal(camera, pixel);
                let rotation = AxisAngle::new(UnitVec3::z_axis(), Angle::new::<degree>(20.0));
                let Ok(orbited) = orbit_about_pixel(camera, extent(), pixel, rotation) else {
                    panic!("orbit succeeds");
                };
                let after = focal(orbited, pixel);
                assert!(
                    close(pivot, after, 1e-6),
                    "world point under the cursor should survive an orbit"
                );
            });
    }

    #[test]
    fn isometric_frames_unit_cube_centered() {
        let cube = Aabb3::from_corners(
            Point3::from_mm(0.0, 0.0, 0.0),
            Point3::from_mm(1.0, 1.0, 1.0),
        );
        let Ok(camera) = frame_isometric(cube, extent()) else {
            panic!("cube frames isometrically");
        };
        let focus = focal(camera, center());
        assert!(
            close(focus, cube.center(), 1e-6),
            "isometric framing should look at the cube center"
        );
    }

    #[test]
    fn extreme_camera_inverse_reports_error_not_identity() {
        let Ok(projection) = Projection::perspective(
            Angle::new::<degree>(45.0),
            Length::new::<millimeter>(1.0e-9),
            Length::new::<millimeter>(1.0e12),
        ) else {
            panic!("projection is valid");
        };
        let Ok(direction) = UnitVec3::try_from_components(1.0, 1.0, 1.0, RAY_TOLERANCE) else {
            panic!("diagonal is normalizable");
        };
        let eye = Point3::origin() + direction.into_vec(Length::new::<millimeter>(1.0e9));
        let Ok(camera) = Camera3::new(eye, Point3::origin(), UnitVec3::z_axis(), projection) else {
            panic!("camera is non-degenerate");
        };
        assert!(
            matches!(
                world_from_clip(camera, extent()),
                Err(TypesError::NonInvertibleViewProjection)
            ),
            "an inverse that underflows must surface an error, not a silent identity"
        );
        assert!(world_ray(camera, extent(), center()).is_err());
        assert!(world_on_focal_plane(camera, extent(), center()).is_err());
    }

    #[test]
    fn viewport_point_rejects_non_finite() {
        assert!(ViewportPoint::new(f64::NAN, 0.0).is_err());
        assert!(ViewportPoint::new(0.0, f64::INFINITY).is_err());
        assert!(ViewportPoint::new(10.0, 20.0).is_ok());
    }

    #[test]
    fn frame_isometric_rejects_degenerate_aabb() {
        let p = Point3::from_mm(1.0, 2.0, 3.0);
        let degenerate = Aabb3::from_corners(p, p);
        assert!(frame_isometric(degenerate, extent()).is_err());
    }

    #[test]
    fn orbit_preserves_unit_up() {
        let Ok(direction) = UnitVec3::try_from_components(0.3, 0.7, 0.64, RAY_TOLERANCE) else {
            panic!("axis is normalizable");
        };
        let step = AxisAngle::new(direction, Angle::new::<degree>(13.0));
        let final_camera = (0..4096).fold(ortho_camera(), |camera, _| {
            let Ok(next) = orbit_about_point(camera, Point3::origin(), step) else {
                panic!("orbit stays valid");
            };
            next
        });
        let (x, y, z) = final_camera.up().components();
        let norm = (x * x + y * y + z * z).sqrt();
        assert!(
            (norm - 1.0).abs() < 1e-12,
            "up vector should stay unit length across orbits: norm {norm}"
        );
    }

    #[test]
    fn lowering_rejects_zero_extent() {
        let zero = ViewportExtent::square(crate::camera::ViewportPx::new(0));
        assert!(matches!(
            clip_from_world(ortho_camera(), zero),
            Err(TypesError::ZeroViewportExtent {
                width: 0,
                height: 0
            })
        ));
        let no_height = ViewportExtent::new(
            crate::camera::ViewportPx::new(256),
            crate::camera::ViewportPx::new(0),
        );
        assert!(world_from_clip(perspective_camera(), no_height).is_err());
        assert!(world_ray(perspective_camera(), no_height, center()).is_err());
    }
}
