///|
/// Parse a complete XML document into a [`Document`] tree.
///
/// The input must contain exactly one root element; character data is only
/// allowed inside it (top-level whitespace is ignored). A mismatched or
/// unexpected tag raises [`XmlError::Syntax`].
///
/// # Example
/// ```mbt check
/// test {
///   let doc = @xml.Document::read_from("hi")
///   inspect(doc.root.name, content="a")
/// }
/// ```
pub fn Document::read_from(input : String) -> Document raise XmlError {
  let reader = Reader::new(input)
  let stack : Array[Element] = []
  let mut root : Element? = None
  for ;; {
    match reader.next() {
      Eof => break
      Start(name~, attrs~) => {
        if stack.length() == 0 && root is Some(_) {
          raise Syntax(
            pos=reader.position(),
            msg="unexpected second root element '\{name}'",
          )
        }
        stack.push({ name, attrs, children: [] })
      }
      Empty(name~, attrs~) =>
        if stack.length() == 0 {
          guard root is None else {
            raise Syntax(
              pos=reader.position(),
              msg="unexpected second root element '\{name}'",
            )
          }
          root = Some({ name, attrs, children: [] })
        } else {
          stack[stack.length() - 1].children.push(
            Element({ name, attrs, children: [] }),
          )
        }
      End(name) => {
        guard stack.length() > 0 else {
          raise Syntax(
            pos=reader.position(),
            msg="unexpected end tag ",
          )
        }
        let el = stack.pop().unwrap()
        if el.name != name {
          raise Syntax(
            pos=reader.position(),
            msg="end tag  does not match start tag <\{el.name}>",
          )
        }
        if stack.length() == 0 {
          guard root is None else {
            raise Syntax(
              pos=reader.position(),
              msg="unexpected second root element '\{name}'",
            )
          }
          root = Some(el)
        } else {
          stack[stack.length() - 1].children.push(Element(el))
        }
      }
      Text(s) =>
        if stack.length() == 0 {
          guard is_all_ws(s) else {
            raise Syntax(
              pos=reader.position(),
              msg="character data outside the root element",
            )
          }
        } else {
          append_text(stack[stack.length() - 1].children, s)
        }
      CData(s) => {
        guard stack.length() > 0 else {
          raise Syntax(
            pos=reader.position(),
            msg="CDATA section outside the root element",
          )
        }
        stack[stack.length() - 1].children.push(CData(s))
      }
      // The reader currently skips comments; kept for exhaustiveness.
      Comment(_) => ()
    }
  }
  guard root is Some(doc_root) else {
    raise Syntax(pos=0, msg="no root element found")
  }
  { root: doc_root }
}

///|
/// Append character data, merging it with an adjacent `Text` node the way
/// common DOM implementations normalize text.
fn append_text(nodes : Array[Node], s : String) -> Unit {
  let last = nodes.length() - 1
  match nodes.get(last) {
    Some(Text(prev)) => nodes[last] = Text(prev + s)
    _ => nodes.push(Text(s))
  }
}

///|
fn is_all_ws(s : String) -> Bool {
  for c in s {
    if !is_ws(c.to_int()) {
      return false
    }
  }
  true
}