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

class Cell:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.is_wall = False
        self.is_start = False
        self.is_exit = False
    
    def is_passable(self):
        return not self.is_wall
    
    def __repr__(self):
        return f"Cell({self.x},{self.y})"


class Maze:
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.cells = []
        self.start = None
        self.exit = None
        
        for y in range(height):
            row = []
            for x in range(width):
                row.append(Cell(x, y))
            self.cells.append(row)
    
    def get_cell(self, x, y):
        if 0 <= x < self.width and 0 <= y < self.height:
            return self.cells[y][x]
        return None
    
    def get_neighbors(self, cell):
        neighbors = []
        for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
            nx, ny = cell.x + dx, cell.y + dy
            neighbor = self.get_cell(nx, ny)
            if neighbor and neighbor.is_passable():
                neighbors.append(neighbor)
        return neighbors


class TextFileMazeBuilder:
    def build_from_file(self, filename):
        with open(filename, 'r') as f:
            lines = [line.rstrip() for line in f.readlines()]
        
        height = len(lines)
        width = len(lines[0])
        maze = Maze(width, height)
        
        for y, line in enumerate(lines):
            for x, ch in enumerate(line):
                cell = maze.get_cell(x, y)
                if ch == '#':
                    cell.is_wall = True
                elif ch == 'S':
                    maze.start = cell
                    cell.is_start = True
                elif ch == 'E':
                    maze.exit = cell
                    cell.is_exit = True
        
        return maze


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


class BFSStrategy(PathFindingStrategy):
    def find_path(self, maze, start, exit):
        if not start or not exit:
            return [], 0
        
        queue = deque([(start, [start])])
        visited = {start}
        
        while queue:
            current, path = queue.popleft()
            if current == exit:
                return path, len(visited)
            
            for neighbor in maze.get_neighbors(current):
                if neighbor not in visited:
                    visited.add(neighbor)
                    queue.append((neighbor, path + [neighbor]))
        
        return [], len(visited)


class DFSStrategy(PathFindingStrategy):
    def find_path(self, maze, start, exit):
        if not start or not exit:
            return [], 0
        
        stack = [(start, [start])]
        visited = {start}
        
        while stack:
            current, path = stack.pop()
            if current == exit:
                return path, len(visited)
            
            for neighbor in maze.get_neighbors(current):
                if neighbor not in visited:
                    visited.add(neighbor)
                    stack.append((neighbor, path + [neighbor]))
        
        return [], len(visited)


class AStarStrategy(PathFindingStrategy):
    def _heuristic(self, a, b):
        return abs(a.x - b.x) + abs(a.y - b.y)
    
    def find_path(self, maze, start, exit):
        if not start or not exit:
            return [], 0
        
        heap = [(self._heuristic(start, exit), 0, start, [start])]
        g_score = {start: 0}
        visited = set()
        counter = 1
        
        while heap:
            _, _, current, path = heapq.heappop(heap)
            
            if current in visited:
                continue
            
            visited.add(current)
            
            if current == exit:
                return path, len(visited)
            
            for neighbor in maze.get_neighbors(current):
                tentative_g = g_score[current] + 1
                if neighbor not in g_score or tentative_g < g_score[neighbor]:
                    g_score[neighbor] = tentative_g
                    f = tentative_g + self._heuristic(neighbor, exit)
                    heapq.heappush(heap, (f, counter, neighbor, path + [neighbor]))
                    counter += 1
        
        return [], len(visited)


class SearchStats:
    def __init__(self, path, time_ms, visited_count):
        self.path = path
        self.time_ms = time_ms
        self.visited_count = visited_count
        self.path_length = len(path) if path else 0


class MazeSolver:
    def __init__(self, maze, strategy=None):
        self.maze = maze
        self.strategy = strategy
        self.observers = []

    def attach(self, observer):
        self.observers.append(observer)

    def detach(self, observer):
        self.observers.remove(observer)

    def notify(self, event, data=None):
        for observer in self.observers:
            observer.update(event, data)
    
    def set_strategy(self, strategy):
        self.strategy = strategy
    
    def solve(self):
        if self.strategy is  None:
            raise ValueError("Стратегия не установлена")
        self.notify("search_started")

        start_time = time.perf_counter()
        path, visited_count = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit)
        end_time = time.perf_counter()
        time_ms = (end_time - start_time) * 1000

        self.notify("search_finished", time_ms)
        self.notify("path_found", path)

        return SearchStats(path, time_ms, visited_count)

class Observer(ABC):

    @abstractmethod
    def update(self, event, data=None):
        pass

class ConsoleView(Observer):

    def __init__(self):
        self.events = []

    def update(self, event, data=None):
        self.events.append((event, data))

        if event == "maze_loaded":
            print("[Событие] Лфбирин загружен")
        elif event == "path_found":
            print(f"[Событие] Путь найден! Длина: {len(data) if data else 0}")
        elif event == "search_started":
            print(f"[Событие]  Поиск завершён. Время: {data:.3f}мс" if data else "[Событие] Поиск завершён")
        elif event == "mpve":
            print(f"[Событие] Игрок переместился в {data}")
        elif event == "undo":
            print("[Событие] Отмена последнего хода")

    def render(self,maze, player=None, path=None):

        os.system('cls' if os.name == 'nt' else 'clear')

        print("Лабиринт")

        for y in range(maze.height):
            row = ""
            for x in range(maze.width):
                cell = maze.get_cell(x,y)

                if player and cell == player.current_cell:
                    row += "p " #игрок
                elif path and cell in path:
                    row += "* " #путь
                elif cell.is_wall:
                    row += "# " #стена
                elif cell.is_start:
                    row += "S " #старт
                elif cell.is_exit:
                    row += "E " #выход
                else: 
                    row += ". " #прозод
            print(row)
        
        print("Управление: W/A/S/D - движение, U - отмена, Q - выход")

class Command(ABC):

    @abstractmethod
    def execute(self):
        pass

    @abstractmethod
    def undo(self):
        pass

class Player:

    def __init__(self, start_cell):
        self.current_cell = start_cell
        self.start_cell = start_cell

    def move_to(self, cell):
        self.current_cell = cell

    def resent(self):
        self.current_cell = self.start_cell
        
    def  __repr__(self):
        return f"Player at ({self.current_cell.x}, {self.current_cell.y})"
        
class MoveCommand(Command):

    def __init__(self, player, new_cell, view):
        self.player = player
        self.new_cell = new_cell
        self.old_cell = player.current_cell
        self.view = view

    def execute(self):
        self.player.move_to(self.new_cell)
        self.view.update("undo", None)

    def undo(self):
        self.player.move_to(self.old_cell)
        self.view.update("undo",None)

class GameController:

    def __init__(self, maze, view):
        self.maze = maze
        self.view = view
        self.player = Player(maze.start)
        self.command_history = []

    def get_cell_in_direction(self, direction):

        x, y = self.player.current_cell.x, self.player.current_cell.y

        if direction == 'w':
            y -= 1
        elif direction == 's':
            y += 1
        elif direction == 'a':
            x -= 1
        elif direction == 'd':
            x += 1
        else: 
            return None
        
        return self.maze.get_cell(x, y)
    
    def try_move(self, direction):

        new_cell = self.get_cell_in_direction(direction)

        if new_cell and new_cell.is_passable():
            command = MoveCommand(self.player, new_cell, self.view)
            command.execute()
            self.command_history.append(command)

            if new_cell.is_exit:
                self.view.update("path_found", [])
                print("Вы нашли выход.")
            return True
        else:
            print("Невозможно пройти - стена")
            return False
        
    def undo(self):

        if self.command_history:
            command = self.command_history.pop()
            command.undo()
        else:
            print("Нечего отменять")

    def visualize_path(self, path):
        self.view.render(self.maze, self.player, path)

    def run_manual_mode(self):

        while True:
            self.view.render(self.maze, self.player)

            command = input("Введите команду: ").lower().strip()

            if command in ['w', 'a', 's', 'd']:
                self.try_move(command)
            elif command == 'u':
                self.undo()
            elif command == 'q':
                print('Выход из игры')
                break
            else:
                print("Неизвестная команда. Используйте: W/A/S/D - движение, U - отмена, Q - выход")



        


