#[macro_use]
mod support;

/// Helper to get the 'canonical' version of a `Quat`. We define the canonical of quat `q` as:
/// * `q`, if q.w > epsilon
/// * `-q`, if q.w < -epsilon
/// * `(0, 0, 0, 1)` otherwise
/// The rationale is that q and -q represent the same rotation, and any (_, _, _, 0) respresent no rotation at all.
trait CanonicalQuat: Copy {
    fn canonical(self) -> Self;
}

macro_rules! impl_canonical_quat {
    ($t:ty) => {
        impl CanonicalQuat for $t {
            fn canonical(self) -> Self {
                match self {
                    _ if self.w >= 1e-5 => self,
                    _ if self.w <= -1e-5 => -self,
                    _ => <$t>::from_xyzw(0.0, 0.0, 0.0, 1.0),
                }
            }
        }
    };
}

impl_canonical_quat!(glam::Quat);
impl_canonical_quat!(glam::DQuat);

/// Helper to set some alternative epsilons based on the floating point type used
trait EulerEpsilon {
    /// epsilon for comparing quaterions built from eulers and axis-angles
    const Q_EPS: f32;

    /// epsilon for comparing quaternion round-tripped through eulers (quat -> euler -> quat)
    const E_EPS: f32;
}
impl EulerEpsilon for f32 {
    const Q_EPS: f32 = 1e-5;

    // The scalar-math and wasm paths seems to use a particularly bad implementation of the trig functions
    #[cfg(any(feature = "scalar-math", target_arch = "wasm32"))]
    const E_EPS: f32 = 2e-4;

    #[cfg(not(any(feature = "scalar-math", target_arch = "wasm32")))]
    const E_EPS: f32 = 1e-5;
}
impl EulerEpsilon for f64 {
    const Q_EPS: f32 = 1e-8;
    const E_EPS: f32 = 1e-8;
}

macro_rules! impl_3axis_test {
    ($name:ident, $t:ty, $quat:ident, $euler:path, $U:path, $V:path, $W:path, $vec:ident) => {
        glam_test!($name, {
            let euler = $euler;
            assert!($U != $W); // First and last axis must be different for three axis
            for u in (-180..=180).step_by(15) {
                for v in (-180..=180).step_by(15) {
                    for w in (-180..=180).step_by(15) {
                        let u1 = (u as $t).to_radians();
                        let v1 = (v as $t).to_radians();
                        let w1 = (w as $t).to_radians();

                        let q1: $quat = ($quat::from_axis_angle($U, u1)
                            * $quat::from_axis_angle($V, v1)
                            * $quat::from_axis_angle($W, w1))
                        .normalize();

                        // Test if the rotation is the expected
                        let q2: $quat = $quat::from_euler(euler, u1, v1, w1).normalize();
                        assert_approx_eq!(q1.canonical(), q2.canonical(), <$t>::Q_EPS);

                        // Test quat reconstruction from angles
                        let (u2, v2, w2) = q2.to_euler(euler);
                        let q3 = $quat::from_euler(euler, u2, v2, w2).normalize();
                        assert_approx_eq!(
                            q2.canonical(),
                            q3.canonical(),
                            <$t>::E_EPS,
                            format!(
                                "angles {:?} -> {:?}",
                                (u, v, w),
                                (u2.to_degrees(), v2.to_degrees(), w2.to_degrees())
                            )
                        );
                    }
                }
            }
        });
    };
}

macro_rules! impl_all_quat_tests_three_axis {
    ($t:ty, $q:ident, $v:ident) => {
        impl_3axis_test!(test_euler_zyx, $t, $q, ER::ZYX, $v::Z, $v::Y, $v::X, $v);
        impl_3axis_test!(test_euler_zxy, $t, $q, ER::ZXY, $v::Z, $v::X, $v::Y, $v);
        impl_3axis_test!(test_euler_yxz, $t, $q, ER::YXZ, $v::Y, $v::X, $v::Z, $v);
        impl_3axis_test!(test_euler_yzx, $t, $q, ER::YZX, $v::Y, $v::Z, $v::X, $v);
        impl_3axis_test!(test_euler_xyz, $t, $q, ER::XYZ, $v::X, $v::Y, $v::Z, $v);
        impl_3axis_test!(test_euler_xzy, $t, $q, ER::XZY, $v::X, $v::Z, $v::Y, $v);
    };
}

mod euler {
    use super::{CanonicalQuat, EulerEpsilon};
    use glam::*;
    type ER = EulerRot;

    mod quat {
        use super::*;

        impl_all_quat_tests_three_axis!(f32, Quat, Vec3);
    }

    mod dquat {
        use super::*;

        impl_all_quat_tests_three_axis!(f64, DQuat, DVec3);
    }
}
