// The ISO 4217 currency value and the lookups over the generated table.
//
// A `Currency` is a plain immutable value: copy it, compare it, store it. Two
// currencies are equal when every field is equal, and `same_as` compares the
// alphabetic code alone, which is the test money arithmetic uses.

///|
pub(all) struct Currency {
  /// ISO 4217 alphabetic code, upper case, e.g. `USD`.
  code : String
  /// ISO 4217 numeric code, or 0 for a currency built with `custom`.
  numeric : Int32
  /// ISO 4217 minor units: the number of decimal places an amount is stored with.
  exponent : Int32
  /// The number of decimal places CLDR recommends for display, which is at most
  /// `exponent` and is smaller for currencies whose smallest unit has fallen out
  /// of use.
  display_digits : Int32
  /// The decimal places a cash payment is settled in.
  cash_exponent : Int32
  /// The cash rounding step, in units of `10^-cash_exponent`.
  cash_step : Int32
  /// English name.
  name : String
  /// CLDR symbol, empty when the currency has none.
  symbol : String
} derive(Eq, Debug)

///|
pub extend Currency with Eq::{not_equal, equal}

///|
pub extend Currency with @debug.Debug::{to_repr}

///|
/// Ten to the power of `exponent`, or `None` when the result would not fit an
/// `Int64`. This bounds every exponent in the library at 18.
pub fn pow10(exponent : Int32) -> Int64? {
  if exponent < 0 || exponent > 18 {
    return None
  }
  let mut result = 1L
  let mut index = 0
  while index < exponent {
    result = result * 10L
    index += 1
  }
  Some(result)
}

///|
/// How many minor units make one major unit: 100 for USD, 1 for JPY.
pub fn Currency::scale(self : Currency) -> Int64 {
  pow10(self.exponent).unwrap_or(1L)
}

///|
/// The step a cash payment is rounded to, in minor units.
///
/// CHF has an exponent of 2 and a cash step of 5, so this is 5 (0.05 franc).
/// DKK settles to 0.50, so this is 50. A currency without a separate cash rule
/// reports 1.
pub fn Currency::cash_increment(self : Currency) -> Int64 {
  let step = if self.cash_step < 1 { 1L } else { self.cash_step.to_int64() }
  let shift = self.exponent - self.cash_exponent
  if shift <= 0 {
    return step
  }
  step * pow10(shift).unwrap_or(1L)
}

///|
/// True when a cash payment is settled in larger steps than the minor unit.
pub fn Currency::has_cash_rounding(self : Currency) -> Bool {
  self.cash_exponent != self.exponent || self.cash_step > 1
}

///|
/// Identity of a currency for arithmetic: the alphabetic code alone.
pub fn Currency::same_as(self : Currency, other : Currency) -> Bool {
  self.code == other.code
}

///|
/// The symbol, falling back to the code when the currency has none.
pub fn Currency::display_symbol(self : Currency) -> String {
  if self.symbol == "" {
    self.code
  } else {
    self.symbol
  }
}

///|
/// `USD (US Dollar)`.
pub fn Currency::label(self : Currency) -> String {
  "\{self.code} (\{self.name})"
}

///|
/// Builds a currency that is not in the ISO 4217 table, for private or
/// historical codes and for the ISO codes that carry no minor unit.
///
/// `None` when `exponent` is outside `0..=18`, the range an `Int64` can
/// scale exactly. The code is upper-cased so lookups and comparisons stay stable.
pub fn Currency::custom(
  code : String,
  exponent : Int32,
  symbol~ : String = "",
) -> Currency? {
  match pow10(exponent) {
    None => None
    Some(_) => {
      let upper = code.to_upper()
      Some({
        code: upper,
        numeric: 0,
        exponent,
        display_digits: exponent,
        cash_exponent: exponent,
        cash_step: 1,
        name: upper,
        symbol,
      })
    }
  }
}

///|
/// Index over the table, built once at start-up.
let by_code : Map[String, Currency] = {
  let index : Map[String, Currency] = Map([])
  for entry in table {
    index[entry.code] = entry
  }
  index
}

///|
/// Looks an ISO 4217 currency up by its alphabetic code, ignoring case.
pub fn Currency::find(code : StringView) -> Currency? {
  by_code.get(code.to_upper().to_owned())
}

///|
/// Every currency in the registry, in code order.
pub fn Currency::all() -> Array[Currency] {
  table.copy()
}

///|
/// The alphabetic codes of the registry, in order.
pub fn Currency::known_codes() -> Array[String] {
  let codes : Array[String] = []
  for entry in table {
    codes.push(entry.code)
  }
  codes
}

///|
/// How many currencies the registry holds.
pub fn Currency::count() -> Int32 {
  table.length()
}

///|
/// Every currency whose code or symbol matches `token`.
///
/// The parser uses this to tell an unambiguous symbol such as `€` from an
/// ambiguous one such as `$`, which several dollar currencies share.
pub fn Currency::by_token(token : StringView) -> Array[Currency] {
  let upper = token.to_upper()
  let matches : Array[Currency] = []
  for entry in table {
    let same_code = entry.code.view() == upper
    let same_symbol = entry.symbol != "" && entry.symbol.view() == token
    if same_code || same_symbol {
      matches.push(entry)
    }
  }
  matches
}