/*
 * 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.exoplatform.ecms.xcmis.sp.index;

import org.exoplatform.ecms.xcmis.sp.JcrCMIS;
import org.exoplatform.ecms.xcmis.sp.StorageClosableImpl;
import org.exoplatform.ecms.xcmis.sp.StorageConfiguration;
import org.exoplatform.ecms.xcmis.sp.StorageProviderImpl;
import org.exoplatform.services.document.DocumentReaderService;
import org.exoplatform.services.jcr.core.ManageableRepository;
import org.exoplatform.services.jcr.core.NamespaceAccessor;
import org.exoplatform.services.jcr.dataflow.ItemState;
import org.exoplatform.services.jcr.dataflow.ItemStateChangesLog;
import org.exoplatform.services.jcr.dataflow.PersistentDataManager;
import org.exoplatform.services.jcr.dataflow.persistent.ItemsPersistenceListener;
import org.exoplatform.services.jcr.datamodel.ItemData;
import org.exoplatform.services.jcr.datamodel.NodeData;
import org.exoplatform.services.jcr.datamodel.PropertyData;
import org.exoplatform.services.jcr.ext.app.SessionProviderService;
import org.exoplatform.services.jcr.ext.common.SessionProvider;
import org.exoplatform.services.jcr.impl.Constants;
import org.exoplatform.services.jcr.impl.core.LocationFactory;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.xcmis.search.SearchService;
import org.xcmis.search.SearchServiceException;
import org.xcmis.search.config.IndexConfiguration;
import org.xcmis.search.config.SearchServiceConfiguration;
import org.xcmis.search.content.ContentEntry;
import org.xcmis.search.content.IndexModificationException;
import org.xcmis.search.value.ToStringNameConverter;
import org.xcmis.spi.DocumentData;
import org.xcmis.spi.ObjectData;
import org.xcmis.spi.ObjectNotFoundException;
import org.xcmis.spi.PermissionService;
import org.xcmis.spi.Storage;

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

import javax.jcr.PropertyIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;

/**
 * @author <a href="mailto:foo@bar.org">Foo Bar</a>
 * @version $Id: Jcr2XcmisChangesListener.java 34360 2009-07-22 23:58:59Z
 *          aheritier $
 *
 */
public class Jcr2XcmisChangesListener implements ItemsPersistenceListener
{

   private static final Log LOG = ExoLogger.getExoLogger(Jcr2XcmisChangesListener.class);

   private final String currentRepositoryName;

   private final String workspaceName;

   private final SessionProviderService sessionProviderService;

   private final ManageableRepository repository;

   private SearchService searchService;

   private final PersistentDataManager dataManager;

   private Storage rootStorage;

   private final LocationFactory locationFactory;

   private final ContentEntryAdapter contentEntryAdapter;

   // not used
   private final DocumentReaderService documentReaderService;


   public Jcr2XcmisChangesListener(String currentRepositoryName, String workspaceName,
      PersistentDataManager dataManager, SessionProviderService sessionProviderService,
      ManageableRepository repository, NamespaceAccessor namespaceAccessor, DocumentReaderService documentReaderService)
   {
      super();
      this.currentRepositoryName = currentRepositoryName;
      this.workspaceName = workspaceName;
      this.dataManager = dataManager;
      this.sessionProviderService = sessionProviderService;
      this.repository = repository;
      this.documentReaderService = documentReaderService;
      this.locationFactory = new LocationFactory(namespaceAccessor);
      this.contentEntryAdapter = new ContentEntryAdapter();
   }

   /**
    * @return the searchService
    */
   public SearchService getSearchService()
   {
      return searchService;
   }

   /**
    * @see org.exoplatform.services.jcr.dataflow.persistent.ItemsPersistenceListener#isTXAware()
    */
   public boolean isTXAware()
   {
      return false;
   }

   /**
    * @see org.exoplatform.services.jcr.dataflow.persistent.ItemsPersistenceListener#onSaveItems(org.exoplatform.services.jcr.dataflow.ItemStateChangesLog)
    */
   public void onSaveItems(ItemStateChangesLog itemStates)
   {
      applyChangesLog(itemStates);
   }

   private void applyChangesLog(ItemStateChangesLog itemStates)
   {
      if (searchService != null)
      {
         // 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>>();

         String versionHistoryId = null;
         String linkedUUid = null;
         
         for (ItemState itemState : itemStates.getAllStates())
         {
            if (itemState.getData().isNode() && itemState.getData() instanceof NodeData && itemState.isDeleted()) {
                NodeData nodeData = (NodeData) itemState.getData();

                if (nodeData.getPrimaryTypeName().equals(Constants.NT_VERSION)) {
                      
                      String v = nodeData.getQPath().getName().getName();
                                 
                        if (v.equals("rootVersion")) {
                           // save parent for the root version delete
                           if (versionHistoryId != null) {
                              removedNodes.add(versionHistoryId + JcrCMIS.ID_SEPARATOR + "1");
                              versionHistoryId = null;
                           }
                           versionHistoryId = nodeData.getParentIdentifier();
                        } else {
                           Integer versionInt = null;
                           try {
                               versionInt = Integer.parseInt(v);
                           } catch (NumberFormatException e) {
                           }
                           if (versionInt != null) {
                              String versionId = nodeData.getParentIdentifier() + JcrCMIS.ID_SEPARATOR + v;
                              if (versionHistoryId != null) {
                                 if (versionId.startsWith(versionHistoryId)) {
                                    String rootVersionId = nodeData.getParentIdentifier() + JcrCMIS.ID_SEPARATOR + String.valueOf(versionInt + 1);
                                    removedNodes.add(rootVersionId);      
                                 } else {
                                    removedNodes.add(versionHistoryId + JcrCMIS.ID_SEPARATOR + "1");
                                 }
                                 versionHistoryId = null;
                              }
                              removedNodes.add(versionId);      
                           }
                        }
                        
                } else if (nodeData.getPrimaryTypeName().getAsString().equalsIgnoreCase("[http://www.exoplatform.com/jcr/xcmis/1.0]linkedFile")) {
                   if (linkedUUid != null) 
                   {
                      removedNodes.add(linkedUUid);
                      addedNodes.add(linkedUUid);
                   }
                }
            } else if(!itemState.getData().isNode() && itemState.isDeleted()) {
               PropertyData propertyData = (PropertyData) itemState.getData();
               
               // Parse the path as example:
               // "[]:1[http://www.exoplatform.com/jcr/exo/1.0]drives:1[]driveA:1" + 
               // "[]multifilingFolderTest1:1[]multifilingDocumentTest:1[]cmisMultifilingObjectId_d2abd7987f00010110f98160154c674d:1"
               String qPath = propertyData.getQPath().getAsString();
               if (qPath.contains("[]" + JcrCMIS.JCR_MULTIFILING_PROPERTY_PREFIX)) {
                  int beginIndex = qPath.lastIndexOf(JcrCMIS.JCR_MULTIFILING_PROPERTY_PREFIX) + JcrCMIS.JCR_MULTIFILING_PROPERTY_PREFIX.length();
                  int endIndex = qPath.lastIndexOf(":");
                  linkedUUid = qPath.substring(beginIndex, endIndex);
               }
            }
            
            try
            {
               acceptChanges(removedNodes, addedNodes, updatedNodes, itemState);
            }
            catch (RepositoryException e)
            {
               if (LOG.isDebugEnabled())
               {
                  LOG.debug(e.getLocalizedMessage(), e);
               }
            }
            catch (IllegalStateException e)
            {
               if (LOG.isDebugEnabled())
               {
                  LOG.debug(e.getLocalizedMessage(), e);
               }
            }
            catch (IOException e)
            {
               if (LOG.isDebugEnabled())
               {
                  LOG.debug(e.getLocalizedMessage(), e);
               }
            }
         }
         if (versionHistoryId != null) {
            removedNodes.add(versionHistoryId + JcrCMIS.ID_SEPARATOR + "1");
            versionHistoryId = null;
         }
         
         
         for (String uuid : updatedNodes.keySet())
         {
            removedNodes.add(uuid);
            addedNodes.add(uuid);
         }
         if (removedNodes.size() > 0 || addedNodes.size() > 0)
         {
            List<ContentEntry> addedEntries = new ArrayList<ContentEntry>(addedNodes.size());

            // process addedNodes to addedEntries
            for (String id : addedNodes)
            {
               try
               {
                  addEntry(id, addedEntries, removedNodes);
               }
               catch (ObjectNotFoundException e)
               {
                  if (LOG.isDebugEnabled())
                  {
                     LOG.debug(e.getLocalizedMessage(), e);
                  }
               }
               catch (IOException e)
               {
                  if (LOG.isDebugEnabled())
                  {
                     LOG.debug(e.getLocalizedMessage(), e);
                  }
               }
               catch (RepositoryException e)
               {
                  if (LOG.isDebugEnabled())
                  {
                     LOG.debug(e.getLocalizedMessage(), e);
                  }
               }
            }
            // MAIN PART in two updates: first remove one by one ignore exceptions, then add with a list.
            for (String uuid : removedNodes)
            {
               try
               {
                  searchService.update(null, uuid);
               }
               catch (IndexModificationException e)
               {
                  //LOG.error(e.getLocalizedMessage(), e);
               }
            }
            try
            {
               searchService.update(addedEntries, Collections.EMPTY_SET);
            }
            catch (IndexModificationException e)
            {
               LOG.error(e.getLocalizedMessage(), e);
            }
         }
      }
   }

   /**
    * Process the uuid from addedNodes to addedEntries.
    * Can be updated removedNodes.
    * 
    * @param uuid to be add.
    * @param addedEntries will be added in index.
    * @param removedNodes
    * @throws RepositoryException
    * @throws ObjectNotFoundException
    * @throws IOException
    */
   private void addEntry(String uuid, List<ContentEntry> addedEntries, Set<String> removedNodes)
      throws RepositoryException, ObjectNotFoundException, IOException
   {
      ItemData data = dataManager.getItemData(uuid);
      if (data != null && data.isNode())
      {

         String nodeTypeName = locationFactory.createJCRName(((NodeData)data).getPrimaryTypeName()).getAsString();
         if (((StorageClosableImpl)rootStorage).isSupportedNodeType(nodeTypeName))
         {
            ObjectData objectData = ((StorageClosableImpl)rootStorage).getObjectById(uuid);
            
            addedEntries.add(contentEntryAdapter.createEntry(objectData));
            
            if (objectData instanceof DocumentData) {
               String objectId = objectData.getObjectId(); 
               if (uuid != objectData.getObjectId()) {
                  // for multifiling: remove document from index before adding in it again
                  // for checkout/checkin: remove document from index before adding in it again
                  // for updating the document: to remove from index with appropriate id before adding in it again
                  // id must has ID_SEPARATOR since cannot be filled PWC
                  removedNodes.add(objectId);
               }
            }
            
         }
         else if (nodeTypeName.equals(JcrCMIS.JCR_XCMIS_LINKEDFILE))
         {
            List<PropertyData> list = dataManager.getChildPropertiesData((NodeData)data);
            
            PropertyData propertyWithId = null;
            for (Iterator<PropertyData> iter = list.iterator(); iter.hasNext() && propertyWithId == null;) 
            {
               PropertyData nextProperty = iter.next();
               // iterate while don't get the property with CMIS Object Id in the name.
               // xcmis:linkedFile extends nt:base which has two properties by default: jcr:primaryType and jcr:mixinTypes
               if (!nextProperty.getQPath().getAsString().contains(Constants.JCR_PRIMARYTYPE.getAsString()) && !nextProperty.getQPath().getAsString().contains(Constants.JCR_MIXINTYPES.getAsString())) {
                  propertyWithId = nextProperty;
               }
            }
            
            String linkedUUid = new String(propertyWithId.getValues().get(0).getAsByteArray());
            addEntry(linkedUUid, addedEntries, removedNodes);
         }
      }
   }

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

         if (itemState.isAdded())
         {
            if (itemState.isNode())
            {
               addedNodes.add(uuidNode);
            }
            else
            {
               addToUpdate(uuidNode, itemState, updatedNodes);
            }
         }
         else if (itemState.isRenamed())
         {
            if (itemState.isNode())
            {
               addedNodes.add(uuidNode);
            }
            else
            {
               addToUpdate(uuidNode, itemState, updatedNodes);
            }
         }
         else if (itemState.isUpdated())
         {
            addToUpdate(uuidNode, itemState, updatedNodes);
         }
         else if (itemState.isMixinChanged())
         {
            addToUpdate(uuidNode, itemState, updatedNodes);
         }
         else if (itemState.isDeleted())
         {
            if (itemState.isNode())
            {
               if (addedNodes.contains(uuidNode))
               {
                  addedNodes.remove(uuidNode);
               }
               removedNodes.add(uuidNode);
               updatedNodes.remove(uuidNode);
            }
            else
            {
               // if deleted some property in node uuidNode
               addToUpdate(uuidNode, itemState, updatedNodes);
            }
         }
      }
   }

   private void addToUpdate(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);

   }

   /**
    * Called on CmisRegistry start. It will initialize search service.
    *
    * @param cmisRegistry
    * @param readOnlyIndexConfiguration
    * @throws RepositoryException
    * @throws SearchServiceException
    */
   public void onRegistryStart(IndexConfiguration readOnlyIndexConfiguration) throws RepositoryException,
      SearchServiceException
   {

      if (readOnlyIndexConfiguration != null && rootStorage == null)
      {

         try {
	         StorageConfiguration rootStorageConfiguration =
	            new StorageConfiguration(UUID.randomUUID().toString(), currentRepositoryName, workspaceName, "/",
	               Collections.EMPTY_MAP, "Virtual root storage");
	         SessionProvider sessionProvider = sessionProviderService.getSystemSessionProvider(null);
	         
	      // to session check
            Session session = null;
            try
            {
               session = sessionProvider.getSession(workspaceName, repository);
            }
            finally
            {
               session.logout();
            }
            rootStorage =
             new StorageClosableImpl(sessionProvider, workspaceName, repository, rootStorageConfiguration, new PermissionService(),
                StorageProviderImpl.DEFAULT_NODETYPE_MAPPING);
	
	         //prepare search service
	         CmisSchema schema = new CmisSchema(rootStorage);
	         CmisSchemaTableResolver tableResolver =
	            new CmisSchemaTableResolver(new ToStringNameConverter(), schema, rootStorage);
	
	         File rootFolder = new File(readOnlyIndexConfiguration.getIndexDir());
	         File indexFolder = new File(new File(rootFolder, currentRepositoryName), workspaceName);
	         
	         IndexConfiguration indexConfiguration = new IndexConfiguration(indexFolder.getPath(), Constants.ROOT_PARENT_UUID, Constants.ROOT_UUID);
	
	         SearchServiceConfiguration configuration = new SearchServiceConfiguration(schema, tableResolver,
	        		 new CmisContentReader(rootStorage), indexConfiguration);
	
	         searchService = new SearchService(configuration);
	         searchService.start();
	         
    	 } catch (org.apache.tika.mime.MimeTypeException e) {
    	    throw new SearchServiceException(e.getLocalizedMessage(), e);
	     } catch (java.io.IOException e) {
		    throw new SearchServiceException(e.getLocalizedMessage(), e);
	     }
      }
   }

}
