/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.jackrabbit.core.state;

import org.apache.jackrabbit.core.id.ItemId;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.id.PropertyId;
import org.apache.jackrabbit.core.nodetype.EffectiveNodeType;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.QNodeDefinition;
import org.apache.jackrabbit.spi.QPropertyDefinition;
import org.apache.jackrabbit.spi.commons.name.NameConstants;

import javax.jcr.nodetype.NoSuchNodeTypeException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;

/**
 * Internal utility class used for merging concurrent changes that occurred
 * on different <code>NodeState</code> instances representing the same node.
 * <p/>
 * See http://issues.apache.org/jira/browse/JCR-584.
 */
class NodeStateMerger {

    /**
     * Tries to silently merge the given <code>state</code> with its
     * externally (e.g. through another session) modified overlayed state
     * in order to avoid an <code>InvalidItemStateException</code>.
     * <p/>
     * See http://issues.apache.org/jira/browse/JCR-584.
     *
     * @param state node state whose modified overlayed state should be
     *        merged
     * @param context used for analyzing the context of the modifications
     * @return true if the changes could be successfully merged into the
     *         given node state; false otherwise
     */
    static boolean merge(NodeState state, MergeContext context) {

        NodeState overlayedState = (NodeState) state.getOverlayedState();
        if (overlayedState == null
                || state.getModCount() == overlayedState.getModCount()) {
            return false;
        }

        synchronized (overlayedState) {
            synchronized (state) {
                /**
                 * some examples for trivial non-conflicting changes:
                 * - s1 added child node a, s2 removes child node b
                 * - s1 adds child node a, s2 adds child node b
                 * - s1 adds child node a, s2 adds mixin type
                 *
                 * conflicting changes causing staleness:
                 * - s1 added non-sns child node or property a,
                 *   s2 added non-sns child node or property a => name clash
                 * - either session reordered child nodes
                 *   (some combinations could possibly be merged)
                 * - either session moved node
                 */

                // compare current state with externally modified overlayed
                // state and determine what has been changed by whom

                // child node entries order
                if (!state.getReorderedChildNodeEntries().isEmpty()) {
                    // for now we don't even try to merge the result of
                    // a reorder operation
                    return false;
                }

                // the primary node type
                if (!state.getNodeTypeName().equals(overlayedState.getNodeTypeName())) {
                    // the primary node type has changed either in 'state' or 'overlayedState'.
                    return false;
                }

                // mixin types
                if (!mergeMixinTypes(state, overlayedState, context)) {
                    return false;
                }

                // parent id
                if (state.getParentId() != null
                        && !state.getParentId().equals(overlayedState.getParentId())) {
                    return false;
                }

                // child node entries
                if (!state.getChildNodeEntries().equals(
                        overlayedState.getChildNodeEntries())) {
                    ArrayList<ChildNodeEntry> added = new ArrayList<ChildNodeEntry>();
                    ArrayList<ChildNodeEntry> removed = new ArrayList<ChildNodeEntry>();

                    for (ChildNodeEntry cne : state.getAddedChildNodeEntries()) {
                        // locally added?
                        if (context.isAdded(cne.getId()) || context.isModified(cne.getId())) {
                            // a new child node entry has been added to this state;
                            // check for name collisions with other state
                            if (overlayedState.hasChildNodeEntry(cne.getName())) {
                                // conflicting names
                                if (cne.getIndex() < 2) {
                                    // check if same-name siblings are allowed
                                    if (!context.allowsSameNameSiblings(cne.getId())) {
                                        return false;
                                    }
                                }
                                // assume same-name siblings are allowed since index is >= 2
                            }

                            added.add(cne);
                        }
                    }

                    for (ChildNodeEntry cne : state.getRemovedChildNodeEntries()) {
                        // locally removed?
                        if (context.isDeleted(cne.getId()) || context.isModified(cne.getId())) {
                            // a child node entry has been removed from this node state
                            removed.add(cne);
                        }
                    }

                    // copy child node entries from other state and
                    // re-apply changes made on this state
                    state.setChildNodeEntries(overlayedState.getChildNodeEntries());
                    for (ChildNodeEntry cne : removed) {
                        state.removeChildNodeEntry(cne.getId());
                    }
                    for (ChildNodeEntry cne : added) {
                        state.addChildNodeEntry(cne.getName(), cne.getId());
                    }
                }

                // property names
                if (!state.getPropertyNames().equals(
                        overlayedState.getPropertyNames())) {
                    HashSet<Name> added = new HashSet<Name>();
                    HashSet<Name> removed = new HashSet<Name>();

                    for (Name name : state.getAddedPropertyNames()) {
                        PropertyId propId =
                                new PropertyId(state.getNodeId(), name);
                        if (context.isAdded(propId)) {
                            added.add(name);
                        }
                    }
                    for (Name name : state.getRemovedPropertyNames()) {
                        PropertyId propId =
                                new PropertyId(state.getNodeId(), name);
                        if (context.isDeleted(propId)) {
                            // a property name has been removed from this state
                            removed.add(name);
                        }
                    }

                    // copy property names from other and
                    // re-apply changes made on this state
                    state.setPropertyNames(overlayedState.getPropertyNames());
                    for (Name name : added) {
                        state.addPropertyName(name);
                    }
                    for (Name name : removed) {
                        state.removePropertyName(name);
                    }
                }

                // finally sync modification count
                state.setModCount(overlayedState.getModCount());

                return true;
            }
        }
    }

    /**
     * 
     * @param state
     * @param overlayedState
     * @return true if the mixin type names are the same in both node states or
     * if the mixin modifications do not conflict (and could be merged); false
     * otherwise.
     */
    private static boolean mergeMixinTypes(NodeState state, NodeState overlayedState, MergeContext ctx) {
        Set<Name> mixins = new HashSet<Name>(state.getMixinTypeNames());
        Set<Name> overlayedMixins = new HashSet<Name>(overlayedState.getMixinTypeNames());
        if (mixins.equals(overlayedMixins)) {
            // no net effect modifications at all -> merge child items defined
            // by the mixins according to the general rule.
            return true;
        }

        PropertyId mixinPropId = new PropertyId(state.getNodeId(), NameConstants.JCR_MIXINTYPES);

        boolean mergeDone;
        if (ctx.isAdded(mixinPropId)) {
            // jcr:mixinTypes property was created for 'state'.
            // changes is safe (without need to merge),
            // - overlayed state doesn't have any mixins OR
            // - overlayed state got the same (or a subset) added
            // and non of the items defined by the new mixin(s) collides with
            // existing items on the overlayed state
            if (overlayedMixins.isEmpty() || mixins.containsAll(overlayedMixins)) {
                mixins.removeAll(overlayedMixins);
                mergeDone = !conflicts(state, mixins, ctx, true);
            } else {
                // different mixins added in overlayedState and state
                // -> don't merge
                mergeDone = false;
            }
        } else if (ctx.isDeleted(mixinPropId)) {
            // jcr:mixinTypes property was removed in 'state'.
            // we can't determine if there was any change to mixin types in the
            // overlayed state.
            // -> don't merge.
            mergeDone = false;
        } else if (ctx.isModified(mixinPropId)) {
            /* jcr:mixinTypes property was modified in 'state'.
               NOTE: if the mixins of the overlayed state was modified as well
               the property (jcr:mixinTypes) cannot not be persisted (stale).

               since there is not way to determine if the overlayed mixins have
               been modified just check for conflicts related to a net mixin
               addition.
             */
            if (mixins.containsAll(overlayedMixins)) {
                // net result of modifications is only addition.
                // -> so far the changes are save if there are no conflicts
                //    caused by mixins modification in 'state'.
                //    NOTE: the save may still fail if the mixin property has
                //    been modified in the overlayed state as well.
                mixins.removeAll(overlayedMixins);
                mergeDone = !conflicts(state, mixins, ctx, true);
            } else {
                // net result is either a removal in 'state' or modifications
                // in both node states.
                // -> don't merge.
                mergeDone = false;
            }
        } else {
            // jcr:mixinTypes property was added or modified in the overlayed
            // state but neither added nor modified in 'state'.
            if (overlayedMixins.containsAll(mixins)) {
                // the modification in the overlayed state only includes the
                // addition of mixin node types, but no removal.
                // -> need to check if any added items from state would
                //    collide with the items defined by the new mixin on the
                //    overlayed state.
                overlayedMixins.removeAll(mixins);
                if (!conflicts(state, overlayedMixins, ctx, false)) {
                    // update the mixin names in 'state'. the child items defined
                    // by the new mixins will be added later on during merge of
                    // child nodes and properties.
                    state.setMixinTypeNames(overlayedMixins);
                    mergeDone = true;
                } else {
                    mergeDone = false;
                }
            } else {
                // either remove-mixin(s) or both add and removal of mixin in
                // the overlayed state.
                // -> we cannot merge easily
                mergeDone = false;
            }
        }

        return mergeDone;
    }

    /**
     *
     * @param state The state of the node to be saved.
     * @param addedMixins The added mixins to be used for testing
     * @param ctx
     * @param compareToOverlayed
     * @return true if a conflict can be determined, false otherwise.
     */
    private static boolean conflicts(NodeState state,
                                     Set<Name> addedMixins,
                                     MergeContext ctx, boolean compareToOverlayed) {
        try {
            // check for all added mixin types in one state if there are colliding
            // child items in the other state.
            // this is currently a simple check for named item definitions;
            // if the mixin defines residual item definitions -> return false.
            for (Name mixinName : addedMixins) {
                EffectiveNodeType ent = ctx.getEffectiveNodeType(mixinName);

                if (ent.getUnnamedItemDefs().length > 0) {
                    // the mixin defines residual child definitions -> cannot
                    // easily determine conflicts
                    return false;
                }

                NodeState overlayed = (NodeState) state.getOverlayedState();
                for (ChildNodeEntry cne : state.getChildNodeEntries()) {
                    if (ent.getNamedNodeDefs(cne.getName()).length > 0) {
                        if (ctx.isAdded(cne.getId()) || isAutoCreated(cne, ent)) {
                            if (!compareToOverlayed || overlayed.hasChildNodeEntry(cne.getName())) {
                                return true;
                            }
                        } // else: neither added nor autocreated in 'state' .

                    } // else: child node not defined by the added mixin type
                }

                for (Name propName : state.getPropertyNames()) {
                    if (ent.getNamedPropDefs(propName).length > 0) {
                        PropertyId pid = new PropertyId(state.getNodeId(), propName);
                        if (ctx.isAdded(pid) || isAutoCreated(propName, ent)) {
                            if (!compareToOverlayed || overlayed.hasPropertyName(propName)) {
                                return true;
                            }
                        } // else: neither added nor autocreated in 'state'
                    } // else: property not defined by added mixin
                }
            }
        } catch (NoSuchNodeTypeException e) {
            // unable to determine collision
            return true;
        }

        // no conflict detected
        return false;
    }

    private static boolean isAutoCreated(ChildNodeEntry cne, EffectiveNodeType ent) {
        for (QNodeDefinition def : ent.getAutoCreateNodeDefs()) {
            if (def.getName().equals(cne.getName())) {
                return true;
            }
        }
        return false;
    }

    private static boolean isAutoCreated(Name propertyName, EffectiveNodeType ent) {
        for (QPropertyDefinition def : ent.getAutoCreatePropDefs()) {
            if (def.getName().equals(propertyName)) {
                return true;
            }
        }
        return false;
    }

    //-----------------------------------------------------< inner interfaces >

    /**
     * The context of a modification.
     */
    static interface MergeContext {
        boolean isAdded(ItemId id);
        boolean isDeleted(ItemId id);
        boolean isModified(ItemId id);
        boolean allowsSameNameSiblings(NodeId id);
        EffectiveNodeType getEffectiveNodeType(Name ntName) throws NoSuchNodeTypeException;
    }
}
