import os
import time
import heapq
import csv
from collections import deque
from abc import ABC, abstractmethod

class Cell:
    def __init__(self, x, y, wall=False):
        self.x = x
        self.y = y
        self.wall = wall
        self.start = False
        self.exit = False

    def prohodim(self):
        return not self.wall

# лабиринт
class Maze:
    def __init__(self, w, h):
        self.w = w
        self.h = h
        self.grid = []
        for i in range(h):
            row = []
            for j in range(w):
                row.append(Cell(j, i))
            self.grid.append(row)
        self.start = None
        self.exit = None

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

    def set_start(self, x, y):
        c = self.get_cell(x, y)
        if c:
            c.start = True
            self.start = (x, y)

    def set_exit(self, x, y):
        c = self.get_cell(x, y)
        if c:
            c.exit = True
            self.exit = (x, y)

    def neighbors(self, x, y):
        res = []
        for dx, dy in [(0,1),(1,0),(0,-1),(-1,0)]:
            nx, ny = x+dx, y+dy
            c = self.get_cell(nx, ny)
            if c and c.prohodim():
                res.append((nx, ny))
        return res

class MazeBuilder(ABC):
    @abstractmethod
    def load(self, filename):
        pass

class TextMazeBuilder(MazeBuilder):
    def load(self, filename):
        f = open(filename, 'r', encoding='utf-8')
        lines = []
        for line in f:
            lines.append(line.rstrip('\n'))
        f.close()
        if not lines:
            raise Exception("Файл пустой")
        h = len(lines)
        w = max([len(l) for l in lines])
        maze = Maze(w, h)
        for y in range(h):
            line = lines[y]
            for x in range(len(line)):
                ch = line[x]
                if ch == '#':
                    maze.grid[y][x] = Cell(x, y, wall=True)
                elif ch == 'S':
                    maze.set_start(x, y)
                elif ch == 'E':
                    maze.set_exit(x, y)
        return maze

class Strategy(ABC):
    @abstractmethod
    def find_path(self, maze, start, end):
        pass

# BFS
class BFS(Strategy):
    def find_path(self, maze, start, end):
        if start is None or end is None:
            self.visited_count = 0
            return None
        q = deque()
        q.append(start)
        parent = {start: None}
        while q:
            cur = q.popleft()
            if cur == end:
                break
            for nb in maze.neighbors(cur[0], cur[1]):
                if nb not in parent:
                    parent[nb] = cur
                    q.append(nb)
        self.visited_count = len(parent)
        if end not in parent:
            return None
        path = []
        c = end
        while c is not None:
            path.append(c)
            c = parent[c]
        path.reverse()
        return path

# DFS
class DFS(Strategy):
    def find_path(self, maze, start, end):
        if start is None or end is None:
            self.visited_count = 0
            return None
        stack = [start]
        parent = {start: None}
        while stack:
            cur = stack.pop()
            if cur == end:
                break
            for nb in maze.neighbors(cur[0], cur[1]):
                if nb not in parent:
                    parent[nb] = cur
                    stack.append(nb)
        self.visited_count = len(parent)
        if end not in parent:
            return None
        path = []
        c = end
        while c is not None:
            path.append(c)
            c = parent[c]
        path.reverse()
        return path

# A*
class AStar(Strategy):
    def heur(self, a, b):
        return abs(a[0]-b[0]) + abs(a[1]-b[1])

    def find_path(self, maze, start, end):
        if start is None or end is None:
            self.visited_count = 0
            return None
        heap = [(0, start)]
        came_from = {}
        g = {start: 0}
        f = {start: self.heur(start, end)}
        visited = set([start])
        while heap:
            cur = heapq.heappop(heap)[1]
            visited.add(cur)
            if cur == end:
                break
            for nb in maze.neighbors(cur[0], cur[1]):
                newg = g[cur] + 1
                if newg < g.get(nb, 999999):
                    came_from[nb] = cur
                    g[nb] = newg
                    f[nb] = newg + self.heur(nb, end)
                    heapq.heappush(heap, (f[nb], nb))
                    visited.add(nb)
        self.visited_count = len(visited)
        if end not in came_from and end != start:
            return None
        path = []
        c = end
        while c in came_from:
            path.append(c)
            c = came_from[c]
        path.append(start)
        path.reverse()
        return path

# решение и статистика 
class MazeSolver:
    def __init__(self, maze, strategy):
        self.maze = maze
        self.strategy = strategy

    def solve(self):
        if self.maze.start is None or self.maze.exit is None:
            return (0, 0, 0, False)
        t0 = time.perf_counter()
        path = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit)
        t = (time.perf_counter() - t0) * 1000
        if path is None:
            return (t, 0, 0, False)
        return (t, self.strategy.visited_count, len(path), True)


# Наблюдатель
class Observer(ABC):
    @abstractmethod
    def update(self, event, data):
        pass

class ConsoleView(Observer):
    def __init__(self, maze):
        self.maze = maze
        self.player_pos = None
        self.path_set = None

    def update(self, event, data):
        if event == 'init':
            self.draw()
        elif event == 'move':
            self.player_pos = data
            self.draw()
        elif event == 'path':
            self.path_set = set(data) if data else None
            self.draw()

    def clear(self):
        os.system('cls' if os.name == 'nt' else 'clear')

    def draw(self):
        self.clear()
        for y in range(self.maze.h):
            row = ''
            for x in range(self.maze.w):
                cell = self.maze.get_cell(x, y)
                if self.player_pos and (x,y) == self.player_pos:
                    row += 'P'
                elif cell.start:
                    row += 'S'
                elif cell.exit:
                    row += 'E'
                elif cell.wall:
                    row += '#'
                elif self.path_set and (x,y) in self.path_set:
                    row += '*'
                else:
                    row += ' '
            print(row)
        print("WASD - ходить, F - BFS путь, G - A* путь, Z - отмена, Q - выход")


# игрок
class Player:
    def __init__(self, maze):
        self.maze = maze
        self.pos = maze.start
        self.history = []

    def move(self, new_pos):
        cell = self.maze.get_cell(new_pos[0], new_pos[1])
        if cell and cell.prohodim():
            self.history.append(self.pos)
            self.pos = new_pos
            return True
        return False

    def undo(self):
        if self.history:
            self.pos = self.history.pop()
            return True
        return False

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

class MoveCommand(Command):
    def __init__(self, player, dx, dy):
        self.player = player
        self.dx = dx
        self.dy = dy
        self.old = None

    def execute(self):
        self.old = self.player.pos
        nx = self.player.pos[0] + self.dx
        ny = self.player.pos[1] + self.dy
        return self.player.move((nx, ny))

    def undo(self):
        if self.old:
            return self.player.move(self.old)
        return False


# эксперимент
def run_experiment(maze_files, repeats=5):
    builder = TextMazeBuilder()
    results = []
    for fname in maze_files:
        print("Обработка", fname)
        maze = builder.load(fname)
        if maze.start is None or maze.exit is None:
            print(f"Предупреждение: в {fname} нет S или E, пропускаем")
            continue
        for algo_class, name in [(BFS, 'BFS'), (DFS, 'DFS'), (AStar, 'A*')]:
            total_time = 0.0
            total_visited = 0
            path_len = 0
            found = False
            for _ in range(repeats):
                alg = algo_class()
                t0 = time.perf_counter()
                path = alg.find_path(maze, maze.start, maze.exit)
                t = (time.perf_counter() - t0) * 1000
                total_time += t
                total_visited += alg.visited_count
                if path:
                    found = True
                    path_len = len(path)
            results.append({
                'maze': fname,
                'algo': name,
                'time': total_time / repeats,
                'visited': total_visited / repeats,
                'length': path_len,
                'found': found
            })
    with open('results.csv', 'w', newline='') as f:
        writer = csv.DictWriter(f, fieldnames=['maze','algo','time','visited','length','found'])
        writer.writeheader()
        writer.writerows(results)
    print("\nРезультаты:")
    for r in results:
        print(f"{r['maze']:15} {r['algo']:5} время={r['time']:6.2f}ms посещено={r['visited']:6.1f} длина={r['length']}")


# режим игры
def play_game(maze):
    view = ConsoleView(maze)
    player = Player(maze)
    view.update('init', None)
    while True:
        cmd = input("> ").strip().upper()
        if cmd == 'W':
            c = MoveCommand(player, 0, -1)
        elif cmd == 'S':
            c = MoveCommand(player, 0, 1)
        elif cmd == 'A':
            c = MoveCommand(player, -1, 0)
        elif cmd == 'D':
            c = MoveCommand(player, 1, 0)
        elif cmd == 'F':
            solver = MazeSolver(maze, BFS())
            t, v, l, ok = solver.solve()
            if ok:
                path = BFS().find_path(maze, maze.start, maze.exit)
                view.update('path', path)
                print(f"BFS: длина={l} время={t:.2f}ms посещено={v}")
            else:
                print("Путь не найден")
            continue
        elif cmd == 'G':
            astar = AStar()
            path = astar.find_path(maze, maze.start, maze.exit)
            if path:
                view.update('path', path)
                print(f"A*: длина={len(path)} посещено={astar.visited_count}")
            else:
                print("Путь не найден")
            continue
        elif cmd == 'Z':
            if player.undo():
                view.update('move', player.pos)
            continue
        elif cmd == 'Q':
            break
        else:
            print("Неизвестная команда")
            continue
        if c.execute():
            view.update('move', player.pos)
        else:
            print("Стена!")

def main():
    print("1 - Игра\n2 - Эксперимент")
    ch = input("> ")
    builder = TextMazeBuilder()
    if ch == '1':
        maze = builder.load('small.txt') #легкая прогулка) 
        play_game(maze)
    else:
        files = ['small.txt', 'medium.txt', 'large.txt', 'empty.txt', 'no_exit.txt']
        run_experiment(files)

if __name__ == '__main__':
    main()
print ("\nЭто было долго, но вроде бы все готово. ") #можно конечно сделать многофайловую программу чтобы использовать классы как блоки длч строительства