from abc import ABC, abstractmethod
from typing import List
from collections import deque
import heapq
from maze_core import Maze, Cell

class SearchStrategy(ABC):
    @abstractmethod
    def find_path(self, maze: Maze, start: Cell, goal: Cell) -> List[Cell]:
        pass
    @property
    @abstractmethod
    def visited_count(self) -> int:
        pass

class BFSSearch(SearchStrategy):
    def __init__(self):
        self._visited = 0
    def find_path(self, maze: Maze, start: Cell, goal: Cell) -> List[Cell]:
        self._visited = 0
        if start == goal:
            return [start]
        
        visited = {start}
        queue = deque([start])
        start.prev = None
        
        while queue:
            curr = queue.popleft()
            self._visited += 1
            
            if curr == goal:
                return self._build_path(curr)
            
            for nb in maze.neighbors(curr):
                if nb not in visited:
                    visited.add(nb)
                    nb.prev = curr
                    queue.append(nb)
        return []
    
    def _build_path(self, end: Cell) -> List[Cell]:
        path = []
        while end:
            path.append(end)
            end = end.prev
        return path[::-1]
    
    @property
    def visited_count(self) -> int:
        return self._visited

class DFSSearch(SearchStrategy):
    def __init__(self):
        self._visited = 0
    
    def find_path(self, maze: Maze, start: Cell, goal: Cell) -> List[Cell]:
        self._visited = 0
        if start == goal:
            return [start]
        
        visited = set()
        stack = [start]
        start.prev = None
        
        while stack:
            curr = stack.pop()
            if curr in visited:
                continue
            
            visited.add(curr)
            self._visited += 1
            
            if curr == goal:
                return self._build_path(curr)
            
            for nb in maze.neighbors(curr):
                if nb not in visited:
                    nb.prev = curr
                    stack.append(nb)
        
        return []
    
    def _build_path(self, end: Cell) -> List[Cell]:
        path = []
        while end:
            path.append(end)
            end = end.prev
        return path[::-1]
    
    @property
    def visited_count(self) -> int:
        return self._visited

class AStarSearch(SearchStrategy):
    def __init__(self):
        self._visited = 0
    
    def find_path(self, maze: Maze, start: Cell, goal: Cell) -> List[Cell]:
        self._visited = 0
        if start == goal:
            return [start]
        
        counter = 0
        open_set = [(self._h(start, goal), counter, start)]
        came_from = {}
        g_score = {start: 0}
        open_hash = {start}
        start.prev = None
        
        while open_set:
            _, _, curr = heapq.heappop(open_set)
            open_hash.discard(curr)
            self._visited += 1
            
            if curr == goal:
                return self._build_path(curr, came_from)
            
            for nb in maze.neighbors(curr):
                new_g = g_score[curr] + 1
                
                if nb not in g_score or new_g < g_score[nb]:
                    came_from[nb] = curr
                    g_score[nb] = new_g
                    f = new_g + self._h(nb, goal)
                    
                    if nb not in open_hash:
                        counter += 1
                        heapq.heappush(open_set, (f, counter, nb))
                        open_hash.add(nb)
                        nb.prev = curr
        return []
    
    def _h(self, a: Cell, b: Cell) -> float:
        return abs(a.x - b.x) + abs(a.y - b.y)
    
    def _build_path(self, end: Cell, came_from: dict) -> List[Cell]:
        path = [end]
        while end in came_from:
            end = came_from[end]
            path.append(end)
        return path[::-1]
    @property
    def visited_count(self) -> int:
        return self._visited