from __future__ import annotations

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

from nographs import Vertex, VertexToID, VertexIterator, EdgeIterator


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) -> VertexIterator:
        """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) -> VertexIterator:
        """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:
        """Create an edge tuple out of its elements. To be overridden in sub
        classes."""

    def iter_edges_to_start(self, vertex: Vertex) -> EdgeIterator:
        """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) -> EdgeIterator:
        """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) -> Sequence:
        """Return the path that ends at *vertex* as a sequence. 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 as sequence.
        """


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) -> Sequence[Vertex]:
        """Sequence of the vertices in the path from the first to the given
        vertex. Internally, a sequence 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:
        return (from_vertex, to_vertex, *self._edge_data[to_vertex_id])

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