/*
 * Copyright (C) 2009 eXo Platform SAS.
 *
 * 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.xcmis.search.lucene.index;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriter.MaxFieldLength;
import org.apache.lucene.index.MultiReader;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.LockObtainFailedException;
import org.apache.lucene.util.Version;
import org.xcmis.search.Startable;
import org.xcmis.search.config.IndexConfiguration;
import org.xcmis.search.config.IndexConfigurationException;
import org.xcmis.search.content.IndexModificationException;
import org.xcmis.search.lucene.index.merge.IndexAggregator;
import org.xcmis.spi.utils.Logger;

/**
 * Created by The eXo Platform SAS.
 * 
 * @author <a href="mailto:Sergey.Kabashnyuk@gmail.com">Sergey Kabashnyuk</a>
 * @version $Id: LocalStorageIndexDataManager.java 2 2010-02-04 17:21:49Z andrew00x $
 */
public class LocalStorageIndexDataManager implements LuceneIndexDataManager, IndexAggregator, Startable
{
   /**
     * 
     */
   private List<PersistedIndex> chains;

   /**
     * 
     */
   private final PersistentIndexDataKeeperFactory indexFactory;

   /**
    * Class logger.
    */
   private static final Logger LOG = Logger.getLogger(LocalStorageIndexDataManager.class);

   private final IndexConfiguration indexConfuguration;

   /**
    * @throws IndexConfigurationException
    * @throws IndexException
    * @throws IndexConfigurationException
    */
   public LocalStorageIndexDataManager(final IndexConfiguration indexConfuguration) throws IndexException,
      IndexConfigurationException
   {
      super();
      this.indexConfuguration = indexConfuguration;

      indexFactory = new PersistentIndexDataKeeperFactory(indexConfuguration);

   }

   public IndexTransactionModificationReport aggregate(final Collection<LuceneIndexDataManager> indexes)
      throws IndexException, IndexTransactionException
   {
      if (chains.size() == 0)
      {
         chains.add((PersistedIndex)indexFactory.merge(indexes));
      }
      else
      {
         IndexWriter writer = null;
         try
         {
            final PersistedIndex index = chains.get(0);

            writer = new IndexWriter(index.getDirectory(), new StandardAnalyzer(Version.LUCENE_35), MaxFieldLength.UNLIMITED);
            final List<Directory> dirs = new ArrayList<Directory>();
            for (final LuceneIndexDataManager luceneIndexDataManager : indexes)
            {
               // TODO remove get reader
               luceneIndexDataManager.getIndexReader();
               dirs.add(luceneIndexDataManager.getDirectory());
            }
            final Directory[] dirsToMerge = new Directory[dirs.size()];
            writer.addIndexesNoOptimize(dirs.toArray(dirsToMerge));
            writer.optimize();
         }
         catch (final CorruptIndexException e)
         {
            throw new IndexException(e.getLocalizedMessage(), e);
         }
         catch (final LockObtainFailedException e)
         {
            throw new IndexException(e.getLocalizedMessage(), e);
         }
         catch (final IOException e)
         {
            throw new IndexException(e.getLocalizedMessage(), e);
         }
         finally
         {
            if (writer != null)
            {
               try
               {
                  writer.close();
               }
               catch (CorruptIndexException e)
               {
                  throw new IndexException(e.getLocalizedMessage(), e);
               }
               catch (IOException e)
               {
                  throw new IndexException(e.getLocalizedMessage(), e);
               }
            }

         }
      }
      return null;
   }

   public Directory getDirectory() throws IndexException
   {
      if (chains.size() != 0)
      {
         return chains.get(0).getDirectory();
      }
      return null;
   }

   public long getDirectorySize(final boolean includeInherited)
   {
      return 0;
   }

   public Document getDocument(final String uuid) throws IndexException
   {
      Document doc = null;
      synchronized (chains)
      {
         for (int i = 0; i < chains.size(); i++)
         {
            doc = chains.get(i).getDocument(uuid);
            if (doc != null)
            {
               break;
            }
         }
      }
      return doc;
   }

   public synchronized long getDocumentCount()
   {
      long result = 0;
      for (final PersistedIndex index : chains)
      {
         result += index.getDocumentCount();
      }
      return result;
   }

   public IndexReader getIndexReader() throws IndexException
   {
      IndexReader result = null;
      if (chains.size() > 0)
      {
         synchronized (chains)
         {
            if (chains.size() > 0)
            {
               final List<IndexReader> readers = new ArrayList<IndexReader>(chains.size());
               final Iterator<PersistedIndex> it = chains.iterator();

               while (it.hasNext())
               {
                  final LuceneIndexDataManager chain = it.next();

                  final IndexReader indexReader = chain.getIndexReader();
                  if (indexReader != null)
                  {
                     readers.add(indexReader);
                  }

               }
               if (result != null)
               {
                  readers.add(result);
               }
               if (readers.size() > 1)
               {
                  final IndexReader[] indexReaders = new IndexReader[readers.size()];
                  result = new MultiReader(readers.toArray(indexReaders));
               }
               else if (readers.size() == 1)
               {
                  result = readers.get(0);
               }
               else
               {
                  throw new RuntimeException("No readers found");
               }
            }
         }
         if (result == null)
         {
            throw new RuntimeException("No readers found");
         }
      }
      return result;
   }

   public long getLastModifedTime()
   {
      return 0;
   }

   public boolean isStarted()
   {
      return false;
   }

   public boolean isStoped()
   {
      return false;
   }

   public IndexTransactionModificationReport save(IndexTransaction<Document> changes) throws IndexException,
      IndexTransactionException
   {
      // notify all chains about changes
      synchronized (chains)
      {

         if (changes.hasModifacationsDocuments())
         {

            // reverse order
            changes = processModifed(changes);

            if (changes.getRemovedDocuments().size() > 0)
            {
               throw new IndexModificationException("Unable to remove item's with id's "
                  + changes.getRemovedDocuments() + " from index");
            }

         }
         // take care about added modification
         processAdded(changes);

      }
      return null;
   }

   /**
    * {@inheritDoc}
    */
   public void start()
   {
      try
      {
         chains = indexFactory.init();
      }
      catch (final IndexException e)
      {
         throw new RuntimeException(e.getLocalizedMessage(), e);
      }
   }

   /**
    * {@inheritDoc}
    */
   public void stop()
   {
      for (final PersistedIndex index : chains)
      {
         try
         {
            index.getDirectory().close();
         }
         catch (final IndexException e)
         {
            LOG.error(e.getMessage(), e);
         }
         catch (final IOException e)
         {
            LOG.error(e.getMessage(), e);
         }
      }

   }

   /**
    * Process add
    * 
    * @param changes
    * @throws IndexException
    * @throws IndexTransactionException
    */
   private void processAdded(final IndexTransaction<Document> changes) throws IndexException, IndexTransactionException
   {
      if (changes.getAddedDocuments().size() > 0)
      {
         synchronized (chains)
         {
            if (chains.size() == 0)
            {
               final LuceneIndexDataManager indexDataKeeper = indexFactory.createNewIndexDataKeeper(changes);
               indexDataKeeper.start();
               chains.add((PersistedIndex)indexDataKeeper);
            }
            else
            {
               final LuceneIndexDataManager indexDataKeeper = chains.get(0);
               indexDataKeeper.save(changes);
            }

         }
      }
   }

   /**
    * Process remove and update
    * 
    * @param changes
    * @return
    * @throws IndexException
    * @throws IndexTransactionException
    */
   private IndexTransaction<Document> processModifed(IndexTransaction<Document> changes) throws IndexException,
      IndexTransactionException
   {
      synchronized (chains)
      {

         for (final Iterator<PersistedIndex> it = chains.iterator(); it.hasNext();)
         {
            final LuceneIndexDataManager chain = it.next();
            final IndexTransactionModificationReport report = chain.save(changes);

            if (report.isModifed())
            {
               changes = changes.apply(report);
               if (chain.getDocumentCount() == 0)
               {
                  indexFactory.dispose(chain);
                  it.remove();
               }
            }
            if (!changes.hasModifacationsDocuments())
            {
               break;
            }

         }
      }
      return changes;
   }
}
