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

import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

import org.apache.jackrabbit.oak.commons.collections.IteratorUtils;
import org.apache.jackrabbit.oak.plugins.document.util.MergeSortedIterators;
import org.jetbrains.annotations.NotNull;

/**
 * A value map contains the versioned values of a property. The key into this
 * map is the revision when the value was set.
 */
class ValueMap {

    static final SortedMap<Revision, String> EMPTY = Collections.unmodifiableSortedMap(
            new TreeMap<Revision, String>(StableRevisionComparator.REVERSE));

    @NotNull
    static Map<Revision, String> create(@NotNull final NodeDocument doc,
                                        @NotNull final String property) {
        final SortedMap<Revision, String> map = doc.getLocalMap(property);
        if (doc.getPreviousRanges().isEmpty()) {
            return map;
        }
        final Set<Map.Entry<Revision, String>> entrySet
                = new AbstractSet<Map.Entry<Revision, String>>() {

            @Override
            @NotNull
            public Iterator<Map.Entry<Revision, String>> iterator() {

                final Comparator<? super Revision> c = map.comparator();
                final Iterator<NodeDocument> docs;
                if (map.isEmpty()) {
                    docs = doc.getPreviousDocs(property, null).iterator();
                } else {
                    // merge sort local map into maps of previous documents
                    List<Iterator<NodeDocument>> iterators = 
                            new ArrayList<Iterator<NodeDocument>>(2);
                    iterators.add(Collections.singleton(doc).iterator());
                    iterators.add(doc.getPreviousDocs(property, null).iterator());
                    docs = IteratorUtils.mergeSorted(iterators, new Comparator<NodeDocument>() {
                                @Override
                                public int compare(NodeDocument o1,
                                                   NodeDocument o2) {
                                    Revision r1 = getFirstRevision(o1);
                                    Revision r2 = getFirstRevision(o2);
                                    return c.compare(r1, r2);
                                }

                                private Revision getFirstRevision(NodeDocument d) {
                                    Map<Revision, String> values;
                                    if (Objects.equals(d.getId(), doc.getId())) {
                                        // return local map for main document
                                        values = d.getLocalMap(property);
                                    } else {
                                        values = d.getValueMap(property);
                                    }
                                    return values.keySet().iterator().next();
                                }

                            });
                }

                return new MergeSortedIterators<Map.Entry<Revision, String>>(
                        new Comparator<Map.Entry<Revision, String>>() {
                            @Override
                            public int compare(Map.Entry<Revision, String> o1,
                                               Map.Entry<Revision, String> o2) {
                                return c.compare(o1.getKey(), o2.getKey());
                            }
                        }
                ) {
                    @Override
                    public Iterator<Map.Entry<Revision, String>> nextIterator() {
                        NodeDocument d = docs.hasNext() ? docs.next() : null;
                        if (d == null) {
                            return null;
                        }
                        Map<Revision, String> values;
                        if (Objects.equals(d.getId(), doc.getId())) {
                            // return local map for main document
                            values = d.getLocalMap(property);
                        } else {
                            values = d.getValueMap(property);
                        }
                        return values.entrySet().iterator();
                    }

                    @Override
                    public String description() {
                        return "Revisioned values for property " + doc.getId() + "/" + property + ":";
                    }
                };
            }

            @Override
            public int size() {
                int size = map.size();
                for (NodeDocument prev : doc.getPreviousDocs(property, null)) {
                    size += prev.getValueMap(property).size();
                }
                return size;
            }
        };

        return new AbstractMap<Revision, String>() {

            private final Map<Revision, String> map = doc.getLocalMap(property);

            @Override
            @NotNull
            public Set<Entry<Revision, String>> entrySet() {
                return entrySet;
            }

            @Override
            public String get(Object key) {
                Revision r = (Revision) key;
                // first check values map of this document
                if (map.containsKey(r)) {
                    return map.get(r);
                }
                for (NodeDocument prev : doc.getPreviousDocs(property, r)) {
                    String value = prev.getValueMap(property).get(r);
                    if (value != null) {
                        return value;
                    }
                }
                // not found or null
                return null;
            }

            @Override
            public boolean containsKey(Object key) {
                // check local map first
                if (map.containsKey(key)) {
                    return true;
                }
                Revision r = (Revision) key;
                for (NodeDocument prev : doc.getPreviousDocs(property, r)) {
                    if (prev.getValueMap(property).containsKey(key)) {
                        return true;
                    }
                }
                return false;
            }
        };
    }
}
