// Copyright 2016 John D. Hume
//
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// http://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.

use std::ops::{Add, Sub, Mul, Div, Rem, Neg};
use std::cmp::{Ordering, PartialOrd};
use std::i32;
use std::fmt;
use currency::Currency;
use money::Money;
use formatting::{self, FormattableMoney};

/// A 64-bit-sized, signed amount of money in a certain currency, with the currency's standard
/// number of decimal places.
///
/// The range of supported values is small enough to be inappropriate for some practical
/// applications. See `max` and `min` below.
///
///
/// ```
/// # use steel_cent::SmallMoney;
/// assert_eq!(8, std::mem::size_of::<SmallMoney>());
/// ```
///
/// # Examples
///
/// ```
/// # use steel_cent::SmallMoney;
/// # use steel_cent::currency::USD;
/// #
/// let price = SmallMoney::of_major_minor(USD, 19, 95);
/// let shipping_and_handling = SmallMoney::of_major(USD, 10);
/// let convenience_charge = SmallMoney::of_major(USD, 6);
/// let discount: f64 = 1.0 - 0.2; // 20% off
/// let discounted_price = price * discount;
/// let fees = shipping_and_handling + convenience_charge;
/// let total = discounted_price + fees;
/// println!("price: {:?}, discounted_price: {:?}", price, discounted_price);
/// assert_eq!(SmallMoney::of_minor(USD, 1596), discounted_price);
/// assert_eq!(SmallMoney::of_minor(USD, 3196), total);
/// assert_eq!((price * discount) + shipping_and_handling + convenience_charge, total);
/// ```
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub struct SmallMoney {
    pub currency: Currency,
    amount_minor: i32,
}

impl SmallMoney {
    /// Creates a SmallMoney from its "minor" unit (e.g. US cents to USD).
    pub fn of_minor(currency: Currency, amount_minor: i32) -> Self {
        SmallMoney {
            currency: currency,
            amount_minor: amount_minor,
        }
    }

    /// Creates a SmallMoney from its "major" unit (e.g. US dollars to USD).
    pub fn of_major(currency: Currency, amount_major: i32) -> Self {
        SmallMoney::of_minor(currency, currency.major_to_minor_i32(amount_major))
    }

    pub fn of_major_minor(currency: Currency, amount_major: i32, amount_minor: i32) -> Self {
        SmallMoney::of_minor(currency,
                             currency.major_to_minor_i32(amount_major) + amount_minor)
    }

    pub fn zero(currency: Currency) -> Self {
        SmallMoney::of_minor(currency, 0)
    }

    pub fn major_part(&self) -> i32 {
        self.currency.major_part_i32(self.amount_minor)
    }

    pub fn minor_part(&self) -> i32 {
        self.currency.minor_part_i32(self.amount_minor)
    }

    /// ```
    /// # use steel_cent::SmallMoney;
    /// # use steel_cent::currency::USD;
    /// assert_eq!(SmallMoney::of_major_minor(USD, -21_474_836, -48), SmallMoney::min(USD));
    /// ```
    pub fn min(currency: Currency) -> SmallMoney {
        SmallMoney::of_minor(currency, i32::MIN)
    }

    /// ```
    /// # use steel_cent::SmallMoney;
    /// # use steel_cent::currency::USD;
    /// assert_eq!(SmallMoney::of_major_minor(USD, 21_474_836, 47), SmallMoney::max(USD));
    /// ```
    pub fn max(currency: Currency) -> SmallMoney {
        SmallMoney::of_minor(currency, i32::MAX)
    }

    /// ```
    /// # use steel_cent::SmallMoney;
    /// # use steel_cent::currency::USD;
    /// let c = SmallMoney::of_major(USD, 100);
    /// assert_eq!(c, c.abs());
    /// assert_eq!(c, (-c).abs());
    /// ```
    pub fn abs(&self) -> Self {
        if self.amount_minor < 0 {
            SmallMoney::of_minor(self.currency, -self.amount_minor)
        } else {
            *self
        }
    }

    /// ```
    /// # use steel_cent::SmallMoney;
    /// # use steel_cent::currency::{GBP, JPY, USD};
    /// let five_usd = SmallMoney::of_major(USD, 5);
    /// let three_pound_seventy_five = SmallMoney::of_major_minor(GBP, 3, 75);
    /// let gbp_per_usd = 0.75;
    /// let five_eleven_yen = SmallMoney::of_major(JPY, 511);
    /// let jpy_per_usd = 102.15;
    /// assert_eq!(three_pound_seventy_five, five_usd.convert_to(GBP, gbp_per_usd));
    /// assert_eq!(five_usd, three_pound_seventy_five.convert_to(USD, gbp_per_usd.recip()));
    /// assert_eq!(510.75, 5.0 * jpy_per_usd); // but JPY has zero decimal places.
    /// assert_eq!(five_eleven_yen, five_usd.convert_to(JPY, jpy_per_usd)); // rounded.
    /// assert_eq!(five_usd, five_eleven_yen.convert_to(USD, jpy_per_usd.recip())); // rounded.
    /// ```
    pub fn convert_to(&self, currency: Currency, conversion_multiplier: f64) -> SmallMoney {
        let decimal_adjustment = currency.decimal_places() as i32 - self.currency.decimal_places() as i32;
        SmallMoney::of_minor(currency, (self.amount_minor as f64 * conversion_multiplier * 10.0f64.powi(decimal_adjustment)).round() as i32)
    }
}

/// Converts to `Money`.
///
/// ```
/// # use steel_cent::SmallMoney;
/// # use steel_cent::Money;
/// # use steel_cent::currency::*;
/// assert_eq!(Money::of_major(USD, 2),
///            Money::from(SmallMoney::of_major(USD, 2)));
/// ```
impl From<SmallMoney> for Money {
    fn from(small: SmallMoney) -> Self {
        Money::of_minor(small.currency, small.amount_minor as i64)
    }
}

/// Displays the value with `formatting::STYLE_GENERIC`. See the `formatting` module for other
/// pre-defined and custom styles.
///
/// ```
/// # use steel_cent::SmallMoney;
/// # use steel_cent::currency::*;
/// assert_eq!("1,234.56\u{a0}GBP", format!("{}", &SmallMoney::of_minor(GBP, 123456)));
/// ```
impl fmt::Display for SmallMoney {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", formatting::format(&*formatting::STYLE_GENERIC, self))
    }
}

impl FormattableMoney for SmallMoney {
    fn unformatted_minor_amount(&self) -> String {
        format!("{}", self.amount_minor)
    }

    fn currency(&self) -> Currency {
        self.currency
    }
}

impl Add for SmallMoney {
    type Output = SmallMoney;
    fn add(self, other: SmallMoney) -> SmallMoney {
        assert_eq!(self.currency, other.currency);
        SmallMoney::of_minor(self.currency, self.amount_minor + other.amount_minor)
    }
}

impl Sub for SmallMoney {
    type Output = SmallMoney;
    fn sub(self, other: SmallMoney) -> SmallMoney {
        assert_eq!(self.currency, other.currency);
        SmallMoney::of_minor(self.currency, self.amount_minor - other.amount_minor)
    }
}

impl Mul<i32> for SmallMoney {
    type Output = SmallMoney;
    fn mul(self, n: i32) -> SmallMoney {
        SmallMoney::of_minor(self.currency, self.amount_minor * n)
    }
}

/// Rounds if needed.
///
/// ```
/// # use steel_cent::SmallMoney;
/// # use steel_cent::currency::USD;
/// assert_eq!(SmallMoney::of_minor(USD, 101), SmallMoney::of_major(USD, 1) * 1.005001);
/// ```
impl Mul<f64> for SmallMoney {
    type Output = SmallMoney;
    fn mul(self, n: f64) -> SmallMoney {
        SmallMoney::of_minor(self.currency, (self.amount_minor as f64 * n).round() as i32)
    }
}

impl Div<i32> for SmallMoney {
    type Output = SmallMoney;
    fn div(self, n: i32) -> SmallMoney {
        SmallMoney::of_minor(self.currency, self.amount_minor / n)
    }
}

impl Rem<i32> for SmallMoney {
    type Output = SmallMoney;
    fn rem(self, n: i32) -> SmallMoney {
        SmallMoney::of_minor(self.currency, self.amount_minor % n)
    }
}

// Borrowed w/ modifications from https://doc.rust-lang.org/src/core/up/src/libcore/ops.rs.html
// implements binary operators "&T op U", "T op &U", "&T op &U"
// based on "T op U" where T and U are expected to be `Copy`able
macro_rules! forward_ref_binop {
    (impl $imp:ident, $method:ident for $t:ty, $u:ty) => {
        impl<'a> $imp<$u> for &'a $t {
            type Output = <$t as $imp<$u>>::Output;
            fn $method(self, other: $u) -> <$t as $imp<$u>>::Output {
                $imp::$method(*self, other)
            }
        }
        impl<'a> $imp<&'a $u> for $t {
            type Output = <$t as $imp<$u>>::Output;
            fn $method(self, other: &'a $u) -> <$t as $imp<$u>>::Output {
                $imp::$method(self, *other)
            }
        }
        impl<'a, 'b> $imp<&'a $u> for &'b $t {
            type Output = <$t as $imp<$u>>::Output;
            fn $method(self, other: &'a $u) -> <$t as $imp<$u>>::Output {
                $imp::$method(*self, *other)
            }
        }
    }
}

forward_ref_binop!{ impl Add, add for SmallMoney, SmallMoney }
forward_ref_binop!{ impl Sub, sub for SmallMoney, SmallMoney }
forward_ref_binop!{ impl Mul, mul for SmallMoney, i32 }
forward_ref_binop!{ impl Mul, mul for SmallMoney, f64 }
forward_ref_binop!{ impl Div, div for SmallMoney, i32 }
forward_ref_binop!{ impl Rem, rem for SmallMoney, i32 }

impl Neg for SmallMoney {
    type Output = SmallMoney;
    fn neg(self) -> SmallMoney {
        SmallMoney::of_minor(self.currency, -self.amount_minor)
    }
}

impl<'a> Neg for &'a SmallMoney {
    type Output = SmallMoney;
    fn neg(self) -> SmallMoney {
        SmallMoney::of_minor(self.currency, -self.amount_minor)
    }
}

/// Compares to `SmallMoney` of the same currency only.
///
/// Returns `None` when currencies differ, causing all comparison operators to return false when
/// currencies differ.
impl PartialOrd for SmallMoney {
    fn partial_cmp(&self, other: &SmallMoney) -> Option<Ordering> {
        if self.currency == other.currency {
            self.amount_minor.partial_cmp(&other.amount_minor)
        } else {
            None
        }
    }
}

#[cfg(test)]
mod tests {
    use currency::{EUR, JPY, USD};
    use super::SmallMoney;
    use std::cmp::Ordering;
    #[test]
    fn compares_equal() {
        let one_dollar = SmallMoney::of_minor(USD, 100);
        assert_eq!(one_dollar, SmallMoney::of_minor(USD, 100));
        assert!(one_dollar != SmallMoney::of_minor(USD, 101));
    }

    #[test]
    fn converts_major_to_minor() {
        assert_eq!(SmallMoney::of_minor(USD, 100), SmallMoney::of_major(USD, 1));
        assert_eq!(SmallMoney::of_minor(JPY, 100),
                   SmallMoney::of_major(JPY, 100));
    }

    #[test]
    fn exposes_minor_part() {
        assert_eq!(34, SmallMoney::of_minor(USD, 1234).minor_part());
        assert_eq!(-34, SmallMoney::of_minor(USD, -1234).minor_part());
        assert_eq!(0, SmallMoney::of_minor(JPY, 1234).minor_part());
        assert_eq!(0, SmallMoney::of_minor(JPY, -1234).minor_part());
    }

    #[test]
    fn exposes_major_part() {
        assert_eq!(12, SmallMoney::of_minor(USD, 1234).major_part());
        assert_eq!(-12, SmallMoney::of_minor(USD, -1234).major_part());
        assert_eq!(1234, SmallMoney::of_minor(JPY, 1234).major_part());
        assert_eq!(-1234, SmallMoney::of_minor(JPY, -1234).major_part());
    }

    // -----------------
    // Custom Ops impls

    #[test]
    fn adds_when_currencies_match() {
        assert_eq!(SmallMoney::of_minor(USD, 1234),
                   SmallMoney::of_major(USD, 12) + SmallMoney::of_minor(USD, 34))
    }

    #[test]
    #[should_panic]
    fn panics_on_add_when_currencies_differ() {
        SmallMoney::of_major(USD, 1) + SmallMoney::of_major(JPY, 1);
    }

    #[test]
    fn adds_all_combinations_of_ref_and_consumed() {
        let one = SmallMoney::of_major(USD, 1);
        let one_ref: &SmallMoney = &one;
        assert_eq!(SmallMoney::of_major(USD, 2), one + one_ref);
        assert_eq!(SmallMoney::of_major(USD, 2), one_ref + one);
        assert_eq!(SmallMoney::of_major(USD, 2), one_ref + one_ref);
    }

    #[test]
    fn subtracts_when_currencies_match() {
        assert_eq!(SmallMoney::of_minor(USD, 1200),
                   SmallMoney::of_minor(USD, 1234) - SmallMoney::of_minor(USD, 34))
    }

    #[test]
    #[should_panic]
    fn panics_on_subtract_when_currencies_differ() {
        SmallMoney::of_major(USD, 1) - SmallMoney::of_major(JPY, 1);
    }

    #[test]
    fn subtracts_refs() {
        let one = SmallMoney::of_major(USD, 1);
        assert_eq!(SmallMoney::of_major(USD, 0), &one - &one);
    }

    #[test]
    fn multiplies_by_int_or_float() {
        assert_eq!(SmallMoney::of_minor(USD, 400),
                   SmallMoney::of_minor(USD, 200) * 2);
        assert_eq!(SmallMoney::of_minor(USD, 180),
                   SmallMoney::of_minor(USD, 200) * 0.9);
    }

    #[test]
    fn multiplies_refs() {
        let one = SmallMoney::of_major(USD, 1);
        assert_eq!(SmallMoney::of_major(USD, 2), &one * &2);
        assert_eq!(SmallMoney::of_major(USD, 2), &one * &2.0);
    }

    #[test]
    fn divide_and_remainder_by_int() {
        assert_eq!(SmallMoney::of_minor(USD, 333),
                   SmallMoney::of_minor(USD, 1000) / 3);
        assert_eq!(SmallMoney::of_minor(USD, 1),
                   SmallMoney::of_minor(USD, 1000) % 3);
    }

    #[test]
    fn divide_and_remainder_with_refs() {
        assert_eq!(SmallMoney::of_minor(USD, 333),
                   &SmallMoney::of_minor(USD, 1000) / &3);
        assert_eq!(SmallMoney::of_minor(USD, 1),
                   &SmallMoney::of_minor(USD, 1000) % &3);
    }

    #[test]
    fn unary_negation() {
        let one_dollar = SmallMoney::of_major(USD, 1);
        assert_eq!(SmallMoney::of_major(USD, -1), -one_dollar);
        assert_eq!(SmallMoney::of_major(USD, -1), -&one_dollar);
    }

    // ----------
    // cmp

    #[test]
    fn compares_when_currencies_match() {
        let one_dollar = SmallMoney::of_major(USD, 1);
        let two_dollars = SmallMoney::of_major(USD, 2);
        assert_eq!(Some(Ordering::Less), one_dollar.partial_cmp(&two_dollars));
        assert_eq!(Some(Ordering::Equal), one_dollar.partial_cmp(&one_dollar));
        assert_eq!(Some(Ordering::Greater),
                   two_dollars.partial_cmp(&one_dollar));
        assert!(one_dollar < two_dollars);
        assert!(one_dollar <= two_dollars);
        assert!(one_dollar <= one_dollar);
        assert!(two_dollars > one_dollar);
        assert!(two_dollars >= two_dollars);
        assert!(two_dollars >= two_dollars);
    }

    #[test]
    fn does_not_compare_when_currencies_differ() {
        let one_dollar = SmallMoney::of_major(USD, 1);
        let one_euro = SmallMoney::of_major(EUR, 1);
        assert_eq!(None, one_dollar.partial_cmp(&one_euro));
        assert_eq!(None, one_euro.partial_cmp(&one_dollar));
        assert!(!(one_dollar < one_euro));
        assert!(!(one_euro < one_dollar));
        assert!(!(one_dollar <= one_euro));
        assert!(!(one_euro <= one_dollar));
        assert!(!(one_dollar > one_euro));
        assert!(!(one_euro > one_dollar));
        assert!(!(one_dollar >= one_euro));
        assert!(!(one_euro >= one_dollar));
    }
}
