use nalgebra::{Quaternion, Unit, UnitQuaternion, Vector3 as NVec3};
use serde::{Deserialize, Serialize};
use uom::si::angle::{degree, radian};
use uom::si::f64::{Angle, Length};
use uom::si::length::millimeter;

use crate::dimensioned_serde;
use crate::{AngleTolerance, AxisAngle, Point3, Result, Tolerance, TypesError, UnitVec3};

const CAMERA_MIN_SEPARATION: Tolerance = Tolerance::new(1e-9);
const CAMERA_MIN_UP_VIEW_ANGLE: AngleTolerance = AngleTolerance::from_radians(1e-9);

fn point_is_finite(p: Point3) -> bool {
    let (x, y, z) = p.coords_mm();
    x.is_finite() && y.is_finite() && z.is_finite()
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub enum ProjectionKind {
    Orthographic {
        half_height: Length,
    },
    Perspective {
        fov: Angle,
        near: Length,
        far: Length,
    },
}

#[derive(Copy, Clone, PartialEq, Serialize, Deserialize)]
#[serde(try_from = "ProjectionWire", into = "ProjectionWire")]
pub struct Projection(ProjectionKind);

#[derive(Serialize, Deserialize)]
#[serde(rename = "Projection", deny_unknown_fields)]
enum ProjectionWire {
    Orthographic {
        #[serde(with = "dimensioned_serde::length_si")]
        half_height: Length,
    },
    Perspective {
        #[serde(with = "dimensioned_serde::angle_si")]
        fov: Angle,
        #[serde(with = "dimensioned_serde::length_si")]
        near: Length,
        #[serde(with = "dimensioned_serde::length_si")]
        far: Length,
    },
}

impl From<Projection> for ProjectionWire {
    fn from(p: Projection) -> Self {
        match p.0 {
            ProjectionKind::Orthographic { half_height } => Self::Orthographic { half_height },
            ProjectionKind::Perspective { fov, near, far } => Self::Perspective { fov, near, far },
        }
    }
}

impl TryFrom<ProjectionWire> for Projection {
    type Error = TypesError;
    fn try_from(w: ProjectionWire) -> Result<Self> {
        match w {
            ProjectionWire::Orthographic { half_height } => Self::orthographic(half_height),
            ProjectionWire::Perspective { fov, near, far } => Self::perspective(fov, near, far),
        }
    }
}

impl Projection {
    pub fn orthographic(half_height: Length) -> Result<Self> {
        let kind = ProjectionKind::Orthographic { half_height };
        kind.validate()?;
        Ok(Self(kind))
    }

    pub fn perspective(fov: Angle, near: Length, far: Length) -> Result<Self> {
        let kind = ProjectionKind::Perspective { fov, near, far };
        kind.validate()?;
        Ok(Self(kind))
    }

    #[must_use]
    pub fn kind(self) -> ProjectionKind {
        self.0
    }
}

impl ProjectionKind {
    fn validate(self) -> Result<()> {
        match self {
            Self::Orthographic { half_height } => {
                let h = half_height.get::<millimeter>();
                if !h.is_finite() || h <= 0.0 {
                    return Err(TypesError::NonPositiveOrthoHeight(h));
                }
            }
            Self::Perspective { fov, near, far } => {
                let f = fov.get::<radian>();
                if !f.is_finite() || f <= 0.0 || f >= core::f64::consts::PI {
                    return Err(TypesError::FovOutOfRange(f));
                }
                let n = near.get::<millimeter>();
                if !n.is_finite() || n <= 0.0 {
                    return Err(TypesError::NonPositiveNear(n));
                }
                let fa = far.get::<millimeter>();
                if !fa.is_finite() || fa <= n {
                    return Err(TypesError::FarNotBeyondNear { near: n, far: fa });
                }
            }
        }
        Ok(())
    }
}

impl core::fmt::Debug for Projection {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self.0 {
            ProjectionKind::Orthographic { half_height } => write!(
                f,
                "Orthographic {{ half_height: {} mm }}",
                half_height.get::<millimeter>()
            ),
            ProjectionKind::Perspective { fov, near, far } => write!(
                f,
                "Perspective {{ fov: {} deg, near: {} mm, far: {} mm }}",
                fov.get::<degree>(),
                near.get::<millimeter>(),
                far.get::<millimeter>()
            ),
        }
    }
}

impl core::fmt::Display for Projection {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self.0 {
            ProjectionKind::Orthographic { half_height } => {
                write!(
                    f,
                    "ortho{{ half_height={} mm }}",
                    half_height.get::<millimeter>()
                )
            }
            ProjectionKind::Perspective { fov, near, far } => write!(
                f,
                "persp{{ fov={} deg, near={} mm, far={} mm }}",
                fov.get::<degree>(),
                near.get::<millimeter>(),
                far.get::<millimeter>()
            ),
        }
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(try_from = "Camera3Wire", into = "Camera3Wire")]
pub struct Camera3 {
    eye: Point3,
    target: Point3,
    up: UnitVec3,
    projection: Projection,
}

#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename = "Camera3", deny_unknown_fields)]
struct Camera3Wire {
    eye: Point3,
    target: Point3,
    up: UnitVec3,
    projection: Projection,
}

impl From<Camera3> for Camera3Wire {
    fn from(c: Camera3) -> Self {
        Self {
            eye: c.eye,
            target: c.target,
            up: c.up,
            projection: c.projection,
        }
    }
}

impl TryFrom<Camera3Wire> for Camera3 {
    type Error = TypesError;
    fn try_from(w: Camera3Wire) -> Result<Self> {
        Self::new(w.eye, w.target, w.up, w.projection)
    }
}

impl Camera3 {
    pub fn new(eye: Point3, target: Point3, up: UnitVec3, projection: Projection) -> Result<Self> {
        let offset = target - eye;
        let separation = offset.norm_mm();
        if !separation.is_finite() {
            return Err(TypesError::CameraNotFinite);
        }
        if separation <= CAMERA_MIN_SEPARATION.value() {
            return Err(TypesError::DegenerateCamera(separation));
        }
        let (ox, oy, oz) = offset.coords_mm();
        let view = UnitVec3::new_unchecked(ox / separation, oy / separation, oz / separation);
        let (ux, uy, uz) = up.components();
        let (vx, vy, vz) = view.components();
        let up_cross = NVec3::new(uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx).norm();
        if up_cross <= CAMERA_MIN_UP_VIEW_ANGLE.radians() {
            return Err(TypesError::CameraUpParallelToView);
        }
        Ok(Self {
            eye,
            target,
            up,
            projection,
        })
    }

    #[must_use]
    pub fn eye(self) -> Point3 {
        self.eye
    }

    #[must_use]
    pub fn target(self) -> Point3 {
        self.target
    }

    #[must_use]
    pub fn up(self) -> UnitVec3 {
        self.up
    }

    #[must_use]
    pub fn projection(self) -> Projection {
        self.projection
    }
}

impl core::fmt::Display for Camera3 {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(
            f,
            "camera{{ eye={}, target={}, up={}, {} }}",
            self.eye, self.target, self.up, self.projection
        )
    }
}

#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Serialize, Deserialize)]
#[serde(try_from = "f64", into = "f64")]
pub struct ZoomFactor(f64);

impl ZoomFactor {
    pub const IDENTITY: Self = Self(1.0);

    pub fn new(value: f64) -> Result<Self> {
        if !value.is_finite() || value <= 0.0 {
            return Err(TypesError::NonPositiveZoom(value));
        }
        Ok(Self(value))
    }

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

impl TryFrom<f64> for ZoomFactor {
    type Error = TypesError;
    fn try_from(value: f64) -> Result<Self> {
        Self::new(value)
    }
}

impl From<ZoomFactor> for f64 {
    fn from(zoom: ZoomFactor) -> Self {
        zoom.0
    }
}

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

fn axis_angle_to_quat(aa: AxisAngle) -> UnitQuaternion<f64> {
    let (x, y, z) = aa.axis().components();
    let axis = Unit::new_unchecked(NVec3::new(x, y, z));
    UnitQuaternion::from_axis_angle(&axis, aa.angle().get::<radian>())
}

fn quat_to_axis_angle(q: UnitQuaternion<f64>) -> AxisAngle {
    match q.axis_angle() {
        Some((axis, angle)) => AxisAngle::new(
            UnitVec3::new_unchecked(axis.x, axis.y, axis.z),
            Angle::new::<radian>(angle),
        ),
        None => AxisAngle::new(UnitVec3::z_axis(), Angle::new::<radian>(0.0)),
    }
}

#[derive(Copy, Clone, PartialEq, Serialize, Deserialize)]
#[serde(try_from = "OrbitStateWire", into = "OrbitStateWire")]
pub struct OrbitState {
    rotation: UnitQuaternion<f64>,
    zoom: ZoomFactor,
    pan_target: Point3,
}

#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename = "OrbitState", deny_unknown_fields)]
struct OrbitStateWire {
    qw: f64,
    qx: f64,
    qy: f64,
    qz: f64,
    zoom: ZoomFactor,
    pan_target: Point3,
}

impl From<OrbitState> for OrbitStateWire {
    fn from(o: OrbitState) -> Self {
        let q = o.rotation.quaternion();
        Self {
            qw: q.w,
            qx: q.i,
            qy: q.j,
            qz: q.k,
            zoom: o.zoom,
            pan_target: o.pan_target,
        }
    }
}

impl TryFrom<OrbitStateWire> for OrbitState {
    type Error = TypesError;
    fn try_from(w: OrbitStateWire) -> Result<Self> {
        let rotation = Unit::try_new(Quaternion::new(w.qw, w.qx, w.qy, w.qz), f64::EPSILON)
            .ok_or(TypesError::ZeroLengthAxis)?;
        if !point_is_finite(w.pan_target) {
            return Err(TypesError::OrbitPanTargetNotFinite);
        }
        Ok(Self {
            rotation,
            zoom: w.zoom,
            pan_target: w.pan_target,
        })
    }
}

impl OrbitState {
    #[must_use]
    pub fn identity() -> Self {
        Self {
            rotation: UnitQuaternion::identity(),
            zoom: ZoomFactor::IDENTITY,
            pan_target: Point3::origin(),
        }
    }

    pub fn new(rotation: AxisAngle, zoom: ZoomFactor, pan_target: Point3) -> Result<Self> {
        if !point_is_finite(pan_target) {
            return Err(TypesError::OrbitPanTargetNotFinite);
        }
        Ok(Self {
            rotation: axis_angle_to_quat(rotation),
            zoom,
            pan_target,
        })
    }

    #[must_use]
    pub fn rotation(self) -> AxisAngle {
        quat_to_axis_angle(self.rotation)
    }

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

    #[must_use]
    pub fn pan_target(self) -> Point3 {
        self.pan_target
    }

    #[must_use]
    pub fn rotated(self, delta: AxisAngle) -> Self {
        let combined = axis_angle_to_quat(delta) * self.rotation;
        Self {
            rotation: UnitQuaternion::new_normalize(combined.into_inner()),
            zoom: self.zoom,
            pan_target: self.pan_target,
        }
    }
}

impl core::fmt::Debug for OrbitState {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(
            f,
            "OrbitState {{ rotation: {:?}, zoom: {}, pan_target: {:?} }}",
            self.rotation(),
            self.zoom,
            self.pan_target
        )
    }
}

impl core::fmt::Display for OrbitState {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(
            f,
            "orbit{{ rot={}, {}, target={} }}",
            self.rotation(),
            self.zoom,
            self.pan_target
        )
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum StandardView {
    Front,
    Back,
    Left,
    Right,
    Top,
    Bottom,
    Isometric,
    NormalTo,
}

impl StandardView {
    #[must_use]
    pub const fn label(self) -> &'static str {
        match self {
            Self::Front => "front",
            Self::Back => "back",
            Self::Left => "left",
            Self::Right => "right",
            Self::Top => "top",
            Self::Bottom => "bottom",
            Self::Isometric => "isometric",
            Self::NormalTo => "normal_to",
        }
    }
}

impl core::fmt::Display for StandardView {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.write_str(self.label())
    }
}
