// The Money type: an exact amount in the minor units of one currency.
//
// `minor` is the amount in the smallest unit the currency defines -- cents for
// USD, yen for JPY, fils for BHD -- so 12.34 USD is `{ minor: 1234 }`. Nothing
// in this file uses a floating point number: the amount is always an integer,
// and every operation that would leave the integers goes through an explicit
// rounding mode.

///|
/// An amount of money: an integer number of minor units plus its currency.
pub(all) struct Money {
  minor : Int64
  currency : Currency
} derive(Debug)

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

///|
/// Amounts are equal when they are the same number of minor units of the same
/// currency. The rest of the currency record -- name, symbol, cash rules -- is
/// display data and does not take part in the comparison.
pub impl Eq for Money with fn equal(self, other) {
  self.minor == other.minor && self.currency.code == other.currency.code
}

///|
pub impl Eq for Money with fn not_equal(self, other) {
  !self.equal(other)
}

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

///|
/// An amount from an exact number of minor units.
pub fn Money::of_minor(minor : Int64, currency : Currency) -> Money {
  { minor, currency, }
}

///|
/// An amount from whole major units, e.g. `of_units(12L, usd)` is USD 12.00.
pub fn Money::of_units(
  units : Int64,
  currency : Currency,
) -> Money raise MoneyError {
  {
    minor: mul_checked(units, currency.scale(), "units to minor units"),
    currency,
  }
}

///|
/// An amount from whole units plus a non-negative fraction in minor units.
///
/// The fraction must be smaller than one major unit, and the sign of `units`
/// decides the sign of the result: `of_parts(-2L, 50L, usd)` is -USD 2.50.
pub fn Money::of_parts(
  units : Int64,
  fraction : Int64,
  currency : Currency,
) -> Money raise MoneyError {
  let scale = currency.scale()
  if fraction < 0L || fraction >= scale {
    raise InvalidAmount(
      "{units}.{fraction}",
      "the fraction has to be in 0..\{scale}",
    )
  }
  let whole = mul_checked(units, scale, "units to minor units")
  if units < 0L {
    { minor: sub_checked(whole, fraction, "parts to minor units"), currency, }
  } else {
    { minor: add_checked(whole, fraction, "parts to minor units"), currency, }
  }
}

///|
/// The smallest amount an \`Int64\` can hold in minor units.
pub fn min_amount() -> Int64 {
  @int64.MIN_VALUE
}

///|
/// The largest amount an \`Int64\` can hold in minor units.
pub fn max_amount() -> Int64 {
  @int64.MAX_VALUE
}

///|
/// Zero in a given currency.
pub fn Money::zero(currency : Currency) -> Money {
  { minor: 0L, currency, }
}

///|
/// The number of minor units in one major unit of this amount's currency.
pub fn Money::scale(self : Money) -> Int64 {
  self.currency.scale()
}

///|
/// The whole units, truncated towards zero.
pub fn Money::units(self : Money) -> Int64 {
  self.minor / self.scale()
}

///|
/// The fraction in minor units, always non-negative.
pub fn Money::fraction(self : Money) -> Int64 {
  let remainder = self.minor % self.scale()
  if remainder < 0L {
    0L - remainder
  } else {
    remainder
  }
}

///|
/// `-1`, `0` or `1`.
pub fn Money::sign(self : Money) -> Int32 {
  if self.minor == 0L {
    0
  } else if self.minor < 0L {
    -1
  } else {
    1
  }
}

///|
pub fn Money::is_zero(self : Money) -> Bool {
  self.minor == 0L
}

///|
pub fn Money::is_positive(self : Money) -> Bool {
  self.minor > 0L
}

///|
pub fn Money::is_negative(self : Money) -> Bool {
  self.minor < 0L
}

///|
/// True when both amounts are in the same currency.
pub fn Money::same_currency(self : Money, other : Money) -> Bool {
  self.currency.same_as(other.currency)
}

///|
/// Fails unless both amounts are in the same currency.
fn Money::ensure_same_currency(
  self : Money,
  other : Money,
) -> Unit raise MoneyError {
  if !self.same_currency(other) {
    raise CurrencyMismatch(self.currency.code, other.currency.code)
  }
}

///|
pub fn Money::neg(self : Money) -> Money raise MoneyError {
  { minor: sub_checked(0L, self.minor, "negation"), currency: self.currency, }
}

///|
pub fn Money::abs(self : Money) -> Money raise MoneyError {
  if self.minor < 0L {
    self.neg()
  } else {
    self
  }
}

///|
/// Sum of two amounts in the same currency.
pub fn Money::add(self : Money, other : Money) -> Money raise MoneyError {
  self.ensure_same_currency(other)
  {
    minor: add_checked(self.minor, other.minor, "addition"),
    currency: self.currency,
  }
}

///|
/// Difference of two amounts in the same currency.
pub fn Money::sub(self : Money, other : Money) -> Money raise MoneyError {
  self.ensure_same_currency(other)
  {
    minor: sub_checked(self.minor, other.minor, "subtraction"),
    currency: self.currency,
  }
}

///|
/// Multiplication by a whole number.
pub fn Money::mul(self : Money, factor : Int32) -> Money raise MoneyError {
  self.mul_int64(factor.to_int64())
}

///|
pub fn Money::mul_int64(self : Money, factor : Int64) -> Money raise MoneyError {
  {
    minor: mul_checked(self.minor, factor, "multiplication"),
    currency: self.currency,
  }
}

///|
/// Division by a whole number, rounded once under `mode`.
pub fn Money::div_int64(
  self : Money,
  divisor : Int64,
  mode~ : RoundingMode = HalfUp,
) -> Money raise MoneyError {
  { minor: div_round(self.minor, divisor, mode), currency: self.currency, }
}

///|
pub fn Money::div(
  self : Money,
  divisor : Int32,
  mode~ : RoundingMode = HalfUp,
) -> Money raise MoneyError {
  self.div_int64(divisor.to_int64(), mode~)
}

///|
/// Total order over amounts of the same currency, used by sorting and by
/// `min` and `max`.
pub fn Money::compare(self : Money, other : Money) -> Int32 raise MoneyError {
  self.ensure_same_currency(other)
  if self.minor == other.minor {
    0
  } else if self.minor < other.minor {
    -1
  } else {
    1
  }
}

///|
pub fn Money::min(self : Money, other : Money) -> Money raise MoneyError {
  if self.compare(other) <= 0 {
    self
  } else {
    other
  }
}

///|
pub fn Money::max(self : Money, other : Money) -> Money raise MoneyError {
  if self.compare(other) >= 0 {
    self
  } else {
    other
  }
}

///|
/// Sum of a list of amounts. An empty list is only meaningful together with the
/// currency it would be zero in, so the caller supplies that as `fallback`.
pub fn Money::sum(
  amounts : Array[Money],
  fallback : Currency,
) -> Money raise MoneyError {
  let mut total = Money::zero(fallback)
  for amount in amounts {
    total = total.add(amount)
  }
  total
}

///|
/// Rounds the amount to the nearest multiple of `increment` minor units.
pub fn Money::round_to(
  self : Money,
  increment : Int64,
  mode~ : RoundingMode = HalfUp,
) -> Money raise MoneyError {
  if increment <= 0L {
    raise InvalidAmount(
      increment.to_string(),
      "a rounding step has to be positive",
    )
  }
  {
    minor: mul_checked(
      div_round(self.minor, increment, mode),
      increment,
      "rounding",
    ),
    currency: self.currency,
  }
}

///|
/// Rounds to the step a cash payment of this currency is settled in.
///
/// CHF settles cash to 0.05, so 1.02 becomes 1.00 and 1.03 becomes 1.05. A
/// currency without a separate cash rule is returned unchanged.
pub fn Money::round_cash(
  self : Money,
  mode~ : RoundingMode = HalfUp,
) -> Money raise MoneyError {
  self.round_to(self.currency.cash_increment(), mode~)
}

///|
/// The amount as a floating point number, for reporting and for plotting.
///
/// This is a lossy view. Never build an amount back from it: parse the decimal
/// text instead, or the rounding error will be reintroduced.
pub fn Money::to_double(self : Money) -> Double {
  self.minor.to_double() / self.scale().to_double()
}

///|
/// `-12.34 USD`, a debugging form. Use the format package for anything a user
/// is meant to read.
pub fn Money::debug_string(self : Money) -> String {
  "{self.minor} \{self.currency.code}"
}