/*
 * Copyright (c) 2013 by 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
 *
 *     http://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.enzo.gauge.skin;

import eu.hansolo.enzo.common.ConicalGradient;
import eu.hansolo.enzo.common.Marker;
import eu.hansolo.enzo.common.Section;
import eu.hansolo.enzo.common.ValueEvent;
import eu.hansolo.enzo.gauge.RadialBargraph;
import javafx.animation.FadeTransition;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.ParallelTransition;
import javafx.animation.PauseTransition;
import javafx.animation.SequentialTransition;
import javafx.animation.Timeline;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.collections.ListChangeListener;
import javafx.collections.MapChangeListener;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.geometry.Point2D;
import javafx.geometry.VPos;
import javafx.scene.CacheHint;
import javafx.scene.Node;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.effect.Blend;
import javafx.scene.effect.BlendMode;
import javafx.scene.effect.BlurType;
import javafx.scene.effect.DropShadow;
import javafx.scene.effect.InnerShadow;
import javafx.scene.image.Image;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.TouchEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.ImagePattern;
import javafx.scene.paint.RadialGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Arc;
import javafx.scene.shape.ArcType;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.shape.StrokeType;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.scene.transform.Rotate;
import javafx.util.Duration;

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


/**
 * Created by
 * User: hansolo
 * Date: 17.07.13
 * Time: 08:02
 */
public class RadialBargraphSkin extends SkinBase<RadialBargraph> implements Skin<RadialBargraph> {
    private static final double      PREFERRED_WIDTH  = 200;
    private static final double      PREFERRED_HEIGHT = 200;
    private static final double      MINIMUM_WIDTH    = 50;
    private static final double      MINIMUM_HEIGHT   = 50;
    private static final double      MAXIMUM_WIDTH    = 1024;
    private static final double      MAXIMUM_HEIGHT   = 1024;
    private double                   size;
    private double                   centerX;
    private double                   centerY;
    private Pane                     pane;
    private Region                   background;
    private Canvas                   ticksAndSectionsCanvas;
    private GraphicsContext          ticksAndSections;
    private Region                   threshold;
    private Rotate                   thresholdRotate;
    private boolean                  thresholdExceeded;
    private Region                   minMeasuredValue;
    private Rotate                   minMeasuredValueRotate;
    private Region                   maxMeasuredValue;
    private Rotate                   maxMeasuredValueRotate;
    private DoubleProperty           angle;
    private Arc                      bar;
    private Region                   knob;
    private DropShadow               dropShadow;
    private Text                     title;
    private Text                     unit;
    private Text                     value;
    private DropShadow               valueBlendBottomShadow;
    private InnerShadow              valueBlendTopShadow;
    private Blend                    blend;
    private double                   angleStep;
    private Timeline                 timeline;
    private double                   interactiveAngle;
    private EventHandler<MouseEvent> mouseEventHandler;
    private EventHandler<TouchEvent> touchEventHandler;
    private List<Node>               markersToRemove;
    private Color                    barColor;
    private ConicalGradient          barGradient;


    // ******************** Constructors **************************************
    public RadialBargraphSkin(RadialBargraph radialBargraph) {
        super(radialBargraph);
        angleStep         = radialBargraph.getAngleRange() / (radialBargraph.getMaxValue() - radialBargraph.getMinValue());
        angle             = new SimpleDoubleProperty(this, "angle", getSkinnable().getValue() * angleStep);
        timeline          = new Timeline();
        mouseEventHandler = mouseEvent -> handleMouseEvent(mouseEvent);
        touchEventHandler = touchEvent -> handleTouchEvent(touchEvent);
        markersToRemove   = new ArrayList<>();

        init();
        initGraphics();
        registerListeners();
    }


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

        if (Double.compare(getSkinnable().getMinWidth(), 0.0) <= 0 || Double.compare(getSkinnable().getMinHeight(), 0.0) <= 0) {
            getSkinnable().setMinSize(MINIMUM_WIDTH, MINIMUM_HEIGHT);
        }

        if (Double.compare(getSkinnable().getMaxWidth(), 0.0) <= 0 || Double.compare(getSkinnable().getMaxHeight(), 0.0) <= 0) {
            getSkinnable().setMaxSize(MAXIMUM_WIDTH, MAXIMUM_HEIGHT);
        }
    }

    private void initGraphics() {
        Font.loadFont(getClass().getResourceAsStream("/eu/hansolo/enzo/fonts/opensans-semibold.ttf"), (0.06 * PREFERRED_HEIGHT)); // "OpenSans"

        barColor    = getSkinnable().getBarColor();
        barGradient = new ConicalGradient(new Stop(0.0, Color.TRANSPARENT),
                                          new Stop(1.0, Color.TRANSPARENT));

        valueBlendBottomShadow = new DropShadow();
        valueBlendBottomShadow.setBlurType(BlurType.TWO_PASS_BOX);
        valueBlendBottomShadow.setColor(Color.rgb(255, 255, 255, 0.5));
        valueBlendBottomShadow.setOffsetX(0);
        valueBlendBottomShadow.setOffsetY(0.005 * PREFERRED_WIDTH);
        valueBlendBottomShadow.setRadius(0);

        valueBlendTopShadow = new InnerShadow();
        valueBlendTopShadow.setBlurType(BlurType.TWO_PASS_BOX);
        valueBlendTopShadow.setColor(Color.rgb(0, 0, 0, 0.7));
        valueBlendTopShadow.setOffsetX(0);
        valueBlendTopShadow.setOffsetY(0.005 * PREFERRED_WIDTH);
        valueBlendTopShadow.setRadius(0.005 * PREFERRED_WIDTH);

        blend = new Blend();
        blend.setMode(BlendMode.MULTIPLY);
        blend.setBottomInput(valueBlendBottomShadow);
        blend.setTopInput(valueBlendTopShadow);

        background = new Region();
        background.getStyleClass().setAll("background");

        ticksAndSectionsCanvas = new Canvas(PREFERRED_WIDTH, PREFERRED_HEIGHT);
        ticksAndSections = ticksAndSectionsCanvas.getGraphicsContext2D();

        minMeasuredValue = new Region();
        minMeasuredValue.getStyleClass().setAll("min-measured-value");
        minMeasuredValueRotate = new Rotate(180 - getSkinnable().getStartAngle());
        minMeasuredValue.getTransforms().setAll(minMeasuredValueRotate);
        minMeasuredValue.setOpacity(getSkinnable().isMinMeasuredValueVisible() ? 1 : 0);
        minMeasuredValue.setManaged(getSkinnable().isMinMeasuredValueVisible());

        maxMeasuredValue = new Region();
        maxMeasuredValue.getStyleClass().setAll("max-measured-value");
        maxMeasuredValueRotate = new Rotate(180 - getSkinnable().getStartAngle());
        maxMeasuredValue.getTransforms().setAll(maxMeasuredValueRotate);
        maxMeasuredValue.setOpacity(getSkinnable().isMaxMeasuredValueVisible() ? 1 : 0);
        maxMeasuredValue.setManaged(getSkinnable().isMaxMeasuredValueVisible());

        threshold = new Region();
        threshold.getStyleClass().setAll("threshold");
        thresholdRotate = new Rotate(180 - getSkinnable().getStartAngle());
        threshold.getTransforms().setAll(thresholdRotate);
        threshold.setOpacity(getSkinnable().isThresholdVisible() ? 1 : 0);
        threshold.setManaged(getSkinnable().isThresholdVisible());
        thresholdExceeded = false;

        bar = new Arc();
        bar.setType(ArcType.ROUND);
        bar.setCenterX(PREFERRED_WIDTH * 0.5);
        bar.setCenterY(PREFERRED_HEIGHT * 0.5);
        bar.setRadiusX(PREFERRED_WIDTH * 0.5 - 4);
        bar.setRadiusY(PREFERRED_HEIGHT * 0.5 - 4);
        bar.setStartAngle(getSkinnable().getStartAngle() - 90);
        bar.setLength(0);
        bar.setStrokeType(StrokeType.CENTERED);
        bar.setStroke(null);
        bar.setFill(new RadialGradient(0, 0,
                                       PREFERRED_WIDTH * 0.5, PREFERRED_HEIGHT * 0.5,
                                       PREFERRED_WIDTH * 0.45, false, CycleMethod.NO_CYCLE,
                                       new Stop(0.0, barColor),
                                       new Stop(0.76, barColor.deriveColor(-5, 1, 1, 1)), // -5 for on the barColorHue)
                                       new Stop(0.79, barColor),
                                       new Stop(0.97, barColor),
                                       new Stop(1.0, barColor.deriveColor(-5, 1, 1, 1)))); // -5 for on the barColorHue)

        knob = new Region();
        knob.setPickOnBounds(false);
        knob.getStyleClass().setAll("knob");

        dropShadow = new DropShadow();
        dropShadow.setColor(Color.rgb(0, 0, 0, 0.25));
        dropShadow.setBlurType(BlurType.TWO_PASS_BOX);
        dropShadow.setRadius(0.015 * PREFERRED_WIDTH);
        dropShadow.setOffsetY(0.015 * PREFERRED_WIDTH);

        title = new Text(getSkinnable().getTitle());
        title.setMouseTransparent(true);
        title.setTextOrigin(VPos.CENTER);
        title.getStyleClass().setAll("title");
        title.setEffect(getSkinnable().isPlainValue() ? null : blend);

        unit = new Text(getSkinnable().getUnit());
        unit.setMouseTransparent(true);
        unit.setTextOrigin(VPos.CENTER);
        unit.getStyleClass().setAll("unit");
        unit.setEffect(getSkinnable().isPlainValue() ? null : blend);

        value = new Text();
        value.setText(String.format(Locale.US, "%." + getSkinnable().getDecimals() + "f", 0.0));
        value.setMouseTransparent(true);
        value.setTextOrigin(VPos.CENTER);
        value.getStyleClass().setAll("value");
        value.setEffect(getSkinnable().isPlainValue() ? null : blend);


        // Add all nodes
        pane = new Pane();
        pane.getChildren().setAll(background,
                                  bar,
                                  ticksAndSectionsCanvas,
                                  minMeasuredValue,
                                  maxMeasuredValue,
                                  threshold,
                                  knob,
                                  title,
                                  unit,
                                  value);

        pane.getChildren().addAll(getSkinnable().getMarkers().keySet());

        getChildren().setAll(pane);
    }

    private void registerListeners() {
        getSkinnable().widthProperty().addListener(observable -> handleControlPropertyChanged("RESIZE"));
        getSkinnable().heightProperty().addListener(observable -> handleControlPropertyChanged("RESIZE"));
        getSkinnable().valueProperty().addListener(observable -> handleControlPropertyChanged("VALUE"));
        getSkinnable().minValueProperty().addListener(observable -> handleControlPropertyChanged("RECALC"));
        getSkinnable().maxValueProperty().addListener(observable -> handleControlPropertyChanged("RECALC"));
        getSkinnable().minMeasuredValueProperty().addListener(observable -> handleControlPropertyChanged("MIN_MEASURED_VALUE"));
        getSkinnable().minMeasuredValueVisibleProperty().addListener(observable -> handleControlPropertyChanged("MIN_MEASURED_VALUE_VISIBLE"));
        getSkinnable().maxMeasuredValueProperty().addListener(observable -> handleControlPropertyChanged("MAX_MEASURED_VALUE"));
        getSkinnable().maxMeasuredValueVisibleProperty().addListener(observable -> handleControlPropertyChanged("MAX_MEASURED_VALUE_VISIBLE"));
        getSkinnable().barColorProperty().addListener(observable -> handleControlPropertyChanged("BAR_COLOR"));
        getSkinnable().animatedProperty().addListener(observable -> handleControlPropertyChanged("ANIMATED"));
        getSkinnable().thresholdProperty().addListener(observable -> handleControlPropertyChanged("THRESHOLD"));
        getSkinnable().thresholdVisibleProperty().addListener(observable -> handleControlPropertyChanged("THRESHOLD_VISIBLE"));
        getSkinnable().angleRangeProperty().addListener(observable -> handleControlPropertyChanged("ANGLE_RANGE"));
        getSkinnable().numberFormatProperty().addListener(observable -> handleControlPropertyChanged("RECALC"));
        getSkinnable().plainValueProperty().addListener(observable -> handleControlPropertyChanged("PLAIN_VALUE"));
        getSkinnable().interactiveProperty().addListener(observable -> handleControlPropertyChanged("INTERACTIVE"));
        getSkinnable().getSections().addListener((ListChangeListener<Section>) change -> handleControlPropertyChanged("CANVAS_REFRESH"));
        getSkinnable().getMarkers().addListener((MapChangeListener<Marker, Rotate>) change -> handleControlPropertyChanged("MARKER"));
        getSkinnable().barGradientProperty().addListener((ListChangeListener<Stop>) change -> handleControlPropertyChanged("BAR_GRADIENT"));
        getSkinnable().barGradientEnabledProperty().addListener(observable -> handleControlPropertyChanged("BAR_COLOR"));

        angle.addListener(observable -> handleControlPropertyChanged("ANGLE"));

        knob.setOnMousePressed(event -> getSkinnable().setInteractive(!getSkinnable().isInteractive()));

        minMeasuredValue.setOnMousePressed(mouseEventHandler);
        minMeasuredValue.setOnMouseReleased(mouseEventHandler);

        minMeasuredValue.setOnTouchPressed(touchEventHandler);
        minMeasuredValue.setOnTouchReleased(touchEventHandler);

        maxMeasuredValue.setOnMousePressed(mouseEventHandler);
        maxMeasuredValue.setOnMouseReleased(mouseEventHandler);

        maxMeasuredValue.setOnTouchPressed(touchEventHandler);
        maxMeasuredValue.setOnTouchReleased(touchEventHandler);

        threshold.setOnMousePressed(mouseEventHandler);
        threshold.setOnMouseDragged(mouseEventHandler);
        threshold.setOnMouseReleased(mouseEventHandler);

        threshold.setOnTouchPressed(touchEventHandler);
        threshold.setOnTouchMoved(touchEventHandler);
        threshold.setOnTouchReleased(touchEventHandler);

        for (Marker marker : getSkinnable().getMarkers().keySet()) {
            marker.setOnMousePressed(mouseEventHandler);
            marker.setOnMouseDragged(mouseEventHandler);
            marker.setOnMouseReleased(mouseEventHandler);

            marker.setOnTouchPressed(touchEventHandler);
            marker.setOnTouchMoved(touchEventHandler);
            marker.setOnTouchReleased(touchEventHandler);
        }
    }


    // ******************** Methods *******************************************
    protected void handleControlPropertyChanged(final String PROPERTY) {
        if ("RESIZE".equals(PROPERTY)) {
            resize();
        } else if ("VALUE".equals(PROPERTY)) {
            setBar();
        } else if ("RECALC".equals(PROPERTY)) {
            angleStep = getSkinnable().getAngleRange() / (getSkinnable().getMaxValue() - getSkinnable().getMinValue());
            resize();
        } else if ("ANGLE".equals(PROPERTY)) {
            if (getSkinnable().isInteractive()) return;

            double currentValue = angle.get() / angleStep;

            value.setText(String.format(Locale.US, "%." + getSkinnable().getDecimals() + "f", currentValue));
            value.setTranslateX((size - value.getLayoutBounds().getWidth()) * 0.5);
            bar.setLength(-currentValue * angleStep);

            // Check threshold
            if (thresholdExceeded) {
                if (currentValue < getSkinnable().getThreshold()) {
                    getSkinnable().fireEvent(new ValueEvent(this, null, ValueEvent.VALUE_UNDERRUN));
                    thresholdExceeded = false;
                }
            } else {
                if (currentValue > getSkinnable().getThreshold()) {
                    getSkinnable().fireEvent(new ValueEvent(this, null, ValueEvent.VALUE_EXCEEDED));
                    thresholdExceeded = true;
                }
            }
            // Check each marker
            for (Marker marker : getSkinnable().getMarkers().keySet()) {
                if (marker.isExceeded()) {
                    if (currentValue < marker.getValue()) {
                        marker.fireMarkerEvent(new Marker.MarkerEvent(this, null, Marker.MarkerEvent.MARKER_UNDERRUN));
                        marker.setExceeded(false);
                    }
                } else {
                    if (currentValue > marker.getValue()) {
                        marker.fireMarkerEvent(new Marker.MarkerEvent(this, null, Marker.MarkerEvent.MARKER_EXCEEDED));
                        marker.setExceeded(true);
                    }
                }
            }
            // Check min- and maxMeasuredValue
            if (currentValue < getSkinnable().getMinMeasuredValue()) {
                getSkinnable().setMinMeasuredValue(currentValue);
                minMeasuredValueRotate.setAngle(currentValue * angleStep - 180 - getSkinnable().getStartAngle());
            }
            if (currentValue > getSkinnable().getMaxMeasuredValue()) {
                getSkinnable().setMaxMeasuredValue(currentValue);
                maxMeasuredValueRotate.setAngle(currentValue * angleStep - 180 - getSkinnable().getStartAngle());
            }
        } else if ("PLAIN_VALUE".equals(PROPERTY)) {
            value.setEffect(getSkinnable().isPlainValue() ? null : blend);
        } else if ("INTERACTIVE".equals(PROPERTY)) {
            if (getSkinnable().isInteractive()) {
                unit.setText("Interactive");
                value.setText("");
                resizeText();
            } else {
                unit.setText(getSkinnable().getUnit());
                value.setText(String.format(Locale.US, "%." + getSkinnable().getDecimals() + "f", (angle.get() / angleStep)));
                resizeText();
            }
        } else if ("CANVAS_REFRESH".equals(PROPERTY)) {
            ticksAndSections.clearRect(0, 0, size, size);
            drawSections(ticksAndSections);
            drawTickMarks(ticksAndSections);
        } else if ("THRESHOLD".equals(PROPERTY)) {
            thresholdRotate.setAngle(getSkinnable().getThreshold() * angleStep - 180 - getSkinnable().getStartAngle());
        } else if ("THRESHOLD_VISIBLE".equals(PROPERTY)) {
            threshold.setOpacity(getSkinnable().isThresholdVisible() ? 1 : 0);
            threshold.setManaged(getSkinnable().isThresholdVisible());
        } else if ("MIN_MEASURED_VALUE_VISIBLE".equals(PROPERTY)) {
            minMeasuredValue.setOpacity(getSkinnable().isMinMeasuredValueVisible() ? 1 : 0);
            minMeasuredValue.setManaged(getSkinnable().isMinMeasuredValueVisible());
        } else if ("MAX_MEASURED_VALUE_VISIBLE".equals(PROPERTY)) {
            maxMeasuredValue.setOpacity(getSkinnable().isMaxMeasuredValueVisible() ? 1 : 0);
            maxMeasuredValue.setManaged(getSkinnable().isMaxMeasuredValueVisible());
        } else if ("MARKER".equals(PROPERTY)) {
            checkForRemovedMarkers();
            for (Marker marker : getSkinnable().getMarkers().keySet()) {
                if (pane.getChildren().contains(marker)) continue;
                pane.getChildren().add(marker);
                // Add MouseEvent handler
                marker.setOnMousePressed(mouseEventHandler);
                marker.setOnMouseDragged(mouseEventHandler);
                marker.setOnMouseReleased(mouseEventHandler);
                // Add TouchEvent handler
                marker.setOnTouchPressed(touchEventHandler);
                marker.setOnTouchMoved(touchEventHandler);
                marker.setOnTouchReleased(touchEventHandler);
            }
            drawMarkers();
        } else if ("BAR_COLOR".equals(PROPERTY)) {
            barColor = getSkinnable().getBarColor();
            resize();
        }
    }

    @Override protected double computeMinWidth(final double HEIGHT, double TOP_INSET, double RIGHT_INSET, double BOTTOM_INSET, double LEFT_INSET) {
        return super.computeMinWidth(Math.max(MINIMUM_HEIGHT, HEIGHT - TOP_INSET - BOTTOM_INSET), TOP_INSET, RIGHT_INSET, BOTTOM_INSET, LEFT_INSET);
    }
    @Override protected double computeMinHeight(final double WIDTH, double TOP_INSET, double RIGHT_INSET, double BOTTOM_INSET, double LEFT_INSET) {
        return super.computeMinHeight(Math.max(MINIMUM_WIDTH, WIDTH - LEFT_INSET - RIGHT_INSET), TOP_INSET, RIGHT_INSET, BOTTOM_INSET, LEFT_INSET);
    }

    @Override protected double computeMaxWidth(final double HEIGHT, double TOP_INSET, double RIGHT_INSET, double BOTTOM_INSET, double LEFT_INSET) {
        return super.computeMaxWidth(Math.min(MAXIMUM_HEIGHT, HEIGHT - TOP_INSET - BOTTOM_INSET), TOP_INSET, RIGHT_INSET, BOTTOM_INSET, LEFT_INSET);
    }
    @Override protected double computeMaxHeight(final double WIDTH, double TOP_INSET, double RIGHT_INSET, double BOTTOM_INSET, double LEFT_INSET) {
        return super.computeMaxHeight(Math.min(MAXIMUM_WIDTH, WIDTH - LEFT_INSET - RIGHT_INSET), TOP_INSET, RIGHT_INSET, BOTTOM_INSET, LEFT_INSET);
    }

    @Override protected double computePrefWidth(final double HEIGHT, double TOP_INSET, double RIGHT_INSET, double BOTTOM_INSET, double LEFT_INSET) {
        double prefHeight = PREFERRED_HEIGHT;
        if (HEIGHT != -1) {
            prefHeight = Math.max(0, HEIGHT - TOP_INSET - BOTTOM_INSET);
        }
        return super.computePrefWidth(prefHeight, TOP_INSET, RIGHT_INSET, BOTTOM_INSET, LEFT_INSET);
    }
    @Override protected double computePrefHeight(final double WIDTH, double TOP_INSET, double RIGHT_INSET, double BOTTOM_INSET, double LEFT_INSET) {
        double prefWidth = PREFERRED_WIDTH;
        if (WIDTH != -1) {
            prefWidth = Math.max(0, WIDTH - LEFT_INSET - RIGHT_INSET);
        }
        return super.computePrefHeight(prefWidth, TOP_INSET, RIGHT_INSET, BOTTOM_INSET, LEFT_INSET);
    }


    // ******************** Private Methods ***********************************
    private void checkForRemovedMarkers() {
        markersToRemove.clear();
        for (Node node : pane.getChildren()) {
            if (node instanceof Marker) {
                if (getSkinnable().getMarkers().keySet().contains(node)) continue;
                node.setManaged(false);
                node.removeEventHandler(MouseEvent.MOUSE_PRESSED, mouseEventHandler);
                node.removeEventHandler(MouseEvent.MOUSE_DRAGGED, mouseEventHandler);
                node.removeEventHandler(MouseEvent.MOUSE_RELEASED, mouseEventHandler);
                node.removeEventHandler(TouchEvent.TOUCH_PRESSED, touchEventHandler);
                node.removeEventHandler(TouchEvent.TOUCH_MOVED, touchEventHandler);
                node.removeEventHandler(TouchEvent.TOUCH_RELEASED, touchEventHandler);
                markersToRemove.add(node);
            }
        }
        for (Node node : markersToRemove) pane.getChildren().remove(node);
    }

    private void handleMouseEvent(final MouseEvent MOUSE_EVENT) {
        final Object    SRC  = MOUSE_EVENT.getSource();
        final EventType TYPE = MOUSE_EVENT.getEventType();
        if (getSkinnable().isInteractive() && SRC.equals(threshold)) {
            if (MouseEvent.MOUSE_PRESSED == TYPE) {
                unit.setText("Threshold");
                value.setText(String.format(Locale.US, "%." + getSkinnable().getDecimals() + "f", getSkinnable().getThreshold()));
                resizeText();
            } else if (MouseEvent.MOUSE_DRAGGED == TYPE) {
                touchRotate(MOUSE_EVENT.getSceneX() - getSkinnable().getLayoutX(), MOUSE_EVENT.getSceneY() - getSkinnable().getLayoutY(), thresholdRotate);
            } else if (MouseEvent.MOUSE_RELEASED == TYPE) {
                getSkinnable().setThreshold(Double.parseDouble(value.getText()));
                fadeBackToInteractive();
            }
        } else if (getSkinnable().isInteractive() && SRC instanceof Marker) {
            if (MouseEvent.MOUSE_PRESSED == TYPE) {
                unit.setText(((Marker) SRC).getText());
                value.setText(String.format(Locale.US, "%." + getSkinnable().getDecimals() + "f", ((Marker) SRC).getValue()));
                resizeText();
            } else if (MouseEvent.MOUSE_DRAGGED == TYPE) {
                touchRotate(MOUSE_EVENT.getSceneX() - getSkinnable().getLayoutX(), MOUSE_EVENT.getSceneY() - getSkinnable().getLayoutY(), getSkinnable().getMarkers().get(SRC));
            } else if (MouseEvent.MOUSE_RELEASED == TYPE) {
                ((Marker) SRC).setValue(Double.parseDouble(value.getText()));
                fadeBackToInteractive();
            }
        } else if (getSkinnable().isInteractive() && SRC.equals(minMeasuredValue)) {
            if (MouseEvent.MOUSE_PRESSED == TYPE) {
                unit.setText("Min");
                value.setText(String.format(Locale.US, "%." + getSkinnable().getDecimals() + "f", getSkinnable().getMinMeasuredValue()));
                resizeText();
            } else if (MouseEvent.MOUSE_RELEASED == TYPE) {
                fadeBackToInteractive();
            }
        } else if (getSkinnable().isInteractive() && SRC.equals(maxMeasuredValue)) {
            if (MouseEvent.MOUSE_PRESSED == TYPE) {
                unit.setText("Max");
                value.setText(String.format(Locale.US, "%." + getSkinnable().getDecimals() + "f", getSkinnable().getMaxMeasuredValue()));
                resizeText();
            } else if (MouseEvent.MOUSE_RELEASED == TYPE) {
                fadeBackToInteractive();
            }
        }
    }

    private void handleTouchEvent(final TouchEvent TOUCH_EVENT) {
        final Object    SRC  = TOUCH_EVENT.getSource();
        final EventType TYPE = TOUCH_EVENT.getEventType();
        if (SRC.equals(threshold)) {
            if (TouchEvent.TOUCH_PRESSED == TYPE) {
                unit.setText("Threshold");
                value.setText(String.format(Locale.US, "%." + getSkinnable().getDecimals() + "f", getSkinnable().getThreshold()));
                resizeText();
            } else if (TouchEvent.TOUCH_MOVED == TYPE) {
                touchRotate(TOUCH_EVENT.getTouchPoint().getSceneX() - getSkinnable().getLayoutX(), TOUCH_EVENT.getTouchPoint().getSceneY() - getSkinnable().getLayoutY(), thresholdRotate);
            } else if (TouchEvent.TOUCH_RELEASED == TYPE) {
                getSkinnable().setThreshold(Double.parseDouble(value.getText()));
                fadeBackToInteractive();
            }
        } else if (SRC instanceof Marker) {
            if (TouchEvent.TOUCH_PRESSED == TYPE) {
                unit.setText(((Marker) SRC).getText());
                value.setText(String.format(Locale.US, "%." + getSkinnable().getDecimals() + "f", ((Marker) SRC).getValue()));
                resizeText();
            } else if (TouchEvent.TOUCH_MOVED == TYPE) {
                touchRotate(TOUCH_EVENT.getTouchPoint().getSceneX() - getSkinnable().getLayoutX(), TOUCH_EVENT.getTouchPoint().getSceneY() - getSkinnable().getLayoutY(), getSkinnable().getMarkers().get(SRC));
            } else if (TouchEvent.TOUCH_RELEASED == TYPE) {
                ((Marker) SRC).setValue(Double.parseDouble(value.getText()));
                fadeBackToInteractive();
            }
        } else if (SRC.equals(minMeasuredValue)) {
            if (TouchEvent.TOUCH_PRESSED == TYPE) {
                unit.setText("Min");
                value.setText(String.format(Locale.US, "%." + getSkinnable().getDecimals() + "f", getSkinnable().getMinMeasuredValue()));
                resizeText();
            } else if (TouchEvent.TOUCH_RELEASED == TYPE) {
                fadeBackToInteractive();
            }
        } else if (SRC.equals(maxMeasuredValue)) {
            if (TouchEvent.TOUCH_PRESSED == TYPE) {
                unit.setText("Max");
                value.setText(String.format(Locale.US, "%." + getSkinnable().getDecimals() + "f", getSkinnable().getMaxMeasuredValue()));
                resizeText();
            } else if (TouchEvent.TOUCH_RELEASED == TYPE) {
                fadeBackToInteractive();
            }
        }
    }

    private double getTheta(double x, double y) {
        double deltaX = x - centerX;
        double deltaY = y - centerY;
        double radius = Math.sqrt((deltaX * deltaX) + (deltaY * deltaY));
        double nx     = deltaX / radius;
        double ny     = deltaY / radius;
        double theta  = Math.atan2(ny, nx);
        return Double.compare(theta, 0.0) >= 0 ? Math.toDegrees(theta) : Math.toDegrees((theta)) + 360.0;
    }

    private void touchRotate(final double X, final double Y, final Rotate ROTATE) {
        double theta     = getTheta(X, Y);
        interactiveAngle = (theta + 90) % 360;
        double newValue  = Double.compare(interactiveAngle, 180) <= 0 ?
            (interactiveAngle + 180.0 + getSkinnable().getStartAngle() - 360) / angleStep :
            (interactiveAngle - 180.0 + getSkinnable().getStartAngle() - 360) / angleStep;
        if (Double.compare(newValue, getSkinnable().getMinValue()) >= 0 && Double.compare(newValue, getSkinnable().getMaxValue()) <= 0) {
            ROTATE.setAngle(interactiveAngle);
            value.setText(String.format(Locale.US, "%." + getSkinnable().getDecimals() + "f", newValue));
            resizeText();
        }

    }

    private void fadeBackToInteractive() {
        FadeTransition fadeUnitOut = new FadeTransition(Duration.millis(425), unit);
        fadeUnitOut.setFromValue(1.0);
        fadeUnitOut.setToValue(0.0);
        FadeTransition fadeValueOut = new FadeTransition(Duration.millis(425), value);
        fadeValueOut.setFromValue(1.0);
        fadeValueOut.setToValue(0.0);

        PauseTransition pause = new PauseTransition(Duration.millis(50));

        FadeTransition fadeUnitIn = new FadeTransition(Duration.millis(425), unit);
        fadeUnitIn.setFromValue(0.0);
        fadeUnitIn.setToValue(1.0);
        FadeTransition fadeValueIn = new FadeTransition(Duration.millis(425), value);
        fadeValueIn.setFromValue(0.0);
        fadeValueIn.setToValue(1.0);
        ParallelTransition parallelIn = new ParallelTransition(fadeUnitIn, fadeValueIn);

        ParallelTransition parallelOut = new ParallelTransition(fadeUnitOut, fadeValueOut);
        parallelOut.setOnFinished(event -> {
            unit.setText("Interactive");
            value.setText("");
            resizeText();
        });

        SequentialTransition sequence = new SequentialTransition(parallelOut, pause, parallelIn);
        sequence.play();
    }

    private void setBar() {
        double range       = (getSkinnable().getMaxValue() - getSkinnable().getMinValue());
        double angleRange  = getSkinnable().getAngleRange();
        angleStep          = angleRange / range;
        double targetAngle = getSkinnable().getValue() * angleStep;

        if (getSkinnable().isAnimated()) {
            timeline.stop();
            final KeyValue KEY_VALUE = new KeyValue(angle, targetAngle, Interpolator.SPLINE(0.5, 0.4, 0.4, 1.0));
            final KeyFrame KEY_FRAME = new KeyFrame(Duration.millis(getSkinnable().getAnimationDuration()), KEY_VALUE);
            timeline.getKeyFrames().setAll(KEY_FRAME);
            timeline.play();
        } else {
            angle.set(targetAngle);
        }
    }

    private void drawTickMarks(final GraphicsContext CTX) {
        double  sinValue;
        double  cosValue;
        double  startAngle = getSkinnable().getStartAngle();
        Point2D center     = new Point2D(size * 0.5, size * 0.5);
        for (double angle = 0, counter = getSkinnable().getMinValue() ; Double.compare(counter, getSkinnable().getMaxValue()) <= 0 ; angle -= angleStep, counter++) {
            sinValue = Math.sin(Math.toRadians(angle + startAngle));
            cosValue = Math.cos(Math.toRadians(angle + startAngle));

            Point2D innerPoint = new Point2D(center.getX() + size * 0.388 * sinValue, center.getY() + size * 0.388 * cosValue);
            Point2D outerPoint = new Point2D(center.getX() + size * 0.485 * sinValue, center.getY() + size * 0.485 * cosValue);

            CTX.setStroke(getSkinnable().getTickMarkFill());
            if (counter % getSkinnable().getMinorTickSpace() == 0) {
                CTX.setLineWidth(size * 0.0035);
                CTX.strokeLine(innerPoint.getX(), innerPoint.getY(), outerPoint.getX(), outerPoint.getY());
            }
        }
    }

    private final void drawSections(final GraphicsContext CTX) {
        final double xy        = (size - 0.87 * size) * 0.5;
        final double wh        = size * 0.87;
        final double MIN_VALUE = getSkinnable().getMinValue();
        final double OFFSET = 90 - getSkinnable().getStartAngle();
        for (int i = 0 ; i < getSkinnable().getSections().size() ; i++) {
            final Section SECTION      = getSkinnable().getSections().get(i);
            final double  ANGLE_START  = (SECTION.getStart() - MIN_VALUE) * angleStep;
            final double  ANGLE_EXTEND = (SECTION.getStop() - SECTION.getStart()) * angleStep;
            CTX.save();
            switch(i) {
                case 0: CTX.setStroke(getSkinnable().getSectionFill0()); break;
                case 1: CTX.setStroke(getSkinnable().getSectionFill1()); break;
                case 2: CTX.setStroke(getSkinnable().getSectionFill2()); break;
                case 3: CTX.setStroke(getSkinnable().getSectionFill3()); break;
                case 4: CTX.setStroke(getSkinnable().getSectionFill4()); break;
                case 5: CTX.setStroke(getSkinnable().getSectionFill5()); break;
                case 6: CTX.setStroke(getSkinnable().getSectionFill6()); break;
                case 7: CTX.setStroke(getSkinnable().getSectionFill7()); break;
                case 8: CTX.setStroke(getSkinnable().getSectionFill8()); break;
                case 9: CTX.setStroke(getSkinnable().getSectionFill9()); break;
            }
            CTX.setLineWidth(size * 0.1);
            CTX.setLineCap(StrokeLineCap.BUTT);
            CTX.strokeArc(xy, xy, wh, wh, -(OFFSET + ANGLE_START), -ANGLE_EXTEND, ArcType.OPEN);
            CTX.restore();
        }
    }

    private final void drawMarkers() {
        for (Marker marker : getSkinnable().getMarkers().keySet()) {
            marker.setPrefSize(0.05 * size, 0.05 * size);
            marker.relocate((size - marker.getPrefWidth()) * 0.5, size * 0.04);
            getSkinnable().getMarkers().get(marker).setPivotX(marker.getPrefWidth() * 0.5);
            getSkinnable().getMarkers().get(marker).setPivotY(size * 0.46);
            getSkinnable().getMarkers().get(marker).setAngle(marker.getValue() * angleStep - 180 - getSkinnable().getStartAngle());
        }
    }

    private void recalculateBarGradient() {
        double angleFactor = 1d / 360d;
        double emptyRange  = 360d - getSkinnable().getAngleRange();
        double offset      = angleFactor * ((360 - getSkinnable().getStartAngle() + 180 - emptyRange * 0.5) % 360);
        List<Stop> stops   = new LinkedList<>();

        double emptyOffset = (emptyRange * 0.5) * angleFactor;

        double minFraction = 1.0;
        double maxFraction = 0.0;
        Color  minFractionColor = Color.TRANSPARENT;
        Color  maxFractionColor = Color.TRANSPARENT;

        for (Stop stop : getSkinnable().getBarGradient()) {
            double fraction = stop.getOffset();
            if (fraction < minFraction) {
                minFraction      = fraction;
                minFractionColor = stop.getColor();
            }
            if (fraction > maxFraction) {
                maxFraction      = fraction;
                maxFractionColor = stop.getColor();
            }
        }
        stops.add(new Stop(0d, minFractionColor));
        stops.add(new Stop(0d + emptyOffset, minFractionColor));
        stops.add(new Stop(1d - emptyOffset, maxFractionColor));
        stops.add(new Stop(1d, maxFractionColor));
        if (getSkinnable().getBarGradient().size() == 2) {
            stops.add(new Stop((maxFraction - minFraction) * 0.5, (Color) Interpolator.LINEAR.interpolate(minFractionColor, maxFractionColor, 0.5)));
        }
        for (Stop stop : getSkinnable().getBarGradient()) {
            if (Double.compare(stop.getOffset(), minFraction) == 0 || Double.compare(stop.getOffset(), maxFraction) == 0) continue;
            stops.add(stop);
        }
        barGradient = new ConicalGradient(new Point2D(size * 0.5, size * 0.5), offset, stops);
    }

    private void resizeText() {
        title.setFont(Font.font("Open Sans", FontWeight.NORMAL, size * 0.1));
        title.setTranslateX((size - title.getLayoutBounds().getWidth()) * 0.5);
        title.setTranslateY(size * 0.3);

        unit.setFont(Font.font("Open Sans", FontWeight.NORMAL, size * 0.1));
        unit.setTranslateX((size - unit.getLayoutBounds().getWidth()) * 0.5);
        unit.setTranslateY(size * 0.7);

        value.setFont(Font.font("Open Sans", FontWeight.BOLD, size * 0.25));
        value.setTranslateX((size - value.getLayoutBounds().getWidth()) * 0.5);
        value.setTranslateY(size * 0.5);
    }

    private void resize() {
        size = getSkinnable().getWidth() < getSkinnable().getHeight() ? getSkinnable().getWidth() : getSkinnable().getHeight();
        centerX = size * 0.5;
        centerY = size * 0.5;

        final double RADIUS = size * 0.5 - 2;

        valueBlendBottomShadow.setOffsetY(0.005 * size);

        valueBlendTopShadow.setOffsetY(0.005 * size);
        valueBlendTopShadow.setRadius(0.005 * size);

        dropShadow.setRadius(0.015 * size);
        dropShadow.setOffsetY(0.015 * size);

        background.setPrefSize(size, size);

        ticksAndSectionsCanvas.setWidth(size);
        ticksAndSectionsCanvas.setHeight(size);
        ticksAndSections.clearRect(0, 0, size, size);
        drawSections(ticksAndSections);
        drawTickMarks(ticksAndSections);
        ticksAndSectionsCanvas.setCache(true);
        ticksAndSectionsCanvas.setCacheHint(CacheHint.QUALITY);

        drawMarkers();

        minMeasuredValue.setPrefSize(0.03 * size, 0.03 * size);
        minMeasuredValue.relocate((size - minMeasuredValue.getPrefWidth()) * 0.5, size * 0.11);
        minMeasuredValueRotate.setPivotX(minMeasuredValue.getPrefWidth() * 0.5);
        minMeasuredValueRotate.setPivotY(size * 0.39);
        minMeasuredValueRotate.setAngle(getSkinnable().getMinMeasuredValue() * angleStep - 180 - getSkinnable().getStartAngle());

        maxMeasuredValue.setPrefSize(0.03 * size, 0.03 * size);
        maxMeasuredValue.relocate((size - maxMeasuredValue.getPrefWidth()) * 0.5, size * 0.11);
        maxMeasuredValueRotate.setPivotX(maxMeasuredValue.getPrefWidth() * 0.5);
        maxMeasuredValueRotate.setPivotY(size * 0.39);
        maxMeasuredValueRotate.setAngle(getSkinnable().getMaxMeasuredValue() * angleStep - 180 - getSkinnable().getStartAngle());

        threshold.setPrefSize(0.06 * size, 0.055 * size);
        threshold.relocate((size - threshold.getPrefWidth()) * 0.5, size * 0.08);
        thresholdRotate.setPivotX(threshold.getPrefWidth() * 0.5);
        thresholdRotate.setPivotY(size * 0.42);
        thresholdRotate.setAngle(getSkinnable().getThreshold() * angleStep - 180 - getSkinnable().getStartAngle());

        value.setText(String.format(Locale.US, "%." + getSkinnable().getDecimals() + "f", (angle.get() / angleStep)));
        bar.setCenterX(centerX);
        bar.setCenterY(centerY);
        bar.setRadiusX(RADIUS);
        bar.setRadiusY(RADIUS);

        if (getSkinnable().isBarGradientEnabled()) {
            recalculateBarGradient();
            Image image = barGradient.getImage(size, size);
            bar.setFill(new ImagePattern(image, 0, 0, size, size, false));
        } else {
            bar.setFill(new RadialGradient(0, 0,
                                           centerX, centerY,
                                           RADIUS, false, CycleMethod.NO_CYCLE,
                                           new Stop(0.0, barColor),
                                           new Stop(0.76, barColor.deriveColor(-5, 1, 1, 1)), // -5 for on the barColorHue)
                                           new Stop(0.79, barColor),
                                           new Stop(0.97, barColor),
                                           new Stop(1.0, barColor.deriveColor(-5, 1, 1, 1)))); // -5 for on the barColorHue)
        }
        knob.setPrefSize(size * 0.75, size * 0.75);
        knob.setTranslateX((size - knob.getPrefWidth()) * 0.5);
        knob.setTranslateY((size - knob.getPrefHeight()) * 0.5);

        resizeText();
    }
}