class Graph:
    __slots__ = ['_graph_dict','_edges_info']
    def __init__(self, graph_dict=None, edges_info=None):
        if graph_dict is None:
            graph_dict = {}
        self._graph_dict = graph_dict

        if edges_info is None:
            edges_info = {}
        self._edges_info = edges_info

    def add_vertex(self, vertex):
        self._graph_dict.setdefault(vertex, (set(),set()) )
        return vertex

    def add_outgoing_neighbor(self,vertex,neighbor):
        self._graph_dict[vertex][0].add(neighbor)

    def add_incoming_neighbor(self,vertex,neighbor):
        self._graph_dict[vertex][1].add(neighbor)

    def outgoing_neighbors(self,vertex):
        yield from self._graph_dict[vertex][0]

    def incoming_neighbors(self,vertex):
        yield from self._graph_dict[vertex][1]

    def neighbors(self,vertex):
        #This method does not use directed_neighbors to avoid generating tuples with directions
        yield from self.outgoing_neighbors(vertex)
        yield from self.incoming_neighbors(vertex)

    def directed_neighbors(self,vertex):
        #This method does not use directed_neighbors to avoid generating tuples with directions
        for outgoing_neighbor in self.outgoing_neighbors(vertex):
            yield ('>',outgoing_neighbor)
        for incoming_neighbor in self.incoming_neighbors(vertex):
            yield ('<',incoming_neighbor)

    def add_edge(self, vertex1, vertex2,edge_info = None, edge_direction='>'):
        if edge_direction=='>':
            self.add_outgoing_neighbor(vertex1,vertex2)
            self.add_incoming_neighbor(vertex2,vertex1)
            if edge_info is not None:
                self._edges_info[(vertex1, vertex2)] = edge_info

        elif edge_direction=='<':
            self.add_edge(vertex2, vertex1,edge_info = edge_info)
        else:
            raise SyntaxError('edge_direction must be > or <')

    def get_edge_info(self,vertex1,vertex2):
        return self._edges_info.get( (vertex1, vertex2), None )

    def vertices(self):
        return self._graph_dict.keys()

    def edges(self):
            for vertex in self.vertices():
                for neighbor in self.outgoing_neighbors(vertex):
                    yield (vertex,)+ (neighbor,)

    def dfs_pair_iter(self, source_iter=None, root_predecessor=False, rev=False, **kwargs):
        last_vertex_by_level=[]

        if source_iter is None:
            source_iter = self.leveled_dfs_iter(rev=rev, **kwargs)

        for direction,level,vertex in source_iter:
            if level < len(last_vertex_by_level):
                last_vertex_by_level[level] = vertex
            else:
                last_vertex_by_level.append(vertex)

            if level>0:
                prev_vertex = last_vertex_by_level[level-1]
                yield (prev_vertex,direction,vertex)
            elif root_predecessor:
                yield (None,'',vertex)


    def leveled_dfs_iter(self,start_vertex=None,visited = None, level=0, direction='',two_way_transversal=True, rev=False):
        if len(self)>0:
            if start_vertex is None:
                start_vertex = self.get_starting_vertex(rev)
            if visited is None:
                visited = set()
            visited.add(start_vertex)

            yield( (direction,level,start_vertex) )

            for neighbor in self.outgoing_neighbors(start_vertex):
                if neighbor not in visited:
                    yield from self.leveled_dfs_iter(neighbor,visited,
                                                     level=level+1,
                                                     direction='>',
                                                     two_way_transversal=two_way_transversal,
                                                     rev=rev)

            # two_way_transversal indicates whether the DFS will expand through incoming neighbors
            if two_way_transversal:
                for neighbor in self.incoming_neighbors(start_vertex):
                    if neighbor not in visited:
                        yield from self.leveled_dfs_iter(neighbor,visited,
                                                         level=level+1,
                                                         direction='<',
                                                         two_way_transversal=two_way_transversal,
                                                         rev=rev)


    def leaves(self):
        for vertice in self.vertices():
            if sum(1 for neighbor in self.neighbors(vertice)) == 1:
                yield vertice

    def num_leaves(self):
        return len(list(self.leaves()))

    def get_starting_vertex(self, rev):
        vertices = iter(self.vertices())
        if rev:
            return vertices[-1]
        return next(vertices)

    def __repr__(self):
        return repr(self._graph_dict)

    def __len__(self):
         return len(self._graph_dict)

    def str_graph_dict(self):
        return str(self._graph_dict)

    def str_edges_info(self):
        return str(self._edges_info)
