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

import javafx.application.Platform;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.scene.control.Alert;
import javafx.util.Pair;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import uk.mgrove.ac.soton.comp1206.event.GameFailureListener;
import uk.mgrove.ac.soton.comp1206.network.Communicator;

import java.util.ArrayList;
import java.util.Scanner;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class MultiplayerGame extends Game {

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

    /**
     * Communicator to communicate with server
     */
    private final Communicator communicator;

    /**
     * Queue of pieces to be played in the game
     */
    private final BlockingQueue<GamePiece> pieceQueue = new LinkedBlockingQueue<>();

    /**
     * Scores for the leaderboard
     */
    private final SimpleListProperty<Pair<String,Pair<Integer,Integer>>> leaderboardScores = new SimpleListProperty<>(FXCollections.observableArrayList(new ArrayList<>()));

    /**
     * Listener for game failing
     */
    private GameFailureListener gameFailureListener;

    /**
     * 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 MultiplayerGame(int cols, int rows, Communicator communicator) {
        super(cols, rows);

        this.communicator = communicator;
        communicator.addListener(this::handleCommunicatorMessage);

        for (var i=0; i<5; i++) communicator.send("PIECE");
    }

    /**
     * Retrieve next game piece and request another from the server
     * @return next piece
     */
    @Override
    public GamePiece spawnPiece() {
        communicator.send("PIECE");

        GamePiece nextPiece = null;

        try {
            synchronized (pieceQueue) {
                while (pieceQueue.size() == 0) {
                    wait();
                }
                nextPiece = pieceQueue.poll();
            }
            logger.info("Picking next piece: {}", nextPiece.toString());
        } catch (InterruptedException e) {
            logger.error("Unable to retrieve piece from queue - waiting interrupted: " + e);
            Platform.runLater(() -> {
                endGame();
                if (gameFailureListener != null) gameFailureListener.gameFail();
                var error = new Alert(Alert.AlertType.ERROR, "Unable to retrieve game data from server in time - perhaps your connection is slow?");
                error.showAndWait();
            });
        }

        return nextPiece;
    }

    /**
     * Handle incoming messages from the communicator
     * @param message incoming message
     */
    private void handleCommunicatorMessage(String message) {
        if (message.startsWith("PIECE ")) {
            try {
                var newPiece = GamePiece.createPiece(Integer.parseInt(message.replaceFirst("PIECE ", "")));
                synchronized (pieceQueue) {
                    pieceQueue.add(newPiece);
                }
                logger.info("Generated piece from server: {}", newPiece.toString());
            } catch (NumberFormatException ex) {
                logger.error("Unable to generate piece from server - piece value not a number: " + ex);
            }
        } else if (message.startsWith("SCORES ")) {
            logger.info("Setting scores from server");
            leaderboardScores.set(parseScores(message.replaceFirst("SCORES ", "")));
        } else if (message.startsWith("SCORE ")) {
            var info = message.replaceFirst("SCORE ", "").split(":");

            for (var i=0; i<leaderboardScores.size(); i++) {
                if (leaderboardScores.get(i).getKey().equals(info[0])) {
                    logger.info("Updating score for: {} to: {}", info[0], info[1]);
                    leaderboardScores.set(i, new Pair<>(info[0], new Pair<>(Integer.valueOf(info[1]), leaderboardScores.get(i).getValue().getValue())));
                    break;
                }
            }
        } else if (message.startsWith("DIE ")) {
            var username = message.replaceFirst("DIE ", "");
            for (var i=0; i<leaderboardScores.size(); i++) {
                if (leaderboardScores.get(i).getKey().equals(username)) {
                    logger.info("Updating leaderboard as player has died: {}", username);
                    leaderboardScores.set(i, new Pair<>(username, new Pair<>(leaderboardScores.get(i).getValue().getKey(), -1)));
                    break;
                }
            }
        }
    }

    /**
     * Parse high scores from a string to list property
     * @param data string to parse from
     * @return list property containing scores loaded
     */
    private static SimpleListProperty<Pair<String,Pair<Integer,Integer>>> parseScores(String data) {
        logger.info("Parsing scores");

        var scores = new SimpleListProperty<Pair<String,Pair<Integer,Integer>>>(FXCollections.observableArrayList());
        var scanner = new Scanner(data);

        while (scanner.hasNextLine()) {
            var line = scanner.nextLine();
            if (line.matches("^.+:[0-9]+:([0-9]+|DEAD)$")) {
                var info = line.split(":");
                var lives = info[2].equals("DEAD") ? -1 : Integer.parseInt(info[2]);
                scores.add(new Pair<>(info[0], new Pair<>(Integer.valueOf(info[1]), lives)));
            }
        }
        scanner.close();

        logger.info("Parsed scores: {}", scores);
        return scores;
    }

    /**
     * Get the leaderboard scores property
     * @return property for leaderboard scores
     */
    public SimpleListProperty<Pair<String,Pair<Integer,Integer>>> leaderboardScoresProperty() {
        return leaderboardScores;
    }

    /**
     * End game
     */
    @Override
    public void endGame() {
        super.endGame();
        logger.info("Sending die message to server");
        communicator.send("DIE");
    }

    /**
     * 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
     */
    @Override
    protected void score(int lines, int blocks) {
        var oldScore = score.get();
        super.score(lines, blocks);
        if (oldScore != score.get()) {
            logger.info("Updating server with new score: {}", score.get());
            communicator.send("SCORE " + score.get());
        }
    }

    /**
     * Set the player's remaining lives
     */
    @Override
    public void setLives(int lives) {
        this.lives.set(lives);
        communicator.send("LIVES " + lives);
    }

    /**
     * Play current piece at given coordinates
     * @param x x-coordinate
     * @param y y-coordinate
     */
    @Override
    public void dropPiece(int x, int y) {
        var canPlayPiece = grid.canPlayPiece(currentPiece,x,y);
        super.dropPiece(x,y);
        if (canPlayPiece) {
            StringBuilder communicatorMessage = new StringBuilder("BOARD");
            for (var i=0; i<grid.getRows(); i++) {
                for (var j=0; j<grid.getCols(); j++) {
                    communicatorMessage.append(" ").append(grid.get(i, j));
                }
            }
            logger.info("Sending current board to server");
            communicator.send(communicatorMessage.toString());
        }
    }

    /**
     * Set listener for game failure
     * @param listener listener to set
     */
    @Override
    public void setOnGameFail(GameFailureListener listener) {
        this.gameFailureListener = listener;
    }

}
