// Package `oilleelssq-wq/moonxslt/xml` provides the namespace-aware XML data
// model that the XPath and XSLT engines operate on.
//
// The model is arena based: every node lives in `Document.nodes` and is
// addressed by its integer id. Node ids are assigned in document order, which
// makes document-order comparisons cheap and keeps the tree free of parent
// cycles for garbage collection.

///|
/// The six node kinds of the XPath 1.0 data model.
///
/// `Namespace` nodes are not materialised: the namespace axis is derived from
/// the declarations stored on element nodes.
pub(all) enum NodeKind {
  Document
  Element
  Attribute
  Text
  Comment
  ProcessingInstruction
}

///|
/// A namespace-aware qualified name.
///
/// `uri` is the empty string when the name has no namespace.
pub struct QName {
  prefix : String
  local_part : String
  uri : String
}

///|
pub fn QName::new(prefix : String, local_part : String, uri : String) -> QName {
  { prefix, local_part, uri, }
}

///|
pub fn QName::prefix(self : QName) -> String {
  self.prefix
}

///|
pub fn QName::local_part(self : QName) -> String {
  self.local_part
}

///|
pub fn QName::uri(self : QName) -> String {
  self.uri
}

///|
/// The expanded name, used for matching in XPath and XSLT.
pub fn QName::expanded(self : QName) -> String {
  "{" + self.uri + "}" + self.local_part
}

///|
/// The lexical form of the name, i.e. `prefix:local_part` or `local_part`.
pub fn QName::qualified(self : QName) -> String {
  if self.prefix == "" {
    self.local_part
  } else {
    self.prefix + ":" + self.local_part
  }
}

///|
/// Split a lexical name into `(prefix, local_part)`.
///
/// Returns `None` when the name contains more than one colon, which XML does
/// not allow.
pub fn split_qname(name : String) -> (String, String)? {
  let chars = name.to_array()
  let mut colon = -1
  for i in 0..= 0 {
        return None
      }
      colon = i
    }
  }
  if colon < 0 {
    return Some(("", name))
  }
  let prefix = String::from_array(chars[0:colon])
  let local_part = String::from_array(chars[colon + 1:])
  if prefix == "" || local_part == "" {
    return None
  }
  Some((prefix, local_part))
}

///|
/// A single node of the document arena.
///
/// Only `name`, `value` and `parent` are ever reassigned after creation, so the
/// remaining fields are immutable.
struct Node {
  kind : NodeKind
  mut name : QName?
  mut value : String
  attrs : Array[Int32]
  mut children : Array[Int32]
  parent : Int32
  ns_decls : Array[(String, String)]
  escape : Bool
  order : Int32
}

///|
/// A parsed XML document.
pub struct Document {
  nodes : Array[Node]
  mut root_id : Int32?
}

///|
/// Create an empty document containing only the document node.
pub fn Document::empty() -> Document {
  let document_node : Node = {
    kind: Document,
    name: None,
    value: "",
    attrs: [],
    children: [],
    parent: -1,
    ns_decls: [],
    escape: true,
    order: 0,
  }
  { nodes: [document_node], root_id: None, }
}

///|
/// Number of nodes in the arena, including the document node.
pub fn Document::node_count(self : Document) -> Int32 {
  self.nodes.length()
}

///|
/// Id of the document element, if the document has one.
pub fn Document::root(self : Document) -> Int32? {
  self.root_id
}

///|
/// Kind of the node with the given id.
///
/// `None` means the id is out of range.
pub fn Document::kind(self : Document, id : Int32) -> NodeKind? {
  if id < 0 || id >= self.nodes.length() {
    None
  } else {
    Some(self.nodes[id].kind)
  }
}

///|
/// Parent id of a node, or `None` for the document node.
pub fn Document::parent(self : Document, id : Int32) -> Int32? {
  if id <= 0 || id >= self.nodes.length() {
    None
  } else {
    let parent = self.nodes[id].parent
    if parent < 0 {
      None
    } else {
      Some(parent)
    }
  }
}

///|
/// Child node ids of a document or element node, in document order.
pub fn Document::children(self : Document, id : Int32) -> ArrayView[Int32] {
  if id < 0 || id >= self.nodes.length() {
    []
  } else {
    self.nodes[id].children[:]
  }
}

///|
/// Whether the node has children.
pub fn Document::has_children(self : Document, id : Int32) -> Bool {
  self.children(id).length() > 0
}

///|
/// Attribute node ids of an element, in declaration order.
pub fn Document::attributes(self : Document, id : Int32) -> ArrayView[Int32] {
  if id < 0 || id >= self.nodes.length() {
    []
  } else {
    self.nodes[id].attrs[:]
  }
}

///|
/// The expanded name of an element or attribute node.
pub fn Document::name(self : Document, id : Int32) -> QName? {
  if id < 0 || id >= self.nodes.length() {
    None
  } else {
    self.nodes[id].name
  }
}

///|
/// The lexical name of an element or attribute node.
pub fn Document::qualified_name(self : Document, id : Int32) -> String {
  match self.name(id) {
    Some(name) => name.qualified()
    None => ""
  }
}

///|
/// The local_part part of an element or attribute name.
pub fn Document::local_name(self : Document, id : Int32) -> String {
  match self.name(id) {
    Some(name) => name.local_part()
    None => ""
  }
}

///|
/// The namespace URI of an element or attribute name, `""` when unqualified.
pub fn Document::namespace_uri(self : Document, id : Int32) -> String {
  match self.name(id) {
    Some(name) => name.uri()
    None => ""
  }
}

///|
/// Character data of a text, comment or processing-instruction node.
pub fn Document::value(self : Document, id : Int32) -> String {
  if id < 0 || id >= self.nodes.length() {
    ""
  } else {
    self.nodes[id].value
  }
}

///|
/// Namespace declarations attached to an element, as `(prefix, uri)` pairs.
///
/// The empty prefix denotes the default namespace.
pub fn Document::namespace_declarations(
  self : Document,
  id : Int32,
) -> ArrayView[(String, String)] {
  if id < 0 || id >= self.nodes.length() {
    []
  } else {
    self.nodes[id].ns_decls[:]
  }
}

///|
/// Document-order key of a node.
///
/// Nodes are numbered in document order, except that attributes reuse the
/// order key of their owning element so that a sort on `(order, id)` yields
/// element, then its attributes, then its children.
pub fn Document::document_order(self : Document, id : Int32) -> Int32 {
  if id < 0 || id >= self.nodes.length() {
    -1
  } else {
    self.nodes[id].order
  }
}

///|
/// Compare two node ids in document order.
///
/// Returns a negative number when `a` precedes `b`, zero when they are the
/// same node, and a positive number otherwise.
pub fn Document::compare_order(self : Document, a : Int32, b : Int32) -> Int32 {
  if a == b {
    return 0
  }
  let order_a = self.document_order(a)
  let order_b = self.document_order(b)
  if order_a != order_b {
    order_a - order_b
  } else {
    a - b
  }
}

///|
/// Look up an attribute by local_part name, ignoring the namespace.
pub fn Document::attribute_by_local(
  self : Document,
  element : Int32,
  local_part : String,
) -> Int32? {
  for attr in self.attributes(element) {
    if self.local_name(attr) == local_part {
      return Some(attr)
    }
  }
  None
}

///|
/// Look up an attribute by expanded `(uri, local_part)` name.
pub fn Document::attribute_by_expanded(
  self : Document,
  element : Int32,
  uri : String,
  local_part : String,
) -> Int32? {
  for attr in self.attributes(element) {
    if self.local_name(attr) == local_part && self.namespace_uri(attr) == uri {
      return Some(attr)
    }
  }
  None
}

///|
/// Look up an attribute value by local_part name.
pub fn Document::attribute_value(
  self : Document,
  element : Int32,
  local_part : String,
) -> String? {
  match self.attribute_by_local(element, local_part) {
    Some(attr) => Some(self.value(attr))
    None => None
  }
}

///|
/// Concatenated character data of the node and all its descendants, which is
/// the XPath `string()` of an element.
pub fn Document::text_content(self : Document, id : Int32) -> String {
  let sb = StringBuilder::StringBuilder()
  self.collect_text(id, sb)
  sb.to_string()
}

///|
fn Document::collect_text(
  self : Document,
  id : Int32,
  sb : StringBuilder,
) -> Unit {
  match self.kind(id) {
    Some(Text) => sb.write_string(self.value(id))
    Some(Comment) | Some(ProcessingInstruction) => ()
    _ =>
      for child in self.children(id) {
        self.collect_text(child, sb)
      }
  }
}

///|
/// Whether a text node consists only of XML whitespace.
pub fn Document::is_whitespace_text(self : Document, id : Int32) -> Bool {
  match self.kind(id) {
    Some(Text) => ()
    _ => return false
  }
  for c in self.value(id) {
    if !is_xml_whitespace(c) {
      return false
    }
  }
  true
}

///|
/// XML whitespace characters.
pub fn is_xml_whitespace(c : Char) -> Bool {
  c == ' ' || c == '\t' || c == '\n' || c == '\r'
}

///|
/// Append a new element node under `parent` and return its id.
///
/// Used by the parser and by result-tree construction in the XSLT engine.
pub fn Document::add_element(
  self : Document,
  parent : Int32,
  name : QName,
) -> Int32 {
  self.append(parent, {
    kind: Element,
    name: Some(name),
    value: "",
    attrs: [],
    children: [],
    parent,
    ns_decls: [],
    escape: true,
    order: self.nodes.length(),
  })
}

///|
/// Append a text node under `parent`.
///
/// Adjacent text nodes are merged, as required by the XPath data model; in that
/// case the existing node id is returned.
pub fn Document::add_text(
  self : Document,
  parent : Int32,
  text : String,
) -> Int32 {
  self.add_text_node(parent, text, true)
}

///|
/// Append a text node whose content is written without escaping, for
/// disable-output-escaping="yes".
pub fn Document::add_raw_text(
  self : Document,
  parent : Int32,
  text : String,
) -> Int32 {
  self.add_text_node(parent, text, false)
}

///|
fn Document::add_text_node(
  self : Document,
  parent : Int32,
  text : String,
  escape : Bool,
) -> Int32 {
  let children = self.nodes[parent].children
  if children.length() > 0 {
    let last = children[children.length() - 1]
    match self.nodes[last].kind {
      Text =>
        if self.nodes[last].escape == escape {
          self.nodes[last].value = self.nodes[last].value + text
          return last
        }
      _ => ()
    }
  }
  self.append(parent, {
    kind: Text,
    name: None,
    value: text,
    attrs: [],
    children: [],
    parent,
    ns_decls: [],
    escape,
    order: self.nodes.length(),
  })
}

///|
/// Whether the content of a text node is escaped on output.
pub fn Document::is_escaped_text(self : Document, id : Int32) -> Bool {
  self.nodes[id].escape
}

///|
/// Append a comment node under `parent`.
pub fn Document::add_comment(
  self : Document,
  parent : Int32,
  text : String,
) -> Int32 {
  self.append(parent, {
    kind: Comment,
    name: None,
    value: text,
    attrs: [],
    children: [],
    parent,
    ns_decls: [],
    escape: true,
    order: self.nodes.length(),
  })
}

///|
/// Append a processing-instruction node under `parent`.
pub fn Document::add_processing_instruction(
  self : Document,
  parent : Int32,
  target : String,
  data : String,
) -> Int32 {
  self.append(parent, {
    kind: ProcessingInstruction,
    name: Some({ prefix: "", local_part: target, uri: "", }),
    value: data,
    attrs: [],
    children: [],
    parent,
    ns_decls: [],
    escape: true,
    order: self.nodes.length(),
  })
}

///|
/// Append an attribute node to an element.
///
/// Attribute ids are ordered by declaration and share the document-order key of
/// their element.
pub fn Document::add_attribute(
  self : Document,
  element : Int32,
  name : QName,
  value : String,
) -> Int32 {
  let id = self.nodes.length()
  let order = self.nodes[element].order
  self.nodes.push({
    kind: Attribute,
    name: Some(name),
    value,
    attrs: [],
    children: [],
    parent: element,
    ns_decls: [],
    escape: true,
    order,
  })
  self.nodes[element].attrs.push(id)
  id
}

///|
/// Record a namespace declaration on an element.
pub fn Document::declare_namespace(
  self : Document,
  element : Int32,
  prefix : String,
  uri : String,
) -> Unit {
  self.nodes[element].ns_decls.push((prefix, uri))
}

///|
/// Mark the document element.
pub fn Document::set_root(self : Document, id : Int32) -> Unit {
  self.root_id = Some(id)
}

///|
/// Set the resolved name of an element once its namespace scope is known.
pub fn Document::set_element_name(
  self : Document,
  element : Int32,
  prefix : String,
  local_part : String,
  uri : String,
) -> Unit {
  self.nodes[element].name = Some(QName::new(prefix, local_part, uri))
}

///|
fn Document::append(self : Document, parent : Int32, node : Node) -> Int32 {
  let id = self.nodes.length()
  self.nodes.push(node)
  if parent >= 0 && parent < id {
    self.nodes[parent].children.push(id)
  }
  id
}

///|
/// Resolve a namespace prefix for the given node by walking up the tree.
///
/// Returns `None` for an undeclared prefix.
pub fn Document::resolve_prefix(
  self : Document,
  node : Int32,
  prefix : String,
) -> String? {
  let mut current = node
  while current >= 0 {
    for decl in self.namespace_declarations(current) {
      let declared_prefix = decl.0
      let declared_uri = decl.1
      if declared_prefix == prefix {
        return Some(declared_uri)
      }
    }
    match self.parent(current) {
      Some(parent) => current = parent
      None => break
    }
  }
  if prefix == "xml" {
    Some("http://www.w3.org/XML/1998/namespace")
  } else if prefix == "" {
    Some("")
  } else {
    None
  }
}

///|
/// Copy a subtree from another document under `parent`.
///
/// Names are already resolved in the source document, so the copy keeps the
/// resolved names, the namespace declarations and the text escaping flags.
/// Returns the id of the copied node, or `parent` for node kinds that cannot
/// be copied (document and attribute nodes).
pub fn Document::import_subtree(
  self : Document,
  parent : Int32,
  source : Document,
  node : Int32,
) -> Int32 {
  let from = source.nodes[node]
  match from.kind {
    Element => {
      let name = match from.name {
        Some(name) => name
        None => QName::new("", "", "")
      }
      let id = self.add_element(parent, name)
      for declaration in from.ns_decls {
        self.declare_namespace(id, declaration.0, declaration.1)
      }
      for attribute in from.attrs {
        let attribute_node = source.nodes[attribute]
        let attribute_name = match attribute_node.name {
          Some(attribute_name) => attribute_name
          None => QName::new("", "", "")
        }
        let _ = self.add_attribute(id, attribute_name, attribute_node.value)
      }
      for child in from.children {
        let _ = self.import_subtree(id, source, child)
      }
      id
    }
    Text =>
      if from.escape {
        self.add_text(parent, from.value)
      } else {
        self.add_raw_text(parent, from.value)
      }
    Comment => self.add_comment(parent, from.value)
    ProcessingInstruction => {
      let target = match from.name {
        Some(name) => name.local_part
        None => ""
      }
      self.add_processing_instruction(parent, target, from.value)
    }
    _ => parent
  }
}

///|
/// Remove a child from its parent.
pub fn Document::remove_child(
  self : Document,
  parent : Int32,
  child : Int32,
) -> Unit {
  let children = self.nodes[parent].children
  let kept : Array[Int32] = []
  for id in children {
    if id != child {
      kept.push(id)
    }
  }
  self.nodes[parent].children = kept
}

///|
/// Change the value of an existing attribute node.
pub fn Document::set_attribute_value(
  self : Document,
  attribute : Int32,
  value : String,
) -> Unit {
  self.nodes[attribute].value = value
}