// 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::i64;
use std::fmt;
use currency::Currency;
use formatting::{self, FormattableMoney};

/// A signed amount of money in a certain currency, with the currency's standard number of decimal
/// places.
///
/// ```
/// # use steel_cent::Money;
/// assert_eq!(16, std::mem::size_of::<Money>());
/// ```
///
/// # Examples
///
/// ```
/// # use steel_cent::Money;
/// # use steel_cent::currency::USD;
/// #
/// let price = Money::of_major_minor(USD, 19, 95);
/// let shipping_and_handling = Money::of_major(USD, 10);
/// let convenience_charge = Money::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!(Money::of_minor(USD, 1596), discounted_price);
/// assert_eq!(Money::of_minor(USD, 3196), total);
/// assert_eq!((price * discount) + shipping_and_handling + convenience_charge, total);
/// ```
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub struct Money {
    pub currency: Currency,
    amount_minor: i64,
}

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

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

    pub fn of_major_minor(currency: Currency, amount_major: i64, amount_minor: i64) -> Self {
        Money::of_minor(currency,
                        currency.major_to_minor(amount_major) + amount_minor)
    }

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

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

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

    /// ```
    /// # use steel_cent::Money;
    /// # use steel_cent::currency::USD;
    /// assert_eq!(Money::of_major_minor(USD, -92_233_720_368_547_758, -08), Money::min(USD));
    /// ```
    pub fn min(currency: Currency) -> Money {
        Money::of_minor(currency, i64::MIN)
    }

    /// ```
    /// # use steel_cent::Money;
    /// # use steel_cent::currency::USD;
    /// assert_eq!(Money::of_major_minor(USD, 92_233_720_368_547_758, 07), Money::max(USD));
    /// ```
    pub fn max(currency: Currency) -> Money {
        Money::of_minor(currency, i64::MAX)
    }

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

    /// ```
    /// # use steel_cent::Money;
    /// # use steel_cent::currency::{GBP, JPY, USD};
    /// let five_usd = Money::of_major(USD, 5);
    /// let three_pound_seventy_five = Money::of_major_minor(GBP, 3, 75);
    /// let gbp_per_usd = 0.75;
    /// let five_eleven_yen = Money::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) -> Money {
        let decimal_adjustment = currency.decimal_places() as i32 - self.currency.decimal_places() as i32;
        Money::of_minor(currency, (self.amount_minor as f64 * conversion_multiplier * 10.0f64.powi(decimal_adjustment)).round() as i64)
    }
}

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

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

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

/// ```
/// # use steel_cent::Money;
/// # use steel_cent::currency::*;
/// let one = Money::of_major(USD, 1);
/// assert_eq!(Money::of_major(USD, 2), one.clone() + one);
/// ```
///
/// Attempting to add moneys of different currencies will panic.
///
/// ```should_panic
/// # use steel_cent::Money;
/// # use steel_cent::currency::*;
/// Money::of_major(USD, 1) + Money::of_major(JPY, 1); // panics!
/// ```
impl Add for Money {
    type Output = Money;
    fn add(self, other: Money) -> Money {
        assert_eq!(self.currency, other.currency);
        Money::of_minor(self.currency, self.amount_minor + other.amount_minor)
    }
}

/// ```
/// # use steel_cent::Money;
/// # use steel_cent::currency::*;
/// let one = Money::of_major(USD, 1);
/// assert_eq!(Money::of_major(USD, 2), one.clone() + &one);
/// ```
///
/// Attempting to add moneys of different currencies will panic.
///
/// ```should_panic
/// # use steel_cent::Money;
/// # use steel_cent::currency::*;
/// Money::of_major(USD, 1) + &Money::of_major(JPY, 1); // panics!
/// ```
impl<'b> Add<&'b Money> for Money {
    type Output = Money;
    fn add(self, other: &'b Money) -> Money {
        assert_eq!(self.currency, other.currency);
        Money::of_minor(self.currency, self.amount_minor + other.amount_minor)
    }
}

/// ```
/// # use steel_cent::Money;
/// # use steel_cent::currency::*;
/// let one = Money::of_major(USD, 1);
/// assert_eq!(Money::of_major(USD, 2), &one + one.clone());
/// ```
///
/// Attempting to add moneys of different currencies will panic.
///
/// ```should_panic
/// # use steel_cent::Money;
/// # use steel_cent::currency::*;
/// &Money::of_major(USD, 1) + Money::of_major(JPY, 1); // panics!
/// ```
impl<'a> Add<Money> for &'a Money {
    type Output = Money;
    fn add(self, other: Money) -> Money {
        assert_eq!(self.currency, other.currency);
        Money::of_minor(self.currency, self.amount_minor + other.amount_minor)
    }
}

/// ```
/// # use steel_cent::Money;
/// # use steel_cent::currency::*;
/// let one = Money::of_major(USD, 1);
/// assert_eq!(Money::of_major(USD, 2), &one + &one);
/// ```
///
/// Attempting to add moneys of different currencies will panic.
///
/// ```should_panic
/// # use steel_cent::Money;
/// # use steel_cent::currency::*;
/// &Money::of_major(USD, 1) + &Money::of_major(JPY, 1); // panics!
/// ```
impl<'a, 'b> Add<&'b Money> for &'a Money {
    type Output = Money;
    fn add(self, other: &'b Money) -> Money {
        assert_eq!(self.currency, other.currency);
        Money::of_minor(self.currency, self.amount_minor + other.amount_minor)
    }
}

/// ```
/// # use steel_cent::Money;
/// # use steel_cent::currency::*;
/// let one = Money::of_major(USD, 1);
/// assert_eq!(Money::of_major(USD, 0), one.clone() - one);
/// ```
///
/// Attempting to subtract moneys of different currencies will panic.
///
/// ```should_panic
/// # use steel_cent::Money;
/// # use steel_cent::currency::*;
/// Money::of_major(USD, 1) - Money::of_major(JPY, 1); // panics!
/// ```
impl Sub for Money {
    type Output = Money;
    fn sub(self, other: Money) -> Money {
        assert_eq!(self.currency, other.currency);
        Money::of_minor(self.currency, self.amount_minor - other.amount_minor)
    }
}

/// ```
/// # use steel_cent::Money;
/// # use steel_cent::currency::*;
/// let one = Money::of_major(USD, 1);
/// assert_eq!(Money::of_major(USD, 0), one.clone() - &one);
/// ```
///
/// Attempting to subtract moneys of different currencies will panic.
///
/// ```should_panic
/// # use steel_cent::Money;
/// # use steel_cent::currency::*;
/// Money::of_major(USD, 1) - &Money::of_major(JPY, 1); // panics!
/// ```
impl<'b> Sub<&'b Money> for Money {
    type Output = Money;
    fn sub(self, other: &'b Money) -> Money {
        assert_eq!(self.currency, other.currency);
        Money::of_minor(self.currency, self.amount_minor - other.amount_minor)
    }
}

/// ```
/// # use steel_cent::Money;
/// # use steel_cent::currency::*;
/// let one = Money::of_major(USD, 1);
/// assert_eq!(Money::of_major(USD, 0), &one - one.clone());
/// ```
///
/// Attempting to subtract moneys of different currencies will panic.
///
/// ```should_panic
/// # use steel_cent::Money;
/// # use steel_cent::currency::*;
/// &Money::of_major(USD, 1) - Money::of_major(JPY, 1); // panics!
/// ```
impl<'a> Sub<Money> for &'a Money {
    type Output = Money;
    fn sub(self, other: Money) -> Money {
        assert_eq!(self.currency, other.currency);
        Money::of_minor(self.currency, self.amount_minor - other.amount_minor)
    }
}

/// ```
/// # use steel_cent::Money;
/// # use steel_cent::currency::*;
/// let one = Money::of_major(USD, 1);
/// assert_eq!(Money::of_major(USD, 0), &one - &one);
/// ```
///
/// Attempting to subtract moneys of different currencies will panic.
///
/// ```should_panic
/// # use steel_cent::Money;
/// # use steel_cent::currency::*;
/// &Money::of_major(USD, 1) - &Money::of_major(JPY, 1); // panics!
/// ```
impl<'a, 'b> Sub<&'b Money> for &'a Money {
    type Output = Money;
    fn sub(self, other: &Money) -> Money {
        assert_eq!(self.currency, other.currency);
        Money::of_minor(self.currency, self.amount_minor - other.amount_minor)
    }
}

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

impl<'b> Mul<&'b i64> for Money {
    type Output = Money;
    fn mul(self, n: &i64) -> Money {
        Money::of_minor(self.currency, self.amount_minor * n)
    }
}

impl<'a> Mul<i64> for &'a Money {
    type Output = Money;
    fn mul(self, n: i64) -> Money {
        Money::of_minor(self.currency, self.amount_minor * n)
    }
}

impl<'a, 'b> Mul<&'b i64> for &'a Money {
    type Output = Money;
    fn mul(self, n: &i64) -> Money {
        Money::of_minor(self.currency, self.amount_minor * n)
    }
}

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

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

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

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

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

impl<'b> Div<&'b i64> for Money {
    type Output = Money;
    fn div(self, n: &i64) -> Money {
        Money::of_minor(self.currency, self.amount_minor / n)
    }
}

impl<'a> Div<i64> for &'a Money {
    type Output = Money;
    fn div(self, n: i64) -> Money {
        Money::of_minor(self.currency, self.amount_minor / n)
    }
}

impl<'a, 'b> Div<&'b i64> for &'a Money {
    type Output = Money;
    fn div(self, n: &i64) -> Money {
        Money::of_minor(self.currency, self.amount_minor / n)
    }
}

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

impl<'b> Rem<&'b i64> for Money {
    type Output = Money;
    fn rem(self, n: &i64) -> Money {
        Money::of_minor(self.currency, self.amount_minor % n)
    }
}

impl<'a> Rem<i64> for &'a Money {
    type Output = Money;
    fn rem(self, n: i64) -> Money {
        Money::of_minor(self.currency, self.amount_minor % n)
    }
}

impl<'a, 'b> Rem<&'b i64> for &'a Money {
    type Output = Money;
    fn rem(self, n: &i64) -> Money {
        Money::of_minor(self.currency, self.amount_minor % n)
    }
}

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

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

/// Compares to `Money` of the same currency only.
///
/// Returns `None` when currencies differ, causing all comparison operators to return false when
/// currencies differ.
impl PartialOrd for Money {
    fn partial_cmp(&self, other: &Money) -> 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::Money;
    use std::cmp::Ordering;

    #[test]
    fn compares_equal() {
        let one_dollar = Money::of_minor(USD, 100);
        assert_eq!(one_dollar, Money::of_minor(USD, 100));
        assert!(one_dollar != Money::of_minor(USD, 101));
    }

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

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

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

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

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

    #[test]
    fn multiplies_all_combinations_of_ref_and_consumed() {
        let two_usd = Money::of_major(USD, 2);
        assert_eq!(Money::of_major(USD, 4), two_usd.clone() * 2);
        assert_eq!(Money::of_major(USD, 1), two_usd.clone() * 0.5);
        assert_eq!(Money::of_major(USD, 4), two_usd.clone() * &2);
        assert_eq!(Money::of_major(USD, 1), two_usd.clone() * &0.5);
        assert_eq!(Money::of_major(USD, 4), &two_usd * 2);
        assert_eq!(Money::of_major(USD, 1), &two_usd * 0.5);
        assert_eq!(Money::of_major(USD, 4), &two_usd * &2);
        assert_eq!(Money::of_major(USD, 1), &two_usd * &0.5);
    }

    #[test]
    fn divide_and_remainder_by_int() {
        let ten_usd = Money::of_major(USD, 10);
        let three_thirty_three_usd = Money::of_minor(USD, 333);
        let one_cent = Money::of_minor(USD, 1);
        assert_eq!(three_thirty_three_usd, ten_usd.clone() / 3);
        assert_eq!(three_thirty_three_usd, ten_usd.clone() / &3);
        assert_eq!(three_thirty_three_usd, &ten_usd / 3);
        assert_eq!(three_thirty_three_usd, &ten_usd / &3);
        assert_eq!(one_cent, ten_usd.clone() % 3);
        assert_eq!(one_cent, ten_usd.clone() % &3);
        assert_eq!(one_cent, &ten_usd % 3);
        assert_eq!(one_cent, &ten_usd % &3);
    }

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

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

    #[test]
    fn compares_when_currencies_match() {
        let one_dollar = Money::of_major(USD, 1);
        let two_dollars = Money::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 = Money::of_major(USD, 1);
        let one_euro = Money::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));
    }
}
