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

//! Besides defining the `Currency` struct, this module provides constants for most real-world
//! currencies as well as functions to look up a Currency by alphabetic or numeric code.

use base26;
use std::collections::HashMap;

/// A monetary currency with text and numeric codes and a standard number of decimal places,
/// as per [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217).
///
/// Three-letter currency codes are encoded in a `u16`, which is how this type can be small and
/// `Copy`.
///
/// ```
/// # use steel_cent::currency::*;
/// assert_eq!(4, std::mem::size_of::<Currency>());
/// ```
#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)]
pub struct Currency {
    code_base26: u16,
    numeric_code_and_decimal_places: u16,
}

impl Currency {
    /// Create a currency w/ the given 3-character code, 0-999 numeric code, and 0-8 decimal places.
    /// Panics if any arg violates those constraints. This function has no connection to the
    /// global currency maps used by lookup functions, so creating a custom currency won't cause it
    /// to show up in look-ups.
    pub fn new(code: &str, numeric_code: u16, decimal_places: u8) -> Self {
        assert!(numeric_code < 1000,
                "Currency numeric code must be less than 1000");
        assert!(decimal_places < 9,
                "Currency decimal places must be less than 9");
        Currency {
            code_base26: base26::code_to_base26(code),
            numeric_code_and_decimal_places: (numeric_code * 10) + (decimal_places as u16),
        }
    }

    /// ```
    /// # use steel_cent::currency::*;
    /// assert_eq!("AUD", AUD.code());
    /// assert_eq!("CAD", CAD.code());
    /// assert_eq!("CHF", CHF.code());
    /// assert_eq!("EUR", EUR.code());
    /// assert_eq!("GBP", GBP.code());
    /// assert_eq!("JPY", JPY.code());
    /// assert_eq!("USD", USD.code());
    /// assert_eq!("XTS", Currency::new("XTS", 999, 2).code());
    /// ```
    pub fn code(&self) -> String {
        base26::base26_to_code(self.code_base26)
    }

    /// ```
    /// # use steel_cent::currency::*;
    /// assert_eq!(840, USD.numeric_code());
    /// assert_eq!(392, JPY.numeric_code());
    /// ```
    pub fn numeric_code(&self) -> u16 {
        self.numeric_code_and_decimal_places / 10
    }

    /// ```
    /// # use steel_cent::currency::*;
    /// assert_eq!(2, USD.decimal_places());
    /// assert_eq!(0, JPY.decimal_places());
    /// ```
    pub fn decimal_places(&self) -> u8 {
        (self.numeric_code_and_decimal_places % 10) as u8
    }

    /// Converts a "major" amount to a "minor" amount in this currency.
    ///
    /// ```
    /// # use steel_cent::currency::*;
    /// assert_eq!(100, USD.major_to_minor(1));
    /// assert_eq!(100, JPY.major_to_minor(100))
    /// ```
    pub fn major_to_minor(&self, major: i64) -> i64 {
        major * self.multiplier()
    }

    /// ```
    /// # use steel_cent::currency::*;
    /// assert_eq!(100, USD.major_to_minor_i32(1));
    /// assert_eq!(100, JPY.major_to_minor_i32(100))
    /// ```
    pub fn major_to_minor_i32(&self, major: i32) -> i32 {
        major * self.multiplier() as i32
    }

    /// Returns the major part of a minor amount.
    ///
    /// ```
    /// # use steel_cent::currency::*;
    /// assert_eq!(12, USD.major_part(1234));
    /// assert_eq!(1234, JPY.major_part(1234));
    /// ```
    pub fn major_part(&self, minor_amount: i64) -> i64 {
        minor_amount / self.multiplier()
    }
    /// ```
    /// # use steel_cent::currency::*;
    /// assert_eq!(12, USD.major_part_i32(1234));
    /// assert_eq!(1234, JPY.major_part_i32(1234));
    /// ```
    pub fn major_part_i32(&self, minor_amount: i32) -> i32 {
        minor_amount / self.multiplier() as i32
    }

    /// Returns the minor part of a minor amount.
    ///
    /// ```
    /// # use steel_cent::currency::*;
    /// assert_eq!(34, USD.minor_part(1234));
    /// assert_eq!(0, JPY.minor_part(1234));
    /// ```
    pub fn minor_part(&self, minor_amount: i64) -> i64 {
        minor_amount % self.multiplier()
    }
    /// ```
    /// # use steel_cent::currency::*;
    /// assert_eq!(34, USD.minor_part_i32(1234));
    /// assert_eq!(0, JPY.minor_part_i32(1234));
    /// ```
    pub fn minor_part_i32(&self, minor_amount: i32) -> i32 {
        minor_amount % self.multiplier() as i32
    }

    fn multiplier(&self) -> i64 {
        match self.decimal_places() {
            0 => 1,
            1 => 10,
            2 => 100,
            3 => 1000,
            places => 10i64.pow(places as u32),
        }
    }
}

include!(concat!(env!("OUT_DIR"), "/generated_currency_data"));

/// Looks up a "standard" currency by its three-letter code.
///
/// ```
/// # use steel_cent::currency::*;
/// assert_eq!(Some(USD), with_code("USD"));
/// assert_eq!(Some(GBP), with_code("GBP"));
/// assert_eq!(None, with_code("WRONG"));
/// ```
pub fn with_code(code: &str) -> Option<Currency> {
    CURRENCIES_BY_CODE.get(code).map(|c| *c)
}

/// Looks up a "standard" currency by its numeric code.
///
/// ```
/// # use steel_cent::currency::*;
/// assert_eq!(Some(USD), with_numeric_code(&840));
/// assert_eq!(Some(GBP), with_numeric_code(&826));
/// assert_eq!(None, with_numeric_code(&1000));
/// ```
pub fn with_numeric_code(code: &u16) -> Option<Currency> {
    CURRENCIES_BY_NUMERIC_CODE.get(code).map(|c| *c)
}

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

    #[test]
    fn currency_equality() {
        assert_eq!(USD, USD);
        assert_eq!(USD, Currency::new("USD", 840, 2));
        assert!(USD != Currency::new("USD", 841, 2));
        assert!(USD != Currency::new("USD", 840, 3));
        assert!(USD != AUD);
        assert!(AUD != CAD);
        assert!(CHF != EUR);
        assert!(GBP != JPY);
    }

    #[test]
    fn converts_major_amount_to_minor_amount_with_silly_decimal_places() {
        assert_eq!(1000000, Currency::new("XTS", 0, 6).major_to_minor(1));
    }
}
