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

import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.GridPane;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import uk.mgrove.ac.soton.comp1206.event.BlockClickedListener;
import uk.mgrove.ac.soton.comp1206.event.MouseClickListener;
import uk.mgrove.ac.soton.comp1206.game.GamePiece;
import uk.mgrove.ac.soton.comp1206.game.Grid;
import uk.mgrove.ac.soton.comp1206.util.Multimedia;

import java.util.Set;

/**
 * A GameBoard is a visual component to represent the visual GameBoard.
 * It extends a GridPane to hold a grid of GameBlocks.
 *
 * The GameBoard can hold an internal grid of its own, for example, for displaying an upcoming block. It also be
 * linked to an external grid, for the main game board.
 *
 * The GameBoard is only a visual representation and should not contain game logic or model logic in it, which should
 * take place in the Grid.
 */
public class GameBoard extends GridPane {

    private static final Logger logger = LogManager.getLogger(GameBoard.class);

    /**
     * Number of columns in the board
     */
    private final int cols;

    /**
     * Number of rows in the board
     */
    private final int rows;

    /**
     * The visual width of the board - has to be specified due to being a Canvas
     */
    private final double width;

    /**
     * The visual height of the board - has to be specified due to being a Canvas
     */
    private final double height;

    /**
     * The grid this GameBoard represents
     */
    final Grid grid;

    /**
     * The blocks inside the grid
     */
    GameBlock[][] blocks;

    /**
     * The listener to call when a specific block is clicked
     */
    private BlockClickedListener blockClickedListener;

    /**
     * Listener to call when board is right-clicked
     */
    protected MouseClickListener rightClickListener;

    /**
     * Listener for board being left-clicked
     */
    protected MouseClickListener leftClickListener;

    /**
     * Currently focussed game block
     */
    private GameBlock focussedBlock;

    /**
     * Whether focus overlays should be shown
     */
    protected boolean enableFocus = true;

    /**
     * Current game piece
     */
    protected GamePiece currentPiece;

    /**
     * Create a new GameBoard, based off a given grid, with a visual width and height.
     * @param grid linked grid
     * @param width the visual width
     * @param height the visual height
     */
    public GameBoard(Grid grid, double width, double height) {
        this.cols = grid.getCols();
        this.rows = grid.getRows();
        this.width = width;
        this.height = height;
        this.grid = grid;

        //Build the GameBoard
        build();
    }

    /**
     * Create a new GameBoard with its own internal grid, specifying the number of columns and rows, along with the
     * visual width and height.
     *
     * @param cols number of columns for internal grid
     * @param rows number of rows for internal grid
     * @param width the visual width
     * @param height the visual height
     */
    public GameBoard(int cols, int rows, double width, double height) {
        this.cols = cols;
        this.rows = rows;
        this.width = width;
        this.height = height;
        this.grid = new Grid(cols,rows);

        //Build the GameBoard
        build();
    }

    /**
     * Get a specific block from the GameBoard, specified by it's row and column
     * @param x column
     * @param y row
     * @return game block at the given column and row
     */
    public GameBlock getBlock(int x, int y) {
        return blocks[x][y];
    }

    /**
     * Build the GameBoard by creating a block at every x and y column and row
     */
    protected void build() {
        logger.info("Building grid: {} x {}",cols,rows);

        setMaxWidth(width);
        setMaxHeight(height);

        setGridLinesVisible(true);

        blocks = new GameBlock[cols][rows];

        for(var y = 0; y < rows; y++) {
            for (var x = 0; x < cols; x++) {
                createBlock(x,y);
            }
        }

        setOnMouseClicked((event) -> {
            logger.debug("Mouse button clicked: {}", event.getButton());
            if (event.getButton().equals(MouseButton.SECONDARY) && rightClickListener != null) {
                rightClickListener.action();
                grid.clearPreview();
                grid.previewPiece(currentPiece, getXFocus(), getYFocus());
            } else if (event.getButton().equals(MouseButton.PRIMARY) && leftClickListener != null) {
                leftClickListener.action();
            }
        });
    }

    /**
     * Create a block at the given x and y position in the GameBoard
     * @param x column
     * @param y row
     */
    protected GameBlock createBlock(int x, int y) {
        logger.info("Creating new block at x: {}, y: {}", x, y);
        var blockWidth = width / cols;
        var blockHeight = height / rows;

        //Create a new GameBlock UI component
        GameBlock block = new GameBlock(this, x, y, blockWidth, blockHeight);

        //Add to the GridPane
        add(block,x,y);

        //Add to our block directory
        blocks[x][y] = block;

        //Link the GameBlock component to the corresponding value in the Grid
        block.bind(grid.getGridProperty(x,y));
        block.bindPreview(grid.getPreviewGridProperty(x,y));

        //Add a mouse click handler to the block to trigger GameBoard blockClicked method
        block.setOnMouseClicked((e) -> {
            if (e.getButton().equals(MouseButton.PRIMARY)) blockClicked(e, block);
        });
        block.hoverProperty().addListener(((observable, oldValue, newValue) -> {
            if (newValue) blockHovered(block);
        }));

        return block;
    }

    /**
     * Set the listener to handle an event when a block is clicked
     * @param listener listener to add
     */
    public void setOnBlockClick(BlockClickedListener listener) {
        this.blockClickedListener = listener;
    }

    /**
     * Triggered when a block is clicked. Call the attached listener.
     * @param event mouse event
     * @param block block clicked on
     */
    private void blockClicked(MouseEvent event, GameBlock block) {
        logger.info("Block clicked: {}", block);

        if(blockClickedListener != null) {
            blockClickedListener.blockClicked(block);
        }
    }

    public void blockHovered(GameBlock gameBlock) {
        grid.clearPreview();
        if (enableFocus) {
            logger.info("Block hovered: {}", gameBlock);
            if (focussedBlock != null) focussedBlock.setFocussed(false);
            focussedBlock = gameBlock;
            focussedBlock.setFocussed(true);
            grid.previewPiece(currentPiece, getXFocus(), getYFocus());
        }
    }

    /**
     * Change current x-coordinate focus by given amount
     * @param amount amount to change by
     */
    public void changeXFocus(int amount) {
        logger.info("Change of {} requested to x-coordinate of focussed block", amount);
        if (focussedBlock == null) blockHovered(getBlock(0,0));
        else if (focussedBlock.getX() + amount >= 0 && focussedBlock.getX() + amount < cols) blockHovered(getBlock(focussedBlock.getX() + amount, focussedBlock.getY()));
        else Multimedia.playAudio("sounds/fail.wav");
    }

    /**
     * Change current y-coordinate focus by given amount
     * @param amount amount to change by
     */
    public void changeYFocus(int amount) {
        logger.info("Change of {} requested to y-coordinate of focussed block", amount);
        if (focussedBlock == null) blockHovered(getBlock(0,0));
        else if (focussedBlock.getY() + amount >= 0 && focussedBlock.getY() + amount < rows) blockHovered(getBlock(focussedBlock.getX(), focussedBlock.getY() + amount));
        else Multimedia.playAudio("sounds/fail.wav");
    }

    /**
     * Fade out all blocks located at a set of coordinates
     * @param blockCoordinates block coordinates
     */
    public void fadeOut(Set<GameBlockCoordinate> blockCoordinates) {
        for (var block : blockCoordinates) {
            getBlock(block.getX(), block.getY()).fadeOut();
        }
    }

    /**
     * Get the x-coordinate of the currently-focussed block
     * @return x-coordinate of block
     */
    public int getXFocus() {
        if (focussedBlock != null) return focussedBlock.getX();
        else return 0;
    }

    /**
     * Get the y-coordinate of the currently-focussed block
     * @return y-coordinate of block
     */
    public int getYFocus() {
        if (focussedBlock != null) return focussedBlock.getY();
        else return 0;
    }

    /**
     * Set listener for when secondary mouse button clicked
     * @param listener listener to set
     */
    public void setOnRightClicked(MouseClickListener listener) {
        rightClickListener = listener;
    }

    /**
     * Set listener for when primary mouse button clicked
     * @param listener listener to set
     */
    public void setOnLeftClicked(MouseClickListener listener) {
        leftClickListener = listener;
    }

    /**
     * Update current game piece
     * @param newPiece current piece
     */
    public void setCurrentPiece(GamePiece newPiece) {
        currentPiece = newPiece;
    }

}
