use std::cmp::*;
use std::ops::{Add, AddAssign, Div, Mul, Neg, Sub, SubAssign};
use std::str::FromStr;

use num_traits::cast::ToPrimitive;
use num_traits::identities::{One, Zero};
use rust_decimal::{Decimal, Error as DecimalError};

#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Copy, Default)]
pub struct Cents(Decimal);

impl Cents {
    pub fn new(num: i64, scale: u32) -> Self {
        Cents(Decimal::new(num, scale))
    }

    pub fn round(&self) -> Cents {
        Cents(self.0.round_dp(2))
    }

    pub fn scale(&self) -> u32 {
        self.0.scale()
    }

    pub fn to_cents(&self) -> i64 {
        (self.0 * Decimal::new(100, 0)).round().to_i64().unwrap()
    }
}

impl Zero for Cents {
    fn zero() -> Cents {
        Cents(Decimal::zero())
    }

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

impl One for Cents {
    fn one() -> Cents {
        Cents(Decimal::one())
    }
}

impl Add<Cents> for Cents {
    type Output = Cents;

    fn add(self, other: Cents) -> Cents {
        Cents(self.0.add(other.0))
    }
}

impl AddAssign for Cents {
    fn add_assign(&mut self, other: Cents) {
        *self = *self + other;
    }
}

impl Sub<Cents> for Cents {
    type Output = Cents;

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

impl SubAssign for Cents {
    fn sub_assign(&mut self, other: Cents) {
        *self = *self - other;
    }
}

impl Mul<Cents> for Cents {
    type Output = Cents;

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

impl Div<Cents> for Cents {
    type Output = Cents;

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

impl Neg for Cents {
    type Output = Self;

    fn neg(self) -> Self::Output {
        Cents(self.0.neg())
    }
}

impl From<i32> for Cents {
    fn from(t: i32) -> Cents {
        Cents(Decimal::from(t))
    }
}

impl From<Decimal> for Cents {
    fn from(d: Decimal) -> Cents {
        Cents(d)
    }
}

impl FromStr for Cents {
    type Err = DecimalError;

    fn from_str(value: &str) -> Result<Cents, Self::Err> {
        match Decimal::from_str(value) {
            Ok(d) => Ok(Cents(d)),
            Err(err) => Err(err),
        }
    }
}

impl Into<Decimal> for Cents {
    fn into(self) -> Decimal {
        self.0
    }
}

impl std::fmt::Display for Cents {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let mut d = self.0;
        if d.scale() < 2 {
            // rescale to two decimal places
            d.rescale(2);
        }
        d.fmt(f)
    }
}

impl sqlx::Type<sqlx::postgres::Postgres> for Cents {
    fn type_info() -> sqlx::postgres::PgTypeInfo {
        sqlx::postgres::PgTypeInfo::with_name("INT8")
    }
}

impl<'q> sqlx::encode::Encode<'q, sqlx::postgres::Postgres> for Cents {
    fn encode_by_ref(&self, buf: &mut sqlx::postgres::PgArgumentBuffer) -> sqlx::encode::IsNull {
        let cents = self.to_cents();
        sqlx::encode::Encode::encode(&cents, buf)
    }
}

impl<'r> sqlx::decode::Decode<'r, sqlx::postgres::Postgres> for Cents {
    fn decode(
        value: sqlx::postgres::PgValueRef<'r>,
    ) -> Result<Self, Box<dyn std::error::Error + 'static + Send + Sync>> {
        let cents: i64 = sqlx::decode::Decode::decode(value)?;
        Ok(match cents {
            n if n % 100 == 0 => Cents(Decimal::new(n / 100, 0)),
            n if n % 10 == 0 => Cents(Decimal::new(n / 10, 1)),
            _ => Cents(Decimal::new(cents, 2)),
        })
    }
}

impl serde::Serialize for Cents {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(&self.0.to_string())
    }
}

impl<'de> serde::Deserialize<'de> for Cents {
    fn deserialize<D>(deserializer: D) -> Result<Cents, D::Error>
    where
        D: serde::de::Deserializer<'de>,
    {
        serde::Deserialize::deserialize(deserializer).map(Cents)
    }
}

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

    #[test]
    fn print_at_least_two_decimal_places() {
        let cents = Cents::new(42, 0);
        assert_eq!(&cents.to_string(), "42.00");
    }
}
