// SPDX-FileCopyrightText: 2023 - 2024 Markus Haug (Korrat)
//
// SPDX-License-Identifier: EUPL-1.2


use core::cmp::Ordering;
use core::fmt;
use core::fmt::Display;
use core::fmt::Formatter;
use core::hash::Hash;
use core::hash::Hasher;
use core::ops::Add;
use core::ops::AddAssign;
use core::ops::Div;
use core::ops::DivAssign;
use core::ops::Mul;
use core::ops::MulAssign;
use core::ops::Neg;
use core::ops::Sub;
use core::ops::SubAssign;
use core::str::FromStr;

use beancount_commodity::Commodity;
use miette::Diagnostic;
use rust_decimal::Decimal;
use snafu::Backtrace;
use snafu::OptionExt as _;
use snafu::Snafu;

macro_rules! forward_commutative_binop {
    (impl $trait:ident<$ltype:ty> for $rtype:ty { $fn:ident }) => {
        forward_commutative_binop!(@single<'l> $trait, $fn, &'l $ltype, &$rtype);
        forward_commutative_binop!(@single<'l> $trait, $fn, &'l $ltype, $rtype);
        forward_commutative_binop!(@single $trait, $fn, $ltype, &$rtype);
        forward_commutative_binop!(@single $trait, $fn, $ltype, $rtype);
    };
    (@single$(<$lt:lifetime>)? $trait:ident, $fn:ident, $ltype:ty, $rtype:ty) => {
        impl$(<$lt>)? $trait<$ltype> for $rtype {
            type Output = <$ltype as $trait<Self>>::Output;

            #[inline]
            fn $fn(self, rhs: $ltype) -> Self::Output {
                rhs.$fn(self)
            }
        }
    };
}

macro_rules! implement_binop {
    (impl $trait:ident<$rtype:ty> for $ltype:ty { fn $method:ident($self:ident, $rhs:ident) $body:block }) => {
        implement_binop! {@single
            impl $trait<&'_ $rtype> for &'_ $ltype {
                fn $method($self, $rhs) -> $ltype {
                    (*$self).$method($rhs)
                }
            }
        }

        implement_binop! {@single
            impl $trait<&'_ $rtype> for $ltype {
                fn $method($self, $rhs) -> $ltype $body
            }
        }

        implement_binop! {@single
            impl $trait<$rtype> for &'_ $ltype {
                fn $method($self, $rhs) -> $ltype {
                    $self.$method(&$rhs)
                }
            }
        }

        implement_binop! {@single
            impl $trait<$rtype> for $ltype {
                fn $method($self, $rhs) -> $ltype {
                    $self.$method(&$rhs)
                }
            }
        }
    };
    (@single impl $trait:ident<$rtype:ty> for $ltype:ty { fn $method:ident($self:ident, $rhs:ident) -> $output:ty $body:block }) => {
        impl $trait<$rtype> for $ltype {
            type Output = $output;

            #[inline]
            fn $method($self, $rhs: $rtype) -> Self::Output $body
        }
    };
}

macro_rules! implement_binop_assign {
    (impl $trait:ident<$rtype:ty> for $ltype:ty { fn $method:ident($self:ident, $rhs:ident) $body:block }) => {
        implement_binop_assign! {@single
            impl $trait<&'_ $rtype> for $ltype {
                fn $method($self, $rhs) $body
            }
        }
        implement_binop_assign!(@single impl $trait<$rtype> for $ltype { fn $method($self, $rhs) { $self.$method(&$rhs)} });
    };
    (@single impl $trait:ident<$rtype:ty> for $ltype:ty { fn $method:ident($self:ident, $rhs:ident) $body:block }) => {
        impl $trait<$rtype> for $ltype {
            #[inline]
            fn $method(&mut $self, $rhs: $rtype) $body
        }
    };
}

macro_rules! implement_binop_complete {
    (@com impl $assign_trait:ident<$rtype:ty> for $ltype:ty { fn $assign_method:ident($self:ident, $rhs:ident) $body:block } .. $op_trait:ident { $op_method:ident }) => {
        forward_commutative_binop! {
            impl $op_trait<$ltype> for $rtype { $op_method }
        }
        implement_binop_complete!(impl $assign_trait<$rtype> for $ltype { fn $assign_method($self, $rhs) $body } .. $op_trait { $op_method });
    };
    (impl $assign_trait:ident<$rtype:ty> for $ltype:ty { fn $assign_method:ident($self:ident, $rhs:ident) $body:block } .. $op_trait:ident { $op_method:ident }) => {
        implement_binop! {
            impl $op_trait<$rtype> for $ltype {
                fn $op_method($self, $rhs) {
                    let mut this = $self;
                    this.$assign_method($rhs);
                    this
                }
            }
        }

        implement_binop_assign! {
            impl $assign_trait<$rtype> for $ltype {
                fn $assign_method($self, $rhs) $body
            }
        }
    };
}

#[derive(Clone, Copy, Debug, Eq)]
pub struct Amount {
    pub amount: Decimal,

    pub commodity: Commodity,
}

impl Amount {
    #[must_use]
    pub const fn new(amount: Decimal, commodity: Commodity) -> Self {
        Self { amount, commodity }
    }
}

impl Amount {
    #[inline]
    pub fn checked_add(self, rhs: Self) -> Option<Self> {
        self.try_reduce(rhs, Decimal::checked_add)
    }

    #[inline]
    pub fn checked_div(self, rhs: Self) -> Option<Self> {
        self.try_reduce(rhs, Decimal::checked_div)
    }

    #[inline]
    pub fn checked_mul(self, rhs: Self) -> Option<Self> {
        self.try_reduce(rhs, Decimal::checked_mul)
    }

    #[inline]
    pub fn checked_sub(self, rhs: Self) -> Option<Self> {
        self.try_reduce(rhs, Decimal::checked_sub)
    }

    #[inline]
    pub fn saturating_add(self, rhs: Self) -> Option<Self> {
        self.reduce(rhs, Decimal::saturating_add)
    }

    #[inline]
    pub fn saturating_mul(self, rhs: Self) -> Option<Self> {
        self.reduce(rhs, Decimal::saturating_mul)
    }

    #[inline]
    pub fn saturating_sub(self, rhs: Self) -> Option<Self> {
        self.reduce(rhs, Decimal::saturating_sub)
    }
}

impl Amount {
    #[inline]
    fn reduce(self, rhs: Self, f: impl FnOnce(Decimal, Decimal) -> Decimal) -> Option<Self> {
        self.try_reduce(rhs, |lhs, rhs| Some(f(lhs, rhs)))
    }

    #[inline]
    fn try_reduce(
        self,
        rhs: Self,
        f: impl FnOnce(Decimal, Decimal) -> Option<Decimal>,
    ) -> Option<Self> {
        (self.commodity == rhs.commodity)
            .then_some((self.amount, rhs.amount))
            .and_then(|(lhs, rhs)| f(lhs, rhs))
            .map(|amount| Self { amount, ..self })
    }
}

implement_binop_complete! {
    impl AddAssign<Amount> for Amount {
        fn add_assign(self, rhs) {
            assert!(self.commodity == rhs.commodity);
            self.amount.add_assign(rhs.amount);
        }
    }
    .. Add { add }
}

impl Display for Amount {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        let Self { amount, commodity } = self;
        write!(f, "{amount} {commodity}")
    }
}

implement_binop_complete! {
    impl DivAssign<Decimal> for Amount {
        fn div_assign(self, rhs) {
            self.amount.div_assign(rhs);
        }
    }
    .. Div { div }
}

impl FromStr for Amount {
    type Err = <Self as TryFrom<&'static str>>::Error;

    fn from_str(amount: &str) -> Result<Self, Self::Err> {
        Self::try_from(amount)
    }
}

impl Hash for Amount {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.commodity.hash(state);
        self.amount.hash(state);
    }
}

implement_binop_complete! {
    @com
    impl MulAssign<Decimal> for Amount {
        fn mul_assign(self, rhs) {
            self.amount.mul_assign(rhs);
        }
    }
    .. Mul { mul }
}

impl Neg for &Amount {
    type Output = Amount;

    #[inline]
    fn neg(self) -> Self::Output {
        (*self).neg()
    }
}

impl Neg for Amount {
    type Output = Self;

    #[inline]
    fn neg(self) -> Self::Output {
        let amount = self.amount.neg();
        Self { amount, ..self }
    }
}

impl PartialEq for Amount {
    fn eq(&self, other: &Self) -> bool {
        self.partial_cmp(other).is_some_and(Ordering::is_eq)
    }
}

impl PartialOrd for Amount {
    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
        (self.commodity == other.commodity).then(|| self.amount.cmp(&other.amount))
    }
}

implement_binop_complete! {
    impl SubAssign<Amount> for Amount {
        fn sub_assign(self, rhs) {
            assert!(self.commodity == rhs.commodity);
            self.amount.sub_assign(rhs.amount);
        }
    }
    .. Sub { sub }
}

impl TryFrom<&str> for Amount {
    type Error = AmountError;

    fn try_from(amount: &str) -> Result<Self, Self::Error> {
        let context = AmountSnafu { value: amount };
        let (amount, commodity) = amount.split_once(' ').context(context)?;

        let amount = amount.parse().map_err(|_| context.build())?;
        let commodity = commodity.parse().map_err(|_| context.build())?;

        Ok(Self { amount, commodity })
    }
}

#[derive(Debug, Diagnostic, Snafu)]
pub struct AmountError {
    value: String,

    backtrace: Backtrace,
}