package org.unicode.cldr.util;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class MergeLists<T> {

    Collection<Collection<T>> source = new ArrayList<Collection<T>>();
    Set<T> orderedWorkingSet;

    public MergeLists() {
        this(new LinkedHashSet<T>());
    }

    public MergeLists(Set<T> orderedWorkingSet) {
        this.orderedWorkingSet = orderedWorkingSet;
    }

    public MergeLists<T> add(Collection<T> orderedItems) {
        if (orderedItems.size() == 0) { // skip empties
            return this;
        }
        final LinkedHashSet<T> linkedHashSet = new LinkedHashSet<T>(orderedItems);
        if (linkedHashSet.size() != orderedItems.size()) {
            throw new IllegalArgumentException("Multiple items in ordering!");
        }
        source.add(linkedHashSet);
        return this;
    }

    @SuppressWarnings("unchecked")
    public MergeLists<T> add(T... stuff) {
        return add(Arrays.asList(stuff));
    }

    public MergeLists<T> addAll(Collection<Collection<T>> collectionsOfOrderedItems) {
        for (Collection<T> orderedItems : collectionsOfOrderedItems) {
            add(orderedItems);
        }
        return this;
    }

    public List<T> merge() {
        List<T> result = new ArrayList<T>();

        for (Collection<T> sublist : source) {
            orderedWorkingSet.addAll(sublist);
        }

        // now that we have things ordered, we take the first one that is only at the front of a list
        // this is slower, but puts things into as much of the order specified as possible
        // could be optimized further, but we don't care that much

        Set<T> first = new LinkedHashSet<T>();
        while (orderedWorkingSet.size() != 0) {
            getFirsts(first);
            if (first.size() == 0) {
                Map<T, Collection<T>> reasons = new LinkedHashMap<T, Collection<T>>();
                getFirsts(first, reasons);
                throw new IllegalArgumentException(
                    "Inconsistent requested ordering: cannot merge if we have [...A...B...] and [...B...A...]: "
                        + reasons);
            }
            // now get first item that is in first
            T best = extractFirstOk(orderedWorkingSet, first); // removes from working set
            // remaining items now contains no non-first items
            removeFromSource(best);
            result.add(best);
        }
        return result;
    }

    public static <T> boolean hasConsistentOrder(Collection<T> a, Collection<T> b) {
        LinkedHashSet<T> remainder = new LinkedHashSet<T>(a);
        remainder.retainAll(b);
        if (remainder.size() == 0) {
            return true;
        }
        // remainder is now in a's order, and contains only the items that are in both
        Iterator<T> bi = b.iterator();
        T current = bi.next();
        for (T item : remainder) {
            if (item.equals(current)) {
                if (!bi.hasNext()) {
                    return true;
                }
                current = bi.next();
            }
        }
        return !bi.hasNext(); // if we have any left over, we failed
    }

    public static <T> Collection<T> hasConsistentOrderWithEachOf(Collection<T> a, Collection<Collection<T>> bs) {
        for (Collection<T> b : bs) {
            if (!hasConsistentOrder(a, b)) {
                return b;
            }
        }
        return null;
    }

    // could be optimized since we know the item will only occur at the head of a list
    private void removeFromSource(T item) {
        for (Iterator<Collection<T>> iterator = source.iterator(); iterator.hasNext();) {
            Collection<T> sublist = iterator.next();
            sublist.remove(item);
            if (sublist.size() == 0) {
                iterator.remove();
            }
        }
    }

    /**
     * Get the first item that is also in the ok set.
     */
    private T extractFirstOk(Collection<T> remainingItems, Set<T> ok) {
        for (Iterator<T> it = remainingItems.iterator(); it.hasNext();) {
            T item = it.next();
            if (ok.contains(item)) {
                it.remove();
                return item;
            }
        }
        throw new IllegalArgumentException("Internal Error");
    }

    public void getFirsts(Set<T> result) {
        getFirsts(result, null);
    }

    /**
     * Get first of each sets. Guaranteed non-empty
     */
    public void getFirsts(Set<T> result, Map<T, Collection<T>> reasons) {
        result.clear();
        result.addAll(orderedWorkingSet);
        for (Collection<T> sublist : source) {
            // get all the first items
            final Iterator<T> iterator = sublist.iterator();
            iterator.next(); // skip first
            while (iterator.hasNext()) {
                final T nextItem = iterator.next();
                boolean changed = result.remove(nextItem);
                if (changed && reasons != null) {
                    reasons.put(nextItem, sublist);
                }
            }
        }
    }
}
