import time, os
from collections import deque
from abc import ABC, abstractmethod
import heapq # <-- Добавлен импорт для A*

# --- ЭТАП 1: МОДЕЛЬ ---
class Cell:
    def __init__(self, x, y, is_wall=False):
        self.x = x
        self.y = y
        self.is_wall = is_wall
    def isPassable(self):
        return not self.is_wall

class Maze:
    def __init__(self, width, height, grid):
        self.width = width
        self.height = height
        self.grid = grid
        self.start_cell = grid[0][0]
        self.exit_cell = grid[height-1][width-1]
    def getNeighbors(self, cell):
        neighbors = []
        directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
        for d in directions:
            nx = cell.x + d[0]
            ny = cell.y + d[1]
            if nx >= 0 and nx < self.width and ny >= 0 and ny < self.height:
                if not self.grid[ny][nx].is_wall:
                    neighbors.append(self.grid[ny][nx])
        return neighbors

# --- ЭТАП 2: BUILDER ---
class MazeBuilder:
    def buildFromFile(self, filename):
        path = filename
        if "docs/data/" not in path:
            path = os.path.join("docs", "data", filename)
            
        with open(path, 'r') as f:
            lines = []
            for line in f:
                stripped = line.strip()
                if stripped:
                    lines.append(stripped)
        
        h = len(lines)
        w = len(lines[0])
        grid = []
        for y in range(h):
            row = []
            for x in range(w):
                is_wall = False
                if x < len(lines[y]):
                    if lines[y][x] == '#':
                        is_wall = True
                row.append(Cell(x, y, is_wall))
            grid.append(row)
            
        maze = Maze(w, h, grid)
        for y in range(h):
            for x in range(len(lines[y])):
                if lines[y][x] == 'S':
                    maze.start_cell = maze.grid[y][x]
                if lines[y][x] == 'E':
                    maze.exit_cell = maze.grid[y][x]
        return maze

# --- ЭТАП 3: STRATEGY ---
class SearchStats:
    def __init__(self, time_ms, visited, length):
        self.time_ms = time_ms
        self.visited = visited
        self.length = length

class PathFindingStrategy(ABC):
    @abstractmethod
    def findPath(self, maze, start, exit):
        pass

# 1. Поиск в ширину (BFS) - оригинальный алгоритм
class BFSStrategy(PathFindingStrategy):
    def findPath(self, maze, start, exit):
        queue = deque([start])
        visited = {start: None}
        while len(queue) > 0:
            curr = queue.popleft()
            if curr == exit:
                break
            for n in maze.getNeighbors(curr):
                if n not in visited:
                    visited[n] = curr
                    queue.append(n)
        path = []
        curr = exit
        while curr is not None:
            path.append(curr)
            curr = visited.get(curr)
        return path[::-1], len(visited)

# 2. Поиск в глубину (DFS) - добавлен
class DFSStrategy(PathFindingStrategy):
    def findPath(self, maze, start, exit):
        stack = [start]
        visited = {start: None}
        while len(stack) > 0:
            curr = stack.pop()
            if curr == exit:
                break
            for n in maze.getNeighbors(curr):
                if n not in visited:
                    visited[n] = curr
                    stack.append(n)
        path = []
        curr = exit
        while curr is not None:
            path.append(curr)
            curr = visited.get(curr)
        return path[::-1], len(visited)

# 3. Алгоритм A*
class AStarStrategy(PathFindingStrategy):
    def findPath(self, maze, start, exit):
        counter = 0 
        queue = [(0, counter, start)]
        came_from = {start: None}
        g_score = {start: 0}

        while len(queue) > 0:
            _, _, curr = heapq.heappop(queue)
            
            if curr == exit:
                break

            for n in maze.getNeighbors(curr):
                tentative_g_score = g_score[curr] + 1
                if n not in g_score or tentative_g_score < g_score[n]:
                    came_from[n] = curr
                    g_score[n] = tentative_g_score
                    # Эвристика: Манхэттенское расстояние
                    f_score = tentative_g_score + abs(n.x - exit.x) + abs(n.y - exit.y)
                    counter += 1
                    heapq.heappush(queue, (f_score, counter, n))

        path = []
        curr = exit
        while curr is not None:
            path.append(curr)
            curr = came_from.get(curr)
        return path[::-1], len(came_from)

# --- ЭТАП 4: ORCHESTRATOR ---
class MazeSolver:
    def __init__(self, maze, player=None):
        self.maze = maze
        self.player = player
        self.observers = []
    def attach(self, obs):
        self.observers.append(obs)
    def notify(self, event, data):
        for o in self.observers:
            o.update(event, data)
    def solve(self, strat):
        t0 = time.perf_counter()
        path, visited = strat.findPath(self.maze, self.maze.start_cell, self.maze.exit_cell)
        t1 = time.perf_counter()
        return SearchStats((t1 - t0) * 1000, visited, len(path))

# --- ЭТАП 5: OBSERVER & COMMAND ---
class Player:
    def __init__(self, cell):
        self.current_cell = cell

class MoveCommand:
    def __init__(self, player, dx, dy, maze):
        self.player = player
        self.dx = dx
        self.dy = dy
        self.maze = maze
    def execute(self):
        nx = self.player.current_cell.x + self.dx
        ny = self.player.current_cell.y + self.dy
        if nx >= 0 and nx < self.maze.width and ny >= 0 and ny < self.maze.height:
            target = self.maze.grid[ny][nx]
            if target.isPassable():
                self.player.current_cell = target
                return True
        return False

class ConsoleView:
    def update(self, event, data):
        print(f"[INFO] {event.upper()}: {data}")

# --- ЗАПУСК ---
if __name__ == "__main__":
    files = ["maze10-10.txt", "maze50-50.txt", "maze100-100.txt", "maze0.txt", "maze777.txt"]
    mode = input("Эксперимент (e) или игра (i)? ").lower()
    
    if mode == 'e':
        # Обновленная таблица с колонкой "Метод"
        print(f"{'Файл':<15} | {'Метод':<5} | {'Время(мс)':<10} | {'Посещено':<10} | {'Путь':<6}")
        print("-" * 58)
        
        for f in files:
            try:
                m = MazeBuilder().buildFromFile(f)
                
                # Словарь с нашими тремя методами
                strategies = {
                    "BFS": BFSStrategy(),
                    "DFS": DFSStrategy(),
                    "A*": AStarStrategy()
                }
                
                # Запускаем каждый метод для текущего лабиринта
                for strat_name, strat_obj in strategies.items():
                    t_sum, v_sum, l_sum = 0, 0, 0
                    for _ in range(10):
                        s = MazeSolver(m).solve(strat_obj)
                        t_sum += s.time_ms
                        v_sum += s.visited
                        l_sum += s.length
                    
                    print(f"{f:<15} | {strat_name:<5} | {t_sum/10:<10.2f} | {v_sum/10:<10.1f} | {l_sum/10:<6.1f}")
                
                # Линия-разделитель между разными лабиринтами для удобства чтения
                print("-" * 58)
                
            except FileNotFoundError:
                print(f"{f:<15} | ОШИБКА: Файл не найден")
                print("-" * 58)
            
    elif mode == 'i':
        name = input("Имя файла: ")
        m = MazeBuilder().buildFromFile(name)
        p = Player(m.start_cell)
        s = MazeSolver(m, p)
        s.attach(ConsoleView())
        
        while True:
            cmd = input("WASD (q-выход): ").lower()
            if cmd == 'q': break
            dx, dy = 0, 0
            if cmd == 'w': dy = -1
            elif cmd == 'a': dx = -1
            elif cmd == 's': dy = 1
            elif cmd == 'd': dx = 1
            
            if dx != 0 or dy != 0:
                if MoveCommand(p, dx, dy, m).execute():
                    s.notify("move", f"Направление {cmd}, Координата ({p.current_cell.x}, {p.current_cell.y})")
                else:
                    s.notify("error", "Стена!")