// 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.

//! Support for formatting of monetary values.
//!
//! A style of formatting is described by a `FormatSpec`, which will wrap a `Money` or `SmallMoney`
//! in a `Display`-implementing struct for use in `format!`, `println!`, etc.

use currency::{self, Currency};
use std::collections::HashMap;
use std::fmt;

const NBSP: char = '\u{a0}';

/// Elements that can appear in a `FormatSpec` template.
///
/// New variants may be added, so please don't create exhaustive matches.
#[derive(PartialEq, Eq, Clone)]
pub enum FormatPart {
    Amount,
    CurrencySymbol,
    OptionalMinus,
    OptionalMinusOpenParenthesis,
    OptionalMinusCloseParenthesis,
    NonBreakingSpace,
}

/// A specification of a currency format.
#[derive(PartialEq, Eq, Clone)]
pub struct FormatSpec {
    thousands_separator: char,
    decimal_separator: char,
    short_currency_symbols: HashMap<Currency, String>,
    template: Vec<FormatPart>,
}

impl FormatSpec {
    /// Creates a new `FormatSpec` with no short symbol mappings.
    pub fn new(thousands_sep: char, decimal_sep: char, template: Vec<FormatPart>) -> FormatSpec {
        FormatSpec {
            thousands_separator: thousands_sep,
            decimal_separator: decimal_sep,
            short_currency_symbols: HashMap::new(),
            template: template,
        }
    }

    /// Creates a clone with the given short symbol mapping added.
    pub fn with_short_symbol(&self, currency: Currency, symbol: String) -> FormatSpec {
        let mut result = self.clone();
        result.short_currency_symbols.insert(currency, symbol);
        result
    }

    /// Creates a `Display` wrapper for the given money value using this spec.
    ///
    /// ```
    /// # use steel_cent::Money;
    /// # use steel_cent::currency::*;
    /// # use steel_cent::formatting::*;
    /// assert_eq!("$1.00", format!("{}", STYLE_US.display_for(&Money::of_major(USD, 1))));
    /// assert_eq!("GBP1.00", format!("{}", STYLE_US.display_for(&Money::of_major(GBP, 1))));
    /// ```
    pub fn display_for<'a, 'b, T: FormattableMoney>(&'a self, money: &'b T)
                                                    -> MoneyDisplay<'b, 'a, T> {
        MoneyDisplay { money: money, spec: self }
    }
}

lazy_static!{
    /// A generic readable style with no short symbol mappings.
    /// Example: `1,234.56 USD`
    pub static ref STYLE_GENERIC: FormatSpec = FormatSpec::new(
        ',', '.', vec![FormatPart::OptionalMinus,
                       FormatPart::Amount,
                       FormatPart::NonBreakingSpace,
                       FormatPart::CurrencySymbol]);

    pub static ref STYLE_FRANCE: FormatSpec = FormatSpec::new(
        NBSP, ',', vec![FormatPart::OptionalMinus,
                        FormatPart::Amount,
                        FormatPart::NonBreakingSpace,
                        FormatPart::CurrencySymbol])
        .with_short_symbol(currency::EUR, String::from("€"));

    pub static ref STYLE_UK: FormatSpec = FormatSpec::new(
        ',', '.', vec![FormatPart::OptionalMinus, FormatPart::CurrencySymbol, FormatPart::Amount])
        .with_short_symbol(currency::GBP, String::from("£"));

    pub static ref STYLE_US: FormatSpec = FormatSpec::new(
        ',', '.', vec![FormatPart::OptionalMinusOpenParenthesis,
                       FormatPart::CurrencySymbol,
                       FormatPart::Amount,
                       FormatPart::OptionalMinusCloseParenthesis])
        .with_short_symbol(currency::USD, String::from("$"));
}

pub trait FormattableMoney {
    fn unformatted_minor_amount(&self) -> String;
    fn currency(&self) -> Currency;
}

/// A `Display` wrapper for a monetary amount and a `FormatSpec`.
/// Obtained from `FormatSpec::display_for`.
pub struct MoneyDisplay<'a, 'b, T: 'a + FormattableMoney> {
    money: &'a T,
    spec: &'b FormatSpec,
}

impl<'a, 'b, T: FormattableMoney> fmt::Display for MoneyDisplay<'a, 'b, T> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", format(&self.spec, self.money))
    }
}

pub fn format<T: FormattableMoney>(spec: &FormatSpec, money: &T) -> String {
    let currency = money.currency();
    let unformatted_amount = money.unformatted_minor_amount();
    let negative = unformatted_amount.starts_with("-");
    let mut result = String::new();
    for part in spec.template.iter() {
        match *part {
            FormatPart::OptionalMinus => {
                if negative {
                    result.push('-');
                }
            }
            FormatPart::OptionalMinusOpenParenthesis => {
                if negative {
                    result.push('(');
                }
            }
            FormatPart::OptionalMinusCloseParenthesis => {
                if negative {
                    result.push(')');
                }
            }
            FormatPart::NonBreakingSpace => {
                result.push(NBSP);
            }
            FormatPart::Amount => {
                push_formatted_amount(&mut result,
                                      currency,
                                      if negative {
                                          &unformatted_amount[1..]
                                      } else {
                                          unformatted_amount.as_str()
                                      },
                                      spec);
            }
            FormatPart::CurrencySymbol => {
                result.push_str(spec.short_currency_symbols
                    .get(&currency)
                    .unwrap_or(&currency.code())
                    .as_str());
            }
        }
    }
    result
}

fn push_formatted_amount(result: &mut String,
                         currency: Currency,
                         amount: &str,
                         spec: &FormatSpec) {
    let decimal_places = currency.decimal_places() as usize;
    if amount.len() > decimal_places {
        let major_len = amount.len() - decimal_places;
        let mut index = major_len % 3;
        result.push_str(&amount[0..index]);
        while index < major_len {
            result.push(spec.thousands_separator);
            result.push_str(&amount[index..(index + 3)]);
            index += 3;
        }
        result.push(spec.decimal_separator);
        result.push_str(&amount[index..]);
    } else {
        result.push('0');
        result.push(spec.decimal_separator);
        for _ in 0..(decimal_places - amount.len()) {
            result.push('0');
        }
        result.push_str(amount);
    }
}

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

    #[test]
    fn positive_us_money_in_us_format() {
        assert_eq!("$0.01",
                   format(&STYLE_US, &Money::of_minor(currency::USD, 1)).as_str());
        assert_eq!("$0.10",
                   format(&STYLE_US, &Money::of_minor(currency::USD, 10)).as_str());
        assert_eq!("$1.00",
                   format(&STYLE_US, &Money::of_minor(currency::USD, 100)).as_str());
        assert_eq!("$12,345,678.90",
                   format(&STYLE_US, &Money::of_minor(currency::USD, 1234567890)).as_str());
    }

    #[test]
    fn negative_us_money_in_us_format() {
        assert_eq!("($0.01)",
                   format(&STYLE_US, &Money::of_minor(currency::USD, -1)).as_str());
        assert_eq!("($1,234.56)",
                   format(&STYLE_US, &Money::of_minor(currency::USD, -123456)).as_str());
    }

    #[test]
    fn uk_money_in_us_format() {
        assert_eq!("GBP1,234.56",
                   format(&STYLE_US, &Money::of_minor(currency::GBP, 123456)).as_str());
        assert_eq!("(GBP1,234.56)",
                   format(&STYLE_US, &Money::of_minor(currency::GBP, -123456)).as_str());
    }

    #[test]
    fn uk_money_in_uk_format() {
        assert_eq!("£1,234.56",
                   format(&STYLE_UK, &Money::of_minor(currency::GBP, 123456)).as_str());
        assert_eq!("-£1,234.56",
                   format(&STYLE_UK, &Money::of_minor(currency::GBP, -123456)).as_str());
    }

    #[test]
    fn us_money_in_uk_format() {
        assert_eq!("USD12,345,678.90",
                   format(&STYLE_UK, &Money::of_minor(currency::USD, 1234567890)).as_str());
        assert_eq!("-USD12,345,678.90",
                   format(&STYLE_UK, &Money::of_minor(currency::USD, -1234567890)).as_str());
    }

    #[test]
    fn fr_money_in_fr_format() {
        assert_eq!("12\u{a0}345\u{a0}678,90\u{a0}€",
                   format(&STYLE_FRANCE, &Money::of_minor(currency::EUR, 1234567890)).as_str());
        assert_eq!("-12\u{a0}345\u{a0}678,90\u{a0}€",
                   format(&STYLE_FRANCE, &Money::of_minor(currency::EUR, -1234567890)).as_str());
    }

    #[test]
    fn us_money_in_fr_format() {
        assert_eq!("12\u{a0}345\u{a0}678,90\u{a0}USD",
                   format(&STYLE_FRANCE, &Money::of_minor(currency::USD, 1234567890)).as_str());
        assert_eq!("-12\u{a0}345\u{a0}678,90\u{a0}USD",
                   format(&STYLE_FRANCE, &Money::of_minor(currency::USD, -1234567890)).as_str());
    }
}
