/*
 * 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.hibernate.search.engine.DocumentExtractor;
import org.hibernate.search.engine.SearchFactoryImplementor;
import org.apache.lucene.search.IndexSearcher;

import java.util.NoSuchElementException;
import java.util.Arrays;
import java.io.IOException;

/**
 * Implementation for {@link org.jboss.cache.search.QueryResultIterator}. This is what is returned when
 * the {@link CacheQuery#lazyIterator()} method is called. This loads the results only when required and hence differs from
 * {@link org.jboss.cache.search.QueryResultIteratorImpl} which is the other implementation of QueryResultIterator.
 *
 * @author Navin Surtani (<a href="mailto:nsurtani@redhat.com">nsurtani@redhat.com</a>)
 */
public class LazyQueryResultIterator implements QueryResultIterator
{
   private int index = 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 int max = 0;
   private int first = 0;
   private int fetchSize = 0;     // difference between fetchsize and index must always be the value for first. fetchSize has to be at least 1.
   private CacheEntityLoader entityLoader;
   private DocumentExtractor extractor;
   private IndexSearcher searcher;
   private SearchFactoryImplementor searchFactory;
   Object[] buffer;

   public LazyQueryResultIterator(DocumentExtractor extractor, CacheEntityLoader entityLoader,
                                  IndexSearcher searcher, SearchFactoryImplementor searchFactory, int first, int max, int fetchSize)
   {
      if (fetchSize < 1)
      {
         throw new IllegalArgumentException("Incorrect value for fetchsize passed. Your fetchSize is less than 1");
      }

      this.extractor = extractor;
      this.entityLoader = entityLoader;
      index = first;
      this.first = first;
      this.max = max;
      this.fetchSize = fetchSize;
      this.searcher = searcher;
      this.searchFactory = searchFactory;

      //Create an buffer with size fetchSize (which is the size of the required buffer.
      buffer = new Object[this.fetchSize];
   }

   public void jumpToResult(int index) throws IndexOutOfBoundsException
   {
      if (index < first || index > max)
      {
         throw new IndexOutOfBoundsException("The given index is incorrect. Please check and try again.");
      }
      
      this.index = first + index;

   }

   public void first()
   {
      index = first;
   }

   public void last()
   {
      index = max;
   }

   public void afterFirst()
   {
      index = first + 1;
   }

   public void beforeLast()
   {
      index = max - 1;
   }

   public boolean isFirst()
   {
      return index == first;
   }

   public boolean isLast()
   {
      return index == max;
   }

   public boolean isAfterFirst()
   {
      return index == first + 1;
   }

   public boolean isBeforeLast()
   {
      return index == max - 1;
   }

   public void close()
   {
      IndexSearcherCloser.closeSearcher(searcher, searchFactory.getReaderProvider());
   }

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

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

      Object toReturn = null;
      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 = index - bufferIndex;
         toReturn = buffer[indexToReturn];
      }
      else
      {
         // else we need to populate the buffer and get what we need.

         try
         {
            String documentId = (String) extractor.extract(index).id;
            CacheEntityId id = new CacheEntityId(documentId);
            toReturn = entityLoader.load(id);

            //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++)
            {
               String bufferDocumentId = (String) extractor.extract(index + i).id;
               CacheEntityId bufferId = new CacheEntityId(bufferDocumentId);
               Object toBuffer = entityLoader.load(bufferId);
               buffer[i] = toBuffer;
            }
            bufferIndex = index;
         }
         catch (IOException e)
         {
            e.printStackTrace();
         }
      }

      index++;
      return toReturn;
   }

   public boolean hasPrevious()
   {
      return index >= first;
   }

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

      Object toReturn = null;
      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];
      }

      try
      {
         //Wiping the buffer
         Arrays.fill(buffer, null);

         String documentId = (String) extractor.extract(index).id;
         CacheEntityId id = new CacheEntityId(documentId);
         toReturn = entityLoader.load(id);

         buffer[0] = toReturn;

         //now loop through bufferSize times to add the rest of the objects into the list.
         for (int i = 1; i < bufferSize; i++)
         {
            String bufferDocumentId = (String) extractor.extract(index - i).id;    //In this case it has to be index - i because previous() is called.
            CacheEntityId bufferId = new CacheEntityId(bufferDocumentId);
            Object toBuffer = entityLoader.load(bufferId);
            buffer[i] = toBuffer;
         }

         bufferIndex = index;
      }
      catch (IOException e)
      {
         e.printStackTrace();
      }
      index--;
      return toReturn;
   }

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

   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");
   }

}
