///|
fn query_int(n : Int32) -> QueryValue {
  QInt(@bigint.BigInt::from_int(n))
}

///|
fn query_integer(value : QueryValue) -> Int32? {
  match value {
    QBool(b) => Some(if b { 1 } else { 0 })
    QInt(n) => {
      let d = @decimal.Decimal::new(n, 0)
      if d.less_than(@decimal.Decimal::from_int(-2147483647)) ||
        @decimal.Decimal::from_int(2147483647).less_than(d) {
        None
      } else {
        Some(bql_parse_int(n.to_string()))
      }
    }
    _ => None
  }
}

///|
fn truth(value : QueryValue) -> Bool {
  match value {
    QNull => false
    QBool(b) => b
    QText(s) => s != ""
    QInterval(m, d) =>
      m != @bigint.BigInt::from_int(0) || d != @bigint.BigInt::from_int(0)
    QNegativeZero(_) => false
    QNumber(n) => !n.is_zero()
    QInt(n) => n != @bigint.BigInt::from_int(0)
    QDict(v) | QRecord(_, v) => !v.is_empty()
    QList(v) => !v.is_empty()
    QSet(v) | QFrozenSet(v) => !v.is_empty()
    QAmount(a) => !a.number.is_zero()
    QPosition(p) => !p.units.number.is_zero()
    QInventory(v) => !v.is_empty()
    _ => true
  }
}

///|
fn ordinal(d : @ast.Date) -> Int32 {
  let y = d.year - 1
  let mut result = y * 365 + y / 4 - y / 100 + y / 400 + d.day
  for m in 1.. QueryValue {
  if n < 1 || n > 3652059 {
    return QNull
  }
  let mut lo = 1
  let mut hi = 10000
  while lo + 1 < hi {
    let mid = (lo + hi) / 2
    if ordinal(@ast.Date::new(mid, 1, 1).unwrap()) <= n {
      lo = mid
    } else {
      hi = mid
    }
  }
  let mut day = n - ordinal(@ast.Date::new(lo, 1, 1).unwrap()) + 1
  let mut month = 1
  while day > @ast.Date::days_in_month(lo, month) {
    day = day - @ast.Date::days_in_month(lo, month)
    month = month + 1
  }
  QDate(@ast.Date::new(lo, month, day).unwrap())
}

///|
fn metadata_function(
  ctx : QueryContext,
  name : String,
  values : Array[QueryValue],
  row : BqlRow,
) -> QueryValue {
  let key = match values[0] {
    QText(s) => s
    _ => return QNull
  }
  if name == "any_meta" {
    if row.extra is Some(extra) && extra.cells is Some(cells) {
      if dict_get(cells.get("meta").unwrap_or(QNull), key) is Some(value) {
        return value
      }
      let entry_meta = record_metadata(ctx, cells.get("entry").unwrap_or(QNull))
      return dict_get(entry_meta, key).unwrap_or(QNull)
    }
    if row_posting(row) is Some(p) {
      for (k, v) in source_metadata(p.loc, p.meta) {
        if k == key {
          return v
        }
      }
    }
    return match row_entry(row) {
      Some(e) => dict_get(entry_metadata(e), key).unwrap_or(QNull)
      None => QNull
    }
  }
  let entry = if name == "commodity_meta" || name == "currency_meta" {
    let mut found = None
    for e in ctx.realized.entries {
      if e.directive is DCommodity(c) && c.currency == key {
        found = Some(e)
      }
    }
    found
  } else {
    ensure_account_index(ctx)
    if name == "close_date" {
      ctx.closes.unwrap().get(key)
    } else {
      ctx.opens.unwrap().get(key)
    }
  }
  match entry {
    None => QNull
    Some(e) =>
      if name == "open_date" || name == "close_date" {
        QDate(e.date)
      } else {
        let meta = entry_metadata(e)
        if values.length() == 2 {
          match values[1] {
            QText(k) => dict_get(meta, k).unwrap_or(QNull)
            _ => QNull
          }
        } else {
          meta
        }
      }
  }
}

///|
fn map_positions(
  value : QueryValue,
  transform : (@booking.Position) -> @booking.Position?,
) -> QueryValue {
  match value {
    QPosition(p) =>
      match transform(p) {
        Some(p) => QPosition(p)
        None => QNull
      }
    QInventory(inv) => {
      let result = @booking.Inventory::new()
      for p in inv.positions {
        if transform(p) is Some(p) {
          result.add(p)
        }
      }
      QInventory(result)
    }
    _ => QNull
  }
}

///|
fn eval_function(
  ctx : QueryContext,
  name : String,
  values : Array[QueryValue],
  row : BqlRow,
) -> QueryValue {
  if name == "coalesce" {
    if values.is_empty() {
      query_error(ctx, "query.oracle_type_error: IndexError coalesce arguments")
      return QNull
    }
    for value in values {
      if !(value is QNull) {
        return value
      }
    }
    return QNull
  }
  if values.any(fn(v) { v is QNull }) {
    return QNull
  }
  if [
      "any_meta", "open_meta", "open_date", "close_date", "commodity_meta", "currency_meta",
    ].contains(name) {
    return metadata_function(ctx, name, values, row)
  }
  if (name == "str" || name == "repr") && !values.all(portable_repr) {
    query_error(
      ctx, "unsupported query.unsupported.set_representation: CPython hash-seed iteration order",
    )
    return QNull
  }
  match (name, values) {
    ("__in" | "__notin", [item, container]) =>
      match membership(ctx, item, container) {
        QBool(b) => QBool(if name == "__notin" { !b } else { b })
        other => other
      }
    ("__between", [item, low, high]) =>
      match (value_compare(item, low), value_compare(item, high)) {
        (Some(a), Some(b)) => QBool(a >= 0 && b <= 0)
        _ => QNull
      }
    ("parse_date", [QText(text)]) => flexible_date(ctx, text)
    ("parse_date", [QText(text), QText(format)]) =>
      strptime_date(ctx, text, format)
    ("today", []) => query_today(ctx)
    ("interval", [QText(s)]) => query_interval(s)
    ("date_bin", [interval, QDate(date), QDate(origin)]) =>
      query_date_bin(ctx, interval, date, origin)
    ("subst", [QText(pattern), QText(replacement), QText(text)]) =>
      query_substitute(ctx, pattern, replacement, text)
    ("grep" | "grepn" | "findfirst" | "has_account", _) =>
      regex_function(ctx, name, values, row)
    ("day", [QInterval(_, _)]) => QNull
    ("quarter" | "weekday" | "yearmonth", [QInterval(_, _)])
    | ("date_bin", [_, QInterval(_, _), _])
    | ("date_bin", [_, _, QInterval(_, _)]) => {
      query_error(
        ctx, "query.oracle_type_error: TypeError date operation on interval",
      )
      QNull
    }
    ("date_part" | "date_trunc", [QText(part), QInterval(_, _)]) => {
      if name == "date_trunc" ||
        [
          "quarter", "week", "dow", "doy", "isoyear", "decade", "century", "millennium",
        ].contains(part) {
        query_error(
          ctx, "query.oracle_type_error: TypeError date operation on interval",
        )
      }
      QNull
    }
    ("date_part" | "date_trunc", [QText(part), QDate(date)]) =>
      calendar_function(ctx, name, part, date)
    ("joinstr", [QSet(items)]) | ("joinstr", [QFrozenSet(items)]) =>
      if items.length() > 1 {
        query_error(
          ctx, "unsupported query.unsupported.set_representation: joinstr depends on CPython hash seed",
        )
        QNull
      } else {
        QText(items.join(","))
      }
    ("maxwidth", [QText(text), width]) => query_maxwidth(ctx, text, width)
    ("getprice", [QText(base), QText(quote)])
    | ("getprice", [QText(base), QText(quote), _]) =>
      match
        query_price(
          ctx,
          base,
          quote,
          if values.length() == 3 && values[2] is QDate(d) {
            Some(d)
          } else {
            None
          },
        ) {
        Some(n) => QNumber(n)
        None => QNull
      }
    ("convert", [value, QText(target)])
    | ("convert", [value, QText(target), _]) =>
      query_conversion(
        ctx,
        value,
        Some(target),
        if values.length() == 3 && values[2] is QDate(d) {
          Some(d)
        } else {
          None
        },
      )
    ("value", [value]) | ("value", [value, _]) =>
      query_conversion(
        ctx,
        value,
        None,
        if values.length() == 2 && values[1] is QDate(d) {
          Some(d)
        } else {
          None
        },
      )
    ("possign", [value, QText(account)]) => {
      let root = @ast.account_root(account)
      if root == "Assets" || root == "Expenses" {
        value
      } else {
        match value {
          QAmount(a) =>
            QAmount(
              @ast.Amount::new(
                a.number.neg().round_to_significant(28),
                a.currency,
              ),
            )
          _ => eval_function(ctx, "neg", [value], row)
        }
      }
    }
    ("account_sortkey", [QText(account)]) => {
      let root = @ast.account_root(account)
      let defaults = ["Assets", "Liabilities", "Equity", "Income", "Expenses"]
      let names = [
        "name_assets", "name_liabilities", "name_equity", "name_income", "name_expenses",
      ]
      for i in 0.. {
      if separator == "" {
        query_error(ctx, "query.oracle_type_error: ValueError empty separator")
        return QNull
      }
      let parts = s.split(separator).to_array()
      match query_integer(index) {
        Some(i) => {
          let n = if i < 0 { parts.length() + i } else { i }
          if n < 0 || n >= parts.length() {
            query_error(
              ctx, "query.oracle_type_error: IndexError split component",
            )
            QNull
          } else {
            QText(parts[n].to_owned())
          }
        }
        None => {
          query_error(
            ctx, "query.oracle_type_error: IndexError split component",
          )
          QNull
        }
      }
    }
    ("round", [v]) | ("round", [v, _]) => {
      let digits = if values.length() == 1 {
        Some(0)
      } else {
        query_integer(values[1])
      }
      match (value_number(v), digits) {
        (Some(n), Some(digits)) => {
          if (v is QInt(_) || v is QBool(_)) && digits >= 0 {
            return QInt(n.mantissa())
          }
          if digits > 1000000 || digits < -999999 {
            query_error(
              ctx, "unsupported query.unsupported.decimal_representation: rounding resource bound",
            )
            return QNull
          }
          let magnitude = n
            .mantissa()
            .to_string()
            .replace_all(old="-", new="")
            .length()
          if v is QNumber(_) &&
            !n.is_zero() &&
            magnitude + digits - n.scale() > 28 {
            query_error(
              ctx, "query.oracle_type_error: InvalidOperation decimal precision",
            )
            return QNull
          }
          let rounded = if n.is_zero() || n.scale() - digits > magnitude + 1 {
            @decimal.Decimal::zero_scaled(digits)
          } else {
            n.quantize(digits)
          }
          if v is QNumber(_) &&
            !rounded.is_zero() &&
            rounded.mantissa().to_string().replace_all(old="-", new="").length() >
            28 {
            query_error(
              ctx, "query.oracle_type_error: InvalidOperation decimal precision",
            )
            return QNull
          }
          if (v is QNumber(_) || v is QNegativeZero(_)) &&
            rounded.is_zero() &&
            (n.is_negative() || v is QNegativeZero(_)) {
            QNegativeZero(digits)
          } else if v is QInt(_) || v is QBool(_) {
            QInt(
              @decimal.Decimal::parse(rounded.to_string()).unwrap().mantissa(),
            )
          } else {
            QNumber(rounded)
          }
        }
        _ => {
          query_error(
            ctx, "unsupported query.unsupported.decimal_representation: rounding resource bound",
          )
          QNull
        }
      }
    }
    ("str", [v]) =>
      QText(
        if v is QInventory(_) || v is QSet(_) {
          python_repr(v)
        } else {
          v.to_text()
        },
      )
    ("repr", [v]) => QText(python_repr(v))
    ("bool", [QInventory(_)]) => {
      query_error(
        ctx, "query.oracle_type_error: NotImplementedError inventory truth",
      )
      QNull
    }
    ("bool", [v]) => QBool(truth(v))
    ("int" | "decimal", [QText(s)]) =>
      python_number_string(ctx, s, name == "int")
    ("int" | "decimal", [v]) => {
      let n = match v {
        QInt(n) => Some(@decimal.Decimal::new(n, 0))
        QNegativeZero(scale) => Some(@decimal.Decimal::zero_scaled(scale))
        QNumber(n) => Some(n)
        QBool(b) => Some(@decimal.Decimal::from_int(if b { 1 } else { 0 }))
        _ => None
      }
      match n {
        Some(n) =>
          if name == "decimal" {
            if v is QNegativeZero(scale) {
              QNegativeZero(scale)
            } else {
              QNumber(n)
            }
          } else {
            QInt(
              @decimal.Decimal::parse(
                n.to_string().split(".").to_array()[0].to_owned(),
              )
              .unwrap()
              .mantissa(),
            )
          }
        None => QNull
      }
    }
    ("date", [QDate(d)]) => QDate(d)
    ("date", [QText(s)]) =>
      match @ast.Date::parse(s) {
        Some(d) => QDate(d)
        None => QNull
      }
    ("date", [v]) =>
      match v {
        QDate(d) => QDate(d)
        QText(s) =>
          match @ast.Date::parse(s) {
            Some(d) => QDate(d)
            None => QNull
          }
        _ => QNull
      }
    ("date", [y, m, d]) =>
      match (query_integer(y), query_integer(m), query_integer(d)) {
        (Some(y), Some(m), Some(d)) =>
          match @ast.Date::new(y, m, d) {
            Some(d) => QDate(d)
            None => QNull
          }
        _ => QNull
      }
    ("day", [QDate(d)]) => query_int(d.day)
    ("quarter", [QDate(d)]) =>
      QText(
        d.to_string()[:4].to_owned() +
        "-Q" +
        ((d.month - 1) / 3 + 1).to_string(),
      )
    ("weekday", [QDate(d)]) =>
      QText(
        ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"][(ordinal(d) - 1) % 7],
      )
    ("yearmonth", [QDate(d)]) =>
      QDate(@ast.Date::new(d.year, d.month, 1).unwrap())
    ("date_add", [QInterval(m, d), QInt(n)]) =>
      arithmetic(ctx, QInterval(m, d), "+", QInt(n))
    ("date_diff", [QInterval(_, a), QInterval(_, b)]) => QInt(a.sub(b))
    ("date_diff", [QInterval(_, _), _]) | ("date_diff", [_, QInterval(_, _)]) => {
      query_error(
        ctx, "query.oracle_type_error: TypeError date difference on interval",
      )
      QNull
    }
    ("date_add", [QDate(d), v]) =>
      match query_integer(v) {
        Some(n) =>
          if n > 3652059 || n < -3652059 {
            query_error(
              ctx, "query.oracle_type_error: OverflowError date value out of range",
            )
            QNull
          } else if ordinal(d) + n < 1 || ordinal(d) + n > 3652059 {
            query_error(
              ctx, "query.oracle_type_error: OverflowError date value out of range",
            )
            QNull
          } else {
            ordinal_date(ordinal(d) + n)
          }
        None => {
          query_error(
            ctx, "query.oracle_type_error: OverflowError date value out of range",
          )
          QNull
        }
      }
    ("date_diff", [QDate(a), QDate(b)]) => query_int(ordinal(a) - ordinal(b))
    ("root", [QText(s), v]) =>
      match query_integer(v) {
        Some(n) => {
          let parts = s.split(":").to_array()
          let end = if n < 0 {
            (parts.length() + n).max(0)
          } else {
            n.min(parts.length())
          }
          QText(parts[:end].to_owned().map(fn(s) { s.to_owned() }).join(":"))
        }
        None =>
          match v {
            QInt(n) => QText(if n.compare_int(0) > 0 { s } else { "" })
            _ => QNull
          }
      }
    ("number", [QAmount(a)]) => QNumber(a.number)
    ("currency" | "commodity", [QAmount(a)]) => QText(a.currency)
    ("empty", [QInventory(i)]) => QBool(i.is_empty())
    ("only", [QText(currency), QInventory(i)]) =>
      QAmount(@ast.Amount::new(i.quantity(currency), currency))
    ("filter_currency", [v, QText(currency)]) =>
      map_positions(v, fn(p) {
        if p.units.currency == currency {
          Some(p)
        } else {
          None
        }
      })
    ("units" | "cost", [v]) =>
      match v {
        QPosition(p) =>
          QAmount(
            if name == "cost" && p.cost is Some(c) {
              @ast.Amount::new(p.units.number.mul(c.number), c.currency)
            } else {
              p.units
            },
          )
        QInventory(_) =>
          map_positions(v, fn(p) {
            Some(
              @booking.Position::new(
                if name == "cost" && p.cost is Some(c) {
                  @ast.Amount::new(p.units.number.mul(c.number), c.currency)
                } else {
                  p.units
                },
                None,
              ),
            )
          })
        _ => QNull
      }
    ("neg", [QAmount(a)]) =>
      QAmount(
        @ast.Amount::new(a.number.neg().round_to_significant(28), a.currency),
      )
    ("neg" | "abs", [QNegativeZero(scale)]) =>
      QNumber(@decimal.Decimal::zero_scaled(scale))
    ("neg" | "abs", [QNumber(n)]) =>
      QNumber(
        (if name == "neg" { n.neg() } else { n.abs() }).round_to_significant(28),
      )
    ("neg" | "abs", [v]) =>
      map_positions(v, fn(p) {
        Some(
          @booking.Position::new(
            @ast.Amount::new(
              if name == "neg" {
                p.units.number.neg().round_to_significant(28)
              } else {
                p.units.number.abs().round_to_significant(28)
              },
              p.units.currency,
            ),
            p.cost,
          ),
        )
      })
    ("safediv", [QNegativeZero(_), b]) =>
      if value_number(b).map(fn(n) { n.is_zero() }).unwrap_or(false) {
        QNumber(@decimal.Decimal::zero())
      } else {
        arithmetic(ctx, values[0], "/", b)
      }
    ("safediv", [a, b]) =>
      match (value_number(a), value_number(b)) {
        (Some(a), Some(b)) =>
          if b.is_zero() {
            QNumber(@decimal.Decimal::zero())
          } else if a.is_zero() {
            let zero = @decimal.Decimal::zero_scaled(a.scale() - b.scale())
            if b.is_negative() {
              QNegativeZero(zero.scale())
            } else {
              QNumber(zero)
            }
          } else {
            QNumber(a.div(b).unwrap())
          }
        _ => QNull
      }
    _ => {
      query_error(ctx, "unsupported query.unsupported.function `" + name + "`")
      QNull
    }
  }
}