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

# ========== Модель данных ==========
class Tile:
    """Одна клетка лабиринта."""
    def __init__(self, x, y):
        self._x = x
        self._y = y
        self._wall = False
        self._entry = False
        self._goal = False

    @property
    def x(self): return self._x
    @property
    def y(self): return self._y
    @property
    def is_wall(self): return self._wall
    @is_wall.setter
    def is_wall(self, value): self._wall = value
    @property
    def is_entry(self): return self._entry
    @is_entry.setter
    def is_entry(self, value): self._entry = value
    @property
    def is_goal(self): return self._goal
    @is_goal.setter
    def is_goal(self, value): self._goal = value

    def can_walk(self):
        """Можно ли встать на эту клетку."""
        return not self._wall


class Labyrinth:
    """Прямоугольный лабиринт."""
    def __init__(self, width, height):
        self._width = width
        self._height = height
        self._grid = [[Tile(x, y) for x in range(width)] for y in range(height)]
        self._start = None
        self._exit = None

    @property
    def width(self): return self._width
    @property
    def height(self): return self._height
    @property
    def start(self): return self._start
    @property
    def exit(self): return self._exit

    def tile_at(self, x, y):
        if 0 <= x < self._width and 0 <= y < self._height:
            return self._grid[y][x]
        return None

    def configure_tile(self, x, y, kind):
        tile = self.tile_at(x, y)
        if tile is None:
            return
        if kind == 'wall':
            tile.is_wall = True
        elif kind == 'entry':
            if self._start:
                self._start.is_entry = False
            tile.is_entry = True
            tile.is_wall = False
            self._start = tile
        elif kind == 'goal':
            if self._exit:
                self._exit.is_goal = False
            tile.is_goal = True
            tile.is_wall = False
            self._exit = tile
        elif kind == 'floor':
            tile.is_wall = False

    def neighbours(self, tile):
        """Соседние проходимые клетки."""
        res = []
        for dx, dy in ((0, -1), (0, 1), (-1, 0), (1, 0)):
            nb = self.tile_at(tile.x + dx, tile.y + dy)
            if nb and nb.can_walk():
                res.append(nb)
        return res


# ========== Загрузка из файла ==========
class LabyrinthBuilder:
    def build(self, filename):
        raise NotImplementedError

class TextLabyrinthBuilder(LabyrinthBuilder):
    def build(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(l) for l in lines) if h else 0
        if h == 0 or w == 0:
            raise ValueError("Файл лабиринта пуст.")
        entries = exits = 0
        lab = Labyrinth(w, h)
        for y, row in enumerate(lines):
            for x, ch in enumerate(row):
                if ch == '#':
                    lab.configure_tile(x, y, 'wall')
                elif ch == 'S':
                    lab.configure_tile(x, y, 'entry')
                    entries += 1
                elif ch == 'E':
                    lab.configure_tile(x, y, 'goal')
                    exits += 1
                else:
                    lab.configure_tile(x, y, 'floor')
        if entries != 1 or exits != 1:
            raise ValueError(f"Некорректный лабиринт: найдено S={entries}, E={exits}")
        return lab


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

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

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


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


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


class AStar_Pathfinder(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 = []
        cnt = 0
        f_start = self._heuristic(start, goal)
        heapq.heappush(heap, (f_start, cnt, start))
        cnt += 1
        preds = {}
        g = {start: 0}
        f = {start: f_start}
        seen = set()
        while heap:
            cur_f, _, cur = heapq.heappop(heap)
            seen.add(cur)
            if cur == goal:
                self._visited = len(seen)
                return self._build_path(preds, start, goal)
            if cur_f > f.get(cur, float('inf')):
                continue
            for nb in lab.neighbours(cur):
                tent_g = g[cur] + 1
                if tent_g < g.get(nb, float('inf')):
                    preds[nb] = cur
                    g[nb] = tent_g
                    new_f = tent_g + self._heuristic(nb, goal)
                    f[nb] = new_f
                    heapq.heappush(heap, (new_f, cnt, nb))
                    cnt += 1
        self._visited = len(seen)
        return []


# ========== Интерактивный игрок ==========
class Explorer:
    def __init__(self, start_tile, labyrinth):
        self._current = start_tile
        self._previous = None
        self._lab = labyrinth

    @property
    def current(self):
        return self._current

    def move(self, tile):
        if tile and tile.can_walk():
            self._previous = self._current
            self._current = tile
            return True
        return False

    def undo(self):
        if self._previous:
            self._current, self._previous = self._previous, None
            return True
        return False


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


class MoveAction(Action):
    def __init__(self, explorer, direction, lab):
        self._explorer = explorer
        self._dx, self._dy = direction
        self._lab = lab
        self._done = False

    def execute(self):
        nx = self._explorer.current.x + self._dx
        ny = self._explorer.current.y + self._dy
        target = self._lab.tile_at(nx, ny)
        if target and target.can_walk():
            self._explorer.move(target)
            self._done = True
            return True
        return False

    def undo(self):
        if self._done:
            self._explorer.undo()
            self._done = False
            return True
        return False


class GameObserver:
    def update(self, event, data): raise NotImplementedError


class TerminalDisplay(GameObserver):
    def __init__(self, explorer=None):
        self._explorer = explorer
        self._last_path = None

    def update(self, event, data):
        if event == 'labyrinth_loaded':
            self._draw_lab(data)
        elif event == 'path_found':
            self._last_path = data
            self._show_path_info(data)
        elif event == 'player_moved':
            self._draw_with_player(data)

    def _draw_lab(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.tile_at(x, y)
                if t == lab.start: print('S', end=' ')
                elif t == lab.exit: print('E', end=' ')
                elif t.is_wall: print('#', end=' ')
                else: print('.', end=' ')
            print()
        print('=' * (lab.width * 2 + 4))
        print('  S – вход   E – выход   # – стена   . – пол')

    def _draw_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.tile_at(x, y)
                if self._explorer and t == self._explorer.current:
                    print('P', end=' ')
                elif t == lab.start: print('S', end=' ')
                elif t == lab.exit: print('E', end=' ')
                elif t.is_wall: print('#', end=' ')
                else: print('.', end=' ')
            print()
        print('=' * (lab.width * 2 + 4))
        if self._explorer:
            print(f'  Позиция: ({self._explorer.current.x}, {self._explorer.current.y})')

    def _show_path_info(self, path):
        if not path:
            print('\n  Путь не найден!')
        else:
            print(f'\n  Длина найденного пути: {len(path)} клеток.')


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.update(event, data)

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

    def solve(self):
        if self._strategy is None:
            return None
        start_t = time.perf_counter()
        path = self._strategy.find_path(self._lab, self._lab.start, self._lab.exit)
        elapsed = (time.perf_counter() - start_t) * 1000
        self._notify('path_found', path)
        return {
            'time_ms': elapsed,
            'visited': self._strategy.visited_count,
            'length': len(path)
        }


# ========== Эксперименты и визуализация ==========
def run_benchmark(maze_file, strategy, runs=5):
    builder = TextLabyrinthBuilder()
    lab = builder.build(maze_file)
    total_t = total_v = total_l = 0
    for _ in range(runs):
        solver = LabyrinthSolver(lab)
        solver.set_strategy(strategy)
        stats = solver.solve()
        if stats:
            total_t += stats['time_ms']
            total_v += stats['visited']
            total_l += stats['length']
    return {
        'time_ms': total_t / runs,
        'visited_cells': total_v / runs,
        'path_length': total_l / runs
    }


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

    for i, strat in enumerate(strategies):
        times = [next((r['time_ms'] for r in results if r['maze'] == m and r['strategy'] == strat), 0) for m in mazes]
        axes[0].bar(x + i*width, times, width, label=strat)
    axes[0].set_title('Время выполнения (мс)')
    axes[0].set_xticks(x + width)
    axes[0].set_xticklabels(mazes, rotation=30, ha='right')
    axes[0].legend()
    axes[0].grid(alpha=0.3)

    for i, strat in enumerate(strategies):
        visited = [next((r['visited_cells'] for r in results if r['maze'] == m and r['strategy'] == strat), 0) for m in mazes]
        axes[1].bar(x + i*width, visited, width, label=strat)
    axes[1].set_title('Посещено клеток')
    axes[1].set_xticks(x + width)
    axes[1].set_xticklabels(mazes, rotation=30, ha='right')
    axes[1].legend()
    axes[1].grid(alpha=0.3)

    for i, strat in enumerate(strategies):
        lengths = [next((r['path_length'] for r in results if r['maze'] == m and r['strategy'] == strat), 0) for m in mazes]
        axes[2].bar(x + i*width, lengths, width, label=strat)
    axes[2].set_title('Длина пути')
    axes[2].set_xticks(x + width)
    axes[2].set_xticklabels(mazes, rotation=30, ha='right')
    axes[2].legend()
    axes[2].grid(alpha=0.3)

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


# ========== Главный вход ==========
if __name__ == '__main__':
    if len(sys.argv) > 1 and sys.argv[1] == 'experiment':
        print('Запуск экспериментов...')
        maze_list = [
            ('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_Pathfinder()),
            ('DFS', DFS_Pathfinder()),
            ('AStar', AStar_Pathfinder())
        ]
        all_results = []
        for file, name in maze_list:
            print(f'\nТестируем {name}...')
            for sname, strat in strategies:
                try:
                    stats = run_benchmark(file, strat, runs=3)
                    all_results.append({
                        'maze': name,
                        'strategy': sname,
                        'time_ms': stats['time_ms'],
                        'visited_cells': stats['visited_cells'],
                        'path_length': stats['path_length']
                    })
                    print(f'  {sname}: время={stats["time_ms"]:.3f}мс, клеток={stats["visited_cells"]:.0f}, длина={stats["path_length"]:.0f}')
                except Exception as e:
                    print(f'  {sname}: ошибка – {e}')
        with open('experiment_data.csv', 'w', newline='', encoding='utf-8') as f:
            writer = csv.DictWriter(f, fieldnames=['maze','strategy','time_ms','visited_cells','path_length'])
            writer.writeheader()
            writer.writerows(all_results)
        if all_results:
            create_charts(all_results)
        print('\nРезультаты сохранены в experiment_data.csv и maze_performance.png')
    else:
        # Интерактивная игра
        maze_file = 'maze/maze1.txt'
        if len(sys.argv) > 1:
            maze_file = sys.argv[1]
        builder = TextLabyrinthBuilder()
        labyrinth = builder.build(maze_file)

        player = Explorer(labyrinth.start, labyrinth)
        display = TerminalDisplay(player)
        display.update('labyrinth_loaded', labyrinth)

        solver = LabyrinthSolver(labyrinth)
        solver.attach(display)

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

        history = []
        while True:
            cmd = input('\n  Команда > ').lower().strip()
            if cmd == 'q':
                print('\n  До свидания!')
                break
            elif cmd == 'b':
                solver.set_strategy(BFS_Pathfinder())
                stats = solver.solve()
                print(f"\n  BFS: время={stats['time_ms']:.3f}мс, посещено={stats['visited']}, длина={stats['length']}")
            elif cmd == 'd':
                solver.set_strategy(DFS_Pathfinder())
                stats = solver.solve()
                print(f"\n  DFS: время={stats['time_ms']:.3f}мс, посещено={stats['visited']}, длина={stats['length']}")
            elif cmd == 'a':
                solver.set_strategy(AStar_Pathfinder())
                stats = solver.solve()
                print(f"\n  A*: время={stats['time_ms']:.3f}мс, посещено={stats['visited']}, длина={stats['length']}")
            elif cmd in ('h','j','k','l'):
                dirs = {'h': (-1,0), 'l': (1,0), 'k': (0,-1), 'j': (0,1)}
                action = MoveAction(player, dirs[cmd], labyrinth)
                if action.execute():
                    history.append(action)
                    display.update('player_moved', labyrinth)
                    if player.current == labyrinth.exit:
                        print('\n  ПОЗДРАВЛЯЕМ! ВЫ ВЫБРАЛИСЬ ИЗ ЛАБИРИНТА!')
                        print(f'  Всего ходов: {len(history)}')
                        break
                else:
                    print('\n  Там стена!')
            elif cmd == 'u':
                if history:
                    act = history.pop()
                    act.undo()
                    display.update('player_moved', labyrinth)
                    print('\n  Ход отменён')
                else:
                    print('\n  Нечего отменять')
            else:
                print('\n  Неизвестная команда. Используйте H,J,K,L,U,Q,B,D,A')