//
//  Camwood.java
//  FisherMill
//
package org.apache.velocity.tools;

import java.util.Collection;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Map;
import java.util.Vector;

import org.apache.velocity.util.ArrayIterator;
import org.apache.velocity.util.EnumerationIterator;


/**
 * <p>
 * A convenience tool for use in #foreach loops. It wraps a list to let the designer specify
 * a condition to terminate the loop, and reuse the same list in different loops.
 * </p>
 * <p>
 * Important note: The method stop() doesn't stop the #foreach immediately, instead it makes the
 * loop terminate after the end of the current iteration. See also the FisherMill class for an
 * alternate way to terminate the current iteration.
 * </p>
 * <p>
 * Example of use:
 * <pre>  
 *  Java
 *  ----
 *  context.put("cam", new Camwood());
 *
 *  
 *  VTL
 *  ---
 *  
 *  #set ($list = [1, 2, 3, 5, 8, 13])
 *  #set ($numbers = $cam.wrap($list))
 *  
 *  #foreach ($item in $numbers)
 *   $item##
 *  #if ($item < 8)$numbers.stop()#end
 *  #end
 *  
 *  $numbers.next()
 *  
 *  
 *  Output
 *  ------
 *  
 *   1 2 3 5
 *  8
 * </pre>
 * </p>
 * <p>
 * <b>Warning:</b> It is not recommended to call hasNext() directly as this method is used
 * to control the #foreach. Use hasMore() instead.
 * </p>
 * <ul><dl>
 * Changes:
 * </dl><li> 13 Sep 02	Original version
 * </li><li> 13 Sep 02	Added isTool(), cont()
 * </li></ul>
 * @author Denis Bredelet
 */
public class Camwood implements Iterator {

    private Object wrapped;
    private Iterator iterator;
    private boolean wantMore;

    /**
     * Create a Camwood instance to use as tool.
     * When it is created this way, the tool returns a new instance each time wrap() is called.
     * This is useful when you want to allow the designers to create instances.
     */
    public Camwood()
    {
        this.wrapped = null;
        this.iterator = null;
        this.wantMore = false;
    }

    /**
     * Create a Camwood instance to use in #foreach.
     *
     * @param wrapped The list to wrap. 
     */
    public Camwood(Object list)
    {
        this();
        if (list != null)
        {
            wrapAround(list);
        }
    }


    /**
     * Returns true if this object is used as a tool.
     * @return true if this object is used as a tool, i.e. it doesn't wrap a list.
     * @see #wrap(java.lang.Object)
     */
    public boolean isTool()
    {
        return wrapped == null;
    }
    
    /**
     * Wraps a list with the tool.
     * The list can be an array, a Collection, a Map, an Iterator or an Enumeration.
     * <br>If the list is a Map, the wrapper iterates over the values.
     * <br>If the list is an Iterator or an Enumeration, the wrapper can be used only once.
     *
     * @param list The list to wrap.
     * @return A new wrapper if this object is used as a tool, or itself if it is a wrapper.
     */
    public Camwood wrap(Object list)
    {
        if (this.wrapped == null)
        {
            return new Camwood(list);
        }
        else if (list != null)
        {
            this.wrapAround(list);
            return this;
        }
        else
        {
            throw new IllegalArgumentException("Need a valid list to wrap");
        }
    }

    /**
     * <p>
     * Resets the wrapper so that it starts over at the beginning of the list.
     * </p>
     * <p>
     * <b>Note to programmers:</b> This method has no effect if the wrapped object is
     * an enumeration or an iterator.
     * </p>
     */
    public void reset()
    {
        if (this.wrapped != null)
        {
            wrapAround(this.wrapped);
        }
    }

    /**
     * Wraps a list with the tool. This object can therefore be used instead of the list
     * itself in a #foreach.
     * The list can be an array, a Collection, a Map, an Iterator or an Enumeration.
     * <br>- If the list is a Map, the wrapper iterates over the values.
     * <br>- If the list is an Iterator or an Enumeration, the wrapper can be used only once.
     *
     * @param wrapped The list to wrap.
     * @return This object, wrapped around the list.
     */
    private void wrapAround(Object wrapped)
    {
        // rip-off from org/apache/velocity/runtime/directive/ForEach.java
        if ( wrapped.getClass().isArray() )
            this.iterator = new ArrayIterator( (Object[]) wrapped );
        else if ( wrapped instanceof Collection)
            this.iterator = ( (Collection) wrapped).iterator();
        else if ( wrapped instanceof Map )
            this.iterator = ( (Map) wrapped).values().iterator();
        else if ( wrapped instanceof Iterator )
            this.iterator = ( (Iterator) wrapped);
        else if ( wrapped instanceof Enumeration )
            this.iterator = new EnumerationIterator( (Enumeration)  wrapped );
        else
            // Don't know what is the object.
            // Should we put it in a one-item array?
            throw new IllegalArgumentException("Don't know how to wrap this list");

        this.wrapped = wrapped;
        this.wantMore = true;
    }



    /**
     * Gets the next element in the list. This method is called by #foreach to define $item
     * in:
     * <pre>
     * #foreach( $item in $list )</pre>
     *
     * @return The next element in the list.
     * @throws NoSuchElementException if there are no more elements in the list.
     * @see #hasMore()
     */
    public Object next()
    {
        if (this.wrapped == null)
        {
            throw new IllegalStateException("Use wrap() before calling next()");
        }
        return this.iterator.next();
    }

    /**
     * Returns true if there are more elements in the list and stop() was not called.
     * 
     * @return true if there are more elements, and stop() was not called
     *  since last call.
     * @see #hasMore()
     * @see #next()
     */
    public boolean hasNext()
    {
        if (this.wantMore)
        {
            return hasMore();
        }
        else
        {
            this.wantMore = true;	// prepare for next #foreach
            return false;
        }
    }

    /**
     * Removes the current element from the list.
     *
     * @throws UnsupportedOperationException if the wrapped list iterator doesn't support this
     *  operation.
     */
    public void remove() throws UnsupportedOperationException
    {
        if (this.wrapped == null)
        {
            throw new IllegalStateException("Use wrap() before calling remove()");
        }

        // Let the iterator decide whether to implement this or not
        this.iterator.remove();
    }


    /**
     * Returns this object as a String.
     * <br>If this object is used as a tool, it just gives the class name.
     * <br>Otherwise it appends the wrapped list to the class name.
     *
     * @return A string representation of this object.
     */
    public String toString()
    {
        String representation = getClass().getName();
        if (this.wrapped != null)
        {
            representation += "(" + this.wrapped + ")";
        }
        return representation;
    }


    /**
     * Returns true if there are more elements in the wrapped list.
     * <br>If this object doesn't wrap a list, the method always returns false.
     *
     * @return true if there are more elements in the list.
     * @see #next()
     */
    public boolean hasMore()
    {
        if (this.wrapped == null)
        {
            return false;
        }
        else
        {
            return this.iterator.hasNext();
        }
    }

    /**
     * Puts a condition to break out of the loop.
     * The #foreach loop will terminate after this iteration.
     * @see #cont()
     */
    public void stop()
    {
        this.wantMore = false;
    }

    /**
     * Puts a condition to continue the loop.
     * The #foreach loop will continue after this iteration (if there are more elements).
     * @see #stop()
     * @see #hasMore()
     */
    public void cont()
    {
        this.wantMore = true;
    }

}
