///|
fn literal_value(expr : Expr) -> QueryValue? {
  match expr {
    EStr(s) => Some(QText(s))
    EDate(d) => Some(QDate(d))
    EBool(b) => Some(QBool(b))
    ENull => Some(QNull)
    ENum(n) =>
      Some(if n.scale() == 0 { QInt(n.mantissa()) } else { QNumber(n) })
    _ => None
  }
}

///|
fn membership_equal(a : QueryValue, b : QueryValue) -> Bool {
  match (a, b) {
    (QNull, QNull) => true
    (QSet(a) | QFrozenSet(a), QSet(b) | QFrozenSet(b)) =>
      a.length() == b.length() && a.all(fn(s) { b.contains(s) })
    (QList(a), QList(b)) =>
      a.length() == b.length() &&
      a.iter().zip(b.iter()).all(fn(p) { membership_equal(p.0, p.1) })
    (QDict(a), QDict(b)) =>
      a.length() == b.length() &&
      a.all(fn(p) { b.any(fn(q) { p.0 == q.0 && membership_equal(p.1, q.1) }) })
    (QRecord(_, a), QRecord(_, b)) =>
      a.length() == b.length() &&
      a.iter().zip(b.iter()).all(fn(p) { membership_equal(p.0.1, p.1.1) })
    (QEnum(a, x), QEnum(b, y)) => a == b && x == y
    (QPythonType(a), QPythonType(b)) => a == b
    (QAmount(a), QAmount(b)) =>
      a.currency == b.currency && a.number.compare(b.number) == 0
    (QPosition(a), QPosition(b)) =>
      membership_equal(QAmount(a.units), QAmount(b.units)) &&
      (match (a.cost, b.cost) {
        (None, None) => true
        (Some(a), Some(b)) =>
          a.number.compare(b.number) == 0 &&
          a.currency == b.currency &&
          a.date.map(fn(d) { d.to_string() }) ==
          b.date.map(fn(d) { d.to_string() }) &&
          a.label == b.label
        _ => false
      })
    (QInventory(a), QInventory(b)) =>
      a.positions.length() == b.positions.length() &&
      a.positions.all(fn(p) {
        b.positions.any(fn(q) { membership_equal(QPosition(p), QPosition(q)) })
      })
    _ => value_compare(a, b) is Some(0)
  }
}

///|
fn query_comparison(
  ctx : QueryContext,
  a : QueryValue,
  op : String,
  b : QueryValue,
) -> QueryValue {
  if a is QNull || b is QNull {
    return QNull
  }
  if a is QInterval(am, ad) && b is QInterval(bm, bd) {
    if ["=", "==", "!=", "<>"].contains(op) {
      let equal = am == bm && ad == bd
      return QBool(if op == "!=" || op == "<>" { !equal } else { equal })
    }
    query_error(ctx, "query.oracle_type_error: TypeError interval ordering")
    return QNull
  }

  if ["=", "==", "!=", "<>"].contains(op) &&
    (
      structural_value(a) ||
      structural_value(b) ||
      a is QInventory(_) ||
      b is QInventory(_)
    ) {
    let equal = membership_equal(a, b)
    return QBool(if op == "!=" || op == "<>" { !equal } else { equal })
  }
  match (a, b) {
    (QSet(x) | QFrozenSet(x), QSet(y) | QFrozenSet(y)) => {
      let subset = x.all(fn(s) { y.contains(s) })
      let superset = y.all(fn(s) { x.contains(s) })
      return QBool(
        match op {
          "=" | "==" => subset && superset
          "!=" | "<>" => !(subset && superset)
          "<" => subset && !superset
          "<=" => subset
          ">" => superset && !subset
          ">=" => superset
          _ => false
        },
      )
    }
    _ => ()
  }
  match typed_compare(ctx, a, b) {
    None => QNull
    Some(n) =>
      QBool(
        match op {
          "=" | "==" => n == 0
          "!=" | "<>" => n != 0
          "<" => n < 0
          "<=" => n <= 0
          ">" => n > 0
          ">=" => n >= 0
          _ => false
        },
      )
  }
}

///|
fn membership(
  ctx : QueryContext,
  item : QueryValue,
  container : QueryValue,
) -> QueryValue {
  let found = match container {
    QList(values) => values.any(fn(v) { membership_equal(item, v) })
    QSet(values) | QFrozenSet(values) =>
      match item {
        QText(s) => values.contains(s)
        _ => false
      }
    QDict(values) =>
      match item {
        QText(s) => values.any(fn(p) { p.0 == s })
        _ => false
      }
    QText(text) =>
      match item {
        QText(s) => text.contains(s)
        _ => {
          query_error(
            ctx, "query.oracle_type_error: TypeError string membership requires string",
          )
          return QNull
        }
      }
    QAmount(a) =>
      membership_equal(item, QNumber(a.number)) ||
      membership_equal(item, QText(a.currency))
    QRecord(_, fields) => fields.any(fn(p) { membership_equal(item, p.1) })
    QInventory(_) => {
      if item is QList(_) || item is QDict(_) || item is QInventory(_) {
        query_error(
          ctx, "query.oracle_type_error: TypeError unhashable inventory key",
        )
        return QNull
      }
      false
    }
    QPosition(p) =>
      match item {
        QAmount(a) =>
          a.currency == p.units.currency && a.number.equal(p.units.number)
        _ => {
          query_error(
            ctx, "query.oracle_type_error: AttributeError position membership requires amount",
          )
          return QNull
        }
      }
    _ => {
      query_error(
        ctx, "query.oracle_type_error: TypeError value is not iterable",
      )
      return QNull
    }
  }
  QBool(found)
}

///|
fn decimal_modulo(
  ctx : QueryContext,
  a : @decimal.Decimal,
  b : @decimal.Decimal,
) -> @decimal.Decimal? {
  if b.is_zero() {
    return None
  }
  let scale = a.scale().max(b.scale())
  let x = a.shift_scale(scale - a.scale()).mantissa()
  let y = b.shift_scale(scale - b.scale()).mantissa()
  let quotient = x.div(y).to_string().replace_all(old="-", new="")
  if quotient.length() > 28 {
    query_error(
      ctx, "query.oracle_type_error: InvalidOperation DivisionImpossible",
    )
    return None
  }
  Some(@decimal.Decimal::new(x % y, scale).round_to_significant(28))
}