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

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.SharedDataManager;
import org.exoplatform.services.jcr.dataflow.TransactionChangesLog;
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.core.SessionImpl;
import org.exoplatform.services.jcr.impl.core.itemfilters.QPathEntryFilter;
import org.exoplatform.services.jcr.impl.dataflow.persistent.LocalWorkspaceDataManagerStub;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.jcr.InvalidItemStateException;
import javax.jcr.RepositoryException;

/**
 * Created by The eXo Platform SAS.
 * 
 * @author Gennady Azarenkov
 * @version $Id: TransactionableDataManager.java 11907 2008-03-13 15:36:21Z ksm $
 */
public class TransactionableDataManager implements DataManager
{

   // use LocalWorkspaceDataManagerStub, otherwise JVM will use save(ItemStateChangesLog changes) instead of
   // VersionableWorkspaceDataManager.save(CompositeChangesLog changesLog) 
   private LocalWorkspaceDataManagerStub storageDataManager;

   protected static Log LOG = ExoLogger.getLogger("exo.jcr.component.core.TransactionableDataManager");

   private TransactionChangesLog transactionLog;

   private SessionImpl session;

   public TransactionableDataManager(LocalWorkspaceDataManagerStub dataManager, SessionImpl session)
      throws RepositoryException
   {
      super();
      this.session = session;
      this.storageDataManager = dataManager;
   }

   // --------------- ItemDataConsumer --------

   /**
    * {@inheritDoc}
    */
   public List<NodeData> getChildNodesData(NodeData parent) throws RepositoryException
   {
      List<NodeData> nodes = storageDataManager.getChildNodesData(parent);

      if (txStarted())
      {
         // merge data
         List<ItemState> txChanges = transactionLog.getChildrenChanges(parent.getIdentifier(), true);
         if (txChanges.size() > 0)
         {
            List<NodeData> res = new ArrayList<NodeData>(nodes);

            for (ItemState state : txChanges)
            {
               res.remove(state.getData());
               if (!state.isDeleted())
               {
                  res.add((NodeData)state.getData());
               }
            }

            return Collections.unmodifiableList(res);
         }
      }

      return nodes;
   }

   /**
    * {@inheritDoc}
    */
   public boolean getChildNodesDataByPage(final NodeData parent, int fromOrderNum, int offset,int pageSize, List<NodeData> childs)
      throws RepositoryException
   {
      boolean hasNext = storageDataManager.getChildNodesDataByPage(parent, fromOrderNum, offset,pageSize, childs);

      if (txStarted())
      {
         // merge data
         List<ItemState> txChanges = transactionLog.getChildrenChanges(parent.getIdentifier(), true);
         if (txChanges.size() > 0)
         {
            int minOrderNum = childs.size() != 0 ? childs.get(0).getOrderNumber() : -1;
            int maxOrderNum = childs.size() != 0 ? childs.get(childs.size() - 1).getOrderNumber() : -1;

            for (ItemState state : txChanges)
            {
               NodeData data = (NodeData)state.getData();

               if ((state.isAdded() || state.isRenamed() || state.isPathChanged()) && !hasNext)
               {
                  childs.add(data);
               }
               else if (state.isDeleted())
               {
                  childs.remove(data);
               }
               else if (state.isMixinChanged() || state.isUpdated())
               {
                  childs.remove(state.getData());
                  if (minOrderNum <= data.getOrderNumber() && data.getOrderNumber() <= maxOrderNum)
                  {
                     childs.add(data);
                  }
               }
            }
         }
      }

      return hasNext;
   }

   public List<NodeData> getChildNodesData(NodeData parent, List<QPathEntryFilter> patternFilters)
      throws RepositoryException
   {
      List<NodeData> nodes = storageDataManager.getChildNodesData(parent, patternFilters);

      if (txStarted())
      {
         // merge data
         List<ItemState> txChanges = transactionLog.getChildrenChanges(parent.getIdentifier(), true);
         if (txChanges.size() > 0)
         {
            List<NodeData> res = new ArrayList<NodeData>(nodes);

            for (ItemState state : txChanges)
            {
               res.remove(state.getData());
               if (!state.isDeleted())
               {
                  res.add((NodeData)state.getData());
               }
            }

            return Collections.unmodifiableList(res);
         }
      }

      return nodes;
   }

   /**
    * {@inheritDoc}
    */
   public int getLastOrderNumber(final NodeData parent) throws RepositoryException
   {
      if (txStarted())
      {
         int txLastOrderNumber = -1;
         for (ItemState change : transactionLog.getAllStates())
         {
            if (change.isNode() && change.isPersisted() && change.isAdded()
               && change.getData().getParentIdentifier().equals(parent.getIdentifier()))
            {
               int orderNumber = ((NodeData)change.getData()).getOrderNumber();
               if (orderNumber > txLastOrderNumber)
               {
                  txLastOrderNumber = orderNumber;
               }
            }
         }

         int lastOrderNumber = storageDataManager.getLastOrderNumber(parent);

         return Math.max(lastOrderNumber, txLastOrderNumber);
      }
      else
      {
         return storageDataManager.getLastOrderNumber(parent);
      }
   }

   /**
    * {@inheritDoc}
    */
   public int getChildNodesCount(final NodeData parent) throws RepositoryException
   {
      if (txStarted())
      {
         int txChildsCount = 0;
         for (ItemState change : transactionLog.getAllStates())
         {
            if (change.isNode() && change.isPersisted()
               && change.getData().getParentIdentifier().equals(parent.getIdentifier()))
            {
               if (change.isDeleted())
               {
                  txChildsCount--;
               }
               else if (change.isAdded())
               {
                  txChildsCount++;
               }
            }
         }

         final int childsCount = storageDataManager.getChildNodesCount(parent) + txChildsCount;
         if (childsCount < 0)
         {
            throw new InvalidItemStateException("Node's child nodes were changed in another Transaction "
               + parent.getQPath().getAsString());
         }

         return childsCount;
      }
      else
      {
         return storageDataManager.getChildNodesCount(parent);
      }
   }

   /**
    * {@inheritDoc}
    */
   public List<PropertyData> getChildPropertiesData(NodeData parent) throws RepositoryException
   {
      List<PropertyData> props = storageDataManager.getChildPropertiesData(parent);

      if (txStarted())
      {
         // merge data
         List<ItemState> txChanges = transactionLog.getChildrenChanges(parent.getIdentifier(), false);
         if (txChanges.size() > 0)
         {
            List<PropertyData> res = new ArrayList<PropertyData>(props);
            for (ItemState state : txChanges)
            {
               res.remove(state.getData());
               if (!state.isDeleted())
               {
                  res.add((PropertyData)state.getData());
               }
            }

            return Collections.unmodifiableList(res);
         }
      }

      return props;
   }

   /**
    * {@inheritDoc}
    */
   public List<PropertyData> getChildPropertiesData(NodeData parent, List<QPathEntryFilter> itemDataFilters)
      throws RepositoryException
   {
      List<PropertyData> props = storageDataManager.getChildPropertiesData(parent, itemDataFilters);

      if (txStarted())
      {
         // merge data
         List<ItemState> txChanges = transactionLog.getChildrenChanges(parent.getIdentifier(), false);
         if (txChanges.size() > 0)
         {
            List<PropertyData> res = new ArrayList<PropertyData>(props);
            for (ItemState state : txChanges)
            {
               res.remove(state.getData());
               if (!state.isDeleted())
               {
                  res.add((PropertyData)state.getData());
               }
            }

            return Collections.unmodifiableList(res);
         }
      }

      return props;
   }

   /**
    * {@inheritDoc}
    */
   public List<PropertyData> listChildPropertiesData(NodeData parent) throws RepositoryException
   {
      List<PropertyData> props = storageDataManager.listChildPropertiesData(parent);

      if (txStarted())
      {
         // merge data
         List<ItemState> txChanges = transactionLog.getChildrenChanges(parent.getIdentifier(), false);
         if (txChanges.size() > 0)
         {
            List<PropertyData> res = new ArrayList<PropertyData>(props);
            for (ItemState state : txChanges)
            {
               res.remove(state.getData());
               if (!state.isDeleted())
               {
                  res.add((PropertyData)state.getData());
               }
            }

            return Collections.unmodifiableList(res);
         }
      }

      return props;
   }

   /**
    * {@inheritDoc}
    */
   public ItemData getItemData(NodeData parentData, QPathEntry name, ItemType itemType) throws RepositoryException
   {
      return getItemData(parentData, name, itemType, true);
   }

   /**
    * {@inheritDoc}
    */
   public ItemData getItemData(NodeData parentData, QPathEntry name, ItemType itemType, boolean createNullItemData)
      throws RepositoryException
   {
      if (txStarted())
      {
         ItemState state = transactionLog.getItemState(parentData, name, itemType);
         if (state != null)
         {
            return state.isDeleted() ? null : state.getData();
         }
      }
      return storageDataManager.getItemData(parentData, name, itemType, createNullItemData);
   }

   /**
    * {@inheritDoc}
    */
   public boolean hasItemData(NodeData parentData, QPathEntry name, ItemType itemType) throws RepositoryException
   {
      if (txStarted())
      {
         ItemState state = transactionLog.getItemState(parentData, name, itemType);
         if (state != null)
         {
            return state.isDeleted() ? false : true;
         }
      }
      return storageDataManager.hasItemData(parentData, name, itemType);
   }

   /**
    * Return item data by identifier in this transient storage then in storage container.
    *
    * @param identifier
    * @param checkChangesLogOnly
    * @return existed item data or null if not found
    * @throws RepositoryException
    * @see org.exoplatform.services.jcr.dataflow.ItemDataConsumer#getItemData(java.lang.String)
    */
   public ItemData getItemData(String identifier, boolean checkChangesLogOnly) throws RepositoryException
   {
      if (txStarted())
      {
         ItemState state = transactionLog.getItemState(identifier);
         if (state != null)
         {
            return state.isDeleted() ? null : state.getData();
         }
      }

      return (checkChangesLogOnly) ? null: storageDataManager.getItemData(identifier);

   }

   /**
    * {@inheritDoc}
    */
   public ItemData getItemData(String identifier) throws RepositoryException
   {
      return getItemData(identifier,false);
   }

   /**
    * {@inheritDoc}
    */
   public List<PropertyData> getReferencesData(String identifier, boolean skipVersionStorage)
      throws RepositoryException
   {
      return storageDataManager.getReferencesData(identifier, skipVersionStorage);
   }

   // --------------- --------

   /**
    * Initializes the tx changes log
    */
   void start()
   {
      if (LOG.isDebugEnabled())
      {
         LOG.debug("tx start() " + this + " txStarted(): " + txStarted());
      }

      if (!txStarted())
      {
         transactionLog = new TransactionChangesLog();
      }
   }

   /**
    * Re move a given changes log
    */
   void removeLog(PlainChangesLog log)
   {
      if (LOG.isDebugEnabled())
      {
         LOG.debug("tx removeLog() " + this + (transactionLog != null ? "\n" + transactionLog.dump() : "[NULL]"));
      }

      if (txStarted())
      {
         transactionLog.removeLog(log);
         if (transactionLog.getSize() == 0)
         {
            // Clear tx changes log if there is no log left
            transactionLog = null;
         }
      }
   }

   /**
    * Updates the manager with new changes. If transaction is started it will fill manager's changes
    * log, else just move changes to workspace storage manager. It saves the changes AS IS - i.e. id
    * DOES NOT care about cloning of this objects etc. Here PlainChangesLog expected.
    * 
    * @param changes
    * @throws RepositoryException
    */
   public void save(ItemStateChangesLog changes) throws RepositoryException
   {

      PlainChangesLog statesLog = (PlainChangesLog)changes;

      if (LOG.isDebugEnabled())
      {
         LOG.debug("save() " + this + " txStarted: " + txStarted() + "\n====== Changes ======\n"
            + (statesLog != null ? "\n" + statesLog.dump() : "[NULL]") + "=====================");
      }

      if (session.canEnrollChangeToGlobalTx(statesLog))
      {
         // Save within a global tx
         transactionLog.addLog(statesLog);
      }
      else
      {
         // Regular save
         storageDataManager.save(new TransactionChangesLog(statesLog));
      }

   }

   public boolean txStarted()
   {
      return transactionLog != null;
   }

   public boolean txHasPendingChages()
   {
      return txStarted() && transactionLog.getSize() > 0;
   }

   public SharedDataManager getStorageDataManager()
   {
      return storageDataManager;
   }

}
