/*
 * JBoss, Home of Professional Open Source
 * Copyright ${year}, Red Hat Middleware LLC, and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.jboss.cache.search;


import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.util.List;
import java.util.NoSuchElementException;
import java.util.Arrays;

/**
 * This is the implementation class for the interface QueryResultIterator which extends ListIterator. It is what is
 * returned when the iterator() method is run on a CacheQuery instance.
 * <p/>
 * <p/>
 *
 * @author Navin Surtani (<a href="mailto:nsurtani@redhat.com">nsurtani@redhat.com</a>)
 */
public class QueryResultIteratorImpl implements QueryResultIterator
{
   private int index = 0;
   //private final int size;
   private List<CacheEntityId> idList;
   private CacheEntityLoader entityLoader;
   private int lowerLimit = 0;
   private int upperLimit = 0;
   private int fetchSize = 0;
   private int bufferIndex = -1; // this is the index at which the buffer was populated.  Hence, the first element of the buffer is at this index in the overall result set.
   private Object[] buffer;
   private static final Log log = LogFactory.getLog(QueryResultIteratorImpl.class);


   public QueryResultIteratorImpl(List<CacheEntityId> idList, CacheEntityLoader entityLoader, int fetchSize)
   {
      if (fetchSize < 1)
      {
         throw new IllegalArgumentException("Incorrect value for fetchsize passed. Your fetchSize is less than 1");
      }

      this.idList = idList;
      this.entityLoader = entityLoader;
      upperLimit = idList.size() - 1;
      this.fetchSize = fetchSize;

      buffer = new Object[this.fetchSize];
   }

   /**
    * Jumps to a given index in the list of results.
    *
    * @param index to jump to
    * @throws IndexOutOfBoundsException
    */

   public void jumpToResult(int index) throws IndexOutOfBoundsException
   {
      if (index > idList.size() || index < 0)
      {
         throw new IndexOutOfBoundsException("The index you entered is either greater than the size of the list or negative");
      }
      this.index = index;
   }

   /**
    * Jumps to first element in the list.
    */

   public void first()
   {
      index = 0;
   }

   /**
    * Jumps to last element in the list.
    */

   public void last()
   {
      index = idList.size() - 1;
   }

   /**
    * Jumps to second element in the list.
    */

   public void afterFirst()
   {
      index = 1;
   }

   /**
    * Jumps to penultimate element in the list.
    */

   public void beforeLast()
   {
      index = idList.size() - 2;
   }

   /**
    * @return true if the current element is the first in the list.
    */

   public boolean isFirst()
   {
      return idList.get(index) == idList.get(0);
   }

   /**
    * @return true if the current result is the last one.
    */

   public boolean isLast()
   {
      return index == idList.size() - 1;

   }

   /**
    * @return true if the current result is one after the first.
    */

   public boolean isAfterFirst()
   {
      return idList.get(index) == idList.get(1);
   }

   /**
    * @return true if the current result is one before the last
    */

   public boolean isBeforeLast()
   {
      return idList.get(index) == idList.get(idList.size() - 2);
   }

   public void close()
   {
      // This method does not need to do anything for this type of iterator as when an instace of it is
      // created, the iterator() method in CacheQueryImpl closes everything that needs to be closed.
   }

   /**
    * Returns true if the list has more elements when traversing the list in the forward direction.
    *
    * @return true if the list has more elements when traversing the list in the forward direction.
    */

   public boolean hasNext()
   {
      return index <= upperLimit;
   }

   /**
    * Returns the next element in the list
    *
    * @return The next element in the list.
    */
   public Object next()
   {
      if (!hasNext()) throw new IndexOutOfBoundsException("Out of boundaries. There is no next");

      Object toReturn;
      int bufferSize = buffer.length;

      // make sure the index we are after is in the buffer.  If it is, then index >= bufferIndex and index <= (bufferIndex + bufferSize).
      if (bufferIndex >= 0                                       // buffer init check
              && index >= bufferIndex                           // lower boundary
              && index < (bufferIndex + bufferSize))          // upper boundary        //TODO: - Why does this logic work but <= not. >= works with previous() however.
      {
         // now we can get this from the buffer.  Sweet!
         int indexToReturn = index - bufferIndex;
         toReturn = buffer[indexToReturn];
      }

      else
      {
         // We need to populate the buffer.

         toReturn = entityLoader.load(idList.get(index));

         //Wiping bufferObjects and the bufferIndex so that there is no stale data.

         Arrays.fill(buffer, null);
         buffer[0] = toReturn;

         // we now need to buffer item at index "index", as well as the next "fetchsize - 1" elements.  I.e., a total of fetchsize elements will be buffered.
         //now loop through bufferSize times to add the rest of the objects into the list.

         for (int i = 1; i < bufferSize; i++)
         {
            if (index + i > upperLimit)
            {
               if (log.isDebugEnabled())
               {
                  log.debug("Your current index + bufferSize exceeds the size of your number of hits");
               }
               break;
            }

            Object toBuffer = entityLoader.load(idList.get(index + i));
            buffer[i] = toBuffer;
         }
         bufferIndex = index;

      }

      index++;
      return toReturn;
   }

   /**
    * Returns true if the list has more elements when traversing the list in the reverse direction.
    *
    * @return true if the list iterator has more elements when traversing the list in the reverse direction
    */
   public boolean hasPrevious()
   {
      return index >= lowerLimit;
   }

   /**
    * Returns the previous element in the list.
    *
    * @return The previous element in the list.
    */

   public Object previous()
   {
      if (!hasPrevious()) throw new IndexOutOfBoundsException("Index is out of bounds. There is no previous");

      Object toReturn;
      int bufferSize = buffer.length;

      // make sure the index we are after is in the buffer.  If it is, then index >= bufferIndex and index <= (bufferIndex + bufferSize).

      if (bufferIndex >= 0 // buffer init check
              && index <= bufferIndex // lower boundary
              && index >= (bufferIndex + bufferSize)) // upper boundary
      {
         // now we can get this from the buffer.  Sweet!
         int indexToReturn = bufferIndex - index;        // Unlike next() we have to make sure that we are subtracting index from bufferIndex
         toReturn = buffer[indexToReturn];
      }
      else
      {
         toReturn = entityLoader.load(idList.get(index));
         //Wiping bufferObjects and the bufferIndex so that there is no stale data.

         Arrays.fill(buffer, null);
         buffer[0] = toReturn;

         // we now need to buffer item at index "index", as well as the next "fetchsize - 1" elements.  I.e., a total of fetchsize elements will be buffered.
         // ignore loop below, in needs fixing
         //now loop through bufferSize times to add the rest of the objects into the list.

         for (int i = 1; i < bufferSize; i++)
         {
            if (index - i < lowerLimit)
            {
               if (log.isDebugEnabled())
               {
                  log.debug("Your current index - bufferSize exceeds the size of your number of hits");
               }
               break;
            }

            Object toBuffer = entityLoader.load(idList.get(index - i));
            buffer[i] = toBuffer;
         }
         bufferIndex = index;


      }
      index--;
      return toReturn;
   }

   /**
    * Returns the index of the element that would be returned by a subsequent call to next.
    *
    * @return Index of next element.
    */

   public int nextIndex()
   {
      if (!hasNext()) throw new NoSuchElementException("Out of boundaries");
      return index + 1;

   }

   /**
    * Returns the index of the element that would be returned by a subsequent call to previous.
    *
    * @return Index of previous element.
    */

   public int previousIndex()
   {
      if (!hasPrevious()) throw new NoSuchElementException("Out of boundaries");
      return index - 1;
   }

   /**
    * This method is not supported and should not be used. Use cache.remove() instead.
    */
   public void remove()
   {
      throw new UnsupportedOperationException("Not supported as you are trying to change something in the cache");
   }

   /**
    * This method is not supported in and should not be called. Use cache.put() instead.
    *
    * @param o
    * @throws UnsupportedOperationException
    */
   public void set(Object o) throws UnsupportedOperationException
   {
      throw new UnsupportedOperationException("Not supported as you are trying to change something in the cache");
   }

   /**
    * This method is not supported in and should not be called. Use cache.put() instead.
    *
    * @param o
    * @throws UnsupportedOperationException
    */

   public void add(Object o)
   {
      throw new UnsupportedOperationException("Not supported as you are trying to change something in the cache");
   }

}
