package org.apache.velocity.tools.generic;

/*
 * 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.
 */

import java.util.Collection;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Map;
import org.apache.velocity.util.ArrayIterator;
import org.apache.velocity.util.EnumerationIterator;
import org.apache.velocity.tools.config.DefaultKey;

/**
 * <p>
 * A convenience tool to use with #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>
 * Example of use:
 * <pre>
 *  Java
 *  ----
 *  context.put("mill", new IteratorTool());
 *
 *
 *  VTL
 *  ---
 *
 *  #set ($list = [1, 2, 3, 5, 8, 13])
 *  #set ($numbers = $mill.wrap($list))
 *
 *  #foreach ($item in $numbers)
 *  #if ($item < 8) $numbers.more()#end
 *  #end
 *
 *  $numbers.more()
 *
 *
 *  Output
 *  ------
 *
 *   1 2 3 5
 *  8
 *
 * Example tools.xml config (if you want to use this with VelocityView):
 * &lt;tools&gt;
 *   &lt;toolbox scope="request"&gt;
 *     &lt;tool class="org.apache.velocity.tools.generic.IteratorTool"/&gt;
 *   &lt;/toolbox&gt;
 * &lt;/tools&gt;
 * </pre>
 * </p>
 * <p>
 * <b>Warning:</b> It is not recommended to use hasNext() with this
 * tool as it is used to control the #foreach. Use hasMore() instead.
 * </p>
 *
 * @author <a href="mailto:jido@respublica.fr">Denis Bredelet</a>
 * @version $Id: IteratorTool.java 598471 2007-11-27 00:26:10Z nbubna $
 * @deprecated Use {@link LoopTool} instead
 */
@DefaultKey("mill")
@Deprecated
public class IteratorTool implements Iterator {


    private Object wrapped;
    private Iterator iterator;
    private boolean wantMore;
    private boolean cachedNext;
    protected Object next;


    /**
     * Create a IteratorTool 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 IteratorTool()
    {
        this(null);
    }


    /**
     * Create a IteratorTool instance to use in #foreach.
     *
     * @param wrapped The list to wrap.
     */
    public IteratorTool(Object wrapped)
    {
        internalWrap(wrapped);
    }


    /**
     * Wraps a list with the tool.
     * <br>The list can be an array, a Collection, a Map, an Iterator
     * or an Enumeration.
     * <br>If the list is a Map, the tool iterates over the values.
     * <br>If the list is an Iterator or an Enumeration, the tool 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 IteratorTool wrap(Object list)
    {
        if (this.wrapped == null)
        {
            return new IteratorTool(list);
        }
        else if (list != null)
        {
            internalWrap(list);
            return this;
        }
        else
        {
            throw new IllegalArgumentException("Need a valid list to wrap");
        }
    }


    /**
     * 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 tool iterates over the values.
     * <br>- If the list is an Iterator or an Enumeration, the tool
     * can be used only once.
     *
     * @param wrapped The list to wrap.
     */
    private void internalWrap(Object wrapped)
    {
        if (wrapped != null)
        {
            /* 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: "+wrapped);
            }

            this.wrapped = wrapped;
            this.wantMore = true;
            this.cachedNext = false;
        }
        else
        {
            this.iterator = null;
            this.wrapped = null;
            this.wantMore = false;
            this.cachedNext = false;
        }
    }


    /**
     * <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.
     */
    public void reset()
    {
        if (this.wrapped != null)
        {
            internalWrap(this.wrapped);
        }
    }


    /**
     * <p>
     * Gets the next object in the list. This method is called
     * by #foreach to define $item in:
     * <pre>
     * #foreach( $item in $list )
     * </pre>
     * </p>
     * <p>
     * This method is not intended for template designers, but they can use
     * them if they want to read the value of the next item without doing
     * more().
     * </p>
     *
     * @return The next item in the list.
     * @throws NoSuchElementException if there are no more
     *         elements in the list.
     */
    public Object next()
    {
        if (this.wrapped == null)
        {
            throw new IllegalStateException("Use wrap() before calling next()");
        }

        if (!this.cachedNext)
        {
            this.cachedNext = true;
            this.next = this.iterator.next();
            return this.next;
        }
        else
        {
            return this.next;
        }
    }

    /**
     * Returns true if there are more elements in the
     * list and more() was called.
     * <br>This code always return false:
     * <pre>
     * tool.hasNext()? tool.hasNext(): false;
     * </pre>
     *
     * @return true if there are more elements, and either more()
     *         or hasNext() was called since last call.
     */
    public boolean hasNext()
    {
        if (this.wantMore)
        {
            /* don't want more unless more is called */
            this.wantMore = false;
            return hasMore();
        }
        else
        {
            /* prepare for next #foreach */
            this.wantMore = true;
            return false;
        }
    }

    /**
     * Removes the current element from the list.
     * The current element is defined as the last element that was read
     * from the list, either with next() or with more().
     *
     * @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();
    }


    /**
     * <p>
     * Asks for the next element in the list. This method is to be used
     * by the template designer in #foreach loops.
     * </p>
     * <p>
     * If this method is called in the body of #foreach, the loop
     * continues as long as there are elements in the list.
     * <br>If this method is not called the loop terminates after the
     * current iteration.
     * </p>
     *
     * @return The next element in the list, or null if there are no
     *         more elements.
     */
    public Object more()
    {
        this.wantMore = true;
        if (hasMore())
        {
            Object next = next();
            this.cachedNext = false;
            return next;
        }
        else
        {
            return null;
        }
    }

    /**
     * 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.
     */
    public boolean hasMore()
    {
        if (this.wrapped == null)
        {
            return false;
        }
        return cachedNext || this.iterator.hasNext();
    }


    /**
     * Puts a condition to break out of the loop.
     * The #foreach loop will terminate after this iteration, unless more()
     * is called after stop().
     */
    public void stop()
    {
        this.wantMore = false;
    }


    /**
     * 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()
    {
        StringBuilder out = new StringBuilder(this.getClass().getName());
        if (this.wrapped != null)
        {
            out.append('(');
            out.append(this.wrapped);
            out.append(')');
        }
        return out.toString();
    }


}
