#!/usr/bin/env python

class Graph:
    def __init__(self, N):
        self.N = N                     # number of vertices
        
        # maps a vertex number to the list of (vertices, edge value)
        # to which it is connected by edges.
        self.adjacent = [[] for n in xrange(N)]
    
    def connect(self, vertex1, vertex2, edge_value):
        """
        Connect 'vertex1' and 'vertex2' with an edge, with associated
        value 'value'
        """
        # Add vertices to each other's adjacent list
        self.adjacent[vertex1].append( (vertex2, edge_value) )
        self.adjacent[vertex2].append( (vertex1, edge_value) )

    def check(self):
        """
        See if vertex values add up to edge values (mod N).
        """
        for vertex in xrange(self.N):
            for neighbor, edge_value in self.adjacent[vertex]:
                assert (self.vertex_values[vertex] +
                        self.vertex_values[neighbor]) % self.N == edge_value
    
    def calc_tree_sizes(self):
        """
        After running this method, the attribute size will contain a list,
        which maps the vertices to the size of the tree that vertex belongs
        to.
        """
        visited = self.N * [-1]  # -1 unvisited, otherwise the number of tree
        treenum = 0
        
        # Loop over all vertices, taking unvisited ones as roots.
        for root in xrange(self.N):
            if visited[root] >= 0:
                continue
            
            # explore tree starting at 'root'
            # Stack of vertices to visit, a list of tuples (parent, vertex)
            tovisit = [ (None, root) ]
            while tovisit:
                parent, vertex = tovisit.pop()
                visited[vertex] = treenum
                
                # Loop over adjacent vertices, but skip the vertex we arrived
                # here from the first time it is encountered.
                skip = True
                for neighbor, edge_value in self.adjacent[vertex]:
                    if skip and neighbor == parent:
                        skip = False
                        continue
                    
                    if visited[neighbor] >= 0:
                        # We visited here before, so the graph is cyclic.
                        exit('Hmm, graph is cyclic.')
                    
                    tovisit.append( (vertex, neighbor) )
                    
            treenum += 1
        
        # maps the tree number to number of vertices within that tree
        treesizes = treenum * [0]
        for tree in visited:
            treesizes[tree] += 1
        
        self.size = [treesizes[visited[v]] for v in xrange(self.N)]
        
        if verbose:
            freq = (self.N+1) * [0]
            for size in treesizes:
                freq[size] += 1
        
            sys.stderr.write(' Size   Trees\n')
            for i, f in enumerate(freq):
                if f:
                    sys.stderr.write('%5i %5i\n' % (i, f))
                if i == minsize-1:
                    sys.stderr.write('--------------\n')
        
    def write(self, fo, labels = False):
        self.calc_tree_sizes()
        
        fo.write('graph G {\n'
                 '  size = "8,8";\n'
                 '  edge [color="#ff0000"]\n')
        if labels:
            fo.write('  node [color="#a0e0ee", style=filled];\n')
            
            for vertex, value in enumerate(self.vertex_values):
                if self.size[vertex] < minsize: continue
                fo.write('  { node [label="%i: %i"] v%i }\n' % (
                    vertex, value, vertex))
        else:
            fo.write('  node [color="#3377a0", label="",\n'
                     '        style=filled, shape=circle]\n')
            
        for vertex in xrange(self.N):          # edges
            if self.size[vertex] < minsize: continue
            for neighbor, edge_value in self.adjacent[vertex]:
                if neighbor > vertex:  continue
                fo.write('  v%i -- v%i%s;\n' %
                         (vertex, neighbor,
                          (' [label="%s: %i"]' % (K[edge_value], edge_value))
                          if labels else ''))
        fo.write('}\n')
        fo.close()


if __name__ == '__main__':
    import sys
    from optparse import OptionParser
    
    usage = "usage: %prog [options] [PYCODE]"
    
    description = """\
Given the python code for a perfect hash function which was generated by
perfect_hash.py, e.g. by '$ ../perfect_hash.py animals.txt >animals.py',
this program will create the graph which was used in determining the
perfect hash function.  The input python code may also be given to stdin.
The output is saved as in the .dot format which is used by the Graphviz
tools (see http://www.graphviz.org/) to generate a picture of the graph.
"""

    parser = OptionParser(usage = usage,
                          description = description,
                          prog = sys.argv[0])

    parser.add_option("-l", "--labels",
                      action  = "store_true",
                      help    = "Be verbose")

    parser.add_option("-m", "--minsize",
                      action  = "store",
                      default = 1,
                      type    = "int",
                      help    = "Include only trees in the output which "
                                "have at least INT vertices. "
                                "Default is %default, i.e. all trees are "
                                "included within the output.",
                      metavar = "INT")

    parser.add_option("-o", "--output",
                      action  = "store",
                      help    = "Specify output FILE explicitly. "
                                "Default, is stdout. ",
                      metavar = "FILE")

    parser.add_option("-v", "--verbose",
                      action  = "store_true",
                      help    = "Be verbose")
    
    options, args = parser.parse_args()
    
    if options.minsize > 0:
        minsize = options.minsize
    else:
        parser.error("minimal size of trees has to be larger than zero")
    
    verbose = options.verbose
    
    if len(args) > 1:
        parser.error("incorrect number of arguments")

    # --------------------- end parsing and checking -----------------------
    
    if verbose:
        sys.stderr.write('minsize (of trees): %i\n' % minsize)
        sys.stderr.write('labels (in output): %s\n' % options.labels)

    # ------------ input filehandle
    
    if len(args)==1:
        try:
            fi = file(args[0])
        except IOError :
            exit("Error: Can't open `%s' for reading." % args[0])
    else:
        fi = sys.stdin

    # ------------ read input, i.e. execute code
    
    exec(fi.read())
    
    # ------------ make graph
    
    g = Graph(len(G))
    g.vertex_values = G
    for key, hashval in zip(K, H):
        g.connect(hash_f(key, S1),
                  hash_f(key, S2),
                  hashval)
    g.check()

    # ------------ output filehandle
    
    if options.output:
        try:
            fo = file(options.output, 'w')
        except IOError :
            exit("Error: Can't open `%s' for writing." % options.output)
    else:
        fo = sys.stdout

    # ------------ write output, i.e. generate .dot output

    g.write(fo, options.labels)


# Local Variables:
# mode: python
# End:
