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

import static org.apache.jackrabbit.guava.common.collect.ImmutableSet.of;
import static org.apache.jackrabbit.guava.common.collect.Iterables.concat;
import static org.apache.jackrabbit.guava.common.collect.Sets.newHashSet;
import static java.util.Collections.singleton;
import static org.apache.jackrabbit.JcrConstants.JCR_BASEVERSION;
import static org.apache.jackrabbit.JcrConstants.JCR_CREATED;
import static org.apache.jackrabbit.JcrConstants.JCR_ISCHECKEDOUT;
import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES;
import static org.apache.jackrabbit.JcrConstants.JCR_PREDECESSORS;
import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE;
import static org.apache.jackrabbit.JcrConstants.JCR_SYSTEM;
import static org.apache.jackrabbit.JcrConstants.JCR_UUID;
import static org.apache.jackrabbit.JcrConstants.JCR_VERSIONHISTORY;
import static org.apache.jackrabbit.JcrConstants.JCR_VERSIONSTORAGE;
import static org.apache.jackrabbit.JcrConstants.MIX_REFERENCEABLE;
import static org.apache.jackrabbit.JcrConstants.MIX_VERSIONABLE;
import static org.apache.jackrabbit.oak.plugins.memory.MultiGenericPropertyState.nameProperty;
import static org.apache.jackrabbit.oak.spi.version.VersionConstants.REP_VERSIONSTORAGE;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Set;

import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.commons.PathUtils;
import org.apache.jackrabbit.oak.plugins.nodetype.TypePredicate;
import org.apache.jackrabbit.oak.spi.state.ChildNodeEntry;
import org.apache.jackrabbit.oak.spi.state.NodeBuilder;
import org.apache.jackrabbit.oak.spi.state.NodeState;

import org.apache.jackrabbit.guava.common.base.Joiner;
import org.apache.jackrabbit.util.ISO8601;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class VersionHistoryUtil {

    private static final Logger LOG = LoggerFactory.getLogger(VersionHistoryUtil.class);

    public static String getRelativeVersionHistoryPath(String versionableUuid) {
        return Joiner.on('/').join(concat(
                singleton(""),
                getRelativeVersionHistoryPathSegments(versionableUuid),
                singleton(versionableUuid)));
    }

    /**
     * Constructs the version history path based on the versionable's UUID.
     *
     * @param versionStorage below which to look for the version.
     * @param versionableUuid The String representation of the versionable's UUID.
     * @return The NodeState corresponding to the version history, or {@code null}
     *         if it does not exist.
     */
    static NodeState getVersionHistoryNodeState(NodeState versionStorage, String versionableUuid) {
        NodeState historyParent = versionStorage;
        for (String segment : getRelativeVersionHistoryPathSegments(versionableUuid)) {
            historyParent = historyParent.getChildNode(segment);
        }
        return historyParent.getChildNode(versionableUuid);
    }

    public static NodeBuilder getVersionHistoryBuilder(NodeBuilder versionStorage, String versionableUuid) {
        NodeBuilder history = versionStorage;
        for (String segment : getRelativeVersionHistoryPathSegments(versionableUuid)) {
            history = history.getChildNode(segment);
        }
        return history.getChildNode(versionableUuid);
    }

    private static List<String> getRelativeVersionHistoryPathSegments(String versionableUuid) {
        final List<String> segments = new ArrayList<String>();
        for (int i = 0; i < 3; i++) {
            segments.add(versionableUuid.substring(i * 2, i * 2 + 2));
        }
        return segments;
    }

    public static NodeState getVersionStorage(NodeState root) {
        return root.getChildNode(JCR_SYSTEM).getChildNode(JCR_VERSIONSTORAGE);
    }

    public static NodeBuilder getVersionStorage(NodeBuilder root) {
        return root.getChildNode(JCR_SYSTEM).getChildNode(JCR_VERSIONSTORAGE);
    }

    public static NodeBuilder createVersionStorage(NodeBuilder root) {
        NodeBuilder vs = root.child(JCR_SYSTEM).child(JCR_VERSIONSTORAGE);
        if (!vs.hasProperty(JCR_PRIMARYTYPE)) {
            vs.setProperty(JCR_PRIMARYTYPE, REP_VERSIONSTORAGE, Type.NAME);
        }
        return vs;
    }

    public static List<String> getVersionableNodes(NodeState root, NodeState versionStorage, TypePredicate isVersionable, Calendar olderThan) {
        List<String> paths = new ArrayList<>();
        getVersionableNodes(root, versionStorage, isVersionable, olderThan, PathUtils.ROOT_PATH, paths);
        return paths;
    }


    private static void getVersionableNodes(NodeState node, NodeState versionStorage, TypePredicate isVersionable, Calendar olderThan, String path, List<String> paths) {
        if (isVersionable.test(node)) {
            if (olderThan == null) {
                paths.add(path);
            } else {
                NodeState versionHistory = getVersionHistoryNodeState(versionStorage, node.getString(JCR_UUID));
                Calendar lastModified = getVersionHistoryLastModified(versionHistory);
                if (lastModified.before(olderThan)) {
                    paths.add(path);
                }
            }
        }
        for (ChildNodeEntry c : node.getChildNodeEntries()) {
            getVersionableNodes(c.getNodeState(), versionStorage, isVersionable, olderThan, PathUtils.concat(path, c.getName()), paths);
        }
    }

    public static Calendar getVersionHistoryLastModified(NodeState versionHistory) {
        Calendar youngest = Calendar.getInstance();
        youngest.setTimeInMillis(0);
        for (final ChildNodeEntry entry : versionHistory.getChildNodeEntries()) {
            final NodeState version = entry.getNodeState();
            if (version.hasProperty(JCR_CREATED)) {
                final Calendar created = ISO8601.parse(version.getProperty(JCR_CREATED).getValue(Type.DATE));
                if (created.after(youngest)) {
                    youngest = created;
                }
            }
        }
        return youngest;
    }

    public static void removeVersionProperties(NodeBuilder versionableBuilder, TypePredicate isReferenceable) {
        assert versionableBuilder.exists();

        removeMixin(versionableBuilder, MIX_VERSIONABLE);

        // we don't know if the UUID is otherwise referenced,
        // so make sure the node remains referencable
        if (!isReferenceable.test(versionableBuilder.getNodeState())) {
            addMixin(versionableBuilder, MIX_REFERENCEABLE);
        }

        versionableBuilder.removeProperty(JCR_VERSIONHISTORY);
        versionableBuilder.removeProperty(JCR_PREDECESSORS);
        versionableBuilder.removeProperty(JCR_BASEVERSION);
        versionableBuilder.removeProperty(JCR_ISCHECKEDOUT);
    }

    static void addMixin(NodeBuilder builder, String name) {
        if (builder.hasProperty(JCR_MIXINTYPES)) {
            final Set<String> mixins = newHashSet(builder.getProperty(JCR_MIXINTYPES).getValue(Type.NAMES));
            if (mixins.add(name)) {
                builder.setProperty(nameProperty(JCR_MIXINTYPES, mixins));
            }
        } else {
            builder.setProperty(nameProperty(JCR_MIXINTYPES, of(name)));
        }
    }

    private static void removeMixin(NodeBuilder builder, String name) {
        if (builder.hasProperty(JCR_MIXINTYPES)) {
            final Set<String> mixins = newHashSet(builder.getProperty(JCR_MIXINTYPES).getValue(Type.NAMES));
            if (mixins.remove(name)) {
                if (mixins.isEmpty()) {
                    builder.removeProperty(JCR_MIXINTYPES);
                } else {
                    builder.setProperty(nameProperty(JCR_MIXINTYPES, mixins));
                }
            }
        }
    }

    public static NodeBuilder removeVersions(NodeState root,
                               List<String> toRemove) {
        NodeBuilder rootBuilder = root.builder();
        TypePredicate isReferenceable = new TypePredicate(root, MIX_REFERENCEABLE);
        NodeBuilder versionStorage = getVersionStorage(rootBuilder);
        for (String p : toRemove) {
            LOG.info("Removing version history for {}", p);
            NodeBuilder b = getBuilder(rootBuilder, p);
            String uuid = b.getString(JCR_UUID);
            VersionHistoryUtil.removeVersionProperties(b, isReferenceable);
            getVersionHistoryBuilder(versionStorage, uuid).remove();
        }
        return rootBuilder;
    }

    private static NodeBuilder getBuilder(NodeBuilder root, String path) {
        NodeBuilder builder = root;
        for (String e : PathUtils.elements(path)) {
            builder = builder.child(e);
        }
        return builder;
    }

}
