use super::{Diff, Quantity};
use nalgebra::{DefaultAllocator, Dim, OMatrix, OVector, U1, allocator::Allocator};
use num_dual::{
    Dual, Dual2, Dual2Vec, Dual3, DualNum, DualStruct, DualVec, Gradients, HyperDual, HyperDualVec,
    HyperHyperDual, Real,
};
use std::ops::Sub;

impl<D, F, T: DualStruct<D, F>, U> DualStruct<D, F> for Quantity<T, U> {
    type Real = Quantity<T::Real, U>;
    type Inner = Quantity<T::Inner, U>;

    fn re(&self) -> Self::Real {
        Quantity::new(self.0.re())
    }

    fn from_inner(inner: &Self::Inner) -> Self {
        Quantity::new(T::from_inner(&inner.0))
    }
}

pub fn zeroth_derivative<G, T: DualNum<f64>, UX, UY>(g: G, x: Quantity<T, UX>) -> Quantity<T, UY>
where
    G: Fn(Quantity<Real<T, f64>, UX>) -> Quantity<Real<T, f64>, UY>,
    UY: Sub<UX>,
{
    let r = num_dual::zeroth_derivative(|x| g(Quantity::new(x)).0, x.0);
    Quantity::new(r)
}

pub fn first_derivative<G, T: DualNum<f64>, UX, UY>(
    g: G,
    x: Quantity<T, UX>,
) -> (Quantity<T, UY>, Quantity<T, Diff<UY, UX>>)
where
    G: Fn(Quantity<Dual<T, f64>, UX>) -> Quantity<Dual<T, f64>, UY>,
    UY: Sub<UX>,
{
    let r = num_dual::first_derivative(|x| g(Quantity::new(x)).0, x.0);
    (Quantity::new(r.0), Quantity::new(r.1))
}

#[expect(clippy::type_complexity)]
pub fn gradient<G, T: DualNum<f64>, UX, UY, N: Dim>(
    g: G,
    x: &Quantity<OVector<T, N>, UX>,
) -> (Quantity<T, UY>, Quantity<OVector<T, N>, Diff<UY, UX>>)
where
    DefaultAllocator: Allocator<N>,
    G: Fn(Quantity<OVector<DualVec<T, f64, N>, N>, UX>) -> Quantity<DualVec<T, f64, N>, UY>,
    UY: Sub<UX>,
{
    let r = num_dual::gradient(|x| g(Quantity::new(x)).0, &x.0);
    (Quantity::new(r.0), Quantity::new(r.1))
}

#[expect(clippy::type_complexity)]
pub fn gradient_copy<G, T: DualNum<f64> + Copy, UX, UY, N: Gradients>(
    g: G,
    x: &Quantity<OVector<T, N>, UX>,
) -> (Quantity<T, UY>, Quantity<OVector<T, N>, Diff<UY, UX>>)
where
    DefaultAllocator: Allocator<N>,
    G: Fn(Quantity<OVector<N::Dual<T, f64>, N>, UX>) -> Quantity<N::Dual<T, f64>, UY>,
    UY: Sub<UX>,
{
    let r = N::gradient(|x, _: &()| g(Quantity::new(x)).0, &x.0, &());
    (Quantity::new(r.0), Quantity::new(r.1))
}

#[expect(clippy::type_complexity)]
pub fn jacobian<G, T: DualNum<f64>, UX, UY, M: Dim, N: Dim>(
    g: G,
    x: &Quantity<OVector<T, N>, UX>,
) -> (
    Quantity<OVector<T, M>, UY>,
    Quantity<OMatrix<T, M, N>, Diff<UY, UX>>,
)
where
    DefaultAllocator: Allocator<M> + Allocator<N> + Allocator<M, N> + Allocator<U1, N>,
    G: Fn(
        Quantity<OVector<DualVec<T, f64, N>, N>, UX>,
    ) -> Quantity<OVector<DualVec<T, f64, N>, M>, UY>,
    UY: Sub<UX>,
{
    let r = num_dual::jacobian(|x| g(Quantity::new(x)).0, &x.0);
    (Quantity::new(r.0), Quantity::new(r.1))
}

#[expect(clippy::type_complexity)]
pub fn jacobian_copy<G, T: DualNum<f64> + Copy, UX, UY, N: Gradients>(
    g: G,
    x: &Quantity<OVector<T, N>, UX>,
) -> (
    Quantity<OVector<T, N>, UY>,
    Quantity<OMatrix<T, N, N>, Diff<UY, UX>>,
)
where
    DefaultAllocator: Allocator<N> + Allocator<N> + Allocator<N, N> + Allocator<U1, N>,
    G: Fn(Quantity<OVector<N::Dual<T, f64>, N>, UX>) -> Quantity<OVector<N::Dual<T, f64>, N>, UY>,
    UY: Sub<UX>,
{
    let r = N::jacobian(|x, _: &()| g(Quantity::new(x)).0, &x.0, &());
    (Quantity::new(r.0), Quantity::new(r.1))
}

#[expect(clippy::type_complexity)]
pub fn second_derivative<G, T: DualNum<f64>, UX, UY>(
    g: G,
    x: Quantity<T, UX>,
) -> (
    Quantity<T, UY>,
    Quantity<T, Diff<UY, UX>>,
    Quantity<T, Diff<Diff<UY, UX>, UX>>,
)
where
    G: Fn(Quantity<Dual2<T, f64>, UX>) -> Quantity<Dual2<T, f64>, UY>,
    UY: Sub<UX>,
    Diff<UY, UX>: Sub<UX>,
{
    let r = num_dual::second_derivative(|x| g(Quantity::new(x)).0, x.0);
    (Quantity::new(r.0), Quantity::new(r.1), Quantity::new(r.2))
}

#[expect(clippy::type_complexity)]
pub fn hessian<G, T: DualNum<f64>, UX, UY, N: Dim>(
    g: G,
    x: &Quantity<OVector<T, N>, UX>,
) -> (
    Quantity<T, UY>,
    Quantity<OVector<T, N>, Diff<UY, UX>>,
    Quantity<OMatrix<T, N, N>, Diff<Diff<UY, UX>, UX>>,
)
where
    DefaultAllocator: Allocator<N> + Allocator<U1, N> + Allocator<N, N>,
    G: Fn(Quantity<OVector<Dual2Vec<T, f64, N>, N>, UX>) -> Quantity<Dual2Vec<T, f64, N>, UY>,
    UY: Sub<UX>,
    Diff<UY, UX>: Sub<UX>,
{
    let r = num_dual::hessian(|x| g(Quantity::new(x)).0, &x.0);
    (Quantity::new(r.0), Quantity::new(r.1), Quantity::new(r.2))
}

#[expect(clippy::type_complexity)]
pub fn hessian_copy<G, T: DualNum<f64> + Copy, UX, UY, N: Gradients>(
    g: G,
    x: &Quantity<OVector<T, N>, UX>,
) -> (
    Quantity<T, UY>,
    Quantity<OVector<T, N>, Diff<UY, UX>>,
    Quantity<OMatrix<T, N, N>, Diff<Diff<UY, UX>, UX>>,
)
where
    DefaultAllocator: Allocator<N> + Allocator<N, N>,
    G: Fn(Quantity<OVector<N::Dual2<T, f64>, N>, UX>) -> Quantity<N::Dual2<T, f64>, UY>,
    UY: Sub<UX>,
    Diff<UY, UX>: Sub<UX>,
{
    let r = N::hessian(|x, _: &()| g(Quantity::new(x)).0, &x.0, &());
    (Quantity::new(r.0), Quantity::new(r.1), Quantity::new(r.2))
}

#[expect(clippy::type_complexity)]
pub fn second_partial_derivative<G, T: DualNum<f64>, UX, UY, UZ>(
    g: G,
    (x, y): (Quantity<T, UX>, Quantity<T, UY>),
) -> (
    Quantity<T, UZ>,
    Quantity<T, Diff<UZ, UX>>,
    Quantity<T, Diff<UZ, UY>>,
    Quantity<T, Diff<Diff<UZ, UX>, UY>>,
)
where
    G: Fn(
        (
            Quantity<HyperDual<T, f64>, UX>,
            Quantity<HyperDual<T, f64>, UY>,
        ),
    ) -> Quantity<HyperDual<T, f64>, UZ>,
    UZ: Sub<UX>,
    UZ: Sub<UY>,
    Diff<UZ, UX>: Sub<UY>,
{
    let r = num_dual::second_partial_derivative(
        |(x, y)| g((Quantity::new(x), Quantity::new(y))).0,
        (x.0, y.0),
    );
    (
        Quantity::new(r.0),
        Quantity::new(r.1),
        Quantity::new(r.2),
        Quantity::new(r.3),
    )
}

#[expect(clippy::type_complexity)]
pub fn partial_hessian<G, T: DualNum<f64>, UX, UY, UZ, M: Dim, N: Dim>(
    g: G,
    (x, y): (&Quantity<OVector<T, M>, UX>, &Quantity<OVector<T, N>, UY>),
) -> (
    Quantity<T, UZ>,
    Quantity<OVector<T, M>, Diff<UZ, UX>>,
    Quantity<OVector<T, N>, Diff<UZ, UY>>,
    Quantity<OMatrix<T, M, N>, Diff<Diff<UZ, UX>, UY>>,
)
where
    G: Fn(
        (
            Quantity<OVector<HyperDualVec<T, f64, M, N>, M>, UX>,
            Quantity<OVector<HyperDualVec<T, f64, M, N>, N>, UY>,
        ),
    ) -> Quantity<HyperDualVec<T, f64, M, N>, UZ>,
    DefaultAllocator: Allocator<N> + Allocator<M> + Allocator<M, N> + Allocator<U1, N>,
    UZ: Sub<UX>,
    UZ: Sub<UY>,
    Diff<UZ, UX>: Sub<UY>,
{
    let r = num_dual::partial_hessian(
        |(x, y)| g((Quantity::new(x), Quantity::new(y))).0,
        (&x.0, &y.0),
    );
    (
        Quantity::new(r.0),
        Quantity::new(r.1),
        Quantity::new(r.2),
        Quantity::new(r.3),
    )
}

#[expect(clippy::type_complexity)]
pub fn partial_hessian_copy<G, T: DualNum<f64> + Copy, UX, UY, UZ, N: Gradients>(
    g: G,
    (x, y): (&Quantity<OVector<T, N>, UX>, Quantity<T, UY>),
) -> (
    Quantity<T, UZ>,
    Quantity<OVector<T, N>, Diff<UZ, UX>>,
    Quantity<T, Diff<UZ, UY>>,
    Quantity<OVector<T, N>, Diff<Diff<UZ, UX>, UY>>,
)
where
    G: Fn(
        (
            Quantity<OVector<N::HyperDual<T, f64>, N>, UX>,
            Quantity<N::HyperDual<T, f64>, UY>,
        ),
    ) -> Quantity<N::HyperDual<T, f64>, UZ>,
    DefaultAllocator: Allocator<N>,
    UZ: Sub<UX>,
    UZ: Sub<UY>,
    Diff<UZ, UX>: Sub<UY>,
{
    let r = N::partial_hessian(
        |x, y, _: &()| g((Quantity::new(x), Quantity::new(y))).0,
        &x.0,
        y.0,
        &(),
    );
    (
        Quantity::new(r.0),
        Quantity::new(r.1),
        Quantity::new(r.2),
        Quantity::new(r.3),
    )
}

#[expect(clippy::type_complexity)]
pub fn third_derivative<G, T: DualNum<f64>, UX, UY>(
    g: G,
    x: Quantity<T, UX>,
) -> (
    Quantity<T, UY>,
    Quantity<T, Diff<UY, UX>>,
    Quantity<T, Diff<Diff<UY, UX>, UX>>,
    Quantity<T, Diff<Diff<Diff<UY, UX>, UX>, UX>>,
)
where
    G: Fn(Quantity<Dual3<T, f64>, UX>) -> Quantity<Dual3<T, f64>, UY>,
    UY: Sub<UX>,
    Diff<UY, UX>: Sub<UX>,
    Diff<Diff<UY, UX>, UX>: Sub<UX>,
{
    let r = num_dual::third_derivative(|x| g(Quantity::new(x)).0, x.0);
    (
        Quantity::new(r.0),
        Quantity::new(r.1),
        Quantity::new(r.2),
        Quantity::new(r.3),
    )
}

#[expect(clippy::type_complexity)]
pub fn third_partial_derivative<G, T: DualNum<f64>, UX, UY, UZ, U>(
    g: G,
    (x, y, z): (Quantity<T, UX>, Quantity<T, UY>, Quantity<T, UZ>),
) -> (
    Quantity<T, U>,
    Quantity<T, Diff<U, UX>>,
    Quantity<T, Diff<U, UY>>,
    Quantity<T, Diff<U, UZ>>,
    Quantity<T, Diff<Diff<U, UX>, UY>>,
    Quantity<T, Diff<Diff<U, UX>, UY>>,
    Quantity<T, Diff<Diff<U, UX>, UY>>,
    Quantity<T, Diff<Diff<Diff<U, UX>, UY>, UZ>>,
)
where
    G: Fn(
        (
            Quantity<HyperHyperDual<T, f64>, UX>,
            Quantity<HyperHyperDual<T, f64>, UY>,
            Quantity<HyperHyperDual<T, f64>, UZ>,
        ),
    ) -> Quantity<HyperHyperDual<T, f64>, UY>,
    U: Sub<UX>,
    U: Sub<UY>,
    U: Sub<UZ>,
    Diff<U, UX>: Sub<UY>,
    Diff<U, UY>: Sub<UX>,
    Diff<U, UX>: Sub<UX>,
    Diff<Diff<U, UX>, UY>: Sub<UZ>,
{
    let r = num_dual::third_partial_derivative(
        |(x, y, z)| g((Quantity::new(x), Quantity::new(y), Quantity::new(z))).0,
        (x.0, y.0, z.0),
    );
    (
        Quantity::new(r.0),
        Quantity::new(r.1),
        Quantity::new(r.2),
        Quantity::new(r.3),
        Quantity::new(r.4),
        Quantity::new(r.5),
        Quantity::new(r.6),
        Quantity::new(r.7),
    )
}

#[cfg(test)]
mod test_num_dual {
    use super::*;
    use crate::{Area, Length, METER, Volume};
    use approx::assert_relative_eq;
    use nalgebra::{SMatrix, SVector, vector};
    use num_dual::{Dual64, ImplicitDerivative, ImplicitFunction};

    struct AreaImplicit;
    impl ImplicitFunction<f64> for AreaImplicit {
        type Parameters<D> = Area<D>;
        type Variable<D> = D;
        fn residual<D: DualNum<f64> + Copy>(x: D, &area: &Area<D>) -> D {
            let l = Length::new(x);
            (area - l * l).convert_into(area)
        }
    }

    #[test]
    fn test_implicit() {
        let a = Area::new(Dual64::from(25.0).derivative());
        let implicit = ImplicitDerivative::new(AreaImplicit, a);
        let x = implicit.implicit_derivative(5.0);
        println!("{x}");
        assert_eq!(x, a.0.sqrt())
    }

    fn volume<D: DualNum<f64> + Copy>(x: Length<D>) -> Volume<D> {
        x * x * x
    }

    fn distance<D: DualNum<f64>, N: Dim>(x: Length<OVector<D, N>>) -> Length<D>
    where
        DefaultAllocator: Allocator<N>,
    {
        x.dot(&x).sqrt()
    }

    fn volume2<D: DualNum<f64> + Copy>((x, h): (Length<D>, Length<D>)) -> Volume<D> {
        x * x * h
    }

    #[test]
    fn test_derivative() {
        let (v, dv) = first_derivative(volume, 5.0 * METER);
        println!("{v}\t{dv:3}");
        assert_eq!(v, 125.0 * METER.powi::<3>());
        assert_eq!(dv, 75.0 * METER.powi::<2>());

        let (v, dv, d2v) = second_derivative(volume, 5.0 * METER);
        println!("{v}\t{dv:3}\t\t{d2v}");
        assert_eq!(v, 125.0 * METER.powi::<3>(),);
        assert_eq!(dv, 75.0 * METER.powi::<2>(),);
        assert_eq!(d2v, 30.0 * METER);

        let (v, dv_dx, dv_dh, d2v) =
            second_partial_derivative(volume2, (5.0 * METER, 20.0 * METER));
        println!("{v}\t{dv_dx:3}\t{dv_dh:3}\t{d2v}");
        assert_eq!(v, 500.0 * METER.powi::<3>(),);
        assert_eq!(dv_dx, 200.0 * METER.powi::<2>(),);
        assert_eq!(dv_dh, 25.0 * METER.powi::<2>(),);
        assert_eq!(d2v, 10.0 * METER);

        let (v, dv, d2v, d3v) = third_derivative(volume, 5.0 * METER);
        println!("{v}\t{dv:3}\t\t{d2v}\t{d3v}");
        assert_eq!(v, 125.0 * METER.powi::<3>(),);
        assert_eq!(dv, 75.0 * METER.powi::<2>(),);
        assert_eq!(d2v, 30.0 * METER);
        assert_eq!(d3v.into_value(), 6.0);
    }

    #[test]
    fn test_gradient_and_hessian() {
        let x = vector![1.0, 5.0, 5.0, 7.0];
        let (d, grad) = gradient(distance, &Length::new(x));
        println!("{d}\t{grad:.1}");
        assert_eq!(d, 10.0 * METER);
        assert_relative_eq!(grad.into_value(), SVector::from([0.1, 0.5, 0.5, 0.7]));

        let x = vector![1.0, 5.0, 5.0, 7.0];
        let (d, grad, hess) = hessian(distance, &Length::new(x));
        println!("{d}\t{grad:.1}\t{hess:.3?}");
        assert_eq!(d, 10.0 * METER);
        assert_relative_eq!(grad.into_value(), SVector::from([0.1, 0.5, 0.5, 0.7]));
        assert_relative_eq!(
            (hess * METER).into_value(),
            SMatrix::from([
                [0.099, -0.005, -0.005, -0.007],
                [-0.005, 0.075, -0.025, -0.035],
                [-0.005, -0.025, 0.075, -0.035],
                [-0.007, -0.035, -0.035, 0.051]
            ])
        );
    }
}
