/*
 * SPDX-License-Identifier: Apache-2.0
 *
 * Copyright 2016-2020 Gerrit Grunwald.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package eu.hansolo.tilesfx.skins;

import eu.hansolo.tilesfx.Section;
import eu.hansolo.tilesfx.Tile;
import eu.hansolo.tilesfx.Tile.TextSize;
import eu.hansolo.tilesfx.events.BoundsEvent;
import eu.hansolo.tilesfx.events.BoundsEventListener;
import eu.hansolo.tilesfx.events.TileEvent.EventType;
import eu.hansolo.tilesfx.events.TileEventListener;
import eu.hansolo.tilesfx.tools.CtxBounds;
import eu.hansolo.tilesfx.tools.InfoRegion;
import eu.hansolo.tilesfx.tools.LowerRightRegion;
import eu.hansolo.tilesfx.tools.NotifyRegion;
import javafx.beans.InvalidationListener;
import javafx.beans.property.ObjectProperty;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.effect.BlurType;
import javafx.scene.effect.DropShadow;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.BorderWidths;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;

import java.text.DecimalFormat;
import java.util.List;
import java.util.Locale;

import static eu.hansolo.tilesfx.tools.Helper.clamp;
import static eu.hansolo.tilesfx.tools.Helper.enableNode;


/**
 * Created by hansolo on 19.12.16.
 */
public class TileSkin extends SkinBase<Tile> implements Skin<Tile> {
    protected static final double                    PREFERRED_WIDTH  = 250;
    protected static final double                    PREFERRED_HEIGHT = 250;
    protected static final double                    MINIMUM_WIDTH    = 50;
    protected static final double                    MINIMUM_HEIGHT   = 50;
    protected static final double                    MAXIMUM_WIDTH    = 1024;
    protected static final double                    MAXIMUM_HEIGHT   = 1024;
    protected              double                    width;
    protected              double                    height;
    protected              double                    size;
    protected              double                    inset;
    protected              double                    doubleInset;
    protected              CtxBounds                 contentBounds;
    protected              double                    contentCenterX;
    protected              double                    contentCenterY;
    protected              Pane                      pane;
    protected              double                    minValue;
    protected              double                    maxValue;
    protected              double                    range;
    protected              double                    threshold;
    protected              double                    stepSize;
    protected              double                    angleRange;
    protected              double                    angleStep;
    protected              boolean                   highlightSections;
    protected              String                    formatString;
    protected              DecimalFormat             decimalFormat;
    protected              String                    tickLabelFormatString;
    protected              Locale                    locale;
    protected              List<Section>             sections;
    protected              boolean                   sectionsVisible;
    protected              TextSize                  textSize;
    protected              DropShadow                shadow;
    protected              InvalidationListener      sizeListener;
    protected              TileEventListener         tileEventListener;
    protected              InvalidationListener      currentValueListener;
    protected              InvalidationListener      timeListener;
    protected              Tile                      tile;
    private                ImageView                 backgroundImageView;
    private                NotifyRegion              notifyRegion;
    private                InfoRegion                infoRegion;
    private                LowerRightRegion          lowerRightRegion;
    private                EventHandler<MouseEvent>  infoRegionHandler;


    // ******************** Constructors **************************************
    public TileSkin(final Tile TILE) {
        super(TILE);
        tile                  = TILE;
        minValue              = tile.getMinValue();
        maxValue              = tile.getMaxValue();
        range                 = tile.getRange();
        threshold             = tile.getThreshold();
        stepSize              = PREFERRED_WIDTH / range;
        angleRange            = tile.getAngleRange();
        angleStep             = angleRange / range;
        formatString          = new StringBuilder("%.").append(tile.getDecimals()).append("f").toString();
        tickLabelFormatString = new StringBuilder("%.").append(tile.getTickLabelDecimals()).append("f").toString();;
        locale                = tile.getLocale();
        sections              = tile.getSections();
        sectionsVisible       = tile.getSectionsVisible();
        highlightSections     = tile.isHighlightSections();
        textSize              = tile.getTextSize();
        infoRegionHandler     = tile.getInfoRegionHandler();
        sizeListener          = o -> handleEvents("RESIZE");
        tileEventListener     = e -> handleEvents(e.getEventType().name());
        currentValueListener  = o -> handleCurrentValue(tile.getCurrentValue());
        contentBounds         = new CtxBounds();
        decimalFormat         = tile.getCustomDecimalFormat();

        initGraphics();
        registerListeners();
    }


    // ******************** Initialization ************************************
    protected void initGraphics() {
        // Set initial size
        if (Double.compare(tile.getPrefWidth(), 0.0) <= 0 || Double.compare(tile.getPrefHeight(), 0.0) <= 0 ||
            Double.compare(tile.getWidth(), 0.0) <= 0 || Double.compare(tile.getHeight(), 0.0) <= 0) {
            if (tile.getPrefWidth() > 0 && tile.getPrefHeight() > 0) {
                tile.setPrefSize(tile.getPrefWidth(), tile.getPrefHeight());
            } else {
                tile.setPrefSize(PREFERRED_WIDTH, PREFERRED_HEIGHT);
            }
        }

        shadow = new DropShadow(BlurType.TWO_PASS_BOX, Color.rgb(0, 0, 0, 0.65), 3, 0, 0, 0);

        backgroundImageView = new ImageView();
        backgroundImageView.setPreserveRatio(true);
        backgroundImageView.setMouseTransparent(true);
        if (null == tile.getBackgroundImage()) {
            enableNode(backgroundImageView, false);
        } else {
            backgroundImageView.setImage(tile.getBackgroundImage());
            enableNode(backgroundImageView, true);
        }

        notifyRegion = new NotifyRegion();
        enableNode(notifyRegion, false);

        infoRegion = new InfoRegion();
        infoRegion.setPickOnBounds(false);
        enableNode(infoRegion, false);

        lowerRightRegion = new LowerRightRegion();
        enableNode(lowerRightRegion, false);

        pane = new Pane(backgroundImageView, notifyRegion, infoRegion, lowerRightRegion);
        pane.getStyleClass().add("tile");
        pane.setBorder(new Border(new BorderStroke(tile.getBorderColor(), BorderStrokeStyle.SOLID, new CornerRadii(PREFERRED_WIDTH * 0.025), new BorderWidths(tile.getBorderWidth()))));
        pane.setBackground(new Background(new BackgroundFill(tile.getBackgroundColor(), new CornerRadii(PREFERRED_WIDTH * 0.025), Insets.EMPTY)));

        getChildren().setAll(pane);
    }

    protected void registerListeners() {
        tile.widthProperty().addListener(sizeListener);
        tile.heightProperty().addListener(sizeListener);
        tile.setOnTileEvent(tileEventListener);
        tile.currentValueProperty().addListener(currentValueListener);
        if (null != infoRegionHandler) { infoRegion.addEventHandler(MouseEvent.ANY, infoRegionHandler); }
    }


    // ******************** Methods *******************************************
    @Override protected double computeMinWidth(final double HEIGHT, final double TOP, final double RIGHT, final double BOTTOM, final double LEFT)  { return MINIMUM_WIDTH; }
    @Override protected double computeMinHeight(final double WIDTH, final double TOP, final double RIGHT, final double BOTTOM, final double LEFT)  { return MINIMUM_HEIGHT; }
    @Override protected double computePrefWidth(final double HEIGHT, final double TOP, final double RIGHT, final double BOTTOM, final double LEFT) { return super.computePrefWidth(HEIGHT, TOP, RIGHT, BOTTOM, LEFT); }
    @Override protected double computePrefHeight(final double WIDTH, final double TOP, final double RIGHT, final double BOTTOM, final double LEFT) { return super.computePrefHeight(WIDTH, TOP, RIGHT, BOTTOM, LEFT); }
    @Override protected double computeMaxWidth(final double HEIGHT, final double TOP, final double RIGHT, final double BOTTOM, final double LEFT)  { return MAXIMUM_WIDTH; }
    @Override protected double computeMaxHeight(final double WIDTH, final double TOP, final double RIGHT, final double BOTTOM, final double LEFT)  { return MAXIMUM_HEIGHT; }

    protected Pane getPane() { return pane; }

    protected void handleEvents(final String EVENT_TYPE) {
        if (EventType.RESIZE.name().equals(EVENT_TYPE)) {
            resize();
            redraw();
        } else if (EventType.REDRAW.name().equals(EVENT_TYPE)) {
            redraw();
        } else if (EventType.RECALC.name().equals(EVENT_TYPE)) {
            minValue          = tile.getMinValue();
            maxValue          = tile.getMaxValue();
            range             = tile.getRange();
            threshold         = tile.getThreshold();
            stepSize          = size / range;
            angleRange        = clamp(90.0, 180.0, tile.getAngleRange());
            angleStep         = angleRange / range;
            highlightSections = tile.isHighlightSections();
            redraw();
            handleCurrentValue(tile.getCurrentValue());
        } else if (EventType.SECTION.name().equals(EVENT_TYPE)) {
            sections = tile.getSections();
        } else if (EventType.SHOW_NOTIFY_REGION.name().equals(EVENT_TYPE)) {
            enableNode(notifyRegion, true);
        } else if (EventType.HIDE_NOTIFY_REGION.equals(EVENT_TYPE)) {
            enableNode(notifyRegion, false);
        } else if (EventType.SHOW_INFO_REGION.name().equals(EVENT_TYPE)) {
            enableNode(infoRegion, true);
        } else if (EventType.HIDE_INFO_REGION.name().equals(EVENT_TYPE)) {
            enableNode(infoRegion, false);
        } else if (EventType.SHOW_LOWER_RIGHT_REGION.name().equals(EVENT_TYPE)) {
            enableNode(lowerRightRegion, true);
        } else if (EventType.HIDE_LOWER_RIGHT_REGION.name().equals(EVENT_TYPE)) {
            enableNode(lowerRightRegion, false);
        } else if (EventType.BACKGROUND_IMAGE.name().equals(EVENT_TYPE)) {
            if (null == tile.getBackgroundImage()) {
                enableNode(backgroundImageView, false);
            } else {
                backgroundImageView.setImage(tile.getBackgroundImage());
                backgroundImageView.setFitWidth(width);
                backgroundImageView.setFitHeight(height);
                enableNode(backgroundImageView, true);
            }
        } else if (EventType.REGIONS_ON_TOP.name().equals(EVENT_TYPE)) {
            // Set upper left and upper right notifiers to front
            notifyRegion.toFront();
            infoRegion.toFront();
        } else if (EventType.INFO_REGION_HANDLER.name().equals(EVENT_TYPE)) {
            if (null != infoRegionHandler) { infoRegion.removeEventHandler(MouseEvent.ANY, infoRegionHandler); }
            infoRegionHandler = tile.getInfoRegionHandler();
            if (null == infoRegionHandler) { return; }
            infoRegion.addEventHandler(MouseEvent.ANY, infoRegionHandler);
        }
    }

    protected void handleCurrentValue(final double VALUE) {}

    /**
     * Returns the bounds of the content area. Keep in mind that
     * the skin property of the Tile has to be set before you can
     * get the content bounds. As long as getSkin() == null you get
     * null as return value here.
     * @return the bounds of the content area.
     */
    public CtxBounds getContentBounds() { return contentBounds; }

    /**
     * Adds a listener to the content bounds area.
     * Keep in mind that you can only add a listener to the bounds if the skin property is set
     * in the control.
     */
    public void setOnContentBoundsChanged(final BoundsEventListener LISTENER) { contentBounds.setOnBoundsEvent(LISTENER); }
    public void removeOnContentBoundsChanged(final BoundsEventListener LISTENER) { contentBounds.removeBoundsEventListener(LISTENER); }

    public NotifyRegion getNotifyRegion() { return notifyRegion; }

    public InfoRegion getInfoRegion() { return infoRegion; }

    public LowerRightRegion getLowerRightRegion() { return lowerRightRegion; }

    @Override public void dispose() {
        contentBounds.removeAllListeners();
        tile.widthProperty().removeListener(sizeListener);
        tile.heightProperty().removeListener(sizeListener);
        tile.removeTileEventListener(tileEventListener);
        tile.currentValueProperty().removeListener(currentValueListener);
        tile = null;
    }
    

    // ******************** Resizing ******************************************
    protected void resizeDynamicText() {}
    protected void resizeStaticText() {}

    protected void resize() {
        width  = tile.getWidth() - tile.getInsets().getLeft() - tile.getInsets().getRight();
        height = tile.getHeight() - tile.getInsets().getTop() - tile.getInsets().getBottom();
        size   = clamp(0, Double.MAX_VALUE, width < height ? width : height);

        stepSize = width / range;
        shadow.setRadius(size * 0.012);

        inset       = size * 0.05;
        doubleInset = inset * 2;

        if (tile.isShowing() && width > 0 && height > 0) {
            //pane.setMaxSize(size, size);
            //pane.relocate((width - size) * 0.5, (height - size) * 0.5);

            double offsetTop    = tile.getTitle().isEmpty() ? inset : size * 0.15;
            double offsetBottom = (tile.getText().isEmpty() || !tile.isTextVisible()) ? height - inset : height - size * 0.15;
            contentBounds.setX(inset);
            contentBounds.setY(offsetTop);
            contentBounds.setWidth(width - doubleInset);
            contentBounds.setHeight(offsetBottom - offsetTop);

            contentCenterX = contentBounds.getX() + contentBounds.getWidth() * 0.5;
            contentCenterY = contentBounds.getY() + contentBounds.getHeight() * 0.5;

            pane.setMaxSize(width, height);
            pane.setPrefSize(width, height);

            if (backgroundImageView.isVisible()) {
                if (tile.getRoundedCorners()) {
                    Rectangle imgClip = new Rectangle(width, height);
                    imgClip.setArcWidth(clamp(0, Double.MAX_VALUE, inset));
                    imgClip.setArcHeight(clamp(0, Double.MAX_VALUE, inset));
                    backgroundImageView.setClip(imgClip);
                }
                backgroundImageView.setFitWidth(width);
                backgroundImageView.setFitHeight(height);
                backgroundImageView.setPreserveRatio(tile.getBackgroundImageKeepAspect());
                backgroundImageView.relocate((width - backgroundImageView.getLayoutBounds().getWidth()) * 0.5, (height - backgroundImageView.getLayoutBounds().getHeight()) * 0.5);
            }

            double regionSize = size * 0.105;
            notifyRegion.setPrefSize(regionSize, regionSize);
            notifyRegion.relocate(width - regionSize, 0);

            infoRegion.setPrefSize(regionSize, regionSize);
            infoRegion.relocate(0, 0);

            lowerRightRegion.setPrefSize(regionSize, regionSize);
            lowerRightRegion.relocate(width - regionSize, height - regionSize);

            resizeStaticText();
            resizeDynamicText();
        }
    }

    protected void redraw() {
        boolean hasRoundedCorners = tile.getRoundedCorners();
        pane.setBorder(new Border(new BorderStroke(tile.getBorderColor(), BorderStrokeStyle.SOLID, hasRoundedCorners ? new CornerRadii(clamp(0, Double.MAX_VALUE, size * 0.025)) : CornerRadii.EMPTY, new BorderWidths(clamp(0, Double.MAX_VALUE, tile.getBorderWidth() / PREFERRED_WIDTH * size)))));
        pane.setBackground(new Background(new BackgroundFill(tile.getBackgroundColor(), hasRoundedCorners ? new CornerRadii(clamp(0, Double.MAX_VALUE, size * 0.025)) : CornerRadii.EMPTY, Insets.EMPTY)));

        backgroundImageView.setOpacity(tile.getBackgroundImageOpacity());

        notifyRegion.setRoundedCorner(hasRoundedCorners);
        notifyRegion.setBackgroundColor(tile.getNotifyRegionBackgroundColor());
        notifyRegion.setForegroundColor(tile.getNotifyRegionForegroundColor());
        notifyRegion.setTooltipText(tile.getNotifyRegionTooltipText());

        infoRegion.setRoundedCorner(hasRoundedCorners);
        infoRegion.setBackgroundColor(tile.getInfoRegionBackgroundColor());
        infoRegion.setForegroundColor(tile.getInfoRegionForegroundColor());
        infoRegion.setTooltipText(tile.getInfoRegionTooltipText());

        lowerRightRegion.setRoundedCorner(hasRoundedCorners);
        lowerRightRegion.setBackgroundColor(tile.getLowerRightRegionBackgroundColor());
        lowerRightRegion.setForegroundColor(tile.getLowerRightRegionForegroundColor());
        lowerRightRegion.setTooltipText(tile.getLowerRightRegionTooltipText());

        locale                = tile.getLocale();
        if (tile.getCustomDecimalFormatEnabled()) {
            decimalFormat = tile.getCustomDecimalFormat();
        } else {
            formatString = new StringBuilder("%.").append(tile.getDecimals()).append("f").toString();
        }
        tickLabelFormatString = new StringBuilder("%.").append(tile.getTickLabelDecimals()).append("f").toString();
        sectionsVisible       = tile.getSectionsVisible();
        textSize              = tile.getTextSize();
    }
}