# Source code for pynauty.graph

    '''
        graph.py

    Module graph contains the definition of the Graph class
    and utilities dealing with graph objects.
    '''

    __LICENSE__ = '''
    Copyright (c) 2015-2022 Peter Dobsan

    This program is free software; you can redistribute it and/or modify it
    under the terms of the GNU General Public License as published by the
    Free Software Foundation; either version 3 of the License, or (at your
    option) any later version.  This program is distributed in the hope that
    it will be useful, but WITHOUT ANY WARRANTY; without even the implied
    warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    GNU General Public License for more details.
    '''

    __all__ = [
        'Graph',
        'autgrp',
        'isomorphic',
        'certificate',
        'canon_label',
        'delete_random_edge',
    ]

    from . import nautywrap
    import copy
    import random

    class Graph(object):
        '''
        Graph instantiates an adjacency dictionary based graph object.
        It can represent vertex colored, directed or undirected graphs.
        '''

        def __init__(self, number_of_vertices, directed=False,
                     adjacency_dict={},
                     vertex_coloring=[]):
            '''
            *number_of_vertices*
                The number of vertices of the graph; the vertices are
                labeled from zero.  Mandatory argument.

            *directed*
                Indicate wether the grap is directed or not.  Optional,
                default is False.

            *adjacency_dict*
                key: a vertex, value: a list of vertices linked to the
                key vertex.  Optional, default is an empty dictionary.

            *vertex_coloring*
                A list of disjoint sets of vertices representing a
                partition of the vertex set; vertices not listed are
                placed into a single additional part.  Optional, default
                is no coloring.
            '''
            self.number_of_vertices = number_of_vertices
            self.directed = directed
            self.set_adjacency_dict(adjacency_dict)
            self.set_vertex_coloring(vertex_coloring)

        def _check_vertices(self, vs):
            for v in vs:
                if not (0 <= v and v < self.number_of_vertices):
                    raise ValueError(
                        'vertex %d conflicts with number_of_vertices=%d' %
                        (v, self.number_of_vertices))

        def _get_adjacency_dict(self):
            return self._adjacency_dict

        adjacency_dict = property(_get_adjacency_dict)

        def set_adjacency_dict(self, adjacency_dict):
            '''
            Set the adjacency relations of the Graph.

            *adjacency_dict*
                key: a vertex, value: a list of vertices linked to the
                key vertex. Duplicate vertices will be removed, since there is no
                multigraph support.
            '''
            for v, vs in adjacency_dict.items():
                self._check_vertices([v])
                self._check_vertices(vs)
            self._adjacency_dict = dict([(k,list(set(vs)))
                                         for k,vs in adjacency_dict.items()])

        def connect_vertex(self, v, neighbors):
            '''
            Connect a vertex to some other vertices.

            *v*
                A vertex of the Graph. The *tail* of the arcs if the Graph
                is directed.
            *neighbors*
                A vertex or a list of vertices to which *v* should be connected.
                The *heads* of the arcs if the Graph is directed. Duplciate
                vertices are removed, since there is no multigraph support.

            '''
            self._check_vertices([v])
            self._adjacency_dict.setdefault(v, [])
            if isinstance(neighbors, list):
                self._check_vertices(neighbors)
                self._adjacency_dict[v].extend(neighbors)
            else:
                self._check_vertices([neighbors])
                self._adjacency_dict[v].append(neighbors)
            self._adjacency_dict[v] = list(set(self._adjacency_dict[v]))

        def _get_vertex_coloring(self):
            return self._vertex_coloring

        vertex_coloring = property(_get_vertex_coloring)

        def set_vertex_coloring(self, vertex_coloring):
            '''
            Define a vertex coloring of the Graph.

            *vertex_coloring*
                A list of disjoint sets of vertices representing a
                partition of the vertex set; vertices not listed are
                placed into a single additional part.
            '''
            self._vertex_coloring = []
            if vertex_coloring:
                vs = set(range(self.number_of_vertices))
                for p in vertex_coloring:
                    if p <= vs:
                        self._vertex_coloring.append(p)
                        vs -= p
                    else:
                        raise ValueError('Invalid partition: %s' % vertex_coloring)
                if vs:
                    self._vertex_coloring.append(vs)
                if len(self._vertex_coloring) == 1:
                    self._vertex_coloring = []

        def copy(self):
            '''
            Make a copy of the Graph.
            '''
            return copy.deepcopy(self)

        def __repr__(self):
            s = ['Graph(number_of_vertices=%d, directed=%s,' %
                 (self.number_of_vertices, self.directed)]
            s.append(' adjacency_dict = {')
            for k, v in self._adjacency_dict.items():
                v.sort()
                s.append('  %d: %s,' % (k, v))
            s.append(' },')
            s.append(' vertex_coloring = [')
            for x in self._vertex_coloring:
                s.append('  set(%s),' % list(x))
            s.append(' ],')
            s.append(')')
            return '\n'.join(s)

    def autgrp(g):
        '''
        Compute the automorphism group of a graph.

        *g*
            A Graph object.

        return -> (generators, grpsize1, grpsize2, orbits, numorbits)
            For the detailed description of the returned components, see
            Nauty's documentation.
        '''
        if not isinstance(g, Graph):
            raise TypeError
        return nautywrap.graph_autgrp(g)

    def certificate(g):
        '''
        Compute a certificate based on the canonical labeling of vertices.

        *g*
            A Graph object.

        return ->
            The certificate as a byte string.
        '''
        if not isinstance(g, Graph):
            raise TypeError
        return nautywrap.graph_cert(g)

    def canon_label(g):
        '''
        Finds the canonical labeling of vertices.

        *g*
            A Graph object.

        return ->
            A list with each node relabelled.
        '''
        if not isinstance(g, Graph):
            raise TypeError
        return nautywrap.graph_canonlab(g)

    def isomorphic(a, b):
        '''
        Determine if two graphs are isomorphic.

        *a,b*
            Two Graph objects.

        return ->
            True if *a* and *b* are isomorphic graphs, False otherwise,
        '''
        if a.number_of_vertices != b.number_of_vertices:
            return False
        elif list(map(len, a.vertex_coloring)) != list(map(len, b.vertex_coloring)):
            return False
        else:
            return certificate(a) == certificate(b)

    def delete_random_edge(g):
        '''
        Delete a random edge from a graph.

        *g*
            A Graph object.

        return ->
            The deleted edge as a tuple or (None, None) if no edge is left.
        '''
        if g.adjacency_dict:
            # pick a random vertex 'x' which is connected
            x = random.sample(list(g.adjacency_dict), 1)[0]
            # remove a random edge connected to 'x'
            xs = g.adjacency_dict[x]
            y = xs.pop(random.randrange(len(xs)))
            if not xs:
                g.adjacency_dict.pop(x)
            # if g is not directed make sure to remove edge completely
            if (not g.directed) and y in g.adjacency_dict:
                ys = g.adjacency_dict[y]
                if x in ys:
                    ys.remove(x)
        else:
            # the graph has no edges
            x, y = None, None
        return (x, y)