///|
/// Syntax-based normalization, as defined in
/// [Section 6 of RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986#section-6).

///|
/// An error raised when normalizing a URI/IRI (reference).
pub(all) suberror NormalizeError {
  /// An underflow occurred in path normalization. Raised only by the
  /// `normalize_strict` methods.
  NormalizePathUnderflow
} derive(Eq, Debug)

///|
pub impl Show for NormalizeError with fn output(_self, logger) {
  logger.write_string("underflow occurred in path resolution")
}

///|
/// The table used to normalize a query, which additionally preserves the
/// characters matching the `iprivate` ABNF rule.
let table_iquery_data : @enc.Table = @enc.table_idata.or_iprivate()

///|
/// Parses `s` as an IPv4 address, falling back to a registered name.
fn parse_v4_or_reg_name(s : String) -> Host[@enc.IRegName] {
  let p = Parser::new(s, ascii_only=true, scheme_required=false)
  match p.read_v4() {
    Some(addr) if !p.has_remaining() => Ipv4(addr)
    _ => RegName(estr(s))
  }
}

///|
/// Normalizes the percent-encoding of `s`: octets that stand for characters
/// allowed by `table` are decoded, the others are uppercased.
fn normalize_estr(
  s : String,
  to_ascii_lowercase : Bool,
  table : @enc.Table,
) -> String {
  let sb = StringBuilder::new(size_hint=s.length())
  if table.allows_non_ascii() {
    decode_walk(
      s,
      run => {
        if to_ascii_lowercase {
          sb.write_string(ascii_lowercase(run))
        } else {
          sb.write_string(run)
        }
      },
      (valid, invalid) => {
        encode_str(sb, table, valid, to_ascii_lowercase~)
        for i in 0.. {
        if to_ascii_lowercase {
          sb.write_string(ascii_lowercase(run))
        } else {
          sb.write_string(run)
        }
      },
      octet => {
        if table.allows_ascii(octet) {
          let x = if to_ascii_lowercase &&
            octet >= 'A'.to_int() &&
            octet <= 'Z'.to_int() {
            octet + 32
          } else {
            octet
          }
          sb.write_char(x.to_char().unwrap())
        } else {
          push_encoded_byte(sb, octet)
        }
      },
    )
  }
  sb.to_string()
}

///|
/// Normalizes a URI/IRI (reference), also returning whether an underflow
/// occurred in path normalization.
fn normalize_ri(
  r : Ri,
  ascii_only~ : Bool,
  default_port~ : (String) -> Int?,
) -> (Ri, Bool) {
  let data_table = if ascii_only { @enc.table_data } else { @enc.table_idata }
  let scheme = opt_map(r.scheme, s => {
    scheme_validated(ascii_lowercase(s.as_str()))
  })
  let authority = match r.authority {
    None => None
    Some(auth) => {
      let userinfo = opt_map(auth.userinfo, v => {
        estr(normalize_estr(v.as_str(), false, data_table))
      })
      let (host, host_parsed) : (String, Host[@enc.IRegName]) = match
        auth.host_parsed {
        // An IPv4 address is always canonical.
        Ipv4(addr) => (auth.host, Ipv4(addr))
        Ipv6(addr) => ("[" + addr.to_string() + "]", Ipv6(addr))
        IpvFuture => (ascii_lowercase(auth.host), IpvFuture)
        RegName(_) => {
          let host = normalize_estr(auth.host, true, data_table)
          if host.length() < auth.host.length() {
            // Only reparse when the length is less than before.
            (host, parse_v4_or_reg_name(host))
          } else {
            (host, RegName(estr(host)))
          }
        }
      }
      let port = match auth.port {
        Some(port) if !port.is_empty() => {
          let mut eq_default = false
          if scheme is Some(scheme) {
            if default_port(scheme.as_str()) is Some(default) {
              eq_default = port_eq(port.as_str(), default)
            }
          }
          if eq_default {
            None
          } else {
            Some(port)
          }
        }
        _ => None
      }
      Some({ userinfo, host, host_parsed, port })
    }
  }
  let (path, underflow) = if r.scheme is Some(_) && r.path.has_prefix("/") {
    let path = normalize_estr(r.path, false, data_table)
    let (path, underflow) = remove_dot_segments(path, None)
    // Make sure that the output is a valid URI/IRI reference.
    if authority is None && path.has_prefix("//") {
      ("/." + path, underflow)
    } else {
      (path, underflow)
    }
  } else {
    // Don't remove dot segments from a relative reference or a rootless path.
    (normalize_estr(r.path, false, data_table), false)
  }
  let query_data_table = if ascii_only {
    @enc.table_data
  } else {
    table_iquery_data
  }
  let query = opt_map(r.query, q => normalize_estr(q, false, query_data_table))
  let fragment = opt_map(r.fragment, f => normalize_estr(f, false, data_table))
  (ri(scheme~, authority~, path~, query~, fragment~), underflow)
}

///|
/// Checks whether the nonempty port `s` denotes the number `default`.
/// Leading zeros are ignored; a port that does not fit in a `u16` never
/// matches.
fn port_eq(s : String, default : Int) -> Bool {
  let mut value = 0
  for i in 0.. 65535 {
      return false
    }
  }
  value == default
}

///|
/// The default `default_port` function, which knows no default ports.
fn no_default_port(_scheme : String) -> Int? {
  None
}

///|
/// Normalizes the URI.
///
/// The following normalizations are applied:
///
/// - The scheme and the host are lowercased.
/// - Percent-encoded octets that stand for unreserved characters are decoded,
///   and the remaining ones are uppercased.
/// - Dot segments are removed from an absolute path.
/// - An empty port is removed, and so is a port equal to the scheme's default,
///   as given by `default_port`. The scheme passed to `default_port` is
///   already lowercased.
/// - An IPv6 address is rewritten in its canonical form.
///
/// ```mbt check
/// test {
///   let uri = @uri.Uri::parse("eXAMPLE://a/./b/../b/%63/%7bfoo%7d")
///   inspect(uri.normalize(), content="example://a/b/c/%7Bfoo%7D")
///   let uri = @uri.Uri::parse("http://example.com:80/")
///   inspect(
///     uri.normalize(default_port=scheme => {
///       if scheme is "http" {
///         Some(80)
///       } else {
///         None
///       }
///     }),
///     content="http://example.com/",
///   )
/// }
/// ```
pub fn Uri::normalize(
  self : Uri,
  default_port? : (String) -> Int? = no_default_port,
) -> Uri {
  let (r, _) = normalize_ri(self.to_ri(), ascii_only=true, default_port~)
  uri_of_ri(r)
}

///|
/// Normalizes the URI, rejecting an underflow in path normalization.
///
/// # Errors
///
/// Raises [`NormalizeError`] if a `".."` segment of an absolute path tries to
/// escape the root, e.g. in `"http://example.com/.."`.
pub fn Uri::normalize_strict(
  self : Uri,
  default_port? : (String) -> Int? = no_default_port,
) -> Uri raise NormalizeError {
  let (r, underflow) = normalize_ri(
    self.to_ri(),
    ascii_only=true,
    default_port~,
  )
  if underflow {
    raise NormalizePathUnderflow
  }
  uri_of_ri(r)
}

///|
/// Normalizes the URI reference. See [`Uri::normalize`].
pub fn UriRef::normalize(
  self : UriRef,
  default_port? : (String) -> Int? = no_default_port,
) -> UriRef {
  let (r, _) = normalize_ri(self.to_ri(), ascii_only=true, default_port~)
  uri_ref_of_ri(r)
}

///|
/// Normalizes the URI reference, rejecting an underflow in path
/// normalization. See [`Uri::normalize_strict`].
pub fn UriRef::normalize_strict(
  self : UriRef,
  default_port? : (String) -> Int? = no_default_port,
) -> UriRef raise NormalizeError {
  let (r, underflow) = normalize_ri(
    self.to_ri(),
    ascii_only=true,
    default_port~,
  )
  if underflow {
    raise NormalizePathUnderflow
  }
  uri_ref_of_ri(r)
}

///|
/// Normalizes the IRI. See [`Uri::normalize`].
pub fn Iri::normalize(
  self : Iri,
  default_port? : (String) -> Int? = no_default_port,
) -> Iri {
  let (r, _) = normalize_ri(self.to_ri(), ascii_only=false, default_port~)
  iri_of_ri(r)
}

///|
/// Normalizes the IRI, rejecting an underflow in path normalization.
/// See [`Uri::normalize_strict`].
pub fn Iri::normalize_strict(
  self : Iri,
  default_port? : (String) -> Int? = no_default_port,
) -> Iri raise NormalizeError {
  let (r, underflow) = normalize_ri(
    self.to_ri(),
    ascii_only=false,
    default_port~,
  )
  if underflow {
    raise NormalizePathUnderflow
  }
  iri_of_ri(r)
}

///|
/// Normalizes the IRI reference. See [`Uri::normalize`].
pub fn IriRef::normalize(
  self : IriRef,
  default_port? : (String) -> Int? = no_default_port,
) -> IriRef {
  let (r, _) = normalize_ri(self.to_ri(), ascii_only=false, default_port~)
  iri_ref_of_ri(r)
}

///|
/// Normalizes the IRI reference, rejecting an underflow in path
/// normalization. See [`Uri::normalize_strict`].
pub fn IriRef::normalize_strict(
  self : IriRef,
  default_port? : (String) -> Int? = no_default_port,
) -> IriRef raise NormalizeError {
  let (r, underflow) = normalize_ri(
    self.to_ri(),
    ascii_only=false,
    default_port~,
  )
  if underflow {
    raise NormalizePathUnderflow
  }
  iri_ref_of_ri(r)
}