///|
priv enum Decimal128Value {
  DecimalNaN(Bool)
  DecimalInfinity
  DecimalFinite(Int, BigInt)
}

///|
priv struct ParsedDecimal128 {
  negative : Bool
  value : Decimal128Value
}

///|
let decimal128_max_coefficient : BigInt = BigInt::from_string(
  "9999999999999999999999999999999999",
)

///|
let decimal128_coefficient_mask : BigInt = (1N << 113) - 1N

///|
fn parse_decimal128(input : String) -> Decimal128 raise BsonError {
  if input.is_empty() {
    return invalid_decimal128("empty Decimal128 string")
  }
  let (negative, unsigned) = strip_decimal_sign(input)
  if unsigned.is_empty() {
    return invalid_decimal128("Decimal128 contains only a sign")
  }
  let lower = unsigned.to_lower()
  let parsed = match lower {
    "nan" => { negative, value: DecimalNaN(false) }
    "snan" => { negative, value: DecimalNaN(true) }
    "infinity" | "inf" => { negative, value: DecimalInfinity }
    _ => parse_finite_decimal128(negative, lower)
  }
  pack_decimal128(parsed)
}

///|
fn strip_decimal_sign(input : String) -> (Bool, String) {
  match input[0] {
    '-' => (true, input[1:].to_owned())
    '+' => (false, input[1:].to_owned())
    _ => (false, input)
  }
}

///|
fn parse_finite_decimal128(
  negative : Bool,
  input : String,
) -> ParsedDecimal128 raise BsonError {
  let (decimal, exponent_text) = split_decimal_exponent(input)
  let mut exponent = parse_wide_exponent(exponent_text)
  let (digits_with_zeros, fractional_digits) = remove_decimal_point(decimal)
  exponent = exponent - BigInt::from_int(fractional_digits)
  let mut digits = strip_decimal_leading_zeros(digits_with_zeros)
  if digits.length() > 34 {
    let original_length = digits.length()
    digits = truncate_decimal_exact(digits, 34)
    exponent = exponent + BigInt::from_int(original_length - digits.length())
  }
  if exponent < BigInt::from_int(-6176) {
    if digits != "0" {
      let delta_big = BigInt::from_int(-6176) - exponent
      if delta_big > BigInt::from_int(digits.length()) {
        return invalid_decimal128(
          "Decimal128 underflow requires inexact rounding",
        )
      }
      let precision = digits.length() - delta_big.to_int()
      digits = truncate_decimal_exact(digits, precision)
    }
    exponent = BigInt::from_int(-6176)
  }
  if exponent > BigInt::from_int(6111) {
    if digits != "0" {
      let delta_big = exponent - BigInt::from_int(6111)
      if delta_big > BigInt::from_int(34 - digits.length()) {
        return invalid_decimal128("Decimal128 exponent overflow")
      }
      let delta = delta_big.to_int()
      digits = digits + "0".repeat(delta)
    }
    exponent = BigInt::from_int(6111)
  }
  let coefficient = BigInt::from_string(digits)
  if coefficient > decimal128_max_coefficient {
    return invalid_decimal128("Decimal128 coefficient exceeds 34 digits")
  }
  { negative, value: DecimalFinite(exponent.to_int(), coefficient) }
}

///|
fn split_decimal_exponent(input : String) -> (String, String) raise BsonError {
  let mut exponent_index = -1
  for index, code in input.code_units() {
    if code == 'e' {
      if exponent_index >= 0 {
        return invalid_decimal128("Decimal128 contains multiple exponents")
      }
      exponent_index = index
    }
  }
  if exponent_index < 0 {
    (input, "0")
  } else {
    let decimal = input[0:exponent_index].to_owned()
    let exponent = input[exponent_index + 1:].to_owned()
    if exponent.is_empty() {
      return invalid_decimal128("Decimal128 exponent is empty")
    }
    (decimal, exponent)
  }
}

///|
fn parse_wide_exponent(input : String) -> BigInt raise BsonError {
  let (negative, unsigned) = strip_decimal_sign(input)
  if unsigned.is_empty() || !is_ascii_digits(unsigned) {
    return invalid_decimal128("invalid Decimal128 exponent")
  }
  let exponent = BigInt::from_string(unsigned)
  if negative {
    -exponent
  } else {
    exponent
  }
}

///|
fn remove_decimal_point(input : String) -> (String, Int) raise BsonError {
  let mut point = -1
  for index, code in input.code_units() {
    if code == '.' {
      if point >= 0 {
        return invalid_decimal128("Decimal128 contains multiple decimal points")
      }
      point = index
    } else if !(code is ('0'..='9')) {
      return invalid_decimal128("Decimal128 coefficient contains a non-digit")
    }
  }
  if point < 0 {
    if input.is_empty() {
      return invalid_decimal128("Decimal128 coefficient is empty")
    }
    (input, 0)
  } else {
    let before = input[0:point].to_owned()
    let after = input[point + 1:].to_owned()
    if before.is_empty() && after.is_empty() {
      return invalid_decimal128("Decimal128 coefficient has no digits")
    }
    (before + after, after.length())
  }
}

///|
fn strip_decimal_leading_zeros(input : String) -> String {
  let mut first = 0
  while first < input.length() && input[first] == '0' {
    first += 1
  }
  if first == input.length() {
    "0"
  } else {
    input[first:].to_owned()
  }
}

///|
fn truncate_decimal_exact(
  input : String,
  precision : Int,
) -> String raise BsonError {
  if precision < 0 || precision > input.length() {
    return invalid_decimal128("invalid Decimal128 precision")
  }
  for code in input[precision:].code_units() {
    if code != '0' {
      return invalid_decimal128("Decimal128 conversion would require rounding")
    }
  }
  input[0:precision].to_owned()
}

///|
fn is_ascii_digits(input : String) -> Bool {
  for code in input.code_units() {
    if !(code is ('0'..='9')) {
      return false
    }
  }
  true
}

///|
fn pack_decimal128(parsed : ParsedDecimal128) -> Decimal128 {
  let sign = if parsed.negative { 1N << 127 } else { 0N }
  let packed = match parsed.value {
    DecimalNaN(signalling) =>
      sign | (BigInt::from_int(if signalling { 0x7E } else { 0x7C }) << 120)
    DecimalInfinity => sign | (BigInt::from_int(0x78) << 120)
    DecimalFinite(exponent, coefficient) =>
      sign | (BigInt::from_int(exponent + 6176) << 113) | coefficient
  }
  let big_endian = packed.to_octets(length=16)
  let little_endian = Bytes::makei(16, index => big_endian[15 - index])
  Decimal128::from_valid_bytes(little_endian)
}

///|
fn unpack_decimal128(decimal : Decimal128) -> ParsedDecimal128 {
  let bytes = decimal.bytes()
  let negative = (bytes[15].to_int() & 0x80) != 0
  let high = bytes[15].to_int()
  if (high & 0x7C) == 0x7C {
    { negative, value: DecimalNaN((high & 0x02) != 0) }
  } else if (high & 0x78) == 0x78 {
    { negative, value: DecimalInfinity }
  } else {
    let big_endian = Bytes::makei(16, index => bytes[15 - index])
    let packed = BigInt::from_octets(big_endian[:])
    let steering = (high & 0x60) == 0x60
    let exponent_shift = if steering { 111 } else { 113 }
    let exponent = ((packed >> exponent_shift) & 0x3FFFN).to_int() - 6176
    let coefficient = if steering {
      0N
    } else {
      let value = packed & decimal128_coefficient_mask
      if value > decimal128_max_coefficient {
        0N
      } else {
        value
      }
    }
    { negative, value: DecimalFinite(exponent, coefficient) }
  }
}

///|
fn format_decimal128(decimal : Decimal128) -> String {
  let parsed = unpack_decimal128(decimal)
  match parsed.value {
    DecimalNaN(_) => "NaN"
    DecimalInfinity => if parsed.negative { "-Infinity" } else { "Infinity" }
    DecimalFinite(exponent, coefficient) => {
      let prefix = if parsed.negative { "-" } else { "" }
      let digits = coefficient.to_string()
      let adjusted_exponent = exponent + digits.length() - 1
      if exponent <= 0 && adjusted_exponent >= -6 {
        if exponent == 0 {
          prefix + digits
        } else {
          let decimal_places = -exponent
          if decimal_places >= digits.length() {
            prefix +
            "0." +
            "0".repeat(decimal_places - digits.length()) +
            digits
          } else {
            let split = digits.length() - decimal_places
            prefix +
            digits[0:split].to_owned() +
            "." +
            digits[split:].to_owned()
          }
        }
      } else {
        let rest = digits[1:].to_owned()
        let coefficient_text = if rest.is_empty() {
          digits[0:1].to_owned()
        } else {
          digits[0:1].to_owned() + "." + rest
        }
        let exponent_sign = if adjusted_exponent > 0 { "+" } else { "" }
        prefix +
        coefficient_text +
        "E" +
        exponent_sign +
        adjusted_exponent.to_string()
      }
    }
  }
}

///|
fn[T] invalid_decimal128(message : String) -> T raise BsonError {
  raise bson_error(InvalidDecimal128, -1, "$", message)
}