use std::convert::TryFrom;

use traiter::numbers::{LoadExp, Signed};

use super::digits::{FractExpDigits, MaybeReduceDigits};
use super::types::{
    BigInt, TryIntoFloatError, TryIntoSignedIntegerError,
    TryIntoUnsignedIntegerError,
};

macro_rules! float_try_from_big_int_impl {
    ($($float:ty)*) => ($(
        impl<
                Digit: FractExpDigits<$float>,
                const DIGIT_BITNESS: usize,
            > TryFrom<BigInt<Digit, DIGIT_BITNESS>> for $float
        {
            type Error = TryIntoFloatError;

            fn try_from(
                value: BigInt<Digit, DIGIT_BITNESS>,
            ) -> Result<Self, Self::Error> {
                match Digit::fract_exp_digits::<DIGIT_BITNESS>(&value.digits) {
                    Some((fraction_modulus, exponent)) => {
                        Ok(((value.sign as $float) * fraction_modulus)
                            .load_exp(exponent))
                    }
                    None => Err(TryIntoFloatError::TooLarge),
                }
            }
        }

        impl<
                Digit: FractExpDigits<$float>,
                const DIGIT_BITNESS: usize,
            > TryFrom<&BigInt<Digit, DIGIT_BITNESS>> for $float
        {
            type Error = TryIntoFloatError;

            fn try_from(
                value: &BigInt<Digit, DIGIT_BITNESS>,
            ) -> Result<Self, Self::Error> {
                match Digit::fract_exp_digits::<DIGIT_BITNESS>(&value.digits) {
                    Some((fraction_modulus, exponent)) => {
                        Ok(((value.sign as $float) * fraction_modulus)
                            .load_exp(exponent))
                    }
                    None => Err(TryIntoFloatError::TooLarge),
                }
            }
        }
    )*)
}

float_try_from_big_int_impl!(f32 f64);

macro_rules! signed_integer_try_from_big_int_impl {
    ($($integer:ty)*) => ($(
        impl<
                Digit: MaybeReduceDigits<$integer>,
                const DIGIT_BITNESS: usize,
            > TryFrom<BigInt<Digit, DIGIT_BITNESS>> for $integer
        where
            for<'a> &'a BigInt<Digit, DIGIT_BITNESS>: Signed,
        {
            type Error = TryIntoSignedIntegerError;

            fn try_from(
                value: BigInt<Digit, DIGIT_BITNESS>,
            ) -> Result<Self, Self::Error> {
                let result =
                    Digit::maybe_reduce_digits::<DIGIT_BITNESS>(&value.digits)
                        .ok_or(TryIntoSignedIntegerError::TooLarge);
                if value.is_negative() {
                    result.map(|value| -value)
                } else {
                    result
                }
            }
        }

        impl<
                'a,
                Digit: MaybeReduceDigits<$integer>,
                const DIGIT_BITNESS: usize,
            > TryFrom<&'a BigInt<Digit, DIGIT_BITNESS>> for $integer
        where
            &'a BigInt<Digit, DIGIT_BITNESS>: Signed,
        {
            type Error = TryIntoSignedIntegerError;

            fn try_from(
                value: &'a BigInt<Digit, DIGIT_BITNESS>,
            ) -> Result<Self, Self::Error> {
                let result =
                    Digit::maybe_reduce_digits::<DIGIT_BITNESS>(&value.digits)
                        .ok_or(TryIntoSignedIntegerError::TooLarge);
                if value.is_negative() {
                    result.map(|value| -value)
                } else {
                    result
                }
            }
        }
    )*)
}

signed_integer_try_from_big_int_impl!(i8 i16 i32 i64 i128 isize);

macro_rules! unsigned_integer_try_from_big_int_impl {
    ($($integer:ty)*) => ($(
        impl<
                Digit: MaybeReduceDigits<$integer>,
                const DIGIT_BITNESS: usize,
            > TryFrom<BigInt<Digit, DIGIT_BITNESS>> for $integer
        where
            for<'a> &'a BigInt<Digit, DIGIT_BITNESS>: Signed,
        {
            type Error = TryIntoUnsignedIntegerError;

            fn try_from(
                value: BigInt<Digit, DIGIT_BITNESS>,
            ) -> Result<Self, Self::Error> {
                if (&value).is_negative() {
                    Err(TryIntoUnsignedIntegerError::Negative)
                } else {
                    Digit::maybe_reduce_digits::<DIGIT_BITNESS>(&value.digits)
                        .ok_or(TryIntoUnsignedIntegerError::TooLarge)
                }
            }
        }

        impl<
                'a,
                Digit: MaybeReduceDigits<$integer>,
                const DIGIT_BITNESS: usize,
            > TryFrom<&'a BigInt<Digit, DIGIT_BITNESS>> for $integer
        where
            &'a BigInt<Digit, DIGIT_BITNESS>: Signed,
        {
            type Error = TryIntoUnsignedIntegerError;

            fn try_from(
                value: &'a BigInt<Digit, DIGIT_BITNESS>,
            ) -> Result<Self, Self::Error> {
                if value.is_negative() {
                    Err(TryIntoUnsignedIntegerError::Negative)
                } else {
                    Digit::maybe_reduce_digits::<DIGIT_BITNESS>(&value.digits)
                        .ok_or(TryIntoUnsignedIntegerError::TooLarge)
                }
            }
        }
    )*)
}

unsigned_integer_try_from_big_int_impl!(u8 u16 u32 u64 u128 usize);
