#include "Game.h"

#include "GameBoard.h"
#include "GameParameters.h"
#include "Action.h"

#include <set>
#include <algorithm>
#include <memory>

static const int SUBPX_PER_ROW = 60;          // the number of position the active piece can take "between" two rows
static const int LOCK_DELAY_SCORE = 1;        // the score gained when the piece locks due to lock delay
static const int HARD_DROP_SCORE = 10;        // the score gained when the piece locks due to an hard drop
static const int LINE_CLEAR_BASE_SCORE = 100; // the score value of clearing a single line
static const int B2B_SCORE_MULTIPLIER = 2;    // by how much havaing B2B on multiplies the score of the line clear
static const int B2B_MIN_LINE_NUMBER = 4;     // the minimum number of lines needed to be cleared at once to gain B2B (without a spin)


Game::Game(Gamemode gamemode, const Player& controls, int boardWidth, int boardHeight, const std::shared_ptr<PiecesList>& piecesList) :
    parameters(gamemode, controls),
    board(boardWidth, boardHeight, piecesList, parameters.getNextQueueLength()) {
        
    this->initialize();
}

void Game::start() {
    this->started = true;
    this->leftARETime = 1;
}

void Game::reset() {
    this->initialize();

    this->parameters.reset();
    this->board.reset();
}

void Game::initialize() {
    this->started = false;
    this->lost = false;

    this->score = 0;
    this->framesPassed = 0;
    this->B2BChain = 0;

    this->heldActions.clear();
    this->initialActions.clear();
    this->heldDAS = 0;
    this->heldARR = 0;
    this->subVerticalPosition = 0;
    this->totalLockDelay = 0;
    this->totalForcedLockDelay = 0;
}

void Game::nextFrame(const std::set<Action>& playerActions) {
    if (this->lost || this->hasWon()) return;

    bool pieceJustLocked = false;

    if (this->started) {
        bool AREJustEnded = (this->leftARETime == 1);
        if (this->leftARETime > 0) {
            this->leftARETime--;
        }

        if (this->leftARETime == 0) {
            if (AREJustEnded) {
                this->lost = this->board.spawnNextPiece();
                this->resetPiece(true);

                /* IRS and IHS */
                bool initialRotated = (this->initialActions.contains(ROTATE_0) || this->initialActions.contains(ROTATE_CW)
                                       || this->initialActions.contains(ROTATE_180) || this->initialActions.contains(ROTATE_CCW));
                Rotation initialRotation = NONE
                    + ((this->initialActions.contains(ROTATE_CW)) ? CLOCKWISE : NONE)
                    + ((this->initialActions.contains(ROTATE_180)) ? DOUBLE : NONE)
                    + ((this->initialActions.contains(ROTATE_CCW)) ? COUNTERCLOCKWISE : NONE);

                if (this->initialActions.contains(HOLD)) {
                    this->board.hold(initialRotation);
                }
                else {
                    if (initialRotated) {
                        this->board.rotate(initialRotation);
                    }
                }

                this->lost = this->board.activePieceInWall();
                if (this->lost) {
                    this->board.rotate(NONE);
                    this->lost = this->board.activePieceInWall();

                    if (this->lost) {
                        this->framesPassed++;
                        return;
                    }
                }
            }

            /* HOLD */
            if (playerActions.contains(HOLD) && (!this->heldActions.contains(HOLD))) {
                if (this->board.hold({})) {
                    this->resetPiece(false);
                }
            }

            /* MOVE LEFT/RIGHT */
            Position before = this->board.getActivePiecePosition();

            if (this->heldDAS >= 0) {
                if (playerActions.contains(MOVE_LEFT) && (!heldActions.contains(MOVE_LEFT))) this->movePiece(-1);
                else if (playerActions.contains(MOVE_RIGHT)) this->movePiece(1);
                else this->heldDAS = 0;
            }
            else if (this->heldDAS < 0) {
                if (playerActions.contains(MOVE_RIGHT) && (!heldActions.contains(MOVE_RIGHT))) this->movePiece(1);
                else if (playerActions.contains(MOVE_LEFT)) this->movePiece(-1);
                else this->heldDAS = 0;
            }

            if (before != this->board.getActivePiecePosition()) {
                this->totalLockDelay = 0;
            }

            /* ROTATIONS */
            if (playerActions.contains(ROTATE_0) && (!this->heldActions.contains(ROTATE_0))) {
                this->rotatePiece(NONE);
            }
            if (playerActions.contains(ROTATE_CW) && (!this->heldActions.contains(ROTATE_CW))) {
                this->rotatePiece(CLOCKWISE);
            }
            if (playerActions.contains(ROTATE_180) && (!this->heldActions.contains(ROTATE_180))) {
                this->rotatePiece(DOUBLE);
            }
            if (playerActions.contains(ROTATE_CCW) && (!this->heldActions.contains(ROTATE_CCW))) {
                this->rotatePiece(COUNTERCLOCKWISE);
            }
            
            /* SOFT DROP */
            if (playerActions.contains(SOFT_DROP)) {
                int appliedSDR = this->parameters.getSDR();

                // SDR=0 -> instant drop
                if (appliedSDR == 0) {
                    while (this->board.moveDown());
                }
                // SDR>1 -> move down by specified amount
                else {
                    this->subVerticalPosition += (SUBPX_PER_ROW / appliedSDR);
                    while (this->subVerticalPosition >= SUBPX_PER_ROW) {
                        this->board.moveDown();
                        this->subVerticalPosition -= SUBPX_PER_ROW;
                    }
                }
            }

            /* HARD DROP */
            // needs to be done last because we can enter ARE period afterwards
            if (this->initialActions.contains(HARD_DROP) || (playerActions.contains(HARD_DROP) && (!this->heldActions.contains(HARD_DROP)))) {
                while (this->board.moveDown());
                this->lockPiece();
                pieceJustLocked = true;
                this->score += HARD_DROP_SCORE;
            }
            // no need to apply gravity and lock delay if the piece was hard dropped
            else {
                /* GRAVITY */
                if (this->parameters.getLevel() >= 20) {
                    while (this->board.moveDown());
                }
                else {
                    // parameters.getGravity() gives the gravity for an assumed 20-line high board
                    int appliedGravity = this->parameters.getGravity() * std::max((double) this->board.getBoard().getBaseHeight() / 20.0, 1.0);

                    this->subVerticalPosition += appliedGravity;
                    while (this->subVerticalPosition >= SUBPX_PER_ROW) {
                        this->subVerticalPosition -= SUBPX_PER_ROW;
                        this->board.moveDown();
                    }
                }
                
                /* LOCK DELAY */
                if (this->board.touchesGround()) {
                    this->totalLockDelay++;
                    this->totalForcedLockDelay++;
                }
                else {
                    this->totalLockDelay = 0;
                }

                if ((this->totalLockDelay > this->parameters.getLockDelay()) || (this->totalForcedLockDelay > this->parameters.getForcedLockDelay())) {
                    this->lockPiece();
                    pieceJustLocked = true;
                    this->score += LOCK_DELAY_SCORE;
                }
            }

            if (AREJustEnded) {
                this->initialActions.clear();
            }
        }

        this->framesPassed++;
        if (this->lost) {
            return;
        }
    }
        
    if ((!this->started) || this->leftARETime > 0) {
        for (Action action : playerActions) {
            if ((!pieceJustLocked) && (!heldActions.contains(action))) {
                this->initialActions.insert(action);
            }
        }

        if (this->heldDAS >= 0) {
            if (playerActions.contains(MOVE_LEFT)) this->heldDAS = -1;
            else if (playerActions.contains(MOVE_RIGHT)) this->heldDAS++;
            else this->heldDAS = 0;
        }
        else if (this->heldDAS < 0) {
            if (playerActions.contains(MOVE_RIGHT)) this->heldDAS = +1;
            else if (playerActions.contains(MOVE_LEFT)) this->heldDAS--;
            else this->heldDAS = 0;
        }
    }

    this->heldActions = playerActions;
}

void Game::resetPiece(bool newPiece) {
    int appliedDAS = this->parameters.getDAS();

    this->subVerticalPosition = 0;
    this->totalLockDelay = 0;
    if (newPiece) {
        this->totalForcedLockDelay = 0;
    }
    if (abs(this->heldDAS) > appliedDAS) {
        this->heldDAS = (this->heldDAS > 0) ? (+appliedDAS) : (-appliedDAS);
    }
    this->heldARR = 0;
}

void Game::movePiece(int movement) {
    int appliedDAS = this->parameters.getDAS();
    int appliedARR = this->parameters.getARR();

    if ((this->heldDAS * movement) <= 0) {
        this->heldDAS = movement;
        this->heldARR = 0;
        if (movement == -1) this->board.moveLeft();
        if (movement == 1) this->board.moveRight();
    }
    else {
        this->heldDAS += movement;
    }

    if (abs(this->heldDAS) > appliedDAS) {
        // ARR=0 -> instant movement
        if (appliedARR == 0) {
            if (movement == -1) while (this->board.moveLeft());
            if (movement == 1) while (this->board.moveRight());
        }

        // ARR>1 -> move by specified amount
        else {
            if (abs(this->heldDAS) > appliedDAS + 1) {
                this->heldARR++;
            }
            if ((this->heldARR == appliedARR) || (abs(this->heldDAS) == (appliedDAS + 1))) {
                this->heldARR = 0;
                if (movement == -1) this->board.moveLeft();
                if (movement == 1) this->board.moveRight();
            }
        }
    }
}

void Game::rotatePiece(Rotation rotation) {
    Position before = this->board.getActivePiecePosition();

    if (this->board.rotate(rotation)) {
        this->totalLockDelay = 0;
        if (before != this->board.getActivePiecePosition()) {
            this->subVerticalPosition = 0;
        }
    }
}

void Game::lockPiece() {
    LineClear clear = this->board.lockPiece();
    this->parameters.lockedPiece(clear);

    if (clear.lines > 0) {
        bool B2BConditionsAreMet = ((clear.lines >= B2B_MIN_LINE_NUMBER) || clear.isSpin || clear.isMiniSpin);

        /* clearing one more line is worth 2x more
            clearing with a spin is worth as much as clearing 2x more lines */
        long int clearScore = LINE_CLEAR_BASE_SCORE / 2;
        clearScore = clearScore << (clear.lines << clear.isSpin);

        if (this->B2BChain && B2BConditionsAreMet) {
            clearScore *= B2B_SCORE_MULTIPLIER;
        }
        this->score += clearScore;

        this->B2BChain = B2BConditionsAreMet;
    }

    if (!this->hasWon()) {
        this->leftARETime = this->parameters.getARE();
        if (this->leftARETime == 0) {
            this->lost = this->board.spawnNextPiece();
            this->resetPiece(true);
            
            if (this->lost) {
                this->board.rotate(NONE);
                this->lost = this->board.activePieceInWall();
            }
        }
    }   
}

bool Game::hasWon() const {
    return this->parameters.hasWon(this->framesPassed);
}

bool Game::hasLost() const {
    return this->lost;
}

int Game::getClearedLines() const {
    return this->parameters.getClearedLines();
}

int Game::getGrade() const {
    return this->parameters.getGrade();
}

int Game::getLevel() const {
    return this->parameters.getLevel();
}

int Game::getFramesPassed() const {
   return this->framesPassed;
}

int Game::getScore() const {
    return this->score;
}

bool Game::isOnB2BChain() const {
    return this->B2BChain;
}

float Game::getLockDelayProgression() const {
    return (float) this->totalLockDelay / this->parameters.getLockDelay();
}

float Game::getForcedLockDelayProgression() const {
    return (float) this->totalForcedLockDelay / this->parameters.getForcedLockDelay();
}

bool Game::areBlocksBones() const {
    return this->parameters.getBoneBlocks();
}

bool Game::isBoardInvisible() const {
    return this->parameters.getInvisibleBoard();
}

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

const std::shared_ptr<Piece>& Game::getActivePiece() const {
    return this->board.getActivePiece();
}

const Position& Game::getActivePiecePosition() const {
    return this->board.getActivePiecePosition();
}

Position Game::getGhostPiecePosition() const {
    return this->board.lowestPosition();
}

const std::shared_ptr<Piece>& Game::getHeldPiece() const {
    return this->board.getHeldPiece();
}

const std::vector<Piece>& Game::getNextPieces() const {
    return this->board.getNextPieces();
}
