use crate:: {
    Dimension, DynDim, PhysicalQuantity,
    dimension,
    traits::Real,
    error:: { ConversionError, DimensionMismatch },
};

use core:: {
    fmt:: { self, Debug, Display },
    marker::PhantomData,
    ops:: { Mul, Div },
    str::FromStr,
};
use const_frac::Frac;

#[cfg(feature = "parser")]
use combine:: { Parser as _, stream::position };
#[cfg(all(feature = "parser", feature = "std"))]
use combine::EasyParser;
#[cfg(feature = "default-units")]
use crate::predefined::unit::DEFAULT_UNIT_DEF;

#[cfg(feature = "parser")]
mod parser;

const ZERO: Frac = Frac::from_int(0);
const ONE: Frac = Frac::from_int(1);

/// linear convertion coefficients
///     target_unit = a * source_unit_value + b
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Conv(pub Frac, pub Frac);

impl Conv {
    pub const fn add_prefix(mut self, exp10: i8) -> Self {
        if exp10 == 0 { return self }

        let exp = Frac::from_exp10(exp10 as i16);

        self.0 = if exp10 < 0 {
            self.0.div(exp)
        } else {
            self.0.mul(exp)
        };
        self
    }

    pub const fn then(mut self, rhs: Self) -> Self {
        self.0 = self.0.mul(rhs.0);
        self.1 = self.1.mul(rhs.0).add(rhs.1);
        self
    }

    pub const fn mul(mut self, rhs: Self) -> Self {
        self.0 = self.0.mul(rhs.0);
        self.1 = ZERO;
        self
    }

    pub const fn div(mut self, rhs: Self) -> Self {
        self.0 = self.0.div(rhs.0);
        self.1 = ZERO;
        self
    }

    pub const fn powi(mut self, exp: i8) -> Self {
        self.0 = self.0.powi(exp as i16);
        self.1 = ZERO;
        self
    }
}

impl Mul for Conv {
    type Output = Self;

    fn mul(self, rhs: Self) -> Self { Conv::mul(self, rhs) }
}

impl Div for Conv {
    type Output = Self;

    fn div(self, rhs: Self) -> Self { Conv::div(self, rhs) }
}

impl Default for Conv {
    fn default() -> Self {
        Self(ONE, ONE)
    }
}

/// Unit
pub struct Unit<R, D> {
    pub a: R,
    pub b: R,
    pub dim: D,
}

impl<R, D> Unit<R, D>
where
    D: Dimension, R: Real
{
    /// ```
    /// # use physical_quantity:: {
    /// #    PhysicalQuantity, Unit,
    /// #    predefined::dim::*,
    /// # };
    /// let pq = "m".parse::<Unit<_, _>>().unwrap().pq(2.0);
    ///
    /// assert_eq!(pq, PhysicalQuantity {
    ///     value: 2.0,
    ///     dim: Length::new(),
    /// });
    /// ```
    pub fn pq(&self, r: R) -> PhysicalQuantity<R, D> {
        PhysicalQuantity {
            value: r.mul_ref(&self.a).add_ref(&self.b),
            dim: self.dim,
        }
    }

    pub fn value<D0>(&self, pq: PhysicalQuantity<R, D0>) -> Result<R, ConversionError<()>>
    where
        D0: Dimension
    {
        if !dimension::is_equal(&self.dim, &pq.dim) {
            Err(ConversionError::DimensionMismatch(DimensionMismatch))
        } else {
            Ok(pq.value.sub_ref(&self.b).div_ref(&self.a))
        }
    }
}

impl<R> Unit<R, DynDim>
where
    R: Real,
{
    pub fn powi(self, n: i8) -> Self {
        Self {
            a: self.a.poweri(n as i16),
            b: R::from_frac(ZERO),
            dim: self.dim.powi(n),
        }
    }
}

impl<R, D> Clone for Unit<R, D>
where
    R: Clone, D: Clone,
{
    fn clone(&self) -> Self {
        Self {
            a: self.a.clone(),
            b: self.b.clone(),
            dim: self.dim.clone(),
        }
    }
}

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

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

            fn $method(self, rhs: &'a Unit<R, D1>) -> Self::Output {
                Unit {
                    a: self.a.$ref_method(&rhs.a),
                    b: R::from_frac(ZERO),
                    dim: self.dim.$method(rhs.dim),
                }
            }
        }
    };
}

binop_impl! { Mul, mul, mul_ref }
binop_impl! { Div, div, div_ref }

impl<R, D> Default for Unit<R, D>
where
    D: Dimension,
    R: Real,
{
    fn default() -> Self {
        Self {
            a: R::from_frac(ONE),
            b: R::from_frac(ZERO),
            dim: Default::default(),
        }
    }
}

impl<D0, D1, R> PartialEq<Unit<R, D1>> for Unit<R, D0>
where
    D0: Dimension, D1: Dimension, R: Real,
    PhysicalQuantity<R, D0>: PartialEq<PhysicalQuantity<R, D1>>
{
    fn eq(&self, other: &Unit<R, D1>) -> bool {
        self.a == other.a && self.b == other.b
    }
}

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

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

impl<R, D> From<(Conv, D)> for Unit<R, D>
where
    R: Real,
    D: Dimension,
{
    fn from((conv, dim): (Conv, D)) -> Self {
        Self {
            a: R::from_frac(conv.0),
            b: R::from_frac(conv.1),
            dim,
        }
    }
}

#[cfg(feature = "default-units")]
impl<R> FromStr for Unit<R, DynDim>
where
    R: Real,
{
    type Err = ConversionError<const_frac::error::ConversionError>;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Parser::new(&DEFAULT_UNIT_DEF[..]).parse(s)
    }
}

#[cfg(feature = "parser")]
pub struct Parser<K, T>
where
    K: AsRef<str>,
    T: AsRef<[(K, (Conv, DynDim))]>
{
    tbl: T,
    _key: PhantomData<K>,
}

#[cfg(feature = "parser")]
impl<K, T> Parser<K, T>
where
    K: AsRef<str>,
    T: AsRef<[(K, (Conv, DynDim))]> + Copy
{
    pub fn new(tbl: T) -> Self {
        Self {
            tbl,
            _key: PhantomData,
        }
    }

    pub fn parse<R: Real>(&self, s: &str)
        -> Result<Unit<R, DynDim>,ConversionError<const_frac::error::ConversionError>>
    {
        let input = position::Stream::new(s);

        match parser::unit(self.tbl).parse(input) {
            Ok((output, _remaining_input)) => Ok(Unit {
                a: R::from_frac(output.a),
                b: R::from_frac(output.b),
                dim: output.dim,
            }),
            _ => Err(ConversionError::InvalidUnitString),
        }
    }
}

#[cfg(test)]
mod tests {
    extern crate alloc;

    use super::*;
    use crate:: {
        Dim,
        predefined::dim,
    };
    use typenum::Z0;
    use alloc::string::ToString;

    #[test]
    fn test_parse_unit() {
        let u: Result<Unit<_, _>, _> = "kg*m/s2".parse();

        assert_eq!(
            u.unwrap(),
            Unit { a: 1000.0, b: 0.0, dim: DynDim::from(dim::FORCE) }
        );
    }

    #[test]
    fn test_unit_creation() {
        let u = Unit {
            a: 1f64,
            b: 0f64,
            dim: Dim::<Z0, Z0, Z0, Z0, Z0, Z0, Z0>::new(),
        };

        assert_eq!(
            u.to_string(),
            "Unit { a: 1.0, b: 0.0, dim: Dim { L: 0, M: 0, T: 0, θ: 0, N: 0, I: 0, J: 0 } }"
        );
    }
}
