///|
fn join_encoded(parts : Array[String]) -> String {
  let out = StringBuilder()
  for i = 0; i < parts.length(); i = i + 1 {
    if i > 0 {
      out.write_char('/')
    }
    out.write_string(encode_component(parts[i]))
  }
  out.to_string()
}

///|
/// Return canonical spelling for the supported profile: lowercase scheme/type,
/// normalized type-specific name, sorted lowercase qualifier keys and uppercase
/// percent triplets for ASCII reserved data.
pub fn PackageUrl::to_string(self : PackageUrl) -> String {
  let out = StringBuilder()
  out.write_string("pkg:" + ascii_lower(self.package_type) + "/")
  if self.namespace_parts.length() > 0 {
    out.write_string(join_encoded(self.namespace_parts))
    out.write_char('/')
  }
  out.write_string(encode_component(self.name))
  match self.version {
    Some(version) => {
      out.write_char('@')
      out.write_string(encode_component(version))
    }
    None => ()
  }
  if self.qualifiers.length() > 0 {
    out.write_char('?')
    let qualifiers = sort_qualifiers(self.qualifiers)
    for i = 0; i < qualifiers.length(); i = i + 1 {
      if i > 0 {
        out.write_char('&')
      }
      out.write_string(ascii_lower(qualifiers[i].key))
      out.write_char('=')
      out.write_string(encode_component(qualifiers[i].value, allow_colon=true))
    }
  }
  if self.subpath.length() > 0 {
    out.write_char('#')
    out.write_string(join_encoded(self.subpath))
  }
  out.to_string()
}

///|
pub fn canonicalize(input : String) -> String raise PurlError {
  parse(input).to_string()
}

///|
/// Identity used for vulnerability joins and deduplication. Qualifiers and
/// subpath are intentionally excluded; the result retains type, namespace,
/// name and version. This is not a statement that two artifacts are identical.
pub fn PackageUrl::identity(self : PackageUrl) -> String {
  { ..self, qualifiers: [], subpath: [], }.to_string()
}

///|
pub fn PackageUrl::qualifier(self : PackageUrl, key : String) -> String? {
  let expected = ascii_lower(key)
  for item in self.qualifiers {
    if item.key == expected {
      return Some(item.value)
    }
  }
  None
}