// uri_ref.mbt — Minimal RFC 3986 URI-reference support.
//
// RFC 8288 requires the link target, the `anchor` value, and extension
// relation types to be URI-references, and relative references are to be
// resolved per RFC 3986 Section 5. This module implements exactly the
// subset of RFC 3986 that Web Linking needs:
//
//   - strict validation of a URI-reference (character classes and
//     percent-encoding),
//   - decomposition into scheme / authority / path / query / fragment,
//   - `remove_dot_segments` (RFC 3986 Section 5.2.4),
//   - reference resolution (RFC 3986 Section 5.2.2) with the official
//     test vectors from RFC 3986 Section 5.4.
//
// It deliberately does not implement IDNA, DNS, URL fetching, browser
// normalisation, or internationalised URIs (IRIs). URI text is ASCII only.

///|
/// A decomposed URI-reference.
pub struct UriReference {
  scheme : String?
  authority : String?
  path : String
  query : String?
  fragment : String?
}

///|
/// An empty URI-reference (`""`).
pub fn UriReference::empty() -> UriReference {
  { scheme: None, authority: None, path: "", query: None, fragment: None }
}

///|
/// The scheme component, if present.
pub fn UriReference::scheme(self : UriReference) -> String? {
  self.scheme
}

///|
/// The authority component, if present.
pub fn UriReference::authority(self : UriReference) -> String? {
  self.authority
}

///|
/// The path component (possibly empty).
pub fn UriReference::path(self : UriReference) -> String {
  self.path
}

///|
/// The query component (without the leading `?`), if present.
pub fn UriReference::query(self : UriReference) -> String? {
  self.query
}

///|
/// The fragment component (without the leading `#`), if present.
pub fn UriReference::fragment(self : UriReference) -> String? {
  self.fragment
}

///|
/// Recomposes the URI-reference from its components.
pub fn UriReference::to_string(self : UriReference) -> String {
  let sb = StringBuilder()
  match self.scheme {
    Some(s) => {
      sb.write_string(s)
      sb.write_char(':')
    }
    None => ()
  }
  match self.authority {
    Some(a) => {
      sb.write_string("//")
      sb.write_string(a)
    }
    None => ()
  }
  sb.write_string(self.path)
  match self.query {
    Some(q) => {
      sb.write_char('?')
      sb.write_string(q)
    }
    None => ()
  }
  match self.fragment {
    Some(f) => {
      sb.write_char('#')
      sb.write_string(f)
    }
    None => ()
  }
  sb.to_string()
}

///|
/// Parses and strictly validates a URI-reference.
///
/// Errors: `UriReference::InvalidToken` (character not allowed), and
/// `UriReference::InvalidPercentEncoding` (a `%` not followed by two hex
/// digits).
pub fn parse_uri_reference(input : String) -> Result[UriReference, LinkError] {
  Ok(parse_uri_impl(input)) catch {
    e => Err(unwrap_link_error(e))
  }
}

///|
fn parse_uri_impl(input : String) -> UriReference raise {
  let cursor = Scanner::new(input)
  let mut scheme : String? = None
  let mut authority : String? = None

  // Scheme detection: ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) ":"
  let scheme_start = cursor.position()
  let mut scheme_end = scheme_start
  match cursor.peek_byte() {
    Some(b) if is_alpha(b) => {
      // scan scheme chars
      let mut done = false
      while !done {
        match cursor.peek_byte() {
          Some(b) if is_alpha(b) || is_digit(b) || b == 43 || b == 45 || b == 46 => {
            cursor.pos = cursor.pos + 1
            scheme_end = scheme_end + 1
          }
          _ => done = true
        }
      }
    }
    _ => ()
  }
  // check for ":"
  if cursor.peek_byte() == Some(58) {
    cursor.pos = cursor.pos + 1
    scheme = Some(cursor.take_string(scheme_start, scheme_end))
  } else {
    // no scheme: reset cursor to start
    cursor.pos = scheme_start
  }

  // Authority: "//"
  if cursor.peek_byte() == Some(47) && cursor.peek_at(1) == Some(47) {
    cursor.pos = cursor.pos + 2
    let auth_start = cursor.position()
    // authority ends at "/", "?", "#", or end
    let mut done = false
    while !done {
      match cursor.peek_byte() {
        Some(b) if b == 47 || b == 63 || b == 35 => done = true
        Some(b) if b == 37 => consume_pct_encoded(cursor)
        Some(b) => {
          if !uri_authority_char(b) {
            raise link_error_at(
              UriReference,
              InvalidToken,
              cursor.position(),
              "character not allowed in authority",
            )
          }
          cursor.pos = cursor.pos + 1
        }
        None => done = true
      }
    }
    authority = Some(cursor.take_string(auth_start, cursor.position()))
  }

  // Path: pchar / "/"
  let path_start = cursor.position()
  let mut in_first_segment = scheme == None && authority == None
  let mut done = false
  while !done {
    match cursor.peek_byte() {
      Some(b) if b == 63 || b == 35 => done = true
      Some(b) if b == 47 => {
        in_first_segment = false
        cursor.pos = cursor.pos + 1
      }
      Some(b) if b == 58 && in_first_segment =>
        // In a scheme-less, authority-less reference, a ":" in the first
        // path segment is ambiguous with a scheme separator; RFC 3986
        // path-noscheme forbids it.
        raise link_error_at(
          UriReference,
          InvalidToken,
          cursor.position(),
          "':' in first path segment without a scheme is ambiguous",
        )
      Some(b) if uri_pchar(b) => cursor.pos = cursor.pos + 1
      Some(b) if b == 37 => consume_pct_encoded(cursor)
      Some(b) if is_obs_text(b) || b.to_int() < 32 =>
        raise link_error_at(
          UriReference,
          InvalidToken,
          cursor.position(),
          "non-ASCII or control character in URI-reference",
        )
      Some(_) =>
        raise link_error_at(
          UriReference,
          InvalidToken,
          cursor.position(),
          "character not allowed in path",
        )
      None => done = true
    }
  }
  let path = cursor.take_string(path_start, cursor.position())

  // Query: pchar / "/" / "?"
  let mut query : String? = None
  if cursor.consume_char(63) {
    let q_start = cursor.position()
    let mut qdone = false
    while !qdone {
      match cursor.peek_byte() {
        Some(b) if b == 35 => qdone = true
        Some(b) if uri_pchar(b) || b == 47 || b == 63 =>
          cursor.pos = cursor.pos + 1
        Some(b) if b == 37 => consume_pct_encoded(cursor)
        Some(_) =>
          raise link_error_at(
            UriReference,
            InvalidToken,
            cursor.position(),
            "character not allowed in query",
          )
        None => qdone = true
      }
    }
    query = Some(cursor.take_string(q_start, cursor.position()))
  }

  // Fragment: pchar / "/" / "?"
  let mut fragment : String? = None
  if cursor.consume_char(35) {
    let f_start = cursor.position()
    let mut fdone = false
    while !fdone {
      match cursor.peek_byte() {
        Some(b) if uri_pchar(b) || b == 47 || b == 63 =>
          cursor.pos = cursor.pos + 1
        Some(b) if b == 37 => consume_pct_encoded(cursor)
        Some(_) =>
          raise link_error_at(
            UriReference,
            InvalidToken,
            cursor.position(),
            "character not allowed in fragment",
          )
        None => fdone = true
      }
    }
    fragment = Some(cursor.take_string(f_start, cursor.position()))
  }

  if !cursor.eof() {
    raise link_error_at(
      UriReference,
      InvalidToken,
      cursor.position(),
      "trailing bytes after URI-reference",
    )
  }

  { scheme, authority, path, query, fragment }
}

///|
/// Consumes a `% HEXDIG HEXDIG` triplet at the current position, raising
/// `InvalidPercentEncoding` otherwise.
fn consume_pct_encoded(cursor : Scanner) -> Unit raise {
  let pos = cursor.position()
  cursor.pos = cursor.pos + 1
  match (cursor.peek_byte(), cursor.peek_at(1)) {
    (Some(h1), Some(h2)) if is_hexdigit(h1) && is_hexdigit(h2) =>
      cursor.pos = cursor.pos + 2
    _ =>
      raise link_error_at(
        UriReference,
        InvalidPercentEncoding,
        pos,
        "expected '%' followed by two hex digits",
      )
  }
}

///|
/// Whether a byte is valid inside an authority component.
fn uri_authority_char(b : Byte) -> Bool {
  uri_unreserved(b) ||
  uri_sub_delim(b) ||
  b == 58 ||
  b == 64 ||
  b == 91 ||
  b == 93
}

///|
/// Implements `remove_dot_segments` (RFC 3986 Section 5.2.4) on a path.
///
/// The algorithm consumes the input path one rule at a time: leading `./`
/// and `../` are dropped; `/./` (or a trailing `/ .`) collapses to `/`;
/// `/../` (or a trailing `/ ..`) collapses to `/` and pops the last output
/// segment; a path that is exactly `.` or `..` is dropped; otherwise the
/// first segment (including any leading `/`) is moved to the output. Every
/// rule advances `i`, so the loop always terminates.
pub fn remove_dot_segments(path : String) -> String {
  let bytes = @utf8.encode(path)
  let out = StringBuilder()
  let mut i = 0
  let len = bytes.length()
  while i < len {
    // Rule A: "./" and "../" prefixes are dropped entirely.
    if i + 1 < len && bytes[i] == 46 && bytes[i + 1] == 47 {
      i = i + 2
      continue
    }
    if i + 2 < len && bytes[i] == 46 && bytes[i + 1] == 46 && bytes[i + 2] == 47 {
      i = i + 3
      continue
    }
    // Rule B: "/./" (or a trailing "/.") collapses to "/".
    if i + 2 < len && bytes[i] == 47 && bytes[i + 1] == 46 && bytes[i + 2] == 47 {
      i = i + 2
      continue
    }
    if i + 2 == len && bytes[i] == 47 && bytes[i + 1] == 46 {
      out.write_char('/')
      i = i + 2
      continue
    }
    // Rule C: "/../" (or a trailing "/..") collapses to "/" and pops the
    // last segment of the output.
    if i + 3 < len &&
      bytes[i] == 47 &&
      bytes[i + 1] == 46 &&
      bytes[i + 2] == 46 &&
      bytes[i + 3] == 47 {
      remove_last_segment(out)
      i = i + 3
      continue
    }
    if i + 3 == len &&
      bytes[i] == 47 &&
      bytes[i + 1] == 46 &&
      bytes[i + 2] == 46 {
      remove_last_segment(out)
      out.write_char('/')
      i = i + 3
      continue
    }
    // Rule D: a path that is exactly "." or ".." is dropped.
    if i == 0 && len == 1 && bytes[0] == 46 {
      i = len
      continue
    }
    if i == 0 && len == 2 && bytes[0] == 46 && bytes[1] == 46 {
      i = len
      continue
    }
    // Rule E: move the first segment, including a leading "/".
    let seg_start = i
    if bytes[i] == 47 {
      i = i + 1
    }
    while i < len && bytes[i] != 47 {
      i = i + 1
    }
    out.write_string(decode_utf8(bytes.view(start=seg_start, end=i)))
  }
  out.to_string()
}

///|
/// Removes the last segment (from the last `/` onward) of the text
/// currently accumulated in `sb`.
fn remove_last_segment(sb : StringBuilder) -> Unit {
  let s = sb.to_string()
  let bytes = @utf8.encode(s)
  let mut idx : Int = -1
  for i = 0; i < bytes.length(); i = i + 1 {
    if bytes[i] == 47 {
      idx = i
    }
  }
  let new = if idx >= 0 {
    decode_utf8(bytes.view(start=0, end=idx))
  } else {
    ""
  }
  sb.reset()
  sb.write_string(new)
}

///|
/// Whether the path starts with the given ASCII prefix.
fn path_starts_with(path : String, prefix : String) -> Bool {
  let p = @utf8.encode(path)
  let q = @utf8.encode(prefix)
  if q.length() > p.length() {
    return false
  }
  let mut ok = true
  for i = 0; i < q.length(); i = i + 1 {
    if p[i] != q[i] {
      ok = false
    }
  }
  ok
}

///|
/// Resolves a reference against a base URI-reference, returning the
/// resolved absolute URI-reference as a string. Implements RFC 3986
/// Section 5.2.2.
pub fn resolve_uri_reference_string(
  base : String,
  reference : String,
) -> Result[String, LinkError] {
  match (parse_uri_reference(base), parse_uri_reference(reference)) {
    (Ok(b), Ok(r)) => Ok(resolve_uri_ref(b, r).to_string())
    (Err(e), _) => Err(e)
    (_, Err(e)) => Err(e)
  }
}

///|
/// Resolves a reference against a base URI-reference, returning the
/// resolved URI-reference model.
pub fn resolve_uri_reference(
  base : UriReference,
  reference : UriReference,
) -> UriReference {
  resolve_uri_ref(base, reference)
}

///|
fn resolve_uri_ref(base : UriReference, r : UriReference) -> UriReference {
  // Reference is an absolute URI: it stands on its own. The path is
  // normalised.
  match r.scheme {
    Some(s) =>
      {
        scheme: Some(s),
        authority: r.authority,
        path: remove_dot_segments(r.path),
        query: r.query,
        fragment: r.fragment,
      }
    None =>
      match r.authority {
        Some(a) =>
          {
            scheme: base.scheme,
            authority: Some(a),
            path: remove_dot_segments(r.path),
            query: r.query,
            fragment: r.fragment,
          }
        None => {
          let (path, query) = if r.path == "" {
            (
              base.path,
              match r.query {
                Some(q) => Some(q)
                None => base.query
              },
            )
          } else {
            let p = if path_starts_with(r.path, "/") {
              remove_dot_segments(r.path)
            } else {
              remove_dot_segments(merge_paths(base, r.path))
            }
            (p, r.query)
          }
          {
            scheme: base.scheme,
            authority: base.authority,
            path,
            query,
            fragment: r.fragment,
          }
        }
      }
  }
}

///|
/// RFC 3986 Section 5.2.3 `merge`.
fn merge_paths(base : UriReference, ref_path : String) -> String {
  if base.authority is Some(_) && base.path == "" {
    return "/" + ref_path
  }
  let p = @utf8.encode(base.path)
  let mut idx : Int = -1
  for i = 0; i < p.length(); i = i + 1 {
    if p[i] == 47 {
      idx = i
    }
  }
  let prefix = if idx >= 0 {
    decode_utf8(p.view(start=0, end=idx + 1))
  } else {
    ""
  }
  prefix + ref_path
}