///|
fn ascii_digits(s : String) -> Bool {
  !s.is_empty() && s.code_units().all(fn(c) { c >= 48 && c <= 57 })
}

///|
fn ascii_letters(s : String) -> Bool {
  !s.is_empty() &&
  s.code_units().all(fn(c) { (c >= 65 && c <= 90) || (c >= 97 && c <= 122) })
}

///|
fn ascii_alnum(s : String) -> Bool {
  !s.is_empty() &&
  s
  .code_units()
  .all(fn(c) {
    (c >= 65 && c <= 90) || (c >= 97 && c <= 122) || (c >= 48 && c <= 57)
  })
}

///|
fn trim_spaces(s : String) -> String {
  let mut a = 0
  let mut b = s.length()
  while a < b && s[a] == 32 {
    a = a + 1
  }
  while b > a && s[b - 1] == 32 {
    b = b - 1
  }
  s[a:b].to_owned()
}

///|
fn small_number(s : String, max : Int32) -> Bool {
  if !ascii_digits(s) {
    return false
  }
  let mut n = 0
  for c in s.code_units() {
    if n > max / 10 {
      return false
    }
    n = n * 10 + c.to_int() - 48
    if n > max {
      return false
    }
  }
  true
}

///|
fn valid_time(s : String) -> Bool {
  let s = if s.has_suffix("Z") { s[:s.length() - 1].to_owned() } else { s }
  let ts = s.split(":").map(fn(x) { x.to_owned() }).to_array()
  if ts.length() < 2 || ts.length() > 3 {
    return false
  }
  if ts[0].length() > 2 ||
    !small_number(ts[0], 23) ||
    ts[1].length() != 2 ||
    !small_number(ts[1], 59) {
    return false
  }
  if ts.length() == 3 {
    let ss = ts[2].split(".").map(fn(x) { x.to_owned() }).to_array()
    if ss.length() > 2 || ss[0].length() != 2 || !small_number(ss[0], 59) {
      return false
    }
    if ss.length() == 2 && !ascii_digits(ss[1]) {
      return false
    }
  }
  true
}

///|
fn valid_age(s : String) -> Bool {
  if s == "" {
    return true
  }
  let s = if s.has_prefix("<") || s.has_prefix(">") {
    s[1:].to_owned()
  } else {
    s
  }
  let mut previous = -1
  for part in s.split(" ") {
    if part.length() < 2 {
      return false
    }
    let n = part.length() - 1
    let unit = match part[n] {
      121 => 0
      109 => 1
      119 => 2
      100 => 3
      _ => -1
    }
    if unit <= previous || !ascii_digits(part[:n].to_owned()) {
      return false
    }
    previous = unit
  }
  previous >= 0
}

///|
/// RFC 5646 Language-Tag grammar, including grandfathered and private-use forms.
/// Registry membership and preferred spelling are distinct from well-formedness.
fn valid_language(s : String) -> Bool {
  let s = s.to_lower()
  if [
      "en-gb-oed", "i-ami", "i-bnn", "i-default", "i-enochian", "i-hak", "i-klingon",
      "i-lux", "i-mingo", "i-navajo", "i-pwn", "i-tao", "i-tay", "i-tsu", "sgn-be-fr",
      "sgn-be-nl", "sgn-ch-de", "art-lojban", "cel-gaulish", "no-bok", "no-nyn",
      "zh-guoyu", "zh-hakka", "zh-min", "zh-min-nan", "zh-xiang",
    ].contains(s) {
    return true
  }
  let t = s.split("-").map(fn(x) { x.to_owned() }).to_array()
  if t.any(fn(x) { x.length() < 1 || x.length() > 8 || !ascii_alnum(x) }) {
    return false
  }
  if t[0] == "x" {
    return t.length() > 1
  }
  if t[0].length() < 2 || !ascii_letters(t[0]) {
    return false
  }
  let mut i = 1
  if t[0].length() <= 3 {
    let mut n = 0
    while i < t.length() && t[i].length() == 3 && ascii_letters(t[i]) && n < 3 {
      i = i + 1
      n = n + 1
    }
  }
  if i < t.length() && t[i].length() == 4 && ascii_letters(t[i]) {
    i = i + 1
  }
  if i < t.length() &&
    (
      (t[i].length() == 2 && ascii_letters(t[i])) ||
      (t[i].length() == 3 && ascii_digits(t[i]))
    ) {
    i = i + 1
  }
  let variants : Map[String, Bool] = Map([])
  while i < t.length() &&
        (
          t[i].length() >= 5 ||
          (t[i].length() == 4 && t[i][0] >= 48 && t[i][0] <= 57)
        ) {
    if variants.contains(t[i]) {
      return false
    }
    variants.set(t[i], true)
    i = i + 1
  }
  let singletons : Map[String, Bool] = Map([])
  while i < t.length() {
    if t[i] == "x" {
      return i + 1 < t.length()
    }
    if t[i].length() != 1 || singletons.contains(t[i]) {
      return false
    }
    singletons.set(t[i], true)
    i = i + 1
    let start = i
    while i < t.length() && t[i].length() >= 2 {
      i = i + 1
    }
    if i == start {
      return false
    }
  }
  true
}

///|
fn mime_token(s : String) -> Bool {
  !s.is_empty() &&
  s
  .code_units()
  .all(fn(c) {
    c > 32 &&
    c < 127 &&
    ![40, 41, 60, 62, 64, 44, 59, 58, 92, 34, 47, 91, 93, 63, 61].contains(
      c.to_int(),
    )
  })
}

///|
fn valid_media_type(s : String) -> Bool {
  let mut i = 0
  while i < s.length() && s[i] != 59 && s[i] != 32 && s[i] != 9 {
    i = i + 1
  }
  let main = s[:i].split("/").map(fn(x) { x.to_owned() }).to_array()
  if main.length() != 2 || !media_name(main[0]) || !media_name(main[1]) {
    return false
  }
  while i < s.length() {
    while i < s.length() && (s[i] == 32 || s[i] == 9) {
      i = i + 1
    }
    if i == s.length() {
      return true
    }
    if s[i] != 59 {
      return false
    }
    i = i + 1
    while i < s.length() && (s[i] == 32 || s[i] == 9) {
      i = i + 1
    }
    let start = i
    while i < s.length() && s[i] != 61 {
      i = i + 1
    }
    if i == s.length() || !mime_token(s[start:i].to_owned()) {
      return false
    }
    i = i + 1
    if i < s.length() && s[i] == 34 {
      i = i + 1
      let mut closed = false
      while i < s.length() {
        if s[i] == 34 {
          i = i + 1
          closed = true
          break
        }
        if s[i] == 92 {
          i = i + 1
          if i == s.length() {
            return false
          }
        }
        if (s[i] < 32 && s[i] != 9) || s[i] == 127 || s[i] > 255 {
          return false
        }
        i = i + 1
      }
      if !closed {
        return false
      }
    } else {
      let start = i
      while i < s.length() && s[i] != 59 && s[i] != 32 && s[i] != 9 {
        i = i + 1
      }
      if !mime_token(s[start:i].to_owned()) {
        return false
      }
    }
  }
  true
}

///|
fn media_name(s : String) -> Bool {
  if !mime_token(s) {
    return false
  }
  if s.to_lower().has_prefix("x-") {
    return true
  }
  if s.length() > 127 || !ascii_alnum(s[:1].to_owned()) {
    return false
  }
  s
  .code_units()
  .all(fn(c) {
    (c >= 65 && c <= 90) ||
    (c >= 97 && c <= 122) ||
    (c >= 48 && c <= 57) ||
    "!#$&-^_.+".code_units().contains(c)
  })
}

///|
/// RFC media type names/parameter names ignore case; ordinary parameter values
/// do not. Normalize quoting and parameter order for translation identity.
fn media_identity(s : String) -> String {
  if !valid_media_type(s) {
    return s
  }
  let mut i = 0
  while i < s.length() && s[i] != 59 && s[i] != 32 && s[i] != 9 {
    i = i + 1
  }
  let base = s[:i].to_owned().to_lower()
  let parameters : Array[(String, String)] = []
  while i < s.length() {
    while i < s.length() && (s[i] == 32 || s[i] == 9 || s[i] == 59) {
      i = i + 1
    }
    if i == s.length() {
      break
    }
    let start = i
    while i < s.length() && s[i] != 61 {
      i = i + 1
    }
    let name = s[start:i].to_owned().to_lower()
    i = i + 1
    let value = StringBuilder()
    if s[i] == 34 {
      i = i + 1
      while s[i] != 34 {
        if s[i] == 92 {
          i = i + 1
        }
        let start = i
        i = i + 1
        value.write_string(s[start:i].to_owned())
      }
      i = i + 1
    } else {
      let start = i
      while i < s.length() && s[i] != 59 && s[i] != 32 && s[i] != 9 {
        i = i + 1
      }
      value.write_string(s[start:i].to_owned())
    }
    let value = value.to_string()
    parameters.push(
      (name, if name == "charset" { value.to_lower() } else { value }),
    )
  }
  parameters.sort_by(fn(a, b) {
    let n = a.0.compare(b.0)
    if n == 0 {
      a.1.compare(b.1)
    } else {
      n
    }
  })
  base +
  Json::array(
    parameters.map(fn(p) { Json::array([Json::string(p.0), Json::string(p.1)]) }),
  ).stringify()
}

///|
fn hex_digit(c : UInt16) -> Int32 {
  if c >= 48 && c <= 57 {
    c.to_int() - 48
  } else if c >= 65 && c <= 70 {
    c.to_int() - 55
  } else if c >= 97 && c <= 102 {
    c.to_int() - 87
  } else {
    -1
  }
}

///|
fn valid_uri_reference(s : String) -> Bool {
  let mut i = 0
  while i < s.length() {
    let c = s[i]
    if c == 37 {
      if i + 2 >= s.length() ||
        hex_digit(s[i + 1]) < 0 ||
        hex_digit(s[i + 2]) < 0 {
        return false
      }
      i = i + 3
    } else {
      if !((c >= 65 && c <= 90) ||
        (c >= 97 && c <= 122) ||
        (c >= 48 && c <= 57) ||
        "-._~:/?#[]@!$&'()*+,;=".code_units().contains(c)) {
        return false
      }
      i = i + 1
    }
  }
  let fragments = s.split("#").to_array()
  if fragments.length() > 2 {
    return false
  }
  if fragments.length() == 2 &&
    (fragments[1].contains("[") || fragments[1].contains("]")) {
    return false
  }
  let queries = fragments[0].split("?").to_array()
  for q = 1; q < queries.length(); q = q + 1 {
    if queries[q].contains("[") || queries[q].contains("]") {
      return false
    }
  }
  let pre = fragments[0].split("?").to_array()[0].to_owned()
  let mut rest = pre
  let first = pre.split("/").to_array()[0].to_owned()
  if first.split_once(":") is Some((scheme, _)) {
    if scheme.is_empty() ||
      !ascii_letters(scheme[:1].to_owned()) ||
      !scheme
      .code_units()
      .all(fn(c) {
        (c >= 65 && c <= 90) ||
        (c >= 97 && c <= 122) ||
        (c >= 48 && c <= 57) ||
        c == 43 ||
        c == 45 ||
        c == 46
      }) {
      return false
    }
    rest = pre[scheme.length() + 1:].to_owned()
  }
  if rest.has_prefix("//") {
    let authority = rest[2:].split("/").to_array()[0].to_owned()
    let hostport = authority.split("@").to_array()
    if hostport.length() > 2 {
      return false
    }
    if hostport.length() == 2 &&
      (hostport[0].contains("[") || hostport[0].contains("]")) {
      return false
    }
    let host = hostport[hostport.length() - 1].to_owned()
    if host.has_prefix("[") {
      guard host.split_once("]") is Some((ip, port)) else { return false }
      if !(port == "" ||
        (
          port.has_prefix(":") &&
          (port.length() == 1 || ascii_digits(port[1:].to_owned()))
        )) {
        return false
      }
      if !valid_ip_literal(ip[1:].to_owned()) {
        return false
      }
    } else {
      if host.contains("[") || host.contains("]") {
        return false
      }
      let parts = host.split(":").to_array()
      if parts.length() > 2 ||
        (
          parts.length() == 2 &&
          parts[1] != "" &&
          !ascii_digits(parts[1].to_owned())
        ) {
        return false
      }
    }
  } else if rest.contains("[") || rest.contains("]") {
    return false
  }
  true
}

///|
fn valid_ip_literal(s : String) -> Bool {
  if s.has_prefix("v") || s.has_prefix("V") {
    guard s.split_once(".") is Some((version, tail)) else { return false }
    return version.length() > 1 &&
      version[1:].code_units().all(fn(c) { hex_digit(c) >= 0 }) &&
      !tail.is_empty() &&
      tail
      .code_units()
      .all(fn(c) {
        c != 37 &&
        c != 91 &&
        c != 93 &&
        c != 64 &&
        c != 47 &&
        c != 63 &&
        c != 35
      })
  }
  let parts = s.split(":").map(fn(x) { x.to_owned() }).to_array()
  if s.contains(":::") ||
    (s.has_prefix(":") && !s.has_prefix("::")) ||
    (s.has_suffix(":") && !s.has_suffix("::")) ||
    s.split("::").to_array().length() > 2 {
    return false
  }
  let mut n = 0
  for i, p in parts {
    if p == "" {
      continue
    }
    if p.contains(".") {
      if i != parts.length() - 1 {
        return false
      }
      let ipv4 = p.split(".").map(fn(x) { x.to_owned() }).to_array()
      if ipv4.length() != 4 ||
        ipv4.any(fn(x) {
          !small_number(x, 255) || (x.length() > 1 && x.has_prefix("0"))
        }) {
        return false
      }
      n = n + 2
    } else {
      if p.length() > 4 || !p.code_units().all(fn(c) { hex_digit(c) >= 0 }) {
        return false
      }
      n = n + 1
    }
  }
  if s.contains("::") {
    n < 8
  } else {
    n == 8
  }
}

///|
fn valid_file_path(s : String) -> Bool {
  if s == "" ||
    s
    .code_units()
    .any(fn(c) {
      c <= 32 ||
      c == 127 ||
      [92, 34, 60, 62, 94, 96, 123, 124, 125].contains(c.to_int())
    }) {
    return false
  }
  let decoded = StringBuilder()
  let mut i = 0
  while i < s.length() {
    if s[i] == 37 {
      if i + 2 >= s.length() ||
        hex_digit(s[i + 1]) < 0 ||
        hex_digit(s[i + 2]) < 0 {
        return false
      }
      let c = hex_digit(s[i + 1]) * 16 + hex_digit(s[i + 2])
      if c <= 8 ||
        c == 11 ||
        c == 12 ||
        (c >= 14 && c <= 31) ||
        c == 127 ||
        c == 92 {
        return false
      }
      if c == 194 && i + 5 < s.length() && s[i + 3] == 37 {
        let next = hex_digit(s[i + 4]) * 16 + hex_digit(s[i + 5])
        if next >= 128 && next <= 159 {
          return false
        }
      }
      decoded.write_string(if c == 46 { "." } else { "x" })
      i = i + 3
    } else {
      decoded.write_string(s[i:i + 1].to_owned())
      i = i + 1
    }
  }
  let first = s.split("/").to_array()[0]
  if first.contains(":") {
    guard s.split_once(":") is Some((scheme, rest)) else { return false }
    let scheme = scheme.to_owned().to_lower()
    if !["http", "https", "ftp", "file"].contains(scheme) ||
      !rest.has_prefix("//") {
      return false
    }
    let authority = rest[2:].split("/").to_array()[0]
    if scheme != "file" && authority == "" {
      return false
    }
    let host_and_query = authority.split("?").to_array()[0]
    .split("#")
    .to_array()[0].to_owned()
    let pieces = host_and_query.split("@").to_array()
    if pieces.length() > 2 {
      return false
    }
    let host = pieces[pieces.length() - 1].to_owned()
    if host.has_prefix("[") {
      guard host.split_once("]") is Some((ip, port)) else { return false }
      if !valid_ip_literal(ip[1:].to_owned()) {
        return false
      }
      if port != "" &&
        (!port.has_prefix(":") || !small_number(port[1:].to_owned(), 65535)) {
        return false
      }
    } else {
      if host.contains("[") || host.contains("]") {
        return false
      }
      let ps = host.split(":").to_array()
      if ps.length() > 2 ||
        (
          ps.length() == 2 &&
          ps[1] != "" &&
          !small_number(ps[1].to_owned(), 65535)
        ) {
        return false
      }
      if scheme != "file" && ps[0] == "" {
        return false
      }
    }
    return true
  }
  !s.has_prefix("/") &&
  !s.contains("?") &&
  !s.contains("#") &&
  !decoded.to_string().split("/").any(fn(x) { x == ".." })
}

///|
fn valid_coordinate(s : String, latitude : Bool) -> Bool {
  if s.length() < 2 {
    return false
  }
  let dir = s[0]
  if latitude && dir != 78 && dir != 83 && dir != 110 && dir != 115 {
    return false
  }
  if !latitude && dir != 69 && dir != 87 && dir != 101 && dir != 119 {
    return false
  }
  let p = s[1:].split(".").map(fn(x) { x.to_owned() }).to_array()
  let max = if latitude { 90 } else { 180 }
  if p.length() > 2 ||
    p[0].length() > (if latitude { 2 } else { 3 }) ||
    !small_number(p[0], max) {
    return false
  }
  if p.length() == 2 {
    if !ascii_digits(p[1]) {
      return false
    }
    let n = @string.parse_int(p[0][:]) catch { _ => return false }
    if n == max && p[1].code_units().any(fn(c) { c != 48 }) {
      return false
    }
  }
  true
}