from abc import ABC, abstractmethod
from collections import deque
import heapq
from typing import List, Dict, Optional
from models import Cell, Maze


class PathFindingStrategy(ABC):
    def __init__(self):
        self.visited_count = 0

    @abstractmethod
    def findPath(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
        pass

    def _reconstruct_path(self, parent: Dict[Cell, Optional[Cell]], current: Cell) -> List[Cell]:
        path = []
        while current:
            path.append(current)
            current = parent.get(current)
        return path[::-1]  


class BFSStrategy(PathFindingStrategy):
    def findPath(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
        self.visited_count = 0
        queue = deque([start])
        visited = {start}
        parent = {start: None}

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

            if current == exit:
                return self._reconstruct_path(parent, current)

            for neighbor in maze.getNeighbors(current):
                if neighbor not in visited:
                    visited.add(neighbor)
                    parent[neighbor] = current
                    queue.append(neighbor)
        return [] 


class DFSStrategy(PathFindingStrategy):
    def findPath(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
        self.visited_count = 0
        stack = [start]
        visited = {start}
        parent = {start: None}

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

            if current == exit:
                return self._reconstruct_path(parent, current)

            for neighbor in maze.getNeighbors(current):
                if neighbor not in visited:
                    visited.add(neighbor)
                    parent[neighbor] = current
                    stack.append(neighbor)
        return []


class AStarStrategy(PathFindingStrategy):

    @staticmethod
    def _heuristic(a: Cell, b: Cell) -> int:
        return abs(a.x - b.x) + abs(a.y - b.y)

    def findPath(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
        self.visited_count = 0
        counter = 0
        open_set = []
        heapq.heappush(open_set, (0, counter, start))
        counter += 1

        came_from = {start: None}
        g_score = {start: 0}
        visited = set()

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

            if current == exit:
                return self._reconstruct_path(came_from, current)

            if current in visited:
                continue
            visited.add(current)

            for neighbor in maze.getNeighbors(current):
                tentative_g_score = g_score[current] + 1
                if neighbor not in g_score or tentative_g_score < g_score[neighbor]:
                    came_from[neighbor] = current
                    g_score[neighbor] = tentative_g_score
                    f_score = tentative_g_score + self._heuristic(neighbor, exit)
                    heapq.heappush(open_set, (f_score, counter, neighbor))
                    counter += 1
        return []