/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.javascript.xmlimpl;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.mozilla.javascript.Undefined;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.ProcessingInstruction;
import org.w3c.dom.UserDataHandler;

class XmlNode implements Serializable {
    private static final String XML_NAMESPACES_NAMESPACE_URI = "http://www.w3.org/2000/xmlns/";

    private static final String USER_DATA_XMLNODE_KEY = XmlNode.class.getName();

    private static final boolean DOM_LEVEL_3 = true;

    private static XmlNode getUserData(Node node) {
        if (DOM_LEVEL_3) {
            return (XmlNode)node.getUserData(USER_DATA_XMLNODE_KEY);
        }
        return null;
    }

    private static void setUserData(Node node, XmlNode wrap) {
        if (DOM_LEVEL_3) {
            node.setUserData(USER_DATA_XMLNODE_KEY, wrap, wrap.events);
        }
    }

    private static XmlNode createImpl(Node node) {
        if (node instanceof Document) throw new IllegalArgumentException();
        XmlNode rv = null;
        if (getUserData(node) == null) {
            rv = new XmlNode();
            rv.dom = node;
            setUserData(node, rv);
        } else {
            rv = getUserData(node);
        }
        return rv;
    }

    static XmlNode newElementWithText(XmlProcessor processor, XmlNode reference, XmlNode.QName qname, String value) {
        if (reference instanceof org.w3c.dom.Document) throw new IllegalArgumentException("Cannot use Document node as reference");
        Document document = null;
        if (reference != null) {
            document = reference.dom.getOwnerDocument();
        } else {
            document = processor.newDocument();
        }
        Node referenceDom = (reference != null) ? reference.dom : null;
        Namespace ns = qname.getNamespace();
        Element e = (ns == null || ns.getUri().length() == 0)
            ? document.createElementNS(null, qname.getLocalName())
            : document.createElementNS(ns.getUri(),
                                       qname.qualify(referenceDom));
        if (value != null) {
            e.appendChild(document.createTextNode(value));
        }
        return XmlNode.createImpl(e);
    }

    static XmlNode createText(XmlProcessor processor, String value) {
        return createImpl( processor.newDocument().createTextNode(value) );
    }

    static XmlNode createElementFromNode(Node node) {
        if (node instanceof Document)
            node = ((Document) node).getDocumentElement();
        return createImpl(node);
    }

    static XmlNode createElement(XmlProcessor processor, String namespaceUri, String xml) throws org.xml.sax.SAXException {
        return createImpl( processor.toXml(namespaceUri, xml) );
    }

    static XmlNode createEmpty(XmlProcessor processor) {
        return createText(processor, "");
    }

    private static XmlNode copy(XmlNode other) {
        return createImpl( other.dom.cloneNode(true) );
    }

    private static final long serialVersionUID = 1L;

    private UserDataHandler events = new XmlNodeUserDataHandler();

    private Node dom;

    private XML xml;

    private XmlNode() {
    }

    String debug() {
        XmlProcessor raw = new XmlProcessor();
        raw.setIgnoreComments(false);
        raw.setIgnoreProcessingInstructions(false);
        raw.setIgnoreWhitespace(false);
        raw.setPrettyPrinting(false);
        return raw.ecmaToXmlString(this.dom);
    }

    @Override
    public String toString() {
        return "XmlNode: type=" + dom.getNodeType() + " dom=" + dom.toString();
    }

    XML getXml() {
        return xml;
    }

    void setXml(XML xml) {
        this.xml = xml;
    }

    int getChildCount() {
        return this.dom.getChildNodes().getLength();
    }

    XmlNode parent() {
        Node domParent = dom.getParentNode();
        if (domParent instanceof Document) return null;
        if (domParent == null) return null;
        return createImpl(domParent);
    }

    int getChildIndex() {
        if (this.isAttributeType()) return -1;
        if (parent() == null) return -1;
        org.w3c.dom.NodeList siblings = this.dom.getParentNode().getChildNodes();
        for (int i=0; i<siblings.getLength(); i++) {
            if (siblings.item(i) == dom) {
                return i;
            }
        }
        //    Either the parent is -1 or one of the this node's parent's children is this node.
        throw new RuntimeException("Unreachable.");
    }

    void removeChild(int index) {
        this.dom.removeChild( this.dom.getChildNodes().item(index) );
    }

    String toXmlString(XmlProcessor processor) {
        return processor.ecmaToXmlString(this.dom);
    }

    String ecmaValue() {
        //    TODO    See ECMA 357 Section 9.1
        if (isTextType()) {
            return ((org.w3c.dom.Text)dom).getData();
        } else if (isAttributeType()) {
            return ((org.w3c.dom.Attr)dom).getValue();
        } else if (isProcessingInstructionType()) {
            return ((org.w3c.dom.ProcessingInstruction)dom).getData();
        } else if (isCommentType()) {
            return ((org.w3c.dom.Comment)dom).getNodeValue();
        } else if (isElementType()) {
            throw new RuntimeException("Unimplemented ecmaValue() for elements.");
        } else {
            throw new RuntimeException("Unimplemented for node " + dom);
        }
    }

    void deleteMe() {
        if (dom instanceof Attr) {
            Attr attr = (Attr)this.dom;
            attr.getOwnerElement().getAttributes().removeNamedItemNS(attr.getNamespaceURI(), attr.getLocalName());
        } else {
            if (this.dom.getParentNode() != null) {
                this.dom.getParentNode().removeChild(this.dom);
            } else {
                //    This case can be exercised at least when executing the regression
                //    tests under https://bugzilla.mozilla.org/show_bug.cgi?id=354145
            }
        }
    }

    void normalize() {
        this.dom.normalize();
    }

    void insertChildAt(int index, XmlNode node) {
        Node parent = this.dom;
        Node child = parent.getOwnerDocument().importNode( node.dom, true );
        if (parent.getChildNodes().getLength() < index) {
            //    TODO    Check ECMA for what happens here
            throw new IllegalArgumentException("index=" + index + " length=" + parent.getChildNodes().getLength());
        }
        if (parent.getChildNodes().getLength() == index) {
            parent.appendChild(child);
        } else {
            parent.insertBefore(child, parent.getChildNodes().item(index));
        }
    }

    void insertChildrenAt(int index, XmlNode[] nodes) {
        for (int i=0; i<nodes.length; i++) {
            insertChildAt(index+i, nodes[i]);
        }
    }

    XmlNode getChild(int index) {
        Node child = dom.getChildNodes().item(index);
        return createImpl(child);
    }

    //    Helper method for XML.hasSimpleContent()
    boolean hasChildElement() {
        org.w3c.dom.NodeList nodes = this.dom.getChildNodes();
        for (int i=0; i<nodes.getLength(); i++) {
            if (nodes.item(i).getNodeType() == org.w3c.dom.Node.ELEMENT_NODE) return true;
        }
        return false;
    }

    boolean isSameNode(XmlNode other) {
        //    TODO    May need to be changed if we allow XmlNode to refer to several Node objects
        return this.dom == other.dom;
    }

    private String toUri(String ns) {
        return (ns == null) ? "" : ns;
    }

    private void addNamespaces(Namespaces rv, Element element) {
        if (element == null) throw new RuntimeException("element must not be null");
        String myDefaultNamespace = toUri(element.lookupNamespaceURI(null));
        String parentDefaultNamespace = "";
        if (element.getParentNode() != null) {
            parentDefaultNamespace = toUri(element.getParentNode().lookupNamespaceURI(null));
        }
        if (!myDefaultNamespace.equals(parentDefaultNamespace) || !(element.getParentNode() instanceof Element) ) {
            rv.declare(Namespace.create("", myDefaultNamespace));
        }
        NamedNodeMap attributes = element.getAttributes();
        for (int i=0; i<attributes.getLength(); i++) {
            Attr attr = (Attr)attributes.item(i);
            if (attr.getPrefix() != null && attr.getPrefix().equals("xmlns")) {
                rv.declare(Namespace.create(attr.getLocalName(), attr.getValue()));
            }
        }
    }

    private Namespaces getAllNamespaces() {
        Namespaces rv = new Namespaces();

        Node target = this.dom;
        if (target instanceof Attr) {
            target = ((Attr)target).getOwnerElement();
        }
        while(target != null) {
            if (target instanceof Element) {
                addNamespaces(rv, (Element)target);
            }
            target = target.getParentNode();
        }
        //    Fallback in case no namespace was declared
        rv.declare(Namespace.create("", ""));
        return rv;
    }

    Namespace[] getInScopeNamespaces() {
        Namespaces rv = getAllNamespaces();
        return rv.getNamespaces();
    }

    Namespace[] getNamespaceDeclarations() {
        //    ECMA357 13.4.4.24
        if (this.dom instanceof Element) {
            Namespaces rv = new Namespaces();
            addNamespaces( rv, (Element)this.dom );
            return rv.getNamespaces();
        } else {
            return new Namespace[0];
        }
    }

    Namespace getNamespaceDeclaration(String prefix) {
        if (prefix.equals("") && dom instanceof Attr) {
            //    Default namespaces do not apply to attributes; see XML Namespaces section 5.2
            return Namespace.create("", "");
        }
        Namespaces rv = getAllNamespaces();
        return rv.getNamespace(prefix);
    }

    Namespace getNamespaceDeclaration() {
        if (dom.getPrefix() == null) return getNamespaceDeclaration("");
        return getNamespaceDeclaration(dom.getPrefix());
    }

    static class XmlNodeUserDataHandler implements UserDataHandler, Serializable {
        private static final long serialVersionUID = 4666895518900769588L;

        public void handle(short operation, String key, Object data, Node src, Node dest) {
        }
    }

    private static class Namespaces {
        private Map<String,String> map = new HashMap<String,String>();
        private Map<String,String> uriToPrefix = new HashMap<String,String>();

        Namespaces() {
        }

        void declare(Namespace n) {
            if (map.get(n.prefix) == null) {
                map.put(n.prefix, n.uri);
            }
            //    TODO    I think this is analogous to the other way, but have not really thought it through ... should local scope
            //            matter more than outer scope?
            if (uriToPrefix.get(n.uri) == null) {
                uriToPrefix.put(n.uri, n.prefix);
            }
        }

        Namespace getNamespaceByUri(String uri) {
            if (uriToPrefix.get(uri) == null) return null;
            return Namespace.create(uri, uriToPrefix.get(uri));
        }

        Namespace getNamespace(String prefix) {
            if (map.get(prefix) == null) return null;
            return Namespace.create(prefix, map.get(prefix));
        }

        Namespace[] getNamespaces() {
            ArrayList<Namespace> rv = new ArrayList<Namespace>();
            for (String prefix: map.keySet()) {
                String uri = map.get(prefix);
                Namespace n = Namespace.create(prefix, uri);
                if (!n.isEmpty()) {
                    rv.add(n);
                }
            }
            return rv.toArray(new Namespace[rv.size()]);
        }
    }

    final XmlNode copy() {
        return copy( this );
    }

    //    Returns whether this node is capable of being a parent
    final boolean isParentType() {
        return isElementType();
    }

    final boolean isTextType() {
        return dom.getNodeType() == Node.TEXT_NODE || dom.getNodeType() == Node.CDATA_SECTION_NODE;
    }

    final boolean isAttributeType() {
        return dom.getNodeType() == Node.ATTRIBUTE_NODE;
    }

    final boolean isProcessingInstructionType() {
        return dom.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE;
    }

    final boolean isCommentType() {
        return dom.getNodeType() == Node.COMMENT_NODE;
    }

    final boolean isElementType() {
        return dom.getNodeType() == Node.ELEMENT_NODE;
    }

    final void renameNode(QName qname) {
        this.dom = dom.getOwnerDocument().renameNode(dom, qname.getNamespace().getUri(), qname.qualify(dom));
    }

    void invalidateNamespacePrefix() {
        if (!(dom instanceof Element)) throw new IllegalStateException();
        String prefix = this.dom.getPrefix();
        QName after = QName.create(this.dom.getNamespaceURI(), this.dom.getLocalName(), null);
        renameNode(after);
        NamedNodeMap attrs = this.dom.getAttributes();
        for (int i=0; i<attrs.getLength(); i++) {
            if (attrs.item(i).getPrefix().equals(prefix)) {
                createImpl( attrs.item(i) ).renameNode( QName.create(attrs.item(i).getNamespaceURI(), attrs.item(i).getLocalName(), null) );
            }
        }
    }

    private void declareNamespace(Element e, String prefix, String uri) {
        if (prefix.length() > 0) {
            e.setAttributeNS(XML_NAMESPACES_NAMESPACE_URI, "xmlns:" + prefix, uri);
        } else {
            e.setAttribute("xmlns", uri);
        }
    }

    void declareNamespace(String prefix, String uri) {
        if (!(dom instanceof Element)) throw new IllegalStateException();
        if (dom.lookupNamespaceURI(uri) != null && dom.lookupNamespaceURI(uri).equals(prefix)) {
            //    do nothing
        } else {
            Element e = (Element)dom;
            declareNamespace(e, prefix, uri);
        }
    }

    private Namespace getDefaultNamespace() {
        String prefix = "";
        String uri = (dom.lookupNamespaceURI(null) == null) ? "" : dom.lookupNamespaceURI(null);
        return Namespace.create(prefix, uri);
    }

    private String getExistingPrefixFor(Namespace namespace) {
        if (getDefaultNamespace().getUri().equals(namespace.getUri())) {
            return "";
        }
        return dom.lookupPrefix(namespace.getUri());
    }

    private Namespace getNodeNamespace() {
        String uri = dom.getNamespaceURI();
        String prefix = dom.getPrefix();
        if (uri == null) uri = "";
        if (prefix == null) prefix = "";
        return Namespace.create(prefix, uri);
    }

    Namespace getNamespace() {
        return getNodeNamespace();
    }

    void removeNamespace(Namespace namespace) {
        Namespace current = getNodeNamespace();

        //    Do not remove in-use namespace
        if (namespace.is(current)) return;
        NamedNodeMap attrs = this.dom.getAttributes();
        for (int i=0; i<attrs.getLength(); i++) {
            XmlNode attr = XmlNode.createImpl(attrs.item(i));
            if (namespace.is(attr.getNodeNamespace())) return;
        }

        //    TODO    I must confess I am not sure I understand the spec fully.  See ECMA357 13.4.4.31
        String existingPrefix = getExistingPrefixFor(namespace);
        if (existingPrefix != null) {
            if (namespace.isUnspecifiedPrefix()) {
                //    we should remove any namespace with this URI from scope; we do this by declaring a namespace with the same
                //    prefix as the existing prefix and setting its URI to the default namespace
                declareNamespace(existingPrefix, getDefaultNamespace().getUri());
            } else {
                if (existingPrefix.equals(namespace.getPrefix())) {
                    declareNamespace(existingPrefix, getDefaultNamespace().getUri());
                }
            }
        } else {
            //    the argument namespace is not declared in this scope, so do nothing.
        }
    }

    private void setProcessingInstructionName(String localName) {
        org.w3c.dom.ProcessingInstruction pi = (ProcessingInstruction)this.dom;
        //    We cannot set the node name; Document.renameNode() only supports elements and attributes.  So we replace it
        pi.getParentNode().replaceChild(
            pi,
            pi.getOwnerDocument().createProcessingInstruction(localName, pi.getData())
        );
    }

    final void setLocalName(String localName) {
        if (dom instanceof ProcessingInstruction) {
            setProcessingInstructionName(localName);
        } else {
            String prefix = dom.getPrefix();
            if (prefix == null) prefix = "";
            this.dom = dom.getOwnerDocument().renameNode(dom, dom.getNamespaceURI(), QName.qualify(prefix, localName));
        }
    }

    final QName getQname() {
        String uri = (dom.getNamespaceURI()) == null ? "" : dom.getNamespaceURI();
        String prefix = (dom.getPrefix() == null) ? "" : dom.getPrefix();
        return QName.create( uri, dom.getLocalName(), prefix );
    }

    void addMatchingChildren(XMLList result, XmlNode.Filter filter) {
        Node node = this.dom;
        NodeList children = node.getChildNodes();
        for(int i=0; i<children.getLength(); i++) {
            Node childnode = children.item(i);
            XmlNode child = XmlNode.createImpl(childnode);
            if (filter.accept(childnode)) {
                result.addToList(child);
            }
        }
    }

    XmlNode[] getMatchingChildren(Filter filter) {
        ArrayList<XmlNode> rv = new ArrayList<XmlNode>();
        NodeList nodes = this.dom.getChildNodes();
        for (int i=0; i<nodes.getLength(); i++) {
            Node node = nodes.item(i);
            if (filter.accept(node)) {
                rv.add(createImpl(node));
            }
        }
        return rv.toArray(new XmlNode[rv.size()]);
    }

    XmlNode[] getAttributes() {
        NamedNodeMap attrs = this.dom.getAttributes();
        //    TODO    Or could make callers handle null?
        if (attrs == null) throw new IllegalStateException("Must be element.");
        XmlNode[] rv = new XmlNode[attrs.getLength()];
        for (int i=0; i<attrs.getLength(); i++) {
            rv[i] = createImpl( attrs.item(i) );
        }
        return rv;
    }

    String getAttributeValue() {
        return ((Attr)dom).getValue();
    }

    void setAttribute(QName name, String value) {
        if (!(dom instanceof Element)) throw new IllegalStateException("Can only set attribute on elements.");
        name.setAttribute( (Element)dom, value );
    }

    void replaceWith(XmlNode other) {
        Node replacement = other.dom;
        if (replacement.getOwnerDocument() != this.dom.getOwnerDocument()) {
            replacement = this.dom.getOwnerDocument().importNode(replacement, true);
        }
        this.dom.getParentNode().replaceChild(replacement, this.dom);
    }

    String ecmaToXMLString(XmlProcessor processor) {
        if (this.isElementType()) {
            Element copy = (Element)this.dom.cloneNode(true);
            Namespace[] inScope = this.getInScopeNamespaces();
            for (int i=0; i<inScope.length; i++) {
                declareNamespace(copy, inScope[i].getPrefix(), inScope[i].getUri());
            }
            return processor.ecmaToXmlString(copy);
        } else {
            return processor.ecmaToXmlString(dom);
        }
    }

    static class Namespace implements Serializable {

        /**
         * Serial version id for Namespace with fields prefix and uri
         */
        private static final long serialVersionUID = 4073904386884677090L;

        static Namespace create(String prefix, String uri) {
            if (prefix == null) {
                throw new IllegalArgumentException(
                        "Empty string represents default namespace prefix");
            }
            if (uri == null) {
                throw new IllegalArgumentException(
                        "Namespace may not lack a URI");
            }
            Namespace rv = new Namespace();
            rv.prefix = prefix;
            rv.uri = uri;
            return rv;
        }

        static Namespace create(String uri) {
            Namespace rv = new Namespace();
            rv.uri = uri;

            // Avoid null prefix for "" namespace
            if (uri == null || uri.length() == 0) {
                rv.prefix = "";
            }

            return rv;
        }

        static final Namespace GLOBAL = create("", "");

        private String prefix;
        private String uri;

        private Namespace() {
        }

        @Override
        public String toString() {
            if (prefix == null) return "XmlNode.Namespace [" + uri + "]";
            return "XmlNode.Namespace [" + prefix + "{" + uri + "}]";
        }

        boolean isUnspecifiedPrefix() {
            return prefix == null;
        }

        boolean is(Namespace other) {
            return this.prefix != null && other.prefix != null && this.prefix.equals(other.prefix) && this.uri.equals(other.uri);
        }

        boolean isEmpty() {
            return prefix != null && prefix.equals("") && uri.equals("");
        }

        boolean isDefault() {
            return prefix != null && prefix.equals("");
        }

        boolean isGlobal() {
            return uri != null && uri.equals("");
        }

        //    Called by QName
        //    TODO    Move functionality from QName lookupPrefix to here
        private void setPrefix(String prefix) {
            if (prefix == null) throw new IllegalArgumentException();
            this.prefix = prefix;
        }

        String getPrefix() {
            return prefix;
        }

        String getUri() {
            return uri;
        }
    }

    //    TODO    Where is this class used?  No longer using it in QName implementation
    static class QName implements Serializable {
        private static final long serialVersionUID = -6587069811691451077L;

        static QName create(Namespace namespace, String localName) {
            //    A null namespace indicates a wild-card match for any namespace
            //    A null localName indicates "*" from the point of view of ECMA357
            if (localName != null && localName.equals("*")) throw new RuntimeException("* is not valid localName");
            QName rv = new QName();
            rv.namespace = namespace;
            rv.localName = localName;
            return rv;
        }

        /** @deprecated */
        @Deprecated
        static QName create(String uri, String localName, String prefix) {
            return create(Namespace.create(prefix, uri), localName);
        }

        static String qualify(String prefix, String localName) {
            if (prefix == null) throw new IllegalArgumentException("prefix must not be null");
            if (prefix.length() > 0) return prefix + ":" + localName;
            return localName;
        }

        private Namespace namespace;
        private String localName;

        private QName() {
        }

        @Override
        public String toString() {
            return "XmlNode.QName [" + localName + "," + namespace + "]";
        }

        private boolean equals(String one, String two) {
            if (one == null && two == null) return true;
            if (one == null || two == null) return false;
            return one.equals(two);
        }

        private boolean namespacesEqual(Namespace one, Namespace two) {
            if (one == null && two == null) return true;
            if (one == null || two == null) return false;
            return equals(one.getUri(), two.getUri());
        }

        final boolean equals(QName other) {
            if (!namespacesEqual(this.namespace, other.namespace)) return false;
            if (!equals(this.localName, other.localName)) return false;
            return true;
        }

        @Override
        public boolean equals(Object obj) {
            if(!(obj instanceof QName)) {
                return false;
            }
            return equals((QName)obj);
        }

        @Override
        public int hashCode() {
            return localName == null ? 0 : localName.hashCode();
        }

        void lookupPrefix(org.w3c.dom.Node node) {
            if (node == null) throw new IllegalArgumentException("node must not be null");
            String prefix = node.lookupPrefix(namespace.getUri());
            if (prefix == null) {
                //    check to see if we match the default namespace
                String defaultNamespace = node.lookupNamespaceURI(null);
                if (defaultNamespace == null) defaultNamespace = "";
                String nodeNamespace = namespace.getUri();
                if (nodeNamespace.equals(defaultNamespace)) {
                    prefix = "";
                }
            }
            int i = 0;
            while(prefix == null) {
                String generatedPrefix = "e4x_" + i++;
                String generatedUri = node.lookupNamespaceURI(generatedPrefix);
                if (generatedUri == null) {
                    prefix = generatedPrefix;
                    org.w3c.dom.Node top = node;
                    while(top.getParentNode() != null && top.getParentNode() instanceof org.w3c.dom.Element) {
                        top = top.getParentNode();
                    }
                    ((org.w3c.dom.Element)top).setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:" + prefix, namespace.getUri());
                }
            }
            namespace.setPrefix(prefix);
        }

        String qualify(org.w3c.dom.Node node) {
            if (namespace.getPrefix() == null) {
                if (node != null) {
                    lookupPrefix(node);
                } else {
                    if (namespace.getUri().equals("")) {
                        namespace.setPrefix("");
                    } else {
                        //    TODO    I am not sure this is right, but if we are creating a standalone node, I think we can set the
                        //            default namespace on the node itself and not worry about setting a prefix for that namespace.
                        namespace.setPrefix("");
                    }
                }
            }
            return qualify(namespace.getPrefix(), localName);
        }

        void setAttribute(org.w3c.dom.Element element, String value) {
            if (namespace.getPrefix() == null) lookupPrefix(element);
            element.setAttributeNS(namespace.getUri(), qualify(namespace.getPrefix(), localName), value);
        }

        Namespace getNamespace() {
            return namespace;
        }

        String getLocalName() {
            return localName;
        }
    }

    static class InternalList implements Serializable {
        private static final long serialVersionUID = -3633151157292048978L;
        private List<XmlNode> list;

        InternalList() {
            list = new ArrayList<XmlNode>();
        }

        private void _add(XmlNode n) {
            list.add(n);
        }

        XmlNode item(int index) {
            return list.get(index);
        }

        void remove(int index) {
            list.remove(index);
        }

        void add(InternalList other) {
            for (int i=0; i<other.length(); i++) {
                _add(other.item(i));
            }
        }

        void add(InternalList from, int startInclusive, int endExclusive) {
            for (int i=startInclusive; i<endExclusive; i++) {
                _add(from.item(i));
            }
        }

        void add(XmlNode node) {
            _add(node);
        }

        /* TODO: was marked deprecated by original author */
        void add(XML xml) {
            _add(xml.getAnnotation());
        }

        /* TODO: was marked deprecated by original author */
        void addToList(Object toAdd) {
            if (toAdd instanceof Undefined) {
                // Missing argument do nothing...
                return;
            }

            if (toAdd instanceof XMLList) {
                XMLList xmlSrc = (XMLList)toAdd;
                for (int i = 0; i < xmlSrc.length(); i++) {
                    this._add((xmlSrc.item(i)).getAnnotation());
                }
            } else if (toAdd instanceof XML) {
                this._add(((XML)(toAdd)).getAnnotation());
            } else if (toAdd instanceof XmlNode) {
                this._add((XmlNode)toAdd);
            }
        }

        int length() {
            return list.size();
        }
    }

    static abstract class Filter {
        static final Filter COMMENT = new Filter() {
            @Override
            boolean accept(Node node) {
                return node.getNodeType() == Node.COMMENT_NODE;
            }
        };
        static final Filter TEXT = new Filter() {
            @Override
            boolean accept(Node node) {
                return node.getNodeType() == Node.TEXT_NODE;
            }
        };
        static Filter PROCESSING_INSTRUCTION(final XMLName name) {
            return new Filter() {
                @Override
                boolean accept(Node node) {
                    if (node.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE) {
                        ProcessingInstruction pi = (ProcessingInstruction)node;
                        return name.matchesLocalName(pi.getTarget());
                    }
                    return false;
                }
            };
        }
        static Filter ELEMENT = new Filter() {
            @Override
            boolean accept(Node node) {
                return node.getNodeType() == Node.ELEMENT_NODE;
            }
        };
        static Filter TRUE = new Filter() {
            @Override
            boolean accept(Node node) {
                return true;
            }
        };
        abstract boolean accept(Node node);
    }

    //    Support experimental Java interface
    org.w3c.dom.Node toDomNode() {
        return this.dom;
    }
}
