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::{
    Aabb3, Angle, AxisAngle, Camera3, Plane3, Point3, Projection, ProjectionKind, Result,
    StandardView, 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;
const ARCBALL_MIN_AXIS: f64 = 1.0e-9;
const FRAME_MARGIN: f64 = 1.2;
const SILHOUETTE_COLLAPSE_FRACTION: f64 = 1.0e-6;
const NEAR_PARALLEL_DOT: f64 = 0.999;

#[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 arcball_rotation(
    camera: Camera3,
    extent: ViewportExtent,
    from: ViewportPoint,
    to: ViewportPoint,
) -> Result<AxisAngle> {
    let grabbed = arcball_vector(camera, extent, from)?;
    let dragged = arcball_vector(camera, extent, to)?;
    let axis = grabbed.cross(&dragged);
    let axis_norm = axis.norm();
    if axis_norm < ARCBALL_MIN_AXIS {
        return Ok(AxisAngle::new(
            UnitVec3::z_axis(),
            Angle::new::<radian>(0.0),
        ));
    }
    let angle = grabbed.dot(&dragged).clamp(-1.0, 1.0).acos();
    let unit = axis / axis_norm;
    Ok(AxisAngle::new(
        UnitVec3::new_unchecked(unit.x, unit.y, unit.z),
        Angle::new::<radian>(angle),
    ))
}

pub fn orbit_yaw(camera: Camera3, angle: Angle) -> Result<Camera3> {
    orbit_about_point(camera, camera.target(), AxisAngle::new(camera.up(), angle))
}

pub fn orbit_pitch(camera: Camera3, angle: Angle) -> Result<Camera3> {
    let right = screen_right(camera)?;
    orbit_about_point(camera, camera.target(), AxisAngle::new(right, angle))
}

pub fn roll_by(camera: Camera3, angle: Angle) -> Result<Camera3> {
    let forward = view_direction(camera)?;
    Camera3::new(
        camera.eye(),
        camera.target(),
        camera.up().rotated(AxisAngle::new(forward, angle)),
        camera.projection(),
    )
}

pub fn roll_about_view(
    camera: Camera3,
    extent: ViewportExtent,
    from: ViewportPoint,
    to: ViewportPoint,
) -> Result<Camera3> {
    let forward = view_direction(camera)?;
    let cx = f64::from(extent.width().value()) * 0.5;
    let cy = f64::from(extent.height().value()) * 0.5;
    let (ax, ay) = (from.x() - cx, from.y() - cy);
    let (bx, by) = (to.x() - cx, to.y() - cy);
    let angle = (ax * by - ay * bx).atan2(ax * bx + ay * by);
    Camera3::new(
        camera.eye(),
        camera.target(),
        camera
            .up()
            .rotated(AxisAngle::new(forward, Angle::new::<radian>(angle))),
        camera.projection(),
    )
}

pub fn frame_current(camera: Camera3, aabb: Aabb3, extent: ViewportExtent) -> Result<Camera3> {
    let to_eye = (camera.eye() - camera.target()).try_normalize(RAY_TOLERANCE)?;
    frame_along(aabb, extent, to_eye, camera.up())
}

pub fn frame_isometric(aabb: Aabb3, extent: ViewportExtent) -> Result<Camera3> {
    let direction = UnitVec3::try_from_components(1.0, 1.0, 1.0, RAY_TOLERANCE)?;
    frame_along(aabb, extent, direction, UnitVec3::z_axis())
}

pub fn frame_standard_view(
    aabb: Aabb3,
    extent: ViewportExtent,
    view: StandardView,
    normal_to: Option<Plane3>,
) -> Result<Camera3> {
    let (from_center_to_eye, up) = match view {
        StandardView::Front => (UnitVec3::y_axis().reversed(), UnitVec3::z_axis()),
        StandardView::Back => (UnitVec3::y_axis(), UnitVec3::z_axis()),
        StandardView::Left => (UnitVec3::x_axis().reversed(), UnitVec3::z_axis()),
        StandardView::Right => (UnitVec3::x_axis(), UnitVec3::z_axis()),
        StandardView::Top => (UnitVec3::z_axis(), UnitVec3::y_axis()),
        StandardView::Bottom => (UnitVec3::z_axis().reversed(), UnitVec3::y_axis().reversed()),
        StandardView::Isometric => return frame_isometric(aabb, extent),
        StandardView::NormalTo => {
            let plane = normal_to.ok_or(TypesError::NormalToRequiresPlane)?;
            return frame_along(aabb, extent, plane.normal(), plane.y_axis());
        }
    };
    frame_along(aabb, extent, from_center_to_eye, up)
}

pub fn frame_view_direction(
    aabb: Aabb3,
    extent: ViewportExtent,
    from_center_to_eye: UnitVec3,
) -> Result<Camera3> {
    frame_along(
        aabb,
        extent,
        from_center_to_eye,
        stable_up(from_center_to_eye),
    )
}

fn stable_up(direction: UnitVec3) -> UnitVec3 {
    if direction.dot(UnitVec3::z_axis()).abs() > NEAR_PARALLEL_DOT {
        UnitVec3::y_axis()
    } else {
        UnitVec3::z_axis()
    }
}

fn frame_along(
    aabb: Aabb3,
    extent: ViewportExtent,
    from_center_to_eye: UnitVec3,
    up: UnitVec3,
) -> Result<Camera3> {
    let center = aabb.center();
    let radius = 0.5 * aabb.extent().norm_mm();
    let eye = center + from_center_to_eye.into_vec(Length::new::<millimeter>(radius * 3.0));
    let silhouette = silhouette_half_height(aabb, extent, from_center_to_eye, up);
    let half_height = if silhouette > radius * SILHOUETTE_COLLAPSE_FRACTION {
        silhouette
    } else {
        FRAME_MARGIN * radius
    };
    Camera3::new(
        eye,
        center,
        up,
        Projection::orthographic(Length::new::<millimeter>(half_height))?,
    )
}

fn silhouette_half_height(
    aabb: Aabb3,
    extent: ViewportExtent,
    view_axis: UnitVec3,
    up: UnitVec3,
) -> f64 {
    let (sx, sy, sz) = aabb.extent().coords_mm();
    let (hx, hy, hz) = (0.5 * sx, 0.5 * sy, 0.5 * sz);
    let (nx, ny, nz) = view_axis.components();
    let normal = NVec3::new(nx, ny, nz);
    let (ux, uy, uz) = up.components();
    let up_vec = NVec3::new(ux, uy, uz);
    let screen_up = (up_vec - normal * up_vec.dot(&normal)).normalize();
    let screen_right = normal.cross(&screen_up).normalize();
    let project = |axis: NVec3<f64>| hx * axis.x.abs() + hy * axis.y.abs() + hz * axis.z.abs();
    FRAME_MARGIN * project(screen_up).max(project(screen_right) / aspect_ratio(extent))
}

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 screen_right(camera: Camera3) -> Result<UnitVec3> {
    let (fx, fy, fz) = view_direction(camera)?.components();
    let (ux, uy, uz) = camera.up().components();
    let right = NVec3::new(fx, fy, fz).cross(&NVec3::new(ux, uy, uz));
    let norm = right.norm();
    if norm < ARCBALL_MIN_AXIS {
        return Err(TypesError::ZeroLengthAxis);
    }
    let unit = right / norm;
    Ok(UnitVec3::new_unchecked(unit.x, unit.y, unit.z))
}

fn arcball_vector(
    camera: Camera3,
    extent: ViewportExtent,
    pixel: ViewportPoint,
) -> Result<NVec3<f64>> {
    let forward = view_direction(camera)?;
    let (fx, fy, fz) = forward.components();
    let f = NVec3::new(fx, fy, fz);
    let (ux, uy, uz) = camera.up().components();
    let right = f.cross(&NVec3::new(ux, uy, uz)).normalize();
    let up = right.cross(&f);
    let (ndc_x, ndc_y) = ndc_of(pixel, extent);
    let sphere_x = ndc_x * aspect_ratio(extent);
    let radius_sq = sphere_x * sphere_x + ndc_y * ndc_y;
    let depth = if radius_sq <= 1.0 {
        (1.0 - radius_sq).sqrt()
    } else {
        0.0
    };
    let local = NVec3::new(sphere_x, ndc_y, depth).normalize();
    Ok(right * local.x + up * local.y - f * local.z)
}

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 arcball_rotation_is_identity_when_the_cursor_holds_still() {
        let pixel = vp(70.0, 90.0);
        let Ok(rotation) = arcball_rotation(ortho_camera(), extent(), pixel, pixel) else {
            panic!("a zero-length drag still yields a rotation");
        };
        assert!(
            rotation.angle().get::<radian>().abs() < 1e-12,
            "holding the cursor still produces no rotation"
        );
    }

    #[test]
    fn arcball_speed_is_isotropic_across_aspect() {
        let wide = ViewportExtent::new(
            crate::camera::ViewportPx::new(512),
            crate::camera::ViewportPx::new(256),
        );
        let (cx, cy) = (256.0, 128.0);
        let span = 60.0;
        let Ok(horizontal) = arcball_rotation(ortho_camera(), wide, vp(cx, cy), vp(cx + span, cy))
        else {
            panic!("a horizontal drag rotates");
        };
        let Ok(vertical) = arcball_rotation(ortho_camera(), wide, vp(cx, cy), vp(cx, cy + span))
        else {
            panic!("a vertical drag rotates");
        };
        let h = horizontal.angle().get::<radian>();
        let v = vertical.angle().get::<radian>();
        assert!(
            (h - v).abs() < 1e-9,
            "equal pixel drags must rotate equally on a non-square viewport: h={h} v={v}"
        );
    }

    #[test]
    fn antipodal_single_step_drag_collapses_to_no_rotation() {
        let Ok(rotation) =
            arcball_rotation(ortho_camera(), extent(), vp(256.0, 128.0), vp(0.0, 128.0))
        else {
            panic!("the arcball still yields a rotation at the sphere edge");
        };
        assert!(
            rotation.angle().get::<radian>().abs() < 1e-9,
            "a single step between antipodal sphere points is intentionally a no-op, not a pi flip"
        );
    }

    #[test]
    fn roll_sweep_from_right_to_below_rolls_up_toward_positive_x() {
        let Ok(projection) = Projection::orthographic(Length::new::<millimeter>(2.0)) else {
            panic!("half height is positive");
        };
        let Ok(front) = Camera3::new(
            Point3::from_mm(0.0, -10.0, 0.0),
            Point3::origin(),
            UnitVec3::z_axis(),
            projection,
        ) else {
            panic!("camera is non-degenerate");
        };
        let Ok(rolled) = roll_about_view(front, extent(), vp(200.0, 128.0), vp(128.0, 200.0))
        else {
            panic!("a roll drag transforms the camera");
        };
        let (ux, uy, uz) = rolled.up().components();
        assert!(
            (ux - 1.0).abs() < 1e-9 && uy.abs() < 1e-9 && uz.abs() < 1e-9,
            "a right-then-below screen sweep rolls up from +z to +x: ({ux}, {uy}, {uz})"
        );
    }

    #[test]
    fn orbit_yaw_holds_the_target_and_swings_the_eye() {
        let Ok(yawed) = orbit_yaw(ortho_camera(), Angle::new::<degree>(30.0)) else {
            panic!("a yaw rotates the camera");
        };
        assert!(
            close(yawed.target(), ortho_camera().target(), 1e-9),
            "a yaw pivots on the target"
        );
        assert_ne!(
            yawed.eye(),
            ortho_camera().eye(),
            "a yaw swings the eye about the up axis"
        );
    }

    #[test]
    fn orbit_pitch_holds_the_target_and_swings_the_eye() {
        let Ok(pitched) = orbit_pitch(ortho_camera(), Angle::new::<degree>(20.0)) else {
            panic!("a pitch rotates the camera");
        };
        assert!(
            close(pitched.target(), ortho_camera().target(), 1e-9),
            "a pitch pivots on the target"
        );
        assert_ne!(
            pitched.eye(),
            ortho_camera().eye(),
            "a pitch swings the eye about the screen-right axis"
        );
    }

    #[test]
    fn roll_by_keeps_eye_and_target_but_reorients_up() {
        let Ok(rolled) = roll_by(ortho_camera(), Angle::new::<degree>(25.0)) else {
            panic!("a roll rotates the camera");
        };
        assert!(
            close(rolled.eye(), ortho_camera().eye(), 1e-9)
                && close(rolled.target(), ortho_camera().target(), 1e-9),
            "a roll keeps the eye and target fixed"
        );
        assert_ne!(
            rolled.up(),
            ortho_camera().up(),
            "a roll reorients the up vector"
        );
    }

    #[test]
    fn frame_current_keeps_the_view_direction_and_centers_the_box() {
        let cube = Aabb3::from_corners(
            Point3::from_mm(0.0, 0.0, 0.0),
            Point3::from_mm(2.0, 2.0, 2.0),
        );
        let before = ortho_camera();
        let Ok(framed) = frame_current(before, cube, extent()) else {
            panic!("a non-degenerate box frames");
        };
        assert!(
            close(focal(framed, center()), cube.center(), 1e-6),
            "fit looks at the box center"
        );
        let (Ok(before_dir), Ok(after_dir)) = (view_direction(before), view_direction(framed))
        else {
            panic!("both cameras have a view direction");
        };
        let dot = before_dir.dot(after_dir);
        assert!(
            (dot - 1.0).abs() < 1e-6,
            "fit reframes without changing the view direction: {dot}"
        );
    }

    #[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());
    }

    fn box_aabb() -> Aabb3 {
        Aabb3::from_corners(
            Point3::from_mm(2.0, 4.0, 6.0),
            Point3::from_mm(4.0, 8.0, 12.0),
        )
    }

    fn standard(view: bone_types::StandardView) -> Camera3 {
        let Ok(camera) = frame_standard_view(box_aabb(), extent(), view, None) else {
            panic!("a fixed standard view frames a non-degenerate box");
        };
        camera
    }

    #[test]
    fn every_fixed_standard_view_looks_at_the_box_center() {
        use bone_types::StandardView::{Back, Bottom, Front, Isometric, Left, Right, Top};
        let box_center = box_aabb().center();
        [Front, Back, Left, Right, Top, Bottom, Isometric]
            .into_iter()
            .for_each(|view| {
                let focus = focal(standard(view), center());
                assert!(
                    close(focus, box_center, 1e-6),
                    "{view} must look at the box center",
                );
            });
    }

    #[test]
    fn standard_view_eyes_sit_on_the_named_side() {
        use bone_types::StandardView::{Back, Bottom, Front, Left, Right, Top};
        let (cx, cy, cz) = box_aabb().center().coords_mm();
        let offset = |view| {
            let (ex, ey, ez) = standard(view).eye().coords_mm();
            (ex - cx, ey - cy, ez - cz)
        };
        let (_, fy, _) = offset(Front);
        assert!(fy < 0.0, "front looks from -Y");
        let (_, by, _) = offset(Back);
        assert!(by > 0.0, "back looks from +Y");
        let (lx, _, _) = offset(Left);
        assert!(lx < 0.0, "left looks from -X");
        let (rx, _, _) = offset(Right);
        assert!(rx > 0.0, "right looks from +X");
        let (_, _, tz) = offset(Top);
        assert!(tz > 0.0, "top looks from +Z");
        let (_, _, bz) = offset(Bottom);
        assert!(bz < 0.0, "bottom looks from -Z");
    }

    fn direction(x: f64, y: f64, z: f64) -> UnitVec3 {
        let Ok(dir) = UnitVec3::try_from_components(x, y, z, RAY_TOLERANCE) else {
            panic!("the components are non-degenerate");
        };
        dir
    }

    #[test]
    fn frame_view_direction_looks_at_the_box_center_from_the_named_octant() {
        let box_center = box_aabb().center();
        let (cx, cy, cz) = box_center.coords_mm();
        let Ok(camera) = frame_view_direction(box_aabb(), extent(), direction(1.0, 1.0, 1.0))
        else {
            panic!("a corner direction frames a non-degenerate box");
        };
        assert!(close(focal(camera, center()), box_center, 1e-6));
        let (ex, ey, ez) = camera.eye().coords_mm();
        assert!(
            ex > cx && ey > cy && ez > cz,
            "the +++ corner seats the eye in the positive octant",
        );
    }

    #[test]
    fn frame_view_direction_handles_the_z_axis_where_up_would_collapse() {
        [direction(0.0, 0.0, 1.0), direction(0.0, 0.0, -1.0)]
            .into_iter()
            .for_each(|dir| {
                assert!(
                    frame_view_direction(box_aabb(), extent(), dir).is_ok(),
                    "framing along {dir} picks a stable up rather than collapsing",
                );
            });
    }

    #[test]
    fn normal_to_without_a_plane_is_rejected() {
        assert!(matches!(
            frame_standard_view(
                box_aabb(),
                extent(),
                bone_types::StandardView::NormalTo,
                None
            ),
            Err(TypesError::NormalToRequiresPlane)
        ));
    }

    #[test]
    fn normal_to_looks_along_the_plane_normal() {
        let Ok(plane) = bone_types::Plane3::new(
            Point3::origin(),
            UnitVec3::x_axis(),
            UnitVec3::z_axis(),
            RAY_TOLERANCE,
        ) else {
            panic!("x and z axes are orthonormal");
        };
        let Ok(camera) = frame_standard_view(
            box_aabb(),
            extent(),
            bone_types::StandardView::NormalTo,
            Some(plane),
        ) else {
            panic!("normal-to frames against the supplied plane");
        };
        let view = view_direction(camera);
        let Ok(view) = view else {
            panic!("the framed camera has a view direction");
        };
        let along = view.dot(plane.normal());
        assert!(
            (along.abs() - 1.0).abs() < 1e-6,
            "the view direction must be parallel to the plane normal: dot {along}",
        );
    }

    #[test]
    fn silhouette_collapse_falls_back_to_sphere_fit() {
        let segment = Aabb3::from_corners(
            Point3::from_mm(0.0, 0.0, 0.0),
            Point3::from_mm(0.0, 5.0, 0.0),
        );
        let Ok(camera) = frame_standard_view(segment, extent(), StandardView::Front, None) else {
            panic!("a segment parallel to the view axis still frames via the sphere fallback");
        };
        assert!(
            close(focal(camera, center()), segment.center(), 1e-6),
            "the fallback framing still looks at the segment center"
        );
        let ProjectionKind::Orthographic { half_height } = camera.projection().kind() else {
            panic!("standard views are orthographic");
        };
        let radius = 0.5 * segment.extent().norm_mm();
        assert!(
            (half_height.get::<millimeter>() - FRAME_MARGIN * radius).abs() < 1e-9,
            "a collapsed silhouette frames the bounding sphere, not a zero-height view"
        );
    }

    #[test]
    fn full_silhouette_views_keep_the_tight_fit() {
        let silhouette = silhouette_half_height(
            box_aabb(),
            extent(),
            UnitVec3::y_axis().reversed(),
            UnitVec3::z_axis(),
        );
        let radius = 0.5 * box_aabb().extent().norm_mm();
        assert!(
            silhouette > radius * SILHOUETTE_COLLAPSE_FRACTION,
            "a real view stays on the tight silhouette path, not the fallback"
        );
        let standard_front = standard(StandardView::Front);
        let ProjectionKind::Orthographic { half_height } = standard_front.projection().kind()
        else {
            panic!("standard views are orthographic");
        };
        assert!(
            (half_height.get::<millimeter>() - silhouette).abs() < 1e-9,
            "the front view frames on the silhouette, the clamp does not perturb it"
        );
    }

    #[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());
    }
}
