/*
 * 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.Set;
import java.util.HashSet;

import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.UnsupportedRepositoryOperationException;

import org.apache.jackrabbit.core.HierarchyManager;
import org.apache.jackrabbit.core.RepositoryImpl;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.session.SessionContext;
import org.apache.jackrabbit.core.state.ItemStateException;
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.LoggerFactory;
import org.slf4j.Logger;

/**
 * The JCR Version Manager implementation is split in several classes in order to
 * group related methods together.
 * <p/>
 * this class provides methods for the configuration and baselines related operations.
 * <p/>
 * Implementation note: methods starting with "internal" are considered to be
 * executed within a "write operations" block.
 */
abstract public class VersionManagerImplConfig extends VersionManagerImplMerge {

    /**
     * default logger
     */
    private static final Logger log = LoggerFactory.getLogger(VersionManagerImplConfig.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 VersionManagerImplConfig(
            SessionContext context, UpdatableItemStateManager stateMgr,
            HierarchyManager hierMgr) {
        super(context, stateMgr, hierMgr);
    }

    /**
     * Restores the versions recorded in the given baseline below the specified
     * path.
     * @param parent the parent state
     * @param name the name of the new node (tree)
     * @param baseline the baseline that recorded the versions
     * @return the node id of the configuration
     * @throws RepositoryException if an error occurs
     */
    protected NodeId restore(NodeStateEx parent, Name name,
                             InternalBaseline baseline)
            throws RepositoryException {
        // check if nt:configuration exists
        NodeId configId = baseline.getConfigurationId();
        NodeId rootId = baseline.getConfigurationRootId();
        if (stateMgr.hasItemState(rootId)) {
            NodeStateEx existing = parent.getNode(rootId);
            String msg = "Configuration for the given baseline already exists at: " + safeGetJCRPath(existing);
            log.error(msg);
            throw new UnsupportedRepositoryOperationException(msg);
        }

        // find version for configuration root
        VersionSet versions = baseline.getBaseVersions();
        InternalVersion rootVersion = null;
        for (InternalVersion v: versions.versions().values()) {
            if (v.getVersionHistory().getVersionableId().equals(rootId)) {
                rootVersion = v;
                break;
            }
        }
        if (rootVersion == null) {
            String msg = "Internal error: supplied baseline has no version for its configuration root.";
            log.error(msg);
            throw new RepositoryException(msg);
        }

        // create new node below parent
        WriteOperation ops = startWriteOperation();
        try {
            if (!stateMgr.hasItemState(configId)) {
                // create if nt:configuration node is not exists
                internalCreateConfiguration(rootId, configId, baseline.getId());
            }
            NodeStateEx config = parent.getNode(configId);

            // create the root node so that the restore works
            InternalFrozenNode fn = rootVersion.getFrozenNode();
            NodeStateEx state = parent.addNode(name, fn.getFrozenPrimaryType(), fn.getFrozenId());
            state.setMixins(fn.getFrozenMixinTypes());
            parent.store();

            // and finally restore the config and root
            internalRestore(config, baseline, null, false);

            ops.save();
            return configId;
        } catch (ItemStateException e) {
            throw new RepositoryException(e);
        } finally {
            ops.close();
        }
    }

    /**
     * Creates a new configuration node.
     * <p/>
     * The nt:confguration is stored within the nt:configurations storage using
     * the nodeid of the configuration root (rootId) as path.
     *
     * @param state the node of the workspace configuration
     * @return the node id of the created configuration
     * @throws RepositoryException if an error occurs
     */
    protected NodeId createConfiguration(NodeStateEx state)
            throws RepositoryException {

        WriteOperation ops = startWriteOperation();
        try {
            NodeId configId = internalCreateConfiguration(state.getNodeId(), null, null);

            // set configuration reference in state
            state.setPropertyValue(NameConstants.JCR_CONFIGURATION, InternalValue.create(configId));
            state.store();
            ops.save();
            return configId;
        } catch (ItemStateException e) {
            throw new RepositoryException(e);
        } finally {
            ops.close();
        }
    }

    /**
     * Creates a new configuration node.
     * <p/>
     * The nt:confguration is stored within the nt:configurations storage using
     * the nodeid of the configuration root (rootId) as path.
     *
     * @param rootId the id of the configuration root node
     * @param configId the id of the configuration node
     * @param baseLine id of the baseline version or <code>null</code>
     * @return the node id of the created configuration
     * @throws RepositoryException if an error occurs
     */
    private NodeId internalCreateConfiguration(NodeId rootId,
                                              NodeId configId, NodeId baseLine)
            throws RepositoryException {

        NodeStateEx configRoot = internalGetConfigRoot();
        NodeStateEx configParent = InternalVersionManagerBase.getParentNode(
                configRoot,
                rootId.toString(),
                NameConstants.REP_CONFIGURATIONS);
        Name name = InternalVersionManagerBase.getName(rootId.toString());

        if (configId == null) {
            configId = context.getNodeIdFactory().newNodeId();
        }
        NodeStateEx config = configParent.addNode(name, NameConstants.NT_CONFIGURATION, configId, true);
        Set<Name> mix = new HashSet<Name>();
        mix.add(NameConstants.REP_VERSION_REFERENCE);
        config.setMixins(mix);
        config.setPropertyValue(NameConstants.JCR_ROOT, InternalValue.create(rootId));

        // init mix:versionable flags
        VersionHistoryInfo vh = vMgr.getVersionHistory(session, config.getState(), null);

        // and set the base version and history to the config
        InternalValue historyId = InternalValue.create(vh.getVersionHistoryId());
        InternalValue versionId = InternalValue.create(
                baseLine == null ? vh.getRootVersionId() : baseLine);

        config.setPropertyValue(NameConstants.JCR_BASEVERSION, versionId);
        config.setPropertyValue(NameConstants.JCR_VERSIONHISTORY, historyId);
        config.setPropertyValue(NameConstants.JCR_ISCHECKEDOUT, InternalValue.create(true));
        config.setPropertyValues(NameConstants.JCR_PREDECESSORS, PropertyType.REFERENCE, new InternalValue[]{versionId});
        configParent.store();

        return configId;
    }

    /**
     * Returns the root node of the configurations storage located at
     * "/jcr:system/jcr:configurations"
     *
     * @return the root node
     * @throws RepositoryException if an error occurs
     */
    private NodeStateEx internalGetConfigRoot() throws RepositoryException {
        NodeStateEx system = getNodeStateEx(RepositoryImpl.SYSTEM_ROOT_NODE_ID);
        NodeStateEx root = system.getNode(NameConstants.JCR_CONFIGURATIONS, 1);
        if (root == null) {
            root = system.addNode(
                    NameConstants.JCR_CONFIGURATIONS,
                    NameConstants.REP_CONFIGURATIONS,
                    RepositoryImpl.CONFIGURATIONS_NODE_ID, false);
            system.store();
        }
        return root;
    }
}