import os
import time
import heapq
from collections import deque
from typing import List, Dict, Optional, Tuple
from abc import ABC, abstractmethod
from dataclasses import dataclass


class Cell:
    def __init__(self, x: int, y: int, is_wall: bool = True, is_start: bool = False, is_exit: bool = False):
        self.x = x
        self.y = y
        self.is_wall = is_wall
        self.is_start = is_start
        self.is_exit = is_exit
        
    def is_passable(self) -> bool:
        return not self.is_wall
    
    def __eq__(self, other):
        if not isinstance(other, Cell):
            return False
        return self.x == other.x and self.y == other.y
    
    def __hash__(self):
        return hash((self.x, self.y))
    
    def __repr__(self):
        return f"Cell({self.x}, {self.y})"


class Maze:
    def __init__(self, width: int = 0, height: int = 0):
        self.width = width
        self.height = height
        self.grid: List[List[Cell]] = []
        self.start: Optional[Cell] = None
        self.exit: Optional[Cell] = None
        
    def set_cell(self, x: int, y: int, cell: Cell) -> None:
        if 0 <= x < self.width and 0 <= y < self.height:
            self.grid[y][x] = cell
            
    def get_cell(self, x: int, y: int) -> Optional[Cell]:
        if 0 <= x < self.width and 0 <= y < self.height:
            return self.grid[y][x]
        return None
        
    def get_neighbors(self, cell: Cell) -> List[Cell]:
        neighbors = []
        directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]
        
        for dx, dy in directions:
            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: str) -> Maze:
        pass


class TextFileMazeBuilder(MazeBuilder):
    
    def build_from_file(self, filename: str) -> Maze:
        with open(filename, 'r', encoding='utf-8') as file:
            lines = [line.rstrip('\n') for line in file.readlines()]
        
        while lines and not lines[0].strip():
            lines.pop(0)
        while lines and not lines[-1].strip():
            lines.pop()
            
        height = len(lines)
        width = max(len(line) for line in lines) if height > 0 else 0
        
        maze = Maze(width, height)
        maze.grid = [[None for _ in range(width)] for _ in range(height)]
        
        start_count = 0
        exit_count = 0
        
        for y, line in enumerate(lines):
            for x in range(width):
                char = line[x] if x < len(line) else '#'
                
                if char == '#':
                    cell = Cell(x, y, is_wall=True)
                elif char == ' ':
                    cell = Cell(x, y, is_wall=False)
                elif char == 'S':
                    cell = Cell(x, y, is_wall=False, is_start=True)
                    maze.start = cell
                    start_count += 1
                elif char == 'E':
                    cell = Cell(x, y, is_wall=False, is_exit=True)
                    maze.exit = cell
                    exit_count += 1
                else:
                    cell = Cell(x, y, is_wall=True)
                    
                maze.set_cell(x, y, cell)
        
        if start_count == 0:
            raise ValueError("Лабиринт должен содержать старт (S)")
        if start_count > 1:
            raise ValueError("Лабиринт может содержать только один старт (S)")
        if exit_count == 0:
            raise ValueError("Лабиринт должен содержать выход (E)")
        if exit_count > 1:
            raise ValueError("Лабиринт может содержать только один выход (E)")
            
        return maze
class PathFindingStrategy(ABC):
    
    @abstractmethod
    def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> Tuple[List[Cell], int]:
        pass
        
    def _reconstruct_path(self, parents: Dict[Cell, Cell], start: Cell, exit_cell: Cell) -> List[Cell]:
        path = []
        current = exit_cell
        
        while current != start:
            path.append(current)
            if current not in parents:
                return []
            current = parents[current]
        path.append(start)
        path.reverse()
        return path
    
    @property
    def name(self) -> str:
        return self.__class__.__name__.replace('Strategy', '')


class BFSStrategy(PathFindingStrategy):
    
    def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> Tuple[List[Cell], int]:
        queue = deque([start])
        visited = {start}
        parents: Dict[Cell, Cell] = {}
        visited_count = 1
        
        while queue:
            current = queue.popleft()
            
            if current == exit_cell:
                return self._reconstruct_path(parents, start, exit_cell), visited_count
                
            for neighbor in maze.get_neighbors(current):
                if neighbor not in visited:
                    visited.add(neighbor)
                    visited_count += 1
                    parents[neighbor] = current
                    queue.append(neighbor)
                    
        return [], visited_count


class DFSStrategy(PathFindingStrategy):
    
    def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> Tuple[List[Cell], int]:
        stack = [(start, [start])]
        visited = {start}
        visited_count = 1
        
        while stack:
            current, path = stack.pop()
            
            if current == exit_cell:
                return path, visited_count
                
            for neighbor in maze.get_neighbors(current):
                if neighbor not in visited:
                    visited.add(neighbor)
                    visited_count += 1
                    stack.append((neighbor, path + [neighbor]))
                    
        return [], visited_count


class AStarStrategy(PathFindingStrategy):
    
    def _heuristic(self, cell: Cell, target: Cell) -> int:
        return abs(cell.x - target.x) + abs(cell.y - target.y)
    
    def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> Tuple[List[Cell], int]:
        counter = 0
        open_set = [(0, counter, start)]
        came_from: Dict[Cell, Cell] = {}
        
        g_score: Dict[Cell, float] = {start: 0}
        f_score: Dict[Cell, float] = {start: self._heuristic(start, exit_cell)}
        
        open_set_hash = {start}
        visited_count = 1
        
        while open_set:
            current = heapq.heappop(open_set)[2]
            open_set_hash.remove(current)
            
            if current == exit_cell:
                path = self._reconstruct_path(came_from, start, exit_cell)
                return path, visited_count
                
            for neighbor in maze.get_neighbors(current):
                tentative_g_score = g_score[current] + 1
                
                if tentative_g_score < g_score.get(neighbor, float('inf')):
                    came_from[neighbor] = current
                    g_score[neighbor] = tentative_g_score
                    f_score[neighbor] = tentative_g_score + self._heuristic(neighbor, exit_cell)
                    
                    if neighbor not in open_set_hash:
                        visited_count += 1
                        counter += 1
                        heapq.heappush(open_set, (f_score[neighbor], counter, neighbor))
                        open_set_hash.add(neighbor)
                        
        return [], visited_count
@dataclass
class SearchStats:
    execution_time_ms: float
    path_length: int
    visited_cells: int
    success: bool


class MazeSolver:
    
    def __init__(self, maze: Maze, strategy: PathFindingStrategy):
        self.maze = maze
        self.strategy = strategy
        
    def set_strategy(self, strategy: PathFindingStrategy) -> None:
        self.strategy = strategy
        
    def solve(self) -> Tuple[List[Cell], SearchStats]:
        if not self.maze.start or not self.maze.exit:
            raise ValueError("Лабиринт должен содержать старт и выход")
            
        start_time = time.perf_counter()
        path, visited_cells = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit)
        end_time = time.perf_counter()
        
        execution_time = (end_time - start_time) * 1000
        
        stats = SearchStats(
            execution_time_ms=execution_time,
            path_length=len(path),
            visited_cells=visited_cells,
            success=len(path) > 0
        )
        
        return path, stats
class MazeVisualizer:
    
    @staticmethod
    def render(maze: Maze, path: List[Cell] = None, player_pos: Cell = None):
        print("\n" + "=" * (maze.width + 2))
        
        for y in range(maze.height):
            row = "|"
            for x in range(maze.width):
                cell = maze.get_cell(x, y)
                if cell:
                    if player_pos and cell == player_pos:
                        row += "P"
                    elif path and cell in path and not cell.is_start and not cell.is_exit:
                        row += "."
                    elif cell.is_start:
                        row += "S"
                    elif cell.is_exit:
                        row += "E"
                    elif cell.is_wall:
                        row += "#"
                    else:
                        row += " "
                else:
                    row += " "
            row += "|"
            print(row)
        print("=" * (maze.width + 2))
def create_test_mazes():
    
    current_dir = os.path.dirname(os.path.abspath(__file__))
    
    small_maze = """##########
#S       #
# ### ## #
#   #    #
### # ####
#   #    #
# ### #  #
#     # E#
##########"""
    
    medium_maze = """####################
#S                 #
# ### ###########  #
# # # #         #  #
# # # # ####### #  #
# #   #       # #  #
# # ######### # #  #
# #           # #  #
# # ########### #  #
# #             #  #
# ###############  #
#                 E#
####################"""
    
    large_maze = """##################################################
#S                                               #
# ############################################# #
# #                                           # #
# # ######################################### # #
# # #                                       # # #
# # # ##################################### # # #
# # # #                                   # # # #
# # # # ################################# # # # #
# # # # #                               # # # # #
# # # # # ############################# # # # # #
# # # # # #                           # # # # # #
# # # # # # ######################### # # # # # #
# # # # # # #                       # # # # # # #
# # # # # # # ##################### # # # # # # #
# # # # # # # #                   # # # # # # # #
# # # # # # # # ################# # # # # # # # #
# # # # # # # # #               # # # # # # # # #
# # # # # # # # # ############# # # # # # # # # #
# # # # # # # # # #           # # # # # # # # # #
# # # # # # # # # # ######### # # # # # # # # # #
# # # # # # # # # # #       # # # # # # # # # # #
# # # # # # # # # # # ##### # # # # # # # # # # #
# # # # # # # # # # # #   # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # #E#
##################################################"""
    
    empty_maze_lines = ["#" + "#" * 38 + "#"]
    empty_maze_lines.append("#S" + " " * 37 + "#")
    for i in range(35):
        empty_maze_lines.append("#" + " " * 38 + "#")
    empty_maze_lines.append("#" + " " * 37 + "E#")
    empty_maze_lines.append("#" + "#" * 38 + "#")
    empty_maze = "\n".join(empty_maze_lines)
    
    no_exit_maze = """##########
#S       #
# ### ## #
#   #    #
### # ####
#   #    #
# ### #  #
#       # 
##########"""
    
    mazes = [
        ("01_small_maze.txt", small_maze),
        ("02_medium_maze.txt", medium_maze),
        ("03_large_maze.txt", large_maze),
        ("04_empty_maze.txt", empty_maze),
        ("05_no_exit_maze.txt", no_exit_maze)
    ]
    
    print("\n" + "="*60)
    print("CREATING TEST MAZES")
    print("="*60)
    
    for filename, content in mazes:
        filepath = os.path.join(current_dir, filename)
        with open(filepath, 'w', encoding='utf-8') as f:
            f.write(content)
        print(f"Created: {filename}")
    
    print("="*60)


def list_available_mazes():
    current_dir = os.path.dirname(os.path.abspath(__file__))
    maze_files = [f for f in os.listdir(current_dir) if f.endswith('.txt') and ('maze' in f.lower() or f.startswith('0'))]
    
    if not maze_files:
        return []
    
    print("\nAVAILABLE MAZES:")
    print("-" * 50)
    for i, file in enumerate(maze_files, 1):
        print(f"   {i}. {file}")
    
    return maze_files
class Benchmark:
    
    def __init__(self):
        self.strategies = [
            BFSStrategy(),
            DFSStrategy(),
            AStarStrategy()
        ]
        
    def run_experiment(self, maze: Maze, runs: int = 5) -> Dict:
        results = {}
        
        for strategy in self.strategies:
            times = []
            path_lengths = []
            visited_counts = []
            
            for _ in range(runs):
                solver = MazeSolver(maze, strategy)
                path, stats = solver.solve()
                
                if stats.success:
                    times.append(stats.execution_time_ms)
                    path_lengths.append(stats.path_length)
                    visited_counts.append(stats.visited_cells)
                    
            if times:
                results[strategy.name] = {
                    'avg_time_ms': sum(times) / len(times),
                    'avg_path_length': sum(path_lengths) / len(path_lengths),
                    'avg_visited_cells': sum(visited_counts) / len(visited_counts),
                    'success_rate': len(times) / runs * 100
                }
            else:
                results[strategy.name] = {
                    'avg_time_ms': float('inf'),
                    'avg_path_length': 0,
                    'avg_visited_cells': 0,
                    'success_rate': 0
                }
                
        return results
    
    @staticmethod
    def print_results(results: Dict, maze_name: str):
        print(f"\n{'='*60}")
        print(f"RESULTS FOR MAZE: {maze_name}")
        print(f"{'='*60}")
        print(f"{'Algorithm':<12} {'Time(ms)':<12} {'PathLen':<12} {'Visited':<12} {'Success':<8}")
        print(f"{'-'*60}")
        
        for strategy_name, stats in results.items():
            print(f"{strategy_name:<12} {stats['avg_time_ms']:>8.3f}    "
                  f"{stats['avg_path_length']:>8.1f}    "
                  f"{stats['avg_visited_cells']:>8.1f}    "
                  f"{stats['success_rate']:>6.1f}%")
def interactive_mode():
    builder = TextFileMazeBuilder()
    visualizer = MazeVisualizer()
    current_dir = os.path.dirname(os.path.abspath(__file__))
    
    while True:
        print("\n" + "="*60)
        print("MAZE PATHFINDING PROGRAM")
        print("="*60)
        print("1. Load maze and find path")
        print("2. Compare all algorithms on maze")
        print("3. Create test mazes (5 pieces)")
        print("4. Run benchmark on all mazes")
        print("5. Show available mazes")
        print("6. Exit")
        print("="*60)
        
        choice = input("Choose action (1-6): ").strip()
        
        if choice == '6':
            print("\nGoodbye!")
            break
            
        elif choice == '3':
            create_test_mazes()
            input("\nPress Enter to continue...")
            
        elif choice == '5':
            maze_files = list_available_mazes()
            if not maze_files:
                print("\nNo available mazes. Create them first (action 3)")
            input("\nPress Enter to continue...")
            
        elif choice == '1':
            print("\nAvailable mazes:")
            maze_files = list_available_mazes()
            
            if not maze_files:
                print("\nNo available mazes. Create them first (action 3)")
                continue
                
            filename = input("\nEnter path to maze file: ").strip()
            
            if not os.path.exists(filename):
                test_path = os.path.join(current_dir, filename)
                if os.path.exists(test_path):
                    filename = test_path
                    
            try:
                maze = builder.build_from_file(filename)
                print("\nLOADED MAZE:")
                visualizer.render(maze)
                
                print("\nChoose algorithm:")
                print("   1. BFS - finds SHORTEST path")
                print("   2. DFS - FAST but path may be longer")
                print("   3. A* - OPTIMAL balance")
                
                algo_choice = input("\nYour choice (1-3): ").strip()
                
                strategies = {
                    '1': BFSStrategy(),
                    '2': DFSStrategy(),
                    '3': AStarStrategy()
                }
                
                if algo_choice in strategies:
                    print("\nSearching for path...")
                    solver = MazeSolver(maze, strategies[algo_choice])
                    path, stats = solver.solve()
                    
                    if stats.success:
                        print(f"\nPATH FOUND!")
                        print(f"\nSTATISTICS:")
                        print(f"   Time: {stats.execution_time_ms:.3f} ms")
                        print(f"   Path length: {stats.path_length} steps")
                        print(f"   Visited cells: {stats.visited_cells}")
                        print(f"   Efficiency: {stats.visited_cells/stats.path_length:.1f} cells per step")
                        
                        print("\nPATH ON MAP (. = path):")
                        visualizer.render(maze, path)
                    else:
                        print("\nPATH NOT FOUND! Exit unreachable from start.")
                else:
                    print("Invalid choice!")
                    
            except FileNotFoundError:
                print(f"\nFile '{filename}' not found!")
            except Exception as e:
                print(f"\nError: {e}")
                
            input("\nPress Enter to continue...")
            
        elif choice == '2':
            print("\nAvailable mazes:")
            maze_files = list_available_mazes()
            
            if not maze_files:
                print("\nNo available mazes. Create them first (action 3)")
                continue
                
            filename = input("\nEnter path to maze file: ").strip()
            
            if not os.path.exists(filename):
                test_path = os.path.join(current_dir, filename)
                if os.path.exists(test_path):
                    filename = test_path
                    
            try:
                maze = builder.build_from_file(filename)
                print("\nLOADED MAZE:")
                visualizer.render(maze)
                
                print("\nRunning algorithm comparison (3 runs each)...")
                benchmark = Benchmark()
                results = benchmark.run_experiment(maze, runs=3)
                
                maze_name = os.path.basename(filename)
                benchmark.print_results(results, maze_name)
                
                print("\nANALYSIS:")
                print("-" * 40)
                fastest = min(results.items(), key=lambda x: x[1]['avg_time_ms'])
                print(f"Fastest: {fastest[0]} ({fastest[1]['avg_time_ms']:.3f} ms)")
                
                shortest = min(results.items(), key=lambda x: x[1]['avg_path_length'])
                print(f"Shortest path: {shortest[0]} ({shortest[1]['avg_path_length']:.0f} steps)")
                
                efficient = min(results.items(), key=lambda x: x[1]['avg_visited_cells'])
                print(f"Most efficient: {efficient[0]} (checked {efficient[1]['avg_visited_cells']:.0f} cells)")
                
            except FileNotFoundError:
                print(f"\nFile '{filename}' not found!")
            except Exception as e:
                print(f"\nError: {e}")
                
            input("\nPress Enter to continue...")
            
        elif choice == '4':
            print("\nRUNNING FULL BENCHMARK")
            print("="*60)
            
            test_files = [
                "01_small_maze.txt",
                "02_medium_maze.txt", 
                "03_large_maze.txt",
                "04_empty_maze.txt",
                "05_no_exit_maze.txt"
            ]
            
            benchmark = Benchmark()
            all_results = {}
            
            for test_file in test_files:
                filepath = os.path.join(current_dir, test_file)
                
                if not os.path.exists(filepath):
                    print(f"\nFile {test_file} not found. Creating test mazes...")
                    create_test_mazes()
                    break
                    
                try:
                    print(f"\nTesting: {test_file}")
                    maze = builder.build_from_file(filepath)
                    results = benchmark.run_experiment(maze, runs=5)
                    benchmark.print_results(results, test_file)
                    all_results[test_file] = results
                except Exception as e:
                    print(f"Error testing {test_file}: {e}")
            
            if all_results:
                csv_filename = os.path.join(current_dir, "benchmark_results.csv")
                with open(csv_filename, "w", encoding='utf-8') as f:
                    f.write("Maze,Algorithm,AvgTimeMs,AvgPathLength,AvgVisitedCells,SuccessPercent\n")
                    for maze_name, results in all_results.items():
                        for strategy_name, stats in results.items():
                            f.write(f"{maze_name},{strategy_name},"
                                   f"{stats['avg_time_ms']:.3f},{stats['avg_path_length']:.1f},"
                                   f"{stats['avg_visited_cells']:.1f},{stats['success_rate']:.1f}\n")
                
                print(f"\nResults saved to: {csv_filename}")
            
            input("\nPress Enter to continue...")
def main():
    print("="*60)
    print("MAZE PATHFINDING PROGRAM")
    print("Patterns: Builder, Strategy")
    print("Algorithms: BFS, DFS, A*")
    print("="*60)
    
    current_dir = os.path.dirname(os.path.abspath(__file__))
    existing_mazes = [f for f in os.listdir(current_dir) if f.endswith('.txt') and ('maze' in f.lower() or f.startswith('0'))]
    
    if not existing_mazes:
        print("First run: create test mazes (action 3)\n")
    
    interactive_mode()


if __name__ == "__main__":
    main()