macro_rules! impl_from_into_u64 {
    ($main: ident) => {
        impl From<u64> for $main {
            fn from(n: u64) -> $main {
                $main(n)
            }
        }

        impl From<$main> for u64 {
            fn from(from: $main) -> u64 {
                from.0
            }
        }

        impl $main {
            pub fn as_u64(&self) -> u64 {
                self.0
            }
        }
    };
}

macro_rules! impl_from_into_usize {
    ($main: ident) => {
        impl From<usize> for $main {
            fn from(n: usize) -> $main {
                $main(n as u64)
            }
        }

        impl From<$main> for usize {
            fn from(from: $main) -> usize {
                from.0 as usize
            }
        }

        impl $main {
            pub fn as_usize(&self) -> usize {
                self.0 as usize
            }
        }
    };
}

macro_rules! impl_u64_eq_ord {
    ($type: ident) => {
        impl PartialEq<u64> for $type {
            fn eq(&self, other: &u64) -> bool {
                self.as_u64() == *other
            }
        }

        impl PartialOrd<u64> for $type {
            fn partial_cmp(&self, other: &u64) -> Option<std::cmp::Ordering> {
                self.as_u64().partial_cmp(other)
            }
        }
    };
}

macro_rules! impl_safe_arith {
    ($type: ident, $rhs_ty: ident) => {
        impl safe_arith::SafeArith<$rhs_ty> for $type {
            const ZERO: Self = $type::new(0);
            const ONE: Self = $type::new(1);

            fn safe_add(&self, other: $rhs_ty) -> safe_arith::Result<Self> {
                self.0
                    .checked_add(other.into())
                    .map(Self::new)
                    .ok_or(safe_arith::ArithError::Overflow)
            }

            fn safe_sub(&self, other: $rhs_ty) -> safe_arith::Result<Self> {
                self.0
                    .checked_sub(other.into())
                    .map(Self::new)
                    .ok_or(safe_arith::ArithError::Overflow)
            }

            fn safe_mul(&self, other: $rhs_ty) -> safe_arith::Result<Self> {
                self.0
                    .checked_mul(other.into())
                    .map(Self::new)
                    .ok_or(safe_arith::ArithError::Overflow)
            }

            fn safe_div(&self, other: $rhs_ty) -> safe_arith::Result<Self> {
                self.0
                    .checked_div(other.into())
                    .map(Self::new)
                    .ok_or(safe_arith::ArithError::DivisionByZero)
            }

            fn safe_rem(&self, other: $rhs_ty) -> safe_arith::Result<Self> {
                self.0
                    .checked_rem(other.into())
                    .map(Self::new)
                    .ok_or(safe_arith::ArithError::DivisionByZero)
            }

            fn safe_shl(&self, other: u32) -> safe_arith::Result<Self> {
                self.0
                    .checked_shl(other)
                    .map(Self::new)
                    .ok_or(safe_arith::ArithError::Overflow)
            }

            fn safe_shr(&self, other: u32) -> safe_arith::Result<Self> {
                self.0
                    .checked_shr(other)
                    .map(Self::new)
                    .ok_or(safe_arith::ArithError::Overflow)
            }
        }
    };
}

// Deprecated: prefer `SafeArith` methods for new code.
#[cfg(feature = "saturating-arith")]
macro_rules! impl_math_between {
    ($main: ident, $other: ident) => {
        impl Add<$other> for $main {
            type Output = $main;

            fn add(self, other: $other) -> $main {
                $main::from(self.0.saturating_add(other.into()))
            }
        }

        impl AddAssign<$other> for $main {
            fn add_assign(&mut self, other: $other) {
                self.0 = self.0.saturating_add(other.into());
            }
        }

        impl Sub<$other> for $main {
            type Output = $main;

            fn sub(self, other: $other) -> $main {
                $main::from(self.0.saturating_sub(other.into()))
            }
        }

        impl SubAssign<$other> for $main {
            fn sub_assign(&mut self, other: $other) {
                self.0 = self.0.saturating_sub(other.into());
            }
        }

        impl Mul<$other> for $main {
            type Output = $main;

            fn mul(self, rhs: $other) -> $main {
                let rhs: u64 = rhs.into();
                $main::from(self.0.saturating_mul(rhs))
            }
        }

        impl MulAssign<$other> for $main {
            fn mul_assign(&mut self, rhs: $other) {
                let rhs: u64 = rhs.into();
                self.0 = self.0.saturating_mul(rhs)
            }
        }

        impl Div<$other> for $main {
            type Output = $main;

            fn div(self, rhs: $other) -> $main {
                let rhs: u64 = rhs.into();
                $main::from(
                    self.0
                        .checked_div(rhs)
                        .expect("Cannot divide by zero-valued Slot/Epoch"),
                )
            }
        }

        impl DivAssign<$other> for $main {
            fn div_assign(&mut self, rhs: $other) {
                let rhs: u64 = rhs.into();
                self.0 = self
                    .0
                    .checked_div(rhs)
                    .expect("Cannot divide by zero-valued Slot/Epoch");
            }
        }

        impl Rem<$other> for $main {
            type Output = $main;

            fn rem(self, modulus: $other) -> $main {
                let modulus: u64 = modulus.into();
                $main::from(
                    self.0
                        .checked_rem(modulus)
                        .expect("Cannot divide by zero-valued Slot/Epoch"),
                )
            }
        }
    };
}

macro_rules! impl_math {
    ($type: ident) => {
        impl $type {
            pub fn saturating_sub<T: Into<$type>>(&self, other: T) -> $type {
                $type::new(self.as_u64().saturating_sub(other.into().as_u64()))
            }

            pub fn saturating_add<T: Into<$type>>(&self, other: T) -> $type {
                $type::new(self.as_u64().saturating_add(other.into().as_u64()))
            }

            pub fn is_power_of_two(&self) -> bool {
                self.0.is_power_of_two()
            }
        }
    };
}

macro_rules! impl_display {
    ($type: ident) => {
        impl fmt::Display for $type {
            fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
                write!(f, "{}", self.0)
            }
        }
    };
}

macro_rules! impl_debug {
    ($type: ident) => {
        impl fmt::Debug for $type {
            fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
                write!(f, "{}({:?})", stringify!($type), self.0)
            }
        }
    };
}

macro_rules! impl_ssz {
    ($type: ident) => {
        impl Encode for $type {
            fn is_ssz_fixed_len() -> bool {
                <u64 as Encode>::is_ssz_fixed_len()
            }

            fn ssz_fixed_len() -> usize {
                <u64 as Encode>::ssz_fixed_len()
            }

            fn ssz_bytes_len(&self) -> usize {
                0_u64.ssz_bytes_len()
            }

            fn ssz_append(&self, buf: &mut Vec<u8>) {
                self.0.ssz_append(buf)
            }
        }

        impl Decode for $type {
            fn is_ssz_fixed_len() -> bool {
                <u64 as Decode>::is_ssz_fixed_len()
            }

            fn ssz_fixed_len() -> usize {
                <u64 as Decode>::ssz_fixed_len()
            }

            fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, DecodeError> {
                Ok($type(u64::from_ssz_bytes(bytes)?))
            }
        }

        impl tree_hash::TreeHash for $type {
            fn tree_hash_type() -> tree_hash::TreeHashType {
                tree_hash::TreeHashType::Basic
            }

            fn tree_hash_packed_encoding(&self) -> tree_hash::PackedEncoding {
                self.0.tree_hash_packed_encoding()
            }

            fn tree_hash_packing_factor() -> usize {
                32usize.wrapping_div(8)
            }

            fn tree_hash_root(&self) -> tree_hash::Hash256 {
                tree_hash::Hash256::from_slice(&int_to_bytes::int_to_fixed_bytes32(self.0))
            }
        }

        impl SignedRoot for $type {}
    };
}

macro_rules! impl_from_str {
    ($type: ident) => {
        impl std::str::FromStr for $type {
            type Err = std::num::ParseIntError;

            fn from_str(s: &str) -> Result<$type, Self::Err> {
                u64::from_str(s).map($type)
            }
        }
    };
}

macro_rules! impl_common {
    ($type: ident) => {
        impl_from_into_u64!($type);
        impl_from_into_usize!($type);
        impl_u64_eq_ord!($type);
        impl_safe_arith!($type, $type);
        impl_safe_arith!($type, u64);
        #[cfg(feature = "saturating-arith")]
        impl_math_between!($type, $type);
        #[cfg(feature = "saturating-arith")]
        impl_math_between!($type, u64);
        impl_math!($type);
        impl_display!($type);
        impl_debug!($type);
        impl_ssz!($type);
        impl_from_str!($type);
    };
}

// test macros
#[cfg(test)]
macro_rules! new_tests {
    ($type: ident) => {
        #[test]
        fn new() {
            assert_eq!($type(0), $type::new(0));
            assert_eq!($type(3), $type::new(3));
            assert_eq!($type(u64::MAX), $type::new(u64::MAX));
        }
    };
}

#[cfg(test)]
macro_rules! from_into_tests {
    ($type: ident, $other: ident) => {
        #[test]
        fn into() {
            let x: $other = $type(0).into();
            assert_eq!(x, 0);

            let x: $other = $type(3).into();
            assert_eq!(x, 3);

            let x: $other = $type(u64::MAX).into();
            // Note: this will fail on 32 bit systems. This is expected as we don't have a proper
            // 32-bit system strategy in place.
            assert_eq!(x, $other::MAX);
        }

        #[test]
        fn from() {
            assert_eq!($type(0), $type::from(0_u64));
            assert_eq!($type(3), $type::from(3_u64));
            assert_eq!($type(u64::MAX), $type::from($other::MAX));
        }
    };
}

#[cfg(test)]
macro_rules! math_between_tests {
    ($type: ident, $other: ident) => {
        #[test]
        fn partial_ord() {
            use std::cmp::Ordering;
            let assert_partial_ord = |a: u64, partial_ord: Ordering, b: u64| {
                let other: $other = $type(b).into();
                assert_eq!($type(a).partial_cmp(&other), Some(partial_ord));
            };

            assert_partial_ord(1, Ordering::Less, 2);
            assert_partial_ord(2, Ordering::Greater, 1);
            assert_partial_ord(0, Ordering::Less, u64::MAX);
            assert_partial_ord(u64::MAX, Ordering::Greater, 0);
        }

        #[test]
        fn partial_eq() {
            let assert_partial_eq = |a: u64, b: u64, is_equal: bool| {
                let other: $other = $type(b).into();
                assert_eq!($type(a).eq(&other), is_equal);
            };

            assert_partial_eq(0, 0, true);
            assert_partial_eq(0, 1, false);
            assert_partial_eq(1, 0, false);
            assert_partial_eq(1, 1, true);

            assert_partial_eq(u64::MAX, u64::MAX, true);
            assert_partial_eq(0, u64::MAX, false);
            assert_partial_eq(u64::MAX, 0, false);
        }

        #[test]
        fn add_and_add_assign() {
            let assert_add = |a: u64, b: u64, result: u64| {
                let other: $other = $type(b).into();
                assert_eq!($type(a) + other, $type(result));

                let mut add_assigned = $type(a);
                add_assigned += other;

                assert_eq!(add_assigned, $type(result));
            };

            assert_add(0, 1, 1);
            assert_add(1, 0, 1);
            assert_add(1, 2, 3);
            assert_add(2, 1, 3);
            assert_add(7, 7, 14);

            // Addition should be saturating.
            assert_add(u64::MAX, 1, u64::MAX);
            assert_add(u64::MAX, u64::MAX, u64::MAX);
        }

        #[test]
        fn sub_and_sub_assign() {
            let assert_sub = |a: u64, b: u64, result: u64| {
                let other: $other = $type(b).into();
                assert_eq!($type(a) - other, $type(result));

                let mut sub_assigned = $type(a);
                sub_assigned -= other;

                assert_eq!(sub_assigned, $type(result));
            };

            assert_sub(1, 0, 1);
            assert_sub(2, 1, 1);
            assert_sub(14, 7, 7);
            assert_sub(u64::MAX, 1, u64::MAX - 1);
            assert_sub(u64::MAX, u64::MAX, 0);

            // Subtraction should be saturating
            assert_sub(0, 1, 0);
            assert_sub(1, 2, 0);
        }

        #[test]
        fn mul_and_mul_assign() {
            let assert_mul = |a: u64, b: u64, result: u64| {
                let other: $other = $type(b).into();
                assert_eq!($type(a) * other, $type(result));

                let mut mul_assigned = $type(a);
                mul_assigned *= other;

                assert_eq!(mul_assigned, $type(result));
            };

            assert_mul(2, 2, 4);
            assert_mul(1, 2, 2);
            assert_mul(0, 2, 0);

            // Multiplication should be saturating.
            assert_mul(u64::MAX, 2, u64::MAX);
        }

        #[test]
        fn div_and_div_assign() {
            let assert_div = |a: u64, b: u64, result: u64| {
                let other: $other = $type(b).into();
                assert_eq!($type(a) / other, $type(result));

                let mut div_assigned = $type(a);
                div_assigned /= other;

                assert_eq!(div_assigned, $type(result));
            };

            assert_div(0, 2, 0);
            assert_div(2, 2, 1);
            assert_div(100, 50, 2);
            assert_div(128, 2, 64);
            assert_div(u64::MAX, 2, 2_u64.pow(63) - 1);
        }

        #[test]
        #[should_panic]
        fn div_panics_with_divide_by_zero() {
            let other: $other = $type(0).into();
            let _ = $type(2) / other;
        }

        #[test]
        #[should_panic]
        fn div_assign_panics_with_divide_by_zero() {
            let other: $other = $type(0).into();
            let mut assigned = $type(2);
            assigned /= other;
        }

        #[test]
        fn rem() {
            let assert_rem = |a: u64, b: u64, result: u64| {
                let other: $other = $type(b).into();
                assert_eq!($type(a) % other, $type(result));
            };

            assert_rem(3, 2, 1);
            assert_rem(40, 2, 0);
            assert_rem(10, 100, 10);
            assert_rem(302042, 3293, 2379);
        }
    };
}

#[cfg(test)]
macro_rules! math_tests {
    ($type: ident) => {
        #[test]
        fn saturating_sub() {
            let assert_saturating_sub = |a: u64, b: u64, result: u64| {
                assert_eq!($type(a).saturating_sub($type(b)), $type(result));
            };

            assert_saturating_sub(1, 0, 1);
            assert_saturating_sub(2, 1, 1);
            assert_saturating_sub(14, 7, 7);
            assert_saturating_sub(u64::MAX, 1, u64::MAX - 1);
            assert_saturating_sub(u64::MAX, u64::MAX, 0);

            // Subtraction should be saturating
            assert_saturating_sub(0, 1, 0);
            assert_saturating_sub(1, 2, 0);
        }

        #[test]
        fn saturating_add() {
            let assert_saturating_add = |a: u64, b: u64, result: u64| {
                assert_eq!($type(a).saturating_add($type(b)), $type(result));
            };

            assert_saturating_add(0, 1, 1);
            assert_saturating_add(1, 0, 1);
            assert_saturating_add(1, 2, 3);
            assert_saturating_add(2, 1, 3);
            assert_saturating_add(7, 7, 14);

            // Addition should be saturating.
            assert_saturating_add(u64::MAX, 1, u64::MAX);
            assert_saturating_add(u64::MAX, u64::MAX, u64::MAX);
        }

        #[test]
        fn checked_div() {
            let assert_checked_div = |a: u64, b: u64, result: Option<u64>| {
                let division_result_as_u64 =
                    $type(a).safe_div($type(b)).ok().map(|val| val.as_u64());
                assert_eq!(division_result_as_u64, result);
            };

            assert_checked_div(0, 2, Some(0));
            assert_checked_div(2, 2, Some(1));
            assert_checked_div(100, 50, Some(2));
            assert_checked_div(128, 2, Some(64));
            assert_checked_div(u64::MAX, 2, Some(2_u64.pow(63) - 1));

            assert_checked_div(2, 0, None);
            assert_checked_div(0, 0, None);
            assert_checked_div(u64::MAX, 0, None);
        }

        #[test]
        fn is_power_of_two() {
            let assert_is_power_of_two = |a: u64, result: bool| {
                assert_eq!(
                    $type(a).is_power_of_two(),
                    result,
                    "{}.is_power_of_two() != {}",
                    a,
                    result
                );
            };

            assert_is_power_of_two(0, false);
            assert_is_power_of_two(1, true);
            assert_is_power_of_two(2, true);
            assert_is_power_of_two(3, false);
            assert_is_power_of_two(4, true);

            assert_is_power_of_two(2_u64.pow(4), true);
            assert_is_power_of_two(u64::MAX, false);
        }

        #[test]
        fn ord() {
            use std::cmp::Ordering;
            let assert_ord = |a: u64, ord: Ordering, b: u64| {
                assert_eq!($type(a).cmp(&$type(b)), ord);
            };

            assert_ord(1, Ordering::Less, 2);
            assert_ord(2, Ordering::Greater, 1);
            assert_ord(0, Ordering::Less, u64::MAX);
            assert_ord(u64::MAX, Ordering::Greater, 0);
        }
    };
}

#[cfg(test)]
macro_rules! all_tests {
    ($type: ident) => {
        new_tests!($type);
        math_between_tests!($type, $type);
        math_tests!($type);
        ssz_and_tree_hash_tests!($type);

        mod u64_tests {
            use super::*;

            from_into_tests!($type, u64);
            math_between_tests!($type, u64);

            #[test]
            pub fn as_64() {
                let x = $type(0).as_u64();
                assert_eq!(x, 0);

                let x = $type(3).as_u64();
                assert_eq!(x, 3);

                let x = $type(u64::MAX).as_u64();
                assert_eq!(x, u64::MAX);
            }
        }

        mod usize_tests {
            use super::*;

            from_into_tests!($type, usize);

            #[test]
            pub fn as_usize() {
                let x = $type(0).as_usize();
                assert_eq!(x, 0);

                let x = $type(3).as_usize();
                assert_eq!(x, 3);

                let x = $type(u64::MAX).as_usize();
                assert_eq!(x, usize::MAX);
            }
        }
    };
}
