import sys
import os
from collections import deque
import heapq
import time
import csv
import matplotlib.pyplot as plt
import numpy as np

# ---------- 1. Модель клетки и лабиринта ----------
class Tile:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.wall = False
        self.start = False
        self.exit = False

    def passable(self):
        return not self.wall


class Labyrinth:
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self._tiles = [[Tile(x, y) for x in range(width)] for y in range(height)]
        self.start_tile = None
        self.exit_tile = None

    def get_tile(self, x, y):
        if 0 <= x < self.width and 0 <= y < self.height:
            return self._tiles[y][x]
        return None

    def set_tile(self, x, y, kind):
        tile = self.get_tile(x, y)
        if tile is None:
            return
        if kind == 'wall':
            tile.wall = True
        elif kind == 'start':
            if self.start_tile:
                self.start_tile.start = False
            tile.start = True
            tile.wall = False
            self.start_tile = tile
        elif kind == 'exit':
            if self.exit_tile:
                self.exit_tile.exit = False
            tile.exit = True
            tile.wall = False
            self.exit_tile = tile
        elif kind == 'path':
            tile.wall = False

    def neighbours(self, tile):
        dirs = [(0, -1), (0, 1), (-1, 0), (1, 0)]
        result = []
        for dx, dy in dirs:
            nx, ny = tile.x + dx, tile.y + dy
            nb = self.get_tile(nx, ny)
            if nb and nb.passable():
                result.append(nb)
        return result


# ---------- 2. Загрузка лабиринта из файла ----------
class LabyrinthLoader:
    def load(self, filename):
        raise NotImplementedError


class TextLabyrinthLoader(LabyrinthLoader):
    def load(self, filename):
        with open(filename, 'r', encoding='utf-8') as f:
            lines = [line.rstrip('\n') for line in f]
        h = len(lines)
        w = max(len(line) for line in lines) if h > 0 else 0
        start_cnt = 0
        exit_cnt = 0
        lab = Labyrinth(w, h)

        for y, line in enumerate(lines):
            for x, ch in enumerate(line):
                if ch == '#':
                    lab.set_tile(x, y, 'wall')
                elif ch == 'S':
                    lab.set_tile(x, y, 'start')
                    start_cnt += 1
                elif ch == 'E':
                    lab.set_tile(x, y, 'exit')
                    exit_cnt += 1
                else:
                    lab.set_tile(x, y, 'path')
        if start_cnt != 1 or exit_cnt != 1:
            raise ValueError(f"Нужны ровно S и E, найдено S={start_cnt}, E={exit_cnt}")
        return lab


# ---------- 3. Алгоритмы поиска пути (стратегии) ----------
class Pathfinder:
    def find_path(self, lab, start, goal):
        raise NotImplementedError

    def _build_path(self, came_from, start, goal):
        path = []
        cur = goal
        while cur is not None:
            path.append(cur)
            cur = came_from.get(cur)
        path.reverse()
        return path

    def visited_count(self):
        return getattr(self, '_visited', 0)


class BFS(Pathfinder):
    def find_path(self, lab, start, goal):
        q = deque([start])
        parent = {start: None}
        visited = {start}
        while q:
            cur = q.popleft()
            if cur == goal:
                self._visited = len(visited)
                return self._build_path(parent, start, goal)
            for nb in lab.neighbours(cur):
                if nb not in visited:
                    visited.add(nb)
                    parent[nb] = cur
                    q.append(nb)
        self._visited = len(visited)
        return []


class DFS(Pathfinder):
    def find_path(self, lab, start, goal):
        stack = [start]
        parent = {start: None}
        visited = {start}
        while stack:
            cur = stack.pop()
            if cur == goal:
                self._visited = len(visited)
                return self._build_path(parent, start, goal)
            for nb in lab.neighbours(cur):
                if nb not in visited:
                    visited.add(nb)
                    parent[nb] = cur
                    stack.append(nb)
        self._visited = len(visited)
        return []


class AStar(Pathfinder):
    def _heuristic(self, a, b):
        return abs(a.x - b.x) + abs(a.y - b.y)

    def find_path(self, lab, start, goal):
        heap = []
        counter = 0
        start_f = self._heuristic(start, goal)
        heapq.heappush(heap, (start_f, counter, start))
        counter += 1
        parent = {}
        g = {start: 0}
        f = {start: start_f}
        visited = set()
        while heap:
            cur_f, _, cur = heapq.heappop(heap)
            visited.add(cur)
            if cur == goal:
                self._visited = len(visited)
                return self._build_path(parent, start, goal)
            if cur_f > f.get(cur, float('inf')):
                continue
            for nb in lab.neighbours(cur):
                new_g = g[cur] + 1
                if new_g < g.get(nb, float('inf')):
                    parent[nb] = cur
                    g[nb] = new_g
                    new_f = new_g + self._heuristic(nb, goal)
                    f[nb] = new_f
                    heapq.heappush(heap, (new_f, counter, nb))
                    counter += 1
        self._visited = len(visited)
        return []


# ---------- 4. Оркестратор с поддержкой наблюдателей ----------
class LabyrinthSolver:
    def __init__(self, lab):
        self.lab = lab
        self.strategy = None
        self.observers = []

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

    def notify(self, event, data):
        for obs in self.observers:
            obs.notify(event, data)

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

    def solve(self):
        if not self.strategy:
            return None
        t0 = time.perf_counter()
        path = self.strategy.find_path(self.lab, self.lab.start_tile, self.lab.exit_tile)
        t1 = time.perf_counter()
        self.notify("path_found", path)
        return {
            'time_ms': (t1 - t0) * 1000,
            'visited': self.strategy.visited_count(),
            'length': len(path)
        }


# ---------- 5. Визуализация (наблюдатель) ----------
class EventListener:
    def notify(self, event, data):
        raise NotImplementedError


class ConsoleRenderer(EventListener):
    def __init__(self, walker=None):
        self.last_path = None
        self.walker = walker

    def notify(self, event, data):
        if event == "maze_loaded":
            self._draw_maze(data)
        elif event == "path_found":
            self.last_path = data
            self._show_path_info(data)
        elif event == "player_moved":
            self._draw_maze_with_player(data)

    def _draw_maze(self, lab):
        os.system('cls' if os.name == 'nt' else 'clear')
        print("=" * (lab.width * 2 + 4))
        print("   ЛАБИРИНТ")
        print("=" * (lab.width * 2 + 4))
        for y in range(lab.height):
            print("  ", end='')
            for x in range(lab.width):
                t = lab.get_tile(x, y)
                if t == lab.start_tile:
                    print('S', end=' ')
                elif t == lab.exit_tile:
                    print('E', end=' ')
                elif t.wall:
                    print('#', end=' ')
                else:
                    print('.', end=' ')
            print()
        print("=" * (lab.width * 2 + 4))
        print("  S - старт  E - выход  # - стена  . - проход")

    def _draw_maze_with_player(self, lab):
        os.system('cls' if os.name == 'nt' else 'clear')
        print("=" * (lab.width * 2 + 4))
        print("   ЛАБИРИНТ (игрок = P)")
        print("=" * (lab.width * 2 + 4))
        for y in range(lab.height):
            print("  ", end='')
            for x in range(lab.width):
                t = lab.get_tile(x, y)
                if self.walker and t == self.walker.current:
                    print('P', end=' ')
                elif t == lab.start_tile:
                    print('S', end=' ')
                elif t == lab.exit_tile:
                    print('E', end=' ')
                elif t.wall:
                    print('#', end=' ')
                else:
                    print('.', end=' ')
            print()
        print("=" * (lab.width * 2 + 4))
        if self.walker:
            print(f"  Позиция игрока: ({self.walker.current.x}, {self.walker.current.y})")

    def _show_path_info(self, path):
        if not path:
            print("\n  Путь не найден!")
        else:
            print(f"\n  Путь найден! Длина = {len(path)}")


# ---------- 6. Игрок и команды с отменой ----------
class Walker:
    def __init__(self, start_tile, lab):
        self.current = start_tile
        self.prev = None
        self.lab = lab

    def move(self, target_tile):
        if target_tile and target_tile.passable():
            self.prev = self.current
            self.current = target_tile
            return True
        return False

    def undo(self):
        if self.prev:
            self.current, self.prev = self.prev, None
            return True
        return False


class Action:
    def do(self):
        raise NotImplementedError
    def undo(self):
        raise NotImplementedError


class MoveAction(Action):
    def __init__(self, walker, dx, dy, lab):
        self.walker = walker
        self.dx = dx
        self.dy = dy
        self.lab = lab
        self.done = False

    def do(self):
        new_x = self.walker.current.x + self.dx
        new_y = self.walker.current.y + self.dy
        target = self.lab.get_tile(new_x, new_y)
        if target and target.passable():
            self.walker.move(target)
            self.done = True
            return True
        return False

    def undo(self):
        if self.done:
            self.walker.undo()
            self.done = False
            return True
        return False


# ---------- 7. Экспериментальные функции ----------
def run_benchmark(maze_file, strategy, runs=5):
    loader = TextLabyrinthLoader()
    lab = loader.load(maze_file)
    total_time = 0.0
    total_visited = 0
    total_len = 0
    for _ in range(runs):
        solver = LabyrinthSolver(lab)
        solver.set_strategy(strategy)
        stats = solver.solve()
        if stats:
            total_time += stats['time_ms']
            total_visited += stats['visited']
            total_len += stats['length']
    return {
        'time_ms': total_time / runs,
        'visited': total_visited / runs,
        'length': total_len / runs
    }


def make_plots(results):
    mazes = list({r['maze'] for r in results})
    algos = ['BFS', 'DFS', 'AStar']
    fig, axes = plt.subplots(1, 3, figsize=(15, 5))
    x = np.arange(len(mazes))
    width = 0.25

    for i, algo in enumerate(algos):
        times = []
        for m in mazes:
            val = next((r['time_ms'] for r in results if r['maze'] == m and r['algo'] == algo), 0)
            times.append(val)
        axes[0].bar(x + i*width, times, width, label=algo)
    axes[0].set_xlabel('Лабиринт')
    axes[0].set_ylabel('Время (мс)')
    axes[0].set_title('Сравнение времени выполнения')
    axes[0].set_xticks(x + width)
    axes[0].set_xticklabels(mazes, rotation=45, ha='right')
    axes[0].legend()
    axes[0].grid(True, alpha=0.3)

    for i, algo in enumerate(algos):
        visited_vals = []
        for m in mazes:
            val = next((r['visited'] for r in results if r['maze'] == m and r['algo'] == algo), 0)
            visited_vals.append(val)
        axes[1].bar(x + i*width, visited_vals, width, label=algo)
    axes[1].set_xlabel('Лабиринт')
    axes[1].set_ylabel('Посещено клеток')
    axes[1].set_title('Сравнение посещённых клеток')
    axes[1].set_xticks(x + width)
    axes[1].set_xticklabels(mazes, rotation=45, ha='right')
    axes[1].legend()
    axes[1].grid(True, alpha=0.3)

    for i, algo in enumerate(algos):
        lengths = []
        for m in mazes:
            val = next((r['length'] for r in results if r['maze'] == m and r['algo'] == algo), 0)
            lengths.append(val)
        axes[2].bar(x + i*width, lengths, width, label=algo)
    axes[2].set_xlabel('Лабиринт')
    axes[2].set_ylabel('Длина пути')
    axes[2].set_title('Сравнение длины пути')
    axes[2].set_xticks(x + width)
    axes[2].set_xticklabels(mazes, rotation=45, ha='right')
    axes[2].legend()
    axes[2].grid(True, alpha=0.3)

    plt.tight_layout()
    plt.savefig('performance_comparison.png', dpi=150, bbox_inches='tight')
    plt.show()


# ---------- 8. Главный блок ----------
if __name__ == "__main__":
    if len(sys.argv) > 1 and sys.argv[1] == 'experiment':
        # Режим экспериментов
        test_mazes = [
            ("maze/maze1.txt", "Small 10x6"),
            ("maze/maze10x10.txt", "Medium 10x10"),
            ("maze/maze20x20.txt", "Large 20x20"),
            ("maze/maze_empty.txt", "Empty 15x15"),
            ("maze/maze_no_exit.txt", "No exit 10x10")
        ]
        strategies = [
            ("BFS", BFS()),
            ("DFS", DFS()),
            ("AStar", AStar())
        ]
        results = []
        for maze_path, maze_name in test_mazes:
            print(f"Тестируем {maze_name}...")
            for algo_name, algo in strategies:
                try:
                    stats = run_benchmark(maze_path, algo, runs=3)
                    results.append({
                        'maze': maze_name,
                        'algo': algo_name,
                        'time_ms': stats['time_ms'],
                        'visited': stats['visited'],
                        'length': stats['length']
                    })
                    print(f"  {algo_name}: время={stats['time_ms']:.3f}мс, посещено={stats['visited']:.0f}, длина={stats['length']:.0f}")
                except Exception as e:
                    print(f"  {algo_name}: ошибка - {e}")
        # Сохраняем CSV
        with open('experiment_results.csv', 'w', newline='', encoding='utf-8') as f:
            writer = csv.DictWriter(f, fieldnames=['maze', 'algo', 'time_ms', 'visited', 'length'])
            writer.writeheader()
            writer.writerows(results)
        # Строим графики
        if results:
            make_plots(results)
        print("\nРезультаты сохранены в experiment_results.csv и performance_comparison.png")
    else:
        # Интерактивный режим
        loader = TextLabyrinthLoader()
        lab = loader.load("maze/maze1.txt")

        player = Walker(lab.start_tile, lab)
        view = ConsoleRenderer(player)
        view.notify("maze_loaded", lab)

        solver = LabyrinthSolver(lab)
        solver.attach(view)

        print("\n  УПРАВЛЕНИЕ:")
        print("  H (влево)  J (вниз)  K (вверх)  L (вправо)")
        print("  U - отменить ход    Q - выход")
        print("\n  АВТОПОИСК:")
        print("  B - BFS    D - DFS    A - A*")
        print("\n" + "=" * 50)

        command_stack = []
        while True:
            key = input("\n  Команда > ").lower()
            if key == 'q':
                print("\n  До свидания!")
                break
            elif key == 'b':
                solver.set_strategy(BFS())
                stats = solver.solve()
                if stats:
                    print(f"\n  BFS: время={stats['time_ms']:.3f}мс, посещено={stats['visited']}, длина={stats['length']}")
            elif key == 'd':
                solver.set_strategy(DFS())
                stats = solver.solve()
                print(f"\n  DFS: время={stats['time_ms']:.3f}мс, посещено={stats['visited']}, длина={stats['length']}")
            elif key == 'a':
                solver.set_strategy(AStar())
                stats = solver.solve()
                print(f"\n  A*: время={stats['time_ms']:.3f}мс, посещено={stats['visited']}, длина={stats['length']}")
            elif key in ['h', 'j', 'k', 'l']:
                dirs = {'h': (-1,0), 'l': (1,0), 'k': (0,-1), 'j': (0,1)}
                act = MoveAction(player, dirs[key][0], dirs[key][1], lab)
                if act.do():
                    command_stack.append(act)
                    view.notify("player_moved", lab)
                    if player.current == lab.exit_tile:
                        print("\n  ПОЗДРАВЛЯЮ! ВЫ НАШЛИ ВЫХОД!")
                        print(f"  Сделано ходов: {len(command_stack)}")
                        break
                else:
                    print("\n  Туда нельзя – стена!")
            elif key == 'u':
                if command_stack:
                    cmd = command_stack.pop()
                    cmd.undo()
                    view.notify("player_moved", lab)
                    print("\n  Отмена последнего хода")
                else:
                    print("\n  Нечего отменять")
            else:
                print("\n  Неизвестная команда. Используйте h,j,k,l, u, b, d, a, q")

        print("\n  Игра окончена. Спасибо!")