package org.ggp.base.util.gdl.model;

import java.util.Collection;
import java.util.Deque;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;

import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Queues;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;

/**
 * When dealing with GDL, dependency graphs are often useful. DependencyGraphs
 * offers a variety of functionality for dealing with dependency graphs expressed
 * in the form of SetMultimaps.
 *
 * These multimaps are paired with sets of all nodes, to account for the
 * possibility of nodes not included in the multimap representation.
 *
 * All methods assume that keys in multimaps depend on their associated values,
 * or in other words are downstream of or are children of those values.
 */
public class DependencyGraphs {
    private DependencyGraphs() {}

    /**
     * Returns all elements of the dependency graph that match the
     * given predicate, and any elements upstream of those matching
     * elements.
     *
     * The graph may contain cycles.
     *
     * Each key in the dependency graph depends on/is downstream of
     * its associated values.
     */
    public static <T> ImmutableSet<T> getMatchingAndUpstream(
            Set<T> allNodes,
            SetMultimap<T, T> dependencyGraph,
            Predicate<T> matcher) {
        Set<T> results = Sets.newHashSet();

        Deque<T> toTry = Queues.newArrayDeque();
        toTry.addAll(Collections2.filter(allNodes, matcher));

        while (!toTry.isEmpty()) {
            T curElem = toTry.remove();
            if (!results.contains(curElem)) {
                results.add(curElem);
                toTry.addAll(dependencyGraph.get(curElem));
            }
        }
        return ImmutableSet.copyOf(results);
    }

    /**
     * Returns all elements of the dependency graph that match the
     * given predicate, and any elements downstream of those matching
     * elements.
     *
     * The graph may contain cycles.
     *
     * Each key in the dependency graph depends on/is downstream of
     * its associated values.
     */
    public static <T> ImmutableSet<T> getMatchingAndDownstream(
            Set<T> allNodes,
            SetMultimap<T, T> dependencyGraph,
            Predicate<T> matcher) {
        return getMatchingAndUpstream(allNodes, reverseGraph(dependencyGraph), matcher);
    }

    public static <T> SetMultimap<T, T> reverseGraph(SetMultimap<T, T> graph) {
        return Multimaps.invertFrom(graph, HashMultimap.<T, T>create());
    }

    /**
     * Given a dependency graph, return a topologically sorted
     * ordering of its components, stratified in a way that allows
     * for recursion and cycles. (Each set in the list is one
     * unordered "stratum" of elements. Elements may depend on elements
     * in earlier strata or the same stratum, but not in later
     * strata.)
     *
     * If there are no cycles, the result will be a list of singleton
     * sets, topologically sorted.
     *
     * Each key in the given dependency graph depends on/is downstream of
     * its associated values.
     */
    public static <T> List<Set<T>> toposortSafe(
            Set<T> allElements,
            Multimap<T, T> dependencyGraph) {
        Set<Set<T>> strataToAdd = createAllStrata(allElements);
        SetMultimap<Set<T>, Set<T>> strataDependencyGraph = createStrataDependencyGraph(dependencyGraph);
        List<Set<T>> ordering = Lists.newArrayList();

        while (!strataToAdd.isEmpty()) {
            Set<T> curStratum = strataToAdd.iterator().next();
            addOrMergeStratumAndAncestors(curStratum, ordering,
                    strataToAdd, strataDependencyGraph,
                    Lists.<Set<T>>newArrayList());
        }
        return ordering;
    }

    private static <T> void addOrMergeStratumAndAncestors(Set<T> curStratum,
            List<Set<T>> ordering, Set<Set<T>> toAdd,
            SetMultimap<Set<T>, Set<T>> strataDependencyGraph,
            List<Set<T>> downstreamStrata) {
        if (downstreamStrata.contains(curStratum)) {
            int mergeStartIndex = downstreamStrata.indexOf(curStratum);
            List<Set<T>> toMerge = downstreamStrata.subList(mergeStartIndex, downstreamStrata.size());
            mergeStrata(Sets.newHashSet(toMerge), toAdd, strataDependencyGraph);
            return;
        }
        downstreamStrata.add(curStratum);
        for (Set<T> parent : ImmutableList.copyOf(strataDependencyGraph.get(curStratum))) {
            //We could merge away the parent here, so we protect against CMEs and
            //make sure the parent is still in toAdd before recursing.
            if (toAdd.contains(parent)) {
                addOrMergeStratumAndAncestors(parent, ordering, toAdd, strataDependencyGraph, downstreamStrata);
            }
        }
        downstreamStrata.remove(curStratum);
        // - If we've added all our parents, we will still be in toAdd
        //   and none of our dependencies will be in toAdd. Add to the ordering.
        // - If there was a merge upstream that we weren't involved in,
        //   we will still be in toAdd, but we will have (possibly new)
        //   dependencies that are still in toAdd. Do nothing.
        // - If there was a merge upstream that we were involved in,
        //   we won't be in toAdd anymore. Do nothing.
        if (!toAdd.contains(curStratum)) {
            return;
        }
        for (Set<T> parent : strataDependencyGraph.get(curStratum)) {
            if (toAdd.contains(parent)) {
                return;
            }
        }
        ordering.add(curStratum);
        toAdd.remove(curStratum);
    }

    //Replace the old strata with the new stratum in toAdd and strataDependencyGraph.
    private static <T> void mergeStrata(Set<Set<T>> toMerge,
            Set<Set<T>> toAdd,
            SetMultimap<Set<T>, Set<T>> strataDependencyGraph) {
        Set<T> newStratum = ImmutableSet.copyOf(Iterables.concat(toMerge));
        for (Set<T> oldStratum : toMerge) {
            toAdd.remove(oldStratum);
        }
        toAdd.add(newStratum);
        //Change the keys
        for (Set<T> oldStratum : toMerge) {
            Collection<Set<T>> parents = strataDependencyGraph.get(oldStratum);
            strataDependencyGraph.putAll(newStratum, parents);
            strataDependencyGraph.removeAll(oldStratum);
        }
        //Change the values
        for (Entry<Set<T>, Set<T>> entry : ImmutableList.copyOf(strataDependencyGraph.entries())) {
            if (toMerge.contains(entry.getValue())) {
                strataDependencyGraph.remove(entry.getKey(), entry.getValue());
                strataDependencyGraph.put(entry.getKey(), newStratum);
            }
        }
    }

    private static <T> Set<Set<T>> createAllStrata(Set<T> allElements) {
        Set<Set<T>> result = Sets.newHashSet();
        for (T element : allElements) {
            result.add(ImmutableSet.of(element));
        }
        return result;
    }

    private static <T> SetMultimap<Set<T>, Set<T>> createStrataDependencyGraph(
            Multimap<T, T> dependencyGraph) {
        SetMultimap<Set<T>, Set<T>> strataDependencyGraph = HashMultimap.create();
        for (Entry<T, T> entry : dependencyGraph.entries()) {
            strataDependencyGraph.put(ImmutableSet.of(entry.getKey()), ImmutableSet.of(entry.getValue()));
        }
        return strataDependencyGraph;
    }
}
