///|
/// Original IRI reference and its RFC 3986 resolved form. Without an absolute
/// XML/document base, a relative result remains relative and is marked explicitly.
pub(all) struct IriReference {
  raw : String
  resolved : String
  is_absolute : Bool
}

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

///|
fn chars_span(chars : Array[Char], start : Int, end : Int) -> String {
  let out = StringBuilder()
  for i = start; i < end; i = i + 1 {
    out.write_char(chars[i])
  }
  out.to_string()
}

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

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

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

///|
fn iri_authority(value : String) -> Unit raise ParseError {
  let a = value.to_array()
  let mut start = 0
  let mut at_seen = false
  for i, c in a {
    if c == '@' {
      if at_seen {
        raise InvalidRequiredField(
          "iri", "multiple authority userinfo separators",
        )
      }
      at_seen = true
      start = i + 1
    }
  }
  for i = 0; i < start; i = i + 1 {
    if a[i] == '[' || a[i] == ']' {
      raise InvalidRequiredField("iri", "invalid userinfo character")
    }
  }
  let mut port_start = a.length()
  if start < a.length() && a[start] == '[' {
    let mut end = start + 1
    while end < a.length() && a[end] != ']' {
      end += 1
    }
    if end == a.length() {
      raise InvalidRequiredField("iri", "unclosed IP literal")
    }
    let literal = chars_span(a, start + 1, end)
    if !ip_literal(literal) {
      raise InvalidRequiredField("iri", "invalid IP literal")
    }
    if end + 1 < a.length() {
      if a[end + 1] != ':' {
        raise InvalidRequiredField("iri", "invalid data after IP literal")
      }
      port_start = end + 2
    }
  } else {
    let mut colon_seen = false
    for i = start; i < a.length(); i = i + 1 {
      if a[i] == '[' || a[i] == ']' {
        raise InvalidRequiredField("iri", "invalid host brackets")
      }
      if a[i] == ':' {
        if colon_seen {
          raise InvalidRequiredField("iri", "IPv6 host must be bracketed")
        }
        colon_seen = true
        port_start = i + 1
      }
    }
  }
  for i = port_start; i < a.length(); i = i + 1 {
    if !ascii_digit(a[i]) {
      raise InvalidRequiredField("iri", "port must be decimal digits")
    }
  }
}

///|
fn iri_parts(input : String) -> IriParts raise ParseError {
  parse_iri_parts(input, false)
}

///|
fn parse_iri_parts(
  input : String,
  legacy_base : Bool,
) -> IriParts raise ParseError {
  let a = input.to_array()
  for i, c in a {
    if (
        !legacy_base &&
        (
          c.to_int() <= 0x20 ||
          (c.to_int() >= 0x80 && c.to_int() <= 0x9F) ||
          c.to_int() == 0x7F ||
          c == '<' ||
          c == '>' ||
          c == '"' ||
          c == '\\' ||
          c == '^' ||
          c == '`' ||
          c == '{' ||
          c == '|' ||
          c == '}'
        )
      ) ||
      c.to_int() < 0x20 {
      raise InvalidRequiredField("iri", "invalid IRI character")
    }
    if c == '%' &&
      (i + 2 >= a.length() || !hex_digit(a[i + 1]) || !hex_digit(a[i + 2])) {
      raise InvalidRequiredField("iri", "invalid percent escape")
    }
  }
  let mut fragment_at = a.length()
  for i, c in a {
    if c == '#' {
      if fragment_at != a.length() {
        raise InvalidRequiredField("iri", "multiple fragment delimiters")
      }
      fragment_at = i
    }
  }
  let fragment = if fragment_at < a.length() {
    Some(chars_span(a, fragment_at + 1, a.length()))
  } else {
    None
  }
  let mut query_at = fragment_at
  for i = 0; i < fragment_at; i = i + 1 {
    if a[i] == '?' {
      query_at = i
      break
    }
  }
  let query = if query_at < fragment_at {
    Some(chars_span(a, query_at + 1, fragment_at))
  } else {
    None
  }
  let mut scheme = None
  let mut start = 0
  for i = 0; i < query_at; i = i + 1 {
    if a[i] == '/' {
      break
    }
    if a[i] == ':' {
      if i == 0 || !ascii_alpha(a[0]) {
        raise InvalidRequiredField("iri", "invalid scheme")
      }
      for j = 1; j < i; j = j + 1 {
        if !ascii_alpha(a[j]) &&
          !ascii_digit(a[j]) &&
          a[j] != '+' &&
          a[j] != '-' &&
          a[j] != '.' {
          raise InvalidRequiredField("iri", "invalid scheme")
        }
      }
      scheme = Some(chars_span(a, 0, i))
      start = i + 1
      break
    }
  }
  let mut authority = None
  if start + 1 < query_at && a[start] == '/' && a[start + 1] == '/' {
    let auth_start = start + 2
    start = auth_start
    while start < query_at && a[start] != '/' {
      start += 1
    }
    let auth = chars_span(a, auth_start, start)
    let mut brackets = 0
    for c in auth.to_array() {
      if c == '[' {
        brackets += 1
      }
      if c == ']' {
        brackets -= 1
      }
      if brackets < 0 || brackets > 1 {
        raise InvalidRequiredField("iri", "invalid authority brackets")
      }
    }
    if brackets != 0 {
      raise InvalidRequiredField("iri", "unclosed authority bracket")
    }
    authority = Some(auth)
    iri_authority(auth)
  }
  let path = chars_span(a, start, query_at)
  if !legacy_base {
    // Private-use code points are allowed only in iquery, not path/fragment/host.
    for
      (component, private_allowed) in [
        (Some(path), false),
        (query, true),
        (fragment, false),
        (authority, false),
      ] {
      if component is Some(value) {
        for c in value.to_array() {
          if c.to_int() >= 0x80 && !iri_unicode(c, private_allowed) {
            raise InvalidRequiredField(
              "iri", "character outside RFC 3987 component range",
            )
          }
        }
      }
    }
  }
  for component in [Some(path), query, fragment] {
    if component is Some(s) && (s.contains("[") || s.contains("]")) {
      raise InvalidRequiredField(
        "iri", "brackets are only allowed in authority IP literals",
      )
    }
  }
  { scheme, authority, path, query, fragment, }
}

///|
fn drop_last_segment(path : String) -> String {
  let a = path.to_array()
  let mut last = -1
  for i, c in a {
    if c == '/' {
      last = i
    }
  }
  if last < 0 {
    ""
  } else {
    chars_span(a, 0, last)
  }
}

///|
/// RFC 3986 section 5.2.4, preserving empty segments and percent-encoded dots.
fn remove_dot_segments(path : String) -> String {
  let mut input = path
  let mut output = ""
  while !input.is_empty() {
    if input.has_prefix("../") {
      input = input[3:].to_owned()
    } else if input.has_prefix("./") {
      input = input[2:].to_owned()
    } else if input.has_prefix("/./") {
      input = input[2:].to_owned()
    } else if input == "/." {
      input = "/"
    } else if input.has_prefix("/../") {
      input = input[3:].to_owned()
      output = drop_last_segment(output)
    } else if input == "/.." {
      input = "/"
      output = drop_last_segment(output)
    } else if input == "." || input == ".." {
      input = ""
    } else {
      let a = input.to_array()
      let mut end = if a[0] == '/' { 1 } else { 0 }
      while end < a.length() && a[end] != '/' {
        end += 1
      }
      output = output + chars_span(a, 0, end)
      input = chars_span(a, end, a.length())
    }
  }
  output
}

///|
fn recompose(parts : IriParts) -> String {
  let out = StringBuilder()
  if parts.scheme is Some(s) {
    out.write_string(s + ":")
  }
  if parts.authority is Some(a) {
    out.write_string("//" + a)
  }
  out.write_string(parts.path)
  if parts.query is Some(q) {
    out.write_string("?" + q)
  }
  if parts.fragment is Some(f) {
    out.write_string("#" + f)
  }
  out.to_string()
}

///|
/// Until a retrieval/absolute base is known, leading parent segments must survive.
fn unresolved_dot_segments(path : String) -> String {
  let segments = path.split("/").to_array()
  let output : Array[String] = []
  for i, segment in segments {
    if segment == "." {
      continue
    }
    if segment == ".." {
      if !output.is_empty() && output[output.length() - 1] != ".." {
        let _ = output.pop()
      } else {
        output.push("..")
      }
    } else if i != segments.length() - 1 || !segment.is_empty() {
      output.push(segment.to_owned())
    }
  }
  let result = output.join("/")
  if !result.is_empty() &&
    (
      path.has_suffix("/") ||
      path.has_suffix("/.") ||
      path.has_suffix("/..") ||
      path == ".."
    ) {
    result + "/"
  } else {
    result
  }
}

///|
/// Resolve by component parsing, path merging and dot-segment removal (RFC 3986 ยง5).
pub fn resolve_reference(
  base : String?,
  reference : String,
) -> IriReference raise ParseError {
  resolve_iri(base, reference, false)
}

///|
fn resolve_xml_base(
  base : String?,
  reference : String,
) -> IriReference raise ParseError {
  resolve_iri(base, reference, true)
}

///|
fn resolve_iri(
  base : String?,
  reference : String,
  legacy_base : Bool,
) -> IriReference raise ParseError {
  let r = parse_iri_parts(reference, legacy_base)
  let target = if r.scheme is Some(_) {
    { ..r, path: remove_dot_segments(r.path), }
  } else if base is Some(value) {
    let b = parse_iri_parts(value, true)
    if r.authority is Some(_) {
      { ..r, scheme: b.scheme, path: remove_dot_segments(r.path), }
    } else if r.path.is_empty() {
      {
        ..r,
        scheme: b.scheme,
        authority: b.authority,
        path: b.path,
        query: if r.query is Some(_) {
          r.query
        } else {
          b.query
        },
      }
    } else {
      let merged = if r.path.has_prefix("/") {
        r.path
      } else if b.authority is Some(_) && b.path.is_empty() {
        "/" + r.path
      } else {
        let a = b.path.to_array()
        let mut last = -1
        for i, c in a {
          if c == '/' {
            last = i
          }
        }
        chars_span(a, 0, last + 1) + r.path
      }
      {
        ..r,
        scheme: b.scheme,
        authority: b.authority,
        path: if b.scheme is None &&
          b.authority is None &&
          !merged.has_prefix("/") {
          unresolved_dot_segments(merged)
        } else {
          remove_dot_segments(merged)
        },
      }
    }
  } else if legacy_base && !r.path.has_prefix("/") {
    { ..r, path: unresolved_dot_segments(r.path), }
  } else {
    r
  }
  {
    raw: reference,
    resolved: recompose(target),
    is_absolute: target.scheme is Some(_),
  }
}