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

import static org.apache.jackrabbit.core.ItemValidator.CHECK_HOLD;
import static org.apache.jackrabbit.core.ItemValidator.CHECK_LOCK;
import static org.apache.jackrabbit.core.ItemValidator.CHECK_PENDING_CHANGES;
import static org.apache.jackrabbit.core.ItemValidator.CHECK_PENDING_CHANGES_ON_NODE;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.jcr.AccessDeniedException;
import javax.jcr.InvalidItemStateException;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.version.Version;
import javax.jcr.version.VersionException;
import javax.jcr.version.VersionHistory;
import javax.jcr.version.VersionManager;

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.session.SessionOperation;
import org.apache.jackrabbit.core.session.SessionWriteOperation;
import org.apache.jackrabbit.core.state.ItemStateException;
import org.apache.jackrabbit.core.state.NodeState;
import org.apache.jackrabbit.core.state.UpdatableItemStateManager;
import org.apache.jackrabbit.core.version.InconsistentVersioningState;
import org.apache.jackrabbit.core.version.InternalActivity;
import org.apache.jackrabbit.core.version.InternalBaseline;
import org.apache.jackrabbit.core.version.InternalVersion;
import org.apache.jackrabbit.core.version.InternalVersionHistory;
import org.apache.jackrabbit.core.version.NodeStateEx;
import org.apache.jackrabbit.core.version.VersionImpl;
import org.apache.jackrabbit.core.version.VersionManagerImplConfig;
import org.apache.jackrabbit.core.version.VersionSet;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.Path;
import org.apache.jackrabbit.spi.commons.name.NameConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Implementation of the {@link javax.jcr.version.VersionManager}.
 * <p/>
 * This class implements the JCR Version Manager interface but most of the
 * operations are performed in the super classes. this is only cosmetic to
 * avoid huge source files.
 */
public class VersionManagerImpl extends VersionManagerImplConfig
        implements VersionManager {

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

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

    private <T> T perform(SessionOperation<T> operation)
            throws RepositoryException {
        return context.getSessionState().perform(operation);
    }

    /** Wrapper around {@link #checkin(String, Calendar)}. */
    public Version checkin(String absPath) throws RepositoryException {
        return checkin(absPath, null);
    }

    /**
     * Creates a new version of the node at the given path.
     *
     * @param absPath node path
     * @param created create time of the new version,
     *                or <code>null</code> for the current time
     * @return new version
     * @throws RepositoryException if the version can not be created
     */
    public Version checkin(final String absPath, final Calendar created)
            throws RepositoryException {
        return perform(new SessionWriteOperation<Version> () {
            public Version perform(SessionContext context)
                    throws RepositoryException {
                NodeStateEx state = getNodeState(
                        absPath,
                        CHECK_LOCK | CHECK_HOLD | CHECK_PENDING_CHANGES_ON_NODE,
                        Permission.VERSION_MNGMT);
                NodeId baseId = checkoutCheckin(state, true, false, created);
                return (Version) session.getNodeById(baseId);
            }
            public String toString() {
                return "versionManager.checkin(" + absPath + ", " + created + ")";
            }
        });
    }

    /**
     * {@inheritDoc}
     */
    public void checkout(final String absPath) throws RepositoryException {
        perform(new SessionWriteOperation<NodeId> () {
            public NodeId perform(SessionContext context)
                    throws RepositoryException {
                NodeStateEx state = getNodeState(
                        absPath,
                        CHECK_LOCK | CHECK_HOLD,
                        Permission.VERSION_MNGMT);
                return checkoutCheckin(state, false, true, null);
            }
            public String toString() {
                return "versionManager.checkout(" + absPath + ")";
            }
        });
    }

    /**
     * {@inheritDoc}
     */
    public Version checkpoint(final String absPath) throws RepositoryException {
        return perform(new SessionWriteOperation<Version> () {
            public Version perform(SessionContext context)
                    throws RepositoryException {
                NodeStateEx state = getNodeState(
                        absPath,
                        CHECK_LOCK | CHECK_HOLD | CHECK_PENDING_CHANGES_ON_NODE,
                        Permission.VERSION_MNGMT);
                NodeId baseId = checkoutCheckin(state, true, true, null);
                return (Version) session.getNodeById(baseId);
            }
            public String toString() {
                return "versionManager.checkpoint(" + absPath + ")";
            }
        });
    }

    /** Wrapper around {@link Node#isCheckedOut()}. */
    public boolean isCheckedOut(String absPath) throws RepositoryException {
        return session.getNode(absPath).isCheckedOut();
    }

    /**
     * {@inheritDoc}
     */
    public VersionHistory getVersionHistory(final String absPath)
            throws RepositoryException {
        return perform(new SessionOperation<VersionHistory> () {
            public VersionHistory perform(SessionContext context)
                    throws RepositoryException {
                NodeStateEx state = getNodeState(absPath);
                InternalVersionHistory vh = getVersionHistory(state);
                if (vh == null) {
                    throw new InconsistentVersioningState("Couldn't get version history for node " + state.getNodeId());
                }
                return (VersionHistory) session.getNodeById(vh.getId());
            }
            public String toString() {
                return "versionManager.getVersionHistory(" + absPath + ")";
            }
        });
    }

    /**
     * {@inheritDoc}
     */
    public Version getBaseVersion(final String absPath)
            throws RepositoryException {
        return perform(new SessionOperation<Version> () {
            public Version perform(SessionContext context)
                    throws RepositoryException {
                NodeStateEx state = getNodeState(absPath);
                InternalVersion v = getBaseVersion(state);
                return (Version) session.getNodeById(v.getId());
            }
            public String toString() {
                return "versionManager.getBaseVersion(" + absPath + ")";
            }
        });
    }

    /** Wrapper around {@link #restore(Version[], boolean)}. */
    public void restore(Version version, boolean removeExisting)
            throws RepositoryException {
        restore(new Version[]{version}, removeExisting);
    }

    /**
     * {@inheritDoc}
     */
    public void restore(final Version[] versions, final boolean removeExisting)
            throws RepositoryException {
        perform(new SessionWriteOperation<Object> () {
            public Object perform(SessionContext context)
                    throws RepositoryException {
                // check for pending changes
                if (session.hasPendingChanges()) {
                    throw new InvalidItemStateException(
                            "Unable to restore version. Session has pending changes.");
                }

                // add all versions to map of versions to restore
                Map<NodeId, InternalVersion> toRestore =
                    new HashMap<NodeId, InternalVersion>();
                for (Version version : versions) {
                    InternalVersion v =
                        vMgr.getVersion(((VersionImpl) version).getNodeId());
                    // check for collision
                    NodeId historyId = v.getVersionHistory().getId();
                    if (toRestore.containsKey(historyId)) {
                        throw new VersionException(
                                "Unable to restore. Two or more versions have same version history.");
                    }
                    toRestore.put(historyId, v);
                }

                WriteOperation ops = startWriteOperation();
                try {
                    internalRestore(
                            new VersionSet(toRestore, true), removeExisting);
                    ops.save();
                } catch (ItemStateException e) {
                    throw new RepositoryException(e);
                } finally {
                    ops.close();
                }

                return this;
            }
            public String toString() {
                return "versionManager.restore(versions, " + removeExisting + ")";
            }
        });
    }

    /**
     * {@inheritDoc}
     */
    public void restore(
            final String absPath, final String versionName,
            final boolean removeExisting) throws RepositoryException {
        perform(new SessionWriteOperation<Object> () {
            public Object perform(SessionContext context)
                    throws RepositoryException {
                NodeStateEx state = getNodeState(
                        absPath,
                        CHECK_PENDING_CHANGES | CHECK_LOCK | CHECK_HOLD,
                        Permission.NONE);
                restore(state, context.getQName(versionName), removeExisting);
                return this;
            }
            public String toString() {
                return "versionManager.restore("
                    + absPath + ", " +  versionName + ", "
                    +  removeExisting + ")";
            }
        });
    }

    /**
     * {@inheritDoc}
     */
    public void restore(
            final String absPath, final Version version, final boolean removeExisting)
            throws RepositoryException {
        perform(new SessionWriteOperation<Object> () {
            public Object perform(SessionContext context)
                    throws RepositoryException {
                // first check if node exists
                if (session.nodeExists(absPath)) {
                    throw new VersionException(
                            "VersionManager.restore(String, Version, boolean)"
                            + " not allowed on existing nodes; use"
                            + " VersionManager.restore(Version, boolean) instead: "
                            + absPath);
                } else {
                    // parent has to exist
                    Path path = context.getQPath(absPath);
                    Path parentPath = path.getAncestor(1);
                    Name name = path.getName();
                    NodeImpl parent = context.getItemManager().getNode(parentPath);

                    NodeStateEx state = getNodeState(
                            parent,
                            CHECK_PENDING_CHANGES | ItemValidator.CHECK_LOCK | CHECK_HOLD,
                            Permission.NONE);

                    // check if given version is a baseline
                    InternalVersion v = getVersion(version);
                    if (v instanceof InternalBaseline) {
                        restore(state, name, (InternalBaseline) v);
                    } else {
                        restore(state, name, v, removeExisting);
                    }
                }
                return this;
            }
            public String toString() {
                return "versionManager.restore("
                    + absPath + ", version, " + removeExisting + ")";
            }
        });
    }

    /**
     * Same as {@link #restore(String, String, boolean)} but to ensure
     * backward compatibility for Node.restore(Version, boolean).
     *
     * @param node the node to restore
     * @param version the version to restore
     * @param removeExisting the remove existing flag
     * @throws RepositoryException if an error occurs
     */
    protected void restore(NodeImpl node, Version version, boolean removeExisting)
            throws RepositoryException {
        NodeStateEx state = getNodeState(
                node.getPath(),
                CHECK_PENDING_CHANGES | CHECK_LOCK | CHECK_HOLD,
                Permission.NONE);
        InternalVersion v = getVersion(version);
        restore(state, v, removeExisting);
    }

    /**
     * {@inheritDoc}
     */
    public void restoreByLabel(
            final String absPath, final String versionLabel,
            final boolean removeExisting) throws RepositoryException {
        perform(new SessionWriteOperation<Object> () {
            public Object perform(SessionContext context)
                    throws RepositoryException {
                NodeStateEx state = getNodeState(
                        absPath,
                        CHECK_PENDING_CHANGES | CHECK_LOCK | CHECK_HOLD,
                        Permission.NONE);
                restoreByLabel(
                        state, context.getQName(versionLabel), removeExisting);
                return this;
            }
            public String toString() {
                return "versionManager.restoreByLabel("
                    + absPath + ", " +  versionLabel + ", "
                    + removeExisting + ")";
            }
        });
    }

    /**
     * Does an update.
     * @see javax.jcr.Node#update(String)
     *
     * @param node the node to update
     * @param srcWorkspaceName the source workspace name
     * @throws RepositoryException if an error occurs
     */
    public void update(NodeImpl node, String srcWorkspaceName)
            throws RepositoryException {
        NodeStateEx state = getNodeState(node,
                ItemValidator.CHECK_PENDING_CHANGES,
                Permission.VERSION_MNGMT);
        mergeOrUpdate(state, srcWorkspaceName, null, false, false);
    }

    /** Wrapper around {@link #merge(String, String, boolean, boolean)}. */
    public NodeIterator merge(
            String absPath, String srcWorkspace, boolean bestEffort)
            throws RepositoryException {
        return merge(absPath, srcWorkspace, bestEffort, false);
    }

    /**
     * {@inheritDoc}
     */
    public NodeIterator merge(
            final String absPath, final String srcWorkspaceName,
            final boolean bestEffort, final boolean isShallow)
            throws RepositoryException {
        return perform(new SessionWriteOperation<NodeIterator> () {
            public NodeIterator perform(SessionContext context)
                    throws RepositoryException {
                NodeStateEx state = getNodeState(
                        absPath,
                        CHECK_PENDING_CHANGES,
                        Permission.VERSION_MNGMT);
                List<ItemId> failedIds = new LinkedList<ItemId>();
                mergeOrUpdate(state, srcWorkspaceName, failedIds, bestEffort, isShallow);
                return new LazyItemIterator(context, failedIds);
            }
            public String toString() {
                return "versionManager.merge("
                    + absPath + ", " +  srcWorkspaceName + ", "
                    + bestEffort + ", " +  isShallow + ")";
            }
        });
    }

    /**
     * Combines merge and update method
     * @param state the state to merge or update
     * @param srcWorkspaceName source workspace name
     * @param failedIds list that will contain the failed ids.
     *        if <code>null</code> and update will be performed.
     * @param bestEffort best effort flag
     * @param isShallow is shallow flag
     * @throws RepositoryException if an error occurs
     */
    private void mergeOrUpdate(NodeStateEx state, String srcWorkspaceName,
                               List<ItemId> failedIds, boolean bestEffort,
                               boolean isShallow)
            throws RepositoryException {
        // if same workspace, ignore
        if (!srcWorkspaceName.equals(session.getWorkspace().getName())) {
            // check authorization for specified workspace
            if (!session.getAccessManager().canAccess(srcWorkspaceName)) {
                String msg = "not authorized to access " + srcWorkspaceName;
                log.error(msg);
                throw new AccessDeniedException(msg);
            }
            // get root node of src workspace
            SessionImpl srcSession = null;
            try {
                // create session on other workspace for current subject
                // (may throw NoSuchWorkspaceException and AccessDeniedException)
                srcSession = ((RepositoryImpl) session.getRepository())
                        .createSession(session.getSubject(), srcWorkspaceName);
                WorkspaceImpl srcWsp = (WorkspaceImpl) srcSession.getWorkspace();
                NodeId rootNodeId = ((NodeImpl) srcSession.getRootNode()).getNodeId();
                NodeStateEx srcRoot = new NodeStateEx(
                        srcWsp.getItemStateManager(),
                        ntReg,
                        rootNodeId);
                merge(state, srcRoot, failedIds, bestEffort, isShallow);
            } catch (ItemStateException e) {
                throw new RepositoryException(e);
            } finally {
                if (srcSession != null) {
                    // we don't need the other session anymore, logout
                    srcSession.logout();
                }
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public void doneMerge(String absPath, Version version)
            throws RepositoryException {
        NodeStateEx state = getNodeState(absPath,
                ItemValidator.CHECK_LOCK | ItemValidator.CHECK_PENDING_CHANGES_ON_NODE | ItemValidator.CHECK_HOLD,
                Permission.VERSION_MNGMT);
        finishMerge(state, version, false);
    }

    /**
     * {@inheritDoc}
     */
    public void cancelMerge(String absPath, Version version)
            throws RepositoryException {
        NodeStateEx state = getNodeState(absPath,
                ItemValidator.CHECK_LOCK | ItemValidator.CHECK_PENDING_CHANGES_ON_NODE | ItemValidator.CHECK_HOLD,
                Permission.VERSION_MNGMT);
        finishMerge(state, version, true);
    }

    /**
     * {@inheritDoc}
     */
    public Node createConfiguration(String absPath) throws RepositoryException {
        if (session.nodeExists(absPath)) {
            NodeStateEx state = getNodeState(absPath,
                    ItemValidator.CHECK_LOCK | ItemValidator.CHECK_PENDING_CHANGES_ON_NODE | ItemValidator.CHECK_HOLD,
                    Permission.VERSION_MNGMT);
            // check versionable
            if (!checkVersionable(state)) {
                throw new UnsupportedRepositoryOperationException("Node not full versionable: " + absPath);
            }
            if (state.getPropertyValue(NameConstants.JCR_CONFIGURATION) != null) {
                String msg = "Node is already a configuration root: " + absPath;
                log.error(msg);
                throw new UnsupportedRepositoryOperationException(msg);
            }

            NodeId configId = createConfiguration(state);
            return session.getNodeById(configId);
        } else {
            String msg = "Create configuration node must exist: " + absPath;
            log.error(msg);
            throw new UnsupportedRepositoryOperationException(msg);
        }
    }

    /**
     * {@inheritDoc}
     */
    public Node setActivity(Node activity) throws RepositoryException {
        Node oldActivity = getActivity();
        if (activity == null) {
            currentActivity = null;
        } else {
            NodeImpl actNode = (NodeImpl) activity;
            if (!actNode.isNodeType(NameConstants.NT_ACTIVITY)) {
                String msg = "Given node is not an activity: " + actNode.safeGetJCRPath();
                log.error(msg);
                throw new UnsupportedRepositoryOperationException(msg);
            }
            currentActivity = actNode.getNodeId();
        }
        return oldActivity;
    }

    /**
     * {@inheritDoc}
     */
    public Node getActivity() throws RepositoryException {
        if (currentActivity == null) {
            return null;
        } else {
            return session.getNodeById(currentActivity);
        }
    }

    /**
     * {@inheritDoc}
     */
    public Node createActivity(String title) throws RepositoryException {
        NodeId id = vMgr.createActivity(session, title);
        return session.getNodeById(id);
    }

    /**
     * {@inheritDoc}
     */
    public void removeActivity(Node node) throws RepositoryException {
        NodeImpl actNode = (NodeImpl) node;
        if (!actNode.isNodeType(NameConstants.NT_ACTIVITY)) {
            String msg = "Given node is not an activity: " + actNode.safeGetJCRPath();
            log.error(msg);
            throw new UnsupportedRepositoryOperationException(msg);
        }
        NodeId actId = actNode.getNodeId();
        vMgr.removeActivity(session, actId);
        if (actId.equals(currentActivity)) {
            currentActivity = null;
        }
    }

    /**
     * {@inheritDoc}
     */
    public NodeIterator merge(Node activityNode) throws RepositoryException {
        NodeImpl actNode = (NodeImpl) activityNode;
        if (!actNode.isNodeType(NameConstants.NT_ACTIVITY)) {
            String msg = "Given node is not an activity: " + actNode.safeGetJCRPath();
            log.error(msg);
            throw new UnsupportedRepositoryOperationException(msg);
        }
        InternalActivity activity = vMgr.getActivity(actNode.getNodeId());
        if (activity == null) {
            String msg = "Given activity not found in version storage.";
            log.error(msg);
            throw new UnsupportedRepositoryOperationException(msg);
        }
        List<ItemId> failedIds = new ArrayList<ItemId>();
        merge(activity, failedIds);
        return new LazyItemIterator(context, failedIds);
    }

    /**
     * returns the node state for the given path
     * @param path path of the node
     * @throws RepositoryException if an error occurs
     * @return the node state
     */
    private NodeStateEx getNodeState(String path) throws RepositoryException {
        return getNodeState(path, 0, 0);
    }

    /**
     * checks the permissions for the given path and returns the node state
     * @param path path of the node
     * @param options options to check
     * @param permissions permissions to check
     * @throws RepositoryException if an error occurs
     * @return the node state
     */
    private NodeStateEx getNodeState(String path, int options, int permissions)
            throws RepositoryException {
        return getNodeState((NodeImpl) session.getNode(path), options, permissions);
    }

    /**
     * checks the permissions for the given path and returns the node state
     * @param node the node
     * @param options options to check
     * @param permissions permissions to check
     * @throws RepositoryException if an error occurs
     * @return the node state
     */
    private NodeStateEx getNodeState(NodeImpl node, int options, int permissions)
            throws RepositoryException {
        try {
            if (options > 0 || permissions > 0) {
                context.getItemValidator().checkModify(node, options, permissions);
            }
            return new NodeStateEx(
                    stateMgr,
                    ntReg,
                    (NodeState) stateMgr.getItemState(node.getNodeId()),
                    node.getQName());
        } catch (ItemStateException e) {
            throw new RepositoryException(e);
        }
    }

}
