///|
/// Classify Atom content's declared type without conflating the Atom `html`
/// token with the distinct MIME type `text/html`.
fn atom_content_mode(declared : String?) -> String {
  match declared {
    None | Some("text") => "text"
    Some("html") => "html"
    Some("xhtml") => "xhtml"
    Some(value) => {
      guard atom_mime_essence(value) is Some(essence) else { return "invalid" }
      match essence.split_once("/") {
        Some((major, minor)) if atom_mime_token(major.to_owned()) &&
          atom_mime_token(minor.to_owned()) &&
          major != "multipart" &&
          major != "message" =>
          if essence.has_suffix("+xml") || essence.has_suffix("/xml") {
            "xml"
          } else if essence.has_prefix("text/") {
            "text-mime"
          } else {
            "binary"
          }
        _ => "invalid"
      }
    }
  }
}

///|
/// Validate the MIME type and its common parameter forms before dispatching
/// on the essence. Keep the original declaration on the public Atom body.
fn atom_mime_essence(value : String) -> String? {
  let (head, tail) = match value.split_once(";") {
    Some((head, tail)) => (head.to_owned(), Some(tail.to_owned()))
    None => (value, None)
  }
  let essence = head.trim().to_owned().to_lower()
  guard essence.split_once("/") is Some((major, minor)) &&
    atom_mime_token(major.to_owned()) &&
    atom_mime_token(minor.to_owned()) else {
    return None
  }
  guard tail is Some(parameters) else { return Some(essence) }
  let chars = parameters.to_array()
  let mut index = 0
  while index < chars.length() {
    while index < chars.length() &&
          (chars[index] == ' ' || chars[index] == '\t') {
      index = index + 1
    }
    let start = index
    while index < chars.length() && atom_mime_token_char(chars[index]) {
      index = index + 1
    }
    if index == start {
      return None
    }
    while index < chars.length() &&
          (chars[index] == ' ' || chars[index] == '\t') {
      index = index + 1
    }
    if index == chars.length() || chars[index] != '=' {
      return None
    }
    index = index + 1
    while index < chars.length() &&
          (chars[index] == ' ' || chars[index] == '\t') {
      index = index + 1
    }
    if index < chars.length() && chars[index] == '"' {
      index = index + 1
      let mut closed = false
      while index < chars.length() {
        if chars[index] == '"' {
          index = index + 1
          closed = true
          break
        }
        if chars[index] == '\\' {
          index = index + 1
          if index == chars.length() ||
            chars[index].to_int() < 32 ||
            chars[index].to_int() > 126 {
            return None
          }
        } else if chars[index].to_int() < 32 || chars[index].to_int() > 126 {
          return None
        }
        index = index + 1
      }
      if !closed {
        return None
      }
    } else {
      let start = index
      while index < chars.length() && atom_mime_token_char(chars[index]) {
        index = index + 1
      }
      if index == start {
        return None
      }
    }
    while index < chars.length() &&
          (chars[index] == ' ' || chars[index] == '\t') {
      index = index + 1
    }
    if index == chars.length() {
      return Some(essence)
    }
    if chars[index] != ';' {
      return None
    }
    index = index + 1
  }
  None // A trailing semicolon has no parameter.
}

///|
fn atom_mime_token(value : String) -> Bool {
  if value.is_empty() {
    return false
  }
  for c in value.to_array() {
    if !atom_mime_token_char(c) {
      return false
    }
  }
  true
}

///|
fn atom_mime_token_char(c : Char) -> Bool {
  match c {
    'a'..<='z'
    | 'A'..<='Z'
    | '0'..<='9'
    | '!'
    | '#'
    | '$'
    | '%'
    | '&'
    | '\''
    | '*'
    | '+'
    | '-'
    | '.'
    | '^'
    | '_'
    | '`'
    | '|'
    | '~' => true
    _ => false
  }
}

///|
fn atom_content_media_type(declared : String?) -> String {
  match declared {
    Some(value) if value != "text" => value
    _ => "text/plain"
  }
}

///|
fn atom_base_from_references(references : Array[String]) -> String? {
  let mut base : String? = None
  for reference in references {
    base = match base {
      Some(parent) => uri_resolve(parent, reference)
      None => uri_absolute(reference)
    }
  }
  base
}

///|
fn atom_external_content(
  element : @xml.NamespaceElement,
  base_references : Array[String],
) -> AtomExternalContent? {
  guard atom_attribute_value(element, "src") is Some(src) else { return None }
  guard src.is_empty() || !src.trim().is_empty() else { return None }
  guard atom_href_syntax(src) else { return None }
  let references = xhtml_copy_references(base_references)
  match xml_base_attribute(element) {
    Some(reference) => references.push(reference)
    None => ()
  }
  Some({
    src,
    url: src,
    media_type: atom_attribute_value(element, "type"),
    base_uri: atom_base_from_references(references),
  })
}

///|
/// The same deterministic XML serializer used for XHTML is applied to all
/// XML media bodies, but there is deliberately no XHTML div removal.
priv struct AtomXmlCapture {
  mut markup : String
  language : String?
  base_references : Array[String]
}

///|
fn AtomXmlCapture::new(
  language : String?,
  base_references : Array[String],
) -> AtomXmlCapture {
  { markup: "", language, base_references, }
}

///|
fn AtomXmlCapture::node(
  self : AtomXmlCapture,
  element : @xml.NamespaceElement,
  empty : Bool,
) -> Unit {
  self.markup = self.markup + xhtml_start_tag(element, empty)
}

///|
fn AtomXmlCapture::text(self : AtomXmlCapture, value : String) -> Unit {
  self.markup = self.markup + @xml.escape(value)
}

///|
fn AtomXmlCapture::end(self : AtomXmlCapture, name : @xml.XmlName) -> Unit {
  self.markup = self.markup + xhtml_end_tag(name)
}

///|
fn AtomXmlCapture::comment(self : AtomXmlCapture, value : String) -> Unit {
  self.markup = self.markup + ""
}

///|
fn AtomXmlCapture::processing_instruction(
  self : AtomXmlCapture,
  target : String,
  data : String,
) -> Unit {
  self.markup = self.markup +
    ""
}

///|
fn AtomXmlCapture::fragment(self : AtomXmlCapture) -> XmlFragment {
  {
    markup: self.markup,
    language: self.language,
    base_references: self.base_references,
  }
}

///|
fn atom_decode_base64(value : String) -> Bytes? {
  // RFC 4287 permits surrounding XML whitespace and LF-separated lines.
  // Padding is required; interior spaces and tabs are not line separators.
  let trimmed = value.trim()
  let mut encoded_length = 0
  let mut previous_newline = false
  for c in trimmed.to_array() {
    match c {
      '\n' => {
        if previous_newline {
          return None
        }
        previous_newline = true
      }
      ' ' | '\t' | '\r' => return None
      _ => {
        encoded_length = encoded_length + 1
        previous_newline = false
      }
    }
  }
  if encoded_length % 4 != 0 {
    return None
  }
  Some(@base64.decode(trimmed.view(), ignore_whitespace=true)) catch {
    _ => None
  }
}