/**
 * Copyright (C) 2010 Orbeon, Inc.
 *
 * This program is free software; you can redistribute it and/or modify it under the terms of the
 * GNU Lesser General Public License as published by the Free Software Foundation; either version
 * 2.1 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 *
 * The full text of the license is available at http://www.gnu.org/copyleft/lesser.html
 */
package org.orbeon.saxon.dom4j;

import org.dom4j.*;
import org.orbeon.saxon.Configuration;
import org.orbeon.saxon.event.Receiver;
import org.orbeon.saxon.om.*;
import org.orbeon.saxon.pattern.AnyNodeTest;
import org.orbeon.saxon.pattern.NodeTest;
import org.orbeon.saxon.trans.XPathException;
import org.orbeon.saxon.type.Type;
import org.orbeon.saxon.value.AtomicValue;
import org.orbeon.saxon.value.StringValue;
import org.orbeon.saxon.value.UntypedAtomicValue;
import org.orbeon.saxon.value.Value;

import java.util.*;
import java.util.ListIterator;

/**
  * A node in the XML parse tree representing an XML element, character content, or attribute.<P>
  * This is the implementation of the NodeInfo interface used as a wrapper for DOM4J nodes.
  * @author Michael H. Kay
  */

  // History: this started life as the NodeWrapper for JDOM nodes; it was then modified by the
  // Orbeon team to act as a wrapper for DOM4J nodes, and was shipped with the Orbeon product;
  // it has now been absorbed back into Saxon.

public class NodeWrapper implements NodeInfo, VirtualNode, SiblingCountingNode {

    protected Object node;
    protected short nodeKind;
    private NodeWrapper parent;     // null means unknown
    protected DocumentWrapper docWrapper;
    // Beware: with dom4j, this is an index over the result of content(), which may contain Namespace nodes
    protected int index;            // -1 means unknown

    /**
     * This constructor is protected: nodes should be created using the wrap
     * factory method on the DocumentWrapper class
     * @param node    The DOM4J node to be wrapped
     * @param parent  The NodeWrapper that wraps the parent of this node
     * @param index   Position of this node among its siblings
     */
    protected NodeWrapper(Object node, NodeWrapper parent, int index) {
        this.node = node;
        this.parent = parent;
        this.index = index;
    }

    /**
     * Factory method to wrap a DOM4J node with a wrapper that implements the Saxon
     * NodeInfo interface.
     * @param node        The DOM4J node
     * @param docWrapper  The wrapper for the Document containing this node
     * @return            The new wrapper for the supplied node
     */
    protected NodeWrapper makeWrapper(Object node, DocumentWrapper docWrapper) {
        return makeWrapper(node, docWrapper, null, -1);
    }

    /**
     * Factory method to wrap a DOM4J node with a wrapper that implements the Saxon
     * NodeInfo interface.
     * @param node        The DOM4J node
     * @param docWrapper  The wrapper for the Document containing this node
     * @param parent      The wrapper for the parent of the DOM4J node
     * @param index       The position of this node relative to its siblings
     * @return            The new wrapper for the supplied node
     */
    protected NodeWrapper makeWrapper(Object node, DocumentWrapper docWrapper,
                                      NodeWrapper parent, int index) {
        return makeWrapperImpl(node, docWrapper, parent, index);
    }

    protected static NodeWrapper makeWrapperImpl(Object node, DocumentWrapper docWrapper,
                                      NodeWrapper parent, int index) {

        NodeWrapper wrapper;
        final Node dom4jNode = (Node) node;
        switch (dom4jNode.getNodeType()) {
            case Type.DOCUMENT:
                return docWrapper;
            case Type.ELEMENT:
            case Type.ATTRIBUTE:
            case Type.COMMENT:
            case Type.PROCESSING_INSTRUCTION:
            case Type.NAMESPACE:
            case Type.TEXT:
                wrapper = new NodeWrapper(node, parent, index);
                wrapper.nodeKind = dom4jNode.getNodeType();
                break;
            case 4: // dom4j CDATA
                wrapper = new NodeWrapper(node, parent, index);
                wrapper.nodeKind = Type.TEXT;
                break;
            default :
               throw new IllegalArgumentException("Bad node type in dom4j: " + node.getClass() + " instance " + node.toString());
        }

        wrapper.docWrapper = docWrapper;
        return wrapper;
    }

    /**
    * Get the underlying DOM node, to implement the VirtualNode interface
    */

    public Object getUnderlyingNode() {
        return node;
    }


    /**
     * Get the name pool for this node
     * @return the NamePool
     */

    public NamePool getNamePool() {
        return docWrapper.getNamePool();
    }

    /**
    * Return the type of node.
    * @return one of the values Node.ELEMENT, Node.TEXT, Node.ATTRIBUTE, etc.
    */
    public int getNodeKind() {
        return nodeKind;
    }

    /**
    * Get the typed value of the item
    */
    public SequenceIterator getTypedValue() throws XPathException {
        return SingletonIterator.makeIterator((AtomicValue)atomize());
    }

    public Value atomize() throws XPathException {
        switch (getNodeKind()) {
            case Type.COMMENT:
            case Type.PROCESSING_INSTRUCTION:
                return new StringValue(getStringValueCS());
            default:
                return new UntypedAtomicValue(getStringValueCS());
        }
    }

    /**
    * Get the type annotation
    * @return UNTYPED or UNTYPED_ATOMIC
    */
    public int getTypeAnnotation() {
        if (getNodeKind() == Type.ATTRIBUTE) {
            return StandardNames.XS_UNTYPED_ATOMIC;
        }
        return StandardNames.XS_UNTYPED;
    }

    /**
    * Determine whether this is the same node as another node. <br />
    * Note: a.isSameNode(b) if and only if generateId(a)==generateId(b)
     * @param other the node to be compared with
    * @return true if this Node object and the supplied Node object represent the
    * same node in the tree.
    */

    public boolean isSameNode(NodeInfo other) {
        if (!(other instanceof NodeWrapper)) {
            return false;
        }
        NodeWrapper ow = (NodeWrapper)other;
        return node.equals(ow.node);
    }

    /**
    * Get the System ID for the node.
    * @return the System Identifier of the entity in the source document containing the node,
    * or null if not known. Note this is not the same as the base URI: the base URI can be
    * modified by xml:base, but the system ID cannot.
    */

    public String getSystemId() {
        return docWrapper.baseURI;
    }

    public void setSystemId(String uri) {
        docWrapper.baseURI = uri;
    }

    /**
    * Get the Base URI for the node, that is, the URI used for resolving a relative URI contained
     * in the node. In the DOM4J model, base URIs are held only an the document level. We don't
     * currently take any account of xml:base attributes.
    */

    public String getBaseURI() {
        if (getNodeKind() == Type.NAMESPACE) {
            return null;
        }
        NodeInfo n = this;
        if (getNodeKind() != Type.ELEMENT) {
            n = n.getParent();
        }
        // Look for an xml:base attribute
        while (n != null) {
            String xmlbase = n.getAttributeValue(StandardNames.XML_BASE);
            if (xmlbase != null) {
                return xmlbase;
            }
            n = n.getParent();
        }
        // if not found, return the base URI of the document node
        return docWrapper.baseURI;
    }

    /**
    * Get line number
    * @return the line number of the node in its original source document; or -1 if not available
    */

    public int getLineNumber() {
        return -1;
    }

    /**
     * Get column number
     * @return the column number of the node in its original source document; or -1 if not available
     */

    public int getColumnNumber() {
        return -1;
    }

    /**
    * Determine the relative position of this node and another node, in document order.
    * The other node will always be in the same document.
    * @param other The other node, whose position is to be compared with this node
    * @return -1 if this node precedes the other node, +1 if it follows the other
    * node, or 0 if they are the same node. (In this case, isSameNode() will always
    * return true, and the two nodes will produce the same result for generateId())
    */

    public int compareOrder(NodeInfo other) {
        return Navigator.compareOrder(this, (SiblingCountingNode)other);
    }

    /**
    * Return the string value of the node. The interpretation of this depends on the type
    * of node. For an element it is the accumulated character content of the element,
    * including descendant elements.
    * @return the string value of the node
    */

    public String getStringValue() {
        return getStringValue(node);
    }

    public CharSequence getStringValueCS() {
        return getStringValue(node);
    }

    private static String getStringValue(Object node) {
        if (node instanceof Document) {
            return ((Document)node).getStringValue();
        } else if (node instanceof Element) {
            return ((Element)node).getStringValue();
        } else if (node instanceof Attribute) {
                return ((Attribute)node).getValue();
        } else if (node instanceof Text) {
            return ((Text)node).getText();
        } else if (node instanceof CDATA) {
            return ((CDATA)node).getText();
        } else if (node instanceof String) {
            return (String)node;
        } else if (node instanceof Comment) {
            return ((Comment)node).getText();
        } else if (node instanceof ProcessingInstruction) {
            return ((ProcessingInstruction)node).getStringValue();
        } else if (node instanceof Namespace) {
            return ((Namespace)node).getURI();
        } else {
            return "";
        }
    }

    /**
    * Get name code. The name code is a coded form of the node name: two nodes
    * with the same name code have the same namespace URI, the same local name,
    * and the same prefix. By masking the name code with &0xfffff, you get a
    * fingerprint: two nodes with the same fingerprint have the same local name
    * and namespace URI.
    * @see org.orbeon.saxon.om.NamePool#allocate allocate
    */

    public int getNameCode() {
        switch (nodeKind) {
            case Type.ELEMENT:
            case Type.ATTRIBUTE:
            case Type.PROCESSING_INSTRUCTION:
            case Type.NAMESPACE:
                return docWrapper.getNamePool().allocate(getPrefix(),
                                                    getURI(),
                                                    getLocalPart());
            default:
                return -1;
        }
    }

    /**
    * Get fingerprint. The fingerprint is a coded form of the expanded name
    * of the node: two nodes
    * with the same name code have the same namespace URI and the same local name.
    * A fingerprint of -1 should be returned for a node with no name.
    */

    public int getFingerprint() {
        return getNameCode()&0xfffff;
    }

    /**
    * Get the local part of the name of this node. This is the name after the ":" if any.
    * @return the local part of the name. For an unnamed node, returns "".
    */

    public String getLocalPart() {
        switch (nodeKind) {
            case Type.ELEMENT:
                return ((Element)node).getName();
            case Type.ATTRIBUTE:
                return ((Attribute)node).getName();
            case Type.TEXT:
            case Type.COMMENT:
            case Type.DOCUMENT:
                return "";
            case Type.PROCESSING_INSTRUCTION:
                return ((ProcessingInstruction)node).getTarget();
            case Type.NAMESPACE:
                return ((Namespace)node).getPrefix();
            default:
                return null;
        }
    }

    /**
    * Get the prefix part of the name of this node. This is the name before the ":" if any.
     * (Note, this method isn't required as part of the NodeInfo interface.)
    * @return the prefix part of the name. For an unnamed node, return an empty string.
    */

    public String getPrefix() {
        switch (nodeKind) {
            case Type.ELEMENT:
                return ((Element)node).getNamespacePrefix();
            case Type.ATTRIBUTE:
                return ((Attribute)node).getNamespacePrefix();
            default:
                return "";
        }
    }

    /**
    * Get the URI part of the name of this node. This is the URI corresponding to the
    * prefix, or the URI of the default namespace if appropriate.
    * @return The URI of the namespace of this node. For an unnamed node, return null.
    * For a node with an empty prefix, return an empty string.
    */

    public String getURI() {
        switch (nodeKind) {
            case Type.ELEMENT:
                return ((Element)node).getNamespaceURI();
            case Type.ATTRIBUTE:
                return ((Attribute)node).getNamespaceURI();
            default:
                return "";
        }
    }

    /**
    * Get the display name of this node. For elements and attributes this is [prefix:]localname.
    * For unnamed nodes, it is an empty string.
    * @return The display name of this node.
    * For a node with no name, return an empty string.
    */

    public String getDisplayName() {
        switch (nodeKind) {
            case Type.ELEMENT:
                return ((Element)node).getQualifiedName();
            case Type.ATTRIBUTE:
                return ((Attribute)node).getQualifiedName();
            case Type.PROCESSING_INSTRUCTION:
            case Type.NAMESPACE:
                return getLocalPart();
            default:
                return "";

        }
    }

    /**
    * Get the NodeInfo object representing the parent of this node
    */

    public NodeInfo getParent() {
        if (parent==null) {
            if (node instanceof Element) {
                if (((Element)node).isRootElement()) {
                    parent = makeWrapper(((Element) node).getDocument(), docWrapper);
                } else {
                    final Node parentNode = ((Element)node).getParent();
                    // This checks the case of an element detached from a Document
                    if (parentNode != null)
                        parent = makeWrapper(parentNode, docWrapper);
                }
            } else if (node instanceof Text) {
                parent = makeWrapper(((Text) node).getParent(), docWrapper);
            } else if (node instanceof CDATA) {
                parent = makeWrapper(((CDATA) node).getParent(), docWrapper);
            } else if (node instanceof Comment) {
                parent = makeWrapper(((Comment) node).getParent(), docWrapper);
            } else if (node instanceof ProcessingInstruction) {
                parent = makeWrapper(((ProcessingInstruction) node).getParent(), docWrapper);
            } else if (node instanceof Attribute) {
                parent = makeWrapper(((Attribute) node).getParent(), docWrapper);
            } else if (node instanceof Document) {
                parent = null;
            } else if (node instanceof Namespace) {
                throw new UnsupportedOperationException("Cannot find parent of DOM4J namespace node");
            } else {
                throw new IllegalStateException("Unknown DOM4J node type " + node.getClass());
            }
        }
        return parent;
    }

    /**
     * Get the index position of this node among its siblings (starting from 0)
     */
    public int getSiblingPosition() {
        if (index == -1) {
            int ix = 0;
            getParent();
            AxisIterator iter;
            switch (nodeKind) {
                case Type.ELEMENT:
                case Type.TEXT:
                case Type.COMMENT:
                case Type.PROCESSING_INSTRUCTION:
                    {
                        final NodeWrapper parent = (NodeWrapper) getParent();
                        final List children;
                        if (parent.getNodeKind()==Type.DOCUMENT) {
                            // This is an attempt to work around a dom4j bug
                            final Document document = (Document) parent.node;
                            final List content = document.content();
                            if (content.size() == 0 && document.getRootElement() != null)
                                children = Collections.singletonList(document.getRootElement());
                            else
                                children = content;
                        } else {
                            // Beware: dom4j content() contains Namespace nodes (which is broken)!
                            children = ((Element) parent.node).content();
                        }
                        for (ListIterator iterator = children.listIterator(); iterator.hasNext();) {
                            final Object n = iterator.next();
                            if (n == node) {
                                index = ix;
                                return index;
                            }
                            ix++;
                        }
                        throw new IllegalStateException("DOM4J node not linked to parent node");
                    }
                case Type.ATTRIBUTE:
                    iter = parent.iterateAxis(Axis.ATTRIBUTE);
                    break;
                case Type.NAMESPACE:
                    iter = parent.iterateAxis(Axis.NAMESPACE);
                    break;
                default:
                    index = 0;
                    return index;
            }
            while (true) {
                NodeInfo n = (NodeInfo)iter.next();
                if (n == null) {
                    break;
                }
                if (n.isSameNodeInfo(this)) {
                    index = ix;
                    return index;
                }
                ix++;
            }
            throw new IllegalStateException("DOM4J node not linked to parent node");
        }
        return index;
    }

    /**
    * Return an iteration over the nodes reached by the given axis from this node
    * @param axisNumber the axis to be used
    * @return a SequenceIterator that scans the nodes reached by the axis in turn.
    */

    public AxisIterator iterateAxis(byte axisNumber) {
        return iterateAxis(axisNumber, AnyNodeTest.getInstance());
    }

    /**
    * Return an iteration over the nodes reached by the given axis from this node
    * @param axisNumber the axis to be used
    * @param nodeTest A pattern to be matched by the returned nodes
    * @return a SequenceIterator that scans the nodes reached by the axis in turn.
    */

    public AxisIterator iterateAxis(byte axisNumber, NodeTest nodeTest) {
        switch (axisNumber) {
            case Axis.ANCESTOR:
                if (nodeKind==Type.DOCUMENT) {
                    return EmptyIterator.getInstance();
                }
                return new Navigator.AxisFilter(
                            new Navigator.AncestorEnumeration(this, false),
                            nodeTest);

            case Axis.ANCESTOR_OR_SELF:
                if (nodeKind==Type.DOCUMENT) {
                    return Navigator.filteredSingleton(this, nodeTest);
                }
                return new Navigator.AxisFilter(
                            new Navigator.AncestorEnumeration(this, true),
                            nodeTest);

            case Axis.ATTRIBUTE:
                if (nodeKind!=Type.ELEMENT) return EmptyIterator.getInstance();
                return new Navigator.AxisFilter(
                            new AttributeEnumeration(this),
                            nodeTest);

            case Axis.CHILD:
                if (hasChildNodes()) {
                    return new Navigator.AxisFilter(
                            new ChildEnumeration(this, true, true),
                            nodeTest);
                } else {
                    return EmptyIterator.getInstance();
                }

            case Axis.DESCENDANT:
                if (hasChildNodes()) {
                    return new Navigator.AxisFilter(
                            new Navigator.DescendantEnumeration(this, false, true),
                            nodeTest);
                } else {
                    return EmptyIterator.getInstance();
                }

            case Axis.DESCENDANT_OR_SELF:
                 return new Navigator.AxisFilter(
                            new Navigator.DescendantEnumeration(this, true, true),
                            nodeTest);

            case Axis.FOLLOWING:
                 return new Navigator.AxisFilter(
                            new Navigator.FollowingEnumeration(this),
                            nodeTest);

            case Axis.FOLLOWING_SIBLING:
                 switch (nodeKind) {
                    case Type.DOCUMENT:
                    case Type.ATTRIBUTE:
                    case Type.NAMESPACE:
                        return EmptyIterator.getInstance();
                    default:
                        return new Navigator.AxisFilter(
                            new ChildEnumeration(this, false, true),
                            nodeTest);
                 }

            case Axis.NAMESPACE:
                 if (nodeKind!=Type.ELEMENT) {
                     return EmptyIterator.getInstance();
                 }
                 return new Navigator.AxisFilter(
                                new NamespaceEnumeration(this),
                                nodeTest);

            case Axis.PARENT:
                 getParent();
                 return Navigator.filteredSingleton(parent, nodeTest);

            case Axis.PRECEDING:
                 return new Navigator.AxisFilter(
                            new Navigator.PrecedingEnumeration(this, false),
                            nodeTest);

            case Axis.PRECEDING_SIBLING:
                 switch (nodeKind) {
                    case Type.DOCUMENT:
                    case Type.ATTRIBUTE:
                    case Type.NAMESPACE:
                        return EmptyIterator.getInstance();
                    default:
                        return new Navigator.AxisFilter(
                            new ChildEnumeration(this, false, false),
                            nodeTest);
                 }

            case Axis.SELF:
                 return Navigator.filteredSingleton(this, nodeTest);

            case Axis.PRECEDING_OR_ANCESTOR:
                 return new Navigator.AxisFilter(
                            new Navigator.PrecedingEnumeration(this, true),
                            nodeTest);

            default:
                 throw new IllegalArgumentException("Unknown axis number " + axisNumber);
        }
    }

    /**
     * Find the value of a given attribute of this node. <BR>
     * This method is defined on all nodes to meet XSL requirements, but for nodes
     * other than elements it will always return null.
     * @param uri the namespace uri of an attribute ("" if no namespace)
     * @param localName the local name of the attribute
     * @return the value of the attribute, if it exists, otherwise null
     */

//    public String getAttributeValue(String uri, String localName) {
//        if (nodeKind==Type.ELEMENT) {
//            Namespace ns = Namespace.getNamespace(uri);
//            return ((Element)node).getAttributeValue(localName, ns);
//        } else {
//            return "";
//        }
//    }

    /**
    * Get the value of a given attribute of this node
    * @param fingerprint The fingerprint of the attribute name
    * @return the attribute value if it exists or null if not
    */

    public String getAttributeValue(int fingerprint) {
        if (nodeKind==Type.ELEMENT) {
            Iterator list = ((Element)node).attributes().iterator();
            NamePool pool = docWrapper.getNamePool();
            while (list.hasNext()) {
                Attribute att = (Attribute)list.next();
                int nameCode = pool.allocate(att.getNamespacePrefix(),
                                             att.getNamespaceURI(),
                                             att.getName());
                if (fingerprint == (nameCode & 0xfffff)) {
                    return att.getValue();
                }
            }
        }
        return null;
    }

    /**
    * Get the root node - always a document node with this tree implementation
    * @return the NodeInfo representing the containing document
    */

    public NodeInfo getRoot() {
        return docWrapper;
    }

    /**
    * Get the root (document) node
    * @return the DocumentInfo representing the containing document
    */

    public DocumentInfo getDocumentRoot() {
        return docWrapper;
    }

    /**
    * Determine whether the node has any children. <br />
    * Note: the result is equivalent to <br />
    * getEnumeration(Axis.CHILD, AnyNodeTest.getInstance()).hasNext()
    */

    public boolean hasChildNodes() {
        switch (nodeKind) {
            case Type.DOCUMENT:
                return true;
            case Type.ELEMENT:
                // Beware: dom4j content() contains Namespace nodes (which is broken)!
                List content = ((Element)node).content();
                for (int i=0; i<content.size(); i++) {
                    if (!(content.get(i) instanceof Namespace)) {
                        return true;
                    }
                }
                return false;
            default:
                return false;
        }
    }
    /**
    * Get a character string that uniquely identifies this node.
    * Note: a.isSameNode(b) if and only if generateId(a)==generateId(b)
    * @param buffer a Buffer to contain a string that uniquely identifies this node, across all
    * documents
     */

    public void generateId(FastStringBuffer buffer) {
        Navigator.appendSequentialKey(this, buffer, true);
        //buffer.append(Navigator.getSequentialKey(this));
    }

    /**
     * Get the document number of the document containing this node. For a free-standing
     * orphan node, just return the hashcode.
     */

    public int getDocumentNumber() {
        // NOTE: We used to call getParent().getDocumentNumber(), but all other implementations use
        // docWrapper.getDocumentNumber() so we now harmonize with them.

        // This also has another benefit: if a node gets detached from its parent, and getParent() has not yet been
        // cached, getParent() can return null and getDocumentNumber() fails. By using docWrapper.getDocumentNumber()
        // we avoid this issue, although arguably 1) a detached node should not point back to a DocumentWrapper and 2)
        // one should not keep using a NodeInfo created to a node which is then detached.
        return docWrapper.getDocumentNumber();
    }

    /**
    * Copy this node to a given outputter (deep copy)
    */

    public void copy(Receiver out, int whichNamespaces, boolean copyAnnotations, int locationId) throws XPathException {
        Navigator.copy(this, out, docWrapper.getNamePool(), whichNamespaces, copyAnnotations, locationId);
    }

    /**
    * Output all namespace nodes associated with this element. Does nothing if
    * the node is not an element.
    * @param out The relevant outputter
    * @param includeAncestors True if namespaces declared on ancestor elements must
    * be output; false if it is known that these are already on the result tree
    */

    public void outputNamespaceNodes(Receiver out, boolean includeAncestors)
        throws XPathException {
        if (nodeKind==Type.ELEMENT) {
            NamePool pool = docWrapper.getNamePool();
            AxisIterator enm = iterateAxis(Axis.NAMESPACE);
            while (true) {
                NodeWrapper wrapper = (NodeWrapper)enm.next();
                if (wrapper == null) {
                    break;
                }
                if (!includeAncestors && !wrapper.getParent().isSameNodeInfo(this)) {
                    break;
                }
                Namespace ns = (Namespace)wrapper.node;
                int nscode = pool.allocateNamespaceCode(
                                ns.getPrefix(),
                                ns.getURI());
                out.namespace(nscode, 0);
            }
        }
    }

    /**
     * Determine whether this node has the is-id property
     *
     * @return true if the node is an ID
     */

    public boolean isId() {
        return false;
    }

    /**
     * Determine whether this node has the is-idref property
     *
     * @return true if the node is an IDREF or IDREFS element or attribute
     */

    public boolean isIdref() {
        return false;
    }

    /**
     * Determine whether the node has the is-nilled property
     *
     * @return true if the node has the is-nilled property
     */

    public boolean isNilled() {
        return false;
    }

    ///////////////////////////////////////////////////////////////////////////////
    // Axis enumeration classes
    ///////////////////////////////////////////////////////////////////////////////


    private final class AttributeEnumeration extends Navigator.BaseEnumeration {

        private Iterator atts;
        private int ix = 0;
        private NodeWrapper start;

        public AttributeEnumeration(NodeWrapper start) {
            this.start = start;
            atts = ((Element)start.node).attributes().iterator();
        }

        public void advance() {
            if (atts.hasNext()) {
                current = makeWrapper(atts.next(), docWrapper, start, ix++);
            } else {
                current = null;
            }
        }

        public SequenceIterator getAnother() {
            return new AttributeEnumeration(start);
        }

    }  // end of class AttributeEnumeration

    private final class NamespaceEnumeration extends Navigator.BaseEnumeration {

        private HashMap<String, Namespace> nslist = new HashMap<String, Namespace>();
        private Iterator<String> prefixes;
        private int ix = 0;
        private NodeWrapper start;

        public NamespaceEnumeration(NodeWrapper start) {
            this.start = start;
            NodeWrapper curr = start;

            // build the complete list of namespaces

            do {
                final Element elem = (Element)curr.node;
                final Namespace ns = elem.getNamespace();
                final String prefix = ns.getPrefix();
                final String uri = ns.getURI();

                if (! (prefix.length() == 0 && uri.length() == 0)) {
                    if (! nslist.containsKey(prefix)) {
                        nslist.put(prefix, ns);
                    }
                }

                final List<Namespace> additionalNamespaces = elem.additionalNamespaces();
                if (! additionalNamespaces.isEmpty()) {
                    for (final Namespace additionalNamespace : additionalNamespaces) {
                        if (! nslist.containsKey(additionalNamespace.getPrefix())) {
                            nslist.put(additionalNamespace.getPrefix(), additionalNamespace);
                        }
                    }
                }
                curr = (NodeWrapper) curr.getParent();
            } while (curr != null && curr.getNodeKind() == Type.ELEMENT);// NOTE: support elements detached from document

            nslist.put("xml", Namespace.XML_NAMESPACE);
            prefixes = nslist.keySet().iterator();
        }

        public void advance() {
            if (prefixes.hasNext()) {
                final String prefix = prefixes.next();
                final Namespace ns = nslist.get(prefix);
                current = makeWrapper(ns, docWrapper, start, ix++);
            } else {
                current = null;
            }
        }

        public SequenceIterator getAnother() {
            return new NamespaceEnumeration(start);
        }

        // NB: namespace nodes in the DOM4J implementation do not support all
        // XPath functions, for example namespace nodes have no parent.


    }  // end of class NamespaceEnumeration


    /**
    * The class ChildEnumeration handles not only the child axis, but also the
    * following-sibling and preceding-sibling axes. It can also iterate the children
    * of the start node in reverse order, something that is needed to support the
    * preceding and preceding-or-ancestor axes (the latter being used by xsl:number)
    */

    private final class ChildEnumeration extends Navigator.BaseEnumeration {

        private NodeWrapper start;
        private NodeWrapper commonParent;
        private ListIterator children;
        private int ix = 0;
        private boolean downwards;  // iterate children of start node (not siblings)
        private boolean forwards;   // iterate in document order (not reverse order)

        public ChildEnumeration(NodeWrapper start,
                                boolean downwards, boolean forwards) {
            this.start = start;
            this.downwards = downwards;
            this.forwards = forwards;

            if (downwards) {
                commonParent = start;
            } else {
                commonParent = (NodeWrapper)start.getParent();
            }

            if (commonParent.getNodeKind()==Type.DOCUMENT) {
                // This is an attempt to work around a dom4j bug
                final Document document = (Document)commonParent.node;
                final List content = document.content();
                if (content.size() == 0 && document.getRootElement() != null)
                    children = Collections.singletonList(document.getRootElement()).listIterator();
                else
                    children = content.listIterator();
            } else {
                children = ((Element)commonParent.node).content().listIterator();
            }

            if (downwards) {
                if (!forwards) {
                    // backwards enumeration: go to the end
                    while (children.hasNext()) {
                        children.next();
                        ix++;
                    }
                }
            } else {
                ix = start.getSiblingPosition();
                // find the start node among the list of siblings
                if (forwards) {
                    for (int i=0; i<=ix; i++) {
                        children.next();
                    }
                    ix++;
                } else {
                    for (int i=0; i<ix; i++) {
                        children.next();
                    }
                    ix--;
                }
            }
            //advance();
        }



        public void advance() {
            if (forwards) {
                if (children.hasNext()) {
                    Object nextChild = children.next();
                    if (nextChild instanceof DocumentType || nextChild instanceof Namespace) {
                        ix++; // increment anyway so that makeWrapper() passes the correct index)
                        advance();
                        return;
                    }
                    if (nextChild instanceof Entity) {
                        throw new IllegalStateException("Unexpanded entity in DOM4J tree");
                    } else {
//                        if (isAtomizing()) {
//                            current = new UntypedAtomicValue(getStringValue(node));
//                        } else {
                            current = makeWrapper(nextChild, docWrapper, commonParent, ix++);
//                        }
                    }
                } else {
                    current = null;
                }
            } else {    // backwards
                if (children.hasPrevious()) {
                    Object nextChild = children.previous();
                    if (nextChild instanceof DocumentType || nextChild instanceof Namespace) {
                        ix--; // decrement anyway so that makeWrapper() passes the correct index)
                        advance();
                        return;
                    }
                    if (nextChild instanceof Entity) {
                        throw new IllegalStateException("Unexpanded entity in DOM4J tree");
                    } else {
//                        if (isAtomizing()) {
//                            current = new UntypedAtomicValue(getStringValue(node));
//                        } else {
                            current = makeWrapper(nextChild, docWrapper, commonParent, ix--);
//                        }
                    }
                } else {
                    current = null;
                }
            }
        }

        public SequenceIterator getAnother() {
            return new ChildEnumeration(start, downwards, forwards);
        }

    } // end of class ChildEnumeration



    /**
     * Determine whether this is the same node as another node.
     * Note: a.isSameNodeInfo(b) if and only if generateId(a)==generateId(b).
     * This method has the same semantics as isSameNode() in DOM Level 3, but
     * works on Saxon NodeInfo objects rather than DOM Node objects.
     *
     * @param other the node to be compared with this node
     * @return true if this NodeInfo object and the supplied NodeInfo object represent
     *         the same node in the tree.
     */

    public boolean isSameNodeInfo(NodeInfo other) {
        if (!(other instanceof NodeWrapper)) {
            return false;
        }
        NodeWrapper ow = (NodeWrapper)other;
        if (node instanceof Namespace) {
            return getLocalPart().equals(ow.getLocalPart()) && getParent().isSameNodeInfo(ow.getParent());
        }
        return node.equals(ow.node);
    }

    public Configuration getConfiguration() {
        return docWrapper.getConfiguration();
    }

    /**
     * Get all namespace undeclarations and undeclarations defined on this element.
     *
     * @param buffer If this is non-null, and the result array fits in this buffer, then the result
     *               may overwrite the contents of this array, to avoid the cost of allocating a new array on the heap.
     * @return An array of integers representing the namespace declarations and undeclarations present on
     *         this element. For a node other than an element, return null. Otherwise, the returned array is a
     *         sequence of namespace codes, whose meaning may be interpreted by reference to the name pool. The
     *         top half word of each namespace code represents the prefix, the bottom half represents the URI.
     *         If the bottom half is zero, then this is a namespace undeclaration rather than a declaration.
     *         The XML namespace is never included in the list. If the supplied array is larger than required,
     *         then the first unused entry will be set to -1.
     *         <p/>
     *         <p>For a node other than an element, the method returns null.</p>
     */
    public int[] getDeclaredNamespaces(int[] buffer) {
        if (node instanceof Element) {
            final Element elem = (Element) node;
            final List namespaces = elem.declaredNamespaces();

            if (namespaces == null || namespaces.isEmpty()) {
                return EMPTY_NAMESPACE_LIST;
            }
            final int count = namespaces.size();
            if (count == 0) {
                return EMPTY_NAMESPACE_LIST;
            } else {
                int[] result = (buffer == null || count > buffer.length ? new int[count] : buffer);
                NamePool pool = getNamePool();
                int n = 0;
                for (Iterator i = namespaces.iterator(); i.hasNext();) {
                    final Namespace namespace = (Namespace) i.next();
                    final String prefix = namespace.getPrefix();
                    final String uri = namespace.getURI();

                    result[n++] = pool.allocateNamespaceCode(prefix, uri);
                }
                if (count < result.length) {
                    result[count] = -1;
                }
                return result;
            }
        } else {
            return null;
        }
    }

    /**
      * The equals() method compares nodes for identity. It is defined to give the same result
      * as isSameNodeInfo().
      * @param other the node to be compared with this node
      * @return true if this NodeInfo object and the supplied NodeInfo object represent
      *      the same node in the tree.
      * @since 8.7 Previously, the effect of the equals() method was not defined. Callers
      * should therefore be aware that third party implementations of the NodeInfo interface may
      * not implement the correct semantics. It is safer to use isSameNodeInfo() for this reason.
      * The equals() method has been defined because it is useful in contexts such as a Java Set or HashMap.
      */

     public boolean equals(Object other) {
        if (other instanceof NodeInfo) {
            return isSameNodeInfo((NodeInfo)other);
        } else {
            return false;
        }
    }

    /**
      * The hashCode() method obeys the contract for hashCode(): that is, if two objects are equal
      * (represent the same node) then they must have the same hashCode()
      * @since 8.7 Previously, the effect of the equals() and hashCode() methods was not defined. Callers
      * should therefore be aware that third party implementations of the NodeInfo interface may
      * not implement the correct semantics.
      */

     public int hashCode() {
         return node.hashCode();
     }
}

