/*
 * 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.medusa.skins;

import eu.hansolo.medusa.Fonts;
import eu.hansolo.medusa.Gauge;
import eu.hansolo.medusa.tools.Helper;
import javafx.beans.InvalidationListener;
import javafx.geometry.Insets;
import javafx.scene.CacheHint;
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.Arc;
import javafx.scene.shape.ArcType;
import javafx.scene.shape.ClosePath;
import javafx.scene.shape.CubicCurveTo;
import javafx.scene.shape.FillRule;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.text.Text;
import javafx.scene.transform.Rotate;

import java.util.Locale;

import static eu.hansolo.medusa.tools.Helper.formatNumber;


/**
 * Created by hansolo on 15.01.16.
 */
public class KpiSkin extends GaugeSkinBase {
    private double               size;
    private double               oldValue;
    private Arc                  barBackground;
    private Arc                  thresholdBar;
    private Path                 needle;
    private Rotate               needleRotate;
    private Text                 titleText;
    private Text                 valueText;
    private Text                 minValueText;
    private Text                 maxValueText;
    private Text                 thresholdText;
    private Pane                 pane;
    private double               angleRange;
    private double               minValue;
    private double               range;
    private double               angleStep;
    private Locale               locale;
    private InvalidationListener currentValueListener;


    // ******************** Constructors **************************************
    public KpiSkin(Gauge gauge) {
        super(gauge);
        if (gauge.isAutoScale()) gauge.calcAutoScale();
        angleRange           = Helper.clamp(90.0, 180.0, gauge.getAngleRange());
        oldValue             = gauge.getValue();
        minValue             = gauge.getMinValue();
        range                = gauge.getRange();
        angleStep            = angleRange / range;
        locale               = gauge.getLocale();
        currentValueListener = o -> rotateNeedle(gauge.getCurrentValue());

        initGraphics();
        registerListeners();

        rotateNeedle(gauge.getCurrentValue());
    }


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

        barBackground = new Arc(PREFERRED_WIDTH * 0.5, PREFERRED_HEIGHT * 0.696, PREFERRED_WIDTH * 0.275, PREFERRED_WIDTH * 0.275, angleRange * 0.5 + 90, -angleRange);
        barBackground.setType(ArcType.OPEN);
        barBackground.setStroke(gauge.getBarColor());
        barBackground.setStrokeWidth(PREFERRED_WIDTH * 0.02819549 * 2);
        barBackground.setStrokeLineCap(StrokeLineCap.BUTT);
        barBackground.setFill(null);

        thresholdBar = new Arc(PREFERRED_WIDTH * 0.5, PREFERRED_HEIGHT * 0.696, PREFERRED_WIDTH * 0.275, PREFERRED_WIDTH * 0.275, -angleRange * 0.5 + 90, 0);
        thresholdBar.setType(ArcType.OPEN);
        thresholdBar.setStroke(gauge.getThresholdColor());
        thresholdBar.setStrokeWidth(PREFERRED_WIDTH * 0.02819549 * 2);
        thresholdBar.setStrokeLineCap(StrokeLineCap.BUTT);
        thresholdBar.setFill(null);

        needleRotate = new Rotate((gauge.getValue() - oldValue - minValue) * angleStep);

        needle = new Path();
        needle.setFillRule(FillRule.EVEN_ODD);
        needle.getTransforms().setAll(needleRotate);
        needle.setFill(gauge.getNeedleColor());
        needle.setStrokeWidth(0);
        needle.setStroke(Color.TRANSPARENT);

        titleText = new Text(gauge.getTitle());
        titleText.setFill(gauge.getTitleColor());
        Helper.enableNode(titleText, !gauge.getTitle().isEmpty());

        valueText = new Text(formatNumber(gauge.getLocale(), gauge.getFormatString(), gauge.getDecimals(), gauge.getCurrentValue()));
        valueText.setFill(gauge.getValueColor());
        Helper.enableNode(valueText, gauge.isValueVisible());

        minValueText = new Text(String.format(locale, "%." + gauge.getTickLabelDecimals() + "f", gauge.getMinValue()));
        minValueText.setFill(gauge.getTitleColor());

        maxValueText = new Text(String.format(locale, "%." + gauge.getTickLabelDecimals() + "f", gauge.getMaxValue()));
        maxValueText.setFill(gauge.getTitleColor());

        thresholdText = new Text(String.format(locale, "%." + gauge.getTickLabelDecimals() + "f", gauge.getThreshold()));
        thresholdText.setFill(gauge.getTitleColor());
        Helper.enableNode(thresholdText, Double.compare(gauge.getThreshold(), gauge.getMinValue()) != 0 && Double.compare(gauge.getThreshold(), gauge.getMaxValue()) != 0);

        pane = new Pane(barBackground, thresholdBar, needle, titleText, valueText, minValueText, maxValueText, thresholdText);
        pane.setBorder(new Border(new BorderStroke(gauge.getBorderPaint(), BorderStrokeStyle.SOLID, CornerRadii.EMPTY, new BorderWidths(gauge.getBorderWidth()))));
        pane.setBackground(new Background(new BackgroundFill(gauge.getBackgroundPaint(), CornerRadii.EMPTY, Insets.EMPTY)));

        getChildren().setAll(pane);
    }

    @Override protected void registerListeners() {
        super.registerListeners();
        gauge.currentValueProperty().addListener(currentValueListener);
    }


    // ******************** Methods *******************************************
    @Override protected void handleEvents(final String EVENT_TYPE) {
        super.handleEvents(EVENT_TYPE);
        if ("RECALC".equals(EVENT_TYPE)) {
            angleRange = Helper.clamp(90.0, 180.0, gauge.getAngleRange());
            minValue   = gauge.getMinValue();
            range      = gauge.getRange();
            angleStep  = angleRange / range;
            redraw();
        }
    }

    private void rotateNeedle(final double VALUE) {
        double needleStartAngle = angleRange * 0.5;
        double targetAngle = (VALUE - minValue) * angleStep - needleStartAngle;
        targetAngle = Helper.clamp(-needleStartAngle, -needleStartAngle + angleRange, targetAngle);
        needleRotate.setAngle(targetAngle);
        valueText.setText(formatNumber(gauge.getLocale(), gauge.getFormatString(), gauge.getDecimals(), VALUE));
        resizeValueText();
    }

    private void drawNeedle() {
        double needleWidth  = size * 0.064;
        double needleHeight = size * 0.44;
        needle.setCache(false);
        needle.getElements().clear();
        needle.getElements().add(new MoveTo(0.1875 * needleWidth, 0.0));
        needle.getElements().add(new CubicCurveTo(0.1875 * needleWidth, 0.0,
                                                0.1875 * needleWidth, 0.8727272727272727 * needleHeight,
                                                0.1875 * needleWidth, 0.8727272727272727 * needleHeight));
        needle.getElements().add(new CubicCurveTo(0.0625 * needleWidth, 0.8818181818181818 * needleHeight,
                                                0.0, 0.9 * needleHeight,
                                                0.0, 0.9272727272727272 * needleHeight));
        needle.getElements().add(new CubicCurveTo(0.0, 0.9636363636363636 * needleHeight,
                                                0.25 * needleWidth, needleHeight,
                                                0.5 * needleWidth, needleHeight));
        needle.getElements().add(new CubicCurveTo(0.75 * needleWidth, needleHeight,
                                                needleWidth, 0.9636363636363636 * needleHeight,
                                                needleWidth, 0.9272727272727272 * needleHeight));
        needle.getElements().add(new CubicCurveTo(needleWidth, 0.9 * needleHeight,
                                                0.9375 * needleWidth, 0.8818181818181818 * needleHeight,
                                                0.8125 * needleWidth, 0.8727272727272727 * needleHeight));
        needle.getElements().add(new CubicCurveTo(0.8125 * needleWidth, 0.8727272727272727 * needleHeight,
                                                0.8125 * needleWidth, 0.0,
                                                0.8125 * needleWidth, 0.0));
        needle.getElements().add(new LineTo(0.1875 * needleWidth, 0.0));
        needle.getElements().add(new ClosePath());
        needle.setCache(true);
        needle.setCacheHint(CacheHint.ROTATE);
    }

    @Override public void dispose() {
        gauge.currentValueProperty().removeListener(currentValueListener);
        super.dispose();
    }


    // ******************** Resizing ******************************************
    private void resizeValueText() {
        double maxWidth = 0.86466165 * size;
        double fontSize = 0.192 * size;
        valueText.setFont(Fonts.latoRegular(fontSize));
        if (valueText.getLayoutBounds().getWidth() > maxWidth) { Helper.adjustTextSize(valueText, maxWidth, fontSize); }
        valueText.relocate((size - valueText.getLayoutBounds().getWidth()) * 0.5, (size * 0.744));
    }
    private void resizeStaticText() {
        double maxWidth   = 0.98 * size;
        double fontSize   = size * 0.104;
        double textRadius;
        double textAngle;
        double sinValue;
        double cosValue;
        double textX;
        double textY;

        titleText.setFont(Fonts.latoRegular(fontSize));
        if (titleText.getLayoutBounds().getWidth() > maxWidth) { Helper.adjustTextSize(titleText, maxWidth, fontSize); }
        titleText.relocate(size * 0.02, size * 0.02);

        maxWidth = size * 0.144;
        fontSize = size * 0.048;
        maxValueText.setFont(Fonts.latoRegular(fontSize));
        if (maxValueText.getLayoutBounds().getWidth() > maxWidth) { Helper.adjustTextSize(maxValueText, maxWidth, fontSize); }
        textRadius = size * 0.45;
        sinValue  = Math.sin(Math.toRadians(90 + (180 - angleRange) * 0.5));
        cosValue  = Math.cos(Math.toRadians(90 + (180 - angleRange) * 0.5));
        textX     = size * 0.5 + textRadius * sinValue;
        textY     = size * 0.672 + textRadius * cosValue;
        maxValueText.setTranslateX(-maxValueText.getLayoutBounds().getWidth() * 0.5);
        maxValueText.setTranslateY(-maxValueText.getLayoutBounds().getHeight() * 0.5);
        maxValueText.relocate(textX, textY);

        minValueText.setFont(Fonts.latoRegular(maxValueText.getFont().getSize()));
        if (minValueText.getLayoutBounds().getWidth() > maxWidth) { Helper.adjustTextSize(minValueText, maxWidth, fontSize); }
        sinValue  = Math.sin(Math.toRadians(-90 - (180 - angleRange) * 0.5));
        cosValue  = Math.cos(Math.toRadians(-90 - (180 - angleRange) * 0.5));
        textX     = size * 0.5 + textRadius * sinValue;
        textY     = size * 0.672 + textRadius * cosValue;
        minValueText.setTranslateX(-minValueText.getLayoutBounds().getWidth() * 0.5);
        minValueText.setTranslateY(-minValueText.getLayoutBounds().getHeight() * 0.5);
        minValueText.relocate(textX, textY);

        fontSize = size * 0.08;
        thresholdText.setFont(Fonts.latoRegular(fontSize));
        if (thresholdText.getLayoutBounds().getWidth() > maxWidth) { Helper.adjustTextSize(thresholdText, maxWidth, fontSize); }
        textRadius = size * 0.5;
        textAngle  = (gauge.getThreshold() - minValue) * angleStep;
        sinValue   = Math.sin(Math.toRadians(180 + angleRange * 0.5 - textAngle));
        cosValue   = Math.cos(Math.toRadians(180 + angleRange * 0.5 - textAngle));
        textX      = size * 0.5 + textRadius * sinValue;
        textY      = size * 0.72 + textRadius * cosValue;
        thresholdText.setTranslateX(-thresholdText.getLayoutBounds().getWidth() * 0.5);
        thresholdText.setTranslateY(-thresholdText.getLayoutBounds().getHeight() * 0.5);
        thresholdText.relocate(textX, textY);
    }

    @Override protected void resize() {
        double width  = gauge.getWidth() - gauge.getInsets().getLeft() - gauge.getInsets().getRight();
        double height = gauge.getHeight() - gauge.getInsets().getTop() - gauge.getInsets().getBottom();
        size          = width < height ? width : height;

        if (width > 0 && height > 0) {
            double centerX   = size * 0.5;
            double centerY   = size * 0.696;
            double barRadius = size * 0.275;
            double barWidth  = size * 0.216;

            pane.setMaxSize(size, size);
            pane.relocate((width - size) * 0.5, (height - size) * 0.5);

            barBackground.setCenterX(centerX);
            barBackground.setCenterY(centerY - size * 0.008);
            barBackground.setRadiusX(barRadius);
            barBackground.setRadiusY(barRadius);
            barBackground.setStrokeWidth(barWidth);
            barBackground.setStartAngle(angleRange * 0.5 + 90);
            barBackground.setLength(-angleRange);

            thresholdBar.setCenterX(centerX);
            thresholdBar.setCenterY(centerY - size * 0.008);
            thresholdBar.setRadiusX(barRadius);
            thresholdBar.setRadiusY(barRadius);
            thresholdBar.setStrokeWidth(barWidth);
            thresholdBar.setStartAngle(90 - angleRange * 0.5);
            thresholdBar.setLength((gauge.getMaxValue() - gauge.getThreshold()) * angleStep);

            drawNeedle();

            needle.relocate((size - needle.getLayoutBounds().getWidth()) * 0.5, centerY - needle.getLayoutBounds().getHeight() + needle.getLayoutBounds().getWidth() * 0.5);
            needleRotate.setPivotX(needle.getLayoutBounds().getWidth() * 0.5);
            needleRotate.setPivotY(needle.getLayoutBounds().getHeight() - needle.getLayoutBounds().getWidth() * 0.5);

            resizeStaticText();
            resizeValueText();
        }
    }

    @Override protected void redraw() {
        pane.setBorder(new Border(new BorderStroke(gauge.getBorderPaint(), BorderStrokeStyle.SOLID, CornerRadii.EMPTY, new BorderWidths(gauge.getBorderWidth() / PREFERRED_WIDTH * size))));
        pane.setBackground(new Background(new BackgroundFill(gauge.getBackgroundPaint(), CornerRadii.EMPTY, Insets.EMPTY)));

        locale = gauge.getLocale();

        titleText.setText(gauge.getTitle());
        minValueText.setText(String.format(locale, "%." + gauge.getTickLabelDecimals() + "f", gauge.getMinValue()));
        maxValueText.setText(String.format(locale, "%." + gauge.getTickLabelDecimals() + "f", gauge.getMaxValue()));
        thresholdText.setText(String.format(locale, "%." + gauge.getTickLabelDecimals() + "f", gauge.getThreshold()));
        resizeStaticText();

        barBackground.setStroke(gauge.getBarColor());
        thresholdBar.setStroke(gauge.getThresholdColor());
        needle.setFill(gauge.getNeedleColor());
        titleText.setFill(gauge.getTitleColor());
        minValueText.setFill(gauge.getTitleColor());
        maxValueText.setFill(gauge.getTitleColor());
        thresholdText.setFill(gauge.getTitleColor());
        valueText.setFill(gauge.getValueColor());

        thresholdText.setVisible(Double.compare(gauge.getThreshold(), gauge.getMinValue()) != 0 && Double.compare(gauge.getThreshold(), gauge.getMaxValue()) != 0);
    }
}