///|
/// RFC 3986 URI reference.
pub struct Uri {
  scheme : String
  userinfo : String
  host : String
  port : String
  path : String
  query : String
  fragment : String
  has_authority : Bool
}

///|
pub fn Uri::scheme(self : Uri) -> String {
  self.scheme
}

///|
pub fn Uri::userinfo(self : Uri) -> String {
  self.userinfo
}

///|
pub fn Uri::host(self : Uri) -> String {
  self.host
}

///|
pub fn Uri::port(self : Uri) -> String {
  self.port
}

///|
pub fn Uri::path(self : Uri) -> String {
  self.path
}

///|
pub fn Uri::query(self : Uri) -> String {
  self.query
}

///|
pub fn Uri::fragment(self : Uri) -> String {
  self.fragment
}

///|
pub fn Uri::has_authority(self : Uri) -> Bool {
  self.has_authority
}

///|
pub fn parse_uri(input : String) -> Result[Uri, String] {
  if !valid_percent(input) {
    return Err("uri.percent")
  }
  let frag = split_once(input, "#")
  let qy = split_once(frag.0, "?")
  let (scheme, hier) = take_scheme(qy.0)
  let has_authority = hier.has_prefix("//")
  let (userinfo, host, port, path) = if has_authority {
    parse_hier(hier)
  } else {
    ("", "", "", hier)
  }
  if has_authority && host.has_prefix("[") && !host.has_suffix("]") {
    return Err("uri.host")
  }
  if port.length() > 0 && !all_digits(port) {
    return Err("uri.port")
  }
  if port.length() > 0 && !port_in_range(port) {
    return Err("uri.port")
  }
  if has_authority && host.contains(":") && !host.has_prefix("[") {
    return Err("uri.port")
  }
  Ok({
    scheme,
    userinfo,
    host,
    port,
    path,
    query: qy.1,
    fragment: frag.1,
    has_authority,
  })
}

///|
fn take_scheme(input : String) -> (String, String) {
  match input.find(":") {
    Some(i) => {
      let candidate = input.sub(end=i).to_owned()
      if valid_scheme(candidate) {
        (candidate, input.sub(start=i + 1).to_owned())
      } else {
        ("", input)
      }
    }
    None => ("", input)
  }
}

///|
fn parse_hier(hier : String) -> (String, String, String, String) {
  let rest = hier.sub(start=2).to_owned()
  let end = authority_end(rest)
  let authority = rest.sub(end~).to_owned()
  let path = rest.sub(start=end).to_owned()
  let ui = split_last(authority, "@")
  let userinfo = ui.0
  let hostport = ui.1
  if hostport.has_prefix("[") {
    match hostport.find("]") {
      Some(i) => {
        let host = hostport.sub(end=i + 1).to_owned()
        let port = if i + 1 < hostport.length() &&
          hostport.sub(start=i + 1, end=i + 2).to_owned() == ":" {
          hostport.sub(start=i + 2).to_owned()
        } else {
          ""
        }
        (userinfo, host, port, path)
      }
      None => (userinfo, hostport, "", path)
    }
  } else {
    let hp = split_last(hostport, ":")
    if hp.0.length() > 0 && all_digits(hp.1) {
      (userinfo, hp.0, hp.1, path)
    } else {
      (userinfo, hostport, "", path)
    }
  }
}

///|
fn authority_end(s : String) -> Int {
  match s.find("/") {
    Some(i) => i
    None => s.length()
  }
}

///|
fn split_once(input : String, sep : String) -> (String, String) {
  match input.find(sep) {
    Some(i) =>
      (
        input.sub(end=i).to_owned(),
        input.sub(start=i + sep.length()).to_owned(),
      )
    None => (input, "")
  }
}

///|
fn split_last(input : String, sep : String) -> (String, String) {
  match input.rev_find(sep) {
    Some(i) =>
      (
        input.sub(end=i).to_owned(),
        input.sub(start=i + sep.length()).to_owned(),
      )
    None => ("", input)
  }
}

///|
fn valid_scheme(s : String) -> Bool {
  if s.length() == 0 {
    return false
  }
  let mut first = true
  for c in s {
    if first {
      if !is_alpha(c) {
        return false
      }
      first = false
    } else if !(is_alpha(c) || is_digit(c) || c == '+' || c == '-' || c == '.') {
      return false
    }
  }
  true
}

///|
fn is_alpha(c : Char) -> Bool {
  (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
}

///|
fn is_digit(c : Char) -> Bool {
  c >= '0' && c <= '9'
}

///|
fn all_digits(s : String) -> Bool {
  if s.length() == 0 {
    return false
  }
  for c in s {
    if !is_digit(c) {
      return false
    }
  }
  true
}

///|
fn port_in_range(s : String) -> Bool {
  let mut n = 0
  for c in s {
    n = n * 10 + (c.to_int() - 48)
    if n > 65535 {
      return false
    }
  }
  true
}

///|
fn is_hex(c : Char) -> Bool {
  is_digit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')
}

///|
fn valid_percent(s : String) -> Bool {
  let chars = s.to_array()
  let mut i = 0
  while i < chars.length() {
    if chars[i] == '%' {
      if i + 2 >= chars.length() ||
        !is_hex(chars[i + 1]) ||
        !is_hex(chars[i + 2]) {
        return false
      }
      i = i + 3
    } else {
      i = i + 1
    }
  }
  true
}

///|
pub fn to_string(uri : Uri) -> String {
  let mut out = ""
  if uri.scheme.length() > 0 {
    out = out + uri.scheme + ":"
  }
  if uri.has_authority {
    out = out + "//"
    if uri.userinfo.length() > 0 {
      out = out + uri.userinfo + "@"
    }
    out = out + uri.host
    if uri.port.length() > 0 {
      out = out + ":" + uri.port
    }
  }
  out = out + uri.path
  if uri.query.length() > 0 {
    out = out + "?" + uri.query
  }
  if uri.fragment.length() > 0 {
    out = out + "#" + uri.fragment
  }
  out
}

///|
pub fn encode_component(input : String) -> String {
  let mut out = ""
  for c in input {
    if is_unreserved(c) {
      out = out + String::from_array([c])
    } else {
      let n = c.to_int()
      out = out + "%" + hex_digit(n / 16) + hex_digit(n % 16)
    }
  }
  out
}

///|
fn is_unreserved(c : Char) -> Bool {
  is_alpha(c) || is_digit(c) || c == '-' || c == '.' || c == '_' || c == '~'
}

///|
fn hex_digit(n : Int) -> String {
  let table = "0123456789ABCDEF"
  table.sub(start=n, end=n + 1).to_owned()
}

///|
pub fn decode_component(input : String) -> Result[String, String] {
  if !valid_percent(input) {
    return Err("uri.percent")
  }
  let chars = input.to_array()
  let mut out = ""
  let mut i = 0
  while i < chars.length() {
    if chars[i] == '%' {
      let v = hex_val(chars[i + 1]) * 16 + hex_val(chars[i + 2])
      out = out + String::from_array([v.unsafe_to_char()])
      i = i + 3
    } else {
      out = out + String::from_array([chars[i]])
      i = i + 1
    }
  }
  Ok(out)
}

///|
fn hex_val(c : Char) -> Int {
  if is_digit(c) {
    c.to_int() - 48
  } else if c >= 'a' && c <= 'f' {
    c.to_int() - 87
  } else {
    c.to_int() - 55
  }
}

///|

///|
fn plus_to_space(s : String) -> String {
  let mut out = ""
  for c in s {
    if c == '+' {
      out = out + " "
    } else {
      out = out + String::from_array([c])
    }
  }
  out
}

///|
pub fn parse_query(input : String) -> Result[Array[(String, String)], String] {
  let result : Array[(String, String)] = []
  if input.length() == 0 {
    return Ok(result)
  }
  for item in input.split("&") {
    let text = item.to_owned()
    if text.length() == 0 {
      continue
    }
    match text.split_once("=") {
      Some((k, v)) =>
        match decode_component(plus_to_space(k.to_owned())) {
          Ok(dk) =>
            match decode_component(plus_to_space(v.to_owned())) {
              Ok(dv) => result.push((dk, dv))
              Err(e) => return Err(e)
            }
          Err(e) => return Err(e)
        }
      None =>
        match decode_component(plus_to_space(text)) {
          Ok(dk) => result.push((dk, ""))
          Err(e) => return Err(e)
        }
    }
  }
  Ok(result)
}

///|
pub fn query_get_all(
  input : String,
  key : String,
) -> Result[Array[String], String] {
  let values : Array[String] = []
  match parse_query(input) {
    Err(e) => return Err(e)
    Ok(pairs) =>
      for pair in pairs {
        if pair.0 == key {
          values.push(pair.1)
        }
      }
  }
  Ok(values)
}

///|
pub fn remove_dot_segments(path : String) -> String {
  let segs = path.split("/")
  let out : Array[String] = []
  let abs = path.has_prefix("/")
  for seg in segs {
    let s = seg.to_owned()
    if s == "" || s == "." {
      continue
    } else if s == ".." {
      if out.length() > 0 {
        ignore(out.pop())
      }
    } else {
      out.push(s)
    }
  }
  let mut joined = if abs { "/" } else { "" }
  let mut i = 0
  while i < out.length() {
    if i > 0 {
      joined = joined + "/"
    }
    joined = joined + out[i]
    i = i + 1
  }
  if path.has_suffix("/") && !joined.has_suffix("/") && joined != "/" {
    joined = joined + "/"
  }
  joined
}

///|
pub fn normalize_uri(uri : Uri) -> Uri {
  let scheme = uri.scheme.to_lower()
  let port = if uri.port == default_port(scheme) { "" } else { uri.port }
  {
    scheme,
    userinfo: uri.userinfo,
    host: uri.host.to_lower(),
    port,
    path: remove_dot_segments(uri.path),
    query: uri.query,
    fragment: uri.fragment,
    has_authority: uri.has_authority,
  }
}

///|
fn default_port(scheme : String) -> String {
  if scheme == "http" || scheme == "ws" {
    "80"
  } else if scheme == "https" || scheme == "wss" {
    "443"
  } else if scheme == "ftp" {
    "21"
  } else {
    ""
  }
}

///|
pub fn resolve_reference(base : Uri, reference : Uri) -> Uri {
  if reference.scheme.length() > 0 {
    {
      scheme: reference.scheme,
      userinfo: reference.userinfo,
      host: reference.host,
      port: reference.port,
      path: remove_dot_segments(reference.path),
      query: reference.query,
      fragment: reference.fragment,
      has_authority: reference.has_authority,
    }
  } else if reference.has_authority {
    {
      scheme: base.scheme,
      userinfo: reference.userinfo,
      host: reference.host,
      port: reference.port,
      path: remove_dot_segments(reference.path),
      query: reference.query,
      fragment: reference.fragment,
      has_authority: true,
    }
  } else {
    let path = if reference.path.length() == 0 {
      base.path
    } else if reference.path.has_prefix("/") {
      remove_dot_segments(reference.path)
    } else {
      merge_path(base, reference.path)
    }
    let query = if reference.path.length() == 0 && reference.query.length() == 0 {
      base.query
    } else if reference.path.length() == 0 {
      reference.query
    } else {
      reference.query
    }
    {
      scheme: base.scheme,
      userinfo: base.userinfo,
      host: base.host,
      port: base.port,
      path,
      query,
      fragment: reference.fragment,
      has_authority: base.has_authority,
    }
  }
}

///|
fn merge_path(base : Uri, rel : String) -> String {
  if base.has_authority && base.path.length() == 0 {
    remove_dot_segments("/" + rel)
  } else {
    match base.path.rev_find("/") {
      Some(i) => remove_dot_segments(base.path.sub(end=i + 1).to_owned() + rel)
      None => remove_dot_segments(rel)
    }
  }
}

///|
/// Match `/users/{id}` style path templates. Literal segments must equal.
pub fn match_path(
  pattern : String,
  path : String,
) -> Result[Array[(String, String)], String] {
  let p = segs(pattern)
  let s = segs(path)
  if p.length() != s.length() {
    return Err("uri.path.mismatch")
  }
  let out : Array[(String, String)] = []
  let mut i = 0
  while i < p.length() {
    let pat = p[i]
    if pat.has_prefix("{") && pat.has_suffix("}") && pat.length() > 2 {
      match decode_component(s[i]) {
        Ok(val) =>
          out.push((pat.sub(start=1, end=pat.length() - 1).to_owned(), val))
        Err(e) => return Err(e)
      }
    } else if pat != s[i] {
      return Err("uri.path.mismatch")
    }
    i = i + 1
  }
  Ok(out)
}

///|
fn segs(s : String) -> Array[String] {
  let out : Array[String] = []
  for part in s.split("/") {
    let t = part.to_owned()
    if t.length() > 0 {
      out.push(t)
    }
  }
  out
}

///|
pub fn is_absolute(uri : Uri) -> Bool {
  uri.scheme.length() > 0
}

///|
pub fn origin(uri : Uri) -> String {
  if !uri.has_authority {
    return ""
  }
  let mut out = ""
  if uri.scheme.length() > 0 {
    out = out + uri.scheme + "://"
  } else {
    out = out + "//"
  }
  out = out + uri.host
  if uri.port.length() > 0 {
    out = out + ":" + uri.port
  }
  out
}

///|
pub fn query_has(input : String, key : String) -> Bool {
  match query_get_all(input, key) {
    Ok(values) => values.length() > 0
    Err(_) => false
  }
}

///|
pub fn encode_query(pairs : Array[(String, String)]) -> String {
  let mut out = ""
  let mut i = 0
  while i < pairs.length() {
    if i > 0 {
      out = out + "&"
    }
    out = out +
      encode_component(pairs[i].0) +
      "=" +
      encode_component(pairs[i].1)
    i = i + 1
  }
  out
}

///|
pub fn with_query(uri : Uri, query : String) -> Uri {
  {
    scheme: uri.scheme,
    userinfo: uri.userinfo,
    host: uri.host,
    port: uri.port,
    path: uri.path,
    query,
    fragment: uri.fragment,
    has_authority: uri.has_authority,
  }
}

///|
pub fn with_path(uri : Uri, path : String) -> Uri {
  {
    scheme: uri.scheme,
    userinfo: uri.userinfo,
    host: uri.host,
    port: uri.port,
    path,
    query: uri.query,
    fragment: uri.fragment,
    has_authority: uri.has_authority,
  }
}

///|
pub fn with_fragment(uri : Uri, fragment : String) -> Uri {
  {
    scheme: uri.scheme,
    userinfo: uri.userinfo,
    host: uri.host,
    port: uri.port,
    path: uri.path,
    query: uri.query,
    fragment,
    has_authority: uri.has_authority,
  }
}

///|
pub fn same_origin(a : Uri, b : Uri) -> Bool {
  origin(a) == origin(b) && origin(a).length() > 0
}

///|
pub fn strip_fragment(uri : Uri) -> Uri {
  with_fragment(uri, "")
}

///|
pub fn query_drop(input : String, key : String) -> Result[String, String] {
  match parse_query(input) {
    Err(e) => Err(e)
    Ok(pairs) => {
      let kept : Array[(String, String)] = []
      for pair in pairs {
        if pair.0 != key {
          kept.push(pair)
        }
      }
      Ok(encode_query(kept))
    }
  }
}