///|
/// A base-10 value represented without binary floating-point rounding.
///
/// `Decimal::new(12345, 2)` represents `123.45`. Trailing fractional zeroes
/// are normalized so equality is based on the numeric value.
pub struct Decimal {
  coefficient : Int
  scale : Int
} derive(Eq, Debug)

///|
pub fn Decimal::new(coefficient : Int, scale : Int) -> Decimal {
  let safe_scale = if scale < 0 { 0 } else { scale }
  normalize_decimal(coefficient, safe_scale)
}

///|
pub fn Decimal::from_int(value : Int) -> Decimal {
  { coefficient: value, scale: 0 }
}

///|
pub fn Decimal::coefficient(self : Decimal) -> Int {
  self.coefficient
}

///|
pub fn Decimal::scale(self : Decimal) -> Int {
  self.scale
}

///|
pub fn Decimal::is_zero(self : Decimal) -> Bool {
  self.coefficient == 0
}

///|
fn normalize_decimal(coefficient : Int, scale : Int) -> Decimal {
  let mut value = coefficient
  let mut remaining_scale = scale
  while remaining_scale > 0 && value % 10 == 0 {
    value = value / 10
    remaining_scale = remaining_scale - 1
  }
  { coefficient: value, scale: remaining_scale }
}

///|
fn power_of_ten(exponent : Int) -> Int {
  let mut value = 1
  for index = 0; index < exponent; index = index + 1 {
    value = value * 10
  }
  value
}

///|
fn positive_int(value : Int) -> Int {
  if value < 0 {
    -value
  } else {
    value
  }
}

///|
/// Rounds to a fixed number of fractional digits, with halfway values rounded
/// away from zero.
pub fn Decimal::round(self : Decimal, fraction_digits : Int) -> Decimal {
  let target = if fraction_digits < 0 { 0 } else { fraction_digits }
  if self.scale <= target {
    return self
  }
  let divisor = power_of_ten(self.scale - target)
  let magnitude = positive_int(self.coefficient)
  let mut rounded = magnitude / divisor
  if magnitude % divisor * 2 >= divisor {
    rounded = rounded + 1
  }
  let signed = if self.coefficient < 0 { -rounded } else { rounded }
  { coefficient: signed, scale: target }
}

///|
/// Multiplies a decimal by an integer factor.
pub fn Decimal::times_int(self : Decimal, factor : Int) -> Decimal {
  Decimal::new(self.coefficient * factor, self.scale)
}

///|
/// Controls how a currency identifier is displayed.
pub(all) enum CurrencyDisplay {
  Symbol
  NarrowSymbol
  Code
  Name
} derive(Eq, Debug)

///|
/// Controls whether a percentage suffix is separated from the value.
pub(all) enum PercentSpacing {
  LocaleDefault
  Compact
  Spaced
} derive(Eq, Debug)

///|
/// Locale-aware options for decimal, percentage, and currency output.
pub struct DecimalFormatter {
  locale : Locale
  grouping : Bool
  minimum_fraction_digits : Int
  maximum_fraction_digits : Int
  sign_display : SignDisplay
} derive(Eq, Debug)

///|
pub fn DecimalFormatter::new(locale : Locale) -> DecimalFormatter {
  {
    locale,
    grouping: true,
    minimum_fraction_digits: 0,
    maximum_fraction_digits: 3,
    sign_display: Auto,
  }
}

///|
pub fn DecimalFormatter::locale(self : DecimalFormatter) -> Locale {
  self.locale
}

///|
pub fn DecimalFormatter::with_grouping(
  self : DecimalFormatter,
  enabled : Bool,
) -> DecimalFormatter {
  { ..self, grouping: enabled }
}

///|
/// Sets the minimum fraction digits, clamped to 0 through 12.
pub fn DecimalFormatter::with_minimum_fraction_digits(
  self : DecimalFormatter,
  digits : Int,
) -> DecimalFormatter {
  let minimum = clamp_fraction_digits(digits)
  let maximum = if self.maximum_fraction_digits < minimum {
    minimum
  } else {
    self.maximum_fraction_digits
  }
  { ..self, minimum_fraction_digits: minimum, maximum_fraction_digits: maximum }
}

///|
/// Sets the maximum fraction digits, clamped to 0 through 12.
pub fn DecimalFormatter::with_maximum_fraction_digits(
  self : DecimalFormatter,
  digits : Int,
) -> DecimalFormatter {
  let maximum = clamp_fraction_digits(digits)
  let minimum = if self.minimum_fraction_digits > maximum {
    maximum
  } else {
    self.minimum_fraction_digits
  }
  { ..self, minimum_fraction_digits: minimum, maximum_fraction_digits: maximum }
}

///|
pub fn DecimalFormatter::with_sign_display(
  self : DecimalFormatter,
  display : SignDisplay,
) -> DecimalFormatter {
  { ..self, sign_display: display }
}

///|
fn clamp_fraction_digits(value : Int) -> Int {
  if value < 0 {
    0
  } else if value > 12 {
    12
  } else {
    value
  }
}

///|
fn decimal_digit_parts(
  value : Decimal,
  visible_scale : Int,
) -> (Bool, String, String) {
  let rounded = value.round(visible_scale)
  let negative = rounded.coefficient < 0
  let expanded = positive_int(rounded.coefficient) *
    power_of_ten(visible_scale - rounded.scale)
  let raw = expanded.to_string()
  let padding = visible_scale + 1 - raw.length()
  let digits = if padding > 0 { repeat_string("0", padding) + raw } else { raw }
  if visible_scale == 0 {
    (negative, digits, "")
  } else {
    let boundary = digits.length() - visible_scale
    (negative, "\{digits[:boundary]}", "\{digits[boundary:]}")
  }
}

///|
fn trim_fraction(fraction : String, minimum : Int) -> String {
  let mut length = fraction.length()
  while length > minimum && fraction[length - 1] == 48 {
    length = length - 1
  }
  "\{fraction[:length]}"
}

///|
fn format_decimal_body(
  locale : Locale,
  value : Decimal,
  grouping : Bool,
  minimum_fraction : Int,
  maximum_fraction : Int,
) -> (Bool, Bool, String) {
  let symbols = number_symbols(locale)
  let (negative, integer_digits, untrimmed_fraction) = decimal_digit_parts(
    value, maximum_fraction,
  )
  let fraction = trim_fraction(untrimmed_fraction, minimum_fraction)
  let integer = if grouping {
    group_digits(
      integer_digits,
      symbols.group,
      symbols.primary_group,
      symbols.secondary_group,
    )
  } else {
    integer_digits
  }
  let body = if fraction.is_empty() {
    integer
  } else {
    integer + symbols.decimal + fraction
  }
  (negative, value.round(maximum_fraction).coefficient == 0, body)
}

///|
/// Formats a decimal with deterministic base-10 rounding.
pub fn DecimalFormatter::format(
  self : DecimalFormatter,
  value : Decimal,
) -> String {
  let (negative, zero, body) = format_decimal_body(
    self.locale,
    value,
    self.grouping,
    self.minimum_fraction_digits,
    self.maximum_fraction_digits,
  )
  let symbols = number_symbols(self.locale)
  number_sign(negative, zero, self.sign_display, symbols) + body
}

///|
fn percent_separator(locale : Locale, spacing : PercentSpacing) -> String {
  match spacing {
    Compact => ""
    Spaced => "\u{a0}"
    LocaleDefault =>
      match locale.language() {
        "fr" | "ru" | "uk" | "pl" | "sv" | "no" | "fi" | "cs" | "sk" => "\u{a0}"
        _ => ""
      }
  }
}

///|
/// Formats a ratio as a percentage. `Decimal::new(125, 3)` becomes `12.5%`.
pub fn DecimalFormatter::format_percent(
  self : DecimalFormatter,
  value : Decimal,
  spacing : PercentSpacing,
) -> String {
  self.format(value.times_int(100)) +
  percent_separator(self.locale, spacing) +
  "%"
}

///|
/// Returns the conventional number of minor-unit digits for a currency.
pub fn currency_fraction_digits(code : String) -> Int {
  match code.to_upper() {
    "BHD" | "JOD" | "KWD" | "OMR" | "TND" => 3
    "CLP"
    | "DJF"
    | "GNF"
    | "ISK"
    | "JPY"
    | "KRW"
    | "PYG"
    | "RWF"
    | "UGX"
    | "VND"
    | "VUV"
    | "XAF"
    | "XOF"
    | "XPF" => 0
    _ => 2
  }
}

///|
fn currency_symbol(
  code : String,
  locale : Locale,
  display : CurrencyDisplay,
) -> String {
  let normalized = code.to_upper()
  match display {
    Code => normalized
    Name => currency_name(normalized, locale)
    NarrowSymbol =>
      match normalized {
        "USD" => "$"
        "CAD" => "$"
        "AUD" => "$"
        "CNY" | "JPY" => "\u{a5}"
        "EUR" => "\u{20ac}"
        "GBP" => "\u{a3}"
        "KRW" => "\u{20a9}"
        "INR" => "\u{20b9}"
        _ => normalized
      }
    Symbol =>
      match normalized {
        "USD" => if locale.region() == Some("US") { "$" } else { "US$" }
        "CAD" => if locale.region() == Some("CA") { "$" } else { "CA$" }
        "AUD" => if locale.region() == Some("AU") { "$" } else { "A$" }
        "CNY" => "\u{a5}"
        "JPY" => "JP\u{a5}"
        "EUR" => "\u{20ac}"
        "GBP" => "\u{a3}"
        "KRW" => "\u{20a9}"
        "INR" => "\u{20b9}"
        _ => normalized
      }
  }
}

///|
fn currency_name(code : String, locale : Locale) -> String {
  match (locale.language(), code) {
    ("zh", "CNY") => "\u{4eba}\u{6c11}\u{5e01}"
    ("zh", "USD") => "\u{7f8e}\u{5143}"
    ("zh", "EUR") => "\u{6b27}\u{5143}"
    ("en", "USD") => "US dollars"
    ("en", "EUR") => "euros"
    ("en", "GBP") => "British pounds"
    (_, _) => code
  }
}

///|
fn currency_uses_suffix(locale : Locale) -> Bool {
  match locale.language() {
    "fr"
    | "de"
    | "es"
    | "it"
    | "pt"
    | "ru"
    | "uk"
    | "pl"
    | "sv"
    | "no"
    | "fi"
    | "cs"
    | "sk" => true
    _ => false
  }
}

///|
/// Formats a monetary value using common currency digits and locale placement.
pub fn format_currency(
  locale : Locale,
  value : Decimal,
  code : String,
  display : CurrencyDisplay,
) -> String {
  let digits = currency_fraction_digits(code)
  let formatter = DecimalFormatter::new(locale)
    .with_minimum_fraction_digits(digits)
    .with_maximum_fraction_digits(digits)
  let amount = formatter.format(value)
  let label = currency_symbol(code, locale, display)
  if display == Name || display == Code || currency_uses_suffix(locale) {
    amount + "\u{a0}" + label
  } else {
    label + amount
  }
}