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

///|
/// Which component an octet is going into, and so which octets may stand for
/// themselves (RFC 3986 §2.2, §3).
///
/// `&` is ordinary inside a path and a separator inside a query. The grammar
/// answers that, not the caller.
pub(all) enum Kind {
  /// Unreserved only — everything else is escaped. This is what one piece of a
  /// query goes out as, since `&` and `=` inside a value must not read as the
  /// separators they look like.
  Bare
  /// `reg-name` (§3.2.2): unreserved and sub-delims.
  Host
  /// `userinfo` (§3.2.1): `reg-name` and `:`.
  Userinfo
  /// One `segment` (§3.3): `pchar`, so `/` is escaped and stays one segment.
  Segment
  /// A whole `path` (§3.3): `pchar` and `/`.
  Path
  /// `query` (§3.4): `pchar`, `/` and `?`.
  Query
  /// `fragment` (§3.5): the same set as `query`.
  Fragment
  /// A whole URI reference: everything the grammar allows anywhere, `%`
  /// included. What a `Location` header carries — the value is already a URI,
  /// and only the octets no URI may contain need escaping.
  Whole
} derive(Eq, Debug)

///|
pub extend Kind with Eq::{equal, not_equal}

///|
pub extend Kind with Debug::{to_repr}

///|
/// The strictest component, for when nothing says. Escaping an octet that did
/// not need it changes nothing; leaving one that did changes where the
/// component ends.
pub let kind : Kind = Bare

///|
/// A URI reference taken apart (RFC 3986 §3).
///
/// No `scheme` makes it relative (§4.2). No `host` means no authority, which is
/// not an empty host — `file:///tmp` has one, `mailto:x@y` has none.
///
/// Fields stay percent-encoded. Decoding on the way in would lose the difference
/// between a `/` that separates two segments and a `%2F` inside one.
pub(all) struct Url {
  scheme : String?
  userinfo : String?
  host : String?
  port : Int32?
  path : String
  query : String?
  fragment : String?
} derive(Eq, Debug)

///|
pub extend Url with Eq::{equal, not_equal}

///|
pub extend Url with Debug::{to_repr}

///|
/// Why a string is not a URI reference. Only the authority can fail: §B splits
/// any string at all into the five top-level components, and does not split
/// that one.
pub(all) suberror Refused {
  /// A port that is not digits, at this offset.
  Port(at~ : Int32)
  /// An IP-literal with no closing bracket, at the opening one's offset.
  Host(at~ : Int32)
  /// A form-encoded string with more pairs than the limit.
  Pairs(limit~ : Int32)
  /// One form-encoded pair longer than the limit, in octets.
  Pair(limit~ : Int32, got~ : Int32)
} derive(Eq, Debug)

///|
pub extend Refused with Eq::{equal, not_equal}

///|
pub extend Refused with Debug::{to_repr}

// ------------------------------------------------------------ percent coding

///|
/// Percent-encode octets for `kind` (RFC 3986 §2.1).
///
/// Octets, not text: what travels inside a URI is whatever the caller encoded it
/// as. [`escape_text`] is the one that assumes UTF-8.
///
/// `spare` is for percent coding that is not going into a URI at all — gRPC's
/// `grpc-message` keeps all of printable ASCII but `%`, which is no component of
/// RFC 3986's. A URI component names its `kind` and leaves `spare` alone.
pub fn escape(
  raw : BytesView,
  kind~ : Kind = kind,
  spare~ : StringView = ""[:],
) -> String {
  let digits = "0123456789ABCDEF"
  let out = StringBuilder()
  for i = 0; i < raw.length(); i = i + 1 {
    let b = raw[i].to_int()
    if allowed(kind, b) || spared(spare, b) {
      out.write_char(b.unsafe_to_char())
    } else {
      out.write_char('%')
      out.write_char(digits[b / 16].unsafe_to_char())
      out.write_char(digits[b % 16].unsafe_to_char())
    }
  }
  out.to_string()
}

///|
/// Percent-encode text as UTF-8 (RFC 3986 §2.5).
pub fn escape_text(
  text : StringView,
  kind~ : Kind = kind,
  spare~ : StringView = ""[:],
) -> String {
  escape(@utf8.encode(text)[:], kind~, spare~)
}

///|
/// Undo percent-encoding (RFC 3986 §2.1).
///
/// A `%` without two hex digits after it stands for itself. This runs on request
/// targets and form bodies, where refusing the whole message over one stray `%`
/// helps nobody; [`escaped`] is for the caller who would rather be told.
pub fn unescape(raw : BytesView) -> Bytes {
  let out : Array[Byte] = []
  let mut i = 0
  while i < raw.length() {
    if raw[i] == b'%' && i + 2 < raw.length() {
      let hi = nibble(raw[i + 1])
      let lo = nibble(raw[i + 2])
      if hi >= 0 && lo >= 0 {
        out.push(((hi << 4) | lo).to_byte())
        i += 3
        continue
      }
    }
    out.push(raw[i])
    i += 1
  }
  Bytes::from_array(out)
}

///|
/// Undo percent-encoding and read the result as UTF-8, replacing what is not.
pub fn unescape_text(text : StringView) -> String {
  @utf8.decode_lossy(unescape(@utf8.encode(text)[:]))
}

///|
/// Whether every `%` is a complete escape and nothing outside `kind`'s set is
/// there unescaped — the check [`unescape`] does not make.
pub fn escaped(
  text : StringView,
  kind~ : Kind = kind,
  spare~ : StringView = ""[:],
) -> Bool {
  let raw = @utf8.encode(text)
  let mut i = 0
  while i < raw.length() {
    let b = raw[i].to_int()
    if b == 0x25 {
      if i + 2 >= raw.length() ||
        nibble(raw[i + 1]) < 0 ||
        nibble(raw[i + 2]) < 0 {
        return false
      }
      i += 3
    } else {
      if !allowed(kind, b) && !spared(spare, b) {
        return false
      }
      i += 1
    }
  }
  true
}

///|
/// Percent-encoding normalised (RFC 3986 §6.2.2.2): hex digits in upper case,
/// and escapes of unreserved octets written out plainly.
pub fn normalize_escapes(text : StringView) -> String {
  let digits = "0123456789ABCDEF"
  let raw = @utf8.encode(text)
  let out = StringBuilder()
  let mut i = 0
  while i < raw.length() {
    if raw[i] == b'%' && i + 2 < raw.length() {
      let hi = nibble(raw[i + 1])
      let lo = nibble(raw[i + 2])
      if hi >= 0 && lo >= 0 {
        let b = (hi << 4) | lo
        if unreserved(b) {
          out.write_char(b.unsafe_to_char())
        } else {
          out.write_char('%')
          out.write_char(digits[b / 16].unsafe_to_char())
          out.write_char(digits[b % 16].unsafe_to_char())
        }
        i += 3
        continue
      }
    }
    out.write_char(raw[i].to_int().unsafe_to_char())
    i += 1
  }
  out.to_string()
}

// ------------------------------------------------------------------- reading

///|
/// Take a URI reference apart (RFC 3986 §3, by the split §B states).
///
/// Any string splits, so what comes back is a reference even when it is one
/// nobody would write. Only the authority can be malformed.
pub fn Url::decode(text : StringView) -> Url raise Refused {
  let n = text.length()
  let mut i = 0
  let scheme = scheme_of(text)
  match scheme {
    Some(name) => i = name.length() + 1
    None => ()
  }
  let mut userinfo = None
  let mut host = None
  let mut port = None
  if i + 1 < n && text[i].to_int() == 0x2F && text[i + 1].to_int() == 0x2F {
    i += 2
    let start = i
    while i < n && not_in(text[i].to_int(), [0x2F, 0x3F, 0x23]) {
      i += 1
    }
    let (u, h, p) = authority(text[start:i], start)
    userinfo = u
    host = Some(h)
    port = p
  }
  let start = i
  while i < n && not_in(text[i].to_int(), [0x3F, 0x23]) {
    i += 1
  }
  let path = text[start:i].to_owned()
  let mut query = None
  if i < n && text[i].to_int() == 0x3F {
    i += 1
    let start = i
    while i < n && text[i].to_int() != 0x23 {
      i += 1
    }
    query = Some(text[start:i].to_owned())
  }
  let mut fragment = None
  if i < n && text[i].to_int() == 0x23 {
    fragment = Some(text[i + 1:].to_owned())
  }
  { scheme, userinfo, host, port, path, query, fragment, }
}

///|
/// The scheme, if the reference has one (§3.1).
///
/// A colon alone does not make one: `2026:a` has none because a scheme starts
/// with a letter, `a/b:c` because the colon is inside the path. Get this wrong
/// and a relative reference becomes an absolute one with a made-up scheme.
fn scheme_of(text : StringView) -> String? {
  let n = text.length()
  if n == 0 || !alpha(text[0].to_int()) {
    return None
  }
  for i = 1; i < n; i = i + 1 {
    let b = text[i].to_int()
    if b == 0x3A {
      return Some(text[:i].to_owned())
    }
    let ok = alpha(b) || digit(b) || b == 0x2B || b == 0x2D || b == 0x2E
    if !ok {
      return None
    }
  }
  None
}

///|
/// Split an authority into userinfo, host and port (§3.2).
///
/// The last `@` divides, not the first: a userinfo may not hold an unescaped
/// one, so a second `@` is in the host.
///
/// The host is kept without its brackets — those are IP-literal syntax, not
/// address — and [`Url::encode`] puts them back when it sees a colon.
fn authority(
  text : StringView,
  at : Int32,
) -> (String?, String, Int32?) raise Refused {
  let n = text.length()
  let mut cut = -1
  for i = 0; i < n; i = i + 1 {
    if text[i].to_int() == 0x40 {
      cut = i
    }
  }
  let userinfo = if cut >= 0 { Some(text[:cut].to_owned()) } else { None }
  let rest = text[cut + 1:]
  let base = at + cut + 1
  let m = rest.length()
  let mut end = 0
  if m > 0 && rest[0].to_int() == 0x5B {
    let mut close = -1
    for i = 1; i < m; i = i + 1 {
      if rest[i].to_int() == 0x5D {
        close = i
        break
      }
    }
    guard close >= 0 else { raise Host(at=base) }
    end = close + 1
  } else {
    while end < m && rest[end].to_int() != 0x3A {
      end += 1
    }
  }
  let host = if m > 0 && rest[0].to_int() == 0x5B {
    rest[1:end - 1].to_owned()
  } else {
    rest[:end].to_owned()
  }
  if end >= m {
    return (userinfo, host, None)
  }
  guard rest[end].to_int() == 0x3A else { raise Port(at=base + end) }
  let text = rest[end + 1:]
  if text.length() == 0 {
    return (userinfo, host, None)
  }
  let mut value = 0
  for i = 0; i < text.length(); i = i + 1 {
    let b = text[i].to_int()
    guard digit(b) else { raise Port(at=base + end + 1 + i) }
    value = value * 10 + (b - 0x30)
  }
  (userinfo, host, Some(value))
}

// ------------------------------------------------------------------- writing

///|
/// A reference this shape, for building one rather than reading one.
pub fn Url::new(
  scheme? : String,
  userinfo? : String,
  host? : String,
  port? : Int32,
  path~ : String = "",
  query? : String,
  fragment? : String,
) -> Url {
  { scheme, userinfo, host, port, path, query, fragment, }
}

///|
/// Put the reference back together (RFC 3986 §5.3).
pub fn Url::encode(self : Url) -> String {
  let out = StringBuilder()
  match self.scheme {
    Some(scheme) => {
      out.write_string(scheme)
      out.write_char(':')
    }
    None => ()
  }
  match self.authority() {
    Some(authority) => {
      out.write_string("//")
      out.write_string(authority)
    }
    None => ()
  }
  out.write_string(self.path)
  match self.query {
    Some(query) => {
      out.write_char('?')
      out.write_string(query)
    }
    None => ()
  }
  match self.fragment {
    Some(fragment) => {
      out.write_char('#')
      out.write_string(fragment)
    }
    None => ()
  }
  out.to_string()
}

///|
/// The authority as one string, brackets restored around an IP-literal (§3.2).
pub fn Url::authority(self : Url) -> String? {
  match self.host {
    None => None
    Some(host) => {
      let out = StringBuilder()
      match self.userinfo {
        Some(userinfo) => {
          out.write_string(userinfo)
          out.write_char('@')
        }
        None => ()
      }
      if literal(host) {
        out.write_char('[')
        out.write_string(host)
        out.write_char(']')
      } else {
        out.write_string(host)
      }
      match self.port {
        Some(port) => {
          out.write_char(':')
          out.write_string(port.to_string())
        }
        None => ()
      }
      Some(out.to_string())
    }
  }
}

///|
/// A reference with a scheme and no fragment is a URI (§3); anything else is a
/// relative reference or a same-document one.
pub fn Url::is_absolute(self : Url) -> Bool {
  self.scheme is Some(_) && self.fragment is None
}

// ----------------------------------------------------------------- resolving

///|
/// Resolve `reference` against this base (RFC 3986 §5.2.2, strict).
///
/// Strict: a reference that repeats the base's scheme stays absolute. The
/// non-strict reading is for pre-1998 parsers and turns `http:g` into something
/// it does not say.
pub fn Url::resolve(self : Url, reference : Url) -> Url {
  if reference.scheme is Some(_) {
    return { ..reference, path: remove_dots(reference.path), }
  }
  if reference.host is Some(_) {
    return {
      ..reference,
      scheme: self.scheme,
      path: remove_dots(reference.path),
    }
  }
  if reference.path == "" {
    let query = match reference.query {
      Some(_) => reference.query
      None => self.query
    }
    return { ..self, query, fragment: reference.fragment, }
  }
  let path = if reference.path.length() > 0 &&
    reference.path[0].to_int() == 0x2F {
    remove_dots(reference.path)
  } else {
    remove_dots(merge(self, reference.path))
  }
  {
    scheme: self.scheme,
    userinfo: self.userinfo,
    host: self.host,
    port: self.port,
    path,
    query: reference.query,
    fragment: reference.fragment,
  }
}

///|
/// Graft a relative path onto the base's (§5.2.3).
fn merge(base : Url, path : String) -> String {
  if base.host is Some(_) && base.path == "" {
    return "/" + path
  }
  let mut cut = -1
  for i = 0; i < base.path.length(); i = i + 1 {
    if base.path[i].to_int() == 0x2F {
      cut = i
    }
  }
  if cut < 0 {
    path
  } else {
    base.path[:cut + 1].to_owned() + path
  }
}

///|
/// Take out the `.` and `..` segments (RFC 3986 §5.2.4).
///
/// The section's loop rather than a split-and-rejoin: the two differ at exactly
/// the inputs that matter — a trailing `..`, a path that climbs above its own
/// root, `/..//`.
pub fn remove_dots(path : String) -> String {
  let out : Array[String] = []
  let mut rest = path[:]
  while rest.length() > 0 {
    if starts(rest, "../") {
      rest = rest[3:]
    } else if starts(rest, "./") {
      rest = rest[2:]
    } else if starts(rest, "/./") {
      rest = rest[2:]
    } else if rest == "/." {
      rest = "/"[:]
    } else if starts(rest, "/../") {
      rest = rest[3:]
      let _ = out.pop()
    } else if rest == "/.." {
      rest = "/"[:]
      let _ = out.pop()
    } else if rest == "." || rest == ".." {
      rest = ""[:]
    } else {
      let mut i = if rest[0].to_int() == 0x2F { 1 } else { 0 }
      while i < rest.length() && rest[i].to_int() != 0x2F {
        i += 1
      }
      out.push(rest[:i].to_owned())
      rest = rest[i:]
    }
  }
  let joined = StringBuilder()
  for segment in out {
    joined.write_string(segment)
  }
  joined.to_string()
}

///|
/// Syntax-based normalisation (RFC 3986 §6.2.2): scheme and host to lower case,
/// escapes normalised, dot segments out.
///
/// Not §6.2.3: dropping `:80` is true of `http` and false in general, and this
/// package holds no register of schemes.
pub fn Url::normalize(self : Url) -> Url {
  {
    scheme: match self.scheme {
      Some(scheme) => Some(lower(scheme))
      None => None
    },
    userinfo: match self.userinfo {
      Some(userinfo) => Some(normalize_escapes(userinfo))
      None => None
    },
    host: match self.host {
      Some(host) => Some(lower(normalize_escapes(host)))
      None => None
    },
    port: self.port,
    path: remove_dots(normalize_escapes(self.path)),
    query: match self.query {
      Some(query) => Some(normalize_escapes(query))
      None => None
    },
    fragment: match self.fragment {
      Some(fragment) => Some(normalize_escapes(fragment))
      None => None
    },
  }
}

// ------------------------------------------------------------------ plumbing

///|
fn allowed(kind : Kind, b : Int32) -> Bool {
  if unreserved(b) {
    return true
  }
  match kind {
    Bare => false
    Host => sub_delim(b)
    Userinfo => sub_delim(b) || b == 0x3A
    Segment => sub_delim(b) || b == 0x3A || b == 0x40
    Path => sub_delim(b) || b == 0x3A || b == 0x40 || b == 0x2F
    Query | Fragment =>
      sub_delim(b) || b == 0x3A || b == 0x40 || b == 0x2F || b == 0x3F
    Whole => sub_delim(b) || gen_delim(b) || b == 0x25
  }
}

///|
fn spared(spare : StringView, b : Int32) -> Bool {
  if b >= 0x80 {
    return false
  }
  for c in spare {
    if c.to_int() == b {
      return true
    }
  }
  false
}

///|
fn alpha(b : Int32) -> Bool {
  (b >= 0x41 && b <= 0x5A) || (b >= 0x61 && b <= 0x7A)
}

///|
fn digit(b : Int32) -> Bool {
  b >= 0x30 && b <= 0x39
}

///|
fn unreserved(b : Int32) -> Bool {
  alpha(b) || digit(b) || b == 0x2D || b == 0x2E || b == 0x5F || b == 0x7E
}

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

///|
fn gen_delim(b : Int32) -> Bool {
  b == 0x3A ||
  b == 0x2F ||
  b == 0x3F ||
  b == 0x23 ||
  b == 0x5B ||
  b == 0x5D ||
  b == 0x40
}

///|
fn nibble(b : Byte) -> Int32 {
  let c = b.to_int()
  if c >= 0x30 && c <= 0x39 {
    c - 0x30
  } else if c >= 0x41 && c <= 0x46 {
    c - 0x37
  } else if c >= 0x61 && c <= 0x66 {
    c - 0x57
  } else {
    -1
  }
}

///|
fn not_in(b : Int32, set : Array[Int32]) -> Bool {
  for c in set {
    if b == c {
      return false
    }
  }
  true
}

///|
fn starts(text : StringView, prefix : String) -> Bool {
  if text.length() < prefix.length() {
    return false
  }
  for i = 0; i < prefix.length(); i = i + 1 {
    if text[i].to_int() != prefix[i].to_int() {
      return false
    }
  }
  true
}

///|
/// A colon in a host can only be an IP-literal; `reg-name` has none.
fn literal(host : String) -> Bool {
  for i = 0; i < host.length(); i = i + 1 {
    if host[i].to_int() == 0x3A {
      return true
    }
  }
  false
}

///|
fn lower(text : String) -> String {
  let out = StringBuilder()
  for i = 0; i < text.length(); i = i + 1 {
    let b = text[i].to_int()
    if b >= 0x41 && b <= 0x5A {
      out.write_char((b + 32).unsafe_to_char())
    } else {
      out.write_char(b.unsafe_to_char())
    }
  }
  out.to_string()
}