#include <fstream>
#include <iostream>
#include <string>
#include <stdexcept> /*для ошибок*/
#include <cmath>
#include <chrono> /*мерит время*/
#include <cstdlib> /*волшебная отрисовка*/
#include <iomanip>
#include <windows.h>
#include <clocale>

class cell{
    private:
        int x, y;
        bool isWall;
        bool isExit;
        bool isStart;

    public:
        cell() {x=0; y=0; isWall=false; isExit=false; isStart = false;}
        cell(int x, int y, bool isWall, bool isExit, bool isStart) 
        {
            this->x = x;
            this->y = y;
            this->isWall = isWall;
            this->isExit = isExit;
            this->isStart = isStart;
        }

        bool isPassable() {return !isWall;}

        void setStart(bool value) {isStart = value;}
        void setExit(bool value) {isExit = value;}
        void setX(int x) {this->x = x;}
        void setY(int y) {this->y = y;}
        void setIsWall(bool isWall) {this->isWall = isWall;}


        int  getX() {return x;}
        int  getY() {return y;}
        bool getIsWall() {return isWall;}
        bool getIsExit() {return isExit;}
        bool getIsStart() {return isStart;}

};

class maze{
    private:
        int width;
        int height;
        cell** matrix;

        cell* start;
        cell* exit;

    public:
        maze(int width, int height)
        {
            this->width = width;
            this->height = height;
            this->start = nullptr;
            this->exit = nullptr;

            matrix = new cell*[width];

            for (int x = 0; x < width; ++x) {
                matrix[x] = new cell[height];
                for (int y = 0; y < height; ++y) 
                    matrix[x][y] = cell(x, y, false, false, false);
            }
        }
        maze(int width, int height, int startX, int startY, int exitX, int exitY)
        {
            this->width = width;
            this->height = height;
            this->start = nullptr;
            this->exit = nullptr;

            matrix = new cell*[width];
            for (int x = 0; x < width; ++x) {
                matrix[x] = new cell[height];
                for (int y = 0; y < height; ++y) {
                    matrix[x][y] = cell(x, y, false, false, false);
                }
            }


            matrix[startX][startY].setStart(true);
            matrix[exitX][exitY].setExit(true);

            start = &matrix[startX][startY];
            exit = &matrix[exitX][exitY];
        }

        ~maze()
        {
        for (int i = 0; i < width; ++i) 
            delete[] matrix[i];
        delete[] matrix;
        }

        cell* getCell(int x, int y) {
            if (x < 0 || x >= width || y < 0 || y >= height)
                return nullptr;

            return &matrix[x][y];
        }
        void setCell(int x, int y, cell newCell) {
            if (x < 0 || x >= width || y < 0 || y >= height)
                return;

            matrix[x][y] = newCell;

            if (matrix[x][y].getIsStart())
                start = &matrix[x][y];

            if (matrix[x][y].getIsExit())
                exit = &matrix[x][y];
        }

        cell** getNeighbors(cell* current) { /*ДЕЛАТЬ delete[] neighbors; !!!!!!!!!!!!!!!*/
            cell** neighbors = new cell*[5];
            int count = 0;

            int x = current->getX();
            int y = current->getY();

            cell* up = getCell(x, y - 1);
            cell* down = getCell(x, y + 1);
            cell* left = getCell(x - 1, y);
            cell* right = getCell(x + 1, y);

            if (up != nullptr && up->isPassable()) {
                neighbors[count] = up;
                count++;}

            if (down != nullptr && down->isPassable()) {
                neighbors[count] = down;
                count++;}

            if (left != nullptr && left->isPassable()) {
                neighbors[count] = left;
                count++;}

            if (right != nullptr && right->isPassable()) {
                neighbors[count] = right;
                count++;}
            
            neighbors[count] = nullptr;

            return neighbors;
        }

        cell* getStart() {return start;}
        cell* getExit() {return exit;}
        int getWidth() {return width;}
        int getHeight() {return height;}        
};

class MazeBuilder {
    public:
        virtual maze* buildFromFile(const std::string& filename) = 0;
        virtual ~MazeBuilder() {}
};

class TextFileMazeBuilder : public MazeBuilder {
    public:
        maze* buildFromFile(const std::string& filename) override
        {

            std::ifstream file(filename);
            if (!file.is_open())
                throw std::runtime_error("Ошибка: Не удалось открыть файл!");

            std::string line;
            int width = 0;
            int height = 0;

            while (std::getline(file, line)) {
                if (height == 0) {
                    width = line.length();
                }
                else {
                    if (line.length() != width)
                        throw std::runtime_error("Ошибка: строки лабиринта разной длины!");
                }
                height++;
            }

            if (width == 0 || height == 0) {
                throw std::runtime_error("Ошибка: файл пустой!");
            }

            file.clear();
            file.seekg(0);

            maze* labirint = new maze(width, height);
            bool hasStart = false;
            bool hasExit = false;
            int y = 0;


            while (std::getline(file, line)) {

                for (int x = 0; x < width; x++) {
                    char ch = line[x];

                    bool isWall = false;
                    bool isStart = false;
                    bool isExit = false;

                    switch(ch){
                        case '#':
                            isWall = true;
                            break;
                        case ' ':
                            isWall = false;
                            break;
                        case 'S':
                            isStart = true;

                            if (hasStart)
                                throw std::runtime_error("Ошибка: в лабиринте больше одного старта!");

                            hasStart = true;
                            break;
                        case 'E':
                            isExit = true;

                            if (hasExit)
                                throw std::runtime_error("Ошибка: в лабиринте больше одного выхода!");
                            hasExit = true;
                            break;
                        default:
                            throw std::runtime_error("Ошибка: неизвестный символ в файле!");
                            break;
                    }

                    cell current(x, y, isWall, isExit, isStart);
                    labirint->setCell(x, y, current);
                }

                y++;
            }
            file.close();
            if (!hasStart)
                throw std::runtime_error("Ошибка: в лабиринте нет старта!");
            if (!hasExit)
                throw std::runtime_error("Ошибка: в лабиринте нет выхода!");
            return labirint;
        }
};


class PathFindingStrategy {
    public:
        virtual cell** findPath(maze* m, cell* start, cell* exit) = 0;
        virtual int getVisitedCells() = 0; /*для посещенных клеток*/
        virtual ~PathFindingStrategy() {}
};

class PathBuilder {
    public:
        static cell** buildPath(cell* start, cell* exit, cell*** parent) {
            int length = 0;
            cell* current = exit;

            while (current != nullptr) {
                length++;
                if (current == start)
                    break;
                current = parent[current->getX()][current->getY()];
            }

            cell** path = new cell*[length + 1];
            current = exit;

            for (int i = length - 1; i >= 0; i--) {
                path[i] = current;
                if (current == start)
                    break;
                current = parent[current->getX()][current->getY()];
            }
            path[length] = nullptr;
            return path;
        }
};

class BFSStrategy : public PathFindingStrategy {
    private:
        int visitedCells;
    public:

        BFSStrategy() {visitedCells = 0;}

        int getVisitedCells() override {return visitedCells;}

        cell** findPath(maze* m, cell* start, cell* exit) override {
            visitedCells = 0;
            int width = m->getWidth();
            int height = m->getHeight();
            bool** visited = new bool*[width];
            cell*** parent = new cell**[width];

            for (int x = 0; x < width; x++) {
                visited[x] = new bool[height];
                parent[x] = new cell*[height];

                for (int y = 0; y < height; y++) {
                    visited[x][y] = false;
                    parent[x][y] = nullptr;
                }
            }

            cell** deque = new cell*[width * height];
            int head = 0;
            int tail = 0;

            deque[tail] = start;
            tail++;
            visited[start->getX()][start->getY()] = true;
            bool found = false;

            while (head < tail) {
                cell* current = deque[head];
                head++;
                visitedCells++;

                if (current == exit) { //сравниваются указатели
                    found = true;
                    break;
                }

                cell** neighbors = m->getNeighbors(current);

                for (int i = 0; neighbors[i] != nullptr; i++) {
                    cell* next = neighbors[i];

                    int nx = next->getX();
                    int ny = next->getY();

                    if (!visited[nx][ny]) {
                        visited[nx][ny] = true;
                        parent[nx][ny] = current;

                        deque[tail] = next;
                        tail++;
                    }
                }
                delete[] neighbors;
            }

            cell** path;
            
            if (found) {
                path = PathBuilder::buildPath(start, exit, parent);
            } 
            else {
                path = new cell*[1];
                path[0] = nullptr;
            }

            delete[] deque;

            for (int x = 0; x < width; x++) {
                delete[] visited[x];
                delete[] parent[x];
            }
            delete[] visited;
            delete[] parent;
            return path;
        }
};

class DFSStrategy : public PathFindingStrategy {
    private:
        int visitedCells;
    public:
        DFSStrategy() {visitedCells = 0;}

        int getVisitedCells() override {return visitedCells;}

        cell** findPath(maze* m, cell* start, cell* exit) override {
            visitedCells = 0;
            int width = m->getWidth();
            int height = m->getHeight();
            bool** visited = new bool*[width];
            cell*** parent = new cell**[width];

            for (int x = 0; x < width; x++) {
                visited[x] = new bool[height];
                parent[x] = new cell*[height];

                for (int y = 0; y < height; y++) {
                    visited[x][y] = false;
                    parent[x][y] = nullptr;
                }
            }

            cell** stack = new cell*[width * height];
            int top = 0;
            stack[top] = start;
            top++;
            visited[start->getX()][start->getY()] = true;
            bool found = false;

            while (top > 0) {
                top--;
                cell* current = stack[top];
                visitedCells++;

                if (current == exit) { //сравниваются указатели
                    found = true;
                    break;
                }

                cell** neighbors = m->getNeighbors(current);

                for (int i = 0; neighbors[i] != nullptr; i++) {
                    cell* next = neighbors[i];

                    int nx = next->getX();
                    int ny = next->getY();

                    if (!visited[nx][ny]) {
                        visited[nx][ny] = true;
                        parent[nx][ny] = current;

                        stack[top] = next;
                        top++;
                    }
                }
                delete[] neighbors;
            }

            cell** path;
            
            if (found) {
                path = PathBuilder::buildPath(start, exit, parent);
            } 
            else {
                path = new cell*[1];
                path[0] = nullptr;
            }

            delete[] stack;

            for (int x = 0; x < width; x++) {
                delete[] visited[x];
                delete[] parent[x];
            }
            delete[] visited;
            delete[] parent;
            return path;
        }
};

class AStarStrategy : public PathFindingStrategy {
    private:
        int heuristic(cell* current, cell* exit) {return std::abs(current->getX() - exit->getX()) + std::abs(current->getY() - exit->getY());}
        int visitedCells;

    public:
        AStarStrategy() {visitedCells = 0;}

        int getVisitedCells() override {return visitedCells;}

        cell** findPath(maze* m, cell* start, cell* exit) override {
            visitedCells = 0;
            int width = m->getWidth();
            int height = m->getHeight();

            bool** closed = new bool*[width];   /*клетка [x][y] посещена (да/нет)*/
            bool** inOpen = new bool*[width];   /*клетка [x][y] имеет потенциал к посещению (да/нет)*/
            int** gScore = new int*[width];     /* до клетка [x][y] от старта gSchore[x][y] шагов*/
            int** fScore = new int*[width];     /*f = h + g, где h - эвристика клетки[x][y]*/

            cell*** parent = new cell**[width];


            for (int x = 0; x < width; x++) {
                closed[x] = new bool[height];
                inOpen[x] = new bool[height];
                gScore[x] = new int[height];
                fScore[x] = new int[height];

                parent[x] = new cell*[height];

                for (int y = 0; y < height; y++) {
                    closed[x][y] = false;
                    inOpen[x][y] = false;

                    gScore[x][y] = width * height + 100000; /*тупо большое число чтоб было больше чем клеток в лаберинте*/
                    fScore[x][y] = width * height + 100000;

                    parent[x][y] = nullptr;
                }
            }

            cell** open = new cell*[width * height]; /*клетки с потенциалом на посещение*/
            int openCount = 0; /*это количество потенц клеток, а также индекс следующего незанятого места*/

            int sx = start->getX();
            int sy = start->getY();

            gScore[sx][sy] = 0;
            fScore[sx][sy] = heuristic(start, exit);

            open[openCount] = start;
            openCount++;

            inOpen[sx][sy] = true;
            bool found = false;

            while (openCount > 0) {
                int bestIndex = 0;

                for (int i = 1; i < openCount; i++) {
                    int ix = open[i]->getX();
                    int iy = open[i]->getY();

                    int bx = open[bestIndex]->getX();
                    int by = open[bestIndex]->getY();

                    if (fScore[ix][iy] < fScore[bx][by]) {
                        bestIndex = i; /*(fSchore наименьший в [bestIndex])*/
                    }
                }

                cell* current = open[bestIndex];

                if (current == exit) {
                    found = true;
                    visitedCells++; /*чтоб выход засчитывался*/
                    break;
                }

                int cx = current->getX();
                int cy = current->getY();

                open[bestIndex] = open[openCount - 1];
                openCount--;

                inOpen[cx][cy] = false;
                closed[cx][cy] = true;
                visitedCells++;

                cell** neighbors = m->getNeighbors(current);

                for (int i = 0; neighbors[i] != nullptr; i++) {
                    cell* next = neighbors[i];

                    int nx = next->getX();
                    int ny = next->getY();

                    if (closed[nx][ny]) {
                        continue;
                    }

                    int tentativeG = gScore[cx][cy] + 1;

                    if (tentativeG < gScore[nx][ny]) {
                        parent[nx][ny] = current;

                        gScore[nx][ny] = tentativeG;
                        fScore[nx][ny] = gScore[nx][ny] + heuristic(next, exit);

                        if (!inOpen[nx][ny]) {
                            open[openCount] = next;
                            openCount++;

                            inOpen[nx][ny] = true;
                        }
                    }
                }
                delete[] neighbors;
            }

            cell** path;
            if (found) {
                path = PathBuilder::buildPath(start, exit, parent);
            }
            else {
                path = new cell*[1];
                path[0] = nullptr;
            }

            delete[] open;

            for (int x = 0; x < width; x++) {
                delete[] closed[x];
                delete[] inOpen[x];
                delete[] gScore[x];
                delete[] fScore[x];
                delete[] parent[x];
            }
            delete[] closed;
            delete[] inOpen;
            delete[] gScore;
            delete[] fScore;
            delete[] parent;

            return path;
        }
};

class SearchStats {
public:
    double timeMs;
    int visitedCells;
    int pathLength;

    SearchStats(double timeMs, int visitedCells, int pathLength) {
        this->timeMs = timeMs;
        this->visitedCells = visitedCells;
        this->pathLength = pathLength;
    }
};

class MazeSolver{
    private:
        maze* labirint;
        PathFindingStrategy* strategy;
    public:
        MazeSolver(maze* labirint) {this->labirint = labirint; this->strategy = nullptr;}

        void setStrategy(PathFindingStrategy* strategy){
            this->strategy = strategy;
        }

        SearchStats solve(){
            auto start = std::chrono::high_resolution_clock::now();
            cell** path = strategy->findPath(labirint,labirint->getStart(),labirint->getExit());
            auto end = std::chrono::high_resolution_clock::now();
            std::chrono::duration<double, std::milli> duration = end - start;

            int pathLength = 0;

            if (path[0] != nullptr)
                while (path[pathLength] != nullptr) {pathLength++;}

            int visitedCells = 0;
            visitedCells = strategy->getVisitedCells();
            delete[] path;

            SearchStats stats(duration.count(), visitedCells, pathLength);
            return stats;
        }
};

class Player{
    private:
        cell* current;
    public: 
        Player(cell* current) {this->current = current;}

        cell* getCurrent() {return current;}
        void moveTo(cell* cell) {this->current = cell;}
};

class Direction{
    private:
        int dx;
        int dy;

    public:
        Direction(int dx, int dy) {
            this->dx = dx;
            this->dy = dy;
        }

        int getDx() {return dx;}
        int getDy() {return dy;}
};

class Command {
    public:
        virtual void execute() = 0;
        virtual void undo() = 0;
        virtual ~Command() {}
};

class MoveCommand : public Command{
    private:
        Player* player;
        Direction dir;
        cell* previousCell;
        maze* labirint;
    public:
        MoveCommand(Player* player, maze* labirint, Direction dir) : dir(dir) {
            this->player = player;
            this->labirint = labirint;
            this->previousCell = nullptr;
        }

        void execute() override {
            cell* currentCell = player->getCurrent();

            int newX = currentCell->getX() + dir.getDx();
            int newY = currentCell->getY() + dir.getDy();

            cell* nextCell = labirint->getCell(newX, newY);

            if (nextCell != nullptr && nextCell->isPassable()) {
                previousCell = currentCell;
                player->moveTo(nextCell);
            }
            else {
                std::cout << "Нельзя сделать ход!" << std::endl;
            }
        }

        void undo() override {
            if (previousCell != nullptr) {
                player->moveTo(previousCell);
                previousCell = nullptr;
            }
        }

};

class ConsolController{
    private:
        maze* labirint;
        Player* player;
        Command* lastCommand; /*указатель на последнюю команду(на объект класса command)*/
        bool running;
    public:
        ConsolController(maze* labirint, Player* player) {
            this->labirint = labirint;
            this->player = player;
            this->lastCommand = nullptr;
            this->running = false;
        }

        ~ConsolController() {
            if (lastCommand != nullptr) {
                delete lastCommand;
            }
        }

        void run() {
            running = true;

            while (running) {
                clearConsole();
                drawMaze();

                std::cout << "W/A/S/D - ход, Z - отмена, Q - выход" << std::endl;
                char ch;
                std::cin >> ch;

                handleInput(ch);
            }
        }

        void handleInput(char ch) {
            switch(ch){
                case 'q':
                case 'Q':
                    running = false;
                    break;
                case 'z':
                case 'Z':
                    undoLastMove();
                    break;
                default:
                    handleMove(ch);
                    break;
            }
        }

        void handleMove(char ch) {
            Direction dir(0, 0);

            switch (ch) {
                case 'w':
                case 'W':
                    dir = Direction(0, -1);
                    break;
                case 's':
                case 'S':
                    dir = Direction(0, 1);
                    break;
                case 'a':
                case 'A':
                    dir = Direction(-1, 0);
                    break;
                case 'd':
                case 'D':
                    dir = Direction(1, 0);
                    break;
                default:
                    return;
            }

            if (lastCommand != nullptr) {
                delete lastCommand;
                lastCommand = nullptr;
            }

            lastCommand = new MoveCommand(player, labirint, dir);
            lastCommand->execute();
        }

        void undoLastMove() {
            if (lastCommand != nullptr) {
                lastCommand->undo();
                delete lastCommand;
                lastCommand = nullptr; /*можно отменить только одну команду назад, указатель делаем 0, чтобы не долбится в отмену уже отмененной команды*/
            }
        }

        void clearConsole() {
            #ifdef _WIN32
                system("cls");
            #else
                system("clear");
            #endif
        }

        void drawMaze() {
            for (int y = 0; y < labirint->getHeight(); y++) {
                for (int x = 0; x < labirint->getWidth(); x++) {
                    cell* currentCell = labirint->getCell(x, y);

                    if (currentCell == player->getCurrent())
                        std::cout << "P";
                    else if (currentCell->getIsWall())
                        std::cout << "#";
                    else if (currentCell->getIsStart())
                        std::cout << "S";
                    else if (currentCell->getIsExit())
                        std::cout << "E";
                    else
                        std::cout << " ";
                }
            std::cout << std::endl;
            }
        }
};

class Benchmark {
    private:
        int RUNS = 10;
    public:
    Benchmark(int runs) {this->RUNS = runs;}
    void benchmark(){    
        std::string mazeFiles[] = {
            "SolovevDS/docs/data/data_for_task2/maze10.txt",
            "SolovevDS/docs/data/data_for_task2/maze50.txt",
            "SolovevDS/docs/data/data_for_task2/maze100.txt",
            "SolovevDS/docs/data/data_for_task2/maze_empty.txt",
            "SolovevDS/docs/data/data_for_task2/maze_no_path.txt"
        };

        std::string mazeNames[] = {
            "small_10",
            "medium_50",
            "large_100",
            "empty",
            "no_path"
        };

        std::ofstream csv("SolovevDS/docs/data/data_for_task2/results.csv");

        if (!csv.is_open())
            throw std::runtime_error("Ошибка: не удалось создать results.csv!");

        csv << "maze,strategy,time_ms,cells_visited,way_len\n";

        TextFileMazeBuilder builder;

        for (int i = 0; i < 5; i++) {
            maze* labirint = builder.buildFromFile(mazeFiles[i]);

            MazeSolver solver(labirint);

            BFSStrategy bfs;
            DFSStrategy dfs;
            AStarStrategy astar;

            PathFindingStrategy* strategies[] = {&bfs, &dfs, &astar};
            std::string strategyNames[] = {"BFS", "DFS", "AStar"};

            for (int s = 0; s < 3; s++) {
                double sumTime = 0;
                double sumVisited = 0;
                double sumPathLength = 0;

                for (int run = 0; run < RUNS; run++) {
                    solver.setStrategy(strategies[s]);

                    SearchStats stats = solver.solve();

                    sumTime += stats.timeMs;
                    sumVisited += stats.visitedCells;
                    sumPathLength += stats.pathLength;
                }

                double avgTime = sumTime / RUNS;
                double avgVisited = sumVisited / RUNS;
                double avgPathLength = sumPathLength / RUNS;

                csv << mazeNames[i] << ","
                    << strategyNames[s] << ","
                    << std::fixed << std::setprecision(5) << avgTime << ","
                    << avgVisited << ","
                    << avgPathLength << "\n";
            }
            delete labirint;
        }

        csv.close();
    }
};


int main(){
    SetConsoleCP(CP_UTF8);
    SetConsoleOutputCP(CP_UTF8);
    setlocale(LC_ALL, ".UTF-8");
    Benchmark ben(10);
    ben.benchmark();

    TextFileMazeBuilder builder;
    maze* labirint = builder.buildFromFile("SolovevDS/docs/data/data_for_task2/maze10.txt");
    Player player(labirint->getStart());
    ConsolController controller(labirint, &player);
    controller.run();
    delete labirint;

    return 0;
}
