/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.commons.jxpath.ri;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;

import org.apache.commons.jxpath.BasicNodeSet;
import org.apache.commons.jxpath.ExpressionContext;
import org.apache.commons.jxpath.JXPathContext;
import org.apache.commons.jxpath.JXPathException;
import org.apache.commons.jxpath.NodeSet;
import org.apache.commons.jxpath.Pointer;
import org.apache.commons.jxpath.ri.axes.RootContext;
import org.apache.commons.jxpath.ri.model.NodePointer;
import org.apache.commons.jxpath.util.ReverseComparator;

/**
 * An XPath evaluation context.
 *
 * When  evaluating a path, a chain of EvalContexts is created, each context in
 * the chain representing a step of the path. Subclasses of EvalContext
 * implement behavior of various XPath axes: "child::", "parent::" etc.
 *
 * @author Dmitri Plotnikov
 * @version $Revision: 652845 $ $Date: 2008-05-02 12:46:46 -0500 (Fri, 02 May 2008) $
 */
public abstract class EvalContext implements ExpressionContext, Iterator {
    /** parent context */
    protected EvalContext parentContext;

    /** root context */
    protected RootContext rootContext;

    /** position */
    protected int position = 0;

    private boolean startedSetIteration = false;
    private boolean done = false;
    private boolean hasPerformedIteratorStep = false;
    private Iterator pointerIterator;

    /**
     * Create a new EvalContext.
     * @param parentContext parent context
     */
    public EvalContext(EvalContext parentContext) {
        this.parentContext = parentContext;
    }

    public Pointer getContextNodePointer() {
        return getCurrentNodePointer();
    }

    public JXPathContext getJXPathContext() {
        return getRootContext().getJXPathContext();
    }

    public int getPosition() {
        return position;
    }

    /**
     * Determines the document order for this context.
     *
     * @return 1 ascending order, -1 descending order,
     *  0 - does not require ordering
     */
    public int getDocumentOrder() {
        return parentContext != null && parentContext.isChildOrderingRequired() ? 1 : 0;
    }

    /**
     * Even if this context has the natural ordering and therefore does
     * not require collecting and sorting all nodes prior to returning them,
     * such operation may be required for any child context.
     * @return boolean
     */
    public boolean isChildOrderingRequired() {
        // Default behavior: if this context needs to be ordered,
        // the children need to be ordered too
        return getDocumentOrder() != 0;
    }

    /**
     * Returns true if there are mode nodes matching the context's constraints.
     * @return boolean
     */
    public boolean hasNext() {
        if (pointerIterator != null) {
            return pointerIterator.hasNext();
        }
        if (getDocumentOrder() != 0) {
            return constructIterator();
        }
        if (!done && !hasPerformedIteratorStep) {
            performIteratorStep();
        }
        return !done;
    }

    /**
     * Returns the next node pointer in the context
     * @return Object
     */
    public Object next() {
        if (pointerIterator != null) {
            return pointerIterator.next();
        }

        if (getDocumentOrder() != 0) {
            if (!constructIterator()) {
                throw new NoSuchElementException();
            }
            return pointerIterator.next();
        }
        if (!done && !hasPerformedIteratorStep) {
            performIteratorStep();
        }
        if (done) {
            throw new NoSuchElementException();
        }
        hasPerformedIteratorStep = false;
        return getCurrentNodePointer();
    }

    /**
     * Moves the iterator forward by one position
     */
    private void performIteratorStep() {
        done = true;
        if (position != 0 && nextNode()) {
            done = false;
        }
        else {
            while (nextSet()) {
                if (nextNode()) {
                    done = false;
                    break;
                }
            }
        }
        hasPerformedIteratorStep = true;
    }

    /**
     * Operation is not supported
     * @throws UnsupportedOperationException
     */
    public void remove() {
        throw new UnsupportedOperationException(
            "JXPath iterators cannot remove nodes");
    }

    /**
     * Construct an iterator.
     * @return whether the Iterator was constructed
     */
    private boolean constructIterator() {
        HashSet set = new HashSet();
        ArrayList list = new ArrayList();
        while (nextSet()) {
            while (nextNode()) {
                NodePointer pointer = getCurrentNodePointer();
                if (!set.contains(pointer)) {
                    set.add(pointer);
                    list.add(pointer);
                }
            }
        }
        if (list.isEmpty()) {
            return false;
        }

        sortPointers(list);

        pointerIterator = list.iterator();
        return true;
    }

    /**
     * Sort a list of pointers based on document order.
     * @param l the list to sort.
     */
    protected void sortPointers(List l) {
        switch (getDocumentOrder()) {
        case 1:
            Collections.sort(l);
            break;
        case -1:
            Collections.sort(l, ReverseComparator.INSTANCE);
            break;
        default:
        }
    }

    /**
     * Returns the list of all Pointers in this context for the current
     * position of the parent context.
     * @return List
     */
    public List getContextNodeList() {
        int pos = position;
        if (pos != 0) {
            reset();
        }
        List list = new ArrayList();
        while (nextNode()) {
            list.add(getCurrentNodePointer());
        }
        if (pos != 0) {
            setPosition(pos);
        }
        else {
            reset();
        }
        return list;
    }

    /**
     * Returns the list of all Pointers in this context for all positions
     * of the parent contexts.  If there was an ongoing iteration over
     * this context, the method should not be called.
     * @return NodeSet
     */
    public NodeSet getNodeSet() {
        if (position != 0) {
            throw new JXPathException(
                "Simultaneous operations: "
                    + "should not request pointer list while "
                    + "iterating over an EvalContext");
        }
        BasicNodeSet set = new BasicNodeSet();
        while (nextSet()) {
            while (nextNode()) {
                set.add((Pointer) getCurrentNodePointer().clone());
            }
        }

        return set;
    }

    /**
     * Typically returns the NodeSet by calling getNodeSet(),
     * but will be overridden for contexts that more naturally produce
     * individual values, e.g. VariableContext
     * @return Object
     */
    public Object getValue() {
        return getNodeSet();
    }

    public String toString() {
        Pointer ptr = getContextNodePointer();
        return ptr == null ? "Empty expression context" : "Expression context [" + getPosition()
                + "] " + ptr.asPath();
    }

    /**
     * Returns the root context of the path, which provides easy
     * access to variables and functions.
     * @return RootContext
     */
    public RootContext getRootContext() {
        if (rootContext == null) {
            rootContext = parentContext.getRootContext();
        }
        return rootContext;
    }

    /**
     * Sets current position = 0, which is the pre-iteration state.
     */
    public void reset() {
        position = 0;
    }

    /**
     * Get the current position.
     * @return int position.
     */
    public int getCurrentPosition() {
        return position;
    }

    /**
     * Returns the first encountered Pointer that matches the current
     * context's criteria.
     * @return Pointer
     */
    public Pointer getSingleNodePointer() {
        reset();
        while (nextSet()) {
            if (nextNode()) {
                return getCurrentNodePointer();
            }
        }
        return null;
    }

    /**
     * Returns the current context node. Undefined before the beginning
     * of the iteration.
     * @return NodePoiner
     */
    public abstract NodePointer getCurrentNodePointer();

    /**
     * Returns true if there is another sets of objects to interate over.
     * Resets the current position and node.
     * @return boolean
     */
    public boolean nextSet() {
        reset(); // Restart iteration within the set

        // Most of the time you have one set per parent node
        // First time this method is called, we should look for
        // the first parent set that contains at least one node.
        if (!startedSetIteration) {
            startedSetIteration = true;
            while (parentContext.nextSet()) {
                if (parentContext.nextNode()) {
                    return true;
                }
            }
            return false;
        }

        // In subsequent calls, we see if the parent context
        // has any nodes left in the current set
        if (parentContext.nextNode()) {
            return true;
        }

        // If not, we look for the next set that contains
        // at least one node
        while (parentContext.nextSet()) {
            if (parentContext.nextNode()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns true if there is another object in the current set.
     * Switches the current position and node to the next object.
     * @return boolean
     */
    public abstract boolean nextNode();

    /**
     * Moves the current position to the specified index. Used with integer
     * predicates to quickly get to the n'th element of the node set.
     * Returns false if the position is out of the node set range.
     * You can call it with 0 as the position argument to restart the iteration.
     * @param position to set
     * @return boolean
     */
    public boolean setPosition(int position) {
        this.position = position;
        return true;
    }
}
