// Ion decimals.
//
// A decimal is `coefficient * 10^exponent`. Both parts are kept exactly as
// written so that trailing zeros (the scale) survive a round-trip, and a zero
// coefficient may carry a sign: Ion keeps `0.` and `-0.` distinct.

///|
/// An Ion `decimal` value, kept as `coefficient * 10^exponent`.
pub struct Decimal {
  coefficient : BigInt
  exponent : Int32
  negative_zero : Bool
} derive(Debug)

///|
/// Build a decimal from an explicit coefficient and base-10 exponent.
#as_free_fn(decimal)
#alias(new, deprecated="Use `@ion.decimal(...)` instead")
pub fn Decimal::Decimal(coefficient : BigInt, exponent : Int32) -> Decimal {
  { coefficient, exponent, negative_zero: false, }
}

///|
/// Build a decimal from an `Int`, with exponent `0`.
pub fn Decimal::from_int(value : Int32) -> Decimal {
  Decimal::Decimal(BigInt::from_int(value), 0)
}

///|
/// The decimal `0.`.
pub fn Decimal::zero() -> Decimal {
  Decimal::from_int(0)
}

///|
/// The decimal `-0.` (negative zero), which Ion keeps distinct from `0.`.
pub fn Decimal::negative_zero() -> Decimal {
  { coefficient: BigInt::from_int(0), exponent: 0, negative_zero: true, }
}

///|
/// The decimal `-0dN`: negative zero at a given exponent, which Ion keeps
/// distinct from `0dN`.
pub fn Decimal::negative_zero_at(exponent : Int32) -> Decimal {
  { coefficient: BigInt::from_int(0), exponent, negative_zero: true, }
}

///|
/// The unscaled coefficient (significand) of this decimal.
pub fn Decimal::coefficient(self : Decimal) -> BigInt {
  self.coefficient
}

///|
/// The base-10 exponent of this decimal.
pub fn Decimal::exponent(self : Decimal) -> Int32 {
  self.exponent
}

///|
/// Whether this value is negative zero.
pub fn Decimal::is_negative_zero(self : Decimal) -> Bool {
  self.negative_zero
}

///|
/// Whether the coefficient is zero.
pub fn Decimal::is_zero(self : Decimal) -> Bool {
  self.coefficient.is_zero()
}

///|
/// Whether this value is negative, including negative zero.
pub fn Decimal::is_negative(self : Decimal) -> Bool {
  self.negative_zero || self.coefficient.compare_int(0) < 0
}

///|
/// The magnitude of the coefficient.
fn Decimal::magnitude(self : Decimal) -> BigInt {
  if self.coefficient.compare_int(0) < 0 {
    self.coefficient.neg()
  } else {
    self.coefficient
  }
}

///|
/// Read an unsigned integer from `text` starting at `start`, allowing Ion's
/// digit-separating underscores. Returns the digits and the index just past the
/// run. Raises when the run is empty or an underscore is misplaced.
fn read_uint_run(
  text : StringView,
  start : Int32,
) -> (String, Int32) raise IonError {
  let n = text.length()
  let sb = StringBuilder()
  let mut i = start
  let mut last_was_underscore = false
  let mut saw_digit = false
  while i < n && (is_digit(text[i]) || text[i] == '_') {
    if text[i] == '_' {
      if !saw_digit || last_was_underscore {
        raise IonError::Syntax("misplaced underscore in number")
      }
      last_was_underscore = true
    } else {
      sb.write_view(char_view(text, i))
      saw_digit = true
      last_was_underscore = false
    }
    i += 1
  }
  if !saw_digit {
    raise IonError::Syntax("expected a digit")
  }
  if last_was_underscore {
    raise IonError::Syntax("number may not end with an underscore")
  }
  (sb.to_string(), i)
}

///|
/// Read a signed decimal exponent (`d`/`D` body), e.g. `-12` in `1.5d-12`.
fn read_exponent(
  text : StringView,
  start : Int32,
) -> (Int32, Int32) raise IonError {
  let n = text.length()
  let mut i = start
  let mut negative = false
  if i < n && (text[i] == '-' || text[i] == '+') {
    negative = text[i] == '-'
    i += 1
  }
  let (digits, next) = read_uint_run(text, i)
  // Parse the sign with the magnitude so Int::min_value is representable.
  let value = @string.parse_int(if negative { "-" + digits } else { digits }) catch {
    _ => raise IonError::Syntax("decimal exponent is out of range")
  }
  (value, next)
}

///|
/// Parse the body of an Ion decimal: an optional sign, a coefficient with an
/// optional decimal point, and an optional `d`/`D` exponent. For example
/// `-1_234.56d-7`, `0.`, or `123d0`.
pub fn Decimal::parse(text : StringView) -> Decimal raise IonError {
  let n = text.length()
  let mut i = 0
  let mut negative = false
  if i < n && (text[i] == '-' || text[i] == '+') {
    negative = text[i] == '-'
    i += 1
  }
  let digits = StringBuilder()
  let mut fraction_digits = 0
  let mut saw_digit = false
  // Integer part.
  let mut last_was_underscore = false
  while i < n && (is_digit(text[i]) || text[i] == '_') {
    if text[i] == '_' {
      if !saw_digit || last_was_underscore {
        raise IonError::Syntax("misplaced underscore in decimal")
      }
      last_was_underscore = true
    } else {
      digits.write_view(char_view(text, i))
      saw_digit = true
      last_was_underscore = false
    }
    i += 1
  }
  if last_was_underscore {
    raise IonError::Syntax("decimal may not end with an underscore")
  }
  // Fraction part.
  if i < n && text[i] == '.' {
    i += 1
    let mut fraction_seen = false
    let mut last_underscore = false
    while i < n && (is_digit(text[i]) || text[i] == '_') {
      if text[i] == '_' {
        if !fraction_seen || last_underscore {
          raise IonError::Syntax("misplaced underscore in decimal fraction")
        }
        last_underscore = true
      } else {
        digits.write_view(char_view(text, i))
        fraction_digits += 1
        saw_digit = true
        fraction_seen = true
        last_underscore = false
      }
      i += 1
    }
    if last_underscore {
      raise IonError::Syntax("decimal fraction may not end with an underscore")
    }
  }
  if !saw_digit {
    raise IonError::Syntax("decimal literal has no digits")
  }
  let mut exponent = 0
  if i < n && (text[i] == 'd' || text[i] == 'D') {
    i += 1
    let (value, next) = read_exponent(text, i)
    exponent = value
    i = next
  }
  if i != n {
    raise IonError::Syntax("unexpected character in decimal literal")
  }
  let coefficient = parse_digits_to_bigint(digits.to_string(), negative)
  let negative_zero = negative && coefficient.is_zero()
  let adjusted = exponent.to_int64() - fraction_digits.to_int64()
  if adjusted < -2147483648L || adjusted > 2147483647L {
    raise IonError::Syntax("decimal exponent is out of range")
  }
  { coefficient, exponent: adjusted.to_int(), negative_zero, }
}

///|
/// Parse a run of ASCII digits into a (possibly negative) `BigInt`.
fn parse_digits_to_bigint(digits : String, negative : Bool) -> BigInt {
  if digits.is_empty() {
    return BigInt::from_int(0)
  }
  let magnitude = @string.parse_bigint(digits, base=10) catch {
    _ => BigInt::from_int(0)
  }
  if negative {
    magnitude.neg()
  } else {
    magnitude
  }
}

///|
/// Render this decimal as Ion text, preserving coefficient, exponent, and
/// signed zero. Use positional notation for non-positive exponents when the
/// adjusted exponent is at least -6; otherwise retain `d` notation. Positive
/// exponents require `d` notation to preserve precision.
pub fn Decimal::to_ion_string(self : Decimal) -> String {
  let sign = if self.is_negative() { "-" } else { "" }
  let digits = self.magnitude().to_string()
  if self.exponent == 0 {
    return "\{sign}\{digits}."
  }
  // Widen before adding: even the minimum Int exponent must remain compact.
  let point = digits.length().to_int64() + self.exponent.to_int64()
  if self.exponent < 0 && point - 1L >= -6L {
    let buffer = StringBuilder()
    buffer.write_string(sign)
    if point > 0L {
      let index = point.to_int()
      buffer.write_view(digits[:index])
      buffer.write_string(".")
      buffer.write_view(digits[index:])
    } else {
      buffer.write_string("0.")
      for _ in 0..<(-point).to_int() {
        buffer.write_string("0")
      }
      buffer.write_string(digits)
    }
    return buffer.to_string()
  }
  "\{sign}\{digits}d\{self.exponent}"
}