import heapq
import time
import os
import csv
from collections import deque
from abc import ABC, abstractmethod
import matplotlib.pyplot as plt
import numpy as np

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.grid = [[Cell(x, y, True) for y in range(height)] for x in range(width)]
        self.start = None
        self.exit = None

    def get_cell(self, x, y):
        if 0 <= x < self.width and 0 <= y < self.height:
            return self.grid[x][y]
        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 MazeBuilder(ABC):
    @abstractmethod
    def build_from_file(self, filename):
        pass

class TextFileMazeBuilder(MazeBuilder):
    def build_from_file(self, filename):
        with open(filename, 'r') as f:
            lines = [line.rstrip('\n') for line in f.readlines()]
        height = len(lines)
        width = max(len(line) for line in lines) if height > 0 else 0
        maze = Maze(width, height)
        for y, line in enumerate(lines):
            for x, char in enumerate(line):
                if char == '#':
                    maze.grid[x][y] = Cell(x, y, True)
                else:
                    cell = Cell(x, y, False)
                    if char == 'S':
                        cell.is_start = True
                        maze.start = cell
                    elif char == 'E':
                        cell.is_exit = True
                        maze.exit = cell
                    maze.grid[x][y] = cell
        if not maze.start or not maze.exit:
            raise ValueError("Лабиринт должен содержать старт (S) и выход (E)")
        return maze

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

class BFSPathFinding(PathFindingStrategy):
    def find_path(self, maze, start, exit_cell):
        queue = deque([start])
        visited = {start: None}
        visited_count = 0
        while queue:
            current = queue.popleft()
            visited_count += 1
            if exit_cell is not None and current == exit_cell:
                path = []
                while current:
                    path.append(current)
                    current = visited[current]
                return path[::-1], visited_count
            for neighbor in maze.get_neighbors(current):
                if neighbor not in visited:
                    visited[neighbor] = current
                    queue.append(neighbor)
        return [], visited_count

class DFSPathFinding(PathFindingStrategy):
    def find_path(self, maze, start, exit_cell):
        stack = [start]
        visited = {start: None}
        visited_count = 0
        while stack:
            current = stack.pop()
            visited_count += 1
            if exit_cell is not None and current == exit_cell:
                path = []
                while current:
                    path.append(current)
                    current = visited[current]
                return path[::-1], visited_count
            for neighbor in maze.get_neighbors(current):
                if neighbor not in visited:
                    visited[neighbor] = current
                    stack.append(neighbor)
        return [], visited_count

class AStarPathFinding(PathFindingStrategy):
    def heuristic(self, a, b):
        if b is None:
            return 0
        return abs(a.x - b.x) + abs(a.y - b.y)

    def find_path(self, maze, start, exit_cell):
        open_set = [(0, 0, start, None)]
        heapq.heapify(open_set)
        g_score = {start: 0}
        came_from = {}
        visited_count = 0
        while open_set:
            _, _, current, parent = heapq.heappop(open_set)
            if current in came_from:
                continue
            visited_count += 1
            came_from[current] = parent
            if exit_cell is not None and current == exit_cell:
                path = []
                while current:
                    path.append(current)
                    current = came_from[current]
                return path[::-1], visited_count
            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_score = tentative_g + self.heuristic(neighbor, exit_cell)
                    heapq.heappush(open_set, (f_score, id(neighbor), neighbor, current))
        return [], visited_count


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

class Observer(ABC):
    @abstractmethod
    def update(self, event):
        pass

class ConsoleView(Observer):
    def update(self, event):
        if event['type'] == 'path_found':
            self.render(event['maze'], event.get('player_pos'), event['path'])
        elif event['type'] == 'maze_loaded':
            print(f"Лабиринт загружен: {event['maze'].width}x{event['maze'].height}")
        elif event['type'] == 'search_start':
            print(f"Поиск пути алгоритмом {event['strategy']}...")
        elif event['type'] == 'search_end':
            print(
                f"Путь найден: длина {event['stats'].path_length}, посещено клеток {event['stats'].visited_count}, время {event['stats'].time_ms:.3f}мс")

    def render(self, maze, player_pos=None, path=None):
        os.system('cls' if os.name == 'nt' else 'clear')
        path_set = set((c.x, c.y) for c in path) if path else set()
        for y in range(maze.height):
            for x in range(maze.width):
                cell = maze.get_cell(x, y)
                if player_pos and (x, y) == (player_pos.x, player_pos.y):
                    print('P', end='')
                elif cell.is_start:
                    print('S', end='')
                elif cell.is_exit:
                    print('E', end='')
                elif (x, y) in path_set:
                    print('.', end='')
                elif cell.is_wall:
                    print('#', end='')
                else:
                    print(' ', end='')
            print()

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

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

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

    def notify(self, event):
        for observer in self.observers:
            observer.update(event)

    def solve(self):
        if not self.strategy:
            raise ValueError("Стратегия не задана")
        self.notify({'type': 'search_start', 'strategy': type(self.strategy).__name__})
        start_time = time.perf_counter()
        if self.maze.exit is None:
            path, visited = self.strategy.find_path(self.maze, self.maze.start, None)
        else:
            path, visited = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit)
        end_time = time.perf_counter()
        time_ms = (end_time - start_time) * 1000
        stats = SearchStats(path, visited, time_ms)
        self.notify({'type': 'search_end', 'stats': stats, 'strategy': type(self.strategy).__name__})
        self.notify({'type': 'path_found', 'maze': self.maze, 'path': path})
        return stats

def is_path_exists(maze):
    if maze.exit is None:
        return False
    queue = deque([maze.start])
    visited = {maze.start}
    while queue:
        current = queue.popleft()
        if current == maze.exit:
            return True
        for neighbor in maze.get_neighbors(current):
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append(neighbor)
    return False

def generate_maze(width, height, wall_density=0.3, seed=42):
    np.random.seed(seed)
    maze = Maze(width, height)
    for x in range(width):
        for y in range(height):
            if x == 0 or x == width - 1 or y == 0 or y == height - 1:
                maze.grid[x][y] = Cell(x, y, True)
            else:
                is_wall = np.random.random() < wall_density
                maze.grid[x][y] = Cell(x, y, is_wall)
    maze.start = maze.get_cell(1, 1)
    maze.start.is_wall = False
    maze.start.is_start = True
    maze.grid[1][1] = maze.start
    maze.grid[1][2] = Cell(1, 2, False)
    maze.grid[2][1] = Cell(2, 1, False)
    maze.exit = maze.get_cell(width - 2, height - 2)
    maze.exit.is_wall = False
    maze.exit.is_exit = True
    maze.grid[width - 2][height - 3] = Cell(width - 2, height - 3, False)
    maze.grid[width - 3][height - 2] = Cell(width - 3, height - 2, False)
    if not is_path_exists(maze):
        for x in range(1, width - 1):
            for y in range(1, height - 1):
                if np.random.random() < 0.5:
                    maze.grid[x][y].is_wall = False
        if not is_path_exists(maze):
            for x in range(1, width - 1):
                for y in range(1, height - 1):
                    if x == 1 and y == 1:
                        continue
                    if x == width - 2 and y == height - 2:
                        continue
                    maze.grid[x][y].is_wall = False
    return maze

def generate_empty_maze(width, height):
    maze = Maze(width, height)
    for x in range(width):
        for y in range(height):
            maze.grid[x][y] = Cell(x, y, False)
    maze.start = maze.get_cell(0, 0)
    maze.start.is_start = True
    maze.exit = maze.get_cell(width - 1, height - 1)
    maze.exit.is_exit = True
    return maze

def generate_no_exit_maze(width, height):
    maze = Maze(width, height)
    np.random.seed(123)
    for x in range(width):
        for y in range(height):
            if x == 0 or x == width - 1 or y == 0 or y == height - 1:
                maze.grid[x][y] = Cell(x, y, True)
            else:
                is_wall = np.random.random() < 0.3
                maze.grid[x][y] = Cell(x, y, is_wall)
    maze.start = maze.get_cell(1, 1)
    maze.start.is_wall = False
    maze.start.is_start = True
    maze.grid[1][1] = maze.start
    maze.grid[1][2] = Cell(1, 2, False)
    maze.grid[2][1] = Cell(2, 1, False)
    maze.exit = None
    return maze

def save_maze_to_file(maze, filename):
    with open(filename, 'w') as f:
        for y in range(maze.height):
            for x in range(maze.width):
                cell = maze.get_cell(x, y)
                if cell.is_start:
                    f.write('S')
                elif cell.is_exit:
                    f.write('E')
                elif cell.is_wall:
                    f.write('#')
                else:
                    f.write(' ')
            f.write('\n')

def visualize_maze(maze, path=None, title="Лабиринт", ax=None):
    grid = np.zeros((maze.height, maze.width))
    for y in range(maze.height):
        for x in range(maze.width):
            cell = maze.get_cell(x, y)
            if cell.is_wall:
                grid[y, x] = 1
            elif cell.is_start:
                grid[y, x] = 2
            elif cell.is_exit:
                grid[y, x] = 3
    if ax is None:
        fig, ax = plt.subplots(figsize=(8, 8))
    cmap = plt.cm.colors.ListedColormap(['white', 'black', 'green', 'red'])
    ax.imshow(grid, cmap=cmap, interpolation='nearest')
    if path:
        path_x = [cell.x for cell in path]
        path_y = [cell.y for cell in path]
        ax.plot(path_x, path_y, 'b-', linewidth=2, label='Путь')
    ax.set_title(title)
    ax.set_xticks([])
    ax.set_yticks([])

def run_experiments():
    mazes_data = {
        "Маленький (10x10)": generate_maze(10, 10, 0.2, 10),
        "Средний (50x50)": generate_maze(50, 50, 0.3, 20),
        "Большой (100x100)": generate_maze(100, 100, 0.3, 30),
        "Пустой (50x50)": generate_empty_maze(50, 50),
        "Без выхода (50x50)": generate_no_exit_maze(50, 50)
    }
    os.makedirs("mazes", exist_ok=True)
    for name, maze in mazes_data.items():
        filename = f"mazes/{name.replace(' ', '_').replace('(', '').replace(')', '')}.txt"
        save_maze_to_file(maze, filename)
        print(f"Сохранён {filename}")
    strategies = {
        "BFS": BFSPathFinding(),
        "DFS": DFSPathFinding(),
        "A*": AStarPathFinding()
    }
    results = []
    runs = 5
    fig_mazes, axes_mazes = plt.subplots(len(mazes_data), len(strategies) + 1, figsize=(18, 4 * len(mazes_data)))
    if len(mazes_data) == 1:
        axes_mazes = [axes_mazes]
    for row_idx, (maze_name, maze) in enumerate(mazes_data.items()):
        visualize_maze(maze, title=f"{maze_name}", ax=axes_mazes[row_idx][0])
        for col_idx, (strat_name, strategy) in enumerate(strategies.items()):
            solver = MazeSolver(maze, strategy)
            times = []
            visited_counts = []
            path_lengths = []
            best_path = None
            for _ in range(runs):
                stats = solver.solve()
                times.append(stats.time_ms)
                visited_counts.append(stats.visited_count)
                path_lengths.append(stats.path_length)
                if stats.path:
                    best_path = stats.path
            avg_time = np.mean(times)
            avg_visited = np.mean(visited_counts)
            avg_path = np.mean(path_lengths)
            results.append([maze_name, strat_name, avg_time, avg_visited, avg_path])
            print(f"{maze_name} - {strat_name}: Время={avg_time:.3f}мс, Посещено={avg_visited:.0f}, Длина пути={avg_path:.0f}")
            visualize_maze(maze, best_path, f"{maze_name} - {strat_name}", ax=axes_mazes[row_idx][col_idx + 1])
    plt.tight_layout()
    plt.savefig('mazes_visualization.png')
    plt.close()
    with open('results.csv', 'w', newline='', encoding='utf-8-sig') as f:
        writer = csv.writer(f)
        writer.writerow(["Лабиринт", "Стратегия", "Время_мс", "Посещено", "Длина_пути"])
        writer.writerows(results)
    print("\nРезультаты сохранены в results.csv")
    return results

def plot_results(results):
    strategies = ["BFS", "DFS", "A*"]
    mazes = ["Маленький (10x10)", "Средний (50x50)", "Большой (100x100)", "Пустой (50x50)", "Без выхода (50x50)"]
    data = {}
    for strat in strategies:
        data[strat] = {"times": [], "visited": [], "paths": []}
    for row in results:
        maze, strat, time_ms, visited, path_len = row
        data[strat]["times"].append(time_ms)
        data[strat]["visited"].append(visited)
        data[strat]["paths"].append(path_len)

    fig, axes = plt.subplots(1, 3, figsize=(18, 6))
    x = np.arange(len(mazes))
    width = 0.25
    colors = {'BFS': 'skyblue', 'DFS': 'lightgreen', 'A*': 'salmon'}

    for i, strat in enumerate(strategies):
        offset = (i - 1) * width
        times_display = [t if t > 0 else 0.001 for t in data[strat]["times"]]
        bars = axes[0].bar(x + offset, times_display, width, label=strat, color=colors[strat])
        for bar, val in zip(bars, data[strat]["times"]):
            if val > 0:
                axes[0].text(bar.get_x() + bar.get_width() / 2, bar.get_height() * 1.1,
                             f'{val:.2f}', ha='center', va='bottom', fontsize=8, rotation=90)
    axes[0].set_title("Время выполнения (мс)")
    axes[0].set_xticks(x)
    axes[0].set_xticklabels(mazes, rotation=15, ha='right')
    axes[0].set_ylabel("Время (мс)")
    axes[0].set_yscale('log')
    axes[0].legend()
    axes[0].grid(axis='y', alpha=0.3)

    for i, strat in enumerate(strategies):
        offset = (i - 1) * width
        visited_display = [v if v > 0 else 1 for v in data[strat]["visited"]]
        bars = axes[1].bar(x + offset, visited_display, width, label=strat, color=colors[strat])
        for bar, val in zip(bars, data[strat]["visited"]):
            if val > 0:
                axes[1].text(bar.get_x() + bar.get_width() / 2, bar.get_height() * 1.1,
                             f'{val:.0f}', ha='center', va='bottom', fontsize=8, rotation=90)
    axes[1].set_title("Посещено клеток")
    axes[1].set_xticks(x)
    axes[1].set_xticklabels(mazes, rotation=15, ha='right')
    axes[1].set_ylabel("Количество клеток")
    axes[1].set_yscale('log')
    axes[1].legend()
    axes[1].grid(axis='y', alpha=0.3)

    for i, strat in enumerate(strategies):
        offset = (i - 1) * width
        paths_display = [p if p > 0 else 1 for p in data[strat]["paths"]]
        bars = axes[2].bar(x + offset, paths_display, width, label=strat, color=colors[strat])
        for bar, val in zip(bars, data[strat]["paths"]):
            height = bar.get_height()
            axes[2].text(bar.get_x() + bar.get_width() / 2, height * 1.1,
                         f'{val:.0f}', ha='center', va='bottom', fontsize=8, rotation=90)
    axes[2].set_title("Длина найденного пути")
    axes[2].set_xticks(x)
    axes[2].set_xticklabels(mazes, rotation=15, ha='right')
    axes[2].set_ylabel("Длина пути")
    axes[2].set_yscale('log')
    axes[2].legend()
    axes[2].grid(axis='y', alpha=0.3)

    plt.tight_layout()
    plt.savefig('comparative_results.png')
    plt.show()
    print("Сравнительные графики сохранены в comparative_results.png")

if __name__ == "__main__":
    print("\nГенерация лабиринтов и запуск экспериментов\n")
    results = run_experiments()
    print("\nСоздание графиков")
    plot_results(results)
    print("\nЭксперименты завершены")
    print("\nСозданные файлы:")
    print("  - 5 текстовых файлов с лабиринтами")
    print("  - mazes_visualization.png: Визуализация всех лабиринтов с путями")
    print("  - results.csv: Таблица с числовыми результатами")
    print("  - comparative_results.png: Сравнительные графики (Время, Посещено, Длина пути)")
    print("\nСводка результатов:")
    for row in results:
        maze, strat, time_ms, visited, path_len = row
        print(f"{maze:20s} | {strat:5s} | Время: {time_ms:8.3f}мс | Посещено: {visited:6.0f} | Путь: {path_len:4.0f}")