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

import org.exoplatform.services.jcr.core.nodetype.NodeDefinitionData;
import org.exoplatform.services.jcr.core.nodetype.NodeTypeData;
import org.exoplatform.services.jcr.core.nodetype.NodeTypeDataManager;
import org.exoplatform.services.jcr.dataflow.ItemDataConsumer;
import org.exoplatform.services.jcr.dataflow.PlainChangesLog;
import org.exoplatform.services.jcr.dataflow.PlainChangesLogImpl;
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.QPathEntry;
import org.exoplatform.services.jcr.impl.Constants;
import org.exoplatform.services.jcr.impl.core.nodetype.ItemAutocreator;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.jcr.RepositoryException;
import javax.jcr.nodetype.ConstraintViolationException;

/**
 * Created by The eXo Platform SAS.
 * 
 * @author <a href="mailto:Sergey.Kabashnyuk@gmail.com">Sergey Kabashnyuk</a>
 * @version $Id: $
 */
public class NodeDefinitionComparator extends AbstractDefinitionComparator<NodeDefinitionData>
{

   /**
    * Class logger.
    */
   private static final Log LOG = ExoLogger.getLogger("exo.jcr.component.core.NodeDefinitionComparator");

   private final List<NodeData> affectedNodes;

   private final NodeTypeDataManager nodeTypeDataManager;

   private final ItemDataConsumer dataConsumer;

   private final ItemAutocreator itemAutocreator;

   /**
    * @param nodeTypeDataManager
    * @param dataConsumer
    * @param itemAutocreator
    * @param affectedNodes
    */
   public NodeDefinitionComparator(NodeTypeDataManager nodeTypeDataManager, ItemDataConsumer dataConsumer,
      ItemAutocreator itemAutocreator, List<NodeData> affectedNodes)
   {
      this.nodeTypeDataManager = nodeTypeDataManager;
      this.dataConsumer = dataConsumer;
      this.itemAutocreator = itemAutocreator;
      this.affectedNodes = affectedNodes;
   }

   @Override
   public PlainChangesLog compare(NodeTypeData registeredNodeType, NodeDefinitionData[] ancestorDefinition,
      NodeDefinitionData[] recipientDefinition) throws ConstraintViolationException, RepositoryException
   {

      List<NodeDefinitionData> sameDefinitionData = new ArrayList<NodeDefinitionData>();
      List<RelatedDefinition<NodeDefinitionData>> changedDefinitionData =
         new ArrayList<RelatedDefinition<NodeDefinitionData>>();
      List<NodeDefinitionData> newDefinitionData = new ArrayList<NodeDefinitionData>();
      List<NodeDefinitionData> removedDefinitionData = new ArrayList<NodeDefinitionData>();
      init(ancestorDefinition, recipientDefinition, sameDefinitionData, changedDefinitionData, newDefinitionData,
         removedDefinitionData);
      // create changes log
      PlainChangesLog changesLog = new PlainChangesLogImpl();

      // check removed
      validateRemoved(registeredNodeType, removedDefinitionData, recipientDefinition, affectedNodes);

      validateAdded(registeredNodeType.getName(), newDefinitionData, affectedNodes, recipientDefinition);
      // changed
      validateChanged(registeredNodeType.getName(), changedDefinitionData, affectedNodes, recipientDefinition);

      //
      doAdd(newDefinitionData, changesLog, affectedNodes, registeredNodeType);

      return changesLog;

   }

   /**
    * @param nodesData
    * @param nodeDefinitionData
    * @throws RepositoryException
    * @throws ConstraintViolationException
    */
   private void checkMandatoryItems(List<NodeData> nodesData, NodeDefinitionData nodeDefinitionData)
      throws RepositoryException, ConstraintViolationException
   {
      for (NodeData nodeData : nodesData)
      {
         ItemData child =
            dataConsumer.getItemData(nodeData, new QPathEntry(nodeDefinitionData.getName(), 0), ItemType.NODE);
         if (child == null || !child.isNode())
         {
            throw new ConstraintViolationException("Fail to  add mandatory and not auto-created "
               + "child node definition " + nodeDefinitionData.getName().getAsString() + " for "
               + nodeDefinitionData.getDeclaringNodeType().getAsString() + " because node "
               + nodeData.getQPath().getAsString() + " doesn't contains child node with name "
               + nodeDefinitionData.getName().getAsString());

         }
      }
   }

   /**
    * @param registeredNodeType
    * @param nodesData
    * @param ancestorRequiredPrimaryTypes
    * @param recipientDefinitionData
    * @throws RepositoryException
    */
   private void checkRequiredPrimaryType(InternalQName registeredNodeType, List<NodeData> nodesData,
      InternalQName[] ancestorRequiredPrimaryTypes, NodeDefinitionData recipientDefinitionData,
      NodeDefinitionData[] allRecipientDefinition) throws RepositoryException
   {
      // Required type change
      InternalQName[] requiredPrimaryTypes = recipientDefinitionData.getRequiredPrimaryTypes();

      for (NodeData nodeData : nodesData)
      {

         if (recipientDefinitionData.getName().equals(Constants.JCR_ANY_NAME))
         {
            List<NodeData> childs = dataConsumer.getChildNodesData(nodeData);
            for (NodeData child : childs)
            {
               if (isResidualMatch(child.getQPath().getName(), allRecipientDefinition))
               {
                  for (int i = 0; i < requiredPrimaryTypes.length; i++)
                  {
                     if (!nodeTypeDataManager.isNodeType(requiredPrimaryTypes[i], child.getPrimaryTypeName(),
                        child.getMixinTypeNames()))
                     {
                        StringBuilder buffer = new StringBuilder();
                        buffer.append("Fail to change ");
                        buffer.append(recipientDefinitionData.getName().getAsString());
                        buffer.append(" node definition for ");
                        buffer.append(registeredNodeType.getAsString());
                        buffer.append("node type from ");
                        if (ancestorRequiredPrimaryTypes != null)
                           buffer.append(Arrays.toString(ancestorRequiredPrimaryTypes));
                        else
                           buffer.append(" '' ");
                        buffer.append(" to ");
                        buffer.append(Arrays.toString(recipientDefinitionData.getRequiredPrimaryTypes()));
                        buffer.append(" because ");
                        buffer.append(child.getQPath().getAsString());
                        buffer.append(" doesn't much ");
                        buffer.append(requiredPrimaryTypes[i].getAsString());
                        buffer.append(" as required primary type");
                        throw new ConstraintViolationException(buffer.toString());

                     }
                  }
               }
            }
         }
         else
         {
            List<NodeData> childs = dataConsumer.getChildNodesData(nodeData);
            for (NodeData child : childs)
            {
               if (child.getQPath().getName().equals(recipientDefinitionData.getName()))
               {
                  for (int i = 0; i < requiredPrimaryTypes.length; i++)
                  {
                     if (!nodeTypeDataManager.isNodeType(requiredPrimaryTypes[i], child.getPrimaryTypeName(),
                        child.getMixinTypeNames()))
                     {
                        StringBuilder buffer = new StringBuilder();
                        buffer.append("Fail to change ");
                        buffer.append(recipientDefinitionData.getName().getAsString());
                        buffer.append(" node definition for ");
                        buffer.append(registeredNodeType.getAsString());
                        buffer.append("node type from ");
                        if (ancestorRequiredPrimaryTypes != null)
                           buffer.append(Arrays.toString(ancestorRequiredPrimaryTypes));
                        else
                           buffer.append(" '' ");
                        buffer.append(" to ");
                        buffer.append(Arrays.toString(recipientDefinitionData.getRequiredPrimaryTypes()));
                        buffer.append(" because ");
                        buffer.append(child.getQPath().getAsString());
                        buffer.append("doesn't much ");
                        buffer.append(requiredPrimaryTypes[i].getAsString());
                        buffer.append(" as required primary type");
                        throw new ConstraintViolationException(buffer.toString());
                     }
                  }
               }
            }
         }
      }
   }

   /**
    * @param registeredNodeType
    * @param nodesData
    * @param recipientName
    * @param allRecipientDefinition
    * @throws RepositoryException
    */
   private void checkSameNameSibling(InternalQName registeredNodeType, List<NodeData> nodesData,
      InternalQName recipientName, NodeDefinitionData[] allRecipientDefinition) throws RepositoryException
   {

      for (NodeData nodeData : nodesData)
      {

         if (recipientName.equals(Constants.JCR_ANY_NAME))
         {
            // child of node type
            List<NodeData> childs = dataConsumer.getChildNodesData(nodeData);
            for (NodeData child : childs)
            {
               if (isResidualMatch(child.getQPath().getName(), allRecipientDefinition))
               {
                  List<NodeData> childs2 = dataConsumer.getChildNodesData(child);

                  for (NodeData child2 : childs2)
                  {
                     if (child2.getQPath().getIndex() > 1)
                     {
                        StringBuilder buffer = new StringBuilder();
                        buffer.append("Fail to change ");
                        buffer.append(recipientName.getAsString());
                        buffer.append(" node definition for ");
                        buffer.append(registeredNodeType.getAsString());
                        buffer.append("node type from AllowsSameNameSiblings = true to AllowsSameNameSiblings = false");
                        buffer.append(" because ");
                        buffer.append(child.getQPath().getAsString());
                        buffer.append(" contains more then one child with name");
                        buffer.append(child2.getQPath().getName().getAsString());
                        throw new ConstraintViolationException(buffer.toString());
                     }
                  }
               }
            }
         }
         else
         {

            // child of node type
            List<NodeData> childs = dataConsumer.getChildNodesData(nodeData);
            for (NodeData child : childs)
            {
               if (child.getQPath().getName().equals(recipientName))
               {
                  List<NodeData> childs2 = dataConsumer.getChildNodesData(child);

                  for (NodeData child2 : childs2)
                  {
                     if (child2.getQPath().getIndex() > 1)
                     {
                        StringBuilder buffer = new StringBuilder();
                        buffer.append("Fail to change ");
                        buffer.append(recipientName.getAsString());
                        buffer.append(" node definition for ");
                        buffer.append(registeredNodeType.getAsString());
                        buffer.append("node type from AllowsSameNameSiblings = true to AllowsSameNameSiblings = false");
                        buffer.append(" because ");
                        buffer.append(child.getQPath().getAsString());
                        buffer.append(" contains more then one child with name");
                        buffer.append(child2.getQPath().getName().getAsString());
                        throw new ConstraintViolationException(buffer.toString());
                     }
                  }
               }

            }
         }

      }

   }

   /**
    * @param toAddList
    * @param changesLog
    * @param nodesData
    * @param registeredNodeType
    * @throws RepositoryException
    */
   private void doAdd(List<NodeDefinitionData> toAddList, PlainChangesLog changesLog, List<NodeData> nodesData,
      NodeTypeData registeredNodeType) throws RepositoryException
   {

      for (NodeData nodeData : nodesData)
      {

         // added properties
         for (NodeDefinitionData newNodeDefinitionData : toAddList)
         {
            if (!newNodeDefinitionData.getName().equals(Constants.JCR_ANY_NAME)
               && newNodeDefinitionData.isAutoCreated())
               changesLog.addAll(itemAutocreator.makeAutoCreatedNodes(nodeData, registeredNodeType.getName(),
                  new NodeDefinitionData[]{newNodeDefinitionData}, dataConsumer, nodeData.getACL().getOwner())
                  .getAllStates());
         }
      }
   }

   /**
    * @param nodeTypeName
    * @param newDefinitionData
    * @param nodesData
    * @param recipientDefinition
    * @throws RepositoryException
    */
   private void validateAdded(InternalQName nodeTypeName, List<NodeDefinitionData> newDefinitionData,
      List<NodeData> nodesData, NodeDefinitionData[] recipientDefinition) throws RepositoryException
   {

      for (NodeDefinitionData nodeDefinitionData : newDefinitionData)
      {

         if (nodeDefinitionData.getName().equals(Constants.JCR_ANY_NAME))
         {

            checkRequiredPrimaryType(nodeTypeName, nodesData, null, nodeDefinitionData, recipientDefinition);

            checkSameNameSibling(nodeTypeName, nodesData, nodeDefinitionData.getName(), recipientDefinition);

         }
         else
         {
            // check existed nodes for new constraint
            checkRequiredPrimaryType(nodeTypeName, nodesData, null, nodeDefinitionData, recipientDefinition);
            checkSameNameSibling(nodeTypeName, nodesData, nodeDefinitionData.getName(), recipientDefinition);

            // try to add mandatory or auto-created properties for
            // for already addded nodes.
            if (nodeDefinitionData.isMandatory() && !nodeDefinitionData.isAutoCreated())
            {
               checkMandatoryItems(nodesData, nodeDefinitionData);
            }
         }
      }

   }

   /**
    * @param registeredNodeType
    * @param changedDefinitionData
    * @param nodesData
    * @param allRecipientDefinition
    * @throws RepositoryException
    */
   private void validateChanged(InternalQName registeredNodeType,
      List<RelatedDefinition<NodeDefinitionData>> changedDefinitionData, List<NodeData> nodesData,
      NodeDefinitionData[] allRecipientDefinition) throws RepositoryException
   {
      for (RelatedDefinition<NodeDefinitionData> changedDefinitions : changedDefinitionData)
      {
         NodeDefinitionData ancestorDefinitionData = changedDefinitions.getAncestorDefinition();
         NodeDefinitionData recipientDefinitionData = changedDefinitions.getRecepientDefinition();
         // change from mandatory=false to mandatory = true
         if (!ancestorDefinitionData.isMandatory() && recipientDefinitionData.isMandatory())
         {
            for (NodeData nodeData : nodesData)
            {

               ItemData child =
                  dataConsumer.getItemData(nodeData, new QPathEntry(recipientDefinitionData.getName(), 0),
                     ItemType.NODE);
               if (child == null || !child.isNode())
               {
                  String message =
                     "Can not change " + recipientDefinitionData.getName().getAsString() + " node definition for "
                        + registeredNodeType.getAsString() + " node type "
                        + " from mandatory=false to mandatory = true , because " + " node "
                        + nodeData.getQPath().getAsString() + " doesn't have child node with name "
                        + recipientDefinitionData.getName().getAsString();
                  throw new ConstraintViolationException(message);
               }
            }
         }

         // change from Protected=false to Protected = true
         if (!ancestorDefinitionData.isProtected() && recipientDefinitionData.isProtected())
         {
            for (NodeData nodeData : nodesData)
            {

               ItemData child =
                  dataConsumer.getItemData(nodeData, new QPathEntry(recipientDefinitionData.getName(), 0),
                     ItemType.NODE);
               if (child == null || !child.isNode())
               {
                  String message =
                     "Fail to  change " + recipientDefinitionData.getName().getAsString() + " node definition for "
                        + registeredNodeType.getAsString()
                        + " node type  from protected=false to Protected = true , because " + " node "
                        + nodeData.getQPath().getAsString() + " doesn't have child node with name "
                        + recipientDefinitionData.getName().getAsString();
                  throw new ConstraintViolationException(message);
               }
            }
         }
         if (!Arrays.deepEquals(ancestorDefinitionData.getRequiredPrimaryTypes(),
            recipientDefinitionData.getRequiredPrimaryTypes()))
         {
            checkRequiredPrimaryType(registeredNodeType, nodesData, ancestorDefinitionData.getRequiredPrimaryTypes(),
               recipientDefinitionData, allRecipientDefinition);
         }
         // check sibling
         if (ancestorDefinitionData.isAllowsSameNameSiblings() && !recipientDefinitionData.isAllowsSameNameSiblings())
         {
            checkSameNameSibling(registeredNodeType, nodesData, recipientDefinitionData.getName(),
               allRecipientDefinition);
         }
      }
   }

   /**
    * @param registeredNodeType
    * @param removedDefinitionData
    * @param recipientDefinition
    * @throws ConstraintViolationException
    * @throws RepositoryException
    */
   private void validateRemoved(NodeTypeData registeredNodeType, List<NodeDefinitionData> removedDefinitionData,
      NodeDefinitionData[] recipientDefinition, List<NodeData> nodesData) throws ConstraintViolationException,
      RepositoryException
   {

      for (NodeDefinitionData removeNodeDefinitionData : removedDefinitionData)
      {
         if (removeNodeDefinitionData.getName().equals(Constants.JCR_ANY_NAME))
         {
            for (NodeData nodeData : nodesData)
            {
               List<NodeData> childs = dataConsumer.getChildNodesData(nodeData);
               // more then mixin and primary type
               if (childs.size() > 0)
               {
                  for (NodeData nodeData2 : childs)
                  {
                     if (!isNonResidualMatch(nodeData2.getQPath().getName(), recipientDefinition))
                     {
                        String msg =
                           "Can't remove node definition " + removeNodeDefinitionData.getName().getAsString()
                              + "  for " + registeredNodeType.getName().getAsString() + " node type because node "
                              + nodeData.getQPath().getAsString() + " " + " countains child nodes with name "
                              + nodeData2.getQPath().getName().getAsString();
                        throw new ConstraintViolationException(msg);
                     }
                  }
               }
            }
         }
         else
         {
            if (!isResidualMatch(removeNodeDefinitionData.getName(), recipientDefinition))
            {
               for (NodeData nodeData : nodesData)
               {
                  ItemData child =
                     dataConsumer.getItemData(nodeData, new QPathEntry(removeNodeDefinitionData.getName(), 0),
                        ItemType.NODE);
                  if (child != null && child.isNode())
                  {
                     throw new ConstraintViolationException("Can't remove node definition "
                        + removeNodeDefinitionData.getName().getAsString() + "  for "
                        + registeredNodeType.getName().getAsString() + " node type because node "
                        + nodeData.getQPath().getAsString() + " " + " countains child node with name "
                        + child.getQPath().getName().getAsString());

                  }
               }
            }
         }
      }
   }
}
