use core:: {
    fmt:: { self, Debug, Display },
    cmp:: { PartialEq, PartialOrd, Ordering },
    ops:: { Add, AddAssign, Sub, SubAssign, Mul, MulAssign, Div, DivAssign, Neg },
};
use crate:: {
    Dim, Dimension, DynDim,
    predefined::dim:: { self, DIMENSIONLESS },
    traits::Real,
};
use typenum:: { Integer };

/// Represents physical quantity
pub struct PhysicalQuantity<R, D> {
    pub value: R,
    pub dim: D,
}

impl<R, D> Clone for PhysicalQuantity<R, D>
where
    D: Dimension,
    R: Clone,
{
    fn clone(&self) -> Self {
        Self {
            value: self.value.clone(),
            .. *self
        }
    }
}

impl<R, D> Copy for PhysicalQuantity<R, D>
where
    D: Dimension,
    R: Copy,
{}

impl<R> From<R> for PhysicalQuantity<R, dim::Dimensionless> {
    fn from(value: R) -> Self {
        Self {
            value,
            dim: dim::Dimensionless::new(),
        }
    }
}

macro_rules! same_dim_binop_impl {
    ($trait:ident, $method:ident, $ref_method:ident) => {
        #[allow(non_camel_case_types)]
        impl<'a, R, L, M, T, θ, N, I, J> $trait<&'a Self> for PhysicalQuantity<R, Dim<L, M, T, θ, N, I, J>>
        where
            R: Real,
        {
            type Output = Self;

            fn $method(self, rhs: &'a Self) -> Self::Output {
                Self {
                    value: self.value.$ref_method(&rhs.value),
                    .. self
                }
            }
        }

        #[allow(non_camel_case_types)]
        impl<R, L, M, T, θ, N, I, J> $trait<Self> for PhysicalQuantity<R, Dim<L, M, T, θ, N, I, J>>
        where
            R: Real,
        {
            type Output = Self;

            fn $method(self, rhs: Self) -> Self::Output {
                $trait::$method(self, &rhs)
            }
        }

        #[allow(non_camel_case_types)]
        impl<'a, R, L, M, T, θ, N, I, J> $trait<&'a PhysicalQuantity<R, DynDim>>
            for PhysicalQuantity<R, Dim<L, M, T, θ, N, I, J>>
        where
            L: Integer, M: Integer, T: Integer, θ: Integer, N: Integer, I: Integer, J: Integer,
            R: Real,
        {
            type Output = Self;

            fn $method(self, rhs: &'a PhysicalQuantity<R, DynDim>) -> Self::Output {
                assert_eq!(self.dim, rhs.dim);
                Self {
                    value: self.value.$ref_method(&rhs.value),
                    .. self
                }
            }
        }

        #[allow(non_camel_case_types)]
        impl<R, L, M, T, θ, N, I, J> $trait<PhysicalQuantity<R, DynDim>>
            for PhysicalQuantity<R, Dim<L, M, T, θ, N, I, J>>
        where
            L: Integer, M: Integer, T: Integer, θ: Integer, N: Integer, I: Integer, J: Integer,
            R: Real,
        {
            type Output = Self;

            fn $method(self, rhs: PhysicalQuantity<R, DynDim>) -> Self::Output {
                $trait::$method(self, &rhs)
            }
        }

        #[allow(non_camel_case_types)]
        impl<'a, R, L, M, T, θ, N, I, J> $trait<&'a PhysicalQuantity<R, Dim<L, M, T, θ, N, I, J>>>
            for PhysicalQuantity<R, DynDim>
        where
            L: Integer, M: Integer, T: Integer, θ: Integer, N: Integer, I: Integer, J: Integer,
            R: Real,
        {
            type Output = PhysicalQuantity<R, Dim<L, M, T, θ, N, I, J>>;

            fn $method(self, rhs: &'a Self::Output) -> Self::Output {
                assert_eq!(self.dim, rhs.dim);
                PhysicalQuantity {
                    value: self.value.$ref_method(&rhs.value),
                    dim: Dim::<L, M, T, θ, N, I, J>::new(),
                }
            }
        }

        #[allow(non_camel_case_types)]
        impl<R, L, M, T, θ, N, I, J> $trait<PhysicalQuantity<R, Dim<L, M, T, θ, N, I, J>>>
            for PhysicalQuantity<R, DynDim>
        where
            L: Integer, M: Integer, T: Integer, θ: Integer, N: Integer, I: Integer, J: Integer,
            R: Real,
        {
            type Output = PhysicalQuantity<R, Dim<L, M, T, θ, N, I, J>>;

            fn $method(self, rhs: Self::Output) -> Self::Output {
                $trait::$method(self, &rhs)
            }
        }

        impl<'a, R> $trait<&'a Self> for PhysicalQuantity<R, DynDim>
        where
            R: Real,
        {
            type Output = Self;

            fn $method(self, rhs: &'a Self) -> Self::Output {
                assert_eq!(self.dim, rhs.dim);
                Self {
                    value: self.value.$ref_method(&rhs.value),
                    .. self
                }
            }
        }

        impl<R> $trait<Self> for PhysicalQuantity<R, DynDim>
        where
            R: Real,
        {
            type Output = Self;

            fn $method(self, rhs: Self) -> Self::Output {
                $trait::$method(self, &rhs)
            }
        }
    };
}

macro_rules! binop_impl {
    ($trait:ident, $method:ident, $ref_method:ident) => {
        impl<'a, D0, D1, R> $trait<&'a PhysicalQuantity<R, D1>> for PhysicalQuantity<R, D0>
        where
            D0: Dimension + $trait<D1>, <D0 as $trait<D1>>::Output: Dimension,
            D1: Dimension,
            R: Real,
        {
            type Output = PhysicalQuantity<R, <D0 as $trait<D1>>::Output>;

            fn $method(self, rhs: &'a PhysicalQuantity<R, D1>) -> Self::Output {
                PhysicalQuantity {
                    value: self.value.$ref_method(&rhs.value),
                    dim: self.dim.$method(rhs.dim),
                }
            }
        }

        impl<D0, D1, R> $trait<PhysicalQuantity<R, D1>> for PhysicalQuantity<R, D0>
        where
            D0: Dimension + $trait<D1>, <D0 as $trait<D1>>::Output: Dimension,
            D1: Dimension,
            R: Real,
        {
            type Output = PhysicalQuantity<R, <D0 as $trait<D1>>::Output>;

            fn $method(self, rhs: PhysicalQuantity<R, D1>) -> Self::Output {
                $trait::$method(self, &rhs)
            }
        }
    };
}

same_dim_binop_impl! { Add, add, add_ref }
same_dim_binop_impl! { Sub, sub, sub_ref }
binop_impl! { Mul, mul, mul_ref }
binop_impl! { Div, div, div_ref }

macro_rules! same_dim_assign_impl {
    ($trait:ident, $method:ident) => {
        #[allow(non_camel_case_types)]
        impl<R, L, M, T, θ, N, I, J> $trait<Self> for PhysicalQuantity<R, Dim<L, M, T, θ, N, I, J>>
        where
            R: Real,
        {
            fn $method(&mut self, rhs: Self) {
                self.value.$method(rhs.value);
            }
        }

        #[allow(non_camel_case_types)]
        impl<R, L, M, T, θ, N, I, J> $trait<PhysicalQuantity<R, DynDim>>
            for PhysicalQuantity<R, Dim<L, M, T, θ, N, I, J>>
        where
            L: Integer, M: Integer, T: Integer, θ: Integer, N: Integer, I: Integer, J: Integer,
            R: Real,
        {
            fn $method(&mut self, rhs: PhysicalQuantity<R, DynDim>) {
                assert_eq!(self.dim, rhs.dim);
                self.value.$method(rhs.value);
            }
        }

        impl<R, D> $trait<PhysicalQuantity<R, D>> for PhysicalQuantity<R, DynDim>
        where
            D: Dimension,
            R: Real,
        {
            fn $method(&mut self, rhs: PhysicalQuantity<R, D>) {
                assert_eq!(self.dim, rhs.dim);
                self.value.$method(rhs.value);
            }
        }
    };
}

macro_rules! dimension_less_assign_impl {
    ($trait:ident, $method:ident) => {
        #[allow(non_camel_case_types)]
        impl<R, L, M, T, θ, N, I, J> $trait<PhysicalQuantity<R, dim::Dimensionless>>
            for PhysicalQuantity<R, Dim<L, M, T, θ, N, I, J>>
        where
            R: Real,
        {
            fn $method(&mut self, rhs: PhysicalQuantity<R, dim::Dimensionless>) {
                self.value.$method(rhs.value);
            }
        }

        #[allow(non_camel_case_types)]
        impl<R, L, M, T, θ, N, I, J> $trait<PhysicalQuantity<R, DynDim>>
            for PhysicalQuantity<R, Dim<L, M, T, θ, N, I, J>>
        where
            R: Real,
        {
            fn $method(&mut self, rhs: PhysicalQuantity<R, DynDim>) {
                assert_eq!(rhs.dim, DIMENSIONLESS);
                self.value.$method(rhs.value);
            }
        }

        impl<R, D> $trait<PhysicalQuantity<R, D>> for PhysicalQuantity<R, DynDim>
        where
            D: Dimension,
            R: Real,
        {
            fn $method(&mut self, rhs: PhysicalQuantity<R, D>) {
                assert!( DIMENSIONLESS == self.dim || DIMENSIONLESS == rhs.dim );
                self.value.$method(rhs.value);
                self.dim.$method(rhs.dim);
            }
        }
    };
}

same_dim_assign_impl! { AddAssign, add_assign }
same_dim_assign_impl! { SubAssign, sub_assign }
dimension_less_assign_impl! { MulAssign, mul_assign }
dimension_less_assign_impl! { DivAssign, div_assign }

impl<R, D> Neg for PhysicalQuantity<R, D>
where
    D: Dimension,
    R: Real,
{
    type Output = Self;

    fn neg(self) -> Self::Output {
        Self {
            value: -self.value,
            .. self
        }
    }
}

impl<D0, D1, R> PartialEq<PhysicalQuantity<R, D1>> for PhysicalQuantity<R, D0>
where
    D0: Dimension + PartialEq<D1>, D1: Dimension,
    R: PartialEq,
{
    fn eq(&self, other: &PhysicalQuantity<R, D1>) -> bool {
        self.dim == other.dim &&
        self.value == other.value
    }
}

impl<D0, D1, R> PartialOrd<PhysicalQuantity<R, D1>> for PhysicalQuantity<R, D0>
where
    D0: Dimension + PartialEq<D1>, D1: Dimension,
    R: PartialOrd,
{
    fn partial_cmp(&self, other: &PhysicalQuantity<R, D1>) -> Option<Ordering> {
        match self.dim.dim_code().partial_cmp(&other.dim.dim_code()) {
            Some(Ordering::Equal) => self.value.partial_cmp(&other.value),
            order => order,
        }
    }
}

impl<R, D> Debug for PhysicalQuantity<R, D>
where
    D: Dimension, R: Real,
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
        f.debug_struct("PhysicalQuantity")
         .field("value", &self.value)
         .field("dim", &self.dim)
         .finish()
    }
}

impl<R, D> Display for PhysicalQuantity<R, D>
where
    D: Dimension, R: Real,
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
        Debug::fmt(self, f)
    }
}
