///|
/// Namespace-aware XML node with source location and ordered children.
pub struct Node {
  name : String
  uri : String
  attributes : Map[String, String]
  children : Array[Node]
  mut text : String
  line : Int32
  column : Int32
}

///|
/// Return an attribute value, or the empty string if absent.
pub fn Node::attr(n : Node, key : String) -> String {
  n.attributes.get(key).unwrap_or("")
}

///|
/// Return direct children matching this node namespace and local name.
pub fn Node::all(n : Node, name : String) -> Array[Node] {
  n.children.filter(x => x.name == name && x.uri == n.uri)
}

///|
/// Return the first direct child matching namespace and local name.
pub fn Node::child(n : Node, name : String) -> Node? {
  n.all(name).get(0)
}

///|
/// Return the text of the first matching direct child, or the empty string.
pub fn Node::content(n : Node, name : String) -> String {
  match n.child(name) {
    Some(v) => v.text.trim().to_owned()
    None => ""
  }
}

///|
fn node(e : @upstream.NamespaceElement, line : Int32, column : Int32) -> Node {
  let attributes = Map([])
  for a in e.attributes {
    if a.name.namespace_uri is None {
      attributes[a.name.local_name] = a.value
    } else {
      attributes[a.name.qualified_name] = a.value
    }
  }
  {
    name: e.name.local_name,
    uri: e.name.namespace_uri.unwrap_or(""),
    attributes,
    children: [],
    text: "",
    line,
    column,
  }
}

///|
/// Parse namespace-aware XML, rejecting DTDs and excessive input or nesting.
pub fn parse(source : String) -> Node raise @types.EngineError {
  if source.length() > 10000000 {
    raise @types.fail("E_LIMIT", "XML exceeds 10MB")
  }
  let reader = @upstream.NamespaceReader::from_string(source)
  let stack : Array[Node] = []
  let mut root : Node? = None
  while true {
    let ev = reader.read_event() catch {
      @upstream.At(error~, span~) =>
        raise @types.Failure(
          "E_XML",
          span.start.line,
          span.start.column,
          error.to_string(),
        )
    }
    match ev.kind {
      Start(e) | Empty(e) => {
        let n = node(e, ev.span.start.line, ev.span.start.column)
        if stack.length() == 0 {
          root = Some(n)
        } else {
          stack[stack.length() - 1].children.push(n)
        }
        if ev.kind is Start(_) {
          stack.push(n)
        }
        if stack.length() > 128 {
          raise @types.fail("E_LIMIT", "XML nesting exceeds 128")
        }
      }
      End(_) => ignore(stack.pop())
      Text(t) | CData(t) =>
        if stack.length() > 0 {
          let n = stack[stack.length() - 1]
          n.text = n.text + t
        }
      DocType(_) =>
        raise @types.Failure(
          "E_XML_DTD",
          ev.span.start.line,
          ev.span.start.column,
          "DTD forbidden",
        )
      Eof => break
      _ => ()
    }
  }
  match root {
    Some(n) => n
    None => raise @types.fail("E_XML", "empty document")
  }
}