///|
/// A number, by its kind: arithmetic widens integer to decimal to float.
priv enum Num {
  Whole(@bigint.BigInt)
  Exact(@ion_core.Decimal)
  Approx(Double)
}

///|
fn number(v : @value.Value) -> Num? {
  match v {
    Int(n) => Some(Whole(@bigint.BigInt::from_int64(n)))
    BigInt(n) => Some(Whole(n))
    Decimal(d) => Some(Exact(d))
    Float(f) => Some(Approx(f))
    _ => None
  }
}

///|
fn whole_to_double(n : @bigint.BigInt) -> Double raise OpFailure {
  try @string.parse_double(n.to_string()) catch {
    _ => raise DataFailure("an integer out of the float range")
  } noraise {
    d => d
  }
}

///|
fn to_double(n : Num) -> Double raise OpFailure {
  match n {
    Whole(w) => whole_to_double(w)
    Exact(d) =>
      match d.to_double() {
        Some(x) => x
        None => raise DataFailure("a decimal out of the float range")
      }
    Approx(f) => f
  }
}

///|
/// A float becomes the decimal of its shortest text. NaN and the infinities
/// have none.
fn to_decimal(n : Num) -> @ion_core.Decimal raise OpFailure {
  match n {
    Whole(w) => @ion_core.Decimal::Decimal(w, 0)
    Exact(d) => d
    Approx(f) =>
      if f.is_nan() || f.is_inf() {
        raise DataFailure("NaN or infinity in decimal arithmetic")
      } else {
        match parse_decimal(f.to_string()) {
          Some(d) => d
          None => raise DataFailure("a float without a decimal")
        }
      }
  }
}

///|
/// An integer result: `Int` when it fits in Int64. A larger result is an
/// error (partiql-tests extended int.ion, `int_overflow_error`).
fn whole_value(n : @bigint.BigInt) -> @value.Value raise OpFailure {
  if n.compare_int64(9223372036854775807L) <= 0 &&
    n.compare_int64(-9223372036854775808L) >= 0 {
    @value.Value::from_int(n.to_int64())
  } else {
    raise DataFailure("integer overflow")
  }
}

///|
/// The absent-value rules, then the number kinds. `whole`, `exact` and
/// `approx` compute on two integers, two decimals and two floats. An integer
/// widens to a float, and a float or an integer widens to a decimal when the
/// other operand is a decimal (partiql-tests nary-operators.ion,
/// `subIntFloatDecimal`).
fn arithmetic(
  a : @value.Value,
  b : @value.Value,
  whole~ : (@bigint.BigInt, @bigint.BigInt) -> @value.Value raise OpFailure,
  exact~ : (@ion_core.Decimal, @ion_core.Decimal) -> @ion_core.Decimal raise OpFailure,
  approx~ : (Double, Double) -> Double raise OpFailure,
) -> @value.Value raise OpFailure {
  let x = boxed(a)
  let y = boxed(b)
  if x is Missing || y is Missing {
    return Missing
  }
  if x is Null || y is Null {
    return Null
  }
  match (number(x), number(y)) {
    (Some(Whole(m)), Some(Whole(n))) => whole(m, n)
    (Some(p), Some(q)) =>
      if p is Exact(_) || q is Exact(_) {
        @value.Value::from_decimal(exact(to_decimal(p), to_decimal(q)))
      } else {
        @value.Value::from_float(approx(to_double(p), to_double(q)))
      }
    _ => raise TypeFailure("arithmetic on a value that is not a number")
  }
}

///|
/// The datetime and interval rules, for two operands that are neither NULL
/// nor MISSING; `None` leaves the pair to the number rules.
fn datetime_first(
  a : @value.Value,
  b : @value.Value,
  rule : (@value.Value, @value.Value) -> @value.Value? raise OpFailure,
) -> @value.Value? raise OpFailure {
  let x = boxed(a)
  let y = boxed(b)
  if x is (Missing | Null) || y is (Missing | Null) {
    return None
  }
  rule(x, y)
}

///|
/// Add numbers or a supported datetime/interval pair. Unbox Ion and propagate
/// MISSING before NULL. Numeric operands widen to decimal when either is
/// decimal, otherwise to float when either is float. Raise `TypeFailure` for
/// incompatible operands or `DataFailure` for overflow.
pub fn add(a : @value.Value, b : @value.Value) -> @value.Value raise OpFailure {
  if datetime_first(a, b, datetime_add) is Some(v) {
    return v
  }
  arithmetic(a, b, whole=(m, n) => whole_value(m + n), exact=decimal_add, approx=(
    p,
    q,
  ) => p + q)
}

///|
/// Subtract numbers or a supported datetime/interval pair. Use `add`'s
/// unboxing, absence, numeric widening and failure rules.
pub fn sub(a : @value.Value, b : @value.Value) -> @value.Value raise OpFailure {
  if datetime_first(a, b, datetime_sub) is Some(v) {
    return v
  }
  arithmetic(a, b, whole=(m, n) => whole_value(m - n), exact=decimal_sub, approx=(
    p,
    q,
  ) => p - q)
}

///|
/// Multiply numbers or a supported interval/number pair. Use `add`'s
/// unboxing, absence, numeric widening and failure rules.
pub fn mul(a : @value.Value, b : @value.Value) -> @value.Value raise OpFailure {
  if datetime_first(a, b, datetime_mul) is Some(v) {
    return v
  }
  arithmetic(
    a,
    b,
    whole=(m, n) => whole_value(m * n),
    exact=(p, q) => decimal_mul(p, q),
    approx=(p, q) => p * q,
  )
}

///|
/// Integer division truncates toward zero; a zero divisor of an integer or
/// decimal is a `DataFailure`, a float divides as IEEE does.
pub fn div(a : @value.Value, b : @value.Value) -> @value.Value raise OpFailure {
  if datetime_first(a, b, datetime_div) is Some(v) {
    return v
  }
  arithmetic(
    a,
    b,
    whole=(m, n) => {
      if n.is_zero() {
        raise DataFailure("division by zero")
      } else {
        whole_value(m / n)
      }
    },
    exact=decimal_div,
    approx=(p, q) => p / q,
  )
}

///|
/// The remainder has the sign of the dividend.
pub fn rem(a : @value.Value, b : @value.Value) -> @value.Value raise OpFailure {
  arithmetic(
    a,
    b,
    whole=(m, n) => {
      if n.is_zero() {
        raise DataFailure("division by zero")
      } else {
        whole_value(m % n)
      }
    },
    exact=decimal_rem,
    approx=(p, q) => p % q,
  )
}

///|
/// `-x`, and `+x` when `negate` is false.
pub fn neg(v : @value.Value, negate~ : Bool) -> @value.Value raise OpFailure {
  let x = boxed(v)
  match x {
    Null | Missing => x
    IntervalYM(_) | IntervalDT(_) =>
      if negate {
        match interval_neg(x) {
          Some(v) => v
          None => x
        }
      } else {
        x
      }
    _ =>
      match number(x) {
        None => raise TypeFailure("a sign on a value that is not a number")
        Some(_) if !negate => x
        Some(Whole(n)) => whole_value(-n)
        Some(Exact(d)) =>
          @value.Value::from_decimal(
            @ion_core.Decimal::Decimal(-d.coefficient(), d.exponent()),
          )
        Some(Approx(f)) => @value.Value::from_float(-f)
      }
  }
}

///|
/// `a || b`: the concatenation of two strings.
pub fn concat(
  a : @value.Value,
  b : @value.Value,
) -> @value.Value raise OpFailure {
  let x = boxed(a)
  let y = boxed(b)
  match (x, y) {
    (Missing, _) | (_, Missing) => Missing
    (Null, _) | (_, Null) => Null
    (String(p), String(q)) => @value.Value::from_string(p + q)
    _ => raise TypeFailure("|| on a value that is not a string")
  }
}

///|
/// `ABS(x)`: `+x` when x >= 0, else `-x`, so an integer result outside Int64
/// is an overflow as for `-x`. A decimal keeps its exponent; a negative zero
/// becomes positive (partiql-tests ion/abs.ion). An interval gets sign `+`.
pub fn abs(args : Array[@value.Value]) -> @value.Value raise OpFailure {
  apply("ABS", [Numeric], args, xs => {
    let v = xs[0]
    match v {
      Int(n) =>
        if n < 0L {
          whole_value(-@bigint.BigInt::from_int64(n))
        } else {
          v
        }
      BigInt(n) => if is_negative_int(n) { whole_value(-n) } else { v }
      Decimal(x) =>
        @value.Value::from_decimal(
          @ion_core.Decimal::Decimal(abs_int(x.coefficient()), x.exponent()),
        )
      Float(f) => @value.Value::from_float(f.abs())
      IntervalYM(i) if i.is_negative() => interval_abs(v)
      IntervalDT(i) if i.is_negative() => interval_abs(v)
      IntervalYM(_) | IntervalDT(_) => v
      _ => raise TypeFailure("ABS of a value that is not a number")
    }
  })
}

///|
fn interval_abs(v : @value.Value) -> @value.Value raise OpFailure {
  match interval_neg(v) {
    Some(x) => x
    None => raise TypeFailure("ABS of a value that is not an interval")
  }
}

///|
/// `MOD(a, b)`: the remainder with the sign of the dividend, for integers and
/// scale-0 decimals (SQL 6.28). A zero divisor is a `DataFailure`.
pub fn modulo(args : Array[@value.Value]) -> @value.Value raise OpFailure {
  apply("MOD", [Exact, Exact], args, xs => rem(xs[0], xs[1]))
}