///|
let month_names : Array[String] = [
  "january", "february", "march", "april", "may", "june", "july", "august", "september",
  "october", "november", "december",
]

///|
fn month_number(text : String) -> Int32? {
  let lower = text.to_lower()
  for i in 0..<12 {
    if lower == month_names[i] ||
      lower == month_names[i][:3].to_owned() ||
      (i == 8 && lower == "sept") {
      return Some(i + 1)
    }
  }
  None
}

///|
fn parsed_date(
  ctx : QueryContext,
  year : Int32,
  month : Int32,
  day : Int32,
) -> QueryValue {
  match @ast.Date::new(year, month, day) {
    Some(d) => QDate(d)
    None => {
      query_error(
        ctx, "query.oracle_type_error: ValueError invalid parsed date",
      )
      QNull
    }
  }
}

///|
fn strptime_date(
  ctx : QueryContext,
  text : String,
  format : String,
) -> QueryValue {
  let format = format
    .replace_all(old="%c", new="%a %b %d %H:%M:%S %Y")
    .replace_all(old="%x", new="%m/%d/%y")
    .replace_all(old="%X", new="%H:%M:%S")
  let pattern = StringBuilder()
  pattern.write_char('^')
  let fields : Array[Char] = []
  let mut i = 0
  while i < format.length() {
    let c = format.get_char(i).unwrap()
    i = i + 1
    if c == '%' {
      if i == format.length() {
        query_error(ctx, "query.oracle_type_error: ValueError stray percent")
        return QNull
      }
      let field = format.get_char(i).unwrap()
      i = i + 1
      if field == '%' {
        pattern.write_char('%')
        continue
      }
      if fields.contains(field) {
        query_error(
          ctx, "query.oracle_type_error: error duplicate format directive",
        )
        return QNull
      }
      let part = match field {
        'Y' | 'G' => "[0-9][0-9][0-9][0-9]"
        'y' => "[0-9][0-9]"
        'd' => " ?[0-9][0-9]?"
        'm' | 'H' | 'I' | 'M' | 'S' | 'U' | 'W' | 'V' => "[0-9][0-9]?"
        'j' => "[0-9][0-9]?[0-9]?"
        'f' => "[0-9][0-9]?[0-9]?[0-9]?[0-9]?[0-9]?"
        'w' | 'u' => "[0-9]"
        'a' | 'A' | 'b' | 'B' | 'p' | 'Z' => "[a-zA-Z]*"
        'z' => "Z|[+\\-][0-9][0-9]:?[0-9][0-9](?::?[0-9][0-9](?:[.][0-9]+)?)?"
        _ => {
          query_error(
            ctx, "query.oracle_type_error: ValueError invalid strptime directive",
          )
          return QNull
        }
      }
      pattern.write_char('(')
      pattern.write_string(part)
      pattern.write_char(')')
      fields.push(field)
    } else if python_whitespace(c) {
      pattern.write_string("[ \t\n\r]+")
    } else {
      if ".^$*+?{}[]|()\\".contains(c.to_string()) {
        pattern.write_char('\\')
      }
      pattern.write_char(c)
    }
  }
  pattern.write_char('$')
  let regex = @string.Regex("(?i:" + pattern.to_string() + ")") catch {
    _ => {
      query_error(
        ctx, "query.oracle_type_error: ValueError invalid date format",
      )
      return QNull
    }
  }
  let matched = match regex.execute(text) {
    Some(m) => m
    None => {
      query_error(
        ctx, "query.oracle_type_error: ValueError date does not match format",
      )
      return QNull
    }
  }
  let mut year = 1900
  let mut month = 1
  let mut day = 1
  let mut ordinal_day = None
  let mut week = None
  let mut week_kind = ' '
  let mut weekday = 0
  let mut hour = 0
  let mut minute = 0
  let mut second = 0
  let mut iso_year = None
  let mut group = 1
  for field in fields {
    let raw = matched.group(group).unwrap().to_owned()
    let n = bql_parse_int(raw.trim().to_owned())
    match field {
      'Y' => year = n
      'G' => iso_year = Some(n)
      'y' => year = if n <= 68 { 2000 + n } else { 1900 + n }
      'm' => month = n
      'd' => day = n
      'b' | 'B' =>
        match
          (if (field == 'b' && raw.length() != 3) ||
            (field == 'B' && !month_names.contains(raw.to_lower())) {
            None
          } else {
            month_number(raw)
          }) {
          Some(m) => month = m
          None => {
            query_error(
              ctx, "query.oracle_type_error: ValueError invalid month",
            )
            return QNull
          }
        }
      'j' => ordinal_day = Some(n)
      'U' | 'W' | 'V' => {
        week = Some(n)
        week_kind = field
      }
      'w' | 'u' => {
        if n > (if field == 'w' { 6 } else { 7 }) || (field == 'u' && n == 0) {
          query_error(
            ctx, "query.oracle_type_error: ValueError invalid weekday",
          )
          return QNull
        }
        weekday = if field == 'w' { (n + 6) % 7 } else { n - 1 }
      }
      'H' | 'I' => {
        if field == 'I' && (n < 1 || n > 12) {
          query_error(ctx, "query.oracle_type_error: ValueError invalid hour")
          return QNull
        }
        hour = n
      }
      'M' => minute = n
      'S' => second = n
      'z' => {
        let digits = raw.replace_all(old=":", new="")
        if digits != "Z" &&
          (
            bql_parse_int(digits[1:3].to_owned()) > 23 ||
            bql_parse_int(digits[3:5].to_owned()) > 59
          ) {
          query_error(
            ctx, "query.oracle_type_error: ValueError invalid timezone offset",
          )
          return QNull
        }
      }
      'a' | 'A' => {
        let names = [
          "monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday",
        ]
        let mut found = false
        for j in 0..<7 {
          if raw.to_lower() == names[j] ||
            raw.to_lower() == names[j][:3].to_owned() {
            weekday = j
            found = true
          }
        }
        if !found {
          query_error(
            ctx, "query.oracle_type_error: ValueError invalid weekday",
          )
          return QNull
        }
      }
      'p' =>
        if raw.to_lower() != "am" && raw.to_lower() != "pm" {
          query_error(
            ctx, "query.oracle_type_error: ValueError invalid meridiem",
          )
          return QNull
        }
      'Z' =>
        if !["", "utc", "gmt"].contains(raw.to_lower()) {
          query_error(
            ctx, "query.oracle_type_error: ValueError invalid timezone",
          )
          return QNull
        }
      _ => ()
    }
    group = group + 1
  }
  if hour > 23 || minute > 59 || second > 59 || year < 1 || year > 9999 {
    query_error(
      ctx, "query.oracle_type_error: ValueError parsed field out of range",
    )
    return QNull
  }
  let has_weekday = fields.any(fn(c) { ['a', 'A', 'w', 'u'].contains(c) })
  if (iso_year is Some(_) || week_kind == 'V') &&
    !(iso_year is Some(_) && week_kind == 'V' && has_weekday) {
    query_error(
      ctx, "query.oracle_type_error: ValueError incomplete ISO week date",
    )
    return QNull
  }
  if has_weekday && week is Some(w) {
    if w > 53 || weekday < 0 || weekday > 6 {
      query_error(ctx, "query.oracle_type_error: ValueError invalid week")
      return QNull
    }
    if week_kind == 'V' {
      let y = match iso_year {
        Some(y) => y
        None => {
          query_error(
            ctx, "query.oracle_type_error: ValueError ISO year required",
          )
          return QNull
        }
      }
      let jan4 = ordinal(@ast.Date::new(y, 1, 4).unwrap())
      return ordinal_date(jan4 - (jan4 - 1) % 7 + (w - 1) * 7 + weekday)
    }
    let jan1 = ordinal(@ast.Date::new(year, 1, 1).unwrap())
    let start = if week_kind == 'U' { 6 } else { 0 }
    let jan_weekday = (jan1 - 1) % 7
    let offset = (weekday - start + 7) % 7
    return ordinal_date(
      jan1 + (start - jan_weekday + 7) % 7 + (w - 1) * 7 + offset,
    )
  }
  if ordinal_day is Some(n) {
    if n < 1 || n > 366 {
      query_error(
        ctx, "query.oracle_type_error: ValueError invalid day of year",
      )
      return QNull
    }
    return ordinal_date(ordinal(@ast.Date::new(year, 1, 1).unwrap()) + n - 1)
  }
  parsed_date(ctx, year, month, day)
}

///|
fn flexible_date(ctx : QueryContext, input : String) -> QueryValue {
  let today = match query_today(ctx) {
    QDate(d) => d
    _ => return QNull
  }
  let mut text = normalize_python_digits(input.trim().to_owned())
  if text.length() == 14 && text.iter().all(fn(c) { c.is_ascii_digit() }) {
    if bql_parse_int(text[8:10].to_owned()) > 23 ||
      bql_parse_int(text[10:12].to_owned()) > 59 ||
      bql_parse_int(text[12:].to_owned()) > 59 {
      query_error(ctx, "query.oracle_type_error: ParserError invalid time")
      return QNull
    }
    text = text[:8].to_owned()
  }
  let time_patterns = [
    "(^|[Tt ]+)([0-9][0-9]?):([0-9][0-9])(?::([0-9][0-9])(?:[.,][0-9]+)?)?([ ]*[aApP][mM])?(?:[ ]*(?:[A-Z]{1,5}|(?:UTC|GMT)[+\\-][0-9][0-9]?(?::[0-9][0-9])?|[+\\-][0-9][0-9](?::?[0-9][0-9])?))?$",
    "([Tt ]+)([0-9][0-9])([0-9][0-9])([0-9][0-9])?()$", "(^|[Tt ]+)([0-9][0-9]?)()()([ ]*[aApP][mM])$",
    "([Tt])([0-9][0-9])()()()$", "(^|[Tt ]+)([0-9][0-9]?)[hH](?:([0-9][0-9]?)[mM])?(?:([0-9][0-9]?)[sS])?()$",
  ]
  for index in 0.. {
        query_error(
          ctx, "query.oracle_type_error: invalid internal time pattern",
        )
        return QNull
      }
    }
    if regex.execute(text) is Some(m) {
      if index == 1 &&
        m.before().iter().filter(fn(c) { c.is_ascii_digit() }).count() < 6 {
        continue
      }
      let hour = bql_parse_int(m.group(2).unwrap().to_owned())
      let minute = bql_parse_int(
        m.group(3).map(fn(s) { s.to_owned() }).unwrap_or("0"),
      )
      let second = bql_parse_int(
        m.group(4).map(fn(s) { s.to_owned() }).unwrap_or("0"),
      )
      let meridiem = m.group(5).map(fn(s) { s.trim().to_owned() }).unwrap_or("")
      if hour > 23 ||
        minute > 59 ||
        second > 59 ||
        (meridiem != "" && hour > 12) {
        query_error(ctx, "query.oracle_type_error: ParserError invalid time")
        return QNull
      }
      text = m.before().trim().to_owned()
      if text == "" {
        return QDate(today)
      }
      break
    }
  }
  let mut year = today.year
  let mut month = today.month
  let mut day = today.day
  let mut explicit_day = false
  if (text.length() == 8 || text.length() == 6) &&
    text.iter().all(fn(c) { c.is_ascii_digit() }) {
    if text.length() == 8 {
      return parsed_date(
        ctx,
        bql_parse_int(text[:4].to_owned()),
        bql_parse_int(text[4:6].to_owned()),
        bql_parse_int(text[6:].to_owned()),
      )
    }
    let a = bql_parse_int(text[:2].to_owned())
    let b = bql_parse_int(text[2:4].to_owned())
    let c = bql_parse_int(text[4:].to_owned())
    let short_year = if a > 31 { a } else { c }
    let y = today.year / 100 * 100 + short_year
    let year = if y >= today.year + 50 {
      y - 100
    } else if y < today.year - 50 {
      y + 100
    } else {
      y
    }
    let month = if a > 12 { b } else { a }
    let day = if a > 31 { c } else if a > 12 { a } else { b }
    return parsed_date(ctx, year, month, day)
  }

  let tokens : Array[String] = []
  let mut token = StringBuilder()
  let mut numeric = false
  for c in text.iter() {
    if c.is_ascii_alphabetic() || c.is_ascii_digit() {
      let is_numeric = c.is_ascii_digit()
      if token.to_string() != "" && numeric != is_numeric {
        tokens.push(token.to_string())
        token = StringBuilder()
      }
      token.write_char(c)
      numeric = is_numeric
    } else if " -/.,\t\n".contains(c.to_string()) {
      if token.to_string() != "" {
        tokens.push(token.to_string())
        token = StringBuilder()
      }
    } else {
      query_error(
        ctx, "query.oracle_type_error: ParserError unknown date format",
      )
      return QNull
    }
  }
  if token.to_string() != "" {
    tokens.push(token.to_string())
  }
  let numbers : Array[String] = []
  let mut named_month = false
  let mut weekday = None
  for t in tokens {
    let weekdays = [
      "monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday",
    ]
    for i in 0..<7 {
      if t.to_lower() == weekdays[i] ||
        t.to_lower() == weekdays[i][:3].to_owned() {
        weekday = Some(i)
      }
    }
    if month_number(t) is Some(m) {
      month = m
      named_month = true
    } else if t.iter().all(fn(c) { c.is_ascii_digit() }) {
      numbers.push(t)
    } else if ![
        "mon", "monday", "tue", "tuesday", "wed", "wednesday", "thu", "thursday",
        "fri", "friday", "sat", "saturday", "sun", "sunday", "st", "nd", "rd", "th",
        "at", "on", "of", "ad",
      ].contains(t.to_lower()) {
      query_error(
        ctx, "query.oracle_type_error: ParserError unknown date format",
      )
      return QNull
    }
  }
  if !named_month && numbers.is_empty() {
    if weekday is Some(day) {
      return ordinal_date(
        ordinal(today) + (day - (ordinal(today) - 1) % 7 + 7) % 7,
      )
    }
    query_error(ctx, "query.oracle_type_error: ParserError no date")
    return QNull
  }
  let expand_year = fn(s : String) {
    let n = bql_parse_int(s)
    if s.length() >= 3 {
      n
    } else {
      let y = today.year / 100 * 100 + n
      if y >= today.year + 50 {
        y - 100
      } else if y < today.year - 50 {
        y + 100
      } else {
        y
      }
    }
  }
  if named_month {
    if numbers.length() > 2 {
      query_error(ctx, "query.oracle_type_error: ParserError extra date fields")
      return QNull
    }
    for n in numbers {
      if n.length() >= 3 || bql_parse_int(n) > 31 {
        year = expand_year(n)
      } else if !explicit_day {
        day = bql_parse_int(n)
        explicit_day = true
      } else {
        year = expand_year(n)
      }
    }
  } else {
    match numbers.length() {
      1 =>
        if numbers[0].length() >= 3 || bql_parse_int(numbers[0]) > 31 {
          year = expand_year(numbers[0])
        } else {
          day = bql_parse_int(numbers[0])
          explicit_day = true
        }
      2 =>
        if numbers[0].length() >= 3 {
          year = bql_parse_int(numbers[0])
          month = bql_parse_int(numbers[1])
        } else if numbers[1].length() >= 3 {
          year = bql_parse_int(numbers[1])
          month = bql_parse_int(numbers[0])
        } else {
          month = bql_parse_int(numbers[0])
          day = bql_parse_int(numbers[1])
          explicit_day = true
        }
      3 => {
        if numbers[0].length() >= 3 || bql_parse_int(numbers[0]) > 31 {
          year = expand_year(numbers[0])
          month = bql_parse_int(numbers[1])
          day = bql_parse_int(numbers[2])
        } else {
          year = expand_year(numbers[2])
          let a = bql_parse_int(numbers[0])
          let b = bql_parse_int(numbers[1])
          month = if a > 12 { b } else { a }
          day = if a > 12 { a } else { b }
        }
        explicit_day = true
      }
      _ => {
        query_error(
          ctx, "query.oracle_type_error: ParserError invalid date fields",
        )
        return QNull
      }
    }
  }
  if !explicit_day && month >= 1 && month <= 12 {
    day = day.min(@ast.Date::days_in_month(year, month))
  }
  parsed_date(ctx, year, month, day)
}