/*
 * 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.exoplatform.services.jcr.impl.core.query;

import org.exoplatform.container.configuration.ConfigurationManager;
import org.exoplatform.services.jcr.config.QueryHandlerEntry;
import org.exoplatform.services.jcr.dataflow.ItemState;
import org.exoplatform.services.jcr.dataflow.ItemStateChangesLog;
import org.exoplatform.services.jcr.dataflow.persistent.ItemsPersistenceListener;
import org.exoplatform.services.jcr.impl.core.query.lucene.ChangesHolder;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @author <a href="mailto:Sergey.Kabashnyuk@exoplatform.org">Sergey Kabashnyuk</a>
 * @version $Id: exo-jboss-codetemplates.xml 34360 2009-07-22 23:58:59Z ksm $
 *
 */
public abstract class IndexerChangesFilter implements ItemsPersistenceListener
{
   /**
    * Logger instance for this class
    */
   private static final Log log = ExoLogger.getLogger("exo.jcr.component.core.IndexerChangesFilter");

   protected final SearchManager searchManager;

   protected final QueryHandlerEntry config;

   protected final QueryHandler handler;

   protected final QueryHandler parentHandler;

   protected final IndexingTree indexingTree;

   protected final SearchManager parentSearchManager;

   protected final IndexingTree parentIndexingTree;

   /**
    * @param searchManager
    * @param isSystem 
    * @param handler
    * @param indexingTree
    */
   public IndexerChangesFilter(SearchManager searchManager, SearchManager parentSearchManager,
      QueryHandlerEntry config, IndexingTree indexingTree, IndexingTree parentIndexingTree, QueryHandler handler,
      QueryHandler parentHandler, ConfigurationManager cfm)
   {
      super();
      this.searchManager = searchManager;
      this.parentSearchManager = parentSearchManager;
      this.config = config;
      this.parentIndexingTree = parentIndexingTree;
      this.indexingTree = indexingTree;
      this.handler = handler;
      this.parentHandler = parentHandler;
      if (log.isDebugEnabled())
      {
         log.debug("Will the indexing being enrolled in global transactions : " + isTXAware());
      }
   }

   /**
    * @return the handler
    */
   public QueryHandler getHandler()
   {
      return handler;
   }

   /**
    * @return the indexingTree
    */
   public IndexingTree getIndexingTree()
   {
      return indexingTree;
   }

   /**
    * @return the searchManager
    */
   public SearchManager getSearchManager()
   {
      return searchManager;
   }

   /**
    * {@inheritDoc}
    */
   public void onSaveItems(ItemStateChangesLog itemStates)
   {
      // nodes that need to be removed from the index.
      final Set<String> removedNodes = new HashSet<String>();
      // nodes that need to be added to the index.
      final Set<String> addedNodes = new HashSet<String>();
      //updated
      final Map<String, List<ItemState>> updatedNodes = new HashMap<String, List<ItemState>>();

      final Set<String> parentRemovedNodes = new HashSet<String>();
      // nodes that need to be added to the index.
      final Set<String> parentAddedNodes = new HashSet<String>();
      //updated
      final Map<String, List<ItemState>> parentUpdatedNodes = new HashMap<String, List<ItemState>>();

      for (Iterator<ItemState> iter = itemStates.getAllStates().iterator(); iter.hasNext();)
      {
         ItemState itemState = iter.next();

         if (itemState.isPersisted())
         {
            if (!indexingTree.isExcluded(itemState))
            {
               acceptChanges(removedNodes, addedNodes, updatedNodes, itemState);
            }
            else if (parentIndexingTree != null && !parentIndexingTree.isExcluded(itemState))
            {
               acceptChanges(parentRemovedNodes, parentAddedNodes, parentUpdatedNodes, itemState);
            }
         }
      }

      for (String uuid : updatedNodes.keySet())
      {
         removedNodes.add(uuid);
         addedNodes.add(uuid);
      }

      for (String uuid : parentUpdatedNodes.keySet())
      {
         parentRemovedNodes.add(uuid);
         parentAddedNodes.add(uuid);
      }

      doUpdateIndex(removedNodes, addedNodes, parentRemovedNodes, parentAddedNodes);
   }

   /**
    * Frees resources associated with changes filter
    */
   public void close()
   {
   }

   /**
    * @param removedNodes
    * @param addedNodes
    * @param updatedNodes
    * @param itemState
    */
   private void acceptChanges(final Set<String> removedNodes, final Set<String> addedNodes,
      final Map<String, List<ItemState>> updatedNodes, ItemState itemState)
   {
      String uuid =
         itemState.isNode() ? itemState.getData().getIdentifier() : itemState.getData().getParentIdentifier();

      if (itemState.isNode())
      {
         if (itemState.isAdded())
         {
            addedNodes.add(uuid);
         }
         else if (itemState.isRenamed() || itemState.isUpdated() || itemState.isMixinChanged())
         {
            createNewOrAdd(uuid, itemState, updatedNodes);
         }
         else if (itemState.isDeleted())
         {
            addedNodes.remove(uuid);
            removedNodes.add(uuid);

            // remove all changes after node remove
            updatedNodes.remove(uuid);
         }
      }
      else
      {
         if (itemState.isAdded() || itemState.isUpdated() || itemState.isDeleted())
         {
            if (!addedNodes.contains(uuid) && !removedNodes.contains(uuid) && !updatedNodes.containsKey(uuid))
            {
               createNewOrAdd(uuid, itemState, updatedNodes);
            }
         }
      }
   }

   /**
    * Update index.
    * @param removedNodes
    * @param addedNodes
    */
   protected void doUpdateIndex(Set<String> removedNodes, Set<String> addedNodes, Set<String> parentRemovedNodes,
      Set<String> parentAddedNodes)
   {
      ChangesHolder changes = searchManager.getChanges(removedNodes, addedNodes);
      ChangesHolder parentChanges = parentSearchManager.getChanges(parentRemovedNodes, parentAddedNodes);

      if (changes == null && parentChanges == null)
      {
         return;
      }

      try
      {
         doUpdateIndex(new ChangesFilterListsWrapper(changes, parentChanges));
      }
      catch (RuntimeException e)
      {
         if (isTXAware())
         {
            // The indexing is part of the global tx so the error needs to be thrown to
            // allow to roll back other resources
            throw e;
         }
         getLogger().error(e.getLocalizedMessage(), e);
         logErrorChanges(handler, removedNodes, addedNodes);
         logErrorChanges(parentHandler, parentRemovedNodes, parentAddedNodes);
      }
   }

   protected void doUpdateIndex(ChangesFilterListsWrapper changes)
   {
   }

   protected Log getLogger()
   {
      return log;
   }

   protected void logErrorChanges(QueryHandler logHandler, Set<String> removedNodes, Set<String> addedNodes)
   {
      try
      {
         logHandler.logErrorChanges(addedNodes, removedNodes);
      }
      catch (IOException ioe)
      {
         getLogger().warn("Exception occure when errorLog writed. Error log is not complete. " + ioe, ioe);
      }
   }

   private void createNewOrAdd(String key, ItemState state, Map<String, List<ItemState>> updatedNodes)
   {
      List<ItemState> list = updatedNodes.get(key);
      if (list == null)
      {
         list = new ArrayList<ItemState>();
         updatedNodes.put(key, list);
      }
      list.add(state);

   }

   /**
    * {@inheritDoc}
    */
   public boolean isTXAware()
   {
      return false;
   }

   /**
    * If index is shared across the cluster, which means that all modifications or state changes must be
    * replicated along cluster. I.e. when hot reindexing is started, the index marked as "shared" will be
    * set in offline mode within whole cluster. But non-shared index, will only be switched to offline 
    * locally  
    * 
    * @return
    */
   public boolean isShared()
   {
      return false;
   }
}
