// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

// The formats that name a resource: `uri`, `uri-reference` and their
// internationalised twins (RFC 3986 and RFC 3987), and `uri-template`
// (RFC 6570).
//
// moonurl reads a reference into its five components — the scheme, the
// authority, the path, the query and the fragment — including the bracket
// around an IP literal and the one `@` that divides a userinfo from a host.
// What is left for a format to say is whether the characters in each component
// are ones the grammar allows, which is a different question from where the
// components end, and is the one an assertion is asked.

///|
/// Whether `text` is a URI: an absolute one, so a scheme is required.
fn is_uri(text : String, reach : Reach) -> Bool {
  match check_reference(text, reach) {
    Some(absolute) => absolute
    None => false
  }
}

///|
/// Whether `text` is a URI reference: a URI, or a relative reference, or the
/// empty string, which is the reference to this very document.
fn is_uri_reference(text : String, reach : Reach) -> Bool {
  check_reference(text, reach) is Some(_)
}

///|
/// The reference read and checked, answering whether it named a scheme.
///
/// `None` is not a reference at all; `Some(false)` is a relative one.
fn check_reference(text : String, reach : Reach) -> Bool? {
  let url = @moonurl.Url::decode(text) catch { _ => return None }
  let absolute = url.scheme is Some(_)
  if !absolute {
    // A relative reference's first path segment may not hold a colon: that is
    // how a reader tells `1:b` from a scheme, and `./this:that` from a mistake.
    let head = first_segment(url.path)
    if head.contains(":") {
      return None
    }
  }
  match url.userinfo {
    Some(info) => if !component_ok(info, Userinfo, reach) { return None }
    None => ()
  }
  match url.host {
    Some(host) => if !host_ok(host, reach) { return None }
    None => ()
  }
  if !component_ok(url.path, Path, reach) {
    return None
  }
  match url.query {
    Some(query) => if !component_ok(query, Query, reach) { return None }
    None => ()
  }
  match url.fragment {
    Some(fragment) =>
      if !component_ok(fragment, Fragment, reach) {
        return None
      }
    None => ()
  }
  Some(absolute)
}

///|
fn first_segment(path : String) -> String {
  for i = 0; i < path.length(); i = i + 1 {
    if path[i] == '/' {
      return path[0:i].to_owned()
    }
  }
  path
}

///|
/// Which component a run of characters is in, because each allows a different
/// set: a sub-delimiter belongs in a path and not in a host.
priv enum Part {
  Userinfo
  Host
  Path
  Query
  Fragment
}

///|
/// Whether every character in a component is one the grammar gives it, and
/// every `%` introduces two hexadecimal digits.
fn component_ok(text : String, part : Part, reach : Reach) -> Bool {
  // Code points, not code units: a supplementary-plane character is one
  // character to the grammar and two to a reader that counts units.
  let points : Array[Int32] = []
  for c in text {
    points.push(c.to_int())
  }
  let mut i = 0
  while i < points.length() {
    let code = points[i]
    if code == 0x25 {
      guard i + 2 < points.length() else { return false }
      guard hex_value(points[i + 1]) >= 0 && hex_value(points[i + 2]) >= 0 else {
        return false
      }
      i = i + 3
      continue
    }
    if code > 0x7F {
      // RFC 3987 puts a range of non-ASCII into each component and forbids the
      // rest; RFC 3986 has no non-ASCII at all. The private planes belong to a
      // path, a query and a fragment, and to no host.
      guard reach is International &&
        (is_ucschar(code) || (wide_private(part) && is_iprivate(code))) else {
        return false
      }
    } else {
      let allowed = match part {
        Userinfo => is_unreserved(code) || is_sub_delim(code) || code == 0x3A
        Host => is_unreserved(code) || is_sub_delim(code)
        Path => is_pchar(code) || code == 0x2F
        Query | Fragment => is_pchar(code) || code == 0x2F || code == 0x3F
      }
      guard allowed else { return false }
    }
    i = i + 1
  }
  true
}

///|
/// Whether a host is a reg-name, an IPv4 address or an IP literal.
///
/// moonurl hands back an IP literal without its brackets, which leaves the
/// colon as the only sign that there were any — and an unbracketed host cannot
/// hold one, since it would have been read as the port.
fn host_ok(host : String, reach : Reach) -> Bool {
  if host.contains(":") {
    return is_ipv6(host)
  }
  if host.length() > 0 && (host[0] == 'v' || host[0] == 'V') {
    if is_ipvfuture(host) {
      return true
    }
  }
  component_ok(host, Host, reach)
}

///|
/// An IPvFuture literal: `v`, hexadecimal, `.`, and the characters a URI may
/// carry (RFC 3986 §3.2.2).
fn is_ipvfuture(text : String) -> Bool {
  let view = text[:]
  guard view.length() > 3 else { return false }
  let mut i = 1
  let mut digits = 0
  while i < view.length() && hex_value(view[i].to_int()) >= 0 {
    i = i + 1
    digits = digits + 1
  }
  guard digits > 0 && i < view.length() && view[i] == '.' else { return false }
  for j = i + 1; j < view.length(); j = j + 1 {
    let code = view[j].to_int()
    guard is_unreserved(code) || is_sub_delim(code) || code == 0x3A else {
      return false
    }
  }
  true
}

///|
/// Which components RFC 3987 lets a private-use character into.
fn wide_private(part : Part) -> Bool {
  match part {
    Userinfo | Host => false
    Path | Query | Fragment => true
  }
}

///|
fn is_unreserved(code : Int32) -> Bool {
  (code >= 0x41 && code <= 0x5A) ||
  (code >= 0x61 && code <= 0x7A) ||
  (code >= 0x30 && code <= 0x39) ||
  code == 0x2D ||
  code == 0x2E ||
  code == 0x5F ||
  code == 0x7E
}

///|
fn is_sub_delim(code : Int32) -> Bool {
  code == 0x21 ||
  code == 0x24 ||
  code == 0x26 ||
  code == 0x27 ||
  code == 0x28 ||
  code == 0x29 ||
  code == 0x2A ||
  code == 0x2B ||
  code == 0x2C ||
  code == 0x3B ||
  code == 0x3D
}

///|
fn is_pchar(code : Int32) -> Bool {
  is_unreserved(code) || is_sub_delim(code) || code == 0x3A || code == 0x40
}

///|
/// RFC 3987 §2.2's `ucschar`: the non-ASCII a URI may carry once it becomes an
/// identifier rather than a locator. The private ranges are not here — they
/// belong to `iprivate`, which a host may not hold.
fn is_ucschar(code : Int32) -> Bool {
  (code >= 0xA0 && code <= 0xD7FF) ||
  (code >= 0xF900 && code <= 0xFDCF) ||
  (code >= 0xFDF0 && code <= 0xFFEF) ||
  (code >= 0x10000 && code <= 0x1FFFD) ||
  (code >= 0x20000 && code <= 0x2FFFD) ||
  (code >= 0x30000 && code <= 0x3FFFD) ||
  (code >= 0x40000 && code <= 0x4FFFD) ||
  (code >= 0x50000 && code <= 0x5FFFD) ||
  (code >= 0x60000 && code <= 0x6FFFD) ||
  (code >= 0x70000 && code <= 0x7FFFD) ||
  (code >= 0x80000 && code <= 0x8FFFD) ||
  (code >= 0x90000 && code <= 0x9FFFD) ||
  (code >= 0xA0000 && code <= 0xAFFFD) ||
  (code >= 0xB0000 && code <= 0xBFFFD) ||
  (code >= 0xC0000 && code <= 0xCFFFD) ||
  (code >= 0xD0000 && code <= 0xDFFFD) ||
  (code >= 0xE1000 && code <= 0xEFFFD)
}

///|
/// An RFC 6570 template is read by moonurl, which is the same library that
/// expands one: the syntax of a template and the meaning of its expressions
/// are one grammar, and splitting them across two readings is how the two come
/// to disagree.
fn is_uri_template(text : String) -> Bool {
  let points : Array[Int32] = []
  for c in text {
    points.push(c.to_int())
  }
  let mut i = 0
  while i < points.length() {
    if points[i] == 0x7B {
      guard template_expression(points, i) is Some(after) else { return false }
      i = after
    } else {
      guard template_literal(points, i) is Some(after) else { return false }
      i = after
    }
  }
  true
}

///|
/// One literal character, or a percent-coded one, and where it ends.
///
/// The grammar's set, with the apostrophe the suite keeps: `!!!` — RFC 6570 §2
/// says a literal is anything a URI may carry, and the ABNF's list leaves `'`
/// out of a range it otherwise covers.
fn template_literal(points : Array[Int32], from : Int32) -> Int32? {
  let code = points[from]
  if code == 0x25 {
    guard from + 2 < points.length() else { return None }
    guard hex_value(points[from + 1]) >= 0 && hex_value(points[from + 2]) >= 0 else {
      return None
    }
    return Some(from + 3)
  }
  let allowed = (code >= 0x41 && code <= 0x5A) ||
    (code >= 0x61 && code <= 0x7A) ||
    (code >= 0x30 && code <= 0x39) ||
    code == 0x21 ||
    (code >= 0x23 && code <= 0x24) ||
    (code >= 0x26 && code <= 0x27) ||
    (code >= 0x28 && code <= 0x3B) ||
    code == 0x3D ||
    (code >= 0x3F && code <= 0x5B) ||
    code == 0x5D ||
    code == 0x5F ||
    code == 0x7E ||
    (code >= 0xA0 && is_ucschar(code)) ||
    is_iprivate(code)
  if allowed {
    Some(from + 1)
  } else {
    None
  }
}

///|
/// `{` operator? variable-list `}`, and where it ends.
fn template_expression(points : Array[Int32], from : Int32) -> Int32? {
  let mut i = from + 1
  guard i < points.length() else { return None }
  match points[i] {
    // The seven operators of RFC 6570 §2.2.
    0x2B | 0x23 | 0x2E | 0x2F | 0x3B | 0x3F | 0x26 => i = i + 1
    _ => ()
  }
  let mut specs = 0
  while true {
    i = match template_varspec(points, i) {
      Some(after) => after
      None => return None
    }
    specs = specs + 1
    if i < points.length() && points[i] == 0x2C {
      i = i + 1
    } else {
      break
    }
  }
  ignore(specs)
  guard i < points.length() && points[i] == 0x7D else { return None }
  Some(i + 1)
}

///|
/// One `varspec`: a name of name characters, then an optional prefix or an
/// explosion.
fn template_varspec(points : Array[Int32], from : Int32) -> Int32? {
  let mut i = from
  let mut letters = 0
  while i < points.length() {
    let code = points[i]
    if is_varchar(code) {
      i = i + 1
      letters = letters + 1
      continue
    }
    if code == 0x25 {
      // A percent-coded name character.
      guard i + 2 < points.length() &&
        hex_value(points[i + 1]) >= 0 &&
        hex_value(points[i + 2]) >= 0 else {
        return None
      }
      i = i + 3
      letters = letters + 1
      continue
    }
    if code == 0x2E {
      // A dot joins two name characters, and needs one after it: `a..b` is not
      // a name, and neither is `a.`.
      i = i + 1
      guard i < points.length() && is_varchar(points[i]) else { return None }
      continue
    }
    break
  }
  guard letters > 0 else { return None }
  if i < points.length() && points[i] == 0x3A {
    // A prefix length: one to four digits, and no leading zero.
    i = i + 1
    guard i < points.length() else { return None }
    let first = points[i]
    guard first >= 0x31 && first <= 0x39 else { return None }
    let mut digits = 1
    i = i + 1
    while i < points.length() &&
          digits < 4 &&
          points[i] >= 0x30 &&
          points[i] <= 0x39 {
      i = i + 1
      digits = digits + 1
    }
    return Some(i)
  }
  if i < points.length() && points[i] == 0x2A {
    return Some(i + 1)
  }
  Some(i)
}

///|
fn is_varchar(code : Int32) -> Bool {
  (code >= 0x41 && code <= 0x5A) ||
  (code >= 0x61 && code <= 0x7A) ||
  (code >= 0x30 && code <= 0x39) ||
  code == 0x5F
}

///|
/// RFC 3987 §2.2's `iprivate`, which a template literal may carry and a path
/// may not.
fn is_iprivate(code : Int32) -> Bool {
  (code >= 0xE000 && code <= 0xF8FF) ||
  (code >= 0xF0000 && code <= 0xFFFFD) ||
  (code >= 0x100000 && code <= 0x10FFFD)
}