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

import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import javax.jcr.AccessDeniedException;
import javax.jcr.MergeException;
import javax.jcr.Node;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.version.Version;
import javax.jcr.version.VersionException;

import org.apache.jackrabbit.core.HierarchyManager;
import org.apache.jackrabbit.core.ItemValidator;
import org.apache.jackrabbit.core.id.ItemId;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.security.authorization.Permission;
import org.apache.jackrabbit.core.session.SessionContext;
import org.apache.jackrabbit.core.state.ChildNodeEntry;
import org.apache.jackrabbit.core.state.ItemStateException;
import org.apache.jackrabbit.core.state.PropertyState;
import org.apache.jackrabbit.core.state.UpdatableItemStateManager;
import org.apache.jackrabbit.core.value.InternalValue;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.commons.name.NameConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The JCR Version Manager implementation is split in several classes in order to
 * group related methods together.
 * </p>
 * this class provides methods for the merge operations.
 */
abstract public class VersionManagerImplMerge extends VersionManagerImplRestore {

    /**
     * default logger
     */
    private static final Logger log = LoggerFactory.getLogger(VersionManagerImplMerge.class);

    /**
     * Creates a new version manager for the given session
     *
     * @param context component context of the current session
     * @param stateMgr the underlying state manager
     * @param hierMgr local hierarchy manager
     */
    protected VersionManagerImplMerge(
            SessionContext context, UpdatableItemStateManager stateMgr,
            HierarchyManager hierMgr) {
        super(context, stateMgr, hierMgr);
    }

    /**
     * Merges/Updates this node with its corresponding ones
     *
     * @param state state to merge or update
     * @param srcRoot src workspace root node
     * @param failedIds list of failed ids
     * @param bestEffort best effort flag
     * @param shallow is shallow flag
     * @throws RepositoryException if an error occurs
     * @throws ItemStateException if an error occurs
     */
    protected void merge(NodeStateEx state, NodeStateEx srcRoot,
                       List<ItemId> failedIds,
                       boolean bestEffort, boolean shallow)
            throws RepositoryException, ItemStateException {

        if (shallow) {
            // If <code>isShallow</code> is <code>true</code> and this node is not
            // versionable, then this method returns and no changes are made.
            if (!state.getEffectiveNodeType().includesNodeType(NameConstants.MIX_VERSIONABLE)) {
                return;
            }
        }
        NodeStateEx srcNode = getCorrespondingNode(state, srcRoot);
        if (srcNode == null) {
            // If this node (the one on which merge is called) does not have a corresponding
            // node in the indicated workspace, then the merge method returns quietly and no
            // changes are made.
            return;
        }
        WriteOperation ops = startWriteOperation();
        try {
            internalMerge(state, srcRoot, failedIds, bestEffort, shallow);
            state.store();
            ops.save();
        } finally {
            ops.close();
        }
    }

    /**
     * Merges/Updates this node with its corresponding ones
     *
     * @param state state to merge or update
     * @param srcRoot src workspace root node
     * @param failedIds list of failed ids
     * @param bestEffort best effort flag
     * @param shallow is shallow flag
     * @throws RepositoryException if an error occurs
     * @throws ItemStateException if an error occurs
     */
    private void internalMerge(NodeStateEx state, NodeStateEx srcRoot,
                       List<ItemId> failedIds,
                       boolean bestEffort, boolean shallow)
            throws RepositoryException, ItemStateException {

        NodeStateEx srcNode = doMergeTest(state, srcRoot, failedIds, bestEffort);
        if (srcNode == null) {
            if (!shallow) {
                // leave, iterate over children, but ignore non-versionable child
                // nodes (see JCR-1046)
                for (NodeStateEx n: state.getChildNodes()) {
                    if (n.getEffectiveNodeType().includesNodeType(NameConstants.MIX_VERSIONABLE)) {
                        internalMerge(n, srcRoot, failedIds, bestEffort, shallow);
                    }
                }
            }
            return;
        }

        // check lock and hold status if node exists
        checkModify(state, ItemValidator.CHECK_LOCK | ItemValidator.CHECK_HOLD, Permission.NONE);

        // remove all properties that are not present in srcNode
        for (PropertyState prop: state.getProperties()) {
            if (!srcNode.hasProperty(prop.getName())) {
                state.removeProperty(prop.getName());
            }
        }

        // update all properties from the src node
        for (PropertyState prop: srcNode.getProperties()) {
            Name propName = prop.getName();
            // ignore system types
            if (propName.equals(NameConstants.JCR_PRIMARYTYPE)
                    || propName.equals(NameConstants.JCR_MIXINTYPES)
                    || propName.equals(NameConstants.JCR_UUID)) {
                continue;
            }
            state.copyFrom(prop);
        }

        // update the mixin types
        state.setMixins(srcNode.getState().getMixinTypeNames());

        // remove the child nodes in N but not in N'
        LinkedList<ChildNodeEntry> toDelete = new LinkedList<ChildNodeEntry>();
        for (ChildNodeEntry entry: state.getState().getChildNodeEntries()) {
            if (!srcNode.getState().hasChildNodeEntry(entry.getName(), entry.getIndex())) {
                toDelete.add(entry);
            }
        }
        for (ChildNodeEntry entry: toDelete) {
            state.removeNode(entry.getName(), entry.getIndex());
        }
        state.store();

        // add source ones
        for (ChildNodeEntry entry: srcNode.getState().getChildNodeEntries()) {
            NodeStateEx child = state.getNode(entry.getName(), entry.getIndex());
            if (child == null) {
                // if destination workspace already has such an node, remove it
                if (state.hasNode(entry.getId())) {
                    child = state.getNode(entry.getId());
                    NodeStateEx parent = child.getParent();
                    parent.removeNode(child);
                    parent.store();
                }
                // create new child
                NodeStateEx srcChild = srcNode.getNode(entry.getId());
                child = state.addNode(entry.getName(), srcChild.getState().getNodeTypeName(), srcChild.getNodeId());
                child.setMixins(srcChild.getState().getMixinTypeNames());
                // copy src child
                state.store();
                internalMerge(child, srcRoot, null, bestEffort, false);
            } else if (!shallow) {
                // recursively merge
                internalMerge(child, srcRoot, failedIds, bestEffort, false);

            }
        }
    }

    /**
     * Returns the corresponding node in the workspace of the given session.
     * <p/>
     * Given a node N1 in workspace W1, its corresponding node N2 in workspace
     * W2 is defined as follows:
     * <ul>
     * <li>If N1 is the root node of W1 then N2 is the root node of W2.
     * <li>If N1 is referenceable (has a UUID) then N2 is the node in W2 with
     * the same UUID.
     * <li>If N1 is not referenceable (does not have a UUID) then there is some
     * node M1 which is either the nearest ancestor of N1 that is
     * referenceable, or is the root node of W1. If the corresponding node
     * of M1 is M2 in W2, then N2 is the node with the same relative path
     * from M2 as N1 has from M1.
     * </ul>
     *
     * @param state N1
     * @param srcRoot the root node state of W2
     * @return the corresponding node or <code>null</code> if no corresponding
     *         node exists.
     * @throws RepositoryException If another error occurs.
     */
    private NodeStateEx getCorrespondingNode(NodeStateEx state, NodeStateEx srcRoot)
            throws RepositoryException {

        // search nearest ancestor that is referenceable
        NodeStateEx m1 = state;
        LinkedList<ChildNodeEntry> elements = new LinkedList<ChildNodeEntry>();
        while (m1.getParentId() != null &&
                !m1.getEffectiveNodeType().includesNodeType(NameConstants.MIX_REFERENCEABLE)) {
            NodeStateEx parent = m1.getParent();
            elements.addFirst(parent.getState().getChildNodeEntry(m1.getNodeId()));
            m1 = parent;
        }

        // check if corresponding ancestor exists
        if (srcRoot.hasNode(m1.getNodeId())) {
            NodeStateEx m2 = srcRoot.getNode(m1.getNodeId());
            Iterator<ChildNodeEntry> iter = elements.iterator();
            while (iter.hasNext() && m2 != null) {
                ChildNodeEntry e = iter.next();
                m2 = m2.getNode(e.getName(), e.getIndex());
            }
            return m2;
        } else {
            return null;
        }
    }

    /**
     * Performs the merge test. If the result is 'update', then the corresponding
     * source node is returned. if the result is 'leave' or 'besteffort-fail'
     * then <code>null</code> is returned. If the result of the merge test is
     * 'fail' with bestEffort set to <code>false</code> a MergeException is
     * thrown.
     * <p/>
     * jsr170 - 8.2.10 Merge:
     * [...]
     * <ul>
     * <li>If N is currently checked-in then:</li>
     * <ul>
     * <li>If V' is a successor (to any degree) of V, then the merge result
     *     for N is update.
     * </li>
     * <li>If V' is a predecessor (to any degree) of V or if V and
     *     V' are identical (i.e., are actually the same version),
     *     then the merge result for N is leave.
     * </li>
     * <li>If V is neither a successor of, predecessor of, nor
     *     identical with V', then the merge result for N is failed.
     *     This is the case where N and N' represent divergent
     *     branches of the version graph, thus determining the
     *     result of a merge is non-trivial.
     * </li>
     * </ul>
     * <li>If N is currently checked-out then:</li>
     * <ul>
     * <li>If V' is a predecessor (to any degree) of V or if V and
     *     V' are identical (i.e., are actually the same version),
     *     then the merge result for N is leave.
     * </li>
     * <li>If any other relationship holds between V and V',
     *     then the merge result for N is fail.
     * </li>
     * </ul>
     * </ul>
     *
     * @param state state to test
     * @param srcRoot root node state of the source workspace
     * @param failedIds the list to store the failed node ids.
     * @param bestEffort the best effort flag
     * @return the corresponding source node or <code>null</code>
     * @throws RepositoryException if an error occurs.
     * @throws AccessDeniedException if access is denied
     */
    private NodeStateEx doMergeTest(NodeStateEx state, NodeStateEx srcRoot, List<ItemId> failedIds, boolean bestEffort)
            throws RepositoryException, AccessDeniedException {

        // If N does not have a corresponding node then the merge result for N is leave.
        NodeStateEx srcNode = getCorrespondingNode(state, srcRoot);
        if (srcNode == null) {
            return null;
        }

        // if not versionable, update
        if (!state.getEffectiveNodeType().includesNodeType(NameConstants.MIX_VERSIONABLE) || failedIds == null) {
            return srcNode;
        }
        // if source node is not versionable, leave
        if (!srcNode.getEffectiveNodeType().includesNodeType(NameConstants.MIX_VERSIONABLE)) {
            return null;
        }
        // test versions. the following code could be simplified but is
        // intentionally expanded for follow the spec.
        InternalVersion v = getBaseVersion(state);
        InternalVersion vp = getBaseVersion(srcNode);
        if (!isCheckedOut(state)) {
            // If N is currently checked-in then:
            if (vp.isMoreRecent(v)) {
                // - If V' is an eventual successor of V, then the merge result for N is update.
                return srcNode;
            } else if (v.equals(vp) || v.isMoreRecent(vp)) {
                // - If V' is an eventual predecessor of V or if V and V' are identical (i.e., are
                // actually the same version), then the merge result for N is leave.
                return null;
            }
            // - If V is neither an eventual successor of, eventual predecessor of, nor
            // identical with V', then the merge result for N is failed. This is the case
            // where N and N' represent divergent branches of the version graph.

            // failed is covered below
        } else {
            // If N is currently checked-out then:
            if (v.equals(vp) || v.isMoreRecent(vp)) {
                // - If V' is an eventual predecessor of V or if V and V' are identical (i.e., are
                //   actually the same version), then the merge result for
                //   N is leave.
                return null;
            }
            // - If any other relationship holds between V and V', then the merge result
            //   for N is fail.

            // failed is covered below
        }


        if (vp.isMoreRecent(v) && !isCheckedOut(state)) {
            // I f V' is a successor (to any degree) of V, then the merge result for
            // N is update. This case can be thought of as the case where N' is
            // "newer" than N and therefore N should be updated to reflect N'.
            return srcNode;
        } else if (v.equals(vp) || v.isMoreRecent(vp)) {
            // If V' is a predecessor (to any degree) of V or if V and V' are
            // identical (i.e., are actually the same version), then the merge
            // result for N is leave. This case can be thought of as the case where
            // N' is "older" or the "same age" as N and therefore N should be left alone.
            return null;
        } else {
            // If V is neither a successor of, predecessor of, nor identical
            // with V', then the merge result for N is failed. This is the case
            // where N and N' represent divergent branches of the version graph,
            // thus determining the result of a merge is non-trivial.
            if (bestEffort) {
                // add 'offending' version to jcr:mergeFailed property
                Set<NodeId> set = getMergeFailed(state);
                set.add(vp.getId());
                setMergeFailed(state, set);
                failedIds.add(state.getNodeId());
                state.store();
                return null;
            } else {
                String msg =
                    "Unable to merge nodes. Violating versions. " + safeGetJCRPath(state);
                log.debug(msg);
                throw new MergeException(msg);
            }
        }
    }

    /**
     * Perform {@link Node#cancelMerge(Version)} or {@link Node#doneMerge(Version)}
     * depending on the value of <code>cancel</code>.
     * @param state state to finish
     * @param version version
     * @param cancel flag inidicates if this is a cancel operation
     * @throws RepositoryException if an error occurs
     */
    protected void finishMerge(NodeStateEx state, Version version, boolean cancel)
            throws RepositoryException {
        // check versionable
        if (!checkVersionable(state)) {
            throw new UnsupportedRepositoryOperationException("Node not full versionable: " + safeGetJCRPath(state));
        }

        // check if version is in mergeFailed list
        Set<NodeId> failed = getMergeFailed(state);
        NodeId versionId = ((VersionImpl) version).getNodeId();
        if (!failed.remove(versionId)) {
            String msg =
                "Unable to finish merge. Specified version is not in"
                + " jcr:mergeFailed property: " + safeGetJCRPath(state);
            log.error(msg);
            throw new VersionException(msg);
        }

        WriteOperation ops = startWriteOperation();
        try {
            // remove version from mergeFailed list
            setMergeFailed(state, failed);

            if (!cancel) {
                // add version to jcr:predecessors list
                InternalValue[] vals = state.getPropertyValues(NameConstants.JCR_PREDECESSORS);
                InternalValue[] v = new InternalValue[vals.length + 1];
                for (int i = 0; i < vals.length; i++) {
                    v[i] = InternalValue.create(vals[i].getNodeId());
                }
                v[vals.length] = InternalValue.create(versionId);
                state.setPropertyValues(NameConstants.JCR_PREDECESSORS, PropertyType.REFERENCE, v, true);
            }
            state.store();
            ops.save();
        } catch (ItemStateException e) {
            throw new RepositoryException(e);
        } finally {
            ops.close();
        }
    }

    /**
     * Returns a set of nodeid from the jcr:mergeFailed property of the given state
     * @param state the state
     * @return set of node ids
     * @throws RepositoryException if an error occurs
     */
    private Set<NodeId> getMergeFailed(NodeStateEx state)
            throws RepositoryException {
        Set<NodeId> set = new HashSet<NodeId>();
        if (state.hasProperty(NameConstants.JCR_MERGEFAILED)) {
            InternalValue[] vals = state.getPropertyValues(NameConstants.JCR_MERGEFAILED);
            for (InternalValue val : vals) {
                set.add(val.getNodeId());
            }
        }
        return set;
    }

    /**
     * Updates the merge failed property of the given state/
     * @param state the state to update
     * @param set the set of ids
     * @throws RepositoryException if an error occurs
     */
    private void setMergeFailed(NodeStateEx state, Set<NodeId> set)
            throws RepositoryException {
        if (set.isEmpty()) {
            state.removeProperty(NameConstants.JCR_MERGEFAILED);
        } else {
            InternalValue[] vals = new InternalValue[set.size()];
            Iterator<NodeId> iter = set.iterator();
            int i = 0;
            while (iter.hasNext()) {
                NodeId id = iter.next();
                vals[i++] = InternalValue.create(id);
            }
            state.setPropertyValues(NameConstants.JCR_MERGEFAILED, PropertyType.REFERENCE, vals, true);
        }
    }

    /**
     * Merge the given activity to this workspace
     *
     * @param activity internal activity
     * @param failedIds list of failed ids
     * @throws RepositoryException if an error occurs
     */
    protected void merge(InternalActivity activity, List<ItemId> failedIds)
            throws RepositoryException {

        VersionSet changeSet = activity.getChangeSet();
        WriteOperation ops = startWriteOperation();
        try {
            Iterator<NodeId> iter = changeSet.versions().keySet().iterator();
            while (iter.hasNext()) {
                InternalVersion v = changeSet.versions().remove(iter.next());
                NodeStateEx state = getNodeStateEx(v.getFrozenNode().getFrozenId());
                if (state != null) {
                    InternalVersion base = getBaseVersion(state);
                    // if base version is newer than version, add to failed list
                    // but merge it anyways
                    if (base.isMoreRecent(v)) {
                        failedIds.add(state.getNodeId());
                        // add it to the jcr:mergeFailed property
                        Set<NodeId> set = getMergeFailed(state);
                        set.add(base.getId());
                        setMergeFailed(state, set);
                        state.store();
                    } else {
                        for (InternalVersion restored: internalRestore(state, v, changeSet, true)) {
                            changeSet.versions().remove(restored.getVersionHistory().getId());
                        }
                    }
                }

                // reset iterator
                iter = changeSet.versions().keySet().iterator();
            }
            ops.save();
        } catch (ItemStateException e) {
            throw new RepositoryException(e);
        } finally {
            ops.close();
        }
    }

}