# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""

Credit to Bernd Klein for this graph class.
http://www.python-course.eu/graphs_python.php

Modifications by kmisquitta:
1) Added support for pretty printing
2) Added function to return a vertex's neighbours
3) Added support for traversing to a vertex more than once in find_all_paths
4) Removed support for adding edges that are sets
5) Removed support for multiple edges between two vertices
6) Added support for traversing beyond the end vertex in find_all_paths
7) Removed many unneeded features

"""

""" A Python Class
A simple Python graph class, demonstrating the essential 
facts and functionalities of graphs.
"""

import pprint


def is_line_segment_in_path(path, vertex_1, vertex_2):
    for i in range(len(path) - 1):
        if path[i] == vertex_1 and path[i + 1] == vertex_2 \
                or path[i] == vertex_2 and path[i + 1] == vertex_1:
            return True
    return False


class Graph(object):

    def __init__(self, graph_dict={}):
        """ initializes a graph object """
        self.__graph_dict = graph_dict

    def get_vertices(self):
        """ returns the vertices of a graph """
        return list(self.__graph_dict.keys())

    def get_edges(self):
        """ returns the edges of a graph """
        return self.__generate_edges()

    def get_neighbours(self, vertex):
        """ returns the neighbours of a vertex """
        return list(self.__graph_dict[vertex])

    def add_vertex(self, vertex):
        """ 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] = []

    def add_edge(self, edge):
        """ assumes that edge is of type tuple or list """
        if len(edge) < 2:
            return

        vertex1 = edge[0]
        vertex2 = edge[1]
        if vertex1 in self.__graph_dict:
            if not (vertex2 in self.get_neighbours(vertex1)):
                self.__graph_dict[vertex1].append(vertex2)
        else:
            self.__graph_dict[vertex1] = [vertex2]

    def __generate_edges(self):
        """ A static method generating the edges of the 
            graph "graph". Edges are represented as sets 
            with one (a loop back to the vertex) or two 
            vertices 
        """
        edges = []
        for vertex in self.__graph_dict:
            for neighbour in self.__graph_dict[vertex]:
                if {vertex, neighbour} not in edges:
                    edges.append({vertex, neighbour})
        return edges

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

    def find_all_paths(self, start_vertex, end_vertex, path=[]):
        """ Recursive function that finds all paths from the start vertex to the end vertex.
            Starts from the start vertex and traverses through vertices until the end vertex is reached.
            If there are untraversed edges when the end vertex is reached, will continue traversing
            to check for paths back to the end vertex (loops).
            There is no limit to how many times a vertex can be traversed.
            An edge may be traversed only once.
        """
        graph = self.__graph_dict
        paths = []
        path = path + [start_vertex]
        if start_vertex == end_vertex:
            # Check if additional traversals is possible
            neighbours = self.get_neighbours(end_vertex)
            no_possible_traversals = True
            for neighbour in neighbours:
                if not is_line_segment_in_path(path, end_vertex, neighbour):
                    no_possible_traversals = False
                    break
            if no_possible_traversals:  # Base case
                return [path]
            else:
                paths.append(path)  # Add current path, continue finding
        if start_vertex not in graph:
            return []
        for vertex in graph[start_vertex]:
            if not is_line_segment_in_path(path, vertex, start_vertex):
                extended_paths = self.find_all_paths(vertex,
                                                     end_vertex,
                                                     path)
                for p in extended_paths:
                    paths.append(p)
        return paths

    def prettyprint(self):
        pprint.pprint(self.__graph_dict)
