//! XML Tree structure

use crate::avm1::object::xml_attributes_object::XMLAttributesObject;
use crate::avm1::object::xml_object::XMLObject;
use crate::avm1::{Object, TObject};
use crate::xml;
use crate::xml::{Error, Step, XMLDocument, XMLName};
use gc_arena::{Collect, GcCell, MutationContext};
use quick_xml::events::attributes::Attribute;
use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
use quick_xml::{Reader, Writer};
use smallvec::alloc::borrow::Cow;
use std::collections::BTreeMap;
use std::fmt;
use std::io::{Cursor, Write};
use std::mem::swap;

/// Represents a node in the XML tree.
#[derive(Copy, Clone, Collect)]
#[collect(no_drop)]
pub struct XMLNode<'gc>(GcCell<'gc, XMLNodeData<'gc>>);

#[derive(Clone, Collect)]
#[collect(no_drop)]
pub enum XMLNodeData<'gc> {
    /// The root level of an XML document. Has no parent.
    DocumentRoot {
        /// The script object associated with this XML node, if any.
        script_object: Option<Object<'gc>>,

        /// The script object associated with this XML node's attributes, if any.
        attributes_script_object: Option<Object<'gc>>,

        /// The document that this is the root of.
        document: XMLDocument<'gc>,

        /// Child nodes of this element.
        children: Vec<XMLNode<'gc>>,
    },

    /// An element node in the XML tree.
    ///
    /// Element nodes are non-leaf nodes: they can store additional data as
    /// either attributes (for key/value pairs) or child nodes (for more
    /// structured data).
    Element {
        /// The script object associated with this XML node, if any.
        script_object: Option<Object<'gc>>,

        /// The script object associated with this XML node's attributes, if any.
        attributes_script_object: Option<Object<'gc>>,

        /// The document that this tree node currently belongs to.
        document: XMLDocument<'gc>,

        /// The parent node of this one.
        parent: Option<XMLNode<'gc>>,

        /// The previous sibling node to this one.
        prev_sibling: Option<XMLNode<'gc>>,

        /// The next sibling node to this one.
        next_sibling: Option<XMLNode<'gc>>,

        /// The tag name of this element.
        tag_name: XMLName,

        /// Attributes of the element.
        attributes: BTreeMap<XMLName, String>,

        /// Child nodes of this element.
        children: Vec<XMLNode<'gc>>,
    },

    /// A text node in the XML tree.
    Text {
        /// The script object associated with this XML node, if any.
        script_object: Option<Object<'gc>>,

        /// The script object associated with this XML node's attributes, if any.
        attributes_script_object: Option<Object<'gc>>,

        /// The document that this tree node currently belongs to.
        document: XMLDocument<'gc>,

        /// The parent node of this one.
        parent: Option<XMLNode<'gc>>,

        /// The previous sibling node to this one.
        prev_sibling: Option<XMLNode<'gc>>,

        /// The next sibling node to this one.
        next_sibling: Option<XMLNode<'gc>>,

        /// The string representation of the text.
        contents: String,
    },

    /// A comment node in the XML tree.
    Comment {
        /// The script object associated with this XML node, if any.
        script_object: Option<Object<'gc>>,

        /// The document that this tree node currently belongs to.
        document: XMLDocument<'gc>,

        /// The parent node of this one.
        parent: Option<XMLNode<'gc>>,

        /// The previous sibling node to this one.
        prev_sibling: Option<XMLNode<'gc>>,

        /// The next sibling node to this one.
        next_sibling: Option<XMLNode<'gc>>,

        /// The string representation of the comment.
        contents: String,
    },

    /// A DOCTYPE node in the XML tree.
    DocType {
        /// The script object associated with this XML node, if any.
        script_object: Option<Object<'gc>>,

        /// The document that this tree node currently belongs to.
        document: XMLDocument<'gc>,

        /// The parent node of this one.
        parent: Option<XMLNode<'gc>>,

        /// The previous sibling node to this one.
        prev_sibling: Option<XMLNode<'gc>>,

        /// The next sibling node to this one.
        next_sibling: Option<XMLNode<'gc>>,

        /// The string representation of the DOCTYPE.
        contents: String,
    },
}

impl<'gc> XMLNode<'gc> {
    /// Construct a new XML text node.
    pub fn new_text(
        mc: MutationContext<'gc, '_>,
        contents: &str,
        document: XMLDocument<'gc>,
    ) -> Self {
        XMLNode(GcCell::allocate(
            mc,
            XMLNodeData::Text {
                script_object: None,
                attributes_script_object: None,
                document,
                parent: None,
                prev_sibling: None,
                next_sibling: None,
                contents: contents.to_string(),
            },
        ))
    }

    /// Construct a new XML element node.
    pub fn new_element(
        mc: MutationContext<'gc, '_>,
        element_name: &str,
        document: XMLDocument<'gc>,
    ) -> Self {
        XMLNode(GcCell::allocate(
            mc,
            XMLNodeData::Element {
                script_object: None,
                document,
                parent: None,
                prev_sibling: None,
                next_sibling: None,
                tag_name: XMLName::from_str(element_name),
                attributes: BTreeMap::new(),
                attributes_script_object: None,
                children: Vec::new(),
            },
        ))
    }

    /// Construct a new XML root node.
    pub fn new_document_root(mc: MutationContext<'gc, '_>, document: XMLDocument<'gc>) -> Self {
        XMLNode(GcCell::allocate(
            mc,
            XMLNodeData::DocumentRoot {
                script_object: None,
                attributes_script_object: None,
                document,
                children: Vec::new(),
            },
        ))
    }

    /// Ensure that a newly-encountered node is added to an ongoing parsing
    /// stack, or to the document root itself if the parsing stack is empty.
    fn add_child_to_tree(
        &mut self,
        mc: MutationContext<'gc, '_>,
        open_tags: &mut Vec<XMLNode<'gc>>,
        child: XMLNode<'gc>,
    ) -> Result<(), Error> {
        if let Some(node) = open_tags.last_mut() {
            node.append_child(mc, child)?;
        } else {
            self.append_child(mc, child)?;
        }

        Ok(())
    }

    /// Replace the contents of this node with the result of parsing a string.
    ///
    /// Node replacements are only supported on document root nodes; elements
    /// may work but will be incorrect.
    ///
    /// Also, this method does not yet actually remove existing node contents.
    ///
    /// If `process_entity` is `true`, then entities will be processed by this
    /// function. Invalid or unrecognized entities will cause parsing to fail
    /// with an `Err`.
    pub fn replace_with_str(
        &mut self,
        mc: MutationContext<'gc, '_>,
        data: &str,
        process_entity: bool,
        ignore_white: bool,
    ) -> Result<(), Error> {
        let mut parser = Reader::from_str(data);
        let mut buf = Vec::new();
        let document = self.document();
        let mut open_tags: Vec<XMLNode<'gc>> = Vec::new();

        document.clear_parse_error(mc);

        loop {
            let event = document.log_parse_result(mc, parser.read_event(&mut buf))?;

            document.process_event(mc, &event)?;

            match event {
                Event::Start(bs) => {
                    let child = XMLNode::from_start_event(mc, bs, document)?;
                    self.document().update_idmap(mc, child);
                    self.add_child_to_tree(mc, &mut open_tags, child)?;
                    open_tags.push(child);
                }
                Event::Empty(bs) => {
                    let child = XMLNode::from_start_event(mc, bs, document)?;
                    self.document().update_idmap(mc, child);
                    self.add_child_to_tree(mc, &mut open_tags, child)?;
                }
                Event::End(_) => {
                    open_tags.pop();
                }
                Event::Text(bt) => {
                    let child = XMLNode::text_from_text_event(mc, bt, document, process_entity)?;
                    if child.node_value().as_deref() != Some("")
                        && (!ignore_white || !child.is_whitespace_text())
                    {
                        self.add_child_to_tree(mc, &mut open_tags, child)?;
                    }
                }
                Event::Comment(bt) => {
                    let child = XMLNode::comment_from_text_event(mc, bt, document)?;
                    if child.node_value().as_deref() != Some("") {
                        self.add_child_to_tree(mc, &mut open_tags, child)?;
                    }
                }
                Event::DocType(bt) => {
                    let child = XMLNode::doctype_from_text_event(mc, bt, document)?;
                    if child.node_value().as_deref() != Some("") {
                        self.add_child_to_tree(mc, &mut open_tags, child)?;
                    }
                }
                Event::Eof => break,
                _ => {}
            }
        }

        Ok(())
    }

    /// Construct an XML Element node from a `quick_xml` `BytesStart` event.
    ///
    /// The returned node will always be an `Element`, and it must only contain
    /// valid encoded UTF-8 data. (Other encoding support is planned later.)
    pub fn from_start_event<'a>(
        mc: MutationContext<'gc, '_>,
        bs: BytesStart<'a>,
        document: XMLDocument<'gc>,
    ) -> Result<Self, Error> {
        let tag_name = XMLName::from_bytes(bs.name())?;
        let mut attributes = BTreeMap::new();

        for a in bs.attributes() {
            let attribute = a?;
            attributes.insert(
                XMLName::from_bytes(attribute.key)?,
                String::from_utf8(attribute.value.to_owned().to_vec())?,
            );
        }

        let children = Vec::new();

        Ok(XMLNode(GcCell::allocate(
            mc,
            XMLNodeData::Element {
                script_object: None,
                document,
                parent: None,
                prev_sibling: None,
                next_sibling: None,
                tag_name,
                attributes,
                attributes_script_object: None,
                children,
            },
        )))
    }

    /// Construct an XML Text node from a `quick_xml` `BytesText` event.
    ///
    /// The returned node will always be `Text`, and it must only contain
    /// valid encoded UTF-8 data. (Other encoding support is planned later.)
    pub fn text_from_text_event<'a>(
        mc: MutationContext<'gc, '_>,
        bt: BytesText<'a>,
        document: XMLDocument<'gc>,
        process_entity: bool,
    ) -> Result<Self, Error> {
        let contents = if process_entity {
            String::from_utf8(bt.unescaped()?.into_owned())?
        } else {
            String::from_utf8(bt.escaped().to_vec())?
        };

        Ok(XMLNode(GcCell::allocate(
            mc,
            XMLNodeData::Text {
                script_object: None,
                attributes_script_object: None,
                document,
                parent: None,
                prev_sibling: None,
                next_sibling: None,
                contents,
            },
        )))
    }

    /// Construct an XML Comment node from a `quick_xml` `BytesText` event.
    ///
    /// The returned node will always be `Comment`, and it must only contain
    /// valid encoded UTF-8 data. (Other encoding support is planned later.)
    pub fn comment_from_text_event<'a>(
        mc: MutationContext<'gc, '_>,
        bt: BytesText<'a>,
        document: XMLDocument<'gc>,
    ) -> Result<Self, Error> {
        Ok(XMLNode(GcCell::allocate(
            mc,
            XMLNodeData::Comment {
                script_object: None,
                document,
                parent: None,
                prev_sibling: None,
                next_sibling: None,
                contents: String::from_utf8(bt.unescaped()?.into_owned())?,
            },
        )))
    }

    /// Construct an XML DocType node from a `quick_xml` `BytesText` event.
    ///
    /// The returned node will always be `DocType`, and it must only contain
    /// valid encoded UTF-8 data. (Other encoding support is planned later.)
    pub fn doctype_from_text_event<'a>(
        mc: MutationContext<'gc, '_>,
        bt: BytesText<'a>,
        document: XMLDocument<'gc>,
    ) -> Result<Self, Error> {
        Ok(XMLNode(GcCell::allocate(
            mc,
            XMLNodeData::DocType {
                script_object: None,
                document,
                parent: None,
                prev_sibling: None,
                next_sibling: None,
                contents: String::from_utf8(bt.unescaped()?.into_owned())?,
            },
        )))
    }

    /// Return the XML document that this tree node belongs to.
    ///
    /// Every XML node belongs to a document object (see `XMLDocument`) which
    /// stores global information about the document, such as namespace URIs.
    pub fn document(self) -> XMLDocument<'gc> {
        match &*self.0.read() {
            XMLNodeData::DocumentRoot { document, .. } => *document,
            XMLNodeData::Element { document, .. } => *document,
            XMLNodeData::Text { document, .. } => *document,
            XMLNodeData::Comment { document, .. } => *document,
            XMLNodeData::DocType { document, .. } => *document,
        }
    }

    /// Adopt a new child node into the current node.
    ///
    /// This does not add the node to any internal lists; it merely updates the
    /// child to ensure that it considers this node its parent. This function
    /// should always be called after a child node is added to this one. If
    /// you adopt a node that is NOT already added to the children list, bad
    /// things may happen.
    ///
    /// The `new_child_position` parameter is the position of the new child in
    /// this node's child list. This is used to find and link the child's
    /// siblings to each other.
    fn adopt_child(
        &mut self,
        mc: MutationContext<'gc, '_>,
        mut child: XMLNode<'gc>,
        new_child_position: usize,
    ) -> Result<(), Error> {
        if GcCell::ptr_eq(self.0, child.0) {
            return Err(Error::CannotAdoptSelf);
        }

        let (mut document, new_prev, new_next) = match &mut *self.0.write(mc) {
            XMLNodeData::Element {
                document, children, ..
            }
            | XMLNodeData::DocumentRoot {
                document, children, ..
            } => {
                let mut write = child.0.write(mc);
                let (child_document, child_parent) = match &mut *write {
                    XMLNodeData::Element {
                        document, parent, ..
                    } => Ok((document, parent)),
                    XMLNodeData::Text {
                        document, parent, ..
                    } => Ok((document, parent)),
                    XMLNodeData::Comment {
                        document, parent, ..
                    } => Ok((document, parent)),
                    XMLNodeData::DocType {
                        document, parent, ..
                    } => Ok((document, parent)),
                    XMLNodeData::DocumentRoot { .. } => Err(Error::CannotAdoptRoot),
                }?;

                if let Some(parent) = child_parent {
                    if !GcCell::ptr_eq(self.0, parent.0) {
                        parent.orphan_child(mc, child)?;
                    }
                }

                *child_document = *document;
                *child_parent = Some(*self);

                let new_prev = new_child_position
                    .checked_sub(1)
                    .and_then(|p| children.get(p).cloned());
                let new_next = new_child_position
                    .checked_add(1)
                    .and_then(|p| children.get(p).cloned());

                (*document, new_prev, new_next)
            }
            _ => return Err(Error::CannotAdoptHere),
        };

        if child.is_doctype() {
            document.link_doctype(mc, child);
        }

        child.disown_siblings(mc)?;

        child.adopt_siblings(mc, new_prev, new_next)?;

        Ok(())
    }

    /// Get the parent, if this node has one.
    ///
    /// If the node cannot have a parent, then this function yields Err.
    pub fn parent(self) -> Result<Option<XMLNode<'gc>>, Error> {
        match *self.0.read() {
            XMLNodeData::DocumentRoot { .. } => Err(Error::RootCantHaveParent),
            XMLNodeData::Element { parent, .. } => Ok(parent),
            XMLNodeData::Text { parent, .. } => Ok(parent),
            XMLNodeData::Comment { parent, .. } => Ok(parent),
            XMLNodeData::DocType { parent, .. } => Ok(parent),
        }
    }

    /// Get the previous sibling, if this node has one.
    ///
    /// If the node cannot have siblings, then this function yields Err.
    pub fn prev_sibling(self) -> Result<Option<XMLNode<'gc>>, Error> {
        match *self.0.read() {
            XMLNodeData::DocumentRoot { .. } => Err(Error::RootCantHaveSiblings),
            XMLNodeData::Element { prev_sibling, .. } => Ok(prev_sibling),
            XMLNodeData::Text { prev_sibling, .. } => Ok(prev_sibling),
            XMLNodeData::Comment { prev_sibling, .. } => Ok(prev_sibling),
            XMLNodeData::DocType { prev_sibling, .. } => Ok(prev_sibling),
        }
    }

    /// Set this node's previous sibling.
    fn set_prev_sibling(
        &mut self,
        mc: MutationContext<'gc, '_>,
        new_prev: Option<XMLNode<'gc>>,
    ) -> Result<(), Error> {
        match &mut *self.0.write(mc) {
            XMLNodeData::DocumentRoot { .. } => return Err(Error::RootCantHaveSiblings),
            XMLNodeData::Element { prev_sibling, .. } => *prev_sibling = new_prev,
            XMLNodeData::Text { prev_sibling, .. } => *prev_sibling = new_prev,
            XMLNodeData::Comment { prev_sibling, .. } => *prev_sibling = new_prev,
            XMLNodeData::DocType { prev_sibling, .. } => *prev_sibling = new_prev,
        };

        Ok(())
    }

    /// Get the next sibling, if this node has one.
    ///
    /// If the node cannot have siblings, then this function yields Err.
    pub fn next_sibling(self) -> Result<Option<XMLNode<'gc>>, Error> {
        match *self.0.read() {
            XMLNodeData::DocumentRoot { .. } => Err(Error::RootCantHaveSiblings),
            XMLNodeData::Element { next_sibling, .. } => Ok(next_sibling),
            XMLNodeData::Text { next_sibling, .. } => Ok(next_sibling),
            XMLNodeData::Comment { next_sibling, .. } => Ok(next_sibling),
            XMLNodeData::DocType { next_sibling, .. } => Ok(next_sibling),
        }
    }

    /// Set this node's next sibling.
    fn set_next_sibling(
        &mut self,
        mc: MutationContext<'gc, '_>,
        new_next: Option<XMLNode<'gc>>,
    ) -> Result<(), Error> {
        match &mut *self.0.write(mc) {
            XMLNodeData::DocumentRoot { .. } => return Err(Error::RootCantHaveSiblings),
            XMLNodeData::Element { next_sibling, .. } => *next_sibling = new_next,
            XMLNodeData::Text { next_sibling, .. } => *next_sibling = new_next,
            XMLNodeData::Comment { next_sibling, .. } => *next_sibling = new_next,
            XMLNodeData::DocType { next_sibling, .. } => *next_sibling = new_next,
        };

        Ok(())
    }

    /// Remove node from its current siblings list.
    ///
    /// If a former sibling exists, we will also adopt it to the opposing side
    /// of this node, so as to maintain a coherent sibling list.
    ///
    /// This is the opposite of `adopt_siblings` - the former adds a node to a
    /// new sibling list, and this removes it from the current one.
    fn disown_siblings(&mut self, mc: MutationContext<'gc, '_>) -> Result<(), Error> {
        let old_prev = self.prev_sibling()?;
        let old_next = self.next_sibling()?;

        if let Some(mut prev) = old_prev {
            prev.set_next_sibling(mc, old_next)?;
        }

        if let Some(mut next) = old_next {
            next.set_prev_sibling(mc, old_prev)?;
        }

        self.set_prev_sibling(mc, None)?;
        self.set_next_sibling(mc, None)?;

        Ok(())
    }

    /// Unset the parent of this node.
    fn disown_parent(&mut self, mc: MutationContext<'gc, '_>) -> Result<(), Error> {
        match &mut *self.0.write(mc) {
            XMLNodeData::DocumentRoot { .. } => return Err(Error::RootCantHaveParent),
            XMLNodeData::Element { parent, .. } => *parent = None,
            XMLNodeData::Text { parent, .. } => *parent = None,
            XMLNodeData::Comment { parent, .. } => *parent = None,
            XMLNodeData::DocType { parent, .. } => *parent = None,
        };

        Ok(())
    }

    /// Add node to a new siblings list.
    ///
    /// If a given sibling exists, we will also ensure this node is adopted as
    /// its sibling, so as to maintain a coherent sibling list.
    ///
    /// This is the opposite of `disown_siblings` - the former removes a
    /// sibling from its current list, and this adds the sibling to a new one.
    fn adopt_siblings(
        &mut self,
        mc: MutationContext<'gc, '_>,
        new_prev: Option<XMLNode<'gc>>,
        new_next: Option<XMLNode<'gc>>,
    ) -> Result<(), Error> {
        if let Some(mut prev) = new_prev {
            prev.set_next_sibling(mc, Some(*self))?;
        }

        if let Some(mut next) = new_next {
            next.set_prev_sibling(mc, Some(*self))?;
        }

        self.set_prev_sibling(mc, new_prev)?;
        self.set_next_sibling(mc, new_next)?;

        Ok(())
    }

    /// Remove node from this node's child list.
    ///
    /// This function yields Err if this node cannot accept child nodes.
    fn orphan_child(
        &mut self,
        mc: MutationContext<'gc, '_>,
        child: XMLNode<'gc>,
    ) -> Result<(), Error> {
        if let Some(position) = self.child_position(child) {
            match &mut *self.0.write(mc) {
                XMLNodeData::DocumentRoot { children, .. } => children.remove(position),
                XMLNodeData::Element { children, .. } => children.remove(position),
                XMLNodeData::Text { .. } => return Err(Error::TextNodeCantHaveChildren),
                XMLNodeData::Comment { .. } => return Err(Error::CommentNodeCantHaveChildren),
                XMLNodeData::DocType { .. } => return Err(Error::DocTypeCantHaveChildren),
            };
        }

        Ok(())
    }

    /// Insert a child element into the child list of an Element node.
    ///
    /// The child will be adopted into the current tree: all child references
    /// to other nodes or documents will be adjusted to reflect its new
    /// position in the tree. This may remove it from any existing trees or
    /// documents.
    ///
    /// This function yields an error if appending to a Node that cannot accept
    /// children. In that case, no modification will be made to the node.
    pub fn insert_child(
        &mut self,
        mc: MutationContext<'gc, '_>,
        position: usize,
        child: XMLNode<'gc>,
    ) -> Result<(), Error> {
        if GcCell::ptr_eq(self.0, child.0) {
            return Err(Error::CannotInsertIntoSelf);
        }

        match &mut *self.0.write(mc) {
            XMLNodeData::Element {
                ref mut children, ..
            }
            | XMLNodeData::DocumentRoot {
                ref mut children, ..
            } => {
                children.insert(position, child);
            }
            _ => return Err(Error::NotAnElement),
        };

        self.adopt_child(mc, child, position)?;

        Ok(())
    }

    /// Append a child element into the end of the child list of an Element
    /// node.
    pub fn append_child(
        &mut self,
        mc: MutationContext<'gc, '_>,
        child: XMLNode<'gc>,
    ) -> Result<(), Error> {
        self.insert_child(mc, self.children_len(), child)
    }

    /// Remove a previously added node from this tree.
    ///
    /// If the node is not a child of this one, or this node cannot accept
    /// children, then this function yields an error.
    pub fn remove_child(
        &mut self,
        mc: MutationContext<'gc, '_>,
        mut child: XMLNode<'gc>,
    ) -> Result<(), Error> {
        if let Some(position) = self.child_position(child) {
            match &mut *self.0.write(mc) {
                XMLNodeData::Element { children, .. } => children.remove(position),
                XMLNodeData::DocumentRoot { children, .. } => children.remove(position),
                XMLNodeData::Text { .. } => return Err(Error::TextNodeCantHaveChildren),
                XMLNodeData::Comment { .. } => return Err(Error::CommentNodeCantHaveChildren),
                XMLNodeData::DocType { .. } => return Err(Error::DocTypeCantHaveChildren),
            };

            child.disown_siblings(mc)?;
            child.disown_parent(mc)?;
        } else {
            return Err(Error::CantRemoveNonChild);
        }

        Ok(())
    }

    /// Returns the type of this node as an integer.
    ///
    /// This is primarily intended to match W3C DOM L1 specifications and
    /// should not be used in lieu of a proper `match` statement.
    pub fn node_type(self) -> u8 {
        match &*self.0.read() {
            XMLNodeData::DocumentRoot { .. } => xml::DOCUMENT_NODE,
            XMLNodeData::Element { .. } => xml::ELEMENT_NODE,
            XMLNodeData::Text { .. } => xml::TEXT_NODE,
            XMLNodeData::Comment { .. } => xml::COMMENT_NODE,
            XMLNodeData::DocType { .. } => xml::DOCUMENT_TYPE_NODE,
        }
    }

    /// Returns the tagname, if the element has one.
    pub fn tag_name(self) -> Option<XMLName> {
        match &*self.0.read() {
            XMLNodeData::Element { ref tag_name, .. } => Some(tag_name.clone()),
            _ => None,
        }
    }

    /// Returns the string contents of the node, if the element has them.
    pub fn node_value(self) -> Option<String> {
        match &*self.0.read() {
            XMLNodeData::Text { ref contents, .. } => Some(contents.clone()),
            XMLNodeData::Comment { ref contents, .. } => Some(contents.clone()),
            XMLNodeData::DocType { ref contents, .. } => Some(contents.clone()),
            _ => None,
        }
    }

    /// Returns the number of children of the current tree node.
    ///
    /// Nodes that cannot hold children always yield `0`.
    pub fn children_len(self) -> usize {
        match &*self.0.read() {
            XMLNodeData::Element { children, .. } | XMLNodeData::DocumentRoot { children, .. } => {
                children.len()
            }
            _ => 0,
        }
    }

    /// Get the position of a child of this node.
    ///
    /// This function yields None if the node cannot accept children or if the
    /// child node is not a child of this node.
    pub fn child_position(self, child: XMLNode<'gc>) -> Option<usize> {
        if let Some(children) = self.children() {
            for (i, other_child) in children.enumerate() {
                if GcCell::ptr_eq(child.0, other_child.0) {
                    return Some(i);
                }
            }
        }

        None
    }

    /// Retrieve a given child by index (e.g. position in the document).
    pub fn get_child_by_index(self, index: usize) -> Option<XMLNode<'gc>> {
        match &*self.0.read() {
            XMLNodeData::Element { children, .. } | XMLNodeData::DocumentRoot { children, .. } => {
                Some(children)
            }
            _ => None,
        }
        .and_then(|children| children.get(index))
        .cloned()
    }

    /// Returns if the node can yield children.
    ///
    /// Document roots and elements can yield children, while all other
    /// elements are structurally prohibited from adopting child `XMLNode`s.
    pub fn has_children(self) -> bool {
        matches!(
            *self.0.read(),
            XMLNodeData::Element { .. } | XMLNodeData::DocumentRoot { .. }
        )
    }

    /// Returns an iterator that yields child nodes.
    ///
    /// Yields None if this node cannot accept children.
    pub fn children(self) -> Option<impl DoubleEndedIterator<Item = XMLNode<'gc>>> {
        match &*self.0.read() {
            XMLNodeData::Element { .. } | XMLNodeData::DocumentRoot { .. } => {
                Some(xml::iterators::ChildIter::for_node(self))
            }
            _ => None,
        }
    }

    /// Returns an iterator that walks the XML tree.
    ///
    /// Walking is similar to using `descendents`, but the ends of parent nodes
    /// are explicitly marked with `Step::Out`, while nodes that may have
    /// children are marked with `Step::In`.
    ///
    /// Yields None if this node cannot accept children.
    pub fn walk(self) -> Option<impl Iterator<Item = Step<'gc>>> {
        match &*self.0.read() {
            XMLNodeData::Element { .. } | XMLNodeData::DocumentRoot { .. } => {
                Some(xml::iterators::WalkIter::for_node(self))
            }
            _ => None,
        }
    }

    /// Returns an iterator that yields ancestor nodes.
    ///
    /// Yields None if this node does not have a parent.
    pub fn ancestors(self) -> Option<impl Iterator<Item = XMLNode<'gc>>> {
        let parent = match *self.0.read() {
            XMLNodeData::DocumentRoot { .. } => return None,
            XMLNodeData::Element { parent, .. } => parent,
            XMLNodeData::Text { parent, .. } => parent,
            XMLNodeData::Comment { parent, .. } => parent,
            XMLNodeData::DocType { parent, .. } => parent,
        };

        Some(xml::iterators::AnscIter::for_node(parent))
    }

    /// Get the already-instantiated script object from the current node.
    fn get_script_object(self) -> Option<Object<'gc>> {
        match &*self.0.read() {
            XMLNodeData::DocumentRoot { script_object, .. } => *script_object,
            XMLNodeData::Element { script_object, .. } => *script_object,
            XMLNodeData::Text { script_object, .. } => *script_object,
            XMLNodeData::Comment { script_object, .. } => *script_object,
            XMLNodeData::DocType { script_object, .. } => *script_object,
        }
    }

    /// Introduce this node to a new script object.
    ///
    /// This internal function *will* overwrite already extant objects, so only
    /// call this if you need to instantiate the script object for the first
    /// time. Attempting to call it a second time will panic.
    pub fn introduce_script_object(
        &mut self,
        gc_context: MutationContext<'gc, '_>,
        new_object: Object<'gc>,
    ) {
        assert!(self.get_script_object().is_none(), "An attempt was made to change the already-established link between script object and XML node. This has been denied and is likely a bug.");

        match &mut *self.0.write(gc_context) {
            XMLNodeData::DocumentRoot { script_object, .. } => *script_object = Some(new_object),
            XMLNodeData::Element { script_object, .. } => *script_object = Some(new_object),
            XMLNodeData::Text { script_object, .. } => *script_object = Some(new_object),
            XMLNodeData::Comment { script_object, .. } => *script_object = Some(new_object),
            XMLNodeData::DocType { script_object, .. } => *script_object = Some(new_object),
        }
    }

    /// Obtain the script object for a given XML tree node, constructing a new
    /// script object if one does not exist.
    pub fn script_object(
        &mut self,
        gc_context: MutationContext<'gc, '_>,
        prototype: Option<Object<'gc>>,
    ) -> Object<'gc> {
        let mut object = self.get_script_object();
        if object.is_none() {
            object = Some(XMLObject::from_xml_node(gc_context, *self, prototype));
            self.introduce_script_object(gc_context, object.unwrap());
        }

        object.unwrap()
    }

    /// Obtain the script object for a given XML tree node's attributes,
    /// constructing a new script object if one does not exist.
    pub fn attribute_script_object(
        &mut self,
        gc_context: MutationContext<'gc, '_>,
    ) -> Option<Object<'gc>> {
        match &mut *self.0.write(gc_context) {
            XMLNodeData::Element {
                attributes_script_object,
                ..
            }
            | XMLNodeData::DocumentRoot {
                attributes_script_object,
                ..
            }
            | XMLNodeData::Text {
                attributes_script_object,
                ..
            } => {
                if attributes_script_object.is_none() {
                    *attributes_script_object =
                        Some(XMLAttributesObject::from_xml_node(gc_context, *self));
                }

                *attributes_script_object
            }
            _ => None,
        }
    }

    /// Swap the contents of this node with another one.
    ///
    /// After this function completes, the current `XMLNode` will contain all
    /// data present in the `other` node, and vice versa. References to the node
    /// will *not* be updated: it is a logic error to swap nodes that have
    /// existing referents.
    pub fn swap(&mut self, gc_context: MutationContext<'gc, '_>, other: Self) {
        if !GcCell::ptr_eq(self.0, other.0) {
            swap(
                &mut *self.0.write(gc_context),
                &mut *other.0.write(gc_context),
            );
        }
    }

    /// Check if this XML node constitutes the root of a whole document.
    pub fn is_document_root(self) -> bool {
        matches!(*self.0.read(), XMLNodeData::DocumentRoot { .. })
    }

    /// Check if this XML node constitutes an element.
    pub fn is_element(self) -> bool {
        matches!(*self.0.read(), XMLNodeData::Element { .. })
    }

    /// Check if this XML node constitutes text.
    pub fn is_text(self) -> bool {
        matches!(*self.0.read(), XMLNodeData::Text { .. })
    }

    // Check if this XML node is constitutes text and only contains whitespace.
    pub fn is_whitespace_text(self) -> bool {
        const WHITESPACE_CHARS: &[u8] = &[b' ', b'\t', b'\r', b'\n'];
        matches!(&*self.0.read(), XMLNodeData::Text { contents, .. } if contents.bytes().all(|c| WHITESPACE_CHARS.contains(&c)))
    }

    /// Check if this XML node constitutes text.
    #[allow(dead_code)]
    pub fn is_comment(self) -> bool {
        matches!(*self.0.read(), XMLNodeData::Comment { .. })
    }

    /// Check if this XML node constitutes a DOCTYPE declaration
    pub fn is_doctype(self) -> bool {
        matches!(*self.0.read(), XMLNodeData::DocType { .. })
    }

    /// Create a duplicate copy of this node.
    ///
    /// If the `deep` flag is set true, then the entire node tree will be
    /// cloned.
    pub fn duplicate(self, gc_context: MutationContext<'gc, '_>, deep: bool) -> XMLNode<'gc> {
        let mut document = self.document().duplicate(gc_context);
        let mut clone = XMLNode(GcCell::allocate(
            gc_context,
            match &*self.0.read() {
                XMLNodeData::DocumentRoot { .. } => XMLNodeData::DocumentRoot {
                    script_object: None,
                    attributes_script_object: None,
                    document,
                    children: Vec::new(),
                },
                XMLNodeData::Element {
                    tag_name,
                    attributes,
                    ..
                } => XMLNodeData::Element {
                    script_object: None,
                    document,
                    parent: None,
                    prev_sibling: None,
                    next_sibling: None,
                    tag_name: tag_name.clone(),
                    attributes: attributes.clone(),
                    attributes_script_object: None,
                    children: Vec::new(),
                },
                XMLNodeData::Text { contents, .. } => XMLNodeData::Text {
                    script_object: None,
                    attributes_script_object: None,
                    document,
                    parent: None,
                    prev_sibling: None,
                    next_sibling: None,
                    contents: contents.to_string(),
                },
                XMLNodeData::Comment { contents, .. } => XMLNodeData::Comment {
                    script_object: None,
                    document,
                    parent: None,
                    prev_sibling: None,
                    next_sibling: None,
                    contents: contents.to_string(),
                },
                XMLNodeData::DocType { contents, .. } => XMLNodeData::DocType {
                    script_object: None,
                    document,
                    parent: None,
                    prev_sibling: None,
                    next_sibling: None,
                    contents: contents.to_string(),
                },
            },
        ));

        document.link_root_node(gc_context, clone);

        if deep {
            if let Some(children) = self.children() {
                for (position, child) in children.enumerate() {
                    clone
                        .insert_child(gc_context, position, child.duplicate(gc_context, deep))
                        .expect("If I can see my children then my clone should accept children");
                }
            }
        }

        clone
    }

    /// Retrieve the value of a single attribute on this node.
    ///
    /// If the node does not contain attributes, then this function always
    /// yields None.
    pub fn attribute_value(self, name: &XMLName) -> Option<String> {
        match &*self.0.read() {
            XMLNodeData::Element { attributes, .. } => attributes.get(name).cloned(),
            _ => None,
        }
    }

    /// Retrieve all keys defined on this node.
    pub fn attribute_keys(self) -> Vec<String> {
        match &*self.0.read() {
            XMLNodeData::Element { attributes, .. } => attributes
                .keys()
                .map(|v| v.node_name().to_string())
                .collect::<Vec<String>>(),
            _ => Vec::new(),
        }
    }

    /// Retrieve the value of a single attribute on this node, case-insensitively.
    ///
    /// TODO: Probably won't need this when we have a proper HTML parser.
    pub fn attribute_value_ignore_ascii_case(self, name: &XMLName) -> Option<String> {
        match &*self.0.read() {
            XMLNodeData::Element { attributes, .. } => attributes
                .iter()
                .find(|(k, _)| k.eq_ignore_ascii_case(name))
                .map(|(_, v)| v.clone()),
            _ => None,
        }
    }

    /// Set the value of a single attribute on this node.
    ///
    /// If the node does not contain attributes, then this function silently fails.
    pub fn set_attribute_value(
        self,
        gc_context: MutationContext<'gc, '_>,
        name: &XMLName,
        value: &str,
    ) {
        if let XMLNodeData::Element { attributes, .. } = &mut *self.0.write(gc_context) {
            attributes.insert(name.clone(), value.to_string());
        }
    }

    /// Delete the value of a single attribute on this node.
    ///
    /// If the node does not contain attributes, then this function silently fails.
    pub fn delete_attribute(self, gc_context: MutationContext<'gc, '_>, name: &XMLName) {
        if let XMLNodeData::Element { attributes, .. } = &mut *self.0.write(gc_context) {
            attributes.remove(name);
        }
    }

    /// Look up the URI for the given namespace.
    ///
    /// XML namespaces are determined by `xmlns:` namespace attributes on the
    /// current node, or its parent.
    pub fn lookup_uri_for_namespace(self, namespace: &str) -> Option<String> {
        let xmlns_default = XMLName::from_parts(None, "xmlns");
        let xmlns_ns = XMLName::from_parts(Some("xmlns"), namespace);

        if namespace.is_empty() {
            if let Some(url) = self.attribute_value(&xmlns_default) {
                return Some(url);
            }
        }

        if let Some(url) = self.attribute_value(&xmlns_ns) {
            return Some(url);
        }

        if let Ok(Some(parent)) = self.parent() {
            parent.lookup_uri_for_namespace(namespace)
        } else {
            None
        }
    }

    /// Retrieve the first attribute key set to a given value, if any.
    ///
    /// If the node does not contain attributes, then this function always
    /// yields None.
    ///
    /// You may restrict your value search to specific namespaces by setting
    /// `within_namespace`. If it is set to `None`, then any namespace's
    /// attributes may satisfy the search. It is it set to `""`, then
    /// the default namespace will be searched.
    pub fn value_attribute(self, value: &str, within_namespace: Option<&str>) -> Option<XMLName> {
        match &*self.0.read() {
            XMLNodeData::Element { attributes, .. } => {
                for (attr, attr_value) in attributes.iter() {
                    if let Some(namespace) = within_namespace {
                        if attr.prefix().unwrap_or("") == namespace && value == attr_value {
                            return Some(attr.clone());
                        }
                    } else if value == attr_value {
                        return Some(attr.clone());
                    }
                }

                None
            }
            _ => None,
        }
    }

    /// Look up the namespace for the given URI.
    ///
    /// XML namespaces are determined by `xmlns:` namespace attributes on the
    /// current node, or its parent.
    ///
    /// If there are multiple namespaces that match the URI, the first
    /// mentioned on the closest node will be returned.
    pub fn lookup_namespace_for_uri(self, uri: &str) -> Option<String> {
        if let Some(xname) = self.value_attribute(uri, Some("xmlns")) {
            Some(xname.local_name().to_string())
        } else if let Ok(Some(parent)) = self.parent() {
            parent.lookup_namespace_for_uri(uri)
        } else {
            None
        }
    }

    /// Convert the given node to a string of UTF-8 encoded XML.
    ///
    /// The given filter function allows filtering specific children out of the
    /// resulting string. It will be called at least once for each node
    /// encountered in the tree (other than this one) if specified; only nodes
    /// that yield `true` shall be printed.
    pub fn into_string<F>(self, filter: &mut F) -> Result<String, Error>
    where
        F: FnMut(XMLNode<'gc>) -> bool,
    {
        let mut buf = Vec::new();
        let mut writer = Writer::new(Cursor::new(&mut buf));
        self.write_node_to_event_writer(&mut writer, filter)?;
        Ok(String::from_utf8(buf)?)
    }

    /// Write the contents of this node, including its children, to the given
    /// writer.
    ///
    /// The given filter function allows filtering specific children out of the
    /// resulting write stream. It will be called at least once for each node
    /// encountered in the tree (other than this one) if specified; only nodes
    /// that yield `true` shall be printed.
    fn write_node_to_event_writer<W, F>(
        self,
        writer: &mut Writer<W>,
        filter: &mut F,
    ) -> Result<(), Error>
    where
        W: Write,
        F: FnMut(XMLNode<'gc>) -> bool,
    {
        let mut children = Vec::new();
        if let Some(my_children) = self.children() {
            for child in my_children {
                if filter(child) {
                    children.push(child)
                }
            }
        }

        let children_len = children.len();

        match &*self.0.read() {
            XMLNodeData::DocumentRoot { .. } => Ok(()),
            XMLNodeData::Element {
                tag_name,
                attributes,
                ..
            } => {
                let mut bs = if children_len > 0 {
                    match tag_name.node_name() {
                        Cow::Borrowed(name) => BytesStart::borrowed_name(name.as_bytes()),
                        Cow::Owned(name) => BytesStart::owned_name(name),
                    }
                } else {
                    BytesStart::owned_name(format!("{} ", tag_name.node_name()))
                };
                let key_values: Vec<(Cow<str>, &str)> = attributes
                    .iter()
                    .map(|(name, value)| (name.node_name(), value.as_str()))
                    .collect();

                bs.extend_attributes(
                    key_values
                        .iter()
                        .map(|(name, value)| Attribute::from((name.as_ref(), *value))),
                );

                if children_len > 0 {
                    writer.write_event(&Event::Start(bs))
                } else {
                    writer.write_event(&Event::Empty(bs))
                }
            }
            XMLNodeData::Text { contents, .. } => {
                writer.write_event(&Event::Text(BytesText::from_plain_str(contents.as_str())))
            }
            XMLNodeData::Comment { contents, .. } => writer.write_event(&Event::Comment(
                BytesText::from_plain_str(contents.as_str()),
            )),
            XMLNodeData::DocType { contents, .. } => writer.write_event(&Event::DocType(
                BytesText::from_plain_str(contents.as_str()),
            )),
        }?;

        for child in children {
            child.write_node_to_event_writer(writer, filter)?;
        }

        match &*self.0.read() {
            XMLNodeData::DocumentRoot { .. } => Ok(()),
            XMLNodeData::Element { tag_name, .. } => {
                if children_len > 0 {
                    let bs = match tag_name.node_name() {
                        Cow::Borrowed(name) => BytesEnd::borrowed(name.as_bytes()),
                        Cow::Owned(name) => BytesEnd::owned(name.into()),
                    };
                    writer.write_event(&Event::End(bs))
                } else {
                    Ok(())
                }
            }
            XMLNodeData::Text { .. } => Ok(()),
            XMLNodeData::Comment { .. } => Ok(()),
            XMLNodeData::DocType { .. } => Ok(()),
        }?;

        Ok(())
    }
}

impl<'gc> fmt::Debug for XMLNode<'gc> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match &*self.0.read() {
            XMLNodeData::DocumentRoot {
                script_object,
                children,
                ..
            } => f
                .debug_struct("XMLNodeData::DocumentRoot")
                .field("0", &self.0.as_ptr())
                .field(
                    "script_object",
                    &script_object
                        .map(|p| format!("{:p}", p.as_ptr()))
                        .unwrap_or_else(|| "None".to_string()),
                )
                .field("document", &"<Elided>".to_string())
                .field("children", children)
                .finish(),
            XMLNodeData::Element {
                script_object,
                tag_name,
                attributes,
                children,
                parent,
                ..
            } => f
                .debug_struct("XMLNodeData::Element")
                .field("0", &self.0.as_ptr())
                .field(
                    "script_object",
                    &script_object
                        .map(|p| format!("{:p}", p.as_ptr()))
                        .unwrap_or_else(|| "None".to_string()),
                )
                .field("document", &"<Elided>".to_string())
                .field(
                    "parent",
                    &parent
                        .map(|p| format!("{:p}", p.0.as_ptr()))
                        .unwrap_or_else(|| "None".to_string()),
                )
                .field("tag_name", tag_name)
                .field("attributes", attributes)
                .field("children", children)
                .finish(),
            XMLNodeData::Text {
                script_object,
                contents,
                parent,
                ..
            } => f
                .debug_struct("XMLNodeData::Text")
                .field("0", &self.0.as_ptr())
                .field(
                    "script_object",
                    &script_object
                        .map(|p| format!("{:p}", p.as_ptr()))
                        .unwrap_or_else(|| "None".to_string()),
                )
                .field("document", &"<Elided>".to_string())
                .field(
                    "parent",
                    &parent
                        .map(|p| format!("{:p}", p.0.as_ptr()))
                        .unwrap_or_else(|| "None".to_string()),
                )
                .field("contents", contents)
                .finish(),
            XMLNodeData::Comment {
                script_object,
                contents,
                parent,
                ..
            } => f
                .debug_struct("XMLNodeData::Comment")
                .field("0", &self.0.as_ptr())
                .field(
                    "script_object",
                    &script_object
                        .map(|p| format!("{:p}", p.as_ptr()))
                        .unwrap_or_else(|| "None".to_string()),
                )
                .field("document", &"<Elided>".to_string())
                .field(
                    "parent",
                    &parent
                        .map(|p| format!("{:p}", p.0.as_ptr()))
                        .unwrap_or_else(|| "None".to_string()),
                )
                .field("contents", contents)
                .finish(),
            XMLNodeData::DocType {
                script_object,
                contents,
                parent,
                ..
            } => f
                .debug_struct("XMLNodeData::DocType")
                .field("0", &self.0.as_ptr())
                .field(
                    "script_object",
                    &script_object
                        .map(|p| format!("{:p}", p.as_ptr()))
                        .unwrap_or_else(|| "None".to_string()),
                )
                .field("document", &"<Elided>".to_string())
                .field(
                    "parent",
                    &parent
                        .map(|p| format!("{:p}", p.0.as_ptr()))
                        .unwrap_or_else(|| "None".to_string()),
                )
                .field("contents", contents)
                .finish(),
        }
    }
}
