from abc import ABC, abstractmethod
from collections import deque
import heapq


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


def _reconstruct_path(came_from, start, exit_cell):
    path = []
    current = exit_cell
    while current is not None:
        path.append(current)
        current = came_from.get((current.x, current.y))
    path.reverse()
    if path and path[0].x == start.x and path[0].y == start.y:
        return path
    return []

class BFSStrategy(PathFindingStrategy):
    def find_path(self, maze, start, exit_cell):
        queue = deque([start])
        came_from = {(start.x, start.y): None}
        self.visited_count = 0

        while queue:
            current = queue.popleft()
            self.visited_count += 1

            if current.x == exit_cell.x and current.y == exit_cell.y:
                return _reconstruct_path(came_from, start, exit_cell)

            for neighbor in maze.get_neighbors(current):
                key = (neighbor.x, neighbor.y)
                if key not in came_from:
                    came_from[key] = current
                    queue.append(neighbor)

        self.visited_count = len(came_from)
        return []  # путь не найден

class DFSStrategy(PathFindingStrategy):

    def find_path(self, maze, start, exit_cell):
        stack = [start]
        came_from = {(start.x, start.y): None}
        self.visited_count = 0

        while stack:
            current = stack.pop()
            self.visited_count += 1

            if current.x == exit_cell.x and current.y == exit_cell.y:
                return _reconstruct_path(came_from, start, exit_cell)

            for neighbor in maze.get_neighbors(current):
                key = (neighbor.x, neighbor.y)
                if key not in came_from:
                    came_from[key] = current
                    stack.append(neighbor)

        self.visited_count = len(came_from)
        return []

class AStarStrategy(PathFindingStrategy):

    def _heuristic(self, cell, goal):
        return abs(cell.x - goal.x) + abs(cell.y - goal.y)

    def find_path(self, maze, start, exit_cell):
        # (f_score, счётчик для разрыва связей, клетка)
        counter = 0
        open_set = [(0, counter, start)]
        came_from = {(start.x, start.y): None}
        g_score = {(start.x, start.y): 0}
        self.visited_count = 0

        while open_set:
            _, _, current = heapq.heappop(open_set)
            self.visited_count += 1

            if current.x == exit_cell.x and current.y == exit_cell.y:
                return _reconstruct_path(came_from, start, exit_cell)

            for neighbor in maze.get_neighbors(current):
                key = (neighbor.x, neighbor.y)
                tentative_g = g_score[(current.x, current.y)] + 1

                if key not in g_score or tentative_g < g_score[key]:
                    g_score[key] = tentative_g
                    f = tentative_g + self._heuristic(neighbor, exit_cell)
                    counter += 1
                    heapq.heappush(open_set, (f, counter, neighbor))
                    came_from[key] = current

        self.visited_count = len(came_from)
        return []
