/*
 * 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.lock;

import org.exoplatform.management.annotations.Managed;
import org.exoplatform.management.annotations.ManagedDescription;
import org.exoplatform.management.jmx.annotations.NameTemplate;
import org.exoplatform.management.jmx.annotations.Property;
import org.exoplatform.services.jcr.access.SystemIdentity;
import org.exoplatform.services.jcr.config.WorkspaceEntry;
import org.exoplatform.services.jcr.core.ExtendedSession;
import org.exoplatform.services.jcr.dataflow.ChangesLogIterator;
import org.exoplatform.services.jcr.dataflow.CompositeChangesLog;
import org.exoplatform.services.jcr.dataflow.DataManager;
import org.exoplatform.services.jcr.dataflow.ItemState;
import org.exoplatform.services.jcr.dataflow.ItemStateChangesLog;
import org.exoplatform.services.jcr.dataflow.PlainChangesLog;
import org.exoplatform.services.jcr.dataflow.PlainChangesLogImpl;
import org.exoplatform.services.jcr.dataflow.TransactionChangesLog;
import org.exoplatform.services.jcr.dataflow.persistent.ItemsPersistenceListener;
import org.exoplatform.services.jcr.dataflow.persistent.PersistedPropertyData;
import org.exoplatform.services.jcr.datamodel.InternalQName;
import org.exoplatform.services.jcr.datamodel.ItemData;
import org.exoplatform.services.jcr.datamodel.ItemType;
import org.exoplatform.services.jcr.datamodel.NodeData;
import org.exoplatform.services.jcr.datamodel.PropertyData;
import org.exoplatform.services.jcr.datamodel.QPathEntry;
import org.exoplatform.services.jcr.impl.Constants;
import org.exoplatform.services.jcr.impl.core.NodeImpl;
import org.exoplatform.services.jcr.impl.core.SessionDataManager;
import org.exoplatform.services.jcr.impl.core.SessionImpl;
import org.exoplatform.services.jcr.impl.dataflow.TransientItemData;
import org.exoplatform.services.jcr.impl.dataflow.TransientPropertyData;
import org.exoplatform.services.jcr.impl.dataflow.persistent.WorkspacePersistentDataManager;
import org.exoplatform.services.jcr.observation.ExtendedEvent;
import org.exoplatform.services.jcr.util.IdGenerator;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.picocontainer.Startable;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
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 javax.jcr.AccessDeniedException;
import javax.jcr.RepositoryException;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.lock.Lock;
import javax.jcr.lock.LockException;

/**
 * Created by The eXo Platform SAS.
 * 
 * @author <a href="mailto:gennady.azarenkov@exoplatform.com">Gennady Azarenkov</a>
 * @version $Id: LockManagerImpl.java 12096 2008-03-19 11:42:40Z gazarenkov $
 */
@Managed
@NameTemplate(@Property(key = "service", value = "lockmanager"))
public class LockManagerImpl implements WorkspaceLockManager, ItemsPersistenceListener, Startable
{
   /**
    * Default lock time out. 30min
    */
   public static final long DEFAULT_LOCK_TIMEOUT = 1000 * 60 * 30;

   // Search constants
   /**
    * The exact lock token.
    */
   private static final int SEARCH_EXECMATCH = 1;

   /**
    * Lock token of closed parent
    */
   private static final int SEARCH_CLOSEDPARENT = 2;

   /**
    * Lock token of closed child
    */
   private static final int SEARCH_CLOSEDCHILD = 4;

   /**
    * Logger
    */
   private final Log log = ExoLogger.getLogger("exo.jcr.component.core.LockManagerImpl");

   /**
    * Map NodeIdentifier -- lockData
    */
   private final Map<String, LockData> locks;

   /**
    * Data manager.
    */
   private final DataManager dataManager;

   /**
    * Map NodeIdentifier -- lockData
    */
   private final Map<String, LockData> pendingLocks;

   /**
    * Map lockToken --lockData
    */
   private final Map<String, LockData> tokensMap;

   /**
    * Run time lock time out.
    */
   private long lockTimeOut;

   /**
    * Lock remover thread.
    */
   private LockRemover lockRemover;

   /**
    * Lock persister instance.
    */
   private final LockPersister persister;

   /**
    * Constructor for workspace without LockPersister
    * 
    * @param dataManager
    * @param config
    */
   public LockManagerImpl(WorkspacePersistentDataManager dataManager, WorkspaceEntry config,
      LockRemoverHolder lockRemoverHolder)
   {
      this(dataManager, config, null, lockRemoverHolder);
   }

   public LockManagerImpl(WorkspacePersistentDataManager dataManager, WorkspaceEntry config, LockPersister persister,
      LockRemoverHolder lockRemoverHolder)
   {

      this.dataManager = dataManager;
      this.persister = persister;
      if (config.getLockManager() != null)
      {
         lockTimeOut =
            config.getLockManager().getTimeout() > 0 ? config.getLockManager().getTimeout() : DEFAULT_LOCK_TIMEOUT;
      }
      else
         lockTimeOut = DEFAULT_LOCK_TIMEOUT;

      locks = new HashMap<String, LockData>();
      pendingLocks = new HashMap<String, LockData>();
      tokensMap = new HashMap<String, LockData>();

      dataManager.addItemPersistenceListener(this);
      lockRemover = lockRemoverHolder.getLockRemover(this);
   }

   public synchronized void addLockToken(String sessionId, String lt)
   {
      LockData currLock = tokensMap.get(lt);
      if (currLock != null)
      {
         currLock.addLockHolder(sessionId);
      }
   }

   public synchronized Lock addPendingLock(NodeImpl node, boolean isDeep, boolean isSessionScoped, long timeOut)
      throws LockException
   {
      LockData lData = getLockData((NodeData)node.getData(), SEARCH_EXECMATCH | SEARCH_CLOSEDPARENT);
      if (lData != null)
      {
         if (lData.getNodeIdentifier().equals(node.getInternalIdentifier()))
         {
            throw new LockException("Node already locked: " + node.getData().getQPath());
         }
         else if (lData.isDeep())
         {
            throw new LockException("Parent node has deep lock.");
         }
      }

      if (isDeep && getLockData((NodeData)node.getData(), SEARCH_CLOSEDCHILD) != null)
      {
         throw new LockException("Some child node is locked.");
      }

      String lockToken = IdGenerator.generate();
      lData =
         new LockData(node.getInternalIdentifier(), lockToken, isDeep, isSessionScoped, node.getSession().getUserID(),
            timeOut > 0 ? timeOut : lockTimeOut);

      lData.addLockHolder(node.getSession().getId());
      pendingLocks.put(node.getInternalIdentifier(), lData);
      tokensMap.put(lockToken, lData);

      LockImpl lock = new LockImpl(node.getSession(), lData);
      return lock;
   }

   public LockImpl getLock(NodeImpl node) throws LockException, RepositoryException
   {

      LockData lData = getLockData((NodeData)node.getData(), SEARCH_EXECMATCH | SEARCH_CLOSEDPARENT);

      if (lData == null || (!node.getInternalIdentifier().equals(lData.getNodeIdentifier()) && !lData.isDeep()))
      {
         throw new LockException("Node not locked: " + node.getData().getQPath());

      }
      return new LockImpl(node.getSession(), lData);
   }

   public synchronized String[] getLockTokens(String sessionID)
   {
      List<String> retval = new ArrayList<String>();

      for (LockData lockData : locks.values())
      {
         if (lockData.isLockHolder(sessionID))
            retval.add(lockData.getLockToken(sessionID));

      }
      return retval.toArray(new String[retval.size()]);
   }

   public boolean holdsLock(NodeData node) throws RepositoryException
   {
      return getLockData(node, SEARCH_EXECMATCH) != null;
   }

   public boolean isLocked(NodeData node)
   {
      LockData lData = getLockData(node, SEARCH_EXECMATCH | SEARCH_CLOSEDPARENT);

      if (lData == null || (!node.getIdentifier().equals(lData.getNodeIdentifier()) && !lData.isDeep()))
      {
         return false;
      }
      return true;
   }

   public boolean isLockHolder(NodeData node, String sessionId)// throws RepositoryException
   {
      LockData lData = getLockData(node, SEARCH_EXECMATCH | SEARCH_CLOSEDPARENT);
      return lData != null && lData.isLockHolder(sessionId);
   }

   public synchronized void onCloseSession(ExtendedSession session)
   {
      // List<String> deadLocksList = new ArrayList<String>();
      SessionImpl sessionImpl = (SessionImpl)session;
      for (Iterator<Map.Entry<String, LockData>> entries = locks.entrySet().iterator(); entries.hasNext();)
      {
         Map.Entry<String, LockData> entry = entries.next();
         LockData lockData = entry.getValue();
         if (lockData.isLive())
         {
            if (lockData.isLockHolder(session.getId()))
            {
               if (lockData.isSessionScoped())
               {
                  // if no session currently holds lock except this
                  try
                  {
                     // TODO it's possible to have next error
                     // java.lang.NullPointerException
                     // at
                     // org.exoplatform.services.jcr.impl.core.lock.LockManagerImpl.onCloseSession(LockManagerImpl.java:312)
                     // at org.exoplatform.services.jcr.impl.core.SessionImpl.logout(SessionImpl.java:794)
                     // at
                     // org.exoplatform.services.jcr.impl.core.XASessionImpl.logout(XASessionImpl.java:254)
                     // at
                     // org.exoplatform.services.jcr.impl.core.SessionRegistry$SessionCleaner.callPeriodically(SessionRegistry.java:165)
                     // at
                     // org.exoplatform.services.jcr.impl.proccess.WorkerThread.run(WorkerThread.java:46)
                     ((NodeImpl)sessionImpl.getTransientNodesManager().getItemByIdentifier(
                        lockData.getNodeIdentifier(), false)).unlock();
                  }
                  catch (UnsupportedRepositoryOperationException e)
                  {
                     log.error(e.getLocalizedMessage());
                  }
                  catch (LockException e)
                  {
                     log.error(e.getLocalizedMessage());
                  }
                  catch (AccessDeniedException e)
                  {
                     log.error(e.getLocalizedMessage());
                  }
                  catch (RepositoryException e)
                  {
                     log.error(e.getLocalizedMessage());
                  }

               }
               else
               {
                  lockData.removeLockHolder(session.getId());
               }
            }
         }
         else
         {
            entries.remove();
         }
      }
   }

   /**
    * {@inheritDoc}
    */
   public void onSaveItems(ItemStateChangesLog changesLog)
   {
      List<PlainChangesLog> chengesLogList = new ArrayList<PlainChangesLog>();
      if (changesLog instanceof TransactionChangesLog)
      {
         ChangesLogIterator logIterator = ((TransactionChangesLog)changesLog).getLogIterator();

         while (logIterator.hasNextLog())
         {
            chengesLogList.add(logIterator.nextLog());
         }
      }
      else if (changesLog instanceof PlainChangesLog)
      {
         chengesLogList.add((PlainChangesLog)changesLog);
      }
      else if (changesLog instanceof CompositeChangesLog)
      {
         for (ChangesLogIterator iter = ((CompositeChangesLog)changesLog).getLogIterator(); iter.hasNextLog();)
         {
            chengesLogList.add(iter.nextLog());
         }
      }

      for (PlainChangesLog currChangesLog : chengesLogList)
      {
         String nodeIdentifier;
         try
         {
            switch (currChangesLog.getEventType())
            {
               case ExtendedEvent.LOCK :
                  if (currChangesLog.getSize() < 2)
                  {
                     log.error("Incorrect changes log  of type ExtendedEvent.LOCK size=" + currChangesLog.getSize()
                        + "<2 \n" + currChangesLog.dump());
                     break;
                  }
                  nodeIdentifier = currChangesLog.getAllStates().get(0).getData().getParentIdentifier();

                  if (pendingLocks.containsKey(nodeIdentifier))
                  {
                     internalLock(nodeIdentifier);
                  }
                  else
                  {
                     log.warn("No lock in pendingLocks for identifier " + nodeIdentifier
                        + " Probably lock come from replication.");

                     String lockToken = IdGenerator.generate();
                     ItemState ownerState = getItemState(currChangesLog, Constants.JCR_LOCKOWNER);
                     ItemState isDeepState = getItemState(currChangesLog, Constants.JCR_LOCKISDEEP);
                     if (ownerState != null && isDeepState != null)
                     {

                        String owner =
                           new String(((((PersistedPropertyData)(ownerState.getData())).getValues()).get(0))
                              .getAsByteArray(), Constants.DEFAULT_ENCODING);

                        boolean isDeep =
                           Boolean.valueOf(
                              new String(((((PersistedPropertyData)(isDeepState.getData())).getValues()).get(0))
                                 .getAsByteArray(), Constants.DEFAULT_ENCODING)).booleanValue();

                        createRemoteLock(currChangesLog.getSessionId(), nodeIdentifier, lockToken, isDeep, false, owner);
                     }
                  }
                  break;
               case ExtendedEvent.UNLOCK :
                  if (currChangesLog.getSize() < 2)
                  {
                     log.error("Incorrect changes log  of type ExtendedEvent.UNLOCK size=" + currChangesLog.getSize()
                        + "<2 \n" + currChangesLog.dump());
                     break;
                  }

                  internalUnLock(currChangesLog.getSessionId(), currChangesLog.getAllStates().get(0).getData()
                     .getParentIdentifier());
                  break;
               default :
                  HashSet<String> removedLock = new HashSet<String>();
                  for (ItemState itemState : currChangesLog.getAllStates())
                  {
                     // this is a node and node is locked
                     if (itemState.getData().isNode() && locks.containsKey(itemState.getData().getIdentifier()))
                     {
                        nodeIdentifier = itemState.getData().getIdentifier();
                        if (itemState.isDeleted())
                        {
                           removedLock.add(nodeIdentifier);
                        }
                        else if (itemState.isAdded() || itemState.isRenamed() || itemState.isUpdated())
                        {
                           removedLock.remove(nodeIdentifier);
                        }
                     }
                  }
                  for (String identifier : removedLock)
                  {
                     internalUnLock(currChangesLog.getSessionId(), identifier);
                  }
                  break;
            }
         }
         catch (LockException e)
         {
            log.error(e.getLocalizedMessage(), e);
         }
         catch (UnsupportedEncodingException e)
         {
            log.error(e.getLocalizedMessage(), e);
         }
         catch (IllegalStateException e)
         {
            log.error(e.getLocalizedMessage(), e);
         }
         catch (IOException e)
         {
            log.error(e.getLocalizedMessage(), e);
         }
      }
   }

   /**
    * @param sessionId
    * @param lt
    */
   public synchronized void removeLockToken(String sessionId, String lt)
   {
      LockData lData = tokensMap.get(lt);
      if (lData != null && lData.isLockHolder(sessionId))
      {
         lData.removeLockHolder(sessionId);
      }
   }

   /**
    * {@inheritDoc}
    */
   public void start()
   {
      lockRemover.start();
   }

   // Quick method. We need to reconstruct
   synchronized List<LockData> getLockList()
   {
      return new ArrayList<LockData>(locks.values());
   }

   /**
    * {@inheritDoc}
    */
   public synchronized void removeExpired()
   {
      final List<String> removeLockList = new ArrayList<String>();

      for (LockData lock : locks.values())
      {
         if (!lock.isSessionScoped() && lock.getTimeToDeath() < 0)
         {
            removeLockList.add(lock.getNodeIdentifier());
         }
      }

      for (String rLock : removeLockList)
      {
         removeLock(rLock);
      }
   }

   /**
    * {@inheritDoc}
    */
   public void stop()
   {
      lockRemover.stop();

      locks.clear();
      pendingLocks.clear();
      tokensMap.clear();
   }

   /**
    * Copy <code>PropertyData prop<code> to new TransientItemData
    * 
    * @param prop
    * @return
    * @throws RepositoryException
    */
   private TransientItemData copyItemData(PropertyData prop) throws RepositoryException
   {

      if (prop == null)
         return null;

      // make a copy, value may be null for deleting items
      TransientPropertyData newData =
         new TransientPropertyData(prop.getQPath(), prop.getIdentifier(), prop.getPersistedVersion(), prop.getType(),
            prop.getParentIdentifier(), prop.isMultiValued(), prop.getValues());

      return newData;
   }

   /**
    * Search item with name <code>itemName<code> in changesLog
    * 
    * @param changesLog
    * @param itemName
    * @return Item
    */
   private ItemState getItemState(PlainChangesLog changesLog, InternalQName itemName)
   {
      List<ItemState> allStates = changesLog.getAllStates();
      for (int i = allStates.size() - 1; i >= 0; i--)
      {
         ItemState state = allStates.get(i);
         if (state.getData().getQPath().getName().equals(itemName))
            return state;
      }
      return null;
   }

   /**
    * Search lock in maps.
    * 
    * @param data
    * @param searchType
    * @return
    */
   private LockData getLockData(NodeData data, int searchType)
   {
      if (data == null || locks.size() == 0)
         return null;
      LockData retval = null;
      try
      {
         if ((searchType & SEARCH_EXECMATCH) != 0)
         {
            retval = locks.get(data.getIdentifier());
         }
         if (retval == null && (searchType & SEARCH_CLOSEDPARENT) != 0)
         {

            NodeData parentData = (NodeData)dataManager.getItemData(data.getParentIdentifier());
            if (parentData != null)
            {
               retval = locks.get(parentData.getIdentifier());
               // parent not found try to fo upper
               if (retval == null)
               {
                  retval = getLockData(parentData, SEARCH_CLOSEDPARENT);
               }
            }
         }
         if (retval == null && (searchType & SEARCH_CLOSEDCHILD) != 0)
         {

            List<NodeData> childData = dataManager.getChildNodesData(data);
            for (NodeData nodeData : childData)
            {
               retval = locks.get(nodeData.getIdentifier());
               if (retval != null)
                  break;
            }
            if (retval == null)
            {
               // child not found try to find diper
               for (NodeData nodeData : childData)
               {
                  retval = getLockData(nodeData, SEARCH_CLOSEDCHILD);
                  if (retval != null)
                     break;
               }
            }
         }
      }
      catch (RepositoryException e)
      {
         return null;
      }

      return retval;
   }

   /**
    * Internal lock
    * 
    * @param nodeIdentifier
    * @throws LockException
    */
   private synchronized void internalLock(String nodeIdentifier) throws LockException
   {
      LockData ldata = pendingLocks.get(nodeIdentifier);
      if (ldata != null)
      {
         locks.put(nodeIdentifier, ldata);

         if (persister != null)
         {
            persister.add(ldata);
         }
         pendingLocks.remove(nodeIdentifier);
      }
      else
      {
         throw new LockException("No lock in pending locks");
      }
   }

   /**
    * Internal unlock.
    * 
    * @param sessionId
    * @param nodeIdentifier
    * @throws LockException
    */
   private synchronized void internalUnLock(String sessionId, String nodeIdentifier) throws LockException
   {
      LockData lData = locks.get(nodeIdentifier);

      if (lData != null)
      {
         tokensMap.remove(lData.getLockToken(sessionId));
         locks.remove(nodeIdentifier);

         lData.setLive(false);
         if (persister != null)
         {
            persister.remove(lData);
         }
         lData = null;
      }
   }

   /**
    * For locks comes from remote JCRs (replication usecase)
    * 
    * @param sessionId
    *          String
    * @param nodeIdentifier
    *          String
    * @param lockToken
    *          String
    * @param isDeep
    *          boolean
    * @param sessionScoped
    *          boolean
    * @param owner
    *          String
    * @return LockData
    */
   private synchronized LockData createRemoteLock(String sessionId, String nodeIdentifier, String lockToken,
      boolean isDeep, boolean sessionScoped, String owner)
   {
      LockData lData = new LockData(nodeIdentifier, lockToken, isDeep, sessionScoped, owner, lockTimeOut);
      lData.addLockHolder(sessionId);
      locks.put(nodeIdentifier, lData);
      tokensMap.put(lockToken, lData);

      return lData;
   }

   /**
    * Remove lock, used by Lock remover.
    * 
    * @param nodeIdentifier String
    */
   protected void removeLock(String nodeIdentifier)
   {
      try
      {
         NodeData nData = (NodeData)dataManager.getItemData(nodeIdentifier);
         PlainChangesLog changesLog =
            new PlainChangesLogImpl(new ArrayList<ItemState>(), SystemIdentity.SYSTEM, ExtendedEvent.UNLOCK);

         ItemData lockOwner =
            copyItemData((PropertyData)dataManager.getItemData(nData, new QPathEntry(Constants.JCR_LOCKOWNER, 1),
               ItemType.PROPERTY));

         changesLog.add(ItemState.createDeletedState(lockOwner));

         ItemData lockIsDeep =
            copyItemData((PropertyData)dataManager.getItemData(nData, new QPathEntry(Constants.JCR_LOCKISDEEP, 1),
               ItemType.PROPERTY));
         changesLog.add(ItemState.createDeletedState(lockIsDeep));

         // lock probably removed by other thread
         if (lockOwner == null && lockIsDeep == null)
            return;
         dataManager.save(new TransactionChangesLog(changesLog));
      }
      catch (RepositoryException e)
      {
         log.error("Error occur during removing lock" + e.getLocalizedMessage());
      }
   }

   @Managed
   @ManagedDescription("The number of active locks")
   public int getNumLocks()
   {
      return locks.size();
   }

   @Managed
   @ManagedDescription("Remove the expired locks")
   public void cleanExpiredLocks()
   {
      removeExpired();
   }

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

   /**
    * {@inheritDoc}
    */
   public SessionLockManager getSessionLockManager(String sessionId, SessionDataManager transientManager)
   {
      return new SessionLockManagerImpl(sessionId, this, transientManager);
   }

   /**
    * {@inheritDoc}
    */
   public void closeSessionLockManager(String sessionId)
   {
      //do nothing
   }
}
