///|
fn num_of(v : Value) -> Num? {
  match v {
    Int(n) => Some(num_of_int64(n))
    BigInt(n) => Some(num_of_bigint(n))
    Float(d) => Some(num_of_double(d))
    Decimal(d) => Some(num_of_decimal(d))
    _ => None
  }
}

///|
fn lowered(v : Value) -> Value? {
  try v.lower() catch {
    _ => None
  } noraise {
    x => Some(x)
  }
}

///|
/// Grouping equivalence for scalars, including unlowerable boxed Ion.
fn grouping_scalar(a : Value, b : Value) -> Bool {
  match (a, b) {
    (Null, Null) | (Missing, Missing) => return true
    (Float(x), Float(y)) if x.is_nan() && y.is_nan() => return true
    _ => ()
  }
  match (num_of(a), num_of(b)) {
    (Some(x), Some(y)) =>
      return !(x is NaN) && !(y is NaN) && compare_num(x, y) == 0
    (Some(_), None) | (None, Some(_)) => return false
    _ => ()
  }
  match (a, b) {
    (Bool(x), Bool(y)) => x == y
    (String(x), String(y)) => x == y
    (Clob(x) | Blob(x), Clob(y) | Blob(y)) => x == y
    (Date(x), Date(y)) => x == y
    (Time(x), Time(y)) =>
      match (x.offset, y.offset) {
        (Some(_), Some(_)) => x.utc_key() == y.utc_key()
        (None, None) => x.local_key() == y.local_key()
        _ => false
      }
    (Timestamp(x), Timestamp(y)) =>
      match (x.time.offset, y.time.offset) {
        (Some(_), Some(_)) => x.utc_key() == y.utc_key()
        (None, None) => x.local_key() == y.local_key()
        _ => false
      }
    (IntervalYM(x), IntervalYM(y)) => x == y
    (IntervalDT(x), IntervalDT(y)) => x == y
    (Ion(x), Ion(y)) => Value::from_ion(x) == Value::from_ion(y)
    _ => false
  }
}

///|
/// The PartiQL `=` operator: MISSING if either side is MISSING, else NULL if
/// either side is NULL, else a Bool. Never fails on mixed types.
pub fn sql_equals(a : Value, b : Value) -> Value {
  match (lowered(a), lowered(b)) {
    (Some(x), Some(y)) =>
      match (x, y) {
        (Missing, _) | (_, Missing) => Missing
        (Null, _) | (_, Null) => Null
        _ =>
          Value::from_bool(
            !(x is Float(f) && f.is_nan()) &&
            !(y is Float(f) && f.is_nan()) &&
            eqg(x, y),
          )
      }
    _ => Value::from_bool(a == b)
  }
}

///|
/// Grouping equivalence: NULL eqg NULL, MISSING eqg MISSING, NaN eqg NaN;
/// otherwise the same as `=`.
pub fn eqg(a : Value, b : Value) -> Bool {
  equal_values(a, b, identity=false)
}

///|
/// The digits of an exact number without trailing zeros of its coefficient,
/// so that numbers that are equal have one key.
fn number_key(n : Num) -> String {
  match n {
    NaN => "nan"
    NegInf => "-inf"
    PosInf => "+inf"
    Finite(c, e) => {
      let text = c.to_string()
      let mut end = text.length()
      let mut exponent = e
      while end > 1 && text[end - 1] == '0' {
        end -= 1
        exponent += 1
      }
      "n\{text[:end].to_owned()}e\{exponent}"
    }
  }
}

///|
fn bytes_key(b : Bytes) -> String {
  let out = StringBuilder()
  for i in 0.. String {
  let (s, n) = if t.offset is Some(_) { t.utc_key() } else { t.local_key() }
  "\{t.offset is Some(_)}:\{s}:\{n}"
}

///|
fn group_key_at(v : Value, depth : Int32) -> String {
  let sub = (x : Value) => group_key_at(x, depth - 1)
  match lowered(v) {
    None => "x"
    Some(x) =>
      match x {
        Null => "N"
        Missing => "M"
        Bool(b) => "b\{b}"
        String(s) => "s\{s}"
        Clob(b) | Blob(b) => "y" + bytes_key(b)
        Date(d) => "d\{d.year}-\{d.month}-\{d.day}"
        Time(t) => "t" + clock_key(t)
        Timestamp(t) => {
          let (s, n) = if t.time.offset is Some(_) {
            t.utc_key()
          } else {
            t.local_key()
          }
          "T\{t.time.offset is Some(_)}:\{s}:\{n}"
        }
        IntervalYM(i) => "ym\{i.months}"
        IntervalDT(i) => "dt\{i.seconds}:\{i.nanos}"
        List(items) =>
          if depth > 0 {
            "l[" + items.map(sub).join(",") + "]"
          } else {
            "l\{items.length()}"
          }
        Bag(b) =>
          if depth > 0 {
            let keys = b.items().map(sub)
            keys.sort()
            "b[" + keys.join(",") + "]"
          } else {
            "g\{b.length()}"
          }
        Tuple(t) =>
          if depth > 0 {
            let keys = t.fields().map(f => "\{f.0}=" + sub(f.1))
            keys.sort()
            "u[" + keys.join(",") + "]"
          } else {
            "u\{t.length()}"
          }
        Map(m) =>
          if depth > 0 {
            let keys = m.entries().map(e => sub(e.0) + "=" + sub(e.1))
            keys.sort()
            "m[" + keys.join(",") + "]"
          } else {
            "m\{m.entries().length()}"
          }
        Graph(_) => "G"
        Ion(_) => "x"
        _ =>
          match num_of(x) {
            Some(n) => number_key(n)
            None => "?"
          }
      }
  }
}

///|
/// A key that values which are `eqg` share, for hashing (the DISTINCT of
/// SELECT, GROUP BY). It reads two levels into a collection, deeper values
/// by kind and size only, so it takes no deep recursion; values that differ
/// only below that share a key.
pub fn group_key(v : Value) -> String {
  group_key_at(v, 2)
}