///|
pub(all) struct PrinterUri {
  scheme : String
  host : String
  port : Int
  path : String
  ipv6 : Bool
} derive(Eq, Debug)

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

///|
pub extend PrinterUri with @debug.Debug::{to_repr}

///|
fn parse_decimal(text : String, maximum : Int) -> Int raise IppError {
  if text.is_empty() {
    raise Invalid("empty integer")
  }
  let mut value = 0
  for char in text.to_array() {
    let digit = char.to_int() - 48
    if digit < 0 || digit > 9 {
      raise Invalid("invalid integer")
    }
    if value > (maximum - digit) / 10 {
      raise Invalid("integer exceeds maximum \{maximum}")
    }
    value = value * 10 + digit
  }
  value
}

///|
fn string_slice(text : String, start : Int, end : Int) -> String {
  text.exact_view(start~, end~).to_owned()
}

///|
pub fn PrinterUri::parse(text : String) -> PrinterUri raise IppError {
  if text.length() > 1023 {
    raise Invalid("printer URI exceeds 1023 characters")
  }
  for char in text.to_array() {
    if char.to_int() <= 32 || char.to_int() >= 127 {
      raise Invalid("URI requires escaped ASCII characters")
    }
  }
  let (scheme_view, remainder_view) = match text.split_once("://") {
    Some(parts) => parts
    None => raise Invalid("URI must include scheme and authority")
  }
  let scheme = scheme_view.to_owned()
  if scheme != "ipp" && scheme != "ipps" {
    raise Invalid("printer URI scheme must be ipp or ipps")
  }
  let remainder = remainder_view.to_owned()
  let slash = match remainder.find("/") {
    Some(index) => index
    None => remainder.length()
  }
  let authority = string_slice(remainder, 0, slash)
  let path = if slash == remainder.length() {
    "/"
  } else {
    string_slice(remainder, slash, remainder.length())
  }
  if authority.is_empty() || authority.contains("@") {
    raise Invalid("empty host or URI userinfo")
  }
  if path.contains("#") || path.contains("?") || path.contains("\\") {
    raise Invalid(
      "printer URI path must not contain query, fragment or backslash",
    )
  }
  let mut host = ""
  let mut port = 631
  let ipv6 = authority.has_prefix("[")
  if ipv6 {
    let closing = match authority.find("]") {
      Some(index) => index
      None => raise Invalid("unterminated IPv6 host")
    }
    host = string_slice(authority, 1, closing)
    if !host.contains(":") || host.is_empty() {
      raise Invalid("invalid IPv6 host")
    }
    let suffix = string_slice(authority, closing + 1, authority.length())
    if suffix != "" {
      if !suffix.has_prefix(":") {
        raise Invalid("unexpected data after IPv6 host")
      }
      port = parse_decimal(string_slice(suffix, 1, suffix.length()), 65535)
    }
  } else {
    match authority.split_once(":") {
      Some((hostname, port_text)) => {
        host = hostname.to_owned()
        port = parse_decimal(port_text.to_owned(), 65535)
      }
      None => host = authority
    }
    for char in host.to_array() {
      let n = char.to_int()
      if !((n >= 48 && n <= 57) ||
        (n >= 65 && n <= 90) ||
        (n >= 97 && n <= 122) ||
        n == 45 ||
        n == 46) {
        raise Invalid("invalid hostname")
      }
    }
  }
  if host.is_empty() || port < 1 {
    raise Invalid("host and port must not be empty")
  }
  let mut i = 0
  while i < path.length() {
    if path[i].to_int() == 37 {
      if i + 2 >= path.length() ||
        hex_nibble(path[i + 1]) is None ||
        hex_nibble(path[i + 2]) is None {
        raise Invalid("invalid percent escape")
      }
      i += 3
    } else {
      i += 1
    }
  }
  { scheme, host, port, path, ipv6, }
}

///|
pub fn PrinterUri::authority(self : PrinterUri) -> String {
  let host = if self.ipv6 { "[\{self.host}]" } else { self.host }
  "\{host}:\{self.port}"
}

///|
pub fn PrinterUri::text(self : PrinterUri) -> String {
  "\{self.scheme}://\{self.authority()}\{self.path}"
}

///|
pub fn PrinterUri::http_url(self : PrinterUri) -> String {
  let scheme = if self.scheme == "ipps" { "https" } else { "http" }
  "\{scheme}://\{self.authority()}\{self.path}"
}

///|
pub fn PrinterUri::secure(self : PrinterUri) -> Bool {
  self.scheme == "ipps"
}