from abc import ABC, abstractmethod

from collections import deque
import heapq


class PathFindingStrategy(ABC):

    @abstractmethod
    def find_path(self, maze, start, exit_cell):
        pass


class BFSStrategy(PathFindingStrategy):

    def find_path(self, maze, start, exit_cell):

        queue = deque([start])

        visited = set()
        visited.add((start.x, start.y))

        parent = {}

        while queue:

            current = queue.popleft()

            if current == exit_cell:
                return self.restore_path(parent, start, exit_cell), len(visited)

            for neighbor in maze.get_neighbors(current):

                key = (neighbor.x, neighbor.y)

                if key not in visited:

                    visited.add(key)

                    parent[key] = current

                    queue.append(neighbor)

        return [], len(visited)

    def restore_path(self, parent, start, exit_cell):

        path = []

        current = exit_cell

        while current != start:

            path.append(current)

            current = parent[(current.x, current.y)]

        path.append(start)

        path.reverse()

        return path


class DFSStrategy(PathFindingStrategy):

    def find_path(self, maze, start, exit_cell):

        stack = [start]

        visited = set()
        visited.add((start.x, start.y))

        parent = {}

        while stack:

            current = stack.pop()

            if current == exit_cell:
                return self.restore_path(parent, start, exit_cell), len(visited)

            for neighbor in maze.get_neighbors(current):

                key = (neighbor.x, neighbor.y)

                if key not in visited:

                    visited.add(key)

                    parent[key] = current

                    stack.append(neighbor)

        return [], len(visited)

    def restore_path(self, parent, start, exit_cell):

        path = []

        current = exit_cell

        while current != start:

            path.append(current)

            current = parent[(current.x, current.y)]

        path.append(start)

        path.reverse()

        return path


class AStarStrategy(PathFindingStrategy):

    def heuristic(self, cell, exit_cell):

        return abs(cell.x - exit_cell.x) + abs(cell.y - exit_cell.y)

    def find_path(self, maze, start, exit_cell):

        heap = []

        heapq.heappush(heap, (0, start.x, start.y, start))

        visited = set()

        parent = {}

        g_score = {
            (start.x, start.y): 0
        }

        while heap:

            _, _, _, current = heapq.heappop(heap)

            key_current = (current.x, current.y)

            if key_current in visited:
                continue

            visited.add(key_current)

            if current == exit_cell:
                return self.restore_path(parent, start, exit_cell), len(visited)

            for neighbor in maze.get_neighbors(current):

                key = (neighbor.x, neighbor.y)

                tentative = g_score[key_current] + 1

                if key not in g_score or tentative < g_score[key]:

                    g_score[key] = tentative

                    priority = tentative + self.heuristic(neighbor, exit_cell)

                    heapq.heappush(
                        heap,
                        (priority, neighbor.x, neighbor.y, neighbor)
                    )

                    parent[key] = current

        return [], len(visited)

    def restore_path(self, parent, start, exit_cell):

        path = []

        current = exit_cell

        while current != start:

            path.append(current)

            current = parent[(current.x, current.y)]

        path.append(start)

        path.reverse()

        return path