/*
 * 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.ExtendedPropertyType;
import org.exoplatform.services.jcr.core.nodetype.NodeTypeData;
import org.exoplatform.services.jcr.core.nodetype.NodeTypeDataManager;
import org.exoplatform.services.jcr.core.nodetype.PropertyDefinitionData;
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.PropertyData;
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.LocationFactory;
import org.exoplatform.services.jcr.impl.core.nodetype.ItemAutocreator;
import org.exoplatform.services.jcr.impl.core.value.ValueConstraintsMatcher;
import org.exoplatform.services.jcr.impl.dataflow.ValueDataUtil;
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.PropertyType;
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 PropertyDefinitionComparator extends AbstractDefinitionComparator<PropertyDefinitionData>
{
   /**
    * Class logger.
    */
   private static final Log LOG = ExoLogger.getLogger("exo.jcr.component.core.PropertyDefinitionComparator");

   private final List<NodeData> affectedNodes;

   private final LocationFactory locationFactory;

   private final NodeTypeDataManager nodeTypeDataManager;

   private final ItemDataConsumer dataConsumer;

   private final ItemAutocreator itemAutocreator;

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

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

      List<PropertyDefinitionData> sameDefinitionData = new ArrayList<PropertyDefinitionData>();

      List<RelatedDefinition<PropertyDefinitionData>> changedDefinitionData =
         new ArrayList<RelatedDefinition<PropertyDefinitionData>>();
      List<PropertyDefinitionData> newDefinitionData = new ArrayList<PropertyDefinitionData>();
      List<PropertyDefinitionData> removedDefinitionData = new ArrayList<PropertyDefinitionData>();
      init(ancestorDefinition, recipientDefinition, sameDefinitionData, changedDefinitionData, newDefinitionData,
         removedDefinitionData);

      // create changes log
      PlainChangesLog changesLog = new PlainChangesLogImpl();

      // removing properties
      validateRemoved(registeredNodeType, removedDefinitionData, recipientDefinition, affectedNodes);

      // new property definition
      validateAdded(registeredNodeType, newDefinitionData, recipientDefinition, affectedNodes);

      // changed
      validateChanged(registeredNodeType, changedDefinitionData, affectedNodes, recipientDefinition);

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

   }

   /**
    * @param registeredNodeType
    * @param recipientDefinitionData
    * @param allRecipientDefinition
    * @throws RepositoryException
    * @throws ConstraintViolationException
    */
   private void checkIsMultiple(NodeTypeData registeredNodeType, PropertyDefinitionData recipientDefinitionData,
      PropertyDefinitionData[] allRecipientDefinition, List<NodeData> nodesData) throws RepositoryException,
      ConstraintViolationException
   {
      List<NodeData> checkIsMultipleNodes;
      if (Constants.JCR_ANY_NAME.equals(recipientDefinitionData.getName()))
      {
         checkIsMultipleNodes = nodesData;
      }
      else
      {
         checkIsMultipleNodes =
            getNodes(nodesData, new InternalQName[]{recipientDefinitionData.getName()}, new InternalQName[]{});
      }
      for (NodeData nodeData : checkIsMultipleNodes)
      {
         if (Constants.JCR_ANY_NAME.equals(recipientDefinitionData.getName()))
         {
            List<PropertyData> propertyDatas = dataConsumer.getChildPropertiesData(nodeData);
            for (PropertyData propertyData : propertyDatas)
            {
               // skip mixin and primary type
               if (isResidualMatch(propertyData.getQPath().getName(), allRecipientDefinition))
               {
                  if (propertyData.getValues().size() > 1)
                  {
                     throw new ConstraintViolationException("Can't change property definition "
                        + recipientDefinitionData.getName().getAsString() + " to isMultiple = false because property "
                        + propertyData.getQPath().getAsString() + " contains more then one value");
                  }
               }
            }
         }
         else
         {
            PropertyData propertyData =
               (PropertyData)dataConsumer.getItemData(nodeData, new QPathEntry(recipientDefinitionData.getName(), 0),
                  ItemType.PROPERTY);
            if (propertyData.getValues().size() > 1)
            {
               throw new ConstraintViolationException("Can't change property definition "
                  + recipientDefinitionData.getName().getAsString() + " to isMultiple = false because property "
                  + propertyData.getQPath().getAsString() + " contains more then one value");
            }

         }
      }
   }

   /**
    * @param registeredNodeType
    * @param nodesData
    * @param recipientDefinitionData
    * @throws RepositoryException
    */
   private void checkMandatory(NodeTypeData registeredNodeType, List<NodeData> nodesData,
      PropertyDefinitionData recipientDefinitionData) throws RepositoryException
   {
      if (Constants.JCR_ANY_NAME.equals(recipientDefinitionData.getName()) && recipientDefinitionData.isMandatory())
         throw new ConstraintViolationException("Invalid property definition " + recipientDefinitionData.getName()
            + ". Residual definition can't be mandatory");
      List<NodeData> mandatoryNodes =
         getNodes(nodesData, new InternalQName[]{}, new InternalQName[]{recipientDefinitionData.getName()});
      if (mandatoryNodes.size() > 0)
      {
         StringBuilder message =
            new StringBuilder("Can not change ").append(recipientDefinitionData.getName().getAsString()).append(
               " property definition from mandatory=false to mandatory = true , because ").append(" the following nodes ");

         for (NodeData nodeData : mandatoryNodes)
         {
            message.append(nodeData.getQPath().getAsString()).append(" ");
         }
         message.append("  doesn't have these properties ");

         throw new ConstraintViolationException(message.toString());
      }
   }

   /**
    * @param registeredNodeType
    * @param recipientDefinitionData
    * @param allRecipientDefinition
    * @throws RepositoryException
    */
   private void checkRequiredType(NodeTypeData registeredNodeType, PropertyDefinitionData recipientDefinitionData,
      PropertyDefinitionData[] allRecipientDefinition, List<NodeData> nodesData) throws RepositoryException
   {
      List<NodeData> requiredTypeNodes;
      if (Constants.JCR_ANY_NAME.equals(recipientDefinitionData.getName()))
      {
         requiredTypeNodes = nodesData;
      }
      else
      {
         requiredTypeNodes =
            getNodes(nodesData, new InternalQName[]{recipientDefinitionData.getName()}, new InternalQName[]{});
      }
      for (NodeData nodeData : requiredTypeNodes)
      {
         if (Constants.JCR_ANY_NAME.equals(recipientDefinitionData.getName()))
         {
            List<PropertyData> propertyDatas = dataConsumer.getChildPropertiesData(nodeData);
            for (PropertyData propertyData : propertyDatas)
            {
               // skip mixin and primary type
               if (isResidualMatch(propertyData.getQPath().getName(), allRecipientDefinition))
               {
                  if (recipientDefinitionData.getRequiredType() != PropertyType.UNDEFINED
                     && propertyData.getType() != recipientDefinitionData.getRequiredType())
                  {
                     throw new ConstraintViolationException("Can not change  requiredType to "
                        + ExtendedPropertyType.nameFromValue(recipientDefinitionData.getRequiredType()) + " in "
                        + recipientDefinitionData.getName().getAsString() + "  because "
                        + propertyData.getQPath().getAsString() + " have "
                        + ExtendedPropertyType.nameFromValue(propertyData.getType()));

                  }
               }
            }

         }
         else
         {
            PropertyData propertyData =
               (PropertyData)dataConsumer.getItemData(nodeData, new QPathEntry(recipientDefinitionData.getName(), 0),
                  ItemType.PROPERTY);
            if (recipientDefinitionData.getRequiredType() != PropertyType.UNDEFINED
               && propertyData.getType() != recipientDefinitionData.getRequiredType())
            {
               throw new ConstraintViolationException("Can not change  requiredType to "
                  + ExtendedPropertyType.nameFromValue(recipientDefinitionData.getRequiredType()) + " in "
                  + recipientDefinitionData.getName().getAsString() + "  because "
                  + propertyData.getQPath().getAsString() + " have "
                  + ExtendedPropertyType.nameFromValue(propertyData.getType()));
            }
         }

      }
   }

   /**
    * @param registeredNodeType
    * @param recipientDefinitionData
    * @param allRecipientDefinition
    * @throws RepositoryException
    * @throws ConstraintViolationException
    */
   private void checkValueConstraints(NodeTypeData registeredNodeType, PropertyDefinitionData recipientDefinitionData,
      PropertyDefinitionData[] allRecipientDefinition, List<NodeData> nodesData) throws RepositoryException,
      ConstraintViolationException
   {
      List<NodeData> checkValueConstraintsNodes;
      if (Constants.JCR_ANY_NAME.equals(recipientDefinitionData.getName()))
      {
         checkValueConstraintsNodes = nodesData;
      }
      else
      {
         checkValueConstraintsNodes =
            getNodes(nodesData, new InternalQName[]{recipientDefinitionData.getName()}, new InternalQName[]{});
      }
      for (NodeData nodeData : checkValueConstraintsNodes)
      {

         if (Constants.JCR_ANY_NAME.equals(recipientDefinitionData.getName()))
         {
            List<PropertyData> propertyDatas = dataConsumer.getChildPropertiesData(nodeData);
            for (PropertyData propertyData : propertyDatas)
            {
               // skip mixin and primary type
               if (isResidualMatch(propertyData.getQPath().getName(), allRecipientDefinition))
               {
                  checkValueConstraints(recipientDefinitionData, propertyData);
               }
            }
         }
         else
         {
            PropertyData propertyData =
               (PropertyData)dataConsumer.getItemData(nodeData, new QPathEntry(recipientDefinitionData.getName(), 0),
                  ItemType.PROPERTY);
            checkValueConstraints(recipientDefinitionData, propertyData);
         }
      }
   }

   private void checkValueConstraints(PropertyDefinitionData def, PropertyData propertyData) throws RepositoryException
   {

      ValueConstraintsMatcher constraints =
         new ValueConstraintsMatcher(def.getValueConstraints(), locationFactory, dataConsumer, nodeTypeDataManager);

      for (ValueData value : propertyData.getValues())
      {
         if (!constraints.match(value, propertyData.getType()))
         {
            String strVal = null;
            try
            {
               if (propertyData.getType() != PropertyType.BINARY)
               {
                  // may have large size
                  strVal = ValueDataUtil.getString(value);
               }
               else
               {
                  strVal = "PropertyType.BINARY";
               }
            }
            catch (IllegalStateException e)
            {
               LOG.error("Error of value read: " + e.getMessage(), e);
            }
            throw new ConstraintViolationException("Value " + strVal + " for property "
               + propertyData.getQPath().getAsString() + " doesn't match new constraint ");
         }
      }
   }

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

         // added properties
         for (PropertyDefinitionData newPropertyDefinitionData : toAddList)
         {
            if (!newPropertyDefinitionData.getName().equals(Constants.JCR_ANY_NAME)
               && newPropertyDefinitionData.isAutoCreated())
            {
               ItemData pdata =
                  dataConsumer.getItemData(nodeData, new QPathEntry(newPropertyDefinitionData.getName(), 0),
                     ItemType.UNKNOWN);
               if (pdata == null || (pdata != null && pdata.isNode()))
               {
                  PlainChangesLog autoCreatedChanges =
                     itemAutocreator.makeAutoCreatedProperties(nodeData, registeredNodeType.getName(),
                        new PropertyDefinitionData[]{newPropertyDefinitionData}, dataConsumer, nodeData.getACL()
                           .getOwner());
                  if (autoCreatedChanges.getSize() == 0)
                  {
                     throw new ConstraintViolationException("Fail to add property by definition: "
                        + newPropertyDefinitionData.getName().getAsString() + " Possible no default values defined.");
                  }

                  changesLog.addAll(autoCreatedChanges.getAllStates());
               }
            }
         }
      }
   }

   /**
    * @param nodes
    * @param includeProperties
    * @param excludeProperties
    * @return All nodes from list nodes, what include properties from
    *         includeProperties, and doesn't include properties from
    *         excludeProperties.
    * @throws RepositoryException
    */
   private List<NodeData> getNodes(List<NodeData> nodes, InternalQName[] includeProperties,
      InternalQName[] excludeProperties) throws RepositoryException
   {
      List<NodeData> result = new ArrayList<NodeData>();

      for (NodeData nodeData : nodes)
      {
         // search all properties
         List<PropertyData> childProperties = dataConsumer.listChildPropertiesData(nodeData);
         boolean toAdd = includeProperties.length == 0;
         // check included
         for (int i = 0; i < includeProperties.length; i++)
         {
            for (PropertyData propertyData : childProperties)
            {
               if (propertyData.getQPath().getName().equals(includeProperties[i]))
               {
                  toAdd = true;
                  break;
               }
            }
         }
         if (toAdd)
         {
            // check excluded
            for (int i = 0; i < excludeProperties.length; i++)
            {
               for (PropertyData propertyData : childProperties)
               {
                  if (propertyData.getQPath().getName().equals(excludeProperties[i]))
                  {
                     toAdd = false;
                     break;
                  }
               }
            }
            if (toAdd)
               result.add(nodeData);
         }
      }
      return result;
   }

   /**
    * @param registeredNodeType
    * @param newDefinitionData
    * @param allRecipientDefinition
    * @param nodesData
    * @throws RepositoryException
    */
   private void validateAdded(NodeTypeData registeredNodeType, List<PropertyDefinitionData> newDefinitionData,
      PropertyDefinitionData[] allRecipientDefinition, List<NodeData> nodesData) throws RepositoryException
   {
      if (newDefinitionData.size() > 0)
      {
         for (PropertyDefinitionData propertyDefinitionData : newDefinitionData)
         {
            if (propertyDefinitionData.getName().equals(Constants.JCR_ANY_NAME))
            {
               // Required type change
               checkRequiredType(registeredNodeType, propertyDefinitionData, allRecipientDefinition, nodesData);
               // ValueConstraints
               checkValueConstraints(registeredNodeType, propertyDefinitionData, allRecipientDefinition, nodesData);
               // multiple change
               checkIsMultiple(registeredNodeType, propertyDefinitionData, allRecipientDefinition, nodesData);
            }
         }
      }
   }

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

         }
         // No need to check protected
         // change from Protected=false to Protected = true
         // if (!ancestorDefinitionData.isProtected() &&
         // recipientDefinitionData.isProtected()) {
         // checkProtected(registeredNodeType, nodesData, recipientDefinitionData);
         // }
         // Required type change
         if (ancestorDefinitionData.getRequiredType() != recipientDefinitionData.getRequiredType()
            && recipientDefinitionData.getRequiredType() != PropertyType.UNDEFINED)
         {
            checkRequiredType(registeredNodeType, recipientDefinitionData, allRecipientDefinition, nodesData);
         }
         // ValueConstraints
         if (!Arrays.deepEquals(ancestorDefinitionData.getValueConstraints(),
            recipientDefinitionData.getValueConstraints()))
         {
            checkValueConstraints(registeredNodeType, recipientDefinitionData, allRecipientDefinition, nodesData);
         }
         // multiple change
         if (ancestorDefinitionData.isMultiple() && !recipientDefinitionData.isMultiple())
         {
            checkIsMultiple(registeredNodeType, recipientDefinitionData, allRecipientDefinition, nodesData);
         }

      }
   }

   /**
    * @param registeredNodeType
    * @param recipientDefinition
    * @param recipientDefinition
    * @param nodesData
    * @throws RepositoryException
    */
   private void validateRemoved(NodeTypeData registeredNodeType, List<PropertyDefinitionData> removedDefinitionData,
      PropertyDefinitionData[] recipientDefinition, List<NodeData> nodesData) throws RepositoryException
   {
      for (PropertyDefinitionData removePropertyDefinitionData : removedDefinitionData)
      {
         if (removePropertyDefinitionData.getName().equals(Constants.JCR_ANY_NAME))
         {

            for (NodeData nodeData : nodesData)
            {
               List<PropertyData> childs = dataConsumer.getChildPropertiesData(nodeData);
               // more then mixin and primary type
               for (PropertyData propertyData : childs)
               {
                  if (!isNonResidualMatch(propertyData.getQPath().getName(), recipientDefinition))
                  {
                     throw new ConstraintViolationException("Can't remove residual property definition for "
                        + registeredNodeType.getName().getAsString() + " node type, because node "
                        + nodeData.getQPath().getAsString() + " contains property "
                        + propertyData.getQPath().getName().getAsString());
                  }
               }
            }
         }
         else if (!isResidualMatch(removePropertyDefinitionData.getName(), recipientDefinition))
         {
            List<NodeData> nodes =
               getNodes(nodesData, new InternalQName[]{removePropertyDefinitionData.getName()}, new InternalQName[]{});
            if (nodes.size() > 0)
            {
               StringBuilder message =
                  new StringBuilder("Can not remove ").append(removePropertyDefinitionData.getName().getAsString())
                     .append(" PropertyDefinitionData, because the following nodes have these properties: ");
               for (NodeData nodeData : nodes)
               {
                  message.append(nodeData.getQPath().getAsString()).append(" ");
               }
               throw new ConstraintViolationException(message.toString());

            }
         }
      }
   }
}
