use bone_types::{Length, Vec2};
use uom::si::length::millimeter;

#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ViewportPx(u32);

impl ViewportPx {
    #[must_use]
    pub const fn new(value: u32) -> Self {
        Self(value)
    }

    #[must_use]
    pub const fn value(self) -> u32 {
        self.0
    }
}

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

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct ViewportExtent {
    width: ViewportPx,
    height: ViewportPx,
}

impl ViewportExtent {
    #[must_use]
    pub const fn new(width: ViewportPx, height: ViewportPx) -> Self {
        Self { width, height }
    }

    #[must_use]
    pub const fn square(side: ViewportPx) -> Self {
        Self {
            width: side,
            height: side,
        }
    }

    #[must_use]
    pub const fn width(self) -> ViewportPx {
        self.width
    }

    #[must_use]
    pub const fn height(self) -> ViewportPx {
        self.height
    }

    #[must_use]
    pub fn pixel_count(self) -> u64 {
        u64::from(self.width.value()) * u64::from(self.height.value())
    }
}

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

#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
pub struct PixelsPerMm(f64);

impl PixelsPerMm {
    pub const DEFAULT: Self = Self(10.0);

    #[must_use]
    pub fn new(value: f64) -> Self {
        assert!(
            value.is_finite() && value > 0.0,
            "PixelsPerMm requires a positive, finite value: got {value}",
        );
        Self(value)
    }

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

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

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Camera2 {
    extent: ViewportExtent,
    pan_mm: Vec2,
    zoom: PixelsPerMm,
}

impl Camera2 {
    #[must_use]
    pub fn new(extent: ViewportExtent) -> Self {
        Self {
            extent,
            pan_mm: Vec2::zero(),
            zoom: PixelsPerMm::DEFAULT,
        }
    }

    #[must_use]
    pub const fn extent(self) -> ViewportExtent {
        self.extent
    }

    #[must_use]
    pub const fn pan_mm(self) -> Vec2 {
        self.pan_mm
    }

    #[must_use]
    pub const fn zoom(self) -> PixelsPerMm {
        self.zoom
    }

    #[must_use]
    pub fn with_extent(self, extent: ViewportExtent) -> Self {
        Self { extent, ..self }
    }

    #[must_use]
    pub fn with_pan(self, pan_mm: Vec2) -> Self {
        Self { pan_mm, ..self }
    }

    #[must_use]
    pub fn with_zoom(self, zoom: PixelsPerMm) -> Self {
        Self { zoom, ..self }
    }

    #[must_use]
    pub fn world_mm_per_pixel(self) -> f64 {
        1.0 / self.zoom.value()
    }

    #[must_use]
    #[allow(
        clippy::cast_possible_truncation,
        reason = "mm coordinates fit f32 mantissa at CAD scales"
    )]
    pub fn clip_from_world_mm(self) -> [f32; 16] {
        let (sx, sy) = self.scale();
        let (px, py) = self.pan_mm.coords_mm();
        let tx = -px * sx;
        let ty = -py * sy;
        column_major(sx as f32, sy as f32, tx as f32, ty as f32)
    }

    #[must_use]
    #[allow(
        clippy::cast_possible_truncation,
        reason = "mm coordinates fit f32 mantissa at CAD scales"
    )]
    pub fn world_mm_from_clip(self) -> [f32; 16] {
        let (sx, sy) = self.scale();
        let inv_x = 1.0 / sx;
        let inv_y = 1.0 / sy;
        let (px, py) = self.pan_mm.coords_mm();
        column_major(inv_x as f32, inv_y as f32, px as f32, py as f32)
    }

    fn scale(self) -> (f64, f64) {
        let w = f64::from(self.extent.width.value());
        let h = f64::from(self.extent.height.value());
        let z = self.zoom.value();
        (2.0 * z / w, 2.0 * z / h)
    }
}

fn column_major(sx: f32, sy: f32, tx: f32, ty: f32) -> [f32; 16] {
    [
        sx, 0.0, 0.0, 0.0, 0.0, sy, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, tx, ty, 0.0, 1.0,
    ]
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct GridSpacing {
    minor: Length,
    major_every: u32,
}

impl GridSpacing {
    #[must_use]
    pub fn minor(self) -> Length {
        self.minor
    }

    #[must_use]
    pub const fn major_every(self) -> u32 {
        self.major_every
    }

    #[must_use]
    pub fn major(self) -> Length {
        self.minor * f64::from(self.major_every)
    }

    #[must_use]
    pub fn from_zoom(zoom: PixelsPerMm, target_minor_px: f32) -> Self {
        assert!(
            target_minor_px.is_finite() && target_minor_px > 0.0,
            "GridSpacing::from_zoom requires a positive, finite target: got {target_minor_px}",
        );
        let desired_mm = f64::from(target_minor_px) / zoom.value();
        let k = desired_mm.log10().floor();
        let frac = desired_mm / 10f64.powf(k);
        let (multiplier, major_every, k_bump) = if frac < f64::sqrt(2.0) {
            (1.0, 10, 0.0)
        } else if frac < f64::sqrt(10.0) {
            (2.0, 5, 0.0)
        } else if frac < f64::sqrt(50.0) {
            (5.0, 2, 0.0)
        } else {
            (1.0, 10, 1.0)
        };
        let minor_mm = multiplier * 10f64.powf(k + k_bump);
        Self {
            minor: Length::new::<millimeter>(minor_mm),
            major_every,
        }
    }
}

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

    fn extent(w: u32, h: u32) -> ViewportExtent {
        ViewportExtent::new(ViewportPx::new(w), ViewportPx::new(h))
    }

    fn approx_eq(a: f32, b: f32) -> bool {
        (a - b).abs() < 1e-6
    }

    #[test]
    fn default_camera_maps_origin_to_clip_center() {
        let cam = Camera2::new(extent(200, 100));
        let m = cam.clip_from_world_mm();
        assert!(approx_eq(m[12], 0.0));
        assert!(approx_eq(m[13], 0.0));
    }

    #[test]
    fn clip_from_world_is_column_major() {
        let cam = Camera2::new(extent(100, 100)).with_pan(Vec2::from_mm(2.0, 3.0));
        let m = cam.clip_from_world_mm();
        assert!(approx_eq(m[3], 0.0));
        assert!(approx_eq(m[7], 0.0));
        assert!(approx_eq(m[11], 0.0));
        assert!(approx_eq(m[15], 1.0));
    }

    #[test]
    fn inverse_round_trips_origin() {
        let cam = Camera2::new(extent(256, 128))
            .with_pan(Vec2::from_mm(-5.0, 4.0))
            .with_zoom(PixelsPerMm::new(8.0));
        let fwd = cam.clip_from_world_mm();
        let inv = cam.world_mm_from_clip();
        let rt = mul4(&inv, &fwd);
        (0..16).for_each(|idx| {
            let (col, row) = (idx / 4, idx % 4);
            let want = if col == row { 1.0 } else { 0.0 };
            assert!(
                approx_eq(rt[idx], want),
                "round trip broke at col={col},row={row}: {}",
                rt[idx]
            );
        });
    }

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

    #[test]
    fn grid_spacing_selects_one_two_five_in_decade() {
        [1.0_f64, 10.0, 100.0]
            .into_iter()
            .map(PixelsPerMm::new)
            .map(|z| GridSpacing::from_zoom(z, 16.0))
            .for_each(|s| {
                let minor_mm = s.minor().get::<millimeter>();
                let k = minor_mm.log10().floor();
                let decade = 10f64.powf(k);
                let mult = minor_mm / decade;
                let snapped = [1.0, 2.0, 5.0]
                    .into_iter()
                    .any(|m: f64| (mult - m).abs() < 1e-9);
                assert!(snapped, "minor {minor_mm} mm not in {{1, 2, 5}} x decade");
                assert!([2, 5, 10].contains(&s.major_every()));
            });
    }

    #[test]
    fn grid_spacing_rolls_over_decade_past_sqrt_fifty() {
        let zoom = PixelsPerMm::new(2.0);
        let s = GridSpacing::from_zoom(zoom, 16.0);
        let minor_mm = s.minor().get::<millimeter>();
        assert!(
            (minor_mm - 10.0).abs() < 1e-9,
            "expected rollover to 10 mm, got {minor_mm}",
        );
        assert_eq!(s.major_every(), 10);
    }

    #[test]
    fn clip_x_of_pan_point_is_zero() {
        let cam = Camera2::new(extent(320, 240))
            .with_pan(Vec2::from_mm(7.0, -3.0))
            .with_zoom(PixelsPerMm::new(5.0));
        let m = cam.clip_from_world_mm();
        let world = [7.0_f32, -3.0_f32, 0.0_f32, 1.0_f32];
        let clip = apply(&m, world);
        assert!(approx_eq(clip[0], 0.0));
        assert!(approx_eq(clip[1], 0.0));
    }

    fn apply(m: &[f32; 16], v: [f32; 4]) -> [f32; 4] {
        core::array::from_fn(|row| (0..4).map(|col| m[col * 4 + row] * v[col]).sum())
    }
}
