use std::convert::TryFrom;
use std::mem::size_of;

use traiter::numbers::{CheckedShl, DivRem, Sign, Signed, Zeroable};

use super::digits::{PrimitiveShiftDigitsLeft, ShiftDigitsLeft};
use super::types::{BigInt, ShlError};

impl<Digit: ShiftDigitsLeft, const DIGIT_BITNESS: usize> CheckedShl
    for BigInt<Digit, DIGIT_BITNESS>
where
    for<'a> &'a Self: Signed,
{
    type Output = Result<Self, ShlError>;

    fn checked_shl(self, shift: Self) -> Self::Output {
        match (&shift).sign() {
            Sign::Negative => Err(ShlError::NegativeShift),
            Sign::Positive => Ok(BigInt::<Digit, DIGIT_BITNESS> {
                sign: self.sign,
                digits: Digit::shift_digits_left::<DIGIT_BITNESS>(
                    &self.digits,
                    &shift.digits,
                )?,
            }),
            Sign::Zero => Ok(self),
        }
    }
}

impl<Digit: ShiftDigitsLeft, const DIGIT_BITNESS: usize> CheckedShl<&Self>
    for BigInt<Digit, DIGIT_BITNESS>
where
    for<'a> &'a Self: Signed,
{
    type Output = Result<Self, ShlError>;

    fn checked_shl(self, shift: &Self) -> Self::Output {
        match shift.sign() {
            Sign::Negative => Err(ShlError::NegativeShift),
            Sign::Positive => Ok(BigInt::<Digit, DIGIT_BITNESS> {
                sign: self.sign,
                digits: Digit::shift_digits_left::<DIGIT_BITNESS>(
                    &self.digits,
                    &shift.digits,
                )?,
            }),
            Sign::Zero => Ok(self),
        }
    }
}

impl<Digit: ShiftDigitsLeft, const DIGIT_BITNESS: usize>
    CheckedShl<BigInt<Digit, DIGIT_BITNESS>> for &BigInt<Digit, DIGIT_BITNESS>
where
    for<'a> &'a BigInt<Digit, DIGIT_BITNESS>: Signed,
    BigInt<Digit, DIGIT_BITNESS>: Clone,
{
    type Output = Result<BigInt<Digit, DIGIT_BITNESS>, ShlError>;

    fn checked_shl(self, shift: BigInt<Digit, DIGIT_BITNESS>) -> Self::Output {
        match (&shift).sign() {
            Sign::Negative => Err(ShlError::NegativeShift),
            Sign::Positive => Ok(BigInt::<Digit, DIGIT_BITNESS> {
                sign: self.sign,
                digits: Digit::shift_digits_left::<DIGIT_BITNESS>(
                    &self.digits,
                    &shift.digits,
                )?,
            }),
            Sign::Zero => Ok(self.clone()),
        }
    }
}

impl<Digit: ShiftDigitsLeft, const DIGIT_BITNESS: usize> CheckedShl
    for &BigInt<Digit, DIGIT_BITNESS>
where
    for<'a> &'a BigInt<Digit, DIGIT_BITNESS>: Signed,
    BigInt<Digit, DIGIT_BITNESS>: Clone,
{
    type Output = Result<BigInt<Digit, DIGIT_BITNESS>, ShlError>;

    fn checked_shl(self, shift: Self) -> Self::Output {
        match shift.sign() {
            Sign::Negative => Err(ShlError::NegativeShift),
            Sign::Positive => Ok(BigInt::<Digit, DIGIT_BITNESS> {
                sign: self.sign,
                digits: Digit::shift_digits_left::<DIGIT_BITNESS>(
                    &self.digits,
                    &shift.digits,
                )?,
            }),
            Sign::Zero => Ok(self.clone()),
        }
    }
}

macro_rules! checked_shl_signed_integer_impl {
    ($($integer:ty)*) => ($(
        impl<
                Digit: PrimitiveShiftDigitsLeft + TryFrom<usize>,
                const DIGIT_BITNESS: usize,
            > CheckedShl<$integer> for BigInt<Digit, DIGIT_BITNESS>
        {
            type Output = Result<Self, ShlError>;

            fn checked_shl(self, shift: $integer) -> Self::Output {
                debug_assert!(
                    usize::BITS < <$integer>::BITS
                        || DIGIT_BITNESS < <$integer>::MAX as usize
                );
                match shift.sign() {
                    Sign::Negative => Err(ShlError::NegativeShift),
                    Sign::Positive => {
                        let (shift_quotient, shift_remainder) =
                            shift.div_rem(DIGIT_BITNESS as $integer);
                        if (<$integer>::BITS as usize) + 8 * size_of::<Digit>()
                            >= (usize::BITS as usize)
                            && unsafe {
                                usize::try_from(shift_quotient)
                                    .unwrap_unchecked()
                            } >= (usize::MAX / size_of::<Digit>())
                        {
                            Err(ShlError::TooLarge)
                        } else {
                            let digits = Digit::primitive_shift_digits_left::<
                                DIGIT_BITNESS,
                            >(
                                &self.digits,
                                shift_quotient as usize,
                                unsafe {
                                    Digit::try_from(shift_remainder as usize)
                                        .unwrap_unchecked()
                                },
                            )
                            .ok_or(ShlError::OutOfMemory)?;
                            Ok(Self {
                                sign: self.sign,
                                digits,
                            })
                        }
                    }
                    Sign::Zero => Ok(self),
                }
            }
        }

        impl<
                Digit: PrimitiveShiftDigitsLeft + TryFrom<usize>,
                const DIGIT_BITNESS: usize,
            > CheckedShl<$integer> for &BigInt<Digit, DIGIT_BITNESS>
        where
            BigInt<Digit, DIGIT_BITNESS>: Clone,
        {
            type Output = Result<BigInt<Digit, DIGIT_BITNESS>, ShlError>;

            fn checked_shl(self, shift: $integer) -> Self::Output {
                debug_assert!(
                    usize::BITS < <$integer>::BITS
                        || DIGIT_BITNESS < <$integer>::MAX as usize
                );
                match shift.sign() {
                    Sign::Negative => Err(ShlError::NegativeShift),
                    Sign::Positive => {
                        let (shift_quotient, shift_remainder) =
                            shift.div_rem(DIGIT_BITNESS as $integer);
                        if (<$integer>::BITS as usize) + 8 * size_of::<Digit>()
                            >= (usize::BITS as usize)
                            && unsafe {
                                usize::try_from(shift_quotient)
                                    .unwrap_unchecked()
                            } >= (usize::MAX / size_of::<Digit>())
                        {
                            Err(ShlError::TooLarge)
                        } else {
                            let digits = Digit::primitive_shift_digits_left::<
                                DIGIT_BITNESS,
                            >(
                                &self.digits,
                                shift_quotient as usize,
                                unsafe {
                                    Digit::try_from(shift_remainder as usize)
                                        .unwrap_unchecked()
                                },
                            )
                            .ok_or(ShlError::OutOfMemory)?;
                            Ok(BigInt::<Digit, DIGIT_BITNESS> {
                                sign: self.sign,
                                digits,
                            })
                        }
                    }
                    Sign::Zero => Ok(self.clone()),
                }
            }
        }
    )*)
}

checked_shl_signed_integer_impl!(i8 i16 i32 i64 i128 isize);

macro_rules! checked_shl_unsigned_integer_impl {
    ($($integer:ty)*) => ($(
        impl<
                Digit: PrimitiveShiftDigitsLeft + TryFrom<usize>,
                const DIGIT_BITNESS: usize,
            > CheckedShl<$integer> for BigInt<Digit, DIGIT_BITNESS>
        where
            for<'a> &'a BigInt<Digit, DIGIT_BITNESS>: Zeroable,
        {
            type Output = Result<Self, ShlError>;

            fn checked_shl(self, shift: $integer) -> Self::Output {
                debug_assert!(
                    usize::BITS < <$integer>::BITS
                        || DIGIT_BITNESS < <$integer>::MAX as usize
                );
                if shift.is_zero() {
                    Ok(self)
                } else {
                    let (shift_quotient, shift_remainder) =
                        shift.div_rem(DIGIT_BITNESS as $integer);
                    if (<$integer>::BITS as usize) + 8 * size_of::<Digit>()
                        >= (usize::BITS as usize)
                        && unsafe {
                            usize::try_from(shift_quotient).unwrap_unchecked()
                        } >= (usize::MAX / size_of::<Digit>())
                    {
                        Err(ShlError::TooLarge)
                    } else {
                        let digits =
                            Digit::primitive_shift_digits_left::<DIGIT_BITNESS>(
                                &self.digits,
                                shift_quotient as usize,
                                unsafe {
                                    Digit::try_from(shift_remainder as usize)
                                        .unwrap_unchecked()
                                },
                            )
                            .ok_or(ShlError::OutOfMemory)?;
                        Ok(Self {
                            sign: self.sign,
                            digits,
                        })
                    }
                }
            }
        }

        impl<
                Digit: PrimitiveShiftDigitsLeft + TryFrom<usize>,
                const DIGIT_BITNESS: usize,
            > CheckedShl<$integer> for &BigInt<Digit, DIGIT_BITNESS>
        where
            for<'a> &'a BigInt<Digit, DIGIT_BITNESS>: Zeroable,
            BigInt<Digit, DIGIT_BITNESS>: Clone,
        {
            type Output = Result<BigInt<Digit, DIGIT_BITNESS>, ShlError>;

            fn checked_shl(self, shift: $integer) -> Self::Output {
                debug_assert!(
                    usize::BITS < <$integer>::BITS
                        || DIGIT_BITNESS < <$integer>::MAX as usize
                );
                if shift.is_zero() {
                    Ok(self.clone())
                } else {
                    let (shift_quotient, shift_remainder) =
                        shift.div_rem(DIGIT_BITNESS as $integer);
                    if (<$integer>::BITS as usize) + 8 * size_of::<Digit>()
                        >= (usize::BITS as usize)
                        && unsafe {
                            usize::try_from(shift_quotient).unwrap_unchecked()
                        } >= (usize::MAX / size_of::<Digit>())
                    {
                        Err(ShlError::TooLarge)
                    } else {
                        let digits =
                            Digit::primitive_shift_digits_left::<DIGIT_BITNESS>(
                                &self.digits,
                                shift_quotient as usize,
                                unsafe {
                                    Digit::try_from(shift_remainder as usize)
                                        .unwrap_unchecked()
                                },
                            )
                            .ok_or(ShlError::OutOfMemory)?;
                        Ok(BigInt::<Digit, DIGIT_BITNESS> {
                            sign: self.sign,
                            digits,
                        })
                    }
                }
            }
        }
    )*)
}

checked_shl_unsigned_integer_impl!(u8 u16 u32 u64 u128 usize);
