/*
 * Copyright 2001-2005 (C) MetaStuff, Ltd. All Rights Reserved.
 *
 * This software is open source.
 * See the bottom of this file for the licence.
 */

package org.dom4j.xpath;

import java.io.Serializable;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.dom4j.InvalidXPathException;
import org.dom4j.Node;
import org.dom4j.NodeFilter;
import org.dom4j.XPathException;

import org.jaxen.FunctionContext;
import org.jaxen.JaxenException;
import org.jaxen.NamespaceContext;
import org.jaxen.SimpleNamespaceContext;
import org.jaxen.VariableContext;
import org.jaxen.XPath;
import org.jaxen.dom4j.Dom4jXPath;

/**
 * <p>
 * Default implementation of {@link org.dom4j.XPath}which uses the <a
 * href="http://jaxen.org">Jaxen </a> project.
 * </p>
 * 
 * @author bob mcwhirter
 * @author <a href="mailto:jstrachan@apache.org">James Strachan </a>
 */
public class DefaultXPath implements org.dom4j.XPath, NodeFilter, Serializable {
    private String text;

    private XPath xpath;

    private NamespaceContext namespaceContext;

    /**
     * Construct an XPath
     * 
     * @param text
     *            DOCUMENT ME!
     * 
     * @throws InvalidXPathException
     *             DOCUMENT ME!
     */
    public DefaultXPath(String text) throws InvalidXPathException {
        this.text = text;
        this.xpath = parse(text);
    }

    public String toString() {
        return "[XPath: " + xpath + "]";
    }

    // XPath interface

    /**
     * Retrieve the textual XPath string used to initialize this Object
     * 
     * @return The XPath string
     */
    public String getText() {
        return text;
    }

    public FunctionContext getFunctionContext() {
        return xpath.getFunctionContext();
    }

    public void setFunctionContext(FunctionContext functionContext) {
        xpath.setFunctionContext(functionContext);
    }

    public NamespaceContext getNamespaceContext() {
        return namespaceContext;
    }

    public void setNamespaceURIs(Map map) {
        setNamespaceContext(new SimpleNamespaceContext(map));
    }

    public void setNamespaceContext(NamespaceContext namespaceContext) {
        this.namespaceContext = namespaceContext;
        xpath.setNamespaceContext(namespaceContext);
    }

    public VariableContext getVariableContext() {
        return xpath.getVariableContext();
    }

    public void setVariableContext(VariableContext variableContext) {
        xpath.setVariableContext(variableContext);
    }

    public Object evaluate(Object context) {
        try {
            setNSContext(context);

            List answer = xpath.selectNodes(context);

            if ((answer != null) && (answer.size() == 1)) {
                return answer.get(0);
            }

            return answer;
        } catch (JaxenException e) {
            handleJaxenException(e);

            return null;
        }
    }

    public Object selectObject(Object context) {
        return evaluate(context);
    }

    public List selectNodes(Object context) {
        try {
            setNSContext(context);

            return xpath.selectNodes(context);
        } catch (JaxenException e) {
            handleJaxenException(e);

            return Collections.EMPTY_LIST;
        }
    }

    public List selectNodes(Object context, org.dom4j.XPath sortXPath) {
        List answer = selectNodes(context);
        sortXPath.sort(answer);

        return answer;
    }

    public List selectNodes(Object context, org.dom4j.XPath sortXPath,
            boolean distinct) {
        List answer = selectNodes(context);
        sortXPath.sort(answer, distinct);

        return answer;
    }

    public Node selectSingleNode(Object context) {
        try {
            setNSContext(context);

            Object answer = xpath.selectSingleNode(context);

            if (answer instanceof Node) {
                return (Node) answer;
            }

            if (answer == null) {
                return null;
            }

            throw new XPathException("The result of the XPath expression is "
                    + "not a Node. It was: " + answer + " of type: "
                    + answer.getClass().getName());
        } catch (JaxenException e) {
            handleJaxenException(e);

            return null;
        }
    }

    public String valueOf(Object context) {
        try {
            setNSContext(context);

            return xpath.stringValueOf(context);
        } catch (JaxenException e) {
            handleJaxenException(e);

            return "";
        }
    }

    public Number numberValueOf(Object context) {
        try {
            setNSContext(context);

            return xpath.numberValueOf(context);
        } catch (JaxenException e) {
            handleJaxenException(e);

            return null;
        }
    }

    public boolean booleanValueOf(Object context) {
        try {
            setNSContext(context);

            return xpath.booleanValueOf(context);
        } catch (JaxenException e) {
            handleJaxenException(e);

            return false;
        }
    }

    /**
     * <p>
     * <code>sort</code> sorts the given List of Nodes using this XPath
     * expression as a {@link Comparator}.
     * </p>
     * 
     * @param list
     *            is the list of Nodes to sort
     */
    public void sort(List list) {
        sort(list, false);
    }

    /**
     * <p>
     * <code>sort</code> sorts the given List of Nodes using this XPath
     * expression as a {@link Comparator}and optionally removing duplicates.
     * </p>
     * 
     * @param list
     *            is the list of Nodes to sort
     * @param distinct
     *            if true then duplicate values (using the sortXPath for
     *            comparisions) will be removed from the List
     */
    public void sort(List list, boolean distinct) {
        if ((list != null) && !list.isEmpty()) {
            int size = list.size();
            HashMap sortValues = new HashMap(size);

            for (int i = 0; i < size; i++) {
                Object object = list.get(i);

                if (object instanceof Node) {
                    Node node = (Node) object;
                    Object expression = getCompareValue(node);
                    sortValues.put(node, expression);
                }
            }

            sort(list, sortValues);

            if (distinct) {
                removeDuplicates(list, sortValues);
            }
        }
    }

    public boolean matches(Node node) {
        try {
            setNSContext(node);

            List answer = xpath.selectNodes(node);

            if ((answer != null) && (answer.size() > 0)) {
                Object item = answer.get(0);

                if (item instanceof Boolean) {
                    return ((Boolean) item).booleanValue();
                }

                return answer.contains(node);
            }

            return false;
        } catch (JaxenException e) {
            handleJaxenException(e);

            return false;
        }
    }

    /**
     * Sorts the list based on the sortValues for each node
     * 
     * @param list
     *            DOCUMENT ME!
     * @param sortValues
     *            DOCUMENT ME!
     */
    protected void sort(List list, final Map sortValues) {
        Collections.sort(list, new Comparator() {
            public int compare(Object o1, Object o2) {
                o1 = sortValues.get(o1);
                o2 = sortValues.get(o2);

                if (o1 == o2) {
                    return 0;
                } else if (o1 instanceof Comparable) {
                    Comparable c1 = (Comparable) o1;

                    return c1.compareTo(o2);
                } else if (o1 == null) {
                    return 1;
                } else if (o2 == null) {
                    return -1;
                } else {
                    return o1.equals(o2) ? 0 : (-1);
                }
            }
        });
    }

    // Implementation methods

    /**
     * Removes items from the list which have duplicate values
     * 
     * @param list
     *            DOCUMENT ME!
     * @param sortValues
     *            DOCUMENT ME!
     */
    protected void removeDuplicates(List list, Map sortValues) {
        // remove distinct
        HashSet distinctValues = new HashSet();

        for (Iterator iter = list.iterator(); iter.hasNext();) {
            Object node = iter.next();
            Object value = sortValues.get(node);

            if (distinctValues.contains(value)) {
                iter.remove();
            } else {
                distinctValues.add(value);
            }
        }
    }

    /**
     * DOCUMENT ME!
     * 
     * @param node
     *            DOCUMENT ME!
     * 
     * @return the node expression used for sorting comparisons
     */
    protected Object getCompareValue(Node node) {
        return valueOf(node);
    }

    protected static XPath parse(String text) {
        try {
            return new Dom4jXPath(text);
        } catch (JaxenException e) {
            throw new InvalidXPathException(text, e.getMessage());
        } catch (Throwable t) {
        	throw new InvalidXPathException(text, t);
        }
    }

    protected void setNSContext(Object context) {
        if (namespaceContext == null) {
            xpath.setNamespaceContext(DefaultNamespaceContext.create(context));
        }
    }

    protected void handleJaxenException(JaxenException exception)
            throws XPathException {
        throw new XPathException(text, exception);
    }
}

/*
 * Redistribution and use of this software and associated documentation
 * ("Software"), with or without modification, are permitted provided that the
 * following conditions are met:
 * 
 * 1. Redistributions of source code must retain copyright statements and
 * notices. Redistributions must also contain a copy of this document.
 * 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 * 
 * 3. The name "DOM4J" must not be used to endorse or promote products derived
 * from this Software without prior written permission of MetaStuff, Ltd. For
 * written permission, please contact dom4j-info@metastuff.com.
 * 
 * 4. Products derived from this Software may not be called "DOM4J" nor may
 * "DOM4J" appear in their names without prior written permission of MetaStuff,
 * Ltd. DOM4J is a registered trademark of MetaStuff, Ltd.
 * 
 * 5. Due credit should be given to the DOM4J Project - http://www.dom4j.org
 * 
 * THIS SOFTWARE IS PROVIDED BY METASTUFF, LTD. AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL METASTUFF, LTD. OR ITS CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 * 
 * Copyright 2001-2005 (C) MetaStuff, Ltd. All Rights Reserved.
 */
