import time
from abc import ABC, abstractmethod

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

    def __repr__(self):
        return (f"SearchStats(time={self.time_ms:.3f}ms, "
                f"visited={self.visited_cells}, "
                f"path_len={self.path_length})")

class Observer(ABC):

    @abstractmethod
    def update(self, event, data=None):
        pass


class ConsoleView(Observer):

    def update(self, event, data=None):
        if event == 'maze_loaded':
            print(f"\n[ConsoleView] Лабиринт загружен: "
                  f"{data['width']}×{data['height']}")

        elif event == 'path_found':
            stats = data['stats']
            strategy_name = data['strategy']
            if stats.path_length > 0:
                print(f"\n[ConsoleView] [{strategy_name}] Путь найден! "
                      f"Длина: {stats.path_length}, "
                      f"Посещено клеток: {stats.visited_cells}, "
                      f"Время: {stats.time_ms:.3f} мс")
            else:
                print(f"\n[ConsoleView] [{strategy_name}] Путь не найден. "
                      f"Посещено клеток: {stats.visited_cells}")

        elif event == 'move':
            print(f"[ConsoleView] Игрок переместился в "
                  f"({data['x']}, {data['y']})")

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, data=None):
        for obs in self._observers:
            obs.update(event, data)

    def solve(self):
        if self.strategy is None:
            raise RuntimeError("Стратегия не задана. Используйте set_strategy().")

        start = time.perf_counter()
        path = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit)
        end = time.perf_counter()

        stats = SearchStats(
            time_ms=(end - start) * 1000,
            visited_cells=getattr(self.strategy, 'visited_count', 0),
            path_length=len(path),
            path=path
        )

        self._notify('path_found', {
            'stats': stats,
            'strategy': type(self.strategy).__name__
        })

        return stats

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

    @abstractmethod
    def undo(self):
        pass


class Player:
    def __init__(self, start_cell):
        self.current_cell = start_cell

    def move_to(self, cell):
        self.current_cell = cell


class MoveCommand(Command):
    def __init__(self, player, target_cell, observers=None):
        self.player = player
        self.target_cell = target_cell
        self.previous_cell = None
        self._observers = observers or []

    def execute(self):
        self.previous_cell = self.player.current_cell
        self.player.move_to(self.target_cell)
        for obs in self._observers:
            obs.update('move', {'x': self.target_cell.x,
                                'y': self.target_cell.y})

    def undo(self):
        if self.previous_cell is not None:
            self.player.move_to(self.previous_cell)
            for obs in self._observers:
                obs.update('move', {'x': self.previous_cell.x,
                                    'y': self.previous_cell.y})
