/*
 * 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.Gauge.ScaleDirection;
import eu.hansolo.medusa.Section;
import eu.hansolo.medusa.tools.Helper;
import javafx.beans.InvalidationListener;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.scene.CacheHint;
import javafx.scene.control.Tooltip;
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.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.shape.StrokeType;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import javafx.scene.transform.Rotate;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;


/**
 * Created by hansolo on 16.01.16.
 */
public class IndicatorSkin extends GaugeSkinBase {
    protected static final double PREFERRED_WIDTH  = 250;
    protected static final double PREFERRED_HEIGHT = 165;
    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;
    private static final double   ASPECT_RATIO     = 0.59375;
    private double                width;
    private double                height;
    private double                oldValue;
    private Arc                   barBackground;
    private Pane                  sectionLayer;
    private Arc                   bar;
    private Path                  needle;
    private MoveTo                needleMoveTo1;
    private CubicCurveTo          needleCubicCurveTo2;
    private CubicCurveTo          needleCubicCurveTo3;
    private CubicCurveTo          needleCubicCurveTo4;
    private CubicCurveTo          needleCubicCurveTo5;
    private CubicCurveTo          needleCubicCurveTo6;
    private CubicCurveTo          needleCubicCurveTo7;
    private ClosePath             needleClosePath8;
    private Rotate                needleRotate;
    private Text                  minValueText;
    private Text                  maxValueText;
    private Text                  titleText;
    private Pane                  pane;
    private double                angleRange;
    private double                minValue;
    private double                range;
    private double                angleStep;
    private double                startAngle;
    private boolean               colorGradientEnabled;
    private int                   noOfGradientStops;
    private boolean               sectionsAlwaysVisible;
    private boolean               sectionsVisible;
    private List<Section>         sections;
    private Tooltip               needleTooltip;
    private String                formatString;
    private Locale                locale;
    private Color                 barColor;
    private Tooltip               barTooltip;
    private InvalidationListener  currentValueListener;
    private InvalidationListener  sectionAlwaysVisibleListener;


    // ******************** Constructors **************************************
    public IndicatorSkin(Gauge gauge) {
        super(gauge);
        if (gauge.isAutoScale()) gauge.calcAutoScale();
        angleRange                   = Helper.clamp(90.0, 180.0, gauge.getAngleRange());
        startAngle                   = getStartAngle();
        oldValue                     = gauge.getValue();
        minValue                     = gauge.getMinValue();
        range                        = gauge.getRange();
        angleStep                    = angleRange / range;
        colorGradientEnabled         = gauge.isGradientBarEnabled();
        noOfGradientStops            = gauge.getGradientBarStops().size();
        sectionsAlwaysVisible        = gauge.getSectionsAlwaysVisible();
        sectionsVisible              = gauge.getSectionsVisible();
        sections                     = gauge.getSections();
        formatString                 = new StringBuilder("%.").append(Integer.toString(gauge.getDecimals())).append("f").toString();
        locale                       = gauge.getLocale();
        barColor                     = gauge.getBarColor();
        currentValueListener         = o -> rotateNeedle(gauge.getCurrentValue());
        sectionAlwaysVisibleListener = o -> bar.setVisible(!gauge.getSectionsAlwaysVisible());

        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.getBarBackgroundColor());
        barBackground.setStrokeWidth(PREFERRED_WIDTH * 0.02819549 * 2);
        barBackground.setStrokeLineCap(StrokeLineCap.BUTT);
        barBackground.setFill(null);

        sectionLayer = new Pane();
        sectionLayer.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)));

        bar = new Arc(PREFERRED_WIDTH * 0.5, PREFERRED_HEIGHT * 0.696, PREFERRED_WIDTH * 0.275, PREFERRED_WIDTH * 0.275, angleRange * 0.5 + 90, 0);
        bar.setType(ArcType.OPEN);
        bar.setStroke(gauge.getBarColor());
        bar.setStrokeWidth(PREFERRED_WIDTH * 0.02819549 * 2);
        bar.setStrokeLineCap(StrokeLineCap.BUTT);
        bar.setFill(null);
        //bar.setMouseTransparent(sectionsAlwaysVisible ? true : false);
        bar.setVisible(!sectionsAlwaysVisible);

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

        needleMoveTo1       = new MoveTo();
        needleCubicCurveTo2 = new CubicCurveTo();
        needleCubicCurveTo3 = new CubicCurveTo();
        needleCubicCurveTo4 = new CubicCurveTo();
        needleCubicCurveTo5 = new CubicCurveTo();
        needleCubicCurveTo6 = new CubicCurveTo();
        needleCubicCurveTo7 = new CubicCurveTo();
        needleClosePath8    = new ClosePath();
        needle = new Path(needleMoveTo1, needleCubicCurveTo2, needleCubicCurveTo3, needleCubicCurveTo4, needleCubicCurveTo5, needleCubicCurveTo6, needleCubicCurveTo7, needleClosePath8);
        needle.setFillRule(FillRule.EVEN_ODD);
        needle.getTransforms().setAll(needleRotate);
        needle.setFill(gauge.getNeedleColor());
        needle.setPickOnBounds(false);
        needle.setStrokeType(StrokeType.INSIDE);
        needle.setStrokeWidth(1);
        needle.setStroke(gauge.getBackgroundPaint());

        needleTooltip = new Tooltip(String.format(locale, formatString, gauge.getValue()));
        needleTooltip.setTextAlignment(TextAlignment.CENTER);
        Tooltip.install(needle, needleTooltip);

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

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

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

        if (!sections.isEmpty() && sectionsVisible && !sectionsAlwaysVisible) {
            barTooltip = new Tooltip();
            barTooltip.setTextAlignment(TextAlignment.CENTER);
            Tooltip.install(bar, barTooltip);
        }

        pane = new Pane(barBackground, sectionLayer, bar, needle, minValueText, maxValueText, titleText);
        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);
        gauge.sectionsAlwaysVisibleProperty().addListener(sectionAlwaysVisibleListener);
    }


    // ******************** 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());
            startAngle = getStartAngle();
            minValue   = gauge.getMinValue();
            range      = gauge.getRange();
            sections   = gauge.getSections();
            angleStep  = angleRange / range;
            redraw();
            rotateNeedle(gauge.getCurrentValue());
        } else if ("FINISHED".equals(EVENT_TYPE)) {
            String text = String.format(locale, formatString, gauge.getValue());
            needleTooltip.setText(text);
            double value = gauge.getValue();
            if (gauge.isValueVisible()) {
                Bounds bounds       = barBackground.localToScreen(barBackground.getBoundsInLocal());
                double tooltipAngle = needleRotate.getAngle();
                double sinValue     = Math.sin(Math.toRadians(90 + angleRange * 0.5 - tooltipAngle));
                double cosValue     = Math.cos(Math.toRadians(90 + angleRange * 0.5 - tooltipAngle));
                double needleTipX   = bounds.getMinX() + bounds.getWidth() * 0.5 + bounds.getHeight() * sinValue;
                double needleTipY   = bounds.getMinY() + bounds.getHeight() * 0.8 + bounds.getHeight() * cosValue;
                if (value < (gauge.getMinValue() + (gauge.getRange() * 0.5))) {
                    needleTipX -= text.length() * 7;
                }
                needleTooltip.show(needle, needleTipX, needleTipY);
            }
            if (sections.isEmpty() || sectionsAlwaysVisible) return;
            for (Section section : sections) {
                if (section.contains(value)) {
                    barTooltip.setText(section.getText());
                    break;
                }
            }
        } else if ("VISIBILITY".equals(EVENT_TYPE)) {
            Helper.enableNode(titleText, !gauge.getTitle().isEmpty());
        }
    }

    private double getStartAngle() {
        ScaleDirection scaleDirection = gauge.getScaleDirection();
        switch(scaleDirection) {
            //case COUNTER_CLOCKWISE: return 180 - angleRange * 0.5;
            case CLOCKWISE        :
            default               : return 180 + angleRange * 0.5;
        }
    }

    private void rotateNeedle(final double VALUE) {
        double needleStartAngle = angleRange * 0.5;
        double targetAngle      = Helper.clamp(-needleStartAngle, -needleStartAngle + angleRange, ((gauge.getCurrentValue() - minValue) * angleStep - needleStartAngle));
        needleRotate.setAngle(targetAngle);
        bar.setLength(-90 - targetAngle);
        setBarColor(VALUE);
    }
    
    private void setBarColor(final double VALUE) {
        if (!sectionsVisible && !colorGradientEnabled) {
            bar.setStroke(barColor);
        } else if (colorGradientEnabled && noOfGradientStops > 1) {
            bar.setStroke(gauge.getGradientLookup().getColorAt((VALUE - minValue) / range));
        } else {
            bar.setStroke(barColor);
            for (Section section : sections) {
                if (section.contains(VALUE)) {
                    bar.setStroke(section.getColor());
                    break;
                }
            }
        }
    }

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


    // ******************** Resizing ******************************************
    private void resizeStaticText() {
        double maxWidth = width * 0.28472222;
        double fontSize = height * 0.12631579;

        minValueText.setFont(Fonts.latoRegular(fontSize));
        if (minValueText.getLayoutBounds().getWidth() > maxWidth) { Helper.adjustTextSize(minValueText, maxWidth, fontSize); }
        minValueText.relocate((width * 0.28472222) - minValueText.getLayoutBounds().getWidth(), height * 0.885);

        maxValueText.setFont(Fonts.latoRegular(fontSize));
        if (maxValueText.getLayoutBounds().getWidth() > maxWidth) { Helper.adjustTextSize(maxValueText, maxWidth, fontSize); }
        maxValueText.relocate(width * 0.71527778, height * 0.885);

        maxWidth = width * 0.9;
        titleText.setFont(Fonts.latoRegular(fontSize));
        if (titleText.getLayoutBounds().getWidth() > maxWidth) { Helper.adjustTextSize(titleText, maxWidth, fontSize); }
        titleText.relocate((width - titleText.getLayoutBounds().getWidth()) * 0.5, height * 0.95);
    }

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

        if (ASPECT_RATIO * width > height) {
            width = 1 / (ASPECT_RATIO / height);
        } else if (1 / (ASPECT_RATIO / height) > width) {
            height = ASPECT_RATIO * width;
        }

        if (width > 0 && height > 0) {
            double centerX   = width * 0.5;
            double centerY   = height * 0.85;
            double barRadius = height * 0.54210526;
            double barWidth  = width * 0.28472222;

            pane.setMaxSize(width, height);
            pane.relocate((gauge.getWidth() - width) * 0.5, (gauge.getHeight() - height) * 0.5);

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

            if (sectionsVisible && sectionsAlwaysVisible) {
                sectionLayer.setPrefSize(width, height);
                drawSections();
            }

            double needleStartAngle = angleRange * 0.5;
            double targetAngle      = Helper.clamp(-needleStartAngle, -needleStartAngle + angleRange, ((gauge.getCurrentValue() - minValue) * angleStep - needleStartAngle));

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

            double needleWidth  = height * 0.13157895;
            double needleHeight = height * 0.91315789;

            needle.setCache(true);

            needleMoveTo1.setX(0.0); needleMoveTo1.setY(0.927953890489914 * needleHeight);

            needleCubicCurveTo2.setControlX1(0); needleCubicCurveTo2.setControlY1(0.968299711815562 * needleHeight);
            needleCubicCurveTo2.setControlX2(0.22 * needleWidth); needleCubicCurveTo2.setControlY2(needleHeight);
            needleCubicCurveTo2.setX(0.5 * needleWidth); needleCubicCurveTo2.setY(needleHeight);

            needleCubicCurveTo3.setControlX1(0.78 * needleWidth); needleCubicCurveTo3.setControlY1(needleHeight);
            needleCubicCurveTo3.setControlX2(needleWidth); needleCubicCurveTo3.setControlY2(0.968299711815562 * needleHeight);
            needleCubicCurveTo3.setX(needleWidth); needleCubicCurveTo3.setY(0.927953890489914 * needleHeight);

            needleCubicCurveTo4.setControlX1(needleWidth); needleCubicCurveTo4.setControlY1(0.92507204610951 * needleHeight);
            needleCubicCurveTo4.setControlX2(0.6 * needleWidth); needleCubicCurveTo4.setControlY2(0.0144092219020173 * needleHeight);
            needleCubicCurveTo4.setX(0.6 * needleWidth); needleCubicCurveTo4.setY(0.0144092219020173 * needleHeight);

            needleCubicCurveTo5.setControlX1(0.6 * needleWidth); needleCubicCurveTo5.setControlY1(0.0144092219020173 * needleHeight);
            needleCubicCurveTo5.setControlX2(0.58 * needleWidth); needleCubicCurveTo5.setControlY2(0);
            needleCubicCurveTo5.setX(0.5 * needleWidth); needleCubicCurveTo5.setY(0);

            needleCubicCurveTo6.setControlX1(0.42 * needleWidth); needleCubicCurveTo6.setControlY1(0);
            needleCubicCurveTo6.setControlX2(0.4 * needleWidth); needleCubicCurveTo6.setControlY2(0.0144092219020173 * needleHeight);
            needleCubicCurveTo6.setX(0.4 * needleWidth); needleCubicCurveTo6.setY(0.0144092219020173 * needleHeight);

            needleCubicCurveTo7.setControlX1(0.4 * needleWidth); needleCubicCurveTo7.setControlY1(0.0144092219020173 * needleHeight);
            needleCubicCurveTo7.setControlX2(0); needleCubicCurveTo7.setControlY2(0.92507204610951 * needleHeight);
            needleCubicCurveTo7.setX(0); needleCubicCurveTo7.setY(0.927953890489914 * needleHeight);

            needle.setCache(true);
            needle.setCacheHint(CacheHint.ROTATE);

            needle.relocate((width - 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();
        }
    }

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

        barColor             = gauge.getBarColor();

        locale               = gauge.getLocale();
        formatString         = new StringBuilder("%.").append(Integer.toString(gauge.getDecimals())).append("f").toString();
        colorGradientEnabled = gauge.isGradientBarEnabled();
        noOfGradientStops    = gauge.getGradientBarStops().size();
        sectionsVisible      = gauge.getSectionsVisible();

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

        barBackground.setStroke(gauge.getBarBackgroundColor());
        bar.setStroke(gauge.getBarColor());
        needle.setFill(gauge.getNeedleColor());

        minValueText.setVisible(gauge.getTickLabelsVisible());
        maxValueText.setVisible(gauge.getTickLabelsVisible());

        minValueText.setFill(gauge.getTitleColor());
        maxValueText.setFill(gauge.getTitleColor());
        titleText.setFill(gauge.getTitleColor());
    }
    
    private void drawSections() {
        if (sections.isEmpty()) return;
        sectionLayer.getChildren().clear();

        double    centerX     = width * 0.5;
        double    centerY     = height * 0.85;
        double    barRadius   = height * 0.54210526;
        double    barWidth    = width * 0.28472222;
        List<Arc> sectionBars = new ArrayList<>(sections.size());
        for (Section section : sections) {
            Arc sectionBar = new Arc(centerX, centerY, barRadius, barRadius, angleRange * 0.5 + 90 - (section.getStart() * angleStep), -((section.getStop() - section.getStart()) - minValue) * angleStep);
            sectionBar.setType(ArcType.OPEN);
            sectionBar.setStroke(section.getColor());
            sectionBar.setStrokeWidth(barWidth);
            sectionBar.setStrokeLineCap(StrokeLineCap.BUTT);
            sectionBar.setFill(null);
            Tooltip sectionTooltip = new Tooltip(new StringBuilder(section.getText()).append("\n").append(String.format(Locale.US, "%.2f", section.getStart())).append(" - ").append(String.format(Locale.US, "%.2f", section.getStop())).toString());
            sectionTooltip.setTextAlignment(TextAlignment.CENTER);
            Tooltip.install(sectionBar, sectionTooltip);
            sectionBars.add(sectionBar);
        }
        sectionLayer.getChildren().addAll(sectionBars);
    }
}