/*
 * Copyright (C) 2010 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;

import org.apache.lucene.document.Document;
import org.apache.lucene.index.IndexReader;
import org.xcmis.search.config.IndexConfiguration;
import org.xcmis.search.content.ContentEntry;
import org.xcmis.search.content.command.read.GetChildEntriesCommand;
import org.xcmis.search.content.command.read.GetContentEntryCommand;
import org.xcmis.search.content.command.read.GetUnfiledEntriesCommand;
import org.xcmis.search.lucene.index.IndexException;
import org.xcmis.search.lucene.index.IndexTransactionException;
import org.xcmis.search.lucene.index.LuceneIndexTransaction;
import org.xcmis.search.lucene.index.LuceneIndexer;
import org.xcmis.spi.utils.Logger;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;

/**
 * Tools for index recovering. 
 */
public class IndexRecoveryTool
{

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

   /**
    * Max documents count in buffer.
    */
   public static final int BUFFER_MAX_SIZE = 1000;

   /**
    * Convert {@link ContentEntry} to {@link Document}.
    */
   private final LuceneIndexer nodeIndexer;

   private final LuceneQueryableIndexStorage indexStorage;

   /**
    * Configuration of index.
    */
   private final IndexConfiguration indexConfiguration;

   /**
    * @param indexStorage
    * @param nodeIndexer
    * @param indexConfiguration
    */
   public IndexRecoveryTool(LuceneQueryableIndexStorage indexStorage, LuceneIndexer nodeIndexer,
      IndexConfiguration indexConfiguration)
   {
      super();
      this.indexStorage = indexStorage;
      this.nodeIndexer = nodeIndexer;
      this.indexConfiguration = indexConfiguration;
   }

   /**
    * Refresh the index of documents returned by 'uuids' iterator.
    * @param uuids
    * @throws IndexException
    */
   public void recover(Iterator<String> uuids) throws IndexException
   {
      IndexReader reader = indexStorage.getIndexReader();
      try
      {
         final HashMap<String, Document> addedDocuments = new HashMap<String, Document>();
         final HashSet<String> removedDocuments = new HashSet<String>();
         while (uuids.hasNext())
         {
            String nodeUuid = uuids.next();

            GetContentEntryCommand getCommand = new GetContentEntryCommand(nodeUuid);
            final ContentEntry contentEntry = (ContentEntry)indexStorage.invokeNextInterceptor(null, getCommand);
            if (contentEntry == null)
            {
               if (indexStorage.getDocument(nodeUuid, reader) != null)
               {
                  // item exist in index storage but doesn't exist in
                  // persistence storage
                  removedDocuments.add(nodeUuid);
               }
            }
            else
            {

               Document doc = nodeIndexer.createDocument(contentEntry);

               if (indexStorage.getDocument(nodeUuid, reader) != null)
               {
                  // out dated content
                  addedDocuments.put(nodeUuid, doc);
                  removedDocuments.add(nodeUuid);
               }
               else
               {
                  // content desn't exist
                  addedDocuments.put(nodeUuid, doc);
               }
            }
            //flash if changes more when BUFFER_MAX_SIZE 
            if (checkFlush(addedDocuments, removedDocuments))
            {
               flash(addedDocuments, removedDocuments);
            }
         }
         //if some changes left
         if (addedDocuments.size() + removedDocuments.size() >= 0)
         {
            indexStorage.save(new LuceneIndexTransaction(addedDocuments, removedDocuments));
         }
      }
      catch (Throwable e)
      {
         throw new IndexException(e.getLocalizedMessage(), e);
      }
   }

   /**
    * Recover all content.
    * @throws IndexException
    */
   public void recoverAll() throws IndexException
   {
      Map<String, Document> documentBuffer = new HashMap<String, Document>();
      final HashSet<String> removedDocuments = new HashSet<String>();
      try
      {
         GetContentEntryCommand getCommand = new GetContentEntryCommand(indexConfiguration.getRootUuid());
         final ContentEntry rootEntry = (ContentEntry)indexStorage.invokeNextInterceptor(null, getCommand);
         if (rootEntry != null)
         {
            restoreBranch(rootEntry, documentBuffer, removedDocuments);
         }
         else
         {
            LOG.warn("Root element with id " + indexConfiguration.getRootUuid() + " not found ");
         }
         if (documentBuffer.size() > 0)
         {
            flash(documentBuffer, removedDocuments);
         }
         //recover unfiled documents.
         GetUnfiledEntriesCommand getUnfiledEntriesCommand = new GetUnfiledEntriesCommand();
         Iterator<String> uuids = (Iterator<String>)indexStorage.invokeNextInterceptor(null, getUnfiledEntriesCommand);
         if (uuids != null)
         {
            recover(uuids);
         }

      }
      catch (Throwable e)
      {
         throw new IndexException(e.getLocalizedMessage(), e);
      }
   }

   /**
    * 
    * @param documentBuffer
    * @return true if we need to flush buffer.
    */
   private boolean checkFlush(Map<String, Document> documentBuffer, HashSet<String> removedDocuments)
   {
      return documentBuffer.size() + removedDocuments.size() >= BUFFER_MAX_SIZE;
   }

   /**
    * Flash changes to the index and  cleans the lists.
    * @param documentBuffer
    * @param removedDocuments
    * @throws IndexTransactionException
    * @throws IndexException
    */
   private void flash(Map<String, Document> documentBuffer, HashSet<String> removedDocuments)
      throws IndexTransactionException, IndexException
   {
      indexStorage.save(new LuceneIndexTransaction(new HashMap<String, Document>(documentBuffer), new HashSet<String>(
         removedDocuments)));
      documentBuffer.clear();
      removedDocuments.clear();
   }

   /**
    * Restore content of branch starting from branchUuid.
    * 
    * @param branchUuid
    *           - Uuid of root element of branch.
    * @param documentBuffer
    * @param removedDocuments 
    * @throws Throwable
    */
   private void restoreBranch(ContentEntry branchRoot, Map<String, Document> documentBuffer,
      HashSet<String> removedDocuments) throws Throwable
   {
      // add root.

      documentBuffer.put(branchRoot.getIdentifier(), nodeIndexer.createDocument(branchRoot));
      if (checkFlush(documentBuffer, removedDocuments))
      {
         flash(documentBuffer, removedDocuments);
      }

      // add childs
      GetChildEntriesCommand getChildCommand = new GetChildEntriesCommand(branchRoot.getIdentifier());
      Collection<ContentEntry> childEntries =
         (Collection<ContentEntry>)indexStorage.invokeNextInterceptor(null, getChildCommand);
      if (childEntries != null)
      {
         for (ContentEntry contentEntry : childEntries)
         {
            restoreBranch(contentEntry, documentBuffer, removedDocuments);
         }
      }
      else
      {
         LOG.warn("Child elements for element with id " + branchRoot.getIdentifier() + " is not found ");
      }

   }
}
