use candid::Nat;
use core::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Rem, RemAssign, Sub, SubAssign};
use ex3_ic_stable_structures::{storable::Bound, Storable};
use num_bigint::BigUint;
use num_traits::{CheckedAdd, CheckedDiv, CheckedMul, CheckedSub, One, Zero};
use serde::{Deserialize, Serialize};
use std::fmt::Display;

#[derive(Debug, Clone, Default, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
#[serde(transparent)]
pub struct Ex3Uint(pub BigUint);
mod add;
mod convert;
mod div;
mod mul;
mod pow;
mod sub;
mod rem;

impl Zero for Ex3Uint {
    fn zero() -> Self {
        Ex3Uint(BigUint::zero())
    }

    fn is_zero(&self) -> bool {
        self.0.is_zero()
    }
}

impl One for Ex3Uint {
    fn one() -> Self {
        Ex3Uint(BigUint::one())
    }
    fn set_one(&mut self) {
        *self = One::one();
    }

    fn is_one(&self) -> bool
    where
        Self: PartialEq,
    {
        *self == Self::one()
    }
}

impl Add for Ex3Uint {
    type Output = Ex3Uint;

    fn add(self, rhs: Self) -> Self::Output {
        Ex3Uint(self.0 + rhs.0)
    }
}

impl CheckedAdd for Ex3Uint {
    fn checked_add(&self, rhs: &Self) -> Option<Self> {
        self.0.checked_add(&rhs.0).map(Self)
    }
}

impl AddAssign for Ex3Uint {
    fn add_assign(&mut self, rhs: Self) {
        self.0 += rhs.0;
    }
}

impl Sub for Ex3Uint {
    type Output = Ex3Uint;

    fn sub(self, rhs: Self) -> Self::Output {
        Ex3Uint(self.0 - rhs.0)
    }
}

impl CheckedSub for Ex3Uint {
    fn checked_sub(&self, rhs: &Self) -> Option<Self> {
        self.0.checked_sub(&rhs.0).map(Self)
    }
}

impl SubAssign for Ex3Uint {
    fn sub_assign(&mut self, rhs: Self) {
        self.0 -= rhs.0;
    }
}

impl Div for Ex3Uint {
    type Output = Ex3Uint;

    fn div(self, rhs: Self) -> Self::Output {
        Ex3Uint(self.0 / rhs.0)
    }
}

impl CheckedDiv for Ex3Uint {
    fn checked_div(&self, rhs: &Self) -> Option<Self> {
        self.0.checked_div(&rhs.0).map(Self)
    }
}

impl DivAssign for Ex3Uint {
    fn div_assign(&mut self, rhs: Self) {
        self.0 /= rhs.0;
    }
}

impl Mul for Ex3Uint {
    type Output = Ex3Uint;

    fn mul(self, rhs: Self) -> Self::Output {
        Ex3Uint(self.0 * rhs.0)
    }
}

impl CheckedMul for Ex3Uint {
    fn checked_mul(&self, rhs: &Self) -> Option<Self> {
        self.0.checked_mul(&rhs.0).map(Self)
    }
}

impl MulAssign for Ex3Uint {
    fn mul_assign(&mut self, rhs: Self) {
        self.0 *= rhs.0;
    }
}

impl Rem for Ex3Uint {
    type Output = Ex3Uint;

    fn rem(self, rhs: Self) -> Self::Output {
        Ex3Uint(self.0 % rhs.0)
    }
}

impl RemAssign for Ex3Uint {
    fn rem_assign(&mut self, rhs: Self) {
        self.0 %= rhs.0;
    }
}

impl Display for Ex3Uint {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl From<BigUint> for Ex3Uint {
    fn from(value: BigUint) -> Self {
        Self(value)
    }
}

impl From<Ex3Uint> for BigUint {
    fn from(value: Ex3Uint) -> Self {
        value.0
    }
}

impl From<Nat> for Ex3Uint {
    fn from(value: Nat) -> Self {
        Self(value.0)
    }
}

impl From<Ex3Uint> for Nat {
    fn from(value: Ex3Uint) -> Self {
        Self(value.0.into())
    }
}

impl Storable for Ex3Uint {
    fn to_bytes(&self) -> std::borrow::Cow<[u8]> {
        self.0.to_bytes_le().into()
    }

    fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self {
        Self(BigUint::from_bytes_le(&bytes))
    }

    const BOUND: Bound = Bound::Bounded {
        max_size: 32,
        is_fixed_size: false,
    };
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_to_bytes() {
        let num = Ex3Uint(BigUint::from(u128::MAX));
        let bytes = num.to_bytes();
        assert!(bytes.len() <= 32);

        let num_decode = Ex3Uint::from_bytes(bytes.into());
        assert_eq!(num, num_decode);
    }

    #[test]
    fn test_add() {
        let num1 = Ex3Uint(BigUint::from(10u128));
        let num2 = Ex3Uint(BigUint::from(20u128));
        let sum = num1 + num2;
        assert_eq!(sum.0, BigUint::from(30u128));
    }

    #[test]
    fn test_sub() {
        let num1 = Ex3Uint(BigUint::from(20u128));
        let num2 = Ex3Uint(BigUint::from(10u128));
        let diff = num1 - num2;
        assert_eq!(diff.0, BigUint::from(10u128));
    }
}
