package jas.common;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;

/**
 * Copied from FML TopologicalSort but with FML logging changed to JAS logging
 */
public class TopologicalSort {
    public static class DirectedGraph<T> implements Iterable<T> {
        private final Map<T, SortedSet<T>> graph = new HashMap<T, SortedSet<T>>();
        private List<T> orderedNodes = new ArrayList<T>();

        public boolean addNode(T node) {
            // Ignore nodes already added
            if (graph.containsKey(node)) {
                return false;
            }

            orderedNodes.add(node);
            graph.put(node, new TreeSet<T>(new Comparator<T>() {
                public int compare(T o1, T o2) {
                    return orderedNodes.indexOf(o1) - orderedNodes.indexOf(o2);
                }
            }));
            return true;
        }

        public void addEdge(T from, T to) {
            if (!(graph.containsKey(from) && graph.containsKey(to))) {
                throw new NoSuchElementException("Missing nodes from graph");
            }

            graph.get(from).add(to);
        }

        public void removeEdge(T from, T to) {
            if (!(graph.containsKey(from) && graph.containsKey(to))) {
                throw new NoSuchElementException("Missing nodes from graph");
            }

            graph.get(from).remove(to);
        }

        public boolean edgeExists(T from, T to) {
            if (!(graph.containsKey(from) && graph.containsKey(to))) {
                throw new NoSuchElementException("Missing nodes from graph");
            }

            return graph.get(from).contains(to);
        }

        public Set<T> edgesFrom(T from) {
            if (!graph.containsKey(from)) {
                throw new NoSuchElementException("Missing node from graph");
            }

            return Collections.unmodifiableSortedSet(graph.get(from));
        }

        @Override
        public Iterator<T> iterator() {
            return orderedNodes.iterator();
        }

        public int size() {
            return graph.size();
        }

        public boolean isEmpty() {
            return graph.isEmpty();
        }

        @Override
        public String toString() {
            return graph.toString();
        }
    }

    /**
     * Sort the input graph into a topologically sorted list
     * 
     * Uses the reverse depth first search as outlined in ...
     * 
     * @param graph
     * @return The sorted mods list.
     */
    public static <T> List<T> topologicalSort(DirectedGraph<T> graph) {
        DirectedGraph<T> rGraph = reverse(graph);
        List<T> sortedResult = new ArrayList<T>();
        Set<T> visitedNodes = new HashSet<T>();
        // A list of "fully explored" nodes. Leftovers in here indicate cycles in the graph
        Set<T> expandedNodes = new HashSet<T>();

        for (T node : rGraph) {
            explore(node, rGraph, sortedResult, visitedNodes, expandedNodes);
        }

        return sortedResult;
    }

    public static <T> DirectedGraph<T> reverse(DirectedGraph<T> graph) {
        DirectedGraph<T> result = new DirectedGraph<T>();

        for (T node : graph) {
            result.addNode(node);
        }

        for (T from : graph) {
            for (T to : graph.edgesFrom(from)) {
                result.addEdge(to, from);
            }
        }

        return result;
    }

    public static <T> void explore(T node, DirectedGraph<T> graph, List<T> sortedResult, Set<T> visitedNodes,
            Set<T> expandedNodes) {
        // Have we been here before?
        if (visitedNodes.contains(node)) {
            // And have completed this node before
            if (expandedNodes.contains(node)) {
                // Then we're fine
                return;
            }

            JASLog.log().severe("Topological Sorting failed.");
            JASLog.log().severe("Visting node %s", node);
            JASLog.log().severe("Current sorted list : %s", sortedResult);
            JASLog.log().severe("Visited set for this node : %s", visitedNodes);
            JASLog.log().severe("Explored node set : %s", expandedNodes);
            SetView<T> cycleList = Sets.difference(visitedNodes, expandedNodes);
            JASLog.log().severe("Likely cycle is in : %s", cycleList);
            throw new TopologicalSortingException(
                    "There was a cycle detected in the input graph, sorting is not possible", node, cycleList);
        }

        // Visit this node
        visitedNodes.add(node);

        // Recursively explore inbound edges
        for (T inbound : graph.edgesFrom(node)) {
            explore(inbound, graph, sortedResult, visitedNodes, expandedNodes);
        }

        // Add ourselves now
        sortedResult.add(node);
        // And mark ourselves as explored
        expandedNodes.add(node);
    }
}
