package uk.mgrove.ac.soton.comp1206.game;

import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import uk.mgrove.ac.soton.comp1206.component.GameBlock;

import java.util.*;

/**
 * The Game class handles the main logic, state and properties of the TetrECS game. Methods to manipulate the game state
 * and to handle actions made by the player should take place inside this class.
 */
public class Game {

    /**
     * Logger
     */
    private static final Logger logger = LogManager.getLogger(Game.class);

    /**
     * Random number generator
     */
    private final Random random = new Random();

    /**
     * Number of rows
     */
    protected final int rows;

    /**
     * Number of columns
     */
    protected final int cols;

    /**
     * The grid model linked to the game
     */
    protected final Grid grid;

    /**
     * Current game piece player is using
     */
    private GamePiece currentPiece;

    /**
     * Player's current score
     */
    protected IntegerProperty score = new SimpleIntegerProperty(0);
    /**
     * Player's current level
     */
    protected IntegerProperty level = new SimpleIntegerProperty(0);
    /**
     * Player's number of remaining lives
     */
    protected IntegerProperty lives = new SimpleIntegerProperty(3);
    /**
     * Player's current multiplier
     */
    protected IntegerProperty multiplier = new SimpleIntegerProperty(1);

    /**
     * Create a new game with the specified rows and columns. Creates a corresponding grid model.
     * @param cols number of columns
     * @param rows number of rows
     */
    public Game(int cols, int rows) {
        this.cols = cols;
        this.rows = rows;

        //Create a new grid model to represent the game state
        this.grid = new Grid(cols,rows);
    }

    /**
     * Start the game
     */
    public void start() {
        logger.info("Starting game");
        initialiseGame();
    }

    /**
     * Get the next piece for the player to use in the game
     * @return the next piece
     */
    public GamePiece nextPiece() {
        currentPiece = spawnPiece();
        logger.info("Next piece is: {}", currentPiece);
        return currentPiece;
    }

    /**
     * Create a new game piece
     * @return newly created piece
     */
    public GamePiece spawnPiece() {
        var maxPieces = GamePiece.PIECES;
        var randomPieceNumber = random.nextInt(maxPieces);
        logger.info("Picking random piece: {}", randomPieceNumber);
        return GamePiece.createPiece(randomPieceNumber);
    }

    /**
     * Initialise a new game and set up anything that needs to be done at the start
     */
    public void initialiseGame() {
        logger.info("Initialising game");
        nextPiece();
    }

    /**
     * Handle what should happen when a particular block is clicked
     * @param gameBlock the block that was clicked
     */
    public void blockClicked(GameBlock gameBlock) {
        //Get the position of this block
        int x = gameBlock.getX();
        int y = gameBlock.getY();

        if (grid.canPlayPiece(currentPiece,x,y)) {
            grid.playPiece(currentPiece,x,y);
            afterPiece();
            nextPiece();
        } else {
            // can't play the piece
        }
    }

    /**
     * Handle additional processing after piece has been played - clear lines of blocks
     */
    public void afterPiece() {
        logger.info("Checking for columns and rows that need clearing");

        Set<IntegerProperty> blocksToRemove = new HashSet<>();
        int linesToRemove = 0;

        for (var x=0; x < grid.getCols(); x++) {
            List<IntegerProperty> columnBlocksToRemove = new ArrayList<>();
            for (var y=0; y < grid.getRows(); y++) {
                // if column isn't full then move to next column
                if (grid.get(x,y) <= 0) break;

                columnBlocksToRemove.add(grid.getGridProperty(x,y));
            }
            // if column is full then store blocks to reset
            if (columnBlocksToRemove.size() == grid.getRows()) {
                for (var block : columnBlocksToRemove) {
                    blocksToRemove.add(block);
                    linesToRemove++;
                }
            }
        }

        // do the same for rows
        for (var y=0; y < grid.getRows(); y++) {
            List<IntegerProperty> rowBlocksToRemove = new ArrayList<>();
            for (var x=0; x < grid.getCols(); x++) {
                // if row isn't full then move to next row
                if (grid.get(x,y) <= 0) break;

                rowBlocksToRemove.add(grid.getGridProperty(x,y));
            }
            // if row is full then store blocks to reset
            if (rowBlocksToRemove.size() == grid.getCols()) {
                for (var block : rowBlocksToRemove) {
                    blocksToRemove.add(block);
                    linesToRemove++;
                }
            }
        }

        // update score and multiplier
        score(linesToRemove,blocksToRemove.size());

        // reset blocks that need resetting
        for (var block : blocksToRemove) {
            block.set(0);
        }
    }

    /**
     * Update the score, multiplier, and level depending on the number of lines and blocks that have been cleared
     * @param lines number of lines cleared
     * @param blocks number of blocks cleared
     */
    private void score(int lines, int blocks) {
        logger.info("Updating score");

        var addScore = lines * blocks * 10 * multiplier.get();
        score.set(score.get() + addScore);

        logger.info("Updating multiplier");
        if (lines > 0) multiplier.set(multiplier.get() + 1);
        else multiplier.set(1);

        logger.info("Setting the level based on the new score");
        // set level
        level.set((int) Math.floor((double) score.get() / 1000));
    }

    /**
     * Get the grid model inside this game representing the game state of the board
     * @return game grid model
     */
    public Grid getGrid() {
        return grid;
    }

    /**
     * Get the number of columns in this game
     * @return number of columns
     */
    public int getCols() {
        return cols;
    }

    /**
     * Get the number of rows in this game
     * @return number of rows
     */
    public int getRows() {
        return rows;
    }

    /**
     * Get the player's current level
     * @return current level
     */
    public int getLevel() {
        return level.get();
    }

    /**
     * Set the player's current level
     */
    public void setLevel(int level) {
        this.level.set(level);
    }

    /**
     * Get the player's current level property
     * @return player's current level property
     */
    public IntegerProperty levelProperty() {
        return level;
    }

    /**
     * Get the player's remaining lives
     * @return number of remaining lives
     */
    public int getLives() {
        return lives.get();
    }

    /**
     * Set the player's remaining lives
     */
    public void setLives(int lives) {
        this.lives.set(lives);
    }

    /**
     * Get the player's remaining lives property
     * @return player's remaining lives property
     */
    public IntegerProperty livesProperty() {
        return lives;
    }

    /**
     * Get the player's current multiplier
     * @return current multiplier
     */
    public int getMultiplier() {
        return multiplier.get();
    }

    /**
     * Set the player's current multiplier
     */
    public void setMultiplier(int multiplier) {
        this.multiplier.set(multiplier);
    }

    /**
     * Get the player's current multiplier property
     * @return player's current multiplier property
     */
    public IntegerProperty multiplierProperty() {
        return multiplier;
    }

    /**
     * Get the player's current score
     * @return current score
     */
    public int getScore() {
        return score.get();
    }

    /**
     * Set the player's current score
     */
    public void setScore(int score) {
        this.score.set(score);
    }

    /**
     * Get the player's current score property
     * @return player's current score property
     */
    public IntegerProperty scoreProperty() {
        return score;
    }
}
