from __future__ import annotations

from collections.abc import Iterator, Hashable
from typing import Optional, Any
from abc import ABC, abstractmethod

from nographs import Vertex, VertexToID


class Paths(ABC):
    """A container that stores the paths ("ways" through a graph) that have been generated by one
    of the traversal algorithms.

    *Paths* provides methods to access and iterate a path.

    A path is identified by its end vertex. The vertices that come before the end
    vertex are themselves stored as a path. The container will never store two paths that
    lead to the same vertex.

    A path can be iterated in forward direction, i.e., from a start vertex to the given vertex, or
    in the backward direction. The forward iteration is implemented based on the backward
    iteration by creating a copy of the path. Thus, it is slower and needs additional memory.

    Objects of concrete subclasses of this class are not intended to be created
    by application code. On demand, the library will create Paths objects.
    """

    def __init__(self, predecessor: dict, vertex_to_id: Optional[VertexToID]):
        self._predecessor = predecessor
        self._vertex_to_id = vertex_to_id

    @abstractmethod
    def append_edge(self, from_vertex: [Vertex], to_vertex_id: Hashable, to_edge: Any):
        """Create a new path that starts with the existing path to from_vertex and ends with the
        given vertex (resp. id). In case, to_edge provides additional edge data, it is stored
        in the path,too."""

    def __contains__(self, vertex: [Vertex]) -> bool:
        """Returns whether a path for *vertex* exists in the *Paths* container.
        This allows for expressions like *vertex in paths*."""
        if vertex is None:
            raise RuntimeError("Paths: None instead of vertex given.")
        vertex_id = self._vertex_to_id(vertex) if self._vertex_to_id else vertex
        return vertex_id in self._predecessor

    def iter_vertices_to_start(self, vertex: [Vertex]) -> Iterator[Vertex]:
        """Iterate the vertices in the path to *vertex* from the last to the first."""
        if vertex not in self:  # Included: check that vertex is not None
            raise RuntimeError("Paths: No path for given vertex.")

        while vertex is not None:
            yield vertex
            vertex_id = self._vertex_to_id(vertex) if self._vertex_to_id else vertex
            vertex = self._predecessor.get(vertex_id)

    def iter_vertices_from_start(self, vertex: [Vertex]) -> Iterator[Vertex]:
        """Iterate the vertices in the path to *vertex* from the first to the last. Internally, a
        list of all the vertices is created and then reversed and iterated."""
        return reversed(tuple(self.iter_vertices_to_start(vertex)))

    @abstractmethod
    def _edge(
        self, from_vertex: [Vertex], to_vertex: [Vertex], to_vertex_id: Hashable
    ) -> tuple[Any]:
        """Create an edge tuple out of its elements. To be overridden in sub classes."""

    def iter_edges_to_start(self, vertex: [Vertex]) -> Iterator[tuple]:
        """Iterate the edges of the path to *vertex* from the last to the first."""
        if vertex not in self:  # Included: check that vertex is not None
            raise RuntimeError("Paths: No path for given vertex.")

        to_vertex = vertex
        while True:
            to_vertex_id = (
                self._vertex_to_id(to_vertex) if self._vertex_to_id else to_vertex
            )
            from_vertex = self._predecessor.get(to_vertex_id)
            if from_vertex is None:
                break
            yield self._edge(from_vertex, to_vertex, to_vertex_id)
            to_vertex = from_vertex

    def iter_edges_from_start(self, vertex: [Vertex]) -> Iterator[tuple]:
        """Iterate the edges of the path to *vertex* from the first to the last. Internally, a list
        of all the edges is created and then reversed and iterated."""
        return reversed(tuple(self.iter_edges_to_start(vertex)))

    @abstractmethod
    def __getitem__(self, vertex: [Vertex]) -> tuple:
        """Returns the path that ends at *vertex* as a tuple. The orientation
        of the path is from first to last vertex / edge. In case of a labeled path, the
        edges are returned, for an unlabeled path the vertices.

        The method allows for expressions like *paths[vertex]*.

        Internally, the path from the given vertex back to the first vertex
        in the path is computed, and then reversed and stored in a tuple.
        """


class PathsOfUnlabeledEdges(Paths):
    def __init__(self, predecessor: dict, vertex_to_id: Optional[VertexToID]):
        """
        :param predecessor: The predecessor information of the paths will be stored in
           the given dictionary.

        :param vertex_to_id: See `VertexToID` function.

        Path of edges that are not labeled, i.e., for some starting vertex, an edge leading to a
        specific end vertex is represented only by this end vertex.

        This class is not part of the public interface of the library. Its signature, methods
        and implementation can be subject to breaking changes even in minor releases. It is
        not intended to be used by application code.
        """
        super().__init__(predecessor, vertex_to_id)

    def append_edge(self, from_vertex: Vertex, to_vertex_id: Hashable, to_edge: Any):
        """Create a new path that starts with the existing path to from_vertex and ends with the
        given vertex (resp. id)."""
        # This method is used nowhere. Its call is inlined in all algorithms.
        self._predecessor[to_vertex_id] = from_vertex

    def _edge(
        self, from_vertex: [Vertex], to_vertex: [Vertex], to_vertex_id: Hashable
    ) -> tuple[Vertex, Vertex]:
        return (from_vertex, to_vertex)

    def __getitem__(self, vertex: [Vertex]) -> tuple[Vertex]:
        """Tuple of the vertices in the path from the first to the given vertex. Internally, a
        list of the vertices in backward order is created and then reversed."""
        return tuple(self.iter_vertices_from_start(vertex))


class PathsOfLabeledEdges(Paths):
    def __init__(
        self, predecessor: dict, edge_data: dict, vertex_to_id: Optional[VertexToID]
    ):
        """
        :param predecessor: The predecessor information of the paths will be stored in
           the given dictionary.

        :param edge_data: The edge data of the paths will be stored in
           the given dictionary.

        :param vertex_to_id: See `VertexToID` function.

        Path of edges that are labeled, i.e., for some starting vertex, an edge leading to a
        specific end vertex is represented by a tuple that starts with the end vertex and,
        optionally, contains additional data.

        This class is not part of the public interface of the library. Its signature, methods
        and implementation can be subject to breaking changes even in minor releases. It is
        not intended to be used by application code.
        """

        super().__init__(predecessor, vertex_to_id)
        self._edge_data = edge_data

    def append_edge(
        self, from_vertex: Vertex, to_vertex_id: Hashable, to_edge: tuple[Vertex, ...]
    ):
        """Create a new path that starts with the existing path to from_vertex and ends with the
        given vertex (resp. id). The additional edge data provided in to_edge after to_vertex is
        stored in the path, too."""
        # This method is used nowhere. Its call is inlined in all algorithms, for improved speed.
        self._predecessor[to_vertex_id] = from_vertex
        self._edge_data[to_vertex_id] = to_edge[1:]

    def _edge(
        self, from_vertex: [Vertex], to_vertex: [Vertex], to_vertex_id: Hashable
    ) -> tuple[Vertex, Vertex, ...]:
        return (from_vertex, to_vertex, *self._edge_data[to_vertex_id])

    def __getitem__(self, vertex: [Vertex]) -> tuple[tuple]:
        """Tuple of the vertices in the path from the first to the given vertex. Internally, a
        list of all the edges in backward order is created and then reversed."""
        return tuple(self.iter_edges_from_start(vertex))
