///|
priv enum Numeric {
  Exact(@bigint.BigInt, Int32, Bool)
  Floating(Double)
}

///|
fn pow10(n : Int32) -> @bigint.BigInt {
  let mut value = @bigint.BigInt::from_int(1)
  for i = 0; i < n; i = i + 1 {
    value = value * @bigint.BigInt::from_int(10)
  }
  value
}

///|
fn big_abs(value : @bigint.BigInt) -> @bigint.BigInt {
  if value < @bigint.BigInt::from_int(0) {
    -value
  } else {
    value
  }
}

///|
fn numeric(term : Term, digits : Int32) -> Numeric raise QueryError {
  if term.kind != LiteralKind {
    raise Evaluation("numeric literal required")
  }
  let text = term.lexical.trim().to_owned()
  if text.length() == 0 || text.length() > digits {
    raise Evaluation("numeric lexical budget")
  }
  let dt = term.datatype
  if dt == xsd_namespace() + "double" || dt == xsd_namespace() + "float" {
    let value = if text == "INF" || text == "+INF" {
      1.0 / 0.0
    } else if text == "-INF" {
      -1.0 / 0.0
    } else if text == "NaN" {
      0.0 / 0.0
    } else {
      @str.parse_double(text[:]) catch {
        _ => raise Evaluation("invalid floating literal")
      }
    }
    return Floating(value)
  }
  let decimal = dt == xsd_namespace() + "decimal"
  if !decimal && dt != xsd_namespace() + "integer" {
    raise Evaluation("unsupported numeric datatype")
  }
  let chars = text.to_array()
  let mut i = 0
  let mut negative = false
  if chars[0] == '+' || chars[0] == '-' {
    negative = chars[0] == '-'
    i = 1
  }
  let out = StringBuilder()
  let mut dot = false
  let mut scale = 0
  let mut count = 0
  while i < chars.length() {
    let c = chars[i]
    if c == '.' && decimal && !dot {
      dot = true
    } else if digit(c) {
      out.write_char(c)
      count += 1
      if dot {
        scale += 1
      }
    } else {
      raise Evaluation("invalid exact numeric literal")
    }
    i += 1
  }
  if count == 0 {
    raise Evaluation("numeric literal has no digits")
  }
  let coefficient = @bigint.BigInt::from_string(out.to_string())
  Exact(if negative { -coefficient } else { coefficient }, scale, decimal)
}

///|
fn Numeric::as_double(self : Numeric) -> Double raise QueryError {
  match self {
    Floating(v) => v
    Exact(_, _, _) => {
      let term = self.term(4096)
      @str.parse_double(term.lexical[:]) catch {
        _ => raise Evaluation("numeric conversion")
      }
    }
  }
}

///|
fn Numeric::term(self : Numeric, digits : Int32) -> Term raise QueryError {
  match self {
    Floating(v) =>
      typed_literal(
        if v.is_nan() {
          "NaN"
        } else if v.is_pos_inf() {
          "INF"
        } else if v.is_neg_inf() {
          "-INF"
        } else {
          v.to_string()
        },
        xsd_namespace() + "double",
      )
    Exact(coefficient, scale, decimal) => {
      let mut c = coefficient
      let mut s = scale
      let ten = @bigint.BigInt::from_int(10)
      while s > 0 && (c % ten).is_zero() {
        c = c / ten
        s -= 1
      }
      let raw = big_abs(c).to_string()
      if raw.length() > digits || s > digits {
        raise Resource("numeric result digits")
      }
      let text = StringBuilder()
      if c < @bigint.BigInt::from_int(0) {
        text.write_string("-")
      }
      if s == 0 {
        text.write_string(raw)
        if decimal {
          text.write_string(".0")
        }
      } else if s >= raw.length() {
        text.write_string("0.")
        for i = 0; i < s - raw.length(); i = i + 1 {
          text.write_string("0")
        }
        text.write_string(raw)
      } else {
        text.write_string(raw[0:raw.length() - s].to_owned())
        text.write_string(".")
        text.write_string(raw[raw.length() - s:].to_owned())
      }
      typed_literal(
        text.to_string(),
        xsd_namespace() + (if decimal { "decimal" } else { "integer" }),
      )
    }
  }
}

///|
fn Numeric::compare(self : Numeric, other : Numeric) -> Int32? raise QueryError {
  match (self, other) {
    (Exact(a, sa, _), Exact(b, sb, _)) => {
      let scale = if sa > sb { sa } else { sb }
      Some((a * pow10(scale - sa)).compare(b * pow10(scale - sb)))
    }
    _ => {
      let a = self.as_double()
      let b = other.as_double()
      if a.is_nan() || b.is_nan() {
        None
      } else if a < b {
        Some(-1)
      } else if a > b {
        Some(1)
      } else {
        Some(0)
      }
    }
  }
}

///|
fn numeric_op(
  op : String,
  left : Numeric,
  right : Numeric,
  digits : Int32,
) -> Term raise QueryError {
  let result = match (left, right) {
    (Exact(a, sa, da), Exact(b, sb, db)) => {
      let scale = if sa > sb { sa } else { sb }
      match op {
        "+" =>
          Exact(a * pow10(scale - sa) + b * pow10(scale - sb), scale, da || db)
        "-" =>
          Exact(a * pow10(scale - sa) - b * pow10(scale - sb), scale, da || db)
        "*" => Exact(a * b, sa + sb, da || db)
        "/" => {
          if b.is_zero() {
            raise Evaluation("division by zero")
          }
          let numerator = a * pow10(sb + 24)
          let denominator = b * pow10(sa)
          let mut quotient = numerator / denominator
          let remainder = big_abs(numerator % denominator)
          let half = (remainder * @bigint.BigInt::from_int(2)).compare(
            big_abs(denominator),
          )
          if half > 0 ||
            (half == 0 && !(quotient % @bigint.BigInt::from_int(2)).is_zero()) {
            quotient += if (a < @bigint.BigInt::from_int(0)) ==
              (b < @bigint.BigInt::from_int(0)) {
              @bigint.BigInt::from_int(1)
            } else {
              @bigint.BigInt::from_int(-1)
            }
          }
          Exact(quotient, 24, true)
        }
        _ => raise Evaluation("unknown arithmetic operator")
      }
    }
    _ => {
      let a = left.as_double()
      let b = right.as_double()
      Floating(
        match op {
          "+" => a + b
          "-" => a - b
          "*" => a * b
          "/" => a / b
          _ => raise Evaluation("unknown arithmetic operator")
        },
      )
    }
  }
  result.term(digits)
}