package nu.xom;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.jaxen.JaxenException;
import org.jaxen.XPath;

/**
 * TODO 
 */ 
class XPathHelper {

    // ???? how does document order work with
    // nodes selected from multiple documents
    // document(url1) | document(url2) ?
    
    /**
     * <p>
     * Returns the nodes selected by the XPath expression in the 
     * context of this node in document order as defined in XSLT. 
     * All namespace prefixes used in the 
     * expression should be bound to namespace URIs by the 
     * second argument. 
     * </p>
     * 
     * <p>
     * Note that XPath expressions operate on the XPath data model,
     * not the XOM data model. XPath counts all adjacent 
     * <code>Text</code> objects as a single text node, and does not
     * consider empty <code>Text</code> objects. For instance, an 
     * element that has exactly three text children in XOM, will
     * have exactly one text child in XPath, whose value is the 
     * concatenation of all three XOM <code>Text</code> objects. 
     * </p>
     * 
     * <p>
     * You can use XPath expressions that use the namespace axis.
     * However, namespace nodes are never returned. If an XPath 
     * expression only selects namespace nodes, then this method will
     * return an empty list.
     * </p>
     * 
     * <p>
     * No variables are bound. 
     * </p>
     * 
     * <p>
     * The context position is the index of this node among its parents
     * children, counting adjacent text nodes as one. The context size 
     * is the number of children this node's parent has, again counting
     * adjacent text nodes as one node. If the parent is a 
     * <code>Document</code>, then the <code>DocType</code> (if any) is 
     * not counted. If the node has no parent, then the context position
     * is 1, and the context size is 1. 
     * </p>
     * 
     * <p>
     * Queries such as /&#x2A;, //, and /&#x2A;//p that refer to the 
     * root node do work when operating with a context node that is not 
     * part of a document. However, the query / (return the root node)
     * throws an <code>XPathException</code> when applied to a node
     * that is not part of the document. Furthermore the top-level 
     * node in the tree is treated as the first and only child of the 
     * root node, not as the root node itself. For instance, this
     * query stores <code>parent</code> in the <code>result</code>
     * variable, not <code>child</code>:
     * </p>
     * 
     * <pre><code>  Element parent = new Element("parent");
     *   Element child = new Element("child");
     *   parent.appendChild(child);
     *   Nodes results = child.query("/*");
     *   Node result = result.get(0);</code></pre>
     * 
     * @param xpath the XPath expression to evaluate
     * @param namespaces a collection of namespace prefix bindings  
     *     used in the XPath expression
     * 
     * @return a list of all matched nodes; possibly empty
     * 
     * @throws XPathException if there's a syntax error in the 
     *     expression, the query returns something other than
     *     a node-set, or the query returns a node-set containing
     *     a namespace node
     * 
     */
    public static final Nodes query(Node node, String xpath, XPathContext namespaces) {
        
        if (node.isDocType()) {
            throw new XPathException("Can't use XPath on a DocType");
        }
        DocumentFragment frag = null;
        
        Node root = node.getRoot();
        if (! root.isDocument()) {
            frag = new DocumentFragment();
            frag.appendChild(root);
        }
        
        try {
            XPath xp = new JaxenConnector(xpath);
            if (namespaces != null) {
                xp.setNamespaceContext(namespaces.getJaxenContext());
            }
            List results = xp.selectNodes(node);
            List namespaceList = new ArrayList();
            Iterator iterator = results.iterator();
            while (iterator.hasNext()) {
                Object o = iterator.next();
                if (o instanceof DocumentFragment) {
                    iterator.remove();
                    // Want to allow // and //* and so forth
                    // but not / for rootless documents
                    if (results.isEmpty()) {
                        throw new XPathException("Tried to get document "
                          + "node of disconnected subtree");
                    }
                }
                else if (o instanceof Node) {
                    Node n = (Node) o;
                    if (n.isNamespace()) {
                        namespaceList.add(n);
                    }
                }
                else {
                    throw new XPathException("XPath expression " 
                      + xpath + " did not return a node-set.");
                }
            }
            
            return sortResults(node, results, namespaceList);
        }
        catch (XPathException ex) {
            ex.setXPath(xpath);
            throw ex;
        }
        catch (JaxenException ex) {
            XPathException xpe = new XPathException("XPath error: " + ex.getMessage(), ex);
            xpe.setXPath(xpath);
            throw xpe;
        }
        catch (RuntimeException ex) {
            XPathException xpe = new XPathException("XPath error: " + ex.getMessage(), ex);
            xpe.setXPath(xpath);
            throw xpe;
        }
        finally {
            if (frag != null) frag.removeChild(0);
        }
        
    }


    // recursively descend through document; in document
    // order, and add results as they are found
    private static Nodes sortResults(Node node, List in, List namespaces) {

        Node root = node.getRoot();
        if (in.size() > 1 && root instanceof ParentNode) {
            Nodes out = new Nodes();
            process(in, namespaces, out, (ParentNode) root);
            return out;
        }
        else {
            return new Nodes(in);
        }
    }


    private static void process(List in, List namespaces, Nodes out, ParentNode parent) {

        if (in.isEmpty()) return;
        if (in.contains(parent)) {
            out.append(parent);
            in.remove(parent);
            if (in.isEmpty()) return;
        }
        
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            Node child = parent.getChild(i);
            if (child.isElement()) {
                Element element = (Element) child;
                if (in.contains(element)) {
                    out.append(element);
                    in.remove(element);
                }
                // attach namespaces
                if (!namespaces.isEmpty()) {
                    Iterator iterator = in.iterator();
                    while (iterator.hasNext()) {
                        Object o = iterator.next();
                        if (o instanceof Namespace) {
                            Namespace n = (Namespace) o;
                            if (element == n.getParent()) {
                                out.append(n);
                                iterator.remove();
                            }
                        }
                    }
                }
                
                // attach attributes
                for (int a = 0; a < element.getAttributeCount(); a++) {
                    Attribute att = element.getAttribute(a);
                    if (in.contains(att)) {
                        out.append(att);
                        in.remove(att);
                        if (in.isEmpty()) return;
                    }
                }
                process(in, namespaces, out, element);
            }
            else {
                if (in.contains(child)) {
                    out.append(child);
                    in.remove(child);
                    if (in.isEmpty()) return;
                }
            }
        }
        
    }


    /**
     * <p>
     * Returns the nodes selected by the XPath expression in the 
     * context of this node in document order as defined by XSLT. 
     * This XPath expression must not contain
     * any namespace prefixes.
     * </p>
     * 
     * <p>
     * No variables are bound. No namespace prefixes are bound.
     * </p>
     * 
     * @param xpath the XPath expression to evaluate
     * 
     * @return a list of all matched nodes; possibly empty
     * 
     * @throws XPathException if there's a syntax error in the 
     *     expression; or the query returns something other than
     *     a node-set
     */
    public static final Nodes query(Node node, String xpath) {
        return query(node, xpath, null);
    }

}

