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

import org.exoplatform.services.jcr.access.AccessControlEntry;
import org.exoplatform.services.jcr.access.AccessControlList;
import org.exoplatform.services.jcr.core.nodetype.NodeDefinitionData;
import org.exoplatform.services.jcr.core.nodetype.NodeTypeDataManager;
import org.exoplatform.services.jcr.core.nodetype.PropertyDefinitionData;
import org.exoplatform.services.jcr.dataflow.ItemState;
import org.exoplatform.services.jcr.dataflow.PlainChangesLog;
import org.exoplatform.services.jcr.datamodel.InternalQName;
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.QPath;
import org.exoplatform.services.jcr.datamodel.QPathEntry;
import org.exoplatform.services.jcr.datamodel.ValueData;
import org.exoplatform.services.jcr.impl.Constants;
import org.exoplatform.services.jcr.impl.core.SessionDataManager;
import org.exoplatform.services.jcr.impl.core.value.BaseValue;
import org.exoplatform.services.jcr.impl.dataflow.AbstractItemDataCopyVisitor;
import org.exoplatform.services.jcr.impl.dataflow.TransientNodeData;
import org.exoplatform.services.jcr.impl.dataflow.TransientPropertyData;
import org.exoplatform.services.jcr.impl.dataflow.TransientValueData;
import org.exoplatform.services.jcr.util.IdGenerator;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.ValueFactory;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.version.OnParentVersionAction;
import javax.jcr.version.VersionException;

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

public class FrozenNodeInitializer extends AbstractItemDataCopyVisitor
{

   private static Log log = ExoLogger.getLogger("exo.jcr.component.core.FrozenNodeInitializer");

   private final Stack<NodeData> contextNodes;

   private final NodeTypeDataManager ntManager;

   private final PlainChangesLog changesLog;

   private final SessionDataManager dataManager;

   private final ValueFactory valueFactory;

   public FrozenNodeInitializer(NodeData frozen, SessionDataManager dataManager, NodeTypeDataManager ntManager,
      PlainChangesLog changesLog, ValueFactory valueFactory) throws RepositoryException
   {
      super(dataManager);
      this.dataManager = dataManager;
      this.ntManager = ntManager;
      this.changesLog = changesLog;
      this.valueFactory = valueFactory;
      this.contextNodes = new Stack<NodeData>();
      this.contextNodes.push(frozen);
   }

   @Override
   protected void entering(PropertyData property, int level) throws RepositoryException
   {

      if (log.isDebugEnabled())
      {
         log.debug("Entering property " + property.getQPath().getAsString());
      }

      if (currentNode() == null)
      {
         // skip if no parent - parent is COMPUTE, INITIALIZE
         return;
      }

      PropertyData frozenProperty = null;
      InternalQName qname = property.getQPath().getName();

      List<ValueData> values = copyValues(property);

      boolean mv = property.isMultiValued();

      if (qname.equals(Constants.JCR_PRIMARYTYPE) && level == 1)
      {
         frozenProperty =
            TransientPropertyData.createPropertyData(currentNode(), Constants.JCR_FROZENPRIMARYTYPE, PropertyType.NAME,
               mv, values);
      }
      else if (qname.equals(Constants.JCR_UUID) && level == 1)
      {
         frozenProperty =
            TransientPropertyData.createPropertyData(currentNode(), Constants.JCR_FROZENUUID, PropertyType.STRING, mv,
               values);
      }
      else if (qname.equals(Constants.JCR_MIXINTYPES) && level == 1)
      {
         frozenProperty =
            TransientPropertyData.createPropertyData(currentNode(), Constants.JCR_FROZENMIXINTYPES, PropertyType.NAME,
               mv, values);
      }
      else
      {
         NodeData parent = (NodeData)dataManager.getItemData(property.getParentIdentifier());

         PropertyDefinitionData pdef =
            ntManager.getPropertyDefinitions(qname, parent.getPrimaryTypeName(), parent.getMixinTypeNames())
               .getAnyDefinition();

         int action = pdef.getOnParentVersion();

         if (action == OnParentVersionAction.IGNORE)
         {
            return;
         }
         else if (action == OnParentVersionAction.ABORT)
         {
            throw new VersionException("Property is aborted " + property.getQPath().getAsString());
         }
         else if (action == OnParentVersionAction.COPY || action == OnParentVersionAction.VERSION
            || action == OnParentVersionAction.COMPUTE)
         {
            frozenProperty =
               TransientPropertyData.createPropertyData(currentNode(), qname, property.getType(), mv, values);
         }
         else if (action == OnParentVersionAction.INITIALIZE)
         {
            // 8.2.11.3 INITIALIZE
            // On checkin of N, a new P will be created and placed in version
            // storage as a child of VN. The new P will be initialized just as it
            // would
            // be if created normally in a workspace
            if (pdef.isAutoCreated())
            {
               if (pdef.getDefaultValues() != null && pdef.getDefaultValues().length > 0)
               {
                  // to use default values
                  values.clear();
                  for (String defValue : pdef.getDefaultValues())
                  {
                     ValueData defData;
                     if (PropertyType.UNDEFINED == pdef.getRequiredType())
                     {
                        defData = ((BaseValue)valueFactory.createValue(defValue)).getInternalData();
                     }
                     else
                     {
                        defData =
                           ((BaseValue)valueFactory.createValue(defValue, pdef.getRequiredType())).getInternalData();
                     }
                     // TransientValueData defData = ((BaseValue)
                     // defValue).getInternalData();
                     // values.add(defData.createTransientCopy());
                     values.add(defData);
                  }
               }
               else if (ntManager.isNodeType(Constants.NT_HIERARCHYNODE, parent.getPrimaryTypeName(), parent
                  .getMixinTypeNames())
                  && qname.equals(Constants.JCR_CREATED))
               {
                  // custom logic for nt:hierarchyNode jcr:created
                  values.clear();
                  values.add(new TransientValueData(dataManager.getTransactManager().getStorageDataManager()
                     .getCurrentTime()));
               }
            } // else... just as it would be if created normally in a workspace
            // (sure with value data)
            frozenProperty =
               TransientPropertyData.createPropertyData(currentNode(), qname, property.getType(), mv, values);
         }
         else
            throw new RepositoryException("Unknown OnParentVersion value " + action);
      }

      changesLog.add(ItemState.createAddedState(frozenProperty));
   }

   @Override
   protected void entering(NodeData node, int level) throws RepositoryException
   {
      // this node is not taken in account
      if (level == 0)
      {
         if (log.isDebugEnabled())
            log.debug("Entering node " + node.getQPath().getAsString() + ", level=0");
         return;
      }

      // ignored parent
      if (currentNode() == null)
      {
         contextNodes.push(null);
         if (log.isDebugEnabled())
            log.debug("Entering node " + node.getQPath().getAsString() + ", HAS NULL PARENT");
         return;
      }

      InternalQName qname = node.getQPath().getName();

      NodeData parent = (NodeData)dataManager.getItemData(node.getParentIdentifier());
      NodeDefinitionData ndef =
         ntManager.getChildNodeDefinition(qname, node.getPrimaryTypeName(), parent.getPrimaryTypeName(),
            parent.getMixinTypeNames());
      if (ndef == null)
      {
         throw new ConstraintViolationException("Definition not found for " + qname.getAsString());
      }
      int action = ndef.getOnParentVersion();

      if (log.isDebugEnabled())
         log.debug("Entering node " + node.getQPath().getAsString() + ", "
            + OnParentVersionAction.nameFromValue(action));

      NodeData frozenNode = null;
      if (action == OnParentVersionAction.IGNORE)
      {
         contextNodes.push(null);
      }
      else if (action == OnParentVersionAction.ABORT)
      {
         throw new VersionException("Node is aborted " + node.getQPath().getAsString());
      }
      else if (action == OnParentVersionAction.COPY)
      {
         AccessControlList acl = currentNode().getACL();

         boolean isPrivilegeable =
            ntManager.isNodeType(Constants.EXO_PRIVILEGEABLE, node.getPrimaryTypeName(), node.getMixinTypeNames());

         boolean isOwneable =
            ntManager.isNodeType(Constants.EXO_OWNEABLE, node.getPrimaryTypeName(), node.getMixinTypeNames());

         if (isPrivilegeable || isOwneable)
         {
            List<AccessControlEntry> permissionEntries = new ArrayList<AccessControlEntry>();
            permissionEntries.addAll((isPrivilegeable ? node.getACL() : currentNode().getACL()).getPermissionEntries());

            String owner = isOwneable ? node.getACL().getOwner() : currentNode().getACL().getOwner();

            acl = new AccessControlList(owner, permissionEntries);
         }

         QPath frozenPath = QPath.makeChildPath(currentNode().getQPath(), qname, node.getQPath().getIndex());
         frozenNode =
            new TransientNodeData(frozenPath, IdGenerator.generate(), node.getPersistedVersion(),
               node.getPrimaryTypeName(), node.getMixinTypeNames(), node.getOrderNumber(), currentNode()
                  .getIdentifier(), acl);

         contextNodes.push(frozenNode);
         changesLog.add(ItemState.createAddedState(frozenNode));
      }
      else if (action == OnParentVersionAction.VERSION)
      {
         if (ntManager.isNodeType(Constants.MIX_VERSIONABLE, node.getPrimaryTypeName(), node.getMixinTypeNames()))
         {
            frozenNode =
               TransientNodeData.createNodeData(currentNode(), qname, Constants.NT_VERSIONEDCHILD, node.getQPath()
                  .getIndex());

            PropertyData pt =
               TransientPropertyData.createPropertyData(frozenNode, Constants.JCR_PRIMARYTYPE, PropertyType.NAME,
                  false, new TransientValueData(Constants.NT_VERSIONEDCHILD));

            ValueData vh =
               ((PropertyData)dataManager.getItemData(node, new QPathEntry(Constants.JCR_VERSIONHISTORY, 0),
                  ItemType.PROPERTY)).getValues().get(0);

            PropertyData pd =
               TransientPropertyData.createPropertyData(frozenNode, Constants.JCR_CHILDVERSIONHISTORY,
                  PropertyType.REFERENCE, false, vh);

            contextNodes.push(null);
            changesLog.add(ItemState.createAddedState(frozenNode));
            changesLog.add(ItemState.createAddedState(pt));
            changesLog.add(ItemState.createAddedState(pd));
         }
         else
         { // behaviour of COPY
            AccessControlList acl = currentNode().getACL();

            boolean isPrivilegeable =
               ntManager.isNodeType(Constants.EXO_PRIVILEGEABLE, node.getPrimaryTypeName(), node.getMixinTypeNames());

            boolean isOwneable =
               ntManager.isNodeType(Constants.EXO_OWNEABLE, node.getPrimaryTypeName(), node.getMixinTypeNames());

            if (isPrivilegeable || isOwneable)
            {
               List<AccessControlEntry> accessList = new ArrayList<AccessControlEntry>();
               accessList.addAll((isPrivilegeable ? node.getACL() : currentNode().getACL())
                  .getPermissionEntries());

               String owner = isOwneable ? node.getACL().getOwner() : currentNode().getACL().getOwner();

               acl = new AccessControlList(owner, accessList);
            }

            QPath frozenPath = QPath.makeChildPath(currentNode().getQPath(), qname, node.getQPath().getIndex());
            frozenNode =
               new TransientNodeData(frozenPath, IdGenerator.generate(), node.getPersistedVersion(),
                  node.getPrimaryTypeName(), node.getMixinTypeNames(), node.getOrderNumber(), currentNode()
                     .getIdentifier(), acl);

            contextNodes.push(frozenNode);
            changesLog.add(ItemState.createAddedState(frozenNode));
         }

      }
      else if (action == OnParentVersionAction.INITIALIZE)
      {
         // 8.2.11.3 INITIALIZE
         // On checkin of N, a new node C will be created and placed in version
         // storage as a child of VN. This new C will be initialized just as it
         // would be if created normally in a workspace. No state information
         // of the current C in the workspace is preserved.
         frozenNode =
            TransientNodeData.createNodeData(currentNode(), qname, node.getPrimaryTypeName(), node.getQPath()
               .getIndex());
         contextNodes.push(null);
         changesLog.add(ItemState.createAddedState(frozenNode));
      }
      else if (action == OnParentVersionAction.COMPUTE)
      {
         // 8.2.11.4 COMPUTE
         // On checkin of N, a new node C will be created and placed in version
         // storage as a child of VN. This new C will be initialized by some
         // procedure defined for that type of child node.
         // [PN] 10.04.06 Creating simply as new node with same name and same node
         // type
         frozenNode =
            TransientNodeData.createNodeData(currentNode(), qname, node.getPrimaryTypeName(), node.getQPath()
               .getIndex());
         contextNodes.push(null);
         changesLog.add(ItemState.createAddedState(frozenNode));
      }
      else
      {
         throw new RepositoryException("Unknown onParentVersion type " + action);
      }
   }

   @Override
   protected void leaving(PropertyData property, int level) throws RepositoryException
   {
   }

   @Override
   protected void leaving(NodeData node, int level) throws RepositoryException
   {
      contextNodes.pop();
   }

   private NodeData currentNode()
   {
      return contextNodes.peek();
   }

   @Override
   public SessionDataManager getDataManager()
   {
      return dataManager;
   }

}