# https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm
from collections import deque, namedtuple

# 1. mark all nodes unvisited
# 2. set initial node distance to zero, other nodes to infinite
# 3. select unvisited node with smallest distance
# 4. find unvisited neighbors (to selected node), compare distances, and save smallest one
# 5. mark current node visited
# 6. stop if destination is visited, otherwise repeat step 3-6

inf = float('inf')
edge = namedtuple('Edge', 'start, end, cost')

# make_edge
def make_edge(start, end, cost=1):
    return edge(start, end, cost)

class Graph:
    def __init__(self, edges):
        check_edges = [i for i in edges if len(i) not in [2, 3]]
        if check_edges:
            raise ValueError('Error: check_edges, {}'.format(check_edges))

        self.edges = [make_edge(*edge) for edge in edges]

    # vertices
    @property
    def vertices(self):
        return set(
            # turn ([1,2], [3,4]) into [1, 2, 3, 4]
            sum(([edge.start, edge.end] for edge in self.edges), [])
        )

    # get_node_pairs
    def get_node_pairs(self, node_1, node_2, both_ends=True):
        if both_ends:
            node_pairs = [[node_1, node_2], [node_2, node_1]]
        else:
            node_pairs = [[node_1, node_2]]

        return node_pairs

    # remove_edge
    def remove_edge(self, node_1, node_2, both_ends=True):
        node_pairs = self.get_node_pairs(node_1, node_2, both_ends)
        edges = self.edges[:]

        for edge in edges:
            if [edge.start, edge.end] in node_pairs:
                self.edges.remove(edge)

    # add_edge
    def add_edge(self, node_1, node_2, cost=1, both_ends=True):
        node_pairs = self.get_node_pairs(node_1, node_2, both_ends)
        
        for edge in self.edges:
            if [edge.start, edge.end] in node_pairs:
                return ValueError('Error: add_edge, {} {}'.format(node_1, node_2))

        self.edges.append(edge(start=node_1, end=node_2, cost=cost))
        if both_ends:
            self.edges.append(edge(start=node_2, end=node_1, cost=cost))

    # neighbors
    @property
    def neighbors(self):
        neighbors = { vertex: set() for vertex in self.vertices }

        for edge in self.edges:
            neighbors[edge.start].add((edge.end, edge.cost))

        return neighbors

    # dijkstra
    def dijkstra(self, source, destination):
        assert source in self.vertices, 'assert: source do not exist'

        # 1. mark all nodes unvisited
        # 2. set initial node distance to zero, other nodes to infinite
        distances = { vertex: inf for vertex in self.vertices }
        previous_vertices = { vertex: None for vertex in self.vertices }
        distances[source] = 0
        vertices = self.vertices.copy()

        while vertices:
            # 3. select unvisited node with smallest distance
            current_vertex = min(vertices, key = lambda vertex: distances[vertex])

            # 6. stop if destination is visited, otherwise repeat step 3-6
            if distances[current_vertex] == inf:
                break

            # 4. find unvisited neighbors (to selected node), compare distances, and save smallest one
            for neighbor, cost in self.neighbors[current_vertex]:
                alternative_route = distances[current_vertex] + cost

                if (alternative_route < distances[neighbor]):
                    distances[neighbor] = alternative_route
                    previous_vertices[neighbor] = current_vertex

            # 5. mark current node visited
            vertices.remove(current_vertex)

        path, current_vertex = deque(), destination

        while previous_vertices[current_vertex] is not None:
            path.appendleft(current_vertex)
            current_vertex = previous_vertices[current_vertex]

        if path:
            path.appendleft(current_vertex)

        return path

# create example graph
graph = Graph([
    ('a', 'b', 7),
    ('a', 'c', 9),
    ('a', 'f', 14),
    ('b', 'c', 10),
    ('b', 'd', 15),
    ('c', 'd', 11),
    ('c', 'f', 2),
    ('d', 'e', 6),
    ('e', 'f', 9)
])

print(graph.dijkstra('a', 'f'))
# deque(['a', 'c', 'f'])