# Source code for Lib.Graphes

    """ Python Classes for Oriented and Non Oriented Graphs
    """

    from graphviz import Digraph  # for dot output
    from typing import List, Dict, Set, Tuple, Any

    class GraphError(Exception):
        """Exception raised for self loops.
        """

        message: str

        def __init__(self, message: str):
            self.message = message

    class GeneralGraph(object):
        """
        General class regrouping similarities
        between directed and non oriented graphs.
        The only differences between the two are:

        - how to compute the set of edges
        - how to add an edge
        - how to print the graph
        - how to delete a vertex
        - how to delete an edge
        - we only color undirected graphs
        """

        graph_dict: Dict[Any, Set]

        def __init__(self, graph_dict=None):
            """
            Initializes a graph object.
            If no dictionary or None is given,
            an empty dictionary will be used.
            """
            if graph_dict is None:
                graph_dict = {}
            self.graph_dict = graph_dict

        def vertices(self) -> List[Any]:
            """Return the vertices of a graph."""
            return list(self.graph_dict.keys())

        def add_vertex(self, vertex: Any) -> None:
            """
            If the vertex "vertex" is not in
            self.graph_dict, a key "vertex" with an empty
            list as a value is added to the dictionary.
            Otherwise nothing has to be done.
            """
            if vertex not in self.graph_dict:
                self.graph_dict[vertex] = set()

        def edges(self) -> List[Set]:
            """Return the edges of the graph."""
            return []

        def __str__(self):
            res = "vertices: "
            for k in self.graph_dict:
                res += str(k) + " "
            res += "\nedges: "
            for edge in self.edges():
                res += str(edge) + " "
            return res

        def dfs_traversal(self, root: Any) -> List[Any]:
            """
            Compute a depth first search of the graph,
            from the vertex root.
            """
            seen: List[Any] = []
            todo: List[Any] = [root]
            while len(todo) > 0:  # while todo ...
                current = todo.pop()
                seen.append(current)
                for neighbour in self.graph_dict[current]:
                    if neighbour not in seen:
                        todo.append(neighbour)
            return seen

        def is_reachable_from(self, v1: Any, v2: Any) -> bool:
            """True if there is a path from v1 to v2."""
            return v2 in self.dfs_traversal(v1)

        def connected_components(self) -> List[List[Any]]:
            """
            Compute the list of all connected components of the graph,
            each component being a list of vetices.
            """
            components: List[List[Any]] = []
            done: List[Any] = []
            for v in self.vertices():
                if v not in done:
                    v_comp = self.dfs_traversal(v)
                    components.append(v_comp)
                    done.extend(v_comp)
            return components

        def bfs_traversal(self, root: Any) -> List[Any]:
            """
            Compute a breadth first search of the graph,
            from the vertex root.
            """
            seen: List[Any] = []
            todo: List[Any] = [root]
            while len(todo) > 0:  # while todo ...
                current = todo.pop(0)  # list.pop(0): for dequeuing (on the left...) !
                seen.append(current)
                for neighbour in self.graph_dict[current]:
                    if neighbour not in seen:
                        todo.append(neighbour)
            return seen

    class Graph(GeneralGraph):
        """Class for non oriented graphs."""

        def edges(self) -> List[Set]:
            """
            A static method generating the set of edges
            (they appear twice in the dictionnary).
            Return a list of sets.
            """
            edges = []
            for vertex in self.graph_dict:
                for neighbour in self.graph_dict[vertex]:
                    if {neighbour, vertex} not in edges:
                        edges.append({vertex, neighbour})
            return edges

        def add_edge(self, edge: Tuple[Any, Any]) -> None:
            """
            Add an edge in the graph.
            edge should be a pair and not (c,c)
            (we call g.add_edge((v1,v2)))
            """
            (vertex1, vertex2) = edge
            if vertex1 == vertex2:
                raise GraphError("Cannot add a self loop on vertex {} in an unoriented graph.".format(
                                 str(vertex1)))
            if vertex1 in self.graph_dict:
                self.graph_dict[vertex1].add(vertex2)
            else:
                self.graph_dict[vertex1] = {vertex2}
            if vertex2 in self.graph_dict:
                self.graph_dict[vertex2].add(vertex1)
            else:
                self.graph_dict[vertex2] = {vertex1}

        def print_dot(self, name: str, colors={}) -> None:
            """Print the graph."""
            color_names = ['red', 'blue', 'green', 'yellow', 'cyan', 'magenta'] + \
                [f"grey{i}" for i in range(0, 100, 10)]
            color_shapes = ['ellipse', 'box', 'diamond', 'trapezium', 'egg',
                            'parallelogram', 'house', 'triangle', 'pentagon', 'hexagon',
                            'septagon', 'octagon']
            dot = Digraph(comment='Conflict Graph')
            for k in self.graph_dict:
                shape = None
                if not colors:
                    color = "red"  # Graph not colored: red for everyone
                elif k not in colors:
                    color = "grey"  # Node not colored: grey
                else:
                    n = colors[k]
                    if n < len(color_names):
                        color = color_names[colors[k]]
                    else:
                        color = "black"  # Too many colors anyway, it won't be readable.
                    shape = color_shapes[n % len(color_shapes)]
                dot.node(str(k), color=color, shape=shape)
            for (v1, v2) in self.edges():
                dot.edge(str(v1), str(v2), dir="none")
            # print(dot.source)
            dot.render(name, view=True)  # print in pdf

        def delete_vertex(self, vertex: Any) -> None:
            """Delete a vertex and all the adjacent edges."""
            gdict = self.graph_dict
            for neighbour in gdict[vertex]:
                gdict[neighbour].remove(vertex)
            del gdict[vertex]

        def delete_edge(self, edge: Tuple[Any, Any]):
            """Delete an edge."""
            (v1, v2) = edge
            self.graph_dict[v1].remove(v2)
            self.graph_dict[v2].remove(v1)

        def color(self) -> Dict[Any, int]:
            """
            Color the graph with an unlimited number of colors.
            Return a dict vertex -> color, where color is an integer (0, 1, ...).
            """
            coloring, _, _ = self.color_with_k_colors()
            return coloring

        # see algo of the course
        def color_with_k_colors(self, K=None, avoidingnodes=()) -> Tuple[Dict[Any, int], bool, List]:
            """
            Color with <= K colors (if K is unspecified, use unlimited colors).

            Return 3 values:

            - a dict vertex -> color
            - a Boolean, True if the coloring succeeded
            - the set of nodes actually colored

            Do not color vertices belonging to avoidingnodes.

            Continue even if the algo fails.
            """
            if K is None:
                K = len(self.graph_dict)
            todo_vertices = []
            is_total = True
            gcopy = Graph(self.graph_dict.copy())
            # suppress nodes that are not to be considered.
            for node in avoidingnodes:
                gcopy.delete_vertex(node)
            # append nodes in the list according to their degree and node number:
            while gcopy.graph_dict:
                todo = list(gcopy.graph_dict)
                todo.sort(key=lambda v: (len(gcopy.graph_dict[v]), str(v)))
                lower = todo[0]
                todo_vertices.append(lower)
                gcopy.delete_vertex(lower)
            # Now reverse the list: first elements are those with higher degree
            # print(todo_vertices)
            todo_vertices.reverse()  # in place reversal
            # print(todo_vertices)
            coloring = {}
            colored_nodes = []
            # gdict will be the coloring map to return
            gdict = self.graph_dict
            for v in todo_vertices:
                seen_neighbours = [x for x in gdict[v] if x in coloring]
                choose_among = [i for i in range(K) if not (
                    i in [coloring[v1] for v1 in seen_neighbours])]
                if choose_among:
                    # if the node can be colored, I choose the minimal color.
                    color = min(choose_among)
                    coloring[v] = color
                    colored_nodes.append(v)
                else:
                    # if I cannot color some node, the coloring is not Total
                    # but I continue
                    is_total = False
            return (coloring, is_total, colored_nodes)

    class DiGraph(GeneralGraph):
        """Class for directed graphs."""

        def pred(self, v: Any) -> Set:
            """Return all predecessors of the vertex `v` in the graph."""
            return {src for src, dests in self.graph_dict.items() if v in dests}

        def neighbourhoods(self) -> List[Tuple[Any, Set]]:
            """Return all neighbourhoods in the graph."""
            return list(self.graph_dict.items())

        def edges(self) -> List[Tuple[Any, Any]]:
            """ A static method generating the set of edges"""
            edges = []
            for vertex in self.graph_dict:
                for neighbour in self.graph_dict[vertex]:
                    edges.append((vertex, neighbour))
            return edges

        def add_edge(self, edge: Tuple[Any, Any]) -> None:
            """
            Add an edge in the graph.
            edge should be a pair and not (c,c)
            (we call g.add_edge((v1,v2)))
            """
            (vertex1, vertex2) = edge
            if vertex1 in self.graph_dict:
                self.graph_dict[vertex1].add(vertex2)
            else:
                self.graph_dict[vertex1] = {vertex2}
            if vertex2 not in self.graph_dict:
                self.graph_dict[vertex2] = set()

        def print_dot(self, name: str) -> None:
            """Print the graph."""
            dot = Digraph(comment='Conflict Graph')
            for k in self.graph_dict:
                shape = None
                color = "grey"
                dot.node(str(k), color=color, shape=shape)
            for (v1, v2) in self.edges():
                dot.edge(str(v1), str(v2), dir="none")
            # print(dot.source)
            dot.render(name, view=True)  # print in pdf

        def delete_vertex(self, vertex: Any) -> None:
            """Delete a vertex and all the adjacent edges."""
            for node, neighbours in self.graph_dict.items():
                if vertex in neighbours:
                    neighbours.remove(vertex)
            del self.graph_dict[vertex]

        def delete_edge(self, edge: Tuple[Any, Any]) -> None:
            """Delete an edge."""
            (v1, v2) = edge
            self.graph_dict[v1].remove(v2)