///|
fn query_today(ctx : QueryContext) -> QueryValue {
  let n = local_date_number()
  match @ast.Date::new(n / 10000, n / 100 % 100, n % 100) {
    Some(d) => QDate(d)
    None => {
      query_error(ctx, "query.oracle_type_error: system clock unavailable")
      QNull
    }
  }
}

///|
fn query_interval(text : String) -> QueryValue {
  let normalized = String::from_iter(
    text.iter().map(fn(c) { if python_whitespace(c) { ' ' } else { c } }),
  )
  let words = normalized.split(" ").filter(fn(s) { s != "" }).to_array()
  if words.length() != 2 ||
    normalized.has_prefix(" ") ||
    normalized.has_suffix(" ") {
    return QNull
  }
  let unit = words[1].to_owned()
  if !["day", "days", "month", "months", "year", "years"].contains(unit) {
    return QNull
  }
  let raw = words[0].to_owned()
  let digits = if raw.has_prefix("+") || raw.has_prefix("-") {
    raw[1:].to_owned()
  } else {
    raw
  }
  if digits == "" || !digits.iter().all(fn(c) { c.is_ascii_digit() }) {
    return QNull
  }
  let n = @bigint.BigInt::from_string(
    if raw.has_prefix("+") {
      raw[1:].to_owned()
    } else {
      raw
    },
  )
  let zero = @bigint.BigInt::from_int(0)
  if unit.has_prefix("day") {
    QInterval(zero, n)
  } else {
    QInterval(
      if unit.has_prefix("year") {
        n.mul(@bigint.BigInt::from_int(12))
      } else {
        n
      },
      zero,
    )
  }
}

///|
fn interval_repr(months : @bigint.BigInt, days : @bigint.BigInt) -> String {
  let parts : Array[String] = []
  for
    (name, value) in [
      ("years", months.div(@bigint.BigInt::from_int(12))),
      ("months", months % @bigint.BigInt::from_int(12)),
      ("days", days),
    ] {
    if value != @bigint.BigInt::from_int(0) {
      parts.push(
        name +
        "=" +
        (if value > @bigint.BigInt::from_int(0) { "+" } else { "" }) +
        interval_number(value),
      )
    }
  }
  "relativedelta(" + parts.join(", ") + ")"
}

///|
fn floor_div(a : Int32, b : Int32) -> Int32 {
  let q = a / b
  if a % b < 0 {
    q - 1
  } else {
    q
  }
}

///|
fn query_date_bin(
  ctx : QueryContext,
  interval : QueryValue,
  date : @ast.Date,
  origin : @ast.Date,
) -> QueryValue {
  let interval = match interval {
    QText(s) => query_interval(s)
    other => other
  }
  let (months, days) = match interval {
    QInterval(months, days) => (months, days)
    _ => {
      query_error(
        ctx, "query.oracle_type_error: AttributeError invalid interval",
      )
      return QNull
    }
  }
  let zero = @bigint.BigInt::from_int(0)
  if (months == zero && days < zero) || (months < zero && days <= zero) {
    return QNull
  }
  if months == zero && days == zero {
    query_error(
      ctx, "query.oracle_type_error: ZeroDivisionError interval is zero",
    )
    return QNull
  }
  let unit = if months == zero { "days" } else { "months" }
  let amount = if months == zero { days } else { months }
  let n = match query_integer(QInt(amount)) {
    Some(n) => n
    None => {
      if unit == "days" && date.compare(origin) >= 0 {
        return QDate(origin)
      }
      query_error(
        ctx, "query.oracle_type_error: OverflowError interval out of range",
      )
      return QNull
    }
  }
  let result = if unit == "days" {
    ordinal_date(
      ordinal(origin) + floor_div(ordinal(date) - ordinal(origin), n) * n,
    )
  } else {
    let months = n
    if months > 120000 {
      query_error(ctx, "query.oracle_type_error: ValueError date out of range")
      return QNull
    }
    let mut current = origin
    if date.compare(origin) >= 0 {
      while true {
        let next = match
          add_interval_date(
            ctx,
            current,
            QInterval(@bigint.BigInt::from_int(months), days),
            false,
          ) {
          QDate(d) => Some(d)
          _ => None
        }
        match next {
          Some(next) => {
            if next.compare(current) < 0 {
              return QNull
            }
            if next.compare(current) == 0 {
              query_error(
                ctx, "query.oracle_type_error: ZeroDivisionError interval does not advance",
              )
              return QNull
            }
            if next.compare(date) >= 0 {
              break
            }
            current = next
          }
          None => {
            query_error(
              ctx, "query.oracle_type_error: ValueError date out of range",
            )
            return QNull
          }
        }
      }
    } else {
      while current.compare(date) > 0 {
        match
          add_interval_date(
            ctx,
            current,
            QInterval(@bigint.BigInt::from_int(months), days),
            true,
          ) {
          QDate(previous) => {
            if previous.compare(current) >= 0 {
              return QNull
            }
            current = previous
          }
          _ => {
            query_error(
              ctx, "query.oracle_type_error: ValueError date out of range",
            )
            return QNull
          }
        }
      }
    }
    QDate(current)
  }
  if result is QNull {
    query_error(ctx, "query.oracle_type_error: ValueError date out of range")
  }
  result
}

///|
fn interval_number(value : @bigint.BigInt) -> String {
  let raw = value.to_string()
  let negative = raw.has_prefix("-")
  let digits = if negative { raw[1:].to_owned() } else { raw }
  if digits.length() <= 6 {
    return raw
  }
  let rounded = @decimal.Decimal::new(
    if negative {
      value.neg()
    } else {
      value
    },
    0,
  ).round_to_significant(6)
  let text = rounded.to_string()
  let exponent = text.length() - 1
  let significant = text.trim_end(chars="0").to_owned()
  let fraction = if significant.length() > 1 {
    "." + significant[1:].to_owned()
  } else {
    ""
  }
  (if negative { "-" } else { "" }) +
  significant[:1].to_owned() +
  fraction +
  "e+" +
  (if exponent < 10 { "0" } else { "" }) +
  exponent.to_string()
}

///|
fn add_interval_date(
  ctx : QueryContext,
  date : @ast.Date,
  interval : QueryValue,
  subtract : Bool,
) -> QueryValue {
  guard interval is QInterval(months, days) else { return QNull }
  let months = if subtract { months.neg() } else { months }
  let days = if subtract { days.neg() } else { days }
  let m = match query_integer(QInt(months)) {
    Some(n) => n
    None => {
      query_error(ctx, "query.oracle_type_error: OverflowError interval months")
      return QNull
    }
  }
  if m > 120000 || m < -120000 {
    query_error(
      ctx, "query.oracle_type_error: ValueError interval year out of range",
    )
    return QNull
  }
  let total = date.year * 12 + date.month - 1 + m
  let year = floor_div(total, 12)
  let month = total - year * 12 + 1
  let shifted = @ast.Date::new(
    year,
    month,
    date.day.min(@ast.Date::days_in_month(year, month)),
  )
  guard shifted is Some(shifted) else {
    query_error(
      ctx, "query.oracle_type_error: ValueError interval year out of range",
    )
    return QNull
  }
  add_date_days(ctx, shifted, days)
}

///|
fn add_date_days(
  ctx : QueryContext,
  date : @ast.Date,
  days : @bigint.BigInt,
) -> QueryValue {
  let ordinal = @bigint.BigInt::from_int(ordinal(date)).add(days)
  if ordinal < @bigint.BigInt::from_int(1) ||
    ordinal > @bigint.BigInt::from_int(3652059) {
    query_error(ctx, "query.oracle_type_error: OverflowError date out of range")
    return QNull
  }
  ordinal_date(query_integer(QInt(ordinal)).unwrap())
}