/*
 * 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.Section;
import eu.hansolo.medusa.TickLabelOrientation;
import eu.hansolo.medusa.tools.Helper;
import java.math.BigDecimal;
import java.util.List;
import java.util.Locale;
import javafx.beans.InvalidationListener;
import javafx.collections.ListChangeListener;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.geometry.VPos;
import javafx.scene.CacheHint;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Tooltip;
import javafx.scene.effect.BlurType;
import javafx.scene.effect.DropShadow;
import javafx.scene.effect.InnerShadow;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.ArcType;
import javafx.scene.shape.Circle;
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.shape.StrokeLineJoin;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import javafx.scene.transform.Rotate;

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


/**
 * Created by hansolo on 01.01.16.
 */
public class ModernSkin extends GaugeSkinBase {
    private double                      START_ANGLE      = 300;
    private double                      ANGLE_RANGE      = 240;
    private double                      BAR_START_ANGLE  = -150;
    private double                      size;
    private Pane                        pane;
    private Circle                      background;
    private Path                        mask;
    private Circle                      centerKnob;
    private Path                        needle;
    private MoveTo                      needleMoveTo1;
    private CubicCurveTo                needleCubicCurveTo2;
    private CubicCurveTo                needleCubicCurveTo3;
    private CubicCurveTo                needleCubicCurveTo4;
    private LineTo                      needleLineTo5;
    private LineTo                      needleLineTo6;
    private LineTo                      needleLineTo7;
    private LineTo                      needleLineTo8;
    private ClosePath                   needleClosePath9;
    private Rotate                      needleRotate;
    private Canvas                      mainCanvas;
    private GraphicsContext             mainCtx;
    private Canvas                      tickMarkCanvas;
    private GraphicsContext             tickMarkCtx;
    private InnerShadow                 innerShadow0;
    private InnerShadow                 innerShadow1;
    private InnerShadow                 innerShadow2;
    private InnerShadow                 innerShadow3;
    private DropShadow                  dropShadow4;
    private DropShadow                  glow1;
    private DropShadow                  glow2;
    private DropShadow                  bigGlow;
    private Text                        valueText;
    private Text                        titleText;
    private Text                        subTitleText;
    private Text                        unitText;
    private double                      angleStep;
    private EventHandler<MouseEvent>    mouseHandler;
    private Tooltip                     buttonTooltip;
    private Locale                      locale;
    private boolean                     sectionsVisible;
    private List<Section>               sections;
    private Color                       barColor;
    private Color                       thresholdColor;
    private InvalidationListener        animatedListener;
    private ListChangeListener<Section> sectionListener;
    private InvalidationListener        currentValueListener;
    private int                         titleLength;
    private int                         subTitleLength;
    private int                         unitLength;


    // ******************** Constructors **************************************
    public ModernSkin(Gauge gauge) {
        super(gauge);
        if (gauge.isAutoScale()) gauge.calcAutoScale();
        angleStep            = ANGLE_RANGE / (gauge.getRange());
        mouseHandler         = event -> handleMouseEvent(event);
        buttonTooltip        = new Tooltip();
        locale               = gauge.getLocale();
        sectionsVisible      = gauge.getSectionsVisible();
        sections             = gauge.getSections();
        barColor             = gauge.getBarColor();
        thresholdColor       = gauge.getThresholdColor();
        animatedListener     = o -> handleEvents("ANIMATED");
        sectionListener      = c -> handleEvents("RESIZE");
        currentValueListener = o -> rotateNeedle(gauge.getCurrentValue());
        titleLength          = 0;
        subTitleLength       = 0;
        unitLength           = 0;

        initGraphics();
        registerListeners();
    }


    // ******************** 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);
            }
        }

        innerShadow0 = new InnerShadow(BlurType.TWO_PASS_BOX, Color.rgb(0, 0, 0, 0.65), 1, 0, 0, 1);

        innerShadow1 = new InnerShadow(BlurType.TWO_PASS_BOX, Color.rgb(255, 255, 255, 0.65), 1, 0, 0, -1);
        innerShadow1.setInput(innerShadow0);

        background = new Circle(PREFERRED_WIDTH * 0.5, PREFERRED_HEIGHT * 0.5, PREFERRED_WIDTH * 0.5);
        background.setFill(Color.rgb(32, 32, 32));
        background.setStroke(null);
        background.setEffect(innerShadow1);

        innerShadow2 = new InnerShadow(BlurType.TWO_PASS_BOX, Color.rgb(255, 255, 255, 0.65), 1, 0, 0, 1);

        innerShadow3 = new InnerShadow(BlurType.TWO_PASS_BOX, Color.rgb(0, 0, 0, 0.65), 1, 0, 0, -1);
        innerShadow3.setInput(innerShadow2);

        dropShadow4 = new DropShadow();
        dropShadow4.setBlurType(BlurType.TWO_PASS_BOX);
        dropShadow4.setColor(Color.rgb(0, 0, 0, 0.65));
        dropShadow4.setOffsetX(0);
        dropShadow4.setInput(innerShadow3);

        mask = new Path();
        mask.setFillRule(FillRule.EVEN_ODD);
        mask.setFill(Color.rgb(32, 32, 32));
        mask.setStroke(null);
        mask.setEffect(dropShadow4);

        needleRotate = new Rotate(180 - START_ANGLE);

        double targetAngle = 180 - START_ANGLE + (gauge.getCurrentValue() - gauge.getMinValue()) * angleStep;
        needleRotate.setAngle(Helper.clamp(180 - START_ANGLE, 180 - START_ANGLE + ANGLE_RANGE, targetAngle));

        glow1   = new DropShadow(BlurType.TWO_PASS_BOX, barColor, 0.085 * PREFERRED_WIDTH, 0, 0, 0);
        glow2   = new DropShadow(BlurType.TWO_PASS_BOX, barColor, 0.085 * PREFERRED_WIDTH, 0, 0, 0);
        bigGlow = new DropShadow(BlurType.TWO_PASS_BOX, barColor, 0.25 * PREFERRED_WIDTH, 0, 0, 0);

        needleMoveTo1       = new MoveTo();
        needleCubicCurveTo2 = new CubicCurveTo();
        needleCubicCurveTo3 = new CubicCurveTo();
        needleCubicCurveTo4 = new CubicCurveTo();
        needleLineTo5       = new LineTo();
        needleLineTo6       = new LineTo();
        needleLineTo7       = new LineTo();
        needleLineTo8       = new LineTo();
        needleClosePath9    = new ClosePath();
        needle              = new Path(needleMoveTo1, needleCubicCurveTo2, needleCubicCurveTo3, needleCubicCurveTo4,
                                       needleLineTo5, needleLineTo6, needleLineTo7, needleLineTo8, needleClosePath9);
        needle.setFillRule(FillRule.EVEN_ODD);
        needle.setEffect(glow1);

        needle.getTransforms().setAll(needleRotate);
        needle.setFill(gauge.getNeedleColor());
        needle.setStroke(null);
        needle.setStrokeLineCap(StrokeLineCap.ROUND);
        needle.setStrokeLineJoin(StrokeLineJoin.BEVEL);

        mainCanvas = new Canvas(PREFERRED_WIDTH, PREFERRED_HEIGHT);
        mainCtx    = mainCanvas.getGraphicsContext2D();

        tickMarkCanvas = new Canvas(PREFERRED_WIDTH, PREFERRED_HEIGHT);
        tickMarkCtx    = tickMarkCanvas.getGraphicsContext2D();

        centerKnob = new Circle(0.5 * PREFERRED_WIDTH, 0.5 * PREFERRED_HEIGHT, 0.22916667 * PREFERRED_WIDTH);
        centerKnob.setPickOnBounds(false);

        titleText = new Text(gauge.getTitle());
        titleText.setTextOrigin(VPos.CENTER);
        titleText.setFill(gauge.getTitleColor());
        titleText.setEffect(glow1);
        titleText.setMouseTransparent(true);
        Helper.enableNode(titleText, !gauge.getTitle().isEmpty());

        subTitleText = new Text(gauge.getSubTitle());
        subTitleText.setTextOrigin(VPos.CENTER);
        subTitleText.setFill(gauge.getSubTitleColor());
        subTitleText.setEffect(glow1);
        subTitleText.setMouseTransparent(true);
        Helper.enableNode(subTitleText, !gauge.getSubTitle().isEmpty());

        unitText = new Text(gauge.getUnit());
        unitText.setTextOrigin(VPos.CENTER);
        unitText.setFill(gauge.getUnitColor());
        unitText.setEffect(glow1);
        unitText.setMouseTransparent(true);
        Helper.enableNode(unitText, !gauge.getUnit().isEmpty());

        valueText = new Text(formatNumber(gauge.getLocale(), gauge.getFormatString(), gauge.getDecimals(), gauge.getCurrentValue()));
        valueText.setMouseTransparent(true);
        valueText.setTextOrigin(VPos.CENTER);
        valueText.setFill(gauge.getValueColor());
        valueText.setEffect(bigGlow);
        Helper.enableNode(valueText, gauge.isValueVisible());

        // Add all nodes
        pane = new Pane(background,
                        mainCanvas,
                        tickMarkCanvas,
                        mask,
                        needle,
                        centerKnob,
                        titleText,
                        subTitleText,
                        unitText,
                        valueText);

        getChildren().setAll(pane);
    }

    @Override protected void registerListeners() {
        super.registerListeners();
        gauge.animatedProperty().addListener(animatedListener);
        gauge.getSections().addListener(sectionListener);
        gauge.currentValueProperty().addListener(currentValueListener);

        handleEvents("INTERACTIVITY");
    }


    // ******************** Methods *******************************************
    @Override protected void handleEvents(final String EVENT_TYPE) {
        super.handleEvents(EVENT_TYPE);
        if ("VISIBILITY".equals(EVENT_TYPE)) {
            Helper.enableNode(titleText, !gauge.getTitle().isEmpty());
            Helper.enableNode(subTitleText, !gauge.getSubTitle().isEmpty());
            Helper.enableNode(unitText, !gauge.getUnit().isEmpty());
            Helper.enableNode(valueText, gauge.isValueVisible());
            sectionsVisible = gauge.getSectionsVisible();
            redraw();
        } else if ("RECALC".equals(EVENT_TYPE)) {
            angleStep = ANGLE_RANGE / gauge.getRange();
            redraw();
            rotateNeedle(gauge.getCurrentValue());
        } else if ("INTERACTIVITY".equals(EVENT_TYPE)) {
            if (gauge.isInteractive()) {
                centerKnob.setOnMousePressed(mouseHandler);
                centerKnob.setOnMouseReleased(mouseHandler);
                buttonTooltip.setText(gauge.getButtonTooltipText());
                Tooltip.install(centerKnob, buttonTooltip);
            } else {
                centerKnob.removeEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
                centerKnob.removeEventHandler(MouseEvent.MOUSE_RELEASED, mouseHandler);
                Tooltip.uninstall(centerKnob, buttonTooltip);
            }
        } else if ("SECTIONS".equals(EVENT_TYPE)) {
            sectionsVisible = gauge.getSectionsVisible();
            sections        = gauge.getSections();
        }
    }

    public void handleMouseEvent(final MouseEvent EVENT) {
        if (gauge.isDisabled()) return;
        final EventType TYPE = EVENT.getEventType();
        if (MouseEvent.MOUSE_PRESSED.equals(TYPE)) {
            gauge.fireEvent(gauge.BTN_PRESSED_EVENT);
            centerKnob.setFill(new LinearGradient(0.5 * size, 0.2708333333333333 * size,
                                                  0.5 * size, 0.7291666666666666 * size,
                                                  false, CycleMethod.NO_CYCLE,
                                                  new Stop(0.0, Color.rgb(31, 31, 31)),
                                                  new Stop(1.0, Color.rgb(69, 70, 73))));
            valueText.setTranslateY(size * 0.501);
            subTitleText.setTranslateY(size * 0.3525);
            unitText.setTranslateY(size * 0.6675);
        } else if (MouseEvent.MOUSE_RELEASED.equals(TYPE)) {
            gauge.fireEvent(gauge.BTN_RELEASED_EVENT);
            centerKnob.setFill(new LinearGradient(0.5 * size, 0.2708333333333333 * size,
                                                  0.5 * size, 0.7291666666666666 * size,
                                                  false, CycleMethod.NO_CYCLE,
                                                  new Stop(0.0, Color.rgb(69, 70, 73)),
                                                  new Stop(1.0, Color.rgb(31, 31, 31))));
            valueText.setTranslateY(size * 0.5);
            subTitleText.setTranslateY(size * 0.35);
            unitText.setTranslateY(size * 0.67);
        }
    }

    @Override public void dispose() {
        gauge.animatedProperty().removeListener(animatedListener);
        gauge.getSections().removeListener(sectionListener);
        gauge.currentValueProperty().removeListener(currentValueListener);
        if (gauge.isInteractive()) {
            centerKnob.removeEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
            centerKnob.removeEventHandler(MouseEvent.MOUSE_RELEASED, mouseHandler);
        }
        super.dispose();
    }


    // ******************** Private Methods ***********************************
    private void rotateNeedle(final double VALUE) {
        angleStep          = ANGLE_RANGE / gauge.getRange();
        double targetAngle = 180 - START_ANGLE + (VALUE - gauge.getMinValue()) * angleStep;
        needleRotate.setAngle(Helper.clamp(180 - START_ANGLE, 180 - START_ANGLE + ANGLE_RANGE, targetAngle));
        valueText.setText(formatNumber(gauge.getLocale(), gauge.getFormatString(), gauge.getDecimals(), VALUE));

        resizeText();
        placeTextVerticaly();

        if ( gauge.isThresholdVisible() && VALUE > gauge.getThreshold() ) {
            glow2.setColor(thresholdColor);
            bigGlow.setColor(thresholdColor);
        } else {
            glow2.setColor(barColor);
            bigGlow.setColor(barColor);
        }
        highlightValue(tickMarkCtx, VALUE);
    }

    private void highlightValue( final GraphicsContext CTX, final double CURRENT_VALUE ) {

        CTX.clearRect(0, 0, size, size);

        // highlight tickmarks
        double     centerX                = size * 0.5;
        double     centerY                = size * 0.5;
        double     minorTickSpace         = gauge.getMinorTickSpace();
        double     minValue               = gauge.getMinValue();
        double     maxValue               = gauge.getMaxValue();
        double     tmpAngleStep           = angleStep * minorTickSpace;
        BigDecimal minorTickSpaceBD       = BigDecimal.valueOf(minorTickSpace);
        BigDecimal majorTickSpaceBD       = BigDecimal.valueOf(gauge.getMajorTickSpace());
        BigDecimal mediumCheck2           = BigDecimal.valueOf(2 * minorTickSpace);
        BigDecimal mediumCheck5           = BigDecimal.valueOf(5 * minorTickSpace);
        BigDecimal counterBD              = BigDecimal.valueOf(minValue);
        double     counter                = minValue;
        boolean    majorTickMarksVisible  = gauge.getMajorTickMarksVisible();
        boolean    mediumTickMarksVisible = gauge.getMediumTickMarksVisible();
        double     threshold              = gauge.getThreshold();
        Color      tickMarkColor          = Color.TRANSPARENT;
        Color      highlightColor         = ( !gauge.isThresholdVisible() || CURRENT_VALUE <= gauge.getThreshold() ) ? barColor : thresholdColor;
        boolean    startFromZero          = gauge.isStartFromZero();

        CTX.setLineCap(StrokeLineCap.BUTT);
        CTX.setLineWidth(size * 0.0035);

        for (double angle = 0 ; Double.compare(-ANGLE_RANGE - tmpAngleStep, angle) < 0 ; angle -= tmpAngleStep) {

            double  sinValue          = Math.sin(Math.toRadians(angle + START_ANGLE));
            double  cosValue          = Math.cos(Math.toRadians(angle + START_ANGLE));
            double  innerPointX       = centerX + size * 0.375 * sinValue;
            double  innerPointY       = centerY + size * 0.375 * cosValue;
            double  outerPointX       = centerX + size * 0.425 * sinValue;
            double  outerPointY       = centerY + size * 0.425 * cosValue;
            double  innerMediumPointX = centerX + size * 0.35 * sinValue;
            double  innerMediumPointY = centerY + size * 0.35 * cosValue;
            double  outerMediumPointX = centerX + size * 0.4 * sinValue;
            double  outerMediumPointY = centerY + size * 0.4 * cosValue;
            boolean shouldHighlight   = false;

            if ( startFromZero ) {
                if ( ( CURRENT_VALUE > minValue || minValue < 0 ) && ( CURRENT_VALUE < maxValue || maxValue > 0 ) ) {
                    if ( maxValue < 0 ) {
                        shouldHighlight = counter >= CURRENT_VALUE;
                    } else if ( minValue > 0 ) {
                        shouldHighlight = counter <= CURRENT_VALUE;
                    } else if ( CURRENT_VALUE > 0 ) {
                        shouldHighlight = counter >= 0 && counter <= CURRENT_VALUE;
                    } else {
                        shouldHighlight = counter <= 0 && counter >= CURRENT_VALUE;
                    }
                }
            } else {
                shouldHighlight = counter <= CURRENT_VALUE;
            }

            if (Double.compare(counterBD.remainder(majorTickSpaceBD).doubleValue(), 0.0) == 0) {
                // Draw major tick mark
                if (majorTickMarksVisible) {
                    CTX.setStroke(shouldHighlight ? highlightColor : tickMarkColor);
                    CTX.strokeLine(innerPointX, innerPointY, outerPointX, outerPointY);
                }
            } else if (mediumTickMarksVisible &&
                       Double.compare(minorTickSpaceBD.remainder(mediumCheck2).doubleValue(), 0.0) != 0.0 &&
                       Double.compare(counterBD.remainder(mediumCheck5).doubleValue(), 0.0) == 0.0) {
                // Draw medium tick mark
                CTX.setStroke(shouldHighlight ? highlightColor : tickMarkColor);
                CTX.strokeLine(innerMediumPointX, innerMediumPointY, outerMediumPointX, outerMediumPointY);
            }

            counterBD = counterBD.add(minorTickSpaceBD);
            counter   = counterBD.doubleValue();

        }

        // highlight bar
        double barXY = ( size - 0.75 * size ) * 0.5;
        double barWH = size * 0.75;

        CTX.save();
        CTX.setEffect(glow2);
        CTX.setStroke(highlightColor);
        CTX.setLineWidth(size * 0.01666667);
        CTX.setLineCap(StrokeLineCap.BUTT);

        double barLength    = 0;
        double barStart     = 0;
        double clampedValue = Helper.clamp(minValue, maxValue, CURRENT_VALUE);

        if ( startFromZero ) {
            if ( ( CURRENT_VALUE > minValue || minValue < 0 ) && ( CURRENT_VALUE < maxValue || maxValue > 0 ) ) {
                if ( maxValue < 0 ) {
                    barStart = BAR_START_ANGLE - ANGLE_RANGE;
                    barLength = ( maxValue - clampedValue ) * angleStep;
                } else if ( minValue > 0 ) {
                    barStart = BAR_START_ANGLE;
                    barLength = ( minValue - clampedValue ) * angleStep;
                } else {
                    barStart = BAR_START_ANGLE + minValue * angleStep;
                    barLength = - clampedValue * angleStep;
                }
            }
        } else {
            barStart = BAR_START_ANGLE;
            barLength = ( minValue - clampedValue ) * angleStep;
        }

        CTX.strokeArc(barXY, barXY, barWH, barWH, barStart, barLength, ArcType.OPEN);
        CTX.restore();

        valueText.setText(formatNumber(gauge.getLocale(), gauge.getFormatString(), gauge.getDecimals(), CURRENT_VALUE));

    }

    private void drawMainCanvas() {
        mainCtx.clearRect(0, 0, size, size);
        mainCtx.setFillRule(FillRule.EVEN_ODD);

        // Draw sections if available
        final double sectionsXY = (size - 0.75 * size) * 0.5;
        final double sectionsWH = size * 0.75;
        double minValue         = gauge.getMinValue();
        double maxValue         = gauge.getMaxValue();
        double offset           = 90 - START_ANGLE;
        double sectionWidth     = size * 0.06;
        if (sectionsVisible) {
            int listSize = sections.size();
            for (int i = 0; i < listSize; i++) {
                final Section SECTION = sections.get(i);
                final double  SECTION_START_ANGLE;
                if (Double.compare(SECTION.getStart(), maxValue) <= 0 && Double.compare(SECTION.getStop(), minValue) >= 0) {
                    if (Double.compare(SECTION.getStart(), minValue) < 0 && Double.compare(SECTION.getStop(), maxValue) < 0) {
                        SECTION_START_ANGLE = 0;
                    } else {
                        SECTION_START_ANGLE = (SECTION.getStart() - minValue) * angleStep;
                    }
                    final double SECTION_ANGLE_EXTEND;
                    if (Double.compare(SECTION.getStop(), maxValue) > 0) {
                        SECTION_ANGLE_EXTEND = (maxValue - SECTION.getStart()) * angleStep;
                    } else {
                        SECTION_ANGLE_EXTEND = (SECTION.getStop() - SECTION.getStart()) * angleStep;
                    }
                    mainCtx.save();
                    mainCtx.setStroke(SECTION.getColor());
                    mainCtx.setLineWidth(sectionWidth);
                    mainCtx.setLineCap(StrokeLineCap.BUTT);
                    mainCtx.strokeArc(sectionsXY, sectionsXY, sectionsWH, sectionsWH, -(offset + SECTION_START_ANGLE), -SECTION_ANGLE_EXTEND, ArcType.OPEN);
                    mainCtx.restore();
                }
            }
        }

        // Draw tickmarks
        mainCtx.save();
        drawTickMarks(mainCtx);
        mainCtx.restore();

        // Draw black bar overlay
        mainCtx.save();
        mainCtx.setStroke(Color.rgb(23, 23, 23));
        mainCtx.setLineWidth(size * 0.025);
        mainCtx.setLineCap(StrokeLineCap.BUTT);
        mainCtx.strokeArc(sectionsXY, sectionsXY, sectionsWH, sectionsWH, BAR_START_ANGLE, -ANGLE_RANGE, ArcType.OPEN);
        mainCtx.restore();

        // Draw databar background
        double barXY = (size - 0.75 * size) * 0.5;
        double barWH = size * 0.75;
        mainCtx.save();
        mainCtx.setStroke(Color.rgb(57, 57, 57, 0.75));
        mainCtx.setLineWidth(size * 0.01666667);
        mainCtx.setLineCap(StrokeLineCap.BUTT);
        mainCtx.strokeArc(barXY, barXY, barWH, barWH, BAR_START_ANGLE, -ANGLE_RANGE, ArcType.OPEN);
        mainCtx.restore();

        // Draw threshold
        if (gauge.isThresholdVisible()) {
            mainCtx.save();
            mainCtx.translate(size * 0.5, size * 0.5);
            mainCtx.rotate(((gauge.getThreshold()  - minValue ) * angleStep) - 120);
            mainCtx.beginPath();
            mainCtx.moveTo(0, -size * 0.33);
            mainCtx.lineTo(-size * 0.0125, -size * 0.30833333);
            mainCtx.lineTo(size * 0.0125, -size * 0.30833333);
            mainCtx.closePath();
            mainCtx.setFill(gauge.getNeedleColor());
            mainCtx.fill();
            mainCtx.restore();
        }
    }

    private void drawTickMarks(final GraphicsContext CTX) {
        double               sinValue;
        double               cosValue;
        double               centerX                = size * 0.5;
        double               centerY                = size * 0.5;
        double               minorTickSpace         = gauge.getMinorTickSpace();
        double               minValue               = gauge.getMinValue();
        double               maxValue               = gauge.getMaxValue();
        double               tmpAngleStep           = angleStep * minorTickSpace;
        int                  decimals               = gauge.getTickLabelDecimals();
        BigDecimal           minorTickSpaceBD       = BigDecimal.valueOf(minorTickSpace);
        BigDecimal           majorTickSpaceBD       = BigDecimal.valueOf(gauge.getMajorTickSpace());
        BigDecimal           mediumCheck2           = BigDecimal.valueOf(2 * minorTickSpace);
        BigDecimal           mediumCheck5           = BigDecimal.valueOf(5 * minorTickSpace);
        BigDecimal           counterBD              = BigDecimal.valueOf(minValue);
        double               counter                = minValue;
        boolean              majorTickMarksVisible  = gauge.getMajorTickMarksVisible();
        boolean              mediumTickMarksVisible = gauge.getMediumTickMarksVisible();
        boolean              tickLabelsVisible      = gauge.getTickLabelsVisible();
        TickLabelOrientation tickLabelOrientation   = gauge.getTickLabelOrientation();
        Color                tickMarkColor          = gauge.getTickMarkColor();
        Color                majorTickMarkColor     = tickMarkColor;
        Color                mediumTickMarkColor    = tickMarkColor;
        Color                tickLabelColor         = gauge.getTickLabelColor();

        double innerPointX;
        double innerPointY;
        double outerPointX;
        double outerPointY;
        double innerMediumPointX;
        double innerMediumPointY;
        double outerMediumPointX;
        double outerMediumPointY;
        double textPointX;
        double textPointY;

        double orthTextFactor = 0.46; //TickLabelOrientation.ORTHOGONAL == gauge.getTickLabelOrientation() ? 0.46 : 0.46;

        Font tickLabelFont = Fonts.robotoCondensedLight((decimals == 0 ? 0.047 : 0.040) * size);
        CTX.setFont(tickLabelFont);
        CTX.setTextAlign(TextAlignment.CENTER);
        CTX.setTextBaseline(VPos.CENTER);

        CTX.setLineCap(StrokeLineCap.BUTT);
        CTX.setLineWidth(size * 0.0035);
        for (double angle = 0 ; Double.compare(-ANGLE_RANGE - tmpAngleStep, angle) < 0 ; angle -= tmpAngleStep) {
            sinValue = Math.sin(Math.toRadians(angle + START_ANGLE));
            cosValue = Math.cos(Math.toRadians(angle + START_ANGLE));

            innerPointX       = centerX + size * 0.375 * sinValue;
            innerPointY       = centerY + size * 0.375 * cosValue;
            outerPointX       = centerX + size * 0.425 * sinValue;
            outerPointY       = centerY + size * 0.425 * cosValue;
            innerMediumPointX = centerX + size * 0.35 * sinValue;
            innerMediumPointY = centerY + size * 0.35 * cosValue;
            outerMediumPointX = centerX + size * 0.4 * sinValue;
            outerMediumPointY = centerY + size * 0.4 * cosValue;
            textPointX        = centerX + size * orthTextFactor * sinValue;
            textPointY        = centerY + size * orthTextFactor * cosValue;

            // Set the general tickmark color
            CTX.setStroke(tickMarkColor);

            if (Double.compare(counterBD.remainder(majorTickSpaceBD).doubleValue(), 0.0) == 0) {
                // Draw major tick mark
                if (majorTickMarksVisible) {
                    CTX.setFill(majorTickMarkColor);
                    CTX.setStroke(majorTickMarkColor);
                    CTX.strokeLine(innerPointX, innerPointY, outerPointX, outerPointY);
                }
                // Draw tick label text
                if (tickLabelsVisible) {
                    CTX.save();
                    CTX.translate(textPointX, textPointY);

                    Helper.rotateContextForText(CTX, START_ANGLE, angle, tickLabelOrientation);

                    CTX.setFill(tickLabelColor);
                    if (TickLabelOrientation.HORIZONTAL == tickLabelOrientation &&
                        (Double.compare(counter, minValue) == 0 ||
                        Double.compare(counter, maxValue) == 0)) {
                            CTX.setFill(Color.TRANSPARENT);
                    }
                    CTX.fillText(String.format(locale, "%." + decimals + "f", counter), 0, 0);
                    CTX.restore();
                }
            } else if (mediumTickMarksVisible &&
                       Double.compare(minorTickSpaceBD.remainder(mediumCheck2).doubleValue(), 0.0) != 0.0 &&
                       Double.compare(counterBD.remainder(mediumCheck5).doubleValue(), 0.0) == 0.0) {
                // Draw medium tick mark
                CTX.setFill(mediumTickMarkColor);
                CTX.setStroke(mediumTickMarkColor);
                CTX.strokeLine(innerMediumPointX, innerMediumPointY, outerMediumPointX, outerMediumPointY);
            }
            counterBD = counterBD.add(minorTickSpaceBD);
            counter   = counterBD.doubleValue();
        }
    }

    private void resizeText() {
        double maxWidth = 0.405 * size;

        titleText.setText(gauge.getTitle());
        if (titleText.getText().length() > titleLength) {
            if (titleText.getLayoutBounds().getWidth() > maxWidth) { Helper.adjustTextSize(titleText, maxWidth, size * 0.062); }
        }
        titleText.setTranslateX((size - titleText.getLayoutBounds().getWidth()) * 0.5);

        maxWidth = centerKnob.getRadius() * 1.8;
        double fontSize = centerKnob.getRadius() * 0.65;
        valueText.setFont(Fonts.latoRegular(fontSize));
        if (valueText.getBoundsInLocal().getWidth() > maxWidth) { Helper.adjustTextSize(valueText, maxWidth, fontSize); }
        valueText.setTranslateX((size - valueText.getLayoutBounds().getWidth()) * 0.5);

        maxWidth = 0.28 * size;
        subTitleText.setText(gauge.getSubTitle());
        if (subTitleText.getText().length() > subTitleLength) {
            if (subTitleText.getLayoutBounds().getWidth() > maxWidth) { Helper.adjustTextSize(subTitleText, maxWidth, size * 0.047); }
        }
        subTitleText.setTranslateX((size - subTitleText.getLayoutBounds().getWidth()) * 0.5);

        unitText.setText(gauge.getUnit());
        if (unitText.getText().length() > unitLength) {
            if (unitText.getLayoutBounds().getWidth() > maxWidth) { Helper.adjustTextSize(unitText, maxWidth, size * 0.047); }
        }
        unitText.setTranslateX((size - unitText.getLayoutBounds().getWidth()) * 0.5);

        titleLength    = titleText.getText().length();
        subTitleLength = subTitleText.getText().length();
        unitLength     = unitText.getText().length();
    }

    private void placeTextVerticaly() {
        valueText.setTranslateY(size * 0.5);

        titleText.setTranslateY(size * 0.83);

        subTitleText.setTranslateY(size * 0.35);

        unitText.setTranslateY(size * 0.67);
    }

    @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 (size > 0) {
            pane.setMaxSize(size, size);
            pane.relocate((gauge.getWidth() - size) * 0.5, (gauge.getHeight() - size) * 0.5);

            background.setRadius(size * 0.5);
            background.setCenterX(size * 0.5);
            background.setCenterY(size * 0.5);

            mask.getElements().clear();
            mask.getElements().add(new MoveTo(0.23333333333333334 * size, 0.5 * size));
            mask.getElements().add(new CubicCurveTo(0.23333333333333334 * size, 0.3525 * size,
                                                    0.3525 * size, 0.23333333333333334 * size,
                                                    0.5 * size, 0.23333333333333334 * size));
            mask.getElements().add(new CubicCurveTo(0.6475 * size, 0.23333333333333334 * size,
                                                    0.7666666666666667 * size, 0.3525 * size,
                                                    0.7666666666666667 * size, 0.5 * size));
            mask.getElements().add(new CubicCurveTo(0.7666666666666667 * size, 0.6475 * size,
                                                    0.6475 * size, 0.7666666666666667 * size,
                                                    0.5 * size, 0.7666666666666667 * size));
            mask.getElements().add(new CubicCurveTo(0.3525 * size, 0.7666666666666667 * size,
                                                    0.23333333333333334 * size, 0.6475 * size,
                                                    0.23333333333333334 * size, 0.5 * size));
            mask.getElements().add(new ClosePath());
            mask.getElements().add(new MoveTo(0.2 * size, 0.5 * size));
            mask.getElements().add(new CubicCurveTo(0.2 * size, 0.555 * size,
                                                    0.215 * size, 0.6058333333333333 * size,
                                                    0.24 * size, 0.65 * size));
            mask.getElements().add(new CubicCurveTo(0.24 * size, 0.65 * size,
                                                    0.12583333333333332 * size, 0.7166666666666667 * size,
                                                    0.12583333333333332 * size, 0.7166666666666667 * size));
            mask.getElements().add(new CubicCurveTo(0.205 * size, 0.8541666666666666 * size,
                                                    0.3408333333333333 * size, 0.9333333333333333 * size,
                                                    0.5 * size, 0.9333333333333333 * size));
            mask.getElements().add(new CubicCurveTo(0.6591666666666667 * size, 0.9333333333333333 * size,
                                                    0.795 * size, 0.8541666666666666 * size,
                                                    0.8741666666666666 * size, 0.7166666666666667 * size));
            mask.getElements().add(new CubicCurveTo(0.8741666666666666 * size, 0.7166666666666667 * size,
                                                    0.76 * size, 0.65 * size,
                                                    0.76 * size, 0.65 * size));
            mask.getElements().add(new CubicCurveTo(0.785 * size, 0.6058333333333333 * size,
                                                    0.8 * size, 0.555 * size,
                                                    0.8 * size, 0.5 * size));
            mask.getElements().add(new CubicCurveTo(0.8 * size, 0.33416666666666667 * size,
                                                    0.6658333333333334 * size, 0.2 * size,
                                                    0.5 * size, 0.2 * size));
            mask.getElements().add(new CubicCurveTo(0.33416666666666667 * size, 0.2 * size,
                                                    0.2 * size, 0.33416666666666667 * size,
                                                    0.2 * size, 0.5 * size));
            mask.getElements().add(new ClosePath());

            dropShadow4.setOffsetY(0.014 * size);
            dropShadow4.setRadius(0.014 * size);

            needleMoveTo1.setX(0.5008333333333334 * size); needleMoveTo1.setY(0.0775 * size);

            needleCubicCurveTo2.setControlX1(0.5008333333333334 * size); needleCubicCurveTo2.setControlY1(0.0775 * size);
            needleCubicCurveTo2.setControlX2(0.5416666666666666 * size); needleCubicCurveTo2.setControlY2(0.0025 * size);
            needleCubicCurveTo2.setX(0.5416666666666666 * size); needleCubicCurveTo2.setY(0.0025 * size);

            needleCubicCurveTo3.setControlX1(0.5125 * size); needleCubicCurveTo3.setControlY1(0);
            needleCubicCurveTo3.setControlX2(0.4875 * size); needleCubicCurveTo3.setControlY2(0);
            needleCubicCurveTo3.setX(0.4583333333333333 * size); needleCubicCurveTo3.setY(0.0025 * size);

            needleCubicCurveTo4.setControlX1(0.4583333333333333 * size); needleCubicCurveTo4.setControlY1(0.0025 * size);
            needleCubicCurveTo4.setControlX2(0.49833333333333335 * size); needleCubicCurveTo4.setControlY2(0.0775 * size);
            needleCubicCurveTo4.setX(0.49833333333333335 * size); needleCubicCurveTo4.setY(0.0775 * size);

            needleLineTo5.setX(0.49833333333333335 * size); needleLineTo5.setY(0.0775 * size);

            needleLineTo6.setX(0.49833333333333335 * size); needleLineTo6.setY(0.17916666666666667 * size);

            needleLineTo7.setX(0.5008333333333334 * size); needleLineTo7.setY(0.17916666666666667 * size);

            needleLineTo8.setX(0.5008333333333334 * size); needleLineTo8.setY(0.0775 * size);

            needle.relocate(needle.getLayoutBounds().getMinX(), needle.getLayoutBounds().getMinY());
            needleRotate.setPivotX(size * 0.5);
            needleRotate.setPivotY(size * 0.5);

            mainCanvas.setCache(false);
            mainCanvas.setWidth(size);
            mainCanvas.setHeight(size);
            drawMainCanvas();
            mainCanvas.setCache(true);
            mainCanvas.setCacheHint(CacheHint.QUALITY);

            tickMarkCanvas.setWidth(size);
            tickMarkCanvas.setHeight(size);
            highlightValue(tickMarkCtx, gauge.getValue());

            centerKnob.setRadius(0.22916667 * size);
            centerKnob.setCenterX(0.5 * size);
            centerKnob.setCenterY(0.5 * size);
            centerKnob.setFill(new LinearGradient(0.5 * size, 0.2708333333333333 * size,
                                                  0.5 * size, 0.7291666666666666 * size,
                                                  false, CycleMethod.NO_CYCLE,
                                                  new Stop(0.0, Color.rgb(69,70,73)),
                                                  new Stop(1.0, Color.rgb(31,31,31))));
            centerKnob.setEffect(dropShadow4);

            glow1.setRadius(0.085 * size);
            glow2.setRadius(0.085 * size);
            bigGlow.setRadius(0.25 * size);

            titleText.setFont(Fonts.robotoCondensedRegular(size * 0.062));
            subTitleText.setFont(Fonts.robotoCondensedLight(size * 0.047));
            valueText.setFont(Fonts.latoRegular(size * 0.22));
            unitText.setFont(Fonts.robotoCondensedRegular(size * 0.047));

            resizeText();
            placeTextVerticaly();
        }
    }

    @Override protected void redraw() {
        sectionsVisible = gauge.getSectionsVisible();
        locale          = gauge.getLocale();
        barColor        = gauge.getBarColor();
        thresholdColor  = gauge.getThresholdColor();
        needle.setFill(gauge.getNeedleColor());
        titleText.setFill(gauge.getTitleColor());
        subTitleText.setFill(gauge.getSubTitleColor());
        unitText.setFill(gauge.getUnitColor());
        valueText.setFill(gauge.getValueColor());
        buttonTooltip.setText(gauge.getButtonTooltipText());

        mainCanvas.setCache(false);
        mainCanvas.setWidth(size);
        mainCanvas.setHeight(size);
        drawMainCanvas();
        mainCanvas.setCache(true);
        mainCanvas.setCacheHint(CacheHint.QUALITY);
        resizeText();
    }
}