import time
import csv
import heapq
from collections import deque
from abc import ABC, abstractmethod
import matplotlib.pyplot as plt
import pandas as pd
from dataclasses import dataclass
import os


class Cell:
    """Клетка лабиринта"""
    def __init__(self, x, y, is_wall=False):
        self.x = x
        self.y = y
        self.is_wall = is_wall
        self.is_start = False
        self.is_exit = False

    def is_passable(self):
        return not self.is_wall


class Maze:
    """Лабиринт"""
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.cells = [[Cell(x, y) for x in range(width)] for y in range(height)]
        self.start = None
        self.exit = None

    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
            nb = self.get_cell(nx, ny)
            if nb and nb.is_passable():
                neighbors.append(nb)
        return neighbors
    
    def __str__(self):
        result = ""
        for y in range(self.height):
            for x in range(self.width):
                cell = self.get_cell(x, y)
                if cell is None:
                    result += "?"
                elif cell.is_wall:
                    result += "#"
                elif cell.is_start:
                    result += "S"
                elif cell.is_exit:
                    result += "E"
                else:
                    result += " "
            result += "\n"
        return result

class MazeBuilder(ABC):
    @abstractmethod
    def build_from_file(self, filename):
        pass


class TextFileMazeBuilder(MazeBuilder):
    def build_from_file(self, filename):
        with open(filename, 'r', encoding='utf-8') as f:
            lines = [line.rstrip('\n') for line in f.readlines()]
        height = len(lines)
        width = max(len(line) for line in lines)
        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':
                    cell.is_start = True
                    maze.start = cell
                elif ch == 'E':
                    cell.is_exit = True
                    maze.exit = cell
                else:
                    cell.is_wall = False
        return maze


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


class BFSStrategy(PathFindingStrategy):
    """Поиск в ширину"""
    
    def find_path(self, maze, start, exit):
        visited = set()
        if start == exit:
            return [start], 1
        queue = deque([start])
        visited.add(start)
        parent = {start: None}
        while queue:
            current = queue.popleft()
            for nb in maze.get_neighbors(current):
                if nb not in visited:
                    visited.add(nb)
                    parent[nb] = current
                    if nb == exit:
                        path = []
                        node = nb
                        while node is not None:
                            path.append(node)
                            node = parent[node]
                        path.reverse()
                        return path, len(visited)
                    queue.append(nb)
        return [], len(visited)


class DFSStrategy(PathFindingStrategy):
    """Поиск в глубину"""
    
    def find_path(self, maze, start, exit):
        visited = set()
        stack = [(start, [start])]
        while stack:
            current, path = stack.pop()
            if current == exit:
                return path, len(visited)
            visited.add(current)
            for nb in maze.get_neighbors(current):
                if nb not in visited:
                    stack.append((nb, path + [nb]))
        return [], len(visited)


class AStarStrategy(PathFindingStrategy):
    """Алгоритм A"""
    
    def heuristic(self, cell, exit):
        return abs(cell.x - exit.x) + abs(cell.y - exit.y)

    def find_path(self, maze, start, exit):
        open_set = []
        counter = 0
        heapq.heappush(open_set, (0, counter, start))
        counter += 1
        came_from = {}
        g_score = {start: 0}
        f_score = {start: self.heuristic(start, exit)}
        visited = set()
        while open_set:
            _, _, current = heapq.heappop(open_set)
            visited.add(current)
            if current == exit:
                path = []
                node = current
                while node in came_from:
                    path.append(node)
                    node = came_from[node]
                path.append(start)
                path.reverse()
                return path, len(visited)
            for nb in maze.get_neighbors(current):
                tentative_g = g_score[current] + 1
                if tentative_g < g_score.get(nb, float('inf')):
                    came_from[nb] = current
                    g_score[nb] = tentative_g
                    f = tentative_g + self.heuristic(nb, exit)
                    heapq.heappush(open_set, (f, counter, nb))
                    counter += 1
        return [], len(visited)
    
@dataclass
class SearchStats:
    time_ms: float
    visited_cells: int
    path_length: int
    algorithm: str


class MazeSolver:
    def __init__(self, maze, strategy):
        self.maze = maze
        self.strategy = strategy

    def set_strategy(self, strategy):
        self.strategy = strategy

    def solve(self):
        if self.maze.start is None or self.maze.exit is None:
            raise ValueError("Лабиринт не имеет старта или выхода")
        start_time = time.perf_counter()
        path, visited = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit)
        end_time = time.perf_counter()
        stats = SearchStats(
            time_ms=(end_time - start_time) * 1000,
            visited_cells=visited,
            path_length=len(path),
            algorithm=self.strategy.__class__.__name__
        )
        return path, stats

class Observer(ABC):
    @abstractmethod
    def update(self, event_type, data=None):
        pass


class ConsoleLogger(Observer):
    def update(self, event_type, data=None):
        if event_type == "search_start":
            print(f"[LOG] Поиск пути начат")
        elif event_type == "path_found":
            print(f"[LOG] Путь найден! Длина: {data}")
        elif event_type == "no_path":
            print("[LOG] Путь не найден")
        elif event_type == "step":
            print(f"[LOG] Шаг: {data}")


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

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

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

    def notify(self, event_type, data=None):
        for obs in self.observers:
            obs.update(event_type, data)

    def solve(self):
        if self.maze.start is None or self.maze.exit is None:
            raise ValueError("Лабиринт не имеет старта или выхода")
        self.notify("search_start")
        start_time = time.perf_counter()
        path, visited = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit)
        end_time = time.perf_counter()
        if path:
            self.notify("path_found", len(path))
        else:
            self.notify("no_path")
        stats = SearchStats(
            time_ms=(end_time - start_time) * 1000,
            visited_cells=visited,
            path_length=len(path),
            algorithm=self.strategy.__class__.__name__
        )
        return path, stats

class Command(ABC):
    @abstractmethod
    def execute(self):
        pass

    @abstractmethod
    def undo(self):
        pass


class MoveCommand(Command):
    def __init__(self, player, direction, maze):
        self.player = player
        self.direction = direction
        self.maze = maze
        self.prev_pos = None

    def execute(self):
        self.prev_pos = self.player.current_cell
        dx, dy = self.direction
        nx, ny = self.player.current_cell.x + dx, self.player.current_cell.y + dy
        new_cell = self.maze.get_cell(nx, ny)
        if new_cell and new_cell.is_passable():
            self.player.current_cell = new_cell
            return True
        return False

    def undo(self):
        if self.prev_pos:
            self.player.current_cell = self.prev_pos
            return True
        return False


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


def interactive_move_demo(maze, path):
    """Демонстрация движения с отменой последнего шага"""
    if not path:
        print("Путь не найден, демонстрация движения невозможна.")
        return
    player = Player(maze.start)
    command_history = []
    print("\n Интерактивное движение по найденному пути")
    print("Текущая позиция: старт")
    for step, cell in enumerate(path):
        if cell == maze.start:
            continue
        prev = path[step-1]
        dx = cell.x - prev.x
        dy = cell.y - prev.y
        cmd = MoveCommand(player, (dx, dy), maze)
        cmd.execute()
        command_history.append(cmd)
        print(f"Шаг {step}: перемещение на ({dx},{dy}), позиция ({player.current_cell.x},{player.current_cell.y})")
        if cell == maze.exit:
            print("Достигнут выход!")
            break
    if command_history:
        print("\nДемонстрация отмены последнего шага")
        cmd = command_history[-1]
        cmd.undo()
        print(f"Отменён последний шаг, позиция: ({player.current_cell.x},{player.current_cell.y})")
        
def test_single_maze(filename, strategies, repeats=5):
    """Тестирование одного лабиринта с разными стратегиями"""
    builder = TextFileMazeBuilder()
    maze = builder.build_from_file(filename)
    results = []
    for strategy in strategies:
        solver = MazeSolver(maze, strategy)
        times = []
        visits = []
        lengths = []
        for _ in range(repeats):
            _, stats = solver.solve()
            times.append(stats.time_ms)
            visits.append(stats.visited_cells)
            lengths.append(stats.path_length)
        results.append({
            'algorithm': strategy.__class__.__name__,
            'avg_time_ms': sum(times) / repeats,
            'avg_visited': sum(visits) / repeats,
            'avg_path_len': sum(lengths) / repeats
        })
    return results


def save_maze_to_file(maze, filename):
    """Сохранение лабиринта в файл"""
    os.makedirs(os.path.dirname(filename), exist_ok=True)
    with open(filename, 'w', encoding='utf-8') as f:
        for y in range(maze.height):
            line = ""
            for x in range(maze.width):
                cell = maze.get_cell(x, y)
                if cell.is_wall:
                    line += "#"
                elif cell.is_start:
                    line += "S"
                elif cell.is_exit:
                    line += "E"
                else:
                    line += " "
            f.write(line + "\n")


def create_test_mazes():
    """Создание тестовых лабиринтов"""
    os.makedirs("mazes", exist_ok=True)
    
    # 1. Простой лабиринт 10x10 (tiny.txt)
    maze1 = Maze(10, 10)
    for y in range(10):
        for x in range(10):
            is_start = (x == 0 and y == 0)
            is_exit = (x == 9 and y == 0)
            is_wall = False
            if y == 1 and x not in [0, 1, 9]:
                is_wall = True
            if y == 2 and x not in [9]:
                is_wall = True
            if y == 3 and x not in [0, 9]:
                is_wall = True
            if y == 4 and x not in [0, 1, 9]:
                is_wall = True
            if y == 5 and x not in [9]:
                is_wall = True
            if y == 6 and x not in [0, 9]:
                is_wall = True
            if y == 7 and x not in [9]:
                is_wall = True
            if y == 8 and x not in [0, 9]:
                is_wall = True
            cell = Cell(x, y, is_wall=is_wall)
            cell.is_start = is_start
            cell.is_exit = is_exit
            maze1.cells[y][x] = cell
            if is_start:
                maze1.start = cell
            if is_exit:
                maze1.exit = cell
    save_maze_to_file(maze1, "mazes/tiny.txt")
    
    # 2. Средний лабиринт 15x15 (medium.txt)
    maze2 = Maze(15, 15)
    for y in range(15):
        for x in range(15):
            is_start = (x == 0 and y == 0)
            is_exit = (x == 14 and y == 14)
            is_wall = (x % 3 == 1 and y % 2 == 0) and not is_start and not is_exit
            cell = Cell(x, y, is_wall=is_wall)
            cell.is_start = is_start
            cell.is_exit = is_exit
            maze2.cells[y][x] = cell
            if is_start:
                maze2.start = cell
            if is_exit:
                maze2.exit = cell
    save_maze_to_file(maze2, "mazes/medium.txt")
    
    # 3. Большой лабиринт 30x30 (large.txt)
    maze3 = Maze(30, 30)
    for y in range(30):
        for x in range(30):
            is_start = (x == 0 and y == 0)
            is_exit = (x == 29 and y == 29)
            is_wall = (x % 2 == 0 and y % 3 == 0) and not is_start and not is_exit
            cell = Cell(x, y, is_wall=is_wall)
            cell.is_start = is_start
            cell.is_exit = is_exit
            maze3.cells[y][x] = cell
            if is_start:
                maze3.start = cell
            if is_exit:
                maze3.exit = cell
    save_maze_to_file(maze3, "mazes/large.txt")
    
    # 4. Пустой лабиринт 15x15 (empty.txt)
    maze4 = Maze(15, 15)
    for y in range(15):
        for x in range(15):
            is_start = (x == 0 and y == 0)
            is_exit = (x == 14 and y == 14)
            cell = Cell(x, y, is_wall=False)
            cell.is_start = is_start
            cell.is_exit = is_exit
            maze4.cells[y][x] = cell
            if is_start:
                maze4.start = cell
            if is_exit:
                maze4.exit = cell
    save_maze_to_file(maze4, "mazes/empty.txt")
    
    # 5. Лабиринт без выхода 10x10 (no_exit.txt)
    maze5 = Maze(10, 10)
    for y in range(10):
        for x in range(10):
            is_start = (x == 0 and y == 0)
            is_exit = (x == 9 and y == 9)
            is_wall = (x > 0 and y > 0) and not is_start
            cell = Cell(x, y, is_wall=is_wall)
            cell.is_start = is_start
            cell.is_exit = is_exit
            maze5.cells[y][x] = cell
            if is_start:
                maze5.start = cell
            if is_exit:
                maze5.exit = cell
    save_maze_to_file(maze5, "mazes/no_exit.txt")
    
def print_analysis():
    """Вывод анализа эффективности алгоритмов"""
    print(" АНАЛИЗ ЭФФЕКТИВНОСТИ АЛГОРИТМОВ ПОИСКА ПУТИ")
    
    print("""
    BFS (Поиск в ширину):
      - Всегда находит КРАТЧАЙШИЙ путь
      - Сложность O(V+E)
      - Много памяти (очередь)
      - Лучший выбор для поиска минимального пути
    
    DFS (Поиск в глубину):
      - НЕ гарантирует кратчайший путь
      - Сложность O(V+E)
      - Мало памяти 
      - Быстрый, но путь может быть очень длинным
      - Хорош для проверки существования пути
    
    A* (Алгоритм с эвристикой):
      - Находит КРАТЧАЙШИЙ путь (при допустимой эвристике)
      - Эвристика: манхэттенское расстояние |x1-x2| + |y1-y2|
      - Быстрее BFS благодаря целенаправленному поиску
      - Лучший выбор для больших запутанных лабиринтов
    """)
    
    print("""
    ВЛИЯНИЕ ТИПА ЛАБИРИНТА:
    
    Простой лабиринт (tiny.txt):
      - Все алгоритмы работают быстро
      - Разница в скорости незначительна
      - BFS и A* находят оптимальный путь
      - DFS может найти более длинный путь
    
    Средний лабиринт (medium.txt):
      - A* начинает показывать преимущество
      - BFS исследует больше клеток
      - DFS может заблудиться в тупиках
    
    Большой лабиринт (large.txt):
      - A* значительно быстрее BFS
      - DFS сильно проигрывает на запутанных лабиринтах
    
    Пустой лабиринт (empty.txt):
      - A* значительно быстрее BFS
      - DFS быстро уходит вглубь, но путь неоптимальный
    
    Лабиринт без выхода (no_exit.txt):
      - Все алгоритмы обходят все достижимые клетки
      - Возвращают пустой путь
    """)
    
    print("""
    ВЫВОДЫ ПО ПАТТЕРНАМ:
    
    BUILDER:
      - Легко добавить новый формат
      - Код загрузки не смешивается с логикой лабиринта
    
    STRATEGY:
      - Алгоритмы можно менять во время выполнения
      - Легко добавить новый алгоритм
      - Код не дублируется
    
    OBSERVER:
      - Отделяет визуализацию от логики
      - Легко добавить GUI или логирование
      - Наблюдателей можно добавлять динамически
    
    COMMAND:
      - Позволяет выполнять и отменять действия
      - Удобно для пошагового управления
      - История команд позволяет сохранять/загружать состояние
    """)

def main():
    print("ЛАБОРАТОРНАЯ РАБОТА №2: ПОИСК ВЫХОДА ИЗ ЛАБИРИНТА")
    print("Паттерны: Builder, Strategy, Observer, Command")
    
    # Создание тестовых лабиринтов
    print("\n1. СОЗДАНИЕ ТЕСТОВЫХ ЛАБИРИНТОВ...")
    create_test_mazes()
    print("  Созданы лабиринты: tiny, medium, large, empty, no_exit")
    
    # Список файлов лабиринтов
    maze_files = [
        "mazes/tiny.txt",
        "mazes/medium.txt",
        "mazes/large.txt",
        "mazes/empty.txt",
        "mazes/no_exit.txt"
    ]
    
    strategies = [BFSStrategy(), DFSStrategy(), AStarStrategy()]
    all_results = []
    
    # Демонстрация Observer и Command на первом лабиринте
    print("\n2. ДЕМОНСТРАЦИЯ РАБОТЫ ПРОГРАММЫ")
    
    builder = TextFileMazeBuilder()
    maze = builder.build_from_file("mazes/tiny.txt")
    print("Лабиринт tiny.txt:")
    print(maze)
    
    logger = ConsoleLogger()
    solver_with_observer = MazeSolverWithObserver(maze, strategies[0], observers=[logger])
    path, _ = solver_with_observer.solve()
    interactive_move_demo(maze, path)
    
    # Эксперименты
    print("3. ЭКСПЕРИМЕНТАЛЬНОЕ СРАВНЕНИЕ АЛГОРИТМОВ")
    
    for maze_file in maze_files:
        try:
            results = test_single_maze(maze_file, strategies)
            for r in results:
                r['maze'] = maze_file
                all_results.append(r)
            print(f"\n{maze_file}:")
            for r in results:
                print(f"  {r['algorithm']}: {r['avg_time_ms']:.3f} мс, "
                      f"посещено {r['avg_visited']:.1f}, путь {r['avg_path_len']:.1f}")
        except Exception as e:
            print(f"Ошибка при обработке {maze_file}: {e}")
    
    # Сохранение CSV
    if all_results:
        os.makedirs("results", exist_ok=True)
        with open('results/all_results.csv', 'w', newline='', encoding='utf-8') as f:
            writer = csv.DictWriter(f, fieldnames=['maze', 'algorithm', 'avg_time_ms', 'avg_visited', 'avg_path_len'])
            writer.writeheader()
            writer.writerows(all_results)
        print("\nРезультаты сохранены в results/all_results.csv")
        
        # Построение графиков для каждого лабиринта
        df = pd.DataFrame(all_results)
        for maze in df['maze'].unique():
            subset = df[df['maze'] == maze]
            plt.figure(figsize=(8, 5))
            bars = plt.bar(subset['algorithm'], subset['avg_time_ms'], color=['blue', 'green', 'red'])
            plt.title(f'Сравнение алгоритмов на лабиринте {maze}')
            plt.ylabel('Среднее время (мс)')
            plt.xlabel('Алгоритм')
            for bar, val in zip(bars, subset['avg_time_ms']):
                plt.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5, 
                        f'{val:.3f}', ha='center', va='bottom', fontsize=9)
            plt.tight_layout()
            filename = f'results/plot_{maze.replace("/", "_")}.png'
            plt.savefig(filename)
            plt.close()
            print(f"  Сохранён график: {filename}")
        
        # Сводный график
        plt.figure(figsize=(12, 6))
        for alg in df['algorithm'].unique():
            subset = df[df['algorithm'] == alg]
            plt.plot(subset['maze'], subset['avg_time_ms'], marker='o', linewidth=2, markersize=8, label=alg)
        plt.xlabel('Лабиринт')
        plt.ylabel('Среднее время (мс)')
        plt.title('Сравнение эффективности алгоритмов на разных лабиринтах')
        plt.legend()
        plt.grid(True, alpha=0.3)
        plt.xticks(rotation=45)
        plt.tight_layout()
        plt.savefig('results/summary_comparison.png')
        plt.show()
        
        print("\nГрафики сохранены в папке results/")
        print("   - plot_*.png - графики для каждого лабиринта")
        print("   - summary_comparison.png - сводный график")
    
    print_analysis()
    
    print("ЭКСПЕРИМЕНТ ЗАВЕРШЁН")

if __name__ == "__main__":
    main()