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

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;

import org.apache.jackrabbit.guava.common.collect.Iterators;
import org.apache.jackrabbit.guava.common.collect.PeekingIterator;

/**
 * <code>MergeSortedIterators</code> is a specialized implementation of a
 * merge sort of already sorted iterators of some type of comparable elements.
 * The input iterators must return the elements in sorted order according to
 * the provided Comparator. In addition the sequence of iterators must also
 * be sorted in a way that the first element of the next iterator is greater
 * than the first element of the previous iterator.
 *
 * @param <T> the entry type
 */
public abstract class MergeSortedIterators<T> implements Iterator<T> {

    private final List<PeekingIterator<T>> iterators = new ArrayList<PeekingIterator<T>>();
    private final Comparator<T> comparator;
    private T lastPeek;

    public MergeSortedIterators(final Comparator<T> comparator) {
        this.comparator = comparator;
        fetchNextIterator();
    }

    /**
     * @return the next {@link Iterator} or <code>null</code> if there is none.
     */
    public abstract Iterator<T> nextIterator();

    /**
     * Provides details about this iterator
     */
    public String description() {
        return "";
    }

    //----------------------------< Iterator >----------------------------------

    @Override
    public boolean hasNext() {
        return !iterators.isEmpty();
    }

    @Override
    public T next() {
        if (!hasNext()) {
            throw new NoSuchElementException();
        }
        PeekingIterator<T> it = iterators.get(0);
        T next = it.next();
        // more elements?
        if (it.hasNext()) {
            adjustFirst();
        } else {
            // remove from list of iterators
            iterators.remove(0);
        }
        // fetch next iterator?
        if (comparator.compare(next, lastPeek) >= 0) {
            fetchNextIterator();
        }
        return next;
    }

    @Override
    public void remove() {
        throw new UnsupportedOperationException();
    }

    //----------------------------< internal >----------------------------------

    private void fetchNextIterator() {
        Iterator<T> it = nextIterator();
        if (it != null && it.hasNext()) {
            PeekingIterator<T> pIt = Iterators.peekingIterator(it);
            if (!iterators.isEmpty()
                    &&  comparator.compare(pIt.peek(), lastPeek) < 0) {
                throw new IllegalStateException(description() + 
                        " First element of next iterator (" + pIt.peek() + ")" +
                        " must be after previous iterator (" + lastPeek + ")");
            }
            lastPeek = pIt.peek();
            iterators.add(pIt);
            adjustLast();
        }
    }

    private void adjustFirst() {
        // shift first iterator until peeked elements are sorted again
        int i = 0;
        while (i + 1 < iterators.size()) {
            if (comparator.compare(iterators.get(i).peek(), iterators.get(i + 1).peek()) > 0) {
                Collections.swap(iterators, i, i + 1);
                i++;
            } else {
                break;
            }
        }
    }

    private void adjustLast() {
        // shift last until sorted again
        int i = iterators.size() - 1;
        while (i - 1 >= 0) {
            if (comparator.compare(iterators.get(i - 1).peek(), iterators.get(i).peek()) > 0) {
                Collections.swap(iterators, i, i - 1);
                i--;
            } else {
                break;
            }
        }
    }
}
