///|
fn scoped_case_pattern(pattern : String) -> String {
  if pattern.has_prefix("(?i)") {
    "(?i:" + pattern[4:].to_owned() + ")"
  } else {
    pattern
  }
}

///|
fn python_whitespace(c : Char) -> Bool {
  let n = c.to_int()
  (n >= 9 && n <= 13) ||
  (n >= 28 && n <= 32) ||
  n == 0x85 ||
  n == 0xA0 ||
  n == 0x1680 ||
  (n >= 0x2000 && n <= 0x200A) ||
  n == 0x2028 ||
  n == 0x2029 ||
  n == 0x202F ||
  n == 0x205F ||
  n == 0x3000
}

///|
fn query_maxwidth(
  ctx : QueryContext,
  text : String,
  width : QueryValue,
) -> QueryValue {
  let n = match query_integer(width) {
    Some(n) => n
    None => {
      // An enormous positive integer cannot shorten an in-memory string.
      if width.to_text().has_prefix("-") {
        query_error(ctx, "query.oracle_type_error: ValueError invalid width")
        return QNull
      }
      2147483647
    }
  }
  if n < 5 {
    query_error(
      ctx, "query.oracle_type_error: ValueError invalid width or placeholder too large",
    )
    return QNull
  }
  let words : Array[String] = []
  let mut word = StringBuilder()
  for c in text.iter() {
    if python_whitespace(c) {
      if word.to_string() != "" {
        words.push(word.to_string())
        word = StringBuilder()
      }
    } else {
      word.write_char(c)
    }
  }
  if word.to_string() != "" {
    words.push(word.to_string())
  }
  let collapsed = words.join(" ")
  if collapsed.iter().count() <= n {
    return QText(collapsed)
  }
  let chunks : Array[String] = []
  for word in words {
    if !chunks.is_empty() {
      chunks.push(" ")
    }
    let chars = word.iter().to_array()
    let word_char = fn(i : Int32) {
      i >= 0 &&
      i < chars.length() &&
      (
        (chars[i].is_ascii_alphabetic() || chars[i].is_ascii_digit()) ||
        chars[i] == '_' ||
        chars[i].to_int() >= 128
      )
    }
    let mut start = 0
    let mut i = 0
    while i < chars.length() {
      if chars[i] == '-' {
        let mut end = i + 1
        while end < chars.length() && chars[end] == '-' {
          end = end + 1
        }
        if end - i >= 2 && word_char(i - 1) && word_char(end) {
          if i > start {
            chunks.push(String::from_iter(chars[start:i].iter()))
          }
          chunks.push(String::from_iter(chars[i:end].iter()))
          start = end
        } else if end == i + 1 &&
          word_char(i - 1) &&
          word_char(end) &&
          (
            word_char(i - 2) ||
            (i >= 3 && chars[i - 2] == '-' && word_char(i - 3))
          ) &&
          (
            word_char(end + 1) ||
            (
              end + 2 < chars.length() &&
              chars[end + 1] == '-' &&
              word_char(end + 2)
            )
          ) {
          chunks.push(String::from_iter(chars[start:end].iter()))
          start = end
        }
        i = end
      } else {
        i = i + 1
      }
    }
    if start < chars.length() {
      chunks.push(String::from_iter(chars[start:].iter()))
    }
  }
  let kept = StringBuilder()
  let mut used = 0
  for chunk in chunks {
    let size = chunk.iter().count()
    if used + size > n - 6 {
      break
    }
    kept.write_string(chunk)
    used = used + size
  }
  let prefix = kept.to_string().trim_end().to_owned()
  QText(if prefix == "" { "[...]" } else { prefix + " [...]" })
}

///|
fn query_regex(
  ctx : QueryContext,
  pattern : String,
  insensitive : Bool,
) -> @string.Regex? {
  let pattern = scoped_case_pattern(pattern)
  let portable = StringBuilder()
  let chars = pattern.iter().to_array()
  let mut in_class = false
  let mut index = 0
  while index < chars.length() {
    let c = chars[index]
    if c == '\\' {
      index = index + 1
      if index >= chars.length() {
        query_error(
          ctx, "query.oracle_type_error: PatternError trailing escape",
        )
        return None
      }
      let escaped = chars[index]
      if "dDsSwWbBAZ0123456789NxU".contains(escaped.to_string()) {
        query_error(
          ctx, "unsupported query.unsupported.regex: Python Unicode classes or assertions",
        )
        return None
      }
      portable.write_char('\\')
      portable.write_char(escaped)
    } else {
      if c == '[' {
        in_class = true
      }
      if c == ']' {
        in_class = false
      }
      if c == '(' &&
        !in_class &&
        index + 1 < chars.length() &&
        chars[index + 1] == '?' &&
        !((index + 2 < chars.length() && chars[index + 2] == ':') ||
        (
          index + 3 < chars.length() &&
          chars[index + 2] == 'i' &&
          chars[index + 3] == ':'
        )) {
        query_error(
          ctx, "unsupported query.unsupported.regex: Python lookaround, backreferences or flags",
        )
        return None
      }
      if c == '.' && !in_class {
        portable.write_string("[^\n]")
      } else {
        portable.write_char(c)
      }
    }
    index = index + 1
  }
  let pattern = portable.to_string()
  Some(
    @string.Regex(if insensitive { "(?i:" + pattern + ")" } else { pattern }),
  ) catch {
    _ => {
      query_error(
        ctx, "query.oracle_type_error: PatternError invalid regex at BQL:1:1",
      )
      None
    }
  }
}

///|
fn regex_function(
  ctx : QueryContext,
  name : String,
  values : Array[QueryValue],
  row : BqlRow,
) -> QueryValue {
  let pattern = match values[0] {
    QText(s) => s
    _ => return QNull
  }
  let regex = match query_regex(ctx, pattern, name == "has_account") {
    Some(r) => r
    None => return QNull
  }
  if name == "has_account" {
    let accounts = match eval_column(ctx, row, "accounts") {
      QSet(s) | QFrozenSet(s) => s
      _ => []
    }
    return QBool(accounts.any(fn(a) { regex.execute(a) is Some(_) }))
  }
  if name == "findfirst" {
    let items = match values[1] {
      QSet(s) | QFrozenSet(s) => s
      _ => return QNull
    }
    let found = items.filter(fn(s) { regex.execute(s) is Some(_) })
    if found.length() > 1 {
      query_error(
        ctx, "unsupported query.unsupported.set_representation: findfirst depends on CPython hash seed at BQL:1:1",
      )
      return QNull
    }
    return if found.is_empty() { QNull } else { QText(found[0]) }
  }
  let text = match values[1] {
    QText(s) => s
    _ => return QNull
  }
  let matched = match regex.execute(text) {
    Some(m) => m
    None => return QNull
  }
  if name == "grep" {
    return QText(matched.content().to_owned())
  }
  let index = query_integer(values[2]).unwrap_or(-1)
  let groups = regex_group_count(pattern)
  if index < 0 || index > groups {
    query_error(
      ctx, "query.oracle_type_error: IndexError no such group at BQL:1:1",
    )
    QNull
  } else if index == 0 {
    QText(matched.content().to_owned())
  } else {
    matched.group(index).map(fn(s) { QText(s.to_owned()) }).unwrap_or(QNull)
  }
}

///|
fn query_substitute(
  ctx : QueryContext,
  pattern : String,
  replacement : String,
  text : String,
) -> QueryValue {
  let groups = regex_group_count(pattern)
  regex_replacement(ctx, replacement, groups, None) |> ignore
  if ctx.error is Some(_) {
    return QNull
  }
  match query_regex(ctx, pattern, false) {
    Some(regex) =>
      if (
          pattern.contains("*?") ||
          pattern.contains("+?") ||
          pattern.contains("??") ||
          pattern.contains("}?")
        ) &&
        regex
        .find(text)
        .any(fn(m) { m.content().is_empty() && !m.after().is_empty() }) {
        query_error(
          ctx, "unsupported query.unsupported.regex: adjacent lazy empty and nonempty matches at BQL:1:1",
        )
        QNull
      } else {
        QText(
          regex
          .replace_by(text, fn(m) {
            regex_replacement(ctx, replacement, groups, Some(m))[:]
          })
          .to_owned(),
        )
      }
    None => QNull
  }
}

///|
fn regex_group_count(pattern : String) -> Int32 {
  let chars = pattern.iter().to_array()
  let mut groups = 0
  let mut in_class = false
  let mut escaped = false
  for i in 0.. String {
  let chars = replacement.iter().to_array()
  let out = StringBuilder()
  let mut i = 0
  while i < chars.length() {
    let c = chars[i]
    i = i + 1
    if c != '\\' {
      out.write_char(c)
      continue
    }
    if i == chars.length() {
      query_error(
        ctx, "query.oracle_type_error: PatternError trailing replacement escape",
      )
      return ""
    }
    let c = chars[i]
    i = i + 1
    let mut group = -1
    if c == 'g' && i < chars.length() && chars[i] == '<' {
      i = i + 1
      let name = StringBuilder()
      while i < chars.length() && chars[i] != '>' {
        name.write_char(chars[i])
        i = i + 1
      }
      if i == chars.length() ||
        name.to_string() == "" ||
        !name.to_string().iter().all(fn(c) { c >= '0' && c <= '9' }) {
        query_error(
          ctx, "query.oracle_type_error: PatternError invalid replacement group",
        )
        return ""
      }
      i = i + 1
      group = bql_parse_int(name.to_string())
    } else if c >= '0' && c <= '9' {
      let octal = c == '0' ||
        (
          c <= '7' &&
          i + 1 < chars.length() &&
          chars[i] >= '0' &&
          chars[i] <= '7' &&
          chars[i + 1] >= '0' &&
          chars[i + 1] <= '7'
        )
      let mut value = c.to_int() - 48
      let mut digits = 1
      while i < chars.length() &&
            digits < (if octal { 3 } else { 2 }) &&
            chars[i] >= '0' &&
            chars[i] <= (if octal { '7' } else { '9' }) {
        value = value * (if octal { 8 } else { 10 }) + chars[i].to_int() - 48
        i = i + 1
        digits = digits + 1
      }
      if octal {
        if value > 255 {
          query_error(
            ctx, "query.oracle_type_error: PatternError octal escape out of range",
          )
          return ""
        }
        out.write_char(value.unsafe_to_char())
      } else {
        group = value
      }
    } else {
      match c {
        '\\' => out.write_char('\\')
        'n' => out.write_char('\n')
        'r' => out.write_char('\r')
        't' => out.write_char('\t')
        'a' => out.write_char((7).unsafe_to_char())
        'b' => out.write_char((8).unsafe_to_char())
        'f' => out.write_char((12).unsafe_to_char())
        'v' => out.write_char((11).unsafe_to_char())
        _ => {
          if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') {
            query_error(
              ctx, "query.oracle_type_error: PatternError bad replacement escape",
            )
            return ""
          }
          out.write_char('\\')
          out.write_char(c)
        }
      }
    }
    if group >= 0 {
      if group > groups {
        query_error(
          ctx, "query.oracle_type_error: PatternError invalid group reference",
        )
        return ""
      }
      if matched is Some(m) {
        out.write_string(
          if group == 0 {
            m.content().to_owned()
          } else {
            m.group(group).map(fn(s) { s.to_owned() }).unwrap_or("")
          },
        )
      }
    }
  }
  out.to_string()
}