from math import inf
from typing import Iterator, Optional

from src.graph.node import Node
from src.graph.edge import Edge

class Graph:
    def __init__(self):
        self.edges: list[Edge] = []
        self.nodes: list[Node] = []

    def add_node(self, x: int, y: int, name: str) -> None:
        self.nodes.append(Node(x, y, name, len(self.nodes)))

    def add_edge(self, start_index: int, end_index: int, length: float) -> None:
        self.edges.append(Edge(start_index, end_index, length, len(self.edges)))

    def delete_edge(self, edge: Edge) -> None:
        self.edges.remove(edge)
        for ed in self.edges:
            ed.index = self.edges.index(ed)

    def delete_node(self, node: Node) -> None:
        edges_to_delete=[]
        for edge in self.edges:
            if node.index in (edge.start, edge.end):
                edges_to_delete.append(edge)
                continue
            if edge.start > node.index:
                edge.start -= 1
            if edge.end > node.index:
                edge.end -= 1
        for edge in edges_to_delete:
            self.delete_edge(edge)
        self.nodes.remove(node)
        for no in self.nodes:
            no.index = self.nodes.index(no)

    def number_of_nodes(self) -> int:
        return len(self.nodes)
    
    def get_edge(self, node_1: int, node_2: int) -> int:
        for edge in self.edges:
            if (edge.start == node_1 and edge.end == node_2) or (edge.start == node_2 and edge.end == node_1):
                return self.edges.index(edge)
        return -1
    
    def get_edge_nodes(self, edge: Edge) -> tuple[Node, Node]:
        return self.nodes[edge.start], self.nodes[edge.end]

    def edges_adjacent_to(self, node_i: int) -> Iterator[Edge]:
        return filter(lambda e: e.start == node_i or e.end == node_i, self.edges)
    
    def edge_exists(self, node_1: int, node_2: int) -> bool:
        return self.get_edge(node_1, node_2) != -1

    def dijkstra(self, source_index: int, target_index: int) -> Optional[list[int]]:
        n = len(self.nodes)

        if source_index < 0 or source_index >= n:
            return None

        if target_index < 0 or target_index >= n:
            return None

        unvisited = list(range(n))

        distances_from_start = [inf] * n
        distances_from_start[source_index] = 0

        node_sequences = [[] for _ in range(n)]
        node_sequences[source_index] = [source_index]

        while True:
            current_index = min(unvisited, key=lambda i: distances_from_start[i])

            if current_index == target_index:
                break

            unvisited.remove(current_index)

            for edge in self.edges_adjacent_to(current_index):
                start = current_index
                end = edge.end if edge.start == current_index else edge.start

                if end in unvisited and distances_from_start[end] > distances_from_start[start] + edge.length:
                    distances_from_start[end] = distances_from_start[start] + edge.length
                    node_sequences[end] = node_sequences[start].copy()
                    node_sequences[end].append(end)

        return node_sequences[target_index]