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

import javafx.application.Platform;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.util.Pair;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import uk.mgrove.ac.soton.comp1206.component.GameBlock;
import uk.mgrove.ac.soton.comp1206.component.GameBlockCoordinate;
import uk.mgrove.ac.soton.comp1206.event.*;
import uk.mgrove.ac.soton.comp1206.util.Multimedia;

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
     */
    protected GamePiece currentPiece;

    /**
     * Next game piece for player to use
     */
    protected GamePiece followingPiece;

    /**
     * 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);

    /**
     * Listener for when new piece is received by the game
     */
    protected NextPieceListener nextPieceListener;

    /**
     * Listener for when lines of blocks are cleared
     */
    private LineClearedListener lineClearedListener;


    /**
     * Game timer for time-based functionality
     */
    private Timer gameTimer;

    /**
     * Listener for game loop being scheduled
     */
    private GameLoopListener gameLoopListener;

    /**
     * Listener for showing scores scene
     */
    private ShowScoresSceneListener showScoresListener;

    /**
     * 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 = followingPiece;
        followingPiece = spawnPiece();
        if (nextPieceListener != null) nextPieceListener.nextPiece(currentPiece, followingPiece);
        logger.info("New current piece is: {} and following piece is: {}", currentPiece, followingPiece);
        return currentPiece;
    }

    /**
     * Swap the current and next game pieces
     */
    public void swapPieces() {
        logger.info("Swapping current and next pieces");
        var tmp = currentPiece;
        currentPiece = followingPiece;
        followingPiece = tmp;
        if (nextPieceListener != null) nextPieceListener.nextPiece(currentPiece, followingPiece);
    }

    /**
     * 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");
        followingPiece = spawnPiece();
        nextPiece();

        scheduleGameLoop();
    }

    /**
     * Game loop - ongoing time-based functionality
     */
    private void gameLoop() {
        logger.info("Executing game loop");
        setLives(getLives() - 1);
        nextPiece();
        setMultiplier(1);
        Multimedia.playAudio("sounds/lifelose.wav");
        
        if(getLives() < 0) {
            endGame();
            if (showScoresListener != null) Platform.runLater(() -> showScoresListener.show(this));
        }
        else scheduleGameLoop();
    }

    /**
     * Start new game loop
     */
    protected void scheduleGameLoop() {
        logger.info("Scheduling game loop");

        if (gameTimer != null) gameTimer.cancel();

        TimerTask gameTimerTask = new TimerTask() {
            @Override
            public void run() {
                gameLoop();
            }
        };
        gameTimer = new Timer("Timer");
        var timerDelay = getTimerDelay();
        gameTimer.schedule(gameTimerTask, timerDelay);

        if (gameLoopListener != null) gameLoopListener.process(timerDelay);
    }

    /**
     * End game
     */
    public void endGame() {
        if (gameTimer != null) gameTimer.cancel();
        Multimedia.playAudio("sounds/explode.wav");
    }

    /**
     * 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();

        dropPiece(x,y);
    }

    /**
     * Play current piece at given coordinates
     * @param x x-coordinate
     * @param y y-coordinate
     */
    public void dropPiece(int x, int y) {
        if (grid.canPlayPiece(currentPiece,x,y)) {
            logger.info("Playing piece at x: {}, y: {}", x, y);
            grid.playPiece(currentPiece,x,y);
            afterPiece();

            Multimedia.playAudio("sounds/place.wav");
        } else {
            // can't play the piece
            logger.info("Couldn't play piece at x: {}, y: {}", x, y);

            Multimedia.playAudio("sounds/fail.wav");
        }
    }

    /**
     * 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<>();
        Set<GameBlockCoordinate> blockCoordinatesToRemove = new HashSet<>();
        int linesToRemove = 0;

        for (var x=0; x < grid.getCols(); x++) {
            Map<GameBlockCoordinate, IntegerProperty> columnBlocksToRemove = new HashMap<>();
            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.put(new GameBlockCoordinate(x,y), grid.getGridProperty(x,y));
            }
            // if column is full then store blocks to reset
            if (columnBlocksToRemove.size() == grid.getRows()) {
                for (var coordinates : columnBlocksToRemove.keySet()) {
                    blocksToRemove.add(columnBlocksToRemove.get(coordinates));
                    blockCoordinatesToRemove.add(coordinates);
                    linesToRemove++;
                }
            }
        }

        // do the same for rows
        for (var y=0; y < grid.getRows(); y++) {
            Map<GameBlockCoordinate, IntegerProperty> rowBlocksToRemove = new HashMap<>();
            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.put(new GameBlockCoordinate(x,y), grid.getGridProperty(x,y));
            }
            // if row is full then store blocks to reset
            if (rowBlocksToRemove.size() == grid.getCols()) {
                for (var coordinates : rowBlocksToRemove.keySet()) {
                    blocksToRemove.add(rowBlocksToRemove.get(coordinates));
                    blockCoordinatesToRemove.add(coordinates);
                    linesToRemove++;
                }
            }
        }

        if (linesToRemove > 0) {
            Multimedia.playAudio("sounds/clear.wav");
            if (lineClearedListener != null) lineClearedListener.clearLine(blockCoordinatesToRemove);
        }

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

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

        nextPiece();
        scheduleGameLoop();
    }

    /**
     * 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
     */
    protected void score(int lines, int blocks) {
        logger.info("Updating score with lines cleared: {}, blocks cleared: {}", lines, blocks);

        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));
    }

    /**
     * Rotate the current piece clockwise
     */
    public void rotateCurrentPiece() {
        logger.info("Rotating current piece clockwise");
        currentPiece.rotate();
        Multimedia.playAudio("sounds/rotate.wav");

        if (nextPieceListener != null) nextPieceListener.nextPiece(currentPiece, followingPiece);
    }

    /**
     * Rotate the current piece anticlockwise
     */
    public void rotateCurrentPieceAnticlockwise() {
        logger.info("Rotating current piece anticlockwise");
        currentPiece.rotate(3);
        Multimedia.playAudio("sounds/rotate.wav");

        if (nextPieceListener != null) nextPieceListener.nextPiece(currentPiece, followingPiece);
    }

    /**
     * Get timer delay - amount of time before timer should expire
     * @return timer delay
     */
    private int getTimerDelay() {
        return Math.max(2500, 12000 - 500 * getLevel());
    }

    /**
     * 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;
    }

    /**
     * Set next piece listener
     * @param listener listener to set
     */
    public void setNextPieceListener(NextPieceListener listener) {
        nextPieceListener = listener;
    }

    /**
     * Set line cleared listener
     * @param listener listener to set
     */
    public void setLineClearedListener(LineClearedListener listener) {
        lineClearedListener = listener;
    }

    /**
     * Set listener for game loop being executed
     * @param listener listener to set
     */
    public void setOnGameLoop(GameLoopListener listener) {
        gameLoopListener = listener;
    }

    /**
     * Set listener for showing scores scene
     * @param listener listener to set
     */
    public void setShowScoresListener(ShowScoresSceneListener listener) {
        showScoresListener = listener;
    }

    /**
     * Get leaderboard scores property - always null in this class
     * @return null
     */
    public SimpleListProperty<Pair<String, Pair<Integer, Integer>>> leaderboardScoresProperty() {
        return null;
    }

    /**
     * Set listener for game failure - but in this class does nothing
     * @param listener listener to set
     */
    public void setOnGameFail(GameFailureListener listener) {}
}