import abc
import heapq
import time
from collections import deque
from dataclasses import dataclass
from typing import List, Optional, Dict, Set, Tuple, Any
import csv
import os
import sys

class Cell:
    #тут что такое клетка
    def __init__(self, x: int, y: int, is_wall: bool = False,
                 is_exit: bool = False, is_start: bool = False):
        self.x = x
        self.y = y
        self.is_wall = is_wall
        self.is_exit = is_exit
        self.is_start = is_start
    
    def __eq__(self, other):
        return isinstance(other, Cell) and self.x == other.x and self.y == other.y

    def __hash__(self):
        return hash((self.x, self.y))

    def is_passable(self) -> bool:
        return not self.is_wall

    def __repr__(self) -> str:
        return f"Cell({self.x},{self.y})"


class Maze:
    def __init__(self, width: int, height: int): #что содержит лабиринт, начало конец и тд
        self.width = width
        self.height = height
        self.grid: List[List[Cell]] = []
        self.start_cell: Optional[Cell] = None
        self.exit_cell: Optional[Cell] = None

    def set_cell(self, x: int, y: int, cell: Cell) -> None: #ставим клетку куда надо или не ставим если в границы не попала
        if not (0 <= x < self.width and 0 <= y < self.height):
            raise IndexError("координаты вне границ лабиринта")
        self.grid[y][x] = cell

    def get_cell(self, x: int, y: int) -> Optional[Cell]: #тут уже из коррдинат клетку вытаскиваем
        if 0 <= x < self.width and 0 <= y < self.height:
            return self.grid[y][x]
        return None

    def get_neighbors(self, cell: Cell) -> List[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 MazeBuilder(abc.ABC):
    @abc.abstractmethod
    def build_from_file(self, filename: str) -> Maze:
        pass


class TextFileMazeBuilder(MazeBuilder):
    def build_from_file(self, filename: str) -> Maze:
        lines = []
        with open(filename, 'r', encoding='utf-8') as f:
            for line in f:
                line = line.rstrip('\n')
                if line: #игнорируем пустые строки
                    lines.append(line)
        
        if not lines:
            raise ValueError("Файл пуст")
        
        height = len(lines)
        width = max(len(line) for line in lines)
        
        maze = Maze(width, height)
        #инициализируем сетку пустыми клетками,по умолчанию стенами
        maze.grid = [[Cell(x, y, is_wall=True) for x in range(width)] for y in range(height)]
        
        start_cell = None
        exit_cell = None
        
        for y, line in enumerate(lines):
            for x, ch in enumerate(line):
                if x >= width:
                    continue
                if ch == '#':
                    continue
                elif ch == ' ':
                    cell = Cell(x, y, is_wall=False) 
                elif ch == 'S':
                    cell = Cell(x, y, is_wall=False, is_start=True)
                    start_cell = cell
                elif ch == 'E':
                    cell = Cell(x, y, is_wall=False, is_exit=True)
                    exit_cell = cell
                else:
                    #любой другой символ считаем проходом
                    cell = Cell(x, y, is_wall=False)
                maze.set_cell(x, y, cell)
        
        if start_cell is None:
            raise ValueError("отсутствует стартовая клетка (S)") #invalid check
        if exit_cell is None:
            raise ValueError("отсутствует выход (E)")
        
        maze.start_cell = start_cell
        maze.exit_cell = exit_cell
        return maze

class PathFindingStrategy(abc.ABC):
    @abc.abstractmethod
    def find_path(self, maze: Maze, start: Cell, exit_: Cell) -> Tuple[List[Cell], int]:
        pass

#дальше скорее математика, методы вроде ещё в том семаке разбирали
class BFSStrategy(PathFindingStrategy):
    def find_path(self, maze: Maze, start: Cell, exit_: Cell) -> Tuple[List[Cell], int]:
        if start is exit_:
            return [start], 1
        
        queue = deque([start]) #используйте deque 👍
        visited: Set[Cell] = {start}
        parent: Dict[Cell, Optional[Cell]] = {start: None}
        
        while queue:
            current = queue.popleft()
            if current is exit_:
                #восстановление пути
                path = []
                cur = current
                while cur is not None:
                    path.append(cur)
                    cur = parent[cur]
                path.reverse()
                return path, len(visited)
            
            for neighbor in maze.get_neighbors(current):
                if neighbor not in visited:
                    visited.add(neighbor)
                    parent[neighbor] = current
                    queue.append(neighbor)
        
        return [], len(visited)


class DFSStrategy(PathFindingStrategy):
    def find_path(self, maze: Maze, start: Cell, exit_: Cell) -> Tuple[List[Cell], int]:
        if start is exit_:
            return [start], 1
        
        stack = [start]
        visited: Set[Cell] = {start}
        parent: Dict[Cell, Optional[Cell]] = {start: None}
        
        while stack:
            current = stack.pop()
            if current is exit_:
                path = []
                cur = current
                while cur is not None:
                    path.append(cur)
                    cur = parent[cur]
                path.reverse()
                return path, len(visited)
            
            for neighbor in maze.get_neighbors(current):
                if neighbor not in visited:
                    visited.add(neighbor)
                    parent[neighbor] = current
                    stack.append(neighbor)
        
        return [], len(visited)


class AStarStrategy(PathFindingStrategy):
    @staticmethod
    def _heuristic(cell: Cell, target: Cell) -> int:
        return abs(cell.x - target.x) + abs(cell.y - target.y) #самая простая эвристика
    
    def find_path(self, maze: Maze, start: Cell, exit_: Cell) -> Tuple[List[Cell], int]:
        if start is exit_:
            return [start], 1
        counter = 0
        open_set = [(0, counter, start)]
        g_score: Dict[Cell, int] = {start: 0}
        f_score: Dict[Cell, int] = {start: self._heuristic(start, exit_)}
        parent: Dict[Cell, Optional[Cell]] = {start: None}
        closed_set: Set[Cell] = set()
        visited_count = 0
        
        while open_set:
            _, _, current = heapq.heappop(open_set)
            if current in closed_set:
                continue
            closed_set.add(current)
            visited_count = len(closed_set)
            
            if current is exit_:
                path = []
                cur = current
                while cur is not None:
                    path.append(cur)
                    cur = parent[cur]
                path.reverse()
                return path, visited_count
            
            for neighbor in maze.get_neighbors(current):
                if neighbor in closed_set:
                    continue
                tentative_g = g_score[current] + 1
                if neighbor not in g_score or tentative_g < g_score[neighbor]:
                    parent[neighbor] = current
                    g_score[neighbor] = tentative_g
                    f = tentative_g + self._heuristic(neighbor, exit_)
                    f_score[neighbor] = f
                    counter += 1
                    heapq.heappush(open_set, (f, counter, neighbor))
        
        return [], visited_count

@dataclass
class SearchStats:
    time_ms: float          #время выполнения в мс
    visited_cells: int      #количество посещённых клеток
    path_length: int        #длина найденного пути (0 если пути нет)
    path_found: bool        #найден ли путь

class Observer(abc.ABC): #я забыл что я там писать хотел после наблюдателя удачи мне завтра разобрать
    @abc.abstractmethod #а я (гугл + хабр) разобрал снова балбесина!!!
    def update(self, event_type: str, data: Any = None) -> None:
        pass


class Subject:
    def __init__(self):
        self._observers: List[Observer] = []
    
    def attach(self, observer: Observer) -> None:
        if observer not in self._observers:
            self._observers.append(observer)
    
    def detach(self, observer: Observer) -> None:
        if observer in self._observers:
            self._observers.remove(observer)
    
    def notify(self, event_type: str, data: Any = None) -> None:
        for obs in self._observers:
            obs.update(event_type, data)


class MazeSolver(Subject):
    def __init__(self, maze: Maze, strategy: Optional[PathFindingStrategy] = None):
        super().__init__()
        self.maze = maze
        self._strategy = strategy

    def set_strategy(self, strategy: PathFindingStrategy) -> None:
        self._strategy = strategy

    def solve(self) -> Optional[SearchStats]:
        if self._strategy is None:
            return None
        start_time = time.perf_counter()
        path, visited = self._strategy.find_path(self.maze, self.maze.start_cell, self.maze.exit_cell)
        end_time = time.perf_counter()
        time_ms = (end_time - start_time) * 1000.0
        return SearchStats(time_ms, visited, len(path), len(path) > 0)

class Benchmark:
    def __init__(self, maze_files: List[str], runs_per_strategy: int = 5):
        self.maze_files = maze_files
        self.runs = runs_per_strategy
        self.strategies = {
            "BFS": BFSStrategy(),
            "DFS": DFSStrategy(),
            "AStar": AStarStrategy()
        }
        self.builder = TextFileMazeBuilder()
        self.results = []

    def run(self, output_csv: str):
        for maze_file in self.maze_files:
            if not os.path.exists(maze_file):
                print(f"файл {maze_file} не найден")
                continue
            try:
                maze = self.builder.build_from_file(maze_file)
            except Exception as e:
                print(f"ошибка загрузки {maze_file}: {e}")
                continue

            print(f"обработка лабиринта: {maze_file} (размер {maze.width}x{maze.height})")
            for strat_name, strategy in self.strategies.items():
                solver = MazeSolver(maze, strategy)
                times = []
                visited_list = []
                path_lengths = []
                path_found = False
                for run_idx in range(self.runs):
                    stats = solver.solve()
                    if stats is None:
                        continue
                    times.append(stats.time_ms)
                    visited_list.append(stats.visited_cells)
                    path_lengths.append(stats.path_length)
                    path_found = stats.path_found
                if times:
                    avg_time = sum(times) / len(times)
                    avg_visited = sum(visited_list) / len(visited_list)
                    avg_length = sum(path_lengths) / len(path_lengths)
                else:
                    avg_time = avg_visited = avg_length = 0.0
                self.results.append({
                    "лабиринт": os.path.basename(maze_file),
                    "стратегия": strat_name,
                    "время_мс": round(avg_time, 3),
                    "посещено_клеток": round(avg_visited, 1),
                    "длина_пути": round(avg_length, 1),
                    "путь_найден": path_found
                })
                print(f"  {strat_name}: {avg_time:.3f} мс, посещено {avg_visited:.1f}, длина {avg_length:.1f}")

        with open(output_csv, 'w', newline='', encoding='utf-8') as csvfile:
            fieldnames = ["лабиринт", "стратегия", "время_мс", "посещено_клеток", "длина_пути", "путь_найден"]
            writer = csv.DictWriter(csvfile, fieldnames=fieldnames, delimiter=',')
            writer.writeheader()
            for row in self.results:
                writer.writerow(row)
        print(f"\nрезультаты сохранены в {output_csv}")

def interactive_mode(maze_file: str):
    builder = TextFileMazeBuilder()
    try:
        maze = builder.build_from_file(maze_file)
    except Exception as e:
        print(f"ошибка загрузки лабиринта: {e}")
        return

    strategies = {
        "1": ("BFS", BFSStrategy()),
        "2": ("DFS", DFSStrategy()),
        "3": ("A*", AStarStrategy())
    }
    print("\nвыберите алгоритм поиска:")
    print("1. BFS")
    print("2. DFS")
    print("3. A*")
    choice = input("введите (1/2/3): ").strip()

    if choice not in strategies:
        print("неверный выбор, по умолчанию используется BFS.")
        strat_name, strategy = strategies["1"]
    else:
        strat_name, strategy = strategies[choice]

    solver = MazeSolver(maze, strategy)
    stats = solver.solve()
    if stats is None:
        print("ошибка с решением")
        return
    
    path, _ = strategy.find_path(maze, maze.start_cell, maze.exit_cell)
    path_set = set(path)
    for y in range(maze.height):
        row = []
        for x in range(maze.width):
            cell = maze.get_cell(x, y)
            if cell is maze.start_cell:
                row.append('S')
            elif cell is maze.exit_cell:
                row.append('E')
            elif cell in path_set:
                row.append('*')
            elif cell and cell.is_wall:
                row.append('#')
            else:
                row.append(' ')
        print(''.join(row))

    print(f"\nстатистика ({strat_name}):")
    print(f"время выполнения: {stats.time_ms:.3f} мс")
    print(f"посещено клеток: {stats.visited_cells}")
    print(f"длина пути: {stats.path_length}")
    print(f"путь найден: {'да' if stats.path_found else 'нет'}")


def main():
    if len(sys.argv) < 2:
        print("использование:")
        print("режим визуализации: python main.py <файл_лабиринта>")
        print("режим замера: python main.py --benchmark <список_лабиринтов> --runs <кол_во_итераций> --output <название_таблицы>.csv")
        return

    if sys.argv[1] == "--benchmark":
        args = sys.argv[2:]
        maze_files = []
        runs = 5
        output = "benchmark_results.csv"
        i = 0
        while i < len(args):
            if args[i] == "--runs" and i+1 < len(args):
                runs = int(args[i+1])
                i += 2
            elif args[i] == "--output" and i+1 < len(args):
                output = args[i+1]
                i += 2
            else:
                maze_files.append(args[i])
                i += 1
        if not maze_files:
            print("Ошибка: не указаны файлы лабиринтов.")
            return
        benchmark = Benchmark(maze_files, runs_per_strategy=runs)
        benchmark.run(output)
    else:
        interactive_mode(sys.argv[1])

if __name__ == "__main__":
    main()