#! /usr/bin/env python
# -*- coding: utf-8 -*-

"""
`Topological sort by Kahn (1962) on Wikipedia
<http://en.wikipedia.org/wiki/Topological_sorting>`_
L ← Empty list that will contain the sorted elements
S ← Set of all nodes with no incoming edges
while S is non-empty do
    remove a node n from S
    insert n into L
    for each node m with an edge e from n to m do
        remove edge e from the graph
        if m has no other incoming edges then
            insert m into S
if graph has edges then
    return error (graph has at least one cycle)
else 
    return L (a topologically sorted order)
"""

class CircularDependencyError(Exception):
    def __init__(self, keys):
        self.args = keys
        self.message = self.__str__
    def __str__(self):
        return 'Not a DAG. These keys are cyclic:\n\t%s' % str(self.args)

def topological_sort(DAG):
    """
    Kahn ('62) topological sort.

    :param DAG: directed acyclic graph
    :type DAG: dict
    """
    L = []
    S = [k for k, v in DAG.iteritems() if not v]
    while S:
        n = S.pop(0)
        L.append(n)
        for m in (k for k, v in DAG.iteritems() if n in v):
            # idx = DAG[m].index(n); DAG[m].pop(idx)  # Python < 2.7 alternative
            DAG[m] = list(set(DAG[m]).difference([n]))
            if not DAG[m]:
                S.append(m)
    if any([bool(v) for v in DAG.itervalues()]):
        raise CircularDependencyError([k for k, v in DAG.iteritems() if v])
    return L

if __name__ == "__main__":
    # test DAG
    DAG = {'7': [], '5': [], '3': [], '11': ['7', '5'], '8': ['7', '3'],
           '2': ['11'], '9': ['11', '8'], '10': ['11', '3']}
    print topological_sort(DAG.copy())
    
    # test 10->9->8 circular dependency
    notDAG = {'7': [], '5': [], '3': [], '11': ['7', '5'], '8': ['7', '3', '10'],
              '2': ['11'], '9': ['11', '8'], '10': ['11', '3', '9']}
    print topological_sort(notDAG.copy())