///|
/// Whether two values can be ordered: two numbers, two strings, two
/// datetimes of the same kind or two booleans.
fn orderable(x : @value.Value, y : @value.Value) -> Bool {
  match (x, y) {
    (Bool(_), Bool(_))
    | (String(_), String(_))
    | (Date(_), Date(_))
    | (Time(_), Time(_))
    | (Timestamp(_), Timestamp(_))
    | (Date(_), Timestamp(_))
    | (Timestamp(_), Date(_))
    | (IntervalYM(_), IntervalYM(_))
    | (IntervalDT(_), IntervalDT(_)) => true
    _ => number(x) is Some(_) && number(y) is Some(_)
  }
}

///|
fn is_nan(v : @value.Value) -> Bool {
  v is Float(f) && f.is_nan()
}

///|
/// Orders two values; `holds` decides from the sign of the comparison. Any NaN
/// makes every ordering false.
fn ordered(
  a : @value.Value,
  b : @value.Value,
  holds : (Int32) -> Bool,
) -> @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
  }
  if !orderable(x, y) {
    raise TypeFailure("an ordering of values that cannot be compared")
  }
  if is_nan(x) || is_nan(y) {
    return @value.Value::from_bool(false)
  }
  match datetime_order(x, y) {
    Some(c) => @value.Value::from_bool(holds(c))
    None =>
      @value.Value::from_bool(holds(@value.compare(x, y, nulls=NullsFirst)))
  }
}

///|
/// The `<` predicate. Unbox Ion; propagate MISSING before NULL.
/// Return false for NaN and raise `TypeFailure` for incomparable kinds.
pub fn less(a : @value.Value, b : @value.Value) -> @value.Value raise OpFailure {
  ordered(a, b, c => c < 0)
}

///|
/// The `<=` predicate, with the same absence, NaN and type rules as `less`.
pub fn less_eq(
  a : @value.Value,
  b : @value.Value,
) -> @value.Value raise OpFailure {
  ordered(a, b, c => c <= 0)
}

///|
/// The `>` predicate, with the same absence, NaN and type rules as `less`.
pub fn greater(
  a : @value.Value,
  b : @value.Value,
) -> @value.Value raise OpFailure {
  ordered(a, b, c => c > 0)
}

///|
/// The `>=` predicate, with the same absence, NaN and type rules as `less`.
pub fn greater_eq(
  a : @value.Value,
  b : @value.Value,
) -> @value.Value raise OpFailure {
  ordered(a, b, c => c >= 0)
}

///|
/// A value lowered one level; a symbol without text stays as it is.
fn shallow(v : @value.Value) -> @value.Value {
  v.lower() catch {
    _ => v
  }
}

///|
/// The equality of two elements of a collection: the top-level `=` with the
/// datetime rule, recursively, where NULL and MISSING are equal to each
/// other (partiql-tests nary-operators.ion,
/// `equalListDifferentTypesWithNullMissingEquivalenceTrue`) and NaN equals
/// NaN. It is an equivalence relation.
fn same(a : @value.Value, b : @value.Value) -> Bool {
  @value.equivalent_by(a, b, (x, y) => {
    match (x, y) {
      (Null | Missing, Null | Missing) => Some(true)
      (Null | Missing, _) | (_, Null | Missing) => Some(false)
      // Graph payloads retain grouping equivalence, as before.
      (Graph(_), Graph(_)) => Some(@value.eqg(x, y))
      _ => datetime_equal(x, y).map(r => r is Bool(true))
    }
  })
}

///|
/// `=`: NULL if either side is NULL, else MISSING if either is MISSING, else
/// a Bool; it never fails on mixed types (partiql-tests null.ion). A NaN
/// equals nothing. Collections compare their elements with `same`, so
/// `[DATE '2020-01-01'] = [TIMESTAMP '2020-01-01 00:00:00']` as the
/// elements do.
pub fn equal(a : @value.Value, b : @value.Value) -> @value.Value {
  let x = shallow(a)
  let y = shallow(b)
  match (x, y) {
    (Null, _) | (_, Null) => Null
    (Missing, _) | (_, Missing) => Missing
    _ => @value.Value::from_bool(!is_nan(x) && !is_nan(y) && same(x, y))
  }
}

///|
/// `<>`: the negation of `=`, keeping an absent result.
pub fn not_equal(a : @value.Value, b : @value.Value) -> @value.Value {
  match equal(a, b) {
    Bool(x) => @value.Value::from_bool(!x)
    other => other
  }
}

///|
/// A binary operator by its syntax.
pub fn binary(
  op : @ast.BinaryOp,
  a : @value.Value,
  b : @value.Value,
) -> @value.Value raise OpFailure {
  match op {
    Add => add(a, b)
    Sub => sub(a, b)
    Mul => mul(a, b)
    Div => div(a, b)
    Mod => rem(a, b)
    Concat => concat(a, b)
    Eq => equal(a, b)
    Ne => not_equal(a, b)
    Lt => less(a, b)
    Le => less_eq(a, b)
    Gt => greater(a, b)
    Ge => greater_eq(a, b)
  }
}

///|
/// `x IN c` for a list, bag or row `c`: TRUE if some element `=` `x`, else
/// NULL if some comparison is absent, else FALSE.
pub fn in_(x : @value.Value, c : @value.Value) -> @value.Value raise OpFailure {
  let needle = boxed(x)
  let haystack = boxed(c)
  if needle is Missing || haystack is Missing {
    return Missing
  }
  if needle is Null || haystack is Null {
    return Null
  }
  let items : Array[@value.Value] = match haystack {
    List(items) => items
    Bag(bag) => Array::from_iter(bag.items().iter())
    _ => raise TypeFailure("IN of a value that is not a collection")
  }
  let mut absent = false
  for item in items {
    match equal(needle, item) {
      Bool(true) => return @value.Value::from_bool(true)
      Null | Missing => absent = true
      _ => ()
    }
  }
  if absent {
    Null
  } else {
    @value.Value::from_bool(false)
  }
}