use core:: {
    fmt:: { self, Debug, Display },
    marker::PhantomData,
    ops:: { Add, Sub, Mul, MulAssign, Div, DivAssign },
};
use typenum:: { Diff, Integer, Sum };

pub trait Dimension
    : Copy
    + Debug
    + Display
    + Default
{
    fn length(&self) -> i8;
    fn mass(&self) -> i8;
    fn time(&self) -> i8;
    fn temperature(&self) -> i8;
    fn amount(&self) -> i8;
    fn current(&self) -> i8;
    fn luminous(&self) -> i8;

    fn dim_code(&self) -> u64 {
        self.length() as u8 as u64
        | (self.mass() as u8 as u64) << 8
        | (self.time() as u8 as u64) << 16
        | (self.temperature() as u8 as u64) << 24
        | (self.amount() as u8 as u64) << 32
        | (self.current() as u8 as u64) << 40
        | (self.luminous() as u8 as u64) << 48
    }
}

fn fmt_impl<D: Dimension>(dim: &D, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    if dim.dim_code() == 0 {
        f.write_str("\u{2014}")
    } else {
        if dim.length() != 0 { f.write_fmt(format_args!("m{}", dim.length()))?; }
        if dim.mass() != 0 { f.write_fmt(format_args!("g{}", dim.mass()))?; }
        if dim.time() != 0 { f.write_fmt(format_args!("s{}", dim.time()))?; }
        if dim.temperature() != 0 { f.write_fmt(format_args!("K{}", dim.temperature()))?; }
        if dim.amount() != 0 { f.write_fmt(format_args!("mol{}", dim.amount()))?; }
        if dim.current() != 0 { f.write_fmt(format_args!("A{}", dim.current()))?; }
        if dim.luminous() != 0 { f.write_fmt(format_args!("cd{}", dim.luminous()))?; }
        Ok(())
    }
}

pub fn is_equal<D0, D1>(lhs: &D0, rhs: &D1) -> bool
where
    D0: Dimension, D1: Dimension
{
    lhs.length() == rhs.length() &&
    lhs.mass() == rhs.mass() &&
    lhs.time() == rhs.time() &&
    lhs.temperature() == rhs.temperature() &&
    lhs.amount() == rhs.amount() &&
    lhs.current() == rhs.current() &&
    lhs.luminous() == rhs.luminous()
}

/// Dynamic (runtime) Dimension
#[derive(Clone, Copy, Debug, Default)]
pub struct DynDim(pub i8, pub i8, pub i8, pub i8, pub i8, pub i8, pub i8);

impl DynDim {
    pub const fn new(dim_code: u64) -> Self {
        Self(
            dim_code as i8,
            (dim_code >> 8) as i8,
            (dim_code >> 16) as i8,
            (dim_code >> 24) as i8,
            (dim_code >> 32) as i8,
            (dim_code >> 40) as i8,
            (dim_code >> 48) as i8
        )
    }

    pub const fn prod(self, rhs: Self) -> Self {
        Self(
            self.0 + rhs.0,
            self.1 + rhs.1,
            self.2 + rhs.2,
            self.3 + rhs.3,
            self.4 + rhs.4,
            self.5 + rhs.5,
            self.6 + rhs.6
        )
    }

    pub const fn quot(self, rhs: Self) -> Self {
        Self(
            self.0 - rhs.0,
            self.1 - rhs.1,
            self.2 - rhs.2,
            self.3 - rhs.3,
            self.4 - rhs.4,
            self.5 - rhs.5,
            self.6 - rhs.6
        )
    }

    pub const fn powi(self, n: i8) -> Self {
        Self(
            self.0 * n,
            self.1 * n,
            self.2 * n,
            self.3 * n,
            self.4 * n,
            self.5 * n,
            self.6 * n
        )
    }

    pub const fn is_equal(&self, rhs: &Self) -> bool {
        self.0 == rhs.0 && self.1 == rhs.1 && self.2 == rhs.2 && self.3 == rhs.3 &&
        self.4 == rhs.4 && self.5 == rhs.5 && self.6 == rhs.6
    }
}

impl Dimension for DynDim {
    fn length(&self) -> i8 { self.0 }
    fn mass(&self) -> i8 { self.1 }
    fn time(&self) -> i8 { self.2 }
    fn temperature(&self) -> i8 { self.3 }
    fn amount(&self) -> i8 { self.4 }
    fn current(&self) -> i8 { self.5 }
    fn luminous(&self) -> i8 { self.6 }
}

impl From<u64> for DynDim {
    fn from(code: u64) -> Self {
        Self::new(code)
    }
}

impl Display for DynDim {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt_impl(self, f)
    }
}

impl<D0: Dimension> PartialEq<D0> for DynDim {
    fn eq(&self, other: &D0) -> bool {
        is_equal(self, other)
    }
}

#[allow(non_camel_case_types)]
impl<L, M, T, θ, N, I, J> From<Dim<L, M, T, θ, N, I, J>> for DynDim
where
    L: Integer, M: Integer, T: Integer, θ: Integer, N: Integer, I: Integer, J: Integer,
{
    fn from(_: Dim<L, M, T, θ, N, I, J>) -> Self {
        Self(L::I8, M::I8, T::I8, θ::I8, N::I8, I::I8, J::I8)
    }
}

/// Static (compile time) Dimension.
#[allow(non_camel_case_types)]
#[derive(Clone, Copy, Default)]
pub struct Dim<L, M, T, θ, N, I, J>(
    PhantomData<L>, PhantomData<M>, PhantomData<T>, PhantomData<θ>,
    PhantomData<N>, PhantomData<I>, PhantomData<J>)
;

#[allow(non_camel_case_types)]
impl<L, M, T, θ, N, I, J> Dim<L, M, T, θ, N, I, J> {
    pub const fn new() -> Self {
        Self(PhantomData, PhantomData, PhantomData, PhantomData, PhantomData, PhantomData, PhantomData)
    }
}

#[allow(non_camel_case_types)]
impl<L, M, T, θ, N, I, J> Dim<L, M, T, θ, N, I, J>
where
    L: Integer, M: Integer, T: Integer, θ: Integer, N: Integer, I: Integer, J: Integer,
{
    pub const CODE: u64 =
        L::I8 as u8 as u64
        | (M::I8 as u8 as u64) << 8
        | (T::I8 as u8 as u64) << 16
        | (θ::I8 as u8 as u64) << 24
        | (N::I8 as u8 as u64) << 32
        | (I::I8 as u8 as u64) << 40
        | (J::I8 as u8 as u64) << 48
    ;
}

#[allow(non_camel_case_types)]
impl<L, M, T, θ, N, I, J> Dimension for Dim<L, M, T, θ, N, I, J>
where
    L: Integer, M: Integer, T: Integer, θ: Integer, N: Integer, I: Integer, J: Integer,
{
    fn length(&self) -> i8 { L::I8 }
    fn mass(&self) -> i8 { M::I8 }
    fn time(&self) -> i8 { T::I8 }
    fn temperature(&self) -> i8 { θ::I8 }
    fn amount(&self) -> i8 { N::I8 }
    fn current(&self) -> i8 { I::I8 }
    fn luminous(&self) -> i8 { J::I8 }
    fn dim_code(&self) -> u64 { Self::CODE }
}

#[allow(non_camel_case_types)]
impl<L, M, T, θ, N, I, J> Debug for Dim<L, M, T, θ, N, I, J>
where
    L: Integer, M: Integer, T: Integer, θ: Integer, N: Integer, I: Integer, J: Integer,
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Dim")
         .field("L", &self.length())
         .field("M", &self.mass())
         .field("T", &self.time())
         .field("θ", &self.temperature())
         .field("N", &self.amount())
         .field("I", &self.current())
         .field("J", &self.luminous())
         .finish()
    }
}

#[allow(non_camel_case_types)]
impl<L, M, T, θ, N, I, J> Display for Dim<L, M, T, θ, N, I, J>
where
    L: Integer, M: Integer, T: Integer, θ: Integer, N: Integer, I: Integer, J: Integer,
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt_impl(self, f)
    }
}

#[allow(non_camel_case_types)]
impl<L, M, T, θ, N, I, J, D> PartialEq<D> for Dim<L, M, T, θ, N, I, J>
where
    L: Integer, M: Integer, T: Integer, θ: Integer, N: Integer, I: Integer, J: Integer,
    D: Dimension,
{
    fn eq(&self, other: &D) -> bool {
        is_equal(self, other)
    }
}

macro_rules! binop_impl {
    ($trait:ident, $method:ident, $dim_trait:ident, $dim_op:ident, $typenum_trait:ident) => {
        impl<D: Dimension> $trait<D> for DynDim {
            type Output = Self;

            fn $method(self, rhs: D) -> Self::Output {
                Self(
                    self.length().$dim_op(rhs.length()),
                    self.mass().$dim_op(rhs.mass()),
                    self.time().$dim_op(rhs.time()),
                    self.temperature().$dim_op(rhs.temperature()),
                    self.amount().$dim_op(rhs.amount()),
                    self.current().$dim_op(rhs.current()),
                    self.luminous().$dim_op(rhs.luminous()),
                )
            }
        }

        #[allow(non_camel_case_types)]
        impl<L0, M0, T0, θ0, N0, I0, J0, L1, M1, T1, θ1, N1, I1, J1>
            $trait<Dim<L1, M1, T1, θ1, N1, I1, J1>> for Dim<L0, M0, T0, θ0, N0, I0, J0>
        where
            L0: Integer + $dim_trait<L1>, L1: Integer, $typenum_trait<L0, L1>: Integer,
            M0: Integer + $dim_trait<M1>, M1: Integer, $typenum_trait<M0, M1>: Integer,
            T0: Integer + $dim_trait<T1>, T1: Integer, $typenum_trait<T0, T1>: Integer,
            θ0: Integer + $dim_trait<θ1>, θ1: Integer, $typenum_trait<θ0, θ1>: Integer,
            N0: Integer + $dim_trait<N1>, N1: Integer, $typenum_trait<N0, N1>: Integer,
            I0: Integer + $dim_trait<I1>, I1: Integer, $typenum_trait<I0, I1>: Integer,
            J0: Integer + $dim_trait<J1>, J1: Integer, $typenum_trait<J0, J1>: Integer,
        {
            type Output = Dim<
                $typenum_trait<L0, L1>,
                $typenum_trait<M0, M1>,
                $typenum_trait<T0, T1>,
                $typenum_trait<θ0, θ1>,
                $typenum_trait<N0, N1>,
                $typenum_trait<I0, I1>,
                $typenum_trait<J0, J1>,
            >;

            fn $method(self, _: Dim<L1, M1, T1, θ1, N1, I1, J1>) -> Self::Output {
                Default::default()
            }
        }

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

            fn $method(self, rhs: DynDim) -> Self::Output {
                DynDim(
                    self.length().$dim_op(rhs.length()),
                    self.mass().$dim_op(rhs.mass()),
                    self.time().$dim_op(rhs.time()),
                    self.temperature().$dim_op(rhs.temperature()),
                    self.amount().$dim_op(rhs.amount()),
                    self.current().$dim_op(rhs.current()),
                    self.luminous().$dim_op(rhs.luminous()),
                )
            }
        }
    };
}

binop_impl! { Mul, mul, Add, add, Sum }
binop_impl! { Div, div, Sub, sub, Diff }

macro_rules! assignop_impl {
    ($trait:ident, $method:ident) => {
        impl<D: Dimension> $trait<D> for DynDim {
            fn $method(&mut self, rhs: D) {
                self.0.$method(rhs.length());
                self.1.$method(rhs.mass());
                self.2.$method(rhs.time());
                self.3.$method(rhs.temperature());
                self.4.$method(rhs.amount());
                self.5.$method(rhs.current());
                self.5.$method(rhs.luminous());
            }
        }
    };
}

assignop_impl! { MulAssign, mul_assign }
assignop_impl! { DivAssign, div_assign }