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

import javafx.animation.AnimationTimer;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.canvas.Canvas;
import javafx.scene.paint.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/**
 * The Visual User Interface component representing a single block in the grid.
 *
 * Extends Canvas and is responsible for drawing itself.
 *
 * Displays an empty square (when the value is 0) or a coloured square depending on value.
 *
 * The GameBlock value should be bound to a corresponding block in the Grid model.
 */
public class GameBlock extends Canvas {

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

    /**
     * The set of colours for different pieces
     */
    public static final Color[] COLOURS = {
            Color.TRANSPARENT,
            Color.DEEPPINK,
            Color.RED,
            Color.ORANGE,
            Color.YELLOW,
            Color.YELLOWGREEN,
            Color.LIME,
            Color.GREEN,
            Color.DARKGREEN,
            Color.DARKTURQUOISE,
            Color.DEEPSKYBLUE,
            Color.AQUA,
            Color.AQUAMARINE,
            Color.BLUE,
            Color.MEDIUMPURPLE,
            Color.PURPLE
    };

    private final GameBoard gameBoard;

    private final double width;
    private final double height;

    /**
     * The column this block exists as in the grid
     */
    private final int x;

    /**
     * The row this block exists as in the grid
     */
    private final int y;

    /**
     * The value of this block (0 = empty, otherwise specifies the colour to render as)
     */
    private final IntegerProperty value = new SimpleIntegerProperty(0);

    /**
     * The preview value of this block (0 = empty, otherwise specifies the colour to render as)
     */
    private final IntegerProperty previewValue = new SimpleIntegerProperty(0);

    /**
     * Whether the block should appear as focussed
     */
    private boolean isFocussed = false;

    /**
     * Whether indicator should be shown in the middle of the block
     */
    private boolean showMiddleIndicator = false;

    /**
     * Create a new single Game Block
     * @param gameBoard the board this block belongs to
     * @param x the column the block exists in
     * @param y the row the block exists in
     * @param width the width of the canvas to render
     * @param height the height of the canvas to render
     */
    public GameBlock(GameBoard gameBoard, int x, int y, double width, double height) {
        this.gameBoard = gameBoard;
        this.width = width;
        this.height = height;
        this.x = x;
        this.y = y;

        //A canvas needs a fixed width and height
        setWidth(width);
        setHeight(height);

        //Do an initial paint
        paint();

        //When the value property is updated, call the internal updateValue method
        value.addListener(this::updateValue);
        previewValue.addListener(this::updateValue);
    }

    /**
     * When the value of this block is updated,
     * @param observable what was updated
     * @param oldValue the old value
     * @param newValue the new value
     */
    private void updateValue(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
        paint();
    }

    /**
     * Handle painting of the block canvas
     */
    public void paint() {
        //If the block is empty, paint as empty
        if(value.get() == 0) {
            paintEmpty();
        } else {
            //If the block is not empty, paint with the colour represented by the value
            paintColor(COLOURS[value.get()]);
        }

        if (previewValue.get() != 0) paintPreview(COLOURS[previewValue.get()]);

        // paint focus overlay if required
        if (isFocussed) paintFocusOverlay();

        // paint middle indicator if required
        if (showMiddleIndicator) paintMiddleIndicator();
    }

    /**
     * Handle painting of the block canvas with block preview
     */
    public void paintPreview(Color color) {
        logger.info("Painting preview overlay on block at x: {}, y: {} with color: {}", getX(), getY(), color);

        var gc = getGraphicsContext2D();

        gc.setFill(color.deriveColor(0, 1, 1, 0.25));
        gc.fillRect(0,0, width, height);
        gc.setFill(color.deriveColor(0,1,0.8,0.25));
        gc.fillPolygon(new double[]{
                        width,
                        width,
                        0
                }, new double[]{
                        0,
                        height,
                        0
                }, 3
        );
    }

    /**
     * Paint this canvas empty
     */
    private void paintEmpty() {
        logger.info("Painting empty block at x: {}, y: {}", getX(), getY());
        var gc = getGraphicsContext2D();

        //Clear
        gc.clearRect(0,0,width,height);

        //Fill
        gc.setFill(Color.web("111111",0.5));
        gc.fillRect(0,0, width, height);

        //Border
        gc.setStroke(Color.GRAY);
        gc.strokeRect(0,0,width,height);
    }

    /**
     * Paint this canvas with the given colour
     * @param color the colour to paint
     */
    private void paintColor(Color color) {
        logger.info("Painting color block at x: {}, y: {}", getX(), getY());
        var gc = getGraphicsContext2D();

        //Clear
        gc.clearRect(0,0,width,height);

        //Colour fill
        gc.setFill(color);
        gc.fillRect(0,0, width, height);
        gc.setFill(color.deriveColor(0,1,0.8,1));
        gc.fillPolygon(new double[]{
                width,
                width,
                0.0
            }, new double[]{
                0,
                height,
                0
            }, 3
        );

        //Border
        gc.setStroke(Color.GRAY);
        gc.strokeRect(0,0,width,height);
    }

    /**
     * Paint indicator in middle of block
     */
    private void paintMiddleIndicator() {
        logger.info("Painting indicator on block at x: {}, y: {}", getX(), getY());

        var gc = getGraphicsContext2D();
        gc.setFill(Color.WHITE.deriveColor(0,1,1,0.7));
        gc.fillOval(width/4, height/4, width/2, height/2);
    }

    /**
     * Paint overlay to indicate focus
     */
    private void paintFocusOverlay() {
        logger.info("Painting focus overlay on block at x: {}, y: {}", getX(), getY());

        var gc = getGraphicsContext2D();
        gc.setFill(Color.WHITE.deriveColor(0,1,1,0.5));
        gc.fillRect(0,0,width,height);
    }

    /**
     * Set whether the block should appear as focussed
     * @param value whether block should appear as focussed
     */
    public void setFocussed(boolean value) {
        logger.info("Block at x: {}, y: {} has been set to focus: {}", getX(), getY(), value);
        isFocussed = value;
        paint();
    }

    /**
     * Set whether an indicator should be shown in the middle of the block
     * @param value whether indicator should be shown
     */
    public void showMiddleIndicator(boolean value) {
        logger.info("Setting middle indicator on block at x: {}, y: {} to: {}", getX(), getY(), value);
        showMiddleIndicator = value;
        paint();
    }

    /**
     * Fade out block
     */
    public void fadeOut() {
        logger.info("Fading out block at x: {}, y: {}", getX(), getY());

        AnimationTimer timer = new AnimationTimer() {
            private Color color = COLOURS[getValue()];
            private int iterations = 0;
            @Override
            public void handle(long now) {
                if (iterations < 12) {
                    color = color.deriveColor(0,1,1.2,1);
                    paintColor(color);
                    if (isFocussed) paintFocusOverlay();
                } else if (color.getOpacity() > 0.1) {
                    color = color.deriveColor(0,1,1,0.95);
                    paintColor(color);
                    if (isFocussed) paintFocusOverlay();
                } else {
                    paintEmpty();
                    if (isFocussed) paintFocusOverlay();
                    logger.info("Stopping timer at x: {}, y: {}", getX(), getY());
                    stop();
                }
                iterations++;
            }
        };
        timer.start();
    }

    /**
     * Get the column of this block
     * @return column number
     */
    public int getX() {
        return x;
    }

    /**
     * Get the row of this block
     * @return row number
     */
    public int getY() {
        return y;
    }

    /**
     * Get the current value held by this block, representing it's colour
     * @return value
     */
    public int getValue() {
        return this.value.get();
    }

    /**
     * Bind the value of this block to another property. Used to link the visual block to a corresponding block in the Grid.
     * @param input property to bind the value to
     */
    public void bind(ObservableValue<? extends Number> input) {
        value.bind(input);
    }

    /**
     * Bind the value of this block to another property. Used to link the visual block to a corresponding block in the Grid.
     * @param input property to bind the value to
     */
    public void bindPreview(ObservableValue<? extends Number> input) {
        previewValue.bind(input);
    }

    @Override
    public String toString() {
        return "GameBlock{" +
            "x=" + x +
            ", y=" + y +
            ", value=" + value.get() +
            '}';
    }
}
