/*
 * 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.segment.file.tooling;

import static org.apache.jackrabbit.guava.common.base.Preconditions.checkNotNull;
import static org.apache.jackrabbit.oak.commons.PathUtils.elements;
import static org.apache.jackrabbit.oak.json.JsonSerializer.DEFAULT_FILTER_EXPRESSION;
import static org.apache.jackrabbit.oak.segment.file.FileStoreBuilder.fileStoreBuilder;

import java.io.File;
import java.io.IOException;
import java.util.Iterator;

import org.apache.jackrabbit.guava.common.base.Function;
import org.apache.jackrabbit.guava.common.collect.Iterators;
import org.apache.jackrabbit.oak.json.BlobSerializer;
import org.apache.jackrabbit.oak.json.JsonSerializer;
import org.apache.jackrabbit.oak.segment.SegmentNodeState;
import org.apache.jackrabbit.oak.segment.spi.persistence.JournalFile;
import org.apache.jackrabbit.oak.segment.file.InvalidFileStoreVersionException;
import org.apache.jackrabbit.oak.segment.file.JournalEntry;
import org.apache.jackrabbit.oak.segment.file.JournalReader;
import org.apache.jackrabbit.oak.segment.file.ReadOnlyFileStore;
import org.apache.jackrabbit.oak.spi.state.NodeState;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
 * Utility for tracing a node back through the revision history.
 */
public class RevisionHistory {
    private final ReadOnlyFileStore store;

    /**
     * Create a new instance for a {@link ReadOnlyFileStore} in the given {@code directory}.
     *
     * @param directory
     * @throws IOException
     */
    public RevisionHistory(@NotNull File directory) throws IOException, InvalidFileStoreVersionException {
        this.store = fileStoreBuilder(checkNotNull(directory)).buildReadOnly();
    }

    private static NodeState getNode(SegmentNodeState root, String path) {
        NodeState node = root;
        for (String name : elements(path)) {
            node = node.getChildNode(name);
        }
        return node;
    }

    /**
     * Return the history of the node at the given {@code path} according to the passed
     * {@code journal}.
     *
     * @param journal
     * @param path
     * @return
     * @throws IOException
     */
    public Iterator<HistoryElement> getHistory(@NotNull JournalFile journal, @NotNull final String path)
            throws IOException {
        checkNotNull(path);

        try (JournalReader journalReader = new JournalReader(checkNotNull(journal))) {
            return Iterators.transform(journalReader,
                    new Function<JournalEntry, HistoryElement>() {
                        @NotNull @Override
                        public HistoryElement apply(JournalEntry entry) {
                            store.setRevision(entry.getRevision());
                            NodeState node = getNode(store.getHead(), path);
                            return new HistoryElement(entry.getRevision(), node);
                        }
                });
        }
    }

    /**
     * Representation of a point in time for a given node.
     */
    public static final class HistoryElement {
        private final String revision;
        private final NodeState node;

        HistoryElement(String revision, NodeState node) {
            this.revision = revision;
            this.node = node;
        }

        /**
         * Revision of the node
         * @return
         */
        @NotNull
        public String getRevision() {
            return revision;
        }

        /**
         * Node at given revision
         * @return
         */
        @Nullable
        public NodeState getNode() {
            return node;
        }

        /**
         * Serialise this element to JSON up to the given {@code depth}.
         * @param depth
         * @return
         */
        public String toString(int depth) {
            JsonSerializer json = new JsonSerializer(depth, 0, Integer.MAX_VALUE,
                DEFAULT_FILTER_EXPRESSION, new BlobSerializer());
            json.serialize(node);
            return revision + "=" + json;
        }

        /**
         * @return  {@code toString(0)}
         */
        @Override
        public String toString() {
            return toString(0);
        }

        @Override
        public boolean equals(Object other) {
            if (this == other) {
                return true;
            }
            if (other == null || getClass() != other.getClass()) {
                return false;
            }

            HistoryElement that = (HistoryElement) other;
            return revision.equals(that.revision) &&
                (node == null ? that.node == null : node.equals(that.node));

        }

        @Override
        public int hashCode() {
            return 31 * revision.hashCode() +
                (node != null ? node.hashCode() : 0);
        }
    }
}
