#include "GameBoard.h"

#include "../Pieces/Piece.h"
#include "Board.h"
#include "Bag.h"
#include "LineClear.h"

#include <vector>
#include <set>
#include <memory>
#include <utility>
#include <cstdlib>
#include <iostream>
#include <optional>


GameBoard::GameBoard(int boardWidth, int boardHeight, const std::shared_ptr<PiecesList>& piecesList, int nextQueueLength) :
    board(boardWidth, boardHeight),
    generator(piecesList),
    nextQueueLength(nextQueueLength) {
        
    this->initialize();
}

void GameBoard::reset() {
    this->board.clearBoard();
    this->generator.jumpToNextBag();

    this->initialize();
}

void GameBoard::initialize() {
    this->nextQueue.clear();
    for (int i = 0; i < nextQueueLength; i++) {
        this->nextQueue.push_back(this->generator.getNext());
    }

    this->activePiece = nullptr;
    this->heldPiece = nullptr;
    this->isLastMoveKick = false;
    this->movedLeftLast = false;
}

bool GameBoard::moveLeft() {
    this->movedLeftLast = true;

    if (this->activePieceInWall(Position(-1, 0))) {
        return false;
    }
    else {
        this->activePiecePosition.x -= 1;
        this->isLastMoveKick = false;
        return true;
    }
}

bool GameBoard::moveRight() {
    this->movedLeftLast = false;

    if (this->activePieceInWall(Position(1, 0))) {
        return false;
    }
    else {
        this->activePiecePosition.x += 1;
        this->isLastMoveKick = false;
        return true;
    }
}

bool GameBoard::moveDown() {
    if (this->activePieceInWall(Position(0, -1))) {
        return false;
    }
    else {
        this->activePiecePosition.y -= 1;
        this->isLastMoveKick = false;
        return true;
    }
}

bool GameBoard::rotate(Rotation rotation) {
    Piece stored = *this->activePiece;
    this->activePiece->rotate(rotation);

    // before trying to kick, check if the piece can rotate without kicking
    if (rotation == NONE) {
        if (this->moveDown()) {
            this->isLastMoveKick = false;
            return true;
        }
    }
    else {
        if (!this->activePieceInWall()) {
            this->isLastMoveKick = false;
            return true;
        }
    }

    std::set<Position> safePositions;
    for (Position position : stored.getPositions()) {
        Position positionInGrid(position + this->activePiecePosition);
        safePositions.insert(positionInGrid);
        safePositions.insert(positionInGrid + Position(0, 1));
        safePositions.insert(positionInGrid + Position(1, 0));
        safePositions.insert(positionInGrid + Position(0, -1));
        safePositions.insert(positionInGrid + Position(-1, 0));
    }

    // first try kicking the piece down
    if (rotation == NONE) {
        this->activePiecePosition.y -= 1;
    }
    bool suceeded = this->tryKicking(true, safePositions);
    if (suceeded) {
        this->isLastMoveKick = true;
        return true;
    }

    // if it doesn't work try kicking the piece up
    if (rotation == NONE) {
        this->activePiecePosition.y += 1;
    }
    suceeded = this->tryKicking(false, safePositions);
    if (suceeded) {
        this->isLastMoveKick = true;
        return true;
    }

    // if it still doesn't work, abort the rotation
    this->activePiece = std::make_shared<Piece>(stored);
    if (rotation == NONE) {
        this->isLastMoveKick = false;
    }
    return (rotation == NONE);
}

bool GameBoard::tryKicking(bool testingBottom, const std::set<Position>& safePositions) {
    // we try from the original height of the piece, moving vertically as long as the kicked piece touches the original at least once on this row
    bool overlapsVertically = true;
    int j = 0;
    do {
        // we try from the center to the sides as long as the kicked piece touches the original
        bool overlapsLeft = true;
        bool overlapsRight = true;
        int i = (j == 0) ? 1 : 0;
        do {
            // we first check the side to which the player moved last
            if (movedLeftLast) {
                if (overlapsLeft) {
                    if (this->tryFittingKickedPiece(safePositions, Position(-i, j), overlapsLeft)) return true;
                }
                if (overlapsRight) {
                    if (this->tryFittingKickedPiece(safePositions, Position(+i, j), overlapsRight)) return true;
                }
            }
            else {
                if (overlapsRight) {
                    if (this->tryFittingKickedPiece(safePositions, Position(+i, j), overlapsRight)) return true;
                }
                if (overlapsLeft) {
                    if (this->tryFittingKickedPiece(safePositions, Position(-i, j), overlapsLeft)) return true;
                }
            }

            i++;
        } while (overlapsLeft && overlapsRight);

        if (i == 1) {
            overlapsVertically = false;
        }

        (testingBottom) ? j-- : j++;
    } while (overlapsVertically);

    return false;
}

bool GameBoard::tryFittingKickedPiece(const std::set<Position>& safePositions, const Position& shift, bool& overlaps) {
    if (!this->activePieceOverlaps(safePositions, shift)) {
        overlaps = false;
    }
    else {
        if (!this->activePieceInWall(shift)) {
            this->activePiecePosition += shift;
            return true;
        }
    }
    return false;
}

bool GameBoard::activePieceOverlaps(const std::set<Position>& safePositions, const Position& shift) const {
    for (Position position : this->activePiece->getPositions()) {
        if (safePositions.contains(position + this->activePiecePosition + shift)) return true;
    }
    return false;
}

bool GameBoard::hold(std::optional<Rotation> initialRotation) {
    Position storedPosition = this->activePiecePosition;
    std::swap(this->activePiece, this->heldPiece);

    bool isFirstTimeHolding = (this->activePiece == nullptr);
    if (isFirstTimeHolding) {
        if (this->nextQueueLength == 0) {
            this->activePiece = std::make_shared<Piece>(this->generator.lookNext());
        }
        else {
            this->activePiece = std::make_shared<Piece>(this->nextQueue.front());
        }
    }

    // try initial rotation
    this->goToSpawnPosition();
    if (initialRotation.has_value()) {
        std::shared_ptr<Piece> storedPiece(this->activePiece);
        this->rotate(initialRotation.value());

        if (this->activePieceInWall()) {
            this->activePiece = storedPiece;
        }
    }

    // if the piece can't spawn, try 0° rotation
    if (this->activePieceInWall()) {
        std::shared_ptr<Piece> storedPiece(this->activePiece);
        this->rotate(NONE);

        // if the piece still can't spawn, abort holding
        if (this->activePieceInWall()) {
            this->activePiece = (isFirstTimeHolding) ? nullptr : storedPiece;
            std::swap(this->activePiece, this->heldPiece);
            this->activePiecePosition = storedPosition;
            return false;
        }
    }

    if (isFirstTimeHolding) {
        this->nextQueue.push_back(this->generator.getNext());
        this->nextQueue.erase(this->nextQueue.begin());
    }

    this->heldPiece->defaultRotation();

    this->isLastMoveKick = false;

    return true;
}

bool GameBoard::spawnNextPiece() {
    this->nextQueue.push_back(this->generator.getNext());
    this->activePiece = std::make_shared<Piece>(this->nextQueue.front());
    this->nextQueue.erase(this->nextQueue.begin());

    this->goToSpawnPosition();
    this->isLastMoveKick = false;

    return this->activePieceInWall();
}

bool GameBoard::activePieceInWall(const Position& shift) const {
    for (Position position : this->activePiece->getPositions()) {
        if (this->board.getBlock(position + this->activePiecePosition + shift) != NOTHING) return true;
    }
    return false;
}

bool GameBoard::touchesGround() const {
    return this->activePieceInWall(Position(0, -1));
}

Position GameBoard::lowestPosition() const {
    Position shift = Position(0, -1);
    while (!activePieceInWall(shift)) {
        shift.y -= 1;
    }
    shift.y += 1;
    return (this->activePiecePosition + shift);
}

LineClear GameBoard::lockPiece() {
    bool isLockedInPlace = (this->activePieceInWall(Position(0, 1)) && this->activePieceInWall(Position(1, 0))
        && this->activePieceInWall(Position(-1, 0)) && this->activePieceInWall(Position(0, -1)));

    for (Position position : this->activePiece->getPositions()) {
        this->board.changeBlock(position + this->activePiecePosition, this->activePiece->getBlockType());
    }

    this->activePiece = nullptr;

    return LineClear{this->board.clearRows(), isLockedInPlace, (!isLockedInPlace) && this->isLastMoveKick};
}

void GameBoard::addGarbageRows(int number) {
    int holePosition = std::rand() % this->board.getWidth();

    for (int i = 0; i < number; i++) {
        this->board.insertRow(0, holePosition, GARBAGE);
        if (this->touchesGround()) {
            this->activePiecePosition.y += 1;
        }
    }
}

const Board& GameBoard::getBoard() const {
    return this->board;
}

const std::shared_ptr<Piece>& GameBoard::getActivePiece() const {
    return this->activePiece;
}

const Position& GameBoard::getActivePiecePosition() const {
    return this->activePiecePosition;
}

const std::shared_ptr<Piece>& GameBoard::getHeldPiece() const {
    return this->heldPiece;
}

const std::vector<Piece>& GameBoard::getNextPieces() const {
    return this->nextQueue;
}

void GameBoard::goToSpawnPosition() {
    int lowestPosition = this->activePiece->getLength() - 1;
    for (Position position : this->activePiece->getPositions()) {
        if (position.y < lowestPosition) lowestPosition = position.y;
    }

    // set the piece one line above the board
    this->activePiecePosition.y = this->board.getBaseHeight() - lowestPosition;

    // center the piece horizontally, biased towards left
    this->activePiecePosition.x = (this->board.getWidth() - this->activePiece->getLength()) / 2;

    this->activePiece->defaultRotation();
}

std::ostream& operator<<(std::ostream& os, const GameBoard& gameboard) {
    // print over the board (only the active piece if it is there)
    if (gameboard.activePiece != nullptr) {
        Block pieceBlockType = gameboard.activePiece->getBlockType();
        os << getConsoleColorCode(pieceBlockType);

        // print only the position were the active piece is
        for (int y = gameboard.activePiecePosition.y + gameboard.activePiece->getLength() - 1; y >= gameboard.board.getBaseHeight(); y--) {
            for (int x = 0; x < gameboard.board.getWidth(); x++) {
                bool hasActivePiece = gameboard.activePiece->containsSquare(Position(x, y) - gameboard.activePiecePosition);
                if (hasActivePiece) {
                    os << "*";
                }
                else {
                    os << " ";
                }
            }
            os << std::endl;
        }
    }

    // print the board
    Block pieceBlockType = (gameboard.activePiece == nullptr) ? NOTHING : gameboard.activePiece->getBlockType();
    for (int y = gameboard.board.getBaseHeight() - 1; y >= 0; y--) {
        for (int x = 0; x < gameboard.board.getWidth(); x++) {
            bool hasActivePiece = (gameboard.activePiece == nullptr) ? false : gameboard.activePiece->containsSquare(Position(x, y) - gameboard.activePiecePosition);

            // the active piece takes visual priority over the board
            if (hasActivePiece) {
                os << getConsoleColorCode(pieceBlockType);
                os << "*";
            }
            else {
                Block block = gameboard.board.getBlock(Position(x, y));
                os << getConsoleColorCode(block);
                if (block != NOTHING) {
                    os << "*";
                }
                else {
                    os << "-";
                }
            }
        }
        os << std::endl;
    }

    // print hold box
    os << "Hold:" << std::endl;
    if (!(gameboard.heldPiece == nullptr)) {
        os << *gameboard.heldPiece;
    }

    // print next queue
    os << "Next:" << std::endl;
    for (const Piece& piece : gameboard.nextQueue) {
        os << piece;
    }

    os << getResetConsoleColorCode();
    
    return os;
}
