import heapq
import time
import csv
from abc import ABC, abstractmethod

class Cell:
    def __init__(self, x, y, is_wall=False, is_start=False, is_exit=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):
        return not self.is_wall

class Maze:
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.grid = [[None for _ in range(width)] for _ in range(height)]
        self.start = None
        self.exit = None

    def set_cell(self, x, y, cell):
        self.grid[y][x] = cell
        if cell.is_start:
            self.start = cell
        if cell.is_exit:
            self.exit = cell

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

    def get_neighbors(self, 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):
        pass

class TextFileMazeBuilder(MazeBuilder):
    def build_from_file(self, filename):
        with open(filename, 'r', encoding='utf-8') as f:
            lines = [line.rstrip('\n') for line in f.readlines()]
        
        height = len(lines)
        width = len(lines[0]) if height > 0 else 0
        maze = Maze(width, height)
        
        for y, line in enumerate(lines):
            for x, ch in enumerate(line):
                is_wall = (ch == '#')
                is_start = (ch == 'S')
                is_exit = (ch == 'E')
                is_passable = (ch == ' ' or is_start or is_exit)
                
                cell = Cell(x, y, is_wall=is_wall, is_start=is_start, is_exit=is_exit)
                maze.set_cell(x, y, cell)
        
        return maze

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

class BFSStrategy(PathFindingStrategy):
    def find_path(self, maze, start, exit):
        if not start or not exit:
            return []
        
        queue = [(start, [start])]
        visited = set()
        
        while queue:
            current, path = queue.pop(0)
            
            if current == exit:
                return path
            
            if current in visited:
                continue
            
            visited.add(current)
            
            for neighbor in maze.get_neighbors(current):
                if neighbor not in visited:
                    queue.append((neighbor, path + [neighbor]))
        
        return []

class DFSStrategy(PathFindingStrategy):
    def find_path(self, maze, start, exit):
        if not start or not exit:
            return []
        
        stack = [(start, [start])]
        visited = set()
        
        while stack:
            current, path = stack.pop()
            
            if current == exit:
                return path
            
            if current in visited:
                continue
            
            visited.add(current)
            
            for neighbor in maze.get_neighbors(current):
                if neighbor not in visited:
                    stack.append((neighbor, path + [neighbor]))
        
        return []

class AStarStrategy(PathFindingStrategy):
    def heuristic(self, cell, exit):
        return abs(cell.x - exit.x) + abs(cell.y - exit.y)
    
    def find_path(self, maze, start, exit):
        if not start or not exit:
            return []
        
        open_set = [(0, id(start), start)]
        came_from = {}
        g_score = {start: 0}
        f_score = {start: self.heuristic(start, exit)}
        
        while open_set:
            _, _, current = heapq.heappop(open_set)
            
            if current == exit:
                path = []
                while current in came_from:
                    path.append(current)
                    current = came_from[current]
                path.append(start)
                path.reverse()
                return path
            
            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]:
                    came_from[neighbor] = current
                    g_score[neighbor] = tentative_g
                    f_score[neighbor] = tentative_g + self.heuristic(neighbor, exit)
                    heapq.heappush(open_set, (f_score[neighbor], id(neighbor), neighbor))
        
        return []

class DijkstraStrategy(PathFindingStrategy):
    def find_path(self, maze, start, exit):
        if not start or not exit:
            return []
        
        pq = [(0, id(start), start)]
        distances = {start: 0}
        came_from = {}
        
        while pq:
            dist, _, current = heapq.heappop(pq)
            
            if current == exit:
                path = []
                while current in came_from:
                    path.append(current)
                    current = came_from[current]
                path.append(start)
                path.reverse()
                return path
            
            if dist > distances.get(current, float('inf')):
                continue
            
            for neighbor in maze.get_neighbors(current):
                new_dist = dist + 1
                
                if new_dist < distances.get(neighbor, float('inf')):
                    distances[neighbor] = new_dist
                    came_from[neighbor] = current
                    heapq.heappush(pq, (new_dist, id(neighbor), neighbor))
        
        return []

class SearchStats:
    def __init__(self, time_ms, visited_cells, path_length, path=None):
        self.time_ms = time_ms
        self.visited_cells = visited_cells
        self.path_length = path_length
        self.path = path

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 attach(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("Strategy not set")
        
        start_time = time.perf_counter()
        path = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit)
        end_time = time.perf_counter()
        
        time_ms = (end_time - start_time) * 1000
        visited_cells = len(path) if path else 0
        path_length = len(path) if path else 0
        
        self.notify(f"Path found with length {path_length} in {time_ms:.2f}ms")
        
        return SearchStats(time_ms, visited_cells, path_length, path)

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

class ConsoleView(Observer):
    def __init__(self):
        self.last_path = None
    
    def update(self, event):
        print(f"[ConsoleView] {event}")
    
    def render(self, maze, player_pos=None, path=None):
        print("\n" + "=" * (maze.width * 2 + 2))
        for y in range(maze.height):
            row = ""
            for x in range(maze.width):
                cell = maze.get_cell(x, y)
                if player_pos and cell == player_pos:
                    row += "P "
                elif path and cell in path:
                    row += "* "
                elif cell.is_start:
                    row += "S "
                elif cell.is_exit:
                    row += "E "
                elif cell.is_wall:
                    row += "# "
                else:
                    row += ". "
            print(row)
        print("=" * (maze.width * 2 + 2))

class Command(ABC):
    @abstractmethod
    def execute(self):
        pass
    
    @abstractmethod
    def undo(self):
        pass

class Player:
    def __init__(self, start_cell):
        self.current = start_cell
        self.start = start_cell
    
    def move_to(self, cell):
        self.current = cell

class MoveCommand(Command):
    def __init__(self, player, new_cell, maze):
        self.player = player
        self.new_cell = new_cell
        self.old_cell = player.current
        self.maze = maze
    
    def execute(self):
        if self.new_cell.is_passable():
            self.player.move_to(self.new_cell)
            return True
        return False
    
    def undo(self):
        self.player.move_to(self.old_cell)

def generate_test_mazes():
    mazes = {}
    
    simple_maze = Maze(5, 5)
    for y in range(5):
        for x in range(5):
            is_wall = (x == 2 and y == 1) or (x == 2 and y == 2) or (x == 2 and y == 3)
            is_start = (x == 0 and y == 0)
            is_exit = (x == 4 and y == 4)
            cell = Cell(x, y, is_wall=is_wall, is_start=is_start, is_exit=is_exit)
            simple_maze.set_cell(x, y, cell)
    mazes["simple"] = simple_maze
    
    empty_maze = Maze(20, 20)
    for y in range(20):
        for x in range(20):
            is_start = (x == 0 and y == 0)
            is_exit = (x == 19 and y == 19)
            cell = Cell(x, y, is_wall=False, is_start=is_start, is_exit=is_exit)
            empty_maze.set_cell(x, y, cell)
    mazes["empty"] = empty_maze
    
    return mazes

def run_experiments():
    mazes = generate_test_mazes()
    strategies = {
        "BFS": BFSStrategy(),
        "DFS": DFSStrategy(),
        "AStar": AStarStrategy(),
        "Dijkstra": DijkstraStrategy()
    }
    
    results = []
    
    for maze_name, maze in mazes.items():
        for strat_name, strategy in strategies.items():
            solver = MazeSolver(maze, strategy)
            
            times = []
            visited_counts = []
            path_lengths = []
            
            for run in range(5):
                stats = solver.solve()
                times.append(stats.time_ms)
                visited_counts.append(stats.visited_cells)
                path_lengths.append(stats.path_length)
            
            avg_time = sum(times) / len(times)
            avg_visited = sum(visited_counts) / len(visited_counts)
            avg_length = sum(path_lengths) / len(path_lengths)
            
            results.append([maze_name, strat_name, avg_time, avg_visited, avg_length])
            print(f"{maze_name} | {strat_name}: {avg_time:.3f}ms, {avg_visited:.0f} cells, {avg_length:.0f} length")
    
    with open("maze_results.csv", "w", newline="", encoding="utf-8") as f:
        writer = csv.writer(f)
        writer.writerow(["Лабиринт", "Стратегия", "Время_мс", "Посещено_клеток", "Длина_пути"])
        writer.writerows(results)
    
    return results

def main():
    print("Testing maze loading...")
    builder = TextFileMazeBuilder()
    
    try:
        maze = builder.build_from_file("maze.txt")
        print(f"Maze loaded: {maze.width}x{maze.height}")
        
        solver = MazeSolver(maze)
        view = ConsoleView()
        solver.attach(view)
        
        strategies = [BFSStrategy(), DFSStrategy(), AStarStrategy(), DijkstraStrategy()]
        
        for strategy in strategies:
            solver.set_strategy(strategy)
            print(f"\n--- {strategy.__class__.__name__} ---")
            stats = solver.solve()
            view.render(maze, path=stats.path)
            print(f"Time: {stats.time_ms:.3f}ms, Path length: {stats.path_length}")
    
    except FileNotFoundError:
        print("maze.txt not found, running experiments with generated mazes instead")
    
    print("\n" + "="*50)
    print("Running experiments...")
    run_experiments()

if __name__ == "__main__":
    main()