############################################################
# graph and display helpers
############################################################


def neighbors(graph, node):
    return graph.get(node, [])


def path_to_string(path):
    """path is a list of nodes"""
    if path is None:
        return "None"
    node_strs = []
    for node in path:
        node_strs.append(str(node))
    return "-->".join(node_strs)


def pathlist_to_string(queue):
    """path is a list of nodes"""
    path_strs = []
    for path in queue:
        path_strs.append(path_to_string(path))
    return "[" + ", ".join(path_strs) + "]"


############################################################
# example graphs
############################################################


test_graph = {
    "A": [("B", 2), ("C", 2)],
    "B": [("D", 3)],
    "C": [("E", 3)],
    "D": [("F", 2), ("G", 4), ("H", 2)],
    "E": [("B", 1), ("H", 1), ("J", 1)],
    "H": [("G", 1)],
    "J": [("G", 4)],
}
flights = {
    "Boston": [("Providence", 1), ("New York", 2)],
    "Providence": [("Boston", 1), ("New York", 2)],
    "New York": [("Chicago", 3)],
    "Chicago": [("Phoenix", 5), ("Denver", 3)],
    "Denver": [("New York", 4), ("Phoenix", 1)],
}



############################################################
# Dijkstra's: storing paths
############################################################


def remove_min(queue):
    best_idx = 0
    min_so_far = queue[0]
    for idx in range(1, len(queue)):
        item = queue[idx]
        if item < min_so_far:
            best_idx = idx
            min_so_far = item
    return queue.pop(best_idx)


def remove_min(queue):
    queue.sort()
    return queue.pop(0)


def remove_min(queue):
    best = min(queue)
    idx = queue.index(best)
    return queue.pop(idx)


def find_node(queue, node):
    for idx in range(len(queue)):
        cost, path = queue[idx]
        if path[-1] == node:
            return idx
    return None


def update_node(queue, path, cost):
    node = path[-1]
    idx = find_node(queue, node)

    if idx is None:
        print(f"    Adding path to {node!r}")
        queue.append((cost, path))

    else:
        old_cost, old_path = queue[idx]
        if cost < old_cost:
            print(f"    Updating path to {node!r}")
            queue[idx] = cost, path
        else:
            print(f"    No change to queue, new cost ({cost}) >= old cost ({old_cost})")

    print(f"    Current queue: {queue}")


def dijkstra(graph, start, goal):
    # store best cost and path for nodes on a present or future frontier
    queue = [(0, [start])]

    # store nodes that are on past frontiers
    finished = set()

    while len(queue) > 0:
        print(f"Current queue: {queue}")

        # get a path off the true frontier
        cost, path = remove_min(queue)
        node = path[-1]
        finished.add(node)
        print(f"  Finished {node!r} with cost {cost}.")
        print(f"  Finished set: {finished}")

        # shortest path guaranteed for current node, return if goal
        if node == goal:
            return cost, path

        # update paths to neighbors on priority queue
        print(f"Expanding to neighbors from {node}")
        for next_node, weight in neighbors(graph, node):
            if next_node not in finished:
                print(f"  Processing {node!r}-->{next_node!r} with weight {weight}")
                new_cost = cost + weight
                new_path = path + [next_node]
                update_node(queue, new_path, new_cost)

        print()

    return None


# print(dijkstra(test_graph, "A", "G"))
# print(dijkstra(flights, "Boston", "Phoenix"))


############################################################
# Dijkstra's: storing nodes and predecessors
############################################################


def find_node(queue, node):
    for idx in range(len(queue)):
        cost, queue_node = queue[idx]
        if queue_node == node:
            return idx
    return None


def update_node(queue, node, cost):
    idx = find_node(queue, node)

    if idx is None:
        print(f"    Adding path to {node!r}")
        queue.append((cost, node))
        updated = True

    else:
        old_cost, old_path = queue[idx]
        if cost < old_cost:
            print(f"    Updating path to {node!r}")
            queue[idx] = cost, node
            updated = True
        else:
            print(f"    No change to queue, new cost ({cost}) >= old cost ({old_cost})")
            updated = False

    print(f"    Current queue: {queue}")
    return updated


def trace_predecessors(node, start, predecessors):
    current = node
    path = [current]
    while current != start:
        current = predecessors[current]
        path.append(current)
    path.reverse()
    return path


def dijkstra_predecessors(graph, start, goal):
    # store NODES instead of paths on priority queue
    queue = [(0, start)]
    finished = set()

    # store/update PREDECESSOR of each node whenever it's updated on queue
    predecessors = {start: None}

    while len(queue) > 0:
        print(f"Current queue: {queue}")

        # get a node off the true frontier
        cost, node = remove_min(queue)
        finished.add(node)
        print(f"  Finished {node!r} with cost {cost}.")
        print(f"  Finished set: {finished}")

        # shortest path guaranteed for current node, TRACE PREDECESSORS
        # to get path from start
        if node == goal:
            return cost, trace_predecessors(node, start, predecessors)

        # update node's distance in priority queue AND ITS PRECEDESSOR
        print(f"Expanding to neighbors from {node}")
        for next_node, weight in neighbors(graph, node):
            if next_node not in finished:
                print(f"  Processing {node!r}-->{next_node!r} with weight {weight}")
                new_cost = cost + weight
                if update_node(queue, next_node, new_cost):
                    predecessors[next_node] = node

        print()

    return None


# print(dijkstra_predecessors(test_graph, "A", "G"))
# print(dijkstra_predecessors(flights, "Boston", "Phoenix"))
