import os
import random
import time
import csv
from typing import Dict, List

import matplotlib.pyplot as plt
import numpy as np

from maze import Maze, Cell
from maze_builder import RandomMazeBuilder
from pathfinding import BFSStrategy, DFSStrategy, AStarStrategy, DijkstraStrategy
from maze_solver import MazeSolver, SearchStats


def create_test_mazes() -> Dict[str, Maze]:
    """Создаёт тестовые лабиринты разной сложности."""
    mazes = {}
    
    # 1. Маленький 10×10 с простым путём
    small = Maze(10, 10)
    for y in range(10):
        for x in range(10):
            is_wall = (x == 0 or x == 9 or y == 0 or y == 9 or 
                      (x == 3 and y < 7) or (x == 6 and y > 2))
            is_start = (x == 1 and y == 1)
            is_exit = (x == 8 and y == 8)
            small.set_cell(x, y, Cell(x, y, is_wall=is_wall, 
                                      is_start=is_start, is_exit=is_exit))
    mazes['small_10x10'] = small
    
    # 2. Средний 50×50 — случайный
    mazes['medium_50x50'] = RandomMazeBuilder(51, 51).build_from_file()
    
    # 3. Большой 100×100 — случайный
    mazes['large_100x100'] = RandomMazeBuilder(101, 101).build_from_file()
    
    # 4. Пустой 20×20 — без стен
    empty = Maze(20, 20)
    for y in range(20):
        for x in range(20):
            empty.set_cell(x, y, Cell(x, y, is_wall=False,
                                      is_start=(x==1 and y==1),
                                      is_exit=(x==18 and y==18)))
    mazes['empty_20x20'] = empty
    
    # 5. Без выхода — проверка обработки
    no_exit = Maze(10, 10)
    for y in range(10):
        for x in range(10):
            is_wall = (x == 0 or x == 9 or y == 0 or y == 9 or x == 5)
            no_exit.set_cell(x, y, Cell(x, y, is_wall=is_wall,
                                          is_start=(x==1 and y==1)))
    mazes['no_exit_10x10'] = no_exit
    
    # 6. Взвешенный 15×15
    weighted = Maze(15, 15)
    for y in range(15):
        for x in range(15):
            is_wall = (x == 0 or x == 14 or y == 0 or y == 14 or 
                      (x == 5 and y != 7) or (x == 10 and y != 3))
            weight = 1
            if 3 <= x <= 7 and 3 <= y <= 7:
                weight = 2  # песок
            elif 8 <= x <= 12 and 8 <= y <= 12:
                weight = 3  # болото
            
            weighted.set_cell(x, y, Cell(x, y, 
                is_wall=is_wall,
                is_start=(x==1 and y==1),
                is_exit=(x==13 and y==13),
                weight=weight))
    mazes['weighted_15x15'] = weighted
    
    return mazes


def run_experiments(mazes: Dict[str, Maze], 
                   strategies: List, 
                   runs_per_test: int = 5) -> List[SearchStats]:
    """Запускает эксперименты, возвращает список статистик."""
    results = []
    
    for maze_name, maze in mazes.items():
        print(f"\n{'='*60}")
        print(f"Лабиринт: {maze_name} ({maze.width}x{maze.height})")
        print(f"{'='*60}")
        
        for strategy in strategies:
            print(f"\nАлгоритм: {strategy.get_name()}")
            
            times = []
            visited_list = []
            path_lens = []
            
            solver = MazeSolver(maze, strategy)
            
            for run in range(runs_per_test):
                stats = solver.solve(maze_name)
                times.append(stats.time_ms)
                visited_list.append(stats.visited_cells)
                path_lens.append(stats.path_length)
                
                print(f"  Запуск {run+1}: {stats.time_ms:.4f} мс, "
                      f"посещено: {stats.visited_cells}, "
                      f"длина: {stats.path_length}")
            
            # Средние значения
            avg = SearchStats(
                time_ms=np.mean(times),
                visited_cells=int(np.mean(visited_list)),
                path_length=int(np.mean(path_lens)),
                algorithm_name=strategy.get_name(),
                maze_name=maze_name
            )
            results.append(avg)
            
            print(f"  СРЕДНЕЕ: {avg.time_ms:.4f} мс, "
                  f"посещено: {avg.visited_cells}, "
                  f"длина: {avg.path_length}")
    
    return results


def save_csv(results: List[SearchStats], filename: str):
    """Сохраняет результаты в CSV."""
    with open(filename, 'w', newline='', encoding='utf-8') as f:
        writer = csv.writer(f)
        writer.writerow(['Лабиринт', 'Алгоритм', 'Время_мс', 
                        'Посещено_клеток', 'Длина_пути'])
        for r in results:
            writer.writerow([
                r.maze_name, r.algorithm_name,
                f"{r.time_ms:.6f}", r.visited_cells, r.path_length
            ])
    print(f"\nCSV сохранён: {filename}")


def create_plots(results: List[SearchStats], output_dir: str = "."):
    """Создаёт графики сравнения."""
    os.makedirs(output_dir, exist_ok=True)
    
    maze_names = sorted(set(r.maze_name for r in results))
    algorithms = sorted(set(r.algorithm_name for r in results))
    
    # 1. Общее сравнение по лабиринтам
    fig, axes = plt.subplots(2, 3, figsize=(18, 12))
    fig.suptitle('Сравнение алгоритмов поиска пути', fontsize=16)
    
    for idx, maze_name in enumerate(maze_names):
        ax = axes[idx // 3, idx % 3]
        maze_res = [r for r in results if r.maze_name == maze_name]
        
        names = [r.algorithm_name for r in maze_res]
        times = [r.time_ms for r in maze_res]
        visited = [r.visited_cells for r in maze_res]
        paths = [r.path_length for r in maze_res]
        
        x = np.arange(len(names))
        w = 0.25
        
        ax.bar(x - w, times, w, label='Время (мс)', color='skyblue')
        ax.bar(x, [v/10 for v in visited], w, 
               label='Посещено (÷10)', color='lightcoral')
        ax.bar(x + w, paths, w, label='Длина пути', color='lightgreen')
        
        ax.set_title(maze_name)
        ax.set_xticks(x)
        ax.set_xticklabels(names, rotation=45)
        ax.legend(fontsize=8)
        ax.grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.savefig(f'{output_dir}/maze_comparison.png', dpi=150)
    plt.close()
    
    # 2. Посещённые клетки
    fig, ax = plt.subplots(figsize=(14, 8))
    for algo in algorithms:
        algo_res = [r for r in results if r.algorithm_name == algo]
        names = [r.maze_name for r in algo_res]
        vals = [r.visited_cells for r in algo_res]
        ax.plot(names, vals, marker='o', label=algo, linewidth=2)
    
    ax.set_title('Посещённые клетки по лабиринтам', fontsize=14)
    ax.set_xlabel('Лабиринт')
    ax.set_ylabel('Клетки')
    ax.legend()
    ax.grid(True, alpha=0.3)
    plt.xticks(rotation=45)
    plt.tight_layout()
    plt.savefig(f'{output_dir}/visited_cells.png', dpi=150)
    plt.close()
    
    # 3. Время выполнения (логарифмическая шкала)
    fig, ax = plt.subplots(figsize=(14, 8))
    for algo in algorithms:
        algo_res = [r for r in results if r.algorithm_name == algo]
        names = [r.maze_name for r in algo_res]
        vals = [max(r.time_ms, 0.001) for r in algo_res]
        ax.plot(names, vals, marker='s', label=algo, linewidth=2)
    
    ax.set_title('Время выполнения (лог. шкала)', fontsize=14)
    ax.set_xlabel('Лабиринт')
    ax.set_ylabel('Время (мс)')
    ax.set_yscale('log')
    ax.legend()
    ax.grid(True, alpha=0.3, which='both')
    plt.xticks(rotation=45)
    plt.tight_layout()
    plt.savefig(f'{output_dir}/time_comparison.png', dpi=150)
    plt.close()
    
    print(f"Графики сохранены в {output_dir}/")


if __name__ == "__main__":
    print("=" * 70)
    print("ЭКСПЕРИМЕНТАЛЬНАЯ ЧАСТЬ")
    print("=" * 70)
    
    # Создаём лабиринты
    mazes = create_test_mazes()
    
    # Алгоритмы для сравнения
    strategies = [BFSStrategy(), DFSStrategy(), 
                  AStarStrategy(), DijkstraStrategy()]
    
    # Запускаем эксперименты
    results = run_experiments(mazes, strategies, runs_per_test=5)
    
    # Сохраняем CSV
    save_csv(results, "experiment_results.csv")
    
    # Строим графики
    create_plots(results)
    
    print("\n" + "=" * 70)
    print("ГОТОВО!")
    print("=" * 70)