///|
priv struct UriParts {
  scheme : String?
  authority : String?
  path : String
  query : String?
  fragment : String?
}

///|
fn uri_hex(character : Char) -> Bool {
  (character >= '0' && character <= '9') ||
  (character >= 'a' && character <= 'f') ||
  (character >= 'A' && character <= 'F')
}

///|
fn uri_scheme_char(character : Char, first : Bool) -> Bool {
  (character >= 'a' && character <= 'z') ||
  (character >= 'A' && character <= 'Z') ||
  (
    !first &&
    (
      (character >= '0' && character <= '9') ||
      character == '+' ||
      character == '-' ||
      character == '.'
    )
  )
}

///|
/// RFC 3986 pchar with RFC 3987's ucschar extension.
/// Percent escapes are checked separately while scanning the full input.
fn uri_path_character(c : Char) -> Bool {
  uri_reg_name_char(c) || c == ':' || c == '@' || c == '/'
}

///|
fn uri_query_character(c : Char) -> Bool {
  uri_path_character(c) || c == '?' || uri_iprivate(c.to_int())
}

///|
fn uri_fragment_character(c : Char) -> Bool {
  uri_path_character(c) || c == '?'
}

///|
fn uri_parts(text : String) -> UriParts? {
  let chars = date_chars(text)
  for index = 0; index < chars.length(); index = index + 1 {
    let character = chars[index]
    if character.to_int() <= 32 ||
      character == '<' ||
      character == '>' ||
      character == '"' ||
      character == '\\' {
      return None
    }
    if character == '%' &&
      (
        index + 2 >= chars.length() ||
        !uri_hex(chars[index + 1]) ||
        !uri_hex(chars[index + 2])
      ) {
      return None
    }
  }
  let mut fragment_start = chars.length()
  for index = 0; index < chars.length(); index = index + 1 {
    if chars[index] == '#' {
      fragment_start = index
      break
    }
  }
  let fragment = if fragment_start < chars.length() {
    Some(date_slice(chars, fragment_start + 1, chars.length()))
  } else {
    None
  }
  let mut query_start = fragment_start
  for index = 0; index < fragment_start; index = index + 1 {
    if chars[index] == '?' {
      query_start = index
      break
    }
  }
  let query = if query_start < fragment_start {
    Some(date_slice(chars, query_start + 1, fragment_start))
  } else {
    None
  }
  let end = query_start
  let mut start = 0
  let mut scheme : String? = None
  for index = 0; index < end; index = index + 1 {
    if chars[index] == '/' {
      break
    }
    if chars[index] == ':' {
      if index == 0 {
        return None
      }
      for scheme_index = 0
          scheme_index < index
          scheme_index = scheme_index + 1 {
        if !uri_scheme_char(chars[scheme_index], scheme_index == 0) {
          return None
        }
      }
      scheme = Some(date_slice(chars, 0, index))
      start = index + 1
      break
    }
  }
  let mut authority : String? = None
  if start + 1 < end && chars[start] == '/' && chars[start + 1] == '/' {
    let authority_start = start + 2
    start = authority_start
    while start < end && chars[start] != '/' {
      start = start + 1
    }
    authority = Some(date_slice(chars, authority_start, start))
  }
  let path = date_slice(chars, start, end)
  for c in date_chars(path) {
    if !uri_path_character(c) {
      return None
    }
  }
  match query {
    Some(value) =>
      for c in date_chars(value) {
        if !uri_query_character(c) {
          return None
        }
      }
    None => ()
  }
  match fragment {
    Some(value) =>
      for c in date_chars(value) {
        if !uri_fragment_character(c) {
          return None
        }
      }
    None => ()
  }
  Some({ scheme, authority, path, query, fragment, })
}

///|
fn uri_build(parts : UriParts) -> String {
  let builder = StringBuilder()
  match parts.scheme {
    Some(scheme) => {
      builder.write_string(scheme)
      builder.write_string(":")
    }
    None => ()
  }
  match parts.authority {
    Some(authority) => {
      builder.write_string("//")
      builder.write_string(authority)
    }
    None => ()
  }
  builder.write_string(parts.path)
  match parts.query {
    Some(query) => {
      builder.write_string("?")
      builder.write_string(query)
    }
    None => ()
  }
  match parts.fragment {
    Some(fragment) => {
      builder.write_string("#")
      builder.write_string(fragment)
    }
    None => ()
  }
  builder.to_string()
}

///|
fn uri_drop(text : String, count : Int32) -> String {
  let chars = date_chars(text)
  date_slice(chars, count, chars.length())
}

///|
fn uri_remove_last_segment(text : String) -> String {
  let chars = date_chars(text)
  for index = chars.length() - 1; index >= 0; index = index - 1 {
    if chars[index] == '/' {
      return date_slice(chars, 0, index)
    }
  }
  ""
}

///|
/// RFC 3986 section 5.2.4, retaining empty path segments rather than
/// collapsing repeated slashes.
fn uri_remove_dot_segments(path : String) -> String {
  let mut input = path
  let mut output = ""
  while !input.is_empty() {
    if input.has_prefix("../") {
      input = uri_drop(input, 3)
    } else if input.has_prefix("./") {
      input = uri_drop(input, 2)
    } else if input.has_prefix("/./") {
      input = uri_drop(input, 2)
    } else if input == "/." {
      input = "/"
    } else if input.has_prefix("/../") {
      input = uri_drop(input, 3)
      output = uri_remove_last_segment(output)
    } else if input == "/.." {
      input = "/"
      output = uri_remove_last_segment(output)
    } else if input == "." || input == ".." {
      input = ""
    } else {
      let chars = date_chars(input)
      let mut stop = 0
      if chars[0] == '/' {
        stop = 1
      }
      while stop < chars.length() && chars[stop] != '/' {
        stop = stop + 1
      }
      output = output + date_slice(chars, 0, stop)
      input = date_slice(chars, stop, chars.length())
    }
  }
  output
}

///|
fn uri_merge(base : UriParts, path : String) -> String {
  if base.authority is Some(_) && base.path.is_empty() {
    return "/" + path
  }
  let chars = date_chars(base.path)
  for index = chars.length() - 1; index >= 0; index = index - 1 {
    if chars[index] == '/' {
      return date_slice(chars, 0, index + 1) + path
    }
  }
  path
}

///|
fn uri_ipv4_address_valid(text : String) -> Bool {
  let chars = date_chars(text)
  let mut index = 0
  for group = 0; group < 4; group = group + 1 {
    let start = index
    let mut value = 0
    while index < chars.length() && chars[index] >= '0' && chars[index] <= '9' {
      value = value * 10 + (chars[index].to_int() - 48)
      index = index + 1
      if index - start > 3 || value > 255 {
        return false
      }
    }
    if index == start || (index - start > 1 && chars[start] == '0') {
      return false
    }
    if group < 3 {
      if index >= chars.length() || chars[index] != '.' {
        return false
      }
      index = index + 1
    }
  }
  index == chars.length()
}

///|
/// Count 16-bit groups in one side of an IPv6 address. An IPv4 tail is
/// permitted only at the very end of the entire address and consumes two.
fn uri_ipv6_groups(part : String, allow_ipv4 : Bool) -> Int32? {
  let chars = date_chars(part)
  if chars.is_empty() {
    return Some(0)
  }
  let mut index = 0
  let mut groups = 0
  while index < chars.length() {
    let start = index
    while index < chars.length() && chars[index] != ':' {
      index = index + 1
    }
    if index == start {
      return None
    }
    let value = date_slice(chars, start, index)
    if value.contains(".") {
      if !allow_ipv4 ||
        index != chars.length() ||
        !uri_ipv4_address_valid(value) {
        return None
      }
      groups = groups + 2
    } else {
      if index - start > 4 {
        return None
      }
      for position = start; position < index; position = position + 1 {
        if !uri_hex(chars[position]) {
          return None
        }
      }
      groups = groups + 1
    }
    if index < chars.length() {
      index = index + 1
      if index == chars.length() {
        return None
      }
    }
  }
  Some(groups)
}

///|
fn uri_ipv6_valid(text : String) -> Bool {
  let chars = date_chars(text)
  let mut compressed_at = -1
  for index = 0; index + 1 < chars.length(); index = index + 1 {
    if chars[index] == ':' && chars[index + 1] == ':' {
      if compressed_at >= 0 {
        return false
      }
      compressed_at = index
    }
  }
  if compressed_at < 0 {
    return uri_ipv6_groups(text, true) == Some(8)
  }
  let left = date_slice(chars, 0, compressed_at)
  let right = date_slice(chars, compressed_at + 2, chars.length())
  match (uri_ipv6_groups(left, false), uri_ipv6_groups(right, true)) {
    (Some(left_count), Some(right_count)) => left_count + right_count < 8
    _ => false
  }
}

///|
fn uri_ipvfuture_valid(text : String) -> Bool {
  let chars = date_chars(text)
  if chars.length() < 4 || (chars[0] != 'v' && chars[0] != 'V') {
    return false
  }
  let mut index = 1
  while index < chars.length() && uri_hex(chars[index]) {
    index = index + 1
  }
  if index == 1 || index + 1 >= chars.length() || chars[index] != '.' {
    return false
  }
  for position = index + 1; position < chars.length(); position = position + 1 {
    let c = chars[position]
    let ascii = (c >= 'a' && c <= 'z') ||
      (c >= 'A' && c <= 'Z') ||
      (c >= '0' && c <= '9')
    let permitted = "-._~!$&'()*+,;=:"
    if !ascii && !permitted.contains(c.to_string()) {
      return false
    }
  }
  true
}

///|
fn uri_ip_literal_valid(text : String) -> Bool {
  let chars = date_chars(text)
  if !chars.is_empty() && (chars[0] == 'v' || chars[0] == 'V') {
    uri_ipvfuture_valid(text)
  } else {
    uri_ipv6_valid(text)
  }
}

///|
fn uri_http_authority_valid(authority : String) -> Bool {
  let chars = date_chars(authority)
  if chars.is_empty() {
    return false
  }
  let mut host_start = 0
  for index = 0; index < chars.length(); index = index + 1 {
    if chars[index] == '@' {
      if host_start != 0 {
        return false
      }
      for position = 0; position < index; position = position + 1 {
        if !uri_reg_name_char(chars[position]) && chars[position] != ':' {
          return false
        }
      }
      host_start = index + 1
    }
  }
  if host_start >= chars.length() {
    return false
  }
  let mut port_start = chars.length()
  if chars[host_start] == '[' {
    let mut close = host_start + 1
    while close < chars.length() && chars[close] != ']' {
      close = close + 1
    }
    if close == host_start + 1 || close == chars.length() {
      return false
    }
    if !uri_ip_literal_valid(date_slice(chars, host_start + 1, close)) {
      return false
    }
    if close + 1 < chars.length() {
      if chars[close + 1] != ':' {
        return false
      }
      port_start = close + 2
    }
  } else {
    let mut colon_seen = false
    for index = host_start; index < chars.length(); index = index + 1 {
      if chars[index] == '[' || chars[index] == ']' {
        return false
      }
      if chars[index] == ':' {
        if colon_seen || index == host_start {
          return false
        }
        colon_seen = true
        port_start = index + 1
      } else if !uri_reg_name_char(chars[index]) {
        return false
      }
    }
  }
  for index = port_start; index < chars.length(); index = index + 1 {
    if chars[index] < '0' || chars[index] > '9' {
      return false
    }
  }
  true
}

///|
/// RFC 3987 ucschar; noncharacters at the end of each plane are excluded.
fn uri_ucschar(code : Int32) -> Bool {
  if (code >= 0xA0 && code <= 0xD7FF) ||
    (code >= 0xF900 && code <= 0xFDCF) ||
    (code >= 0xFDF0 && code <= 0xFFEF) {
    return true
  }
  let plane_tail = code % 0x10000
  plane_tail <= 0xFFFD &&
  ((code >= 0x10000 && code <= 0xDFFFD) || (code >= 0xE1000 && code <= 0xEFFFD))
}

///|
/// RFC 3987 iprivate is allowed in the query alone.
fn uri_iprivate(code : Int32) -> Bool {
  (code >= 0xE000 && code <= 0xF8FF) ||
  (code >= 0xF0000 && code <= 0xFFFFD) ||
  (code >= 0x100000 && code <= 0x10FFFD)
}

///|
/// RFC 3986 reg-name ASCII syntax plus RFC 3987 iunreserved characters.
fn uri_reg_name_char(c : Char) -> Bool {
  let code = c.to_int()
  if code >= 128 {
    return uri_ucschar(code)
  }
  match c {
    'a'..<='z'
    | 'A'..<='Z'
    | '0'..<='9'
    | '-'
    | '.'
    | '_'
    | '~'
    | '%'
    | '!'
    | '$'
    | '&'
    | '\''
    | '('
    | ')'
    | '*'
    | '+'
    | ','
    | ';'
    | '=' => true
    _ => false
  }
}

///|
fn uri_valid_absolute(parts : UriParts) -> Bool {
  match parts.scheme {
    None => false
    Some(scheme) =>
      if scheme.to_lower() == "http" || scheme.to_lower() == "https" {
        match parts.authority {
          Some(authority) => uri_http_authority_valid(authority)
          // RFC 3986 allows a rootless absolute URI such as "http:g" as a
          // reference. Document source URLs are checked separately and must
          // still have a valid HTTP(S) authority.
          None => !parts.path.is_empty() && !parts.path.has_prefix("/")
        }
      } else {
        true
      }
  }
}

///|
fn uri_resolve(base : String, reference : String) -> String? {
  guard uri_parts(base) is Some(base_parts) else { return None }
  if !uri_valid_absolute(base_parts) {
    return None
  }
  guard uri_parts(reference) is Some(ref_parts) else { return None }
  if ref_parts.scheme is Some(_) {
    if !uri_valid_absolute(ref_parts) {
      return None
    }
    return Some(
      uri_build({
        scheme: ref_parts.scheme,
        authority: ref_parts.authority,
        path: uri_remove_dot_segments(ref_parts.path),
        query: ref_parts.query,
        fragment: ref_parts.fragment,
      }),
    )
  }
  if ref_parts.authority is Some(_) {
    let target = {
      scheme: base_parts.scheme,
      authority: ref_parts.authority,
      path: uri_remove_dot_segments(ref_parts.path),
      query: ref_parts.query,
      fragment: ref_parts.fragment,
    }
    return if uri_valid_absolute(target) {
      Some(uri_build(target))
    } else {
      None
    }
  }
  if ref_parts.path.is_empty() {
    return Some(
      uri_build({
        scheme: base_parts.scheme,
        authority: base_parts.authority,
        path: base_parts.path,
        query: match ref_parts.query {
          Some(_) => ref_parts.query
          None => base_parts.query
        },
        fragment: ref_parts.fragment,
      }),
    )
  }
  let path = if ref_parts.path.has_prefix("/") {
    uri_remove_dot_segments(ref_parts.path)
  } else {
    uri_remove_dot_segments(uri_merge(base_parts, ref_parts.path))
  }
  Some(
    uri_build({
      scheme: base_parts.scheme,
      authority: base_parts.authority,
      path,
      query: ref_parts.query,
      fragment: ref_parts.fragment,
    }),
  )
}