///|
/// A parsed XML document: its single root element.
///
/// The declaration, processing instructions, DOCTYPE and comments are not
/// represented because the underlying reader skips them.
pub struct Document {
  /// The document's root element.
  root : Element
} derive(Debug, Eq)

///|
/// An element node: a qualified tag name, its attributes and its children
/// in document order.
pub(all) struct Element {
  /// Raw qualified tag name, e.g. `"itunes:author"` or `"title"`.
  name : String
  /// Attributes in document order; values are entity-decoded.
  attrs : Array[Attribute]
  /// Child nodes in document order.
  children : Array[Node]
} derive(Debug, Eq)

///|
/// A child node of an element.
pub(all) enum Node {
  /// Character data with entities decoded; adjacent character data is
  /// merged into a single node by the builder.
  Text(String)
  /// The verbatim contents of a `` section.
  CData(String)
  /// A child element.
  Element(Element)
} derive(Debug, Eq)

///|
/// Create an element.
pub fn Element::new(
  name~ : String,
  attrs? : Array[Attribute] = [],
  children? : Array[Node] = [],
) -> Element {
  { name, attrs, children }
}

///|
/// The local part of the qualified tag name:
/// `"itunes:author"` → `"author"`.
pub fn Element::local_name(self : Element) -> String {
  local_name(self.name)
}

///|
/// The value of the attribute named `key`, or `None` when absent.
pub fn Element::attr(self : Element, key : String) -> String? {
  for a in self.attrs {
    if a.key == key {
      return Some(a.value)
    }
  }
  None
}

///|
/// The direct child elements, in document order.
pub fn Element::elements(self : Element) -> Array[Element] {
  let out : Array[Element] = []
  for child in self.children {
    match child {
      Element(el) => out.push(el)
      _ => ()
    }
  }
  out
}

///|
/// The first direct child element whose raw tag name equals `name`,
/// or `None` when absent. Use [`Element::elements`] and
/// [`Element::local_name`] for prefix-insensitive lookups.
pub fn Element::element(self : Element, name : String) -> Element? {
  for el in self.elements() {
    if el.name == name {
      return Some(el)
    }
  }
  None
}

///|
/// The concatenated character data of all descendant `Text` and `CData`
/// nodes, like DOM's `textContent`.
pub fn Element::text_content(self : Element) -> String {
  let sb = StringBuilder()
  fn collect(node : Node) {
    match node {
      Text(s) => sb.write_string(s)
      CData(s) => sb.write_string(s)
      Element(el) =>
        for child in el.children {
          collect(child)
        }
    }
  }

  for child in self.children {
    collect(child)
  }
  sb.to_string()
}