/*
 * Copyright (c) 2020 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.regulators;

import javafx.beans.property.BooleanProperty;
import javafx.beans.property.BooleanPropertyBase;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.DoublePropertyBase;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.IntegerPropertyBase;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.beans.property.StringProperty;
import javafx.beans.property.StringPropertyBase;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.geometry.VPos;
import javafx.scene.CacheHint;
import javafx.scene.Group;
import javafx.scene.effect.BlurType;
import javafx.scene.effect.DropShadow;
import javafx.scene.effect.InnerShadow;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Arc;
import javafx.scene.shape.ArcType;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Scale;
import org.kordamp.ikonli.Ikon;
import org.kordamp.ikonli.javafx.FontIcon;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;


/**
 * Created by hansolo on 01.03.16.
 */
public class FeedbackRegulator extends Region implements RegulatorControl {
    private static final Color          DEFAULT_COLOR    = Color.rgb(66,71,79);
    private static final double         PREFERRED_WIDTH  = 250;
    private static final double         PREFERRED_HEIGHT = 250;
    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         BAR_START_ANGLE  = -130;
    private              double         ANGLE_RANGE      = 280;
    private final        RegulatorEvent ADJUSTING_EVENT  = new RegulatorEvent(RegulatorEvent.ADJUSTING);
    private final        RegulatorEvent ADJUSTED_EVENT   = new RegulatorEvent(RegulatorEvent.ADJUSTED);
    private final        RegulatorEvent TARGET_SET_EVENT = new RegulatorEvent(RegulatorEvent.TARGET_SET);
    private double                      size;
    private Arc                         barArc;
    private Arc                         overlayBarArc;
    private Shape                       ring;
    private Circle                      mainCircle;
    private Text                        text;
    private Text                        targetText;
    private Circle                      indicator;
    private Region                      symbol;
    private StackPane                   iconPane;
    private FontIcon                    icon;
    private Pane                        pane;
    private DropShadow                  dropShadow;
    private InnerShadow                 highlight;
    private InnerShadow                 innerShadow;
    private DropShadow                  indicatorGlow;
    private InnerShadow                 indicatorInnerShadow;
    private InnerShadow                 indicatorHighlight;
    private Rotate                      indicatorRotate;
    private double                      scaleFactor;
    private DoubleProperty              minValue;
    private DoubleProperty              maxValue;
    private DoubleProperty              targetValue;
    private DoubleProperty              currentValue;
    private IntegerProperty             decimals;
    private StringProperty              unit;
    private ObjectProperty<Color>       symbolColor;
    private ObjectProperty<Color>       iconColor;
    private ObjectProperty<Color>       textColor;
    private ObjectProperty<Color>       color;
    private ObjectProperty<Color>       indicatorColor;
    private BooleanProperty             selected;
    private String                      formatString;
    private double                      angleStep;
    private ConicalGradient             barGradient;


    // ******************** Constructors **************************************
    public FeedbackRegulator() {
        getStylesheets().add(FeedbackRegulator.class.getResource("feedback_regulator.css").toExternalForm());
        scaleFactor    = 1.0;
        minValue       = new DoublePropertyBase(0) {
            @Override public void set(final double VALUE) {
                super.set(clamp(-Double.MAX_VALUE, maxValue.get(), VALUE));
                angleStep = ANGLE_RANGE / (maxValue.get() - minValue.get());
            }
            @Override public Object getBean() { return FeedbackRegulator.this; }
            @Override public String getName() { return "minValue"; }
        };
        maxValue       = new DoublePropertyBase(40) {
            @Override public void set(final double VALUE) {
                super.set(clamp(minValue.get(), Double.MAX_VALUE, VALUE));
                angleStep = ANGLE_RANGE / (maxValue.get() - minValue.get());
            }
            @Override public Object getBean() { return FeedbackRegulator.this; }
            @Override public String getName() { return "maxValue"; }
        };
        targetValue    = new DoublePropertyBase(0) {
            @Override public void set(final double VALUE) {
                super.set(clamp(minValue.get(), maxValue.get(), VALUE));
                if ((int) get() == (int) currentValue.get()) {
                    targetText.setVisible(false);
                    overlayBarArc.setVisible(false);
                } else {
                    targetText.setVisible(true);
                    overlayBarArc.setVisible(true);
                }
            }
            @Override public Object getBean() { return FeedbackRegulator.this; }
            @Override public String getName() { return "targetValue"; }
        };
        currentValue   = new DoublePropertyBase(0) {
            @Override public void set(final double VALUE) {
                super.set(clamp(minValue.get(), maxValue.get(), VALUE));
                if ((int) targetValue.get() == (int) get()) {
                    fireEvent(ADJUSTED_EVENT);
                    targetText.setVisible(false);
                    overlayBarArc.setVisible(false);
                } else {
                    fireEvent(ADJUSTING_EVENT);
                    targetText.setVisible(true);
                    overlayBarArc.setVisible(true);
                }
                setText(get());
                drawOverlayBar(get());
                redraw();
            }
            @Override public Object getBean() { return FeedbackRegulator.this; }
            @Override public String getName() { return "currentValue"; }
        };
        decimals       = new IntegerPropertyBase(0) {
            @Override public void set(final int VALUE) {
                super.set(clamp(0, 2, VALUE));
                formatString = new StringBuilder("%.").append(Integer.toString(decimals.get())).append("f").append(getUnit()).toString();
                redraw();
            }
            @Override public Object getBean() { return FeedbackRegulator.this; }
            @Override public String getName() { return "decimals"; }
        };
        unit           = new StringPropertyBase("\u00B0") {
            @Override public void set(final String VALUE) {
                super.set(VALUE.equals("%") ? "%%" : VALUE);
                formatString = new StringBuilder("%.").append(Integer.toString(decimals.get())).append("f").append(get()).toString();
                redraw();
            }
            @Override public Object getBean() { return FeedbackRegulator.this; }
            @Override public String getName() { return "unit"; }
        };
        symbolColor    = new ObjectPropertyBase<Color>(Color.TRANSPARENT) {
            @Override protected void invalidated() {
                set(null == get() ? Color.WHITE : get());
                redraw();
            }
            @Override public Object getBean() { return FeedbackRegulator.this; }
            @Override public String getName() { return "symbolColor"; }
        };
        iconColor      = new ObjectPropertyBase<Color>(Color.TRANSPARENT) {
            @Override protected void invalidated() {
                set(null == get() ? Color.WHITE : get());
                redraw();
            }
            @Override public Object getBean() { return FeedbackRegulator.this; }
            @Override public String getName() { return "iconColor"; }
        };
        textColor      = new ObjectPropertyBase<Color>(Color.WHITE) {
            @Override protected void invalidated() {
                set(null == get() ? Color.WHITE : get());
                redraw();
            }
            @Override public Object getBean() { return FeedbackRegulator.this; }
            @Override public String getName() { return "textColor"; }
        };
        color          = new ObjectPropertyBase<Color>(DEFAULT_COLOR) {
            @Override protected void invalidated() {
                super.set(null == get() ? DEFAULT_COLOR : get());
                redraw();
            }
            @Override public Object getBean() { return FeedbackRegulator.this; }
            @Override public String getName() { return "color"; }
        };
        indicatorColor = new ObjectPropertyBase<Color>(Color.WHITE) {
            @Override protected void invalidated() { indicatorGlow.setColor(get()); }
            @Override public Object getBean() { return FeedbackRegulator.this; }
            @Override public String getName() { return "indicatorColor"; }
        };
        selected       = new BooleanPropertyBase(false) {
            @Override protected void invalidated() {
                if (get()) {
                    indicator.setFill(getIndicatorColor());
                    indicator.setStroke(getIndicatorColor().darker().darker());
                    indicator.setEffect(indicatorGlow);
                } else {
                    indicator.setFill(getColor().darker());
                    indicator.setStroke(getColor().darker().darker());
                    indicator.setEffect(null);
                }
            }
            @Override public Object getBean() { return FeedbackRegulator.this; }
            @Override public String getName() { return "selected"; }
        };
        formatString   = new StringBuilder("%.").append(Integer.toString(decimals.get())).append("f").append(unit.get()).toString();
        angleStep      = ANGLE_RANGE / (maxValue.get() - minValue.get());
        init();
        initGraphics();
        registerListeners();
    }


    // ******************** Initialization ************************************
    private void init() {
        if (Double.compare(getPrefWidth(), 0.0) <= 0 || Double.compare(getPrefHeight(), 0.0) <= 0 ||
            Double.compare(getWidth(), 0.0) <= 0 || Double.compare(getHeight(), 0.0) <= 0) {
            if (getPrefWidth() > 0 && getPrefHeight() > 0) {
                setPrefSize(getPrefWidth(), getPrefHeight());
            } else {
                setPrefSize(PREFERRED_WIDTH, PREFERRED_HEIGHT);
            }
        }
        if (Double.compare(getMinWidth(), 0.0) <= 0 || Double.compare(getMinHeight(), 0.0) <= 0) {
            setMinSize(MINIMUM_WIDTH, MINIMUM_HEIGHT);
        }
        if (Double.compare(getMaxWidth(), 0.0) <= 0 || Double.compare(getMaxHeight(), 0.0) <= 0) {
            setMaxSize(MAXIMUM_WIDTH, MAXIMUM_HEIGHT);
        }
    }

    private void initGraphics() {
        dropShadow  = new DropShadow(BlurType.TWO_PASS_BOX, Color.rgb(0, 0, 0, 0.65), PREFERRED_WIDTH * 0.016, 0.0, 0, PREFERRED_WIDTH * 0.028);
        highlight   = new InnerShadow(BlurType.TWO_PASS_BOX, Color.rgb(255, 255, 255, 0.2), PREFERRED_WIDTH * 0.008, 0.0, 0, PREFERRED_WIDTH * 0.008);
        innerShadow = new InnerShadow(BlurType.TWO_PASS_BOX, Color.rgb(0, 0, 0, 0.2), PREFERRED_WIDTH * 0.008, 0.0, 0, -PREFERRED_WIDTH * 0.008);
        highlight.setInput(innerShadow);
        dropShadow.setInput(highlight);

        Stop[] stops = {
            new Stop(0.0, Color.rgb(135, 255, 190)),
            new Stop(0.125, Color.rgb(254, 190, 106)),
            new Stop(0.389, Color.rgb(252, 84, 68)),
            new Stop(0.611, Color.rgb(99, 195, 255)),
            new Stop(1.0, Color.rgb(125, 255, 190))
        };

        barGradient = new ConicalGradient(stops);

        barArc = new Arc(PREFERRED_WIDTH * 0.5, PREFERRED_HEIGHT * 0.5, PREFERRED_WIDTH * 0.46, PREFERRED_HEIGHT * 0.46, BAR_START_ANGLE, 0);
        barArc.setType(ArcType.OPEN);
        barArc.setStrokeLineCap(StrokeLineCap.ROUND);
        barArc.setFill(null);
        barArc.setStroke(barGradient.getImagePattern(new Rectangle(0, 0, PREFERRED_WIDTH, PREFERRED_HEIGHT)));

        overlayBarArc = new Arc(PREFERRED_WIDTH * 0.5, PREFERRED_HEIGHT * 0.5, PREFERRED_WIDTH * 0.46, PREFERRED_HEIGHT * 0.46, BAR_START_ANGLE, 0);
        overlayBarArc.setType(ArcType.OPEN);
        overlayBarArc.setStrokeLineCap(StrokeLineCap.ROUND);
        overlayBarArc.setFill(null);
        overlayBarArc.setStroke(Color.rgb(0, 0, 0, 0.3));
        overlayBarArc.setVisible((int) targetValue.get() != (int) currentValue.get());

        double center = PREFERRED_WIDTH * 0.5;
        ring = Shape.subtract(new Circle(center, center, PREFERRED_WIDTH * 0.42),
                              new Circle(center, center, PREFERRED_WIDTH * 0.3));
        ring.setFill(color.get());
        ring.setEffect(dropShadow);

        mainCircle = new Circle();
        mainCircle.setFill(color.get().darker().darker());

        text = new Text(String.format(Locale.US, formatString, currentValue.get()));
        text.setFill(textColor.get());
        text.setTextOrigin(VPos.CENTER);

        targetText = new Text(String.format(Locale.US, formatString, targetValue.get()));
        targetText.setFill(textColor.get().darker());
        targetText.setTextOrigin(VPos.CENTER);
        targetText.setVisible((int) targetValue.get() != (int) currentValue.get());

        indicatorRotate = new Rotate(-ANGLE_RANGE *  0.5, center, center);

        indicatorGlow        = new DropShadow(BlurType.TWO_PASS_BOX, getIndicatorColor(), PREFERRED_WIDTH * 0.02, 0.0, 0, 0);
        indicatorInnerShadow = new InnerShadow(BlurType.TWO_PASS_BOX, Color.rgb(0, 0, 0, 0.5), PREFERRED_WIDTH * 0.008, 0.0, 0, PREFERRED_WIDTH * 0.008);
        indicatorHighlight   = new InnerShadow(BlurType.TWO_PASS_BOX, Color.rgb(255, 255, 255, 0.35), PREFERRED_WIDTH * 0.008, 0.0, 0, -PREFERRED_WIDTH * 0.008);
        indicatorHighlight.setInput(indicatorInnerShadow);

        indicator = new Circle();
        indicator.setFill(color.get().darker());
        indicator.setStroke(color.get().darker().darker());
        indicator.setMouseTransparent(true);
        indicator.getTransforms().add(indicatorRotate);

        Group indicatorGroup = new Group(indicator);
        indicatorGroup.setEffect(indicatorHighlight);

        symbol = new Region();
        symbol.getStyleClass().setAll("symbol");
        symbol.setCacheHint(CacheHint.SPEED);

        icon = new FontIcon();
        icon.setTextOrigin(VPos.CENTER);

        iconPane = new StackPane(symbol, icon);

        pane = new Pane(barArc, overlayBarArc, ring, mainCircle, text, targetText, indicatorGroup, iconPane);
        pane.setPrefSize(PREFERRED_HEIGHT, PREFERRED_HEIGHT);
        pane.setBackground(new Background(new BackgroundFill(color.get().darker(), new CornerRadii(1024), Insets.EMPTY)));
        pane.setEffect(highlight);

        getChildren().setAll(pane);
    }

    private void registerListeners() {
        widthProperty().addListener(o -> resize());
        heightProperty().addListener(o -> resize());
        disabledProperty().addListener(o -> setOpacity(isDisabled() ? 0.4 : 1.0));
        targetValueProperty().addListener(o -> rotate(targetValue.get()));
        currentValueProperty().addListener(o -> setText(currentValue.get()));
        ring.addEventHandler(MouseEvent.MOUSE_PRESSED, e -> { if (isDisabled()) return; touchRotate(e.getSceneX(), e.getSceneY()); });
        ring.addEventHandler(MouseEvent.MOUSE_DRAGGED, e -> { if (isDisabled()) return; touchRotate(e.getSceneX(), e.getSceneY()); });
        ring.addEventHandler(MouseEvent.MOUSE_RELEASED, e -> { if (isDisabled()) return; fireEvent(TARGET_SET_EVENT); } );
    }


    // ******************** Methods *******************************************
    public double getMinValue() { return minValue.get(); }
    public void setMinValue(final double VALUE) { minValue.set(VALUE); }
    public DoubleProperty minValueProperty() { return minValue; }

    public double getMaxValue() { return maxValue.get(); }
    public void setMaxValue(final double VALUE) { maxValue.set(VALUE); }
    public DoubleProperty maxValueProperty() { return maxValue; }

    @Override public double getTargetValue() { return targetValue.get(); }
    @Override public void setTargetValue(final double VALUE) { targetValue.set(VALUE); }
    @Override public DoubleProperty targetValueProperty() { return targetValue; }

    public double getCurrentValue() { return currentValue.get(); }
    public void setCurrentValue(final double VALUE) { currentValue.set(VALUE); }
    public DoubleProperty currentValueProperty() { return currentValue; }

    public int getDecimals() { return decimals.get(); }
    public void setDecimals(final int VALUE) { decimals.set(VALUE); }
    public IntegerProperty decimalsProperty() { return decimals; }

    public String getUnit()  { return unit.get(); }
    public void setUnit(final String UNIT) { unit.set(UNIT); }
    public StringProperty unitProperty() { return unit; }

    public Color getSymbolColor() { return symbolColor.get(); }
    public void setSymbolColor(final Color COLOR) { symbolColor.set(COLOR); }
    public ObjectProperty<Color> symbolColorProperty() { return symbolColor; }

    public Color getIconColor() { return iconColor.get(); }
    public void setIconColor(final Color COLOR) { iconColor.set(COLOR); }
    public ObjectProperty<Color> iconColorProperty() { return iconColor; }

    @Override public Color getTextColor() { return textColor.get(); }
    @Override public void setTextColor(final Color COLOR) { textColor.set(COLOR); }
    @Override public ObjectProperty<Color> textColorProperty() { return textColor; }

    @Override public Color getColor() { return color.get(); }
    @Override public void setColor(final Color COLOR) { color.set(COLOR); }
    @Override public ObjectProperty<Color> colorProperty() { return color; }

    @Override public Color getIndicatorColor() { return indicatorColor.get(); }
    @Override public void setIndicatorColor(final Color COLOR) { indicatorColor.set(COLOR); }
    @Override public ObjectProperty<Color> indicatorColorProperty() { return indicatorColor; }

    @Override public boolean isSelected() { return selected.get(); }
    @Override public void setSelected(final boolean SELECTED) { selected.set(SELECTED); }
    @Override public BooleanProperty selectedProperty() { return selected; }

    public List<Stop> getGradientStops() { return barGradient.getStops(); }
    public void setGradientStops(final Stop... STOPS) { setGradientStops(Arrays.asList(STOPS)); }
    public void setGradientStops(final List<Stop> STOPS) {
        barGradient = new ConicalGradient(reorderStops(STOPS));
        barArc.setStroke(barGradient.getImagePattern(new Rectangle(0, 0, PREFERRED_WIDTH, PREFERRED_HEIGHT)));
    }

    private List<Stop> reorderStops(final Stop... STOPS) { return reorderStops(Arrays.asList(STOPS)); }
    private List<Stop> reorderStops(final List<Stop> STOPS) {
        /*
        0.0 -> 0.611
        0.5 -> 0.0 & 1.0
        1.0 -> 0.389
         */
        double range     = 0.778;
        double halfRange = range * 0.5;

        Map<Double, Color> stopMap = new HashMap<>();
        for (Stop stop : STOPS) { stopMap.put(stop.getOffset(), stop.getColor()); }

        List<Stop>        sortedStops     = new ArrayList<>(STOPS.size());
        SortedSet<Double> sortedFractions = new TreeSet<>(stopMap.keySet());
        if (sortedFractions.last() < 1) {
            stopMap.put(1.0, stopMap.get(sortedFractions.last()));
            sortedFractions.add(1.0);
        }
        if (sortedFractions.first() > 0) {
            stopMap.put(0.0, stopMap.get(sortedFractions.first()));
            sortedFractions.add(0.0);
        }
        for (double fraction : sortedFractions) {
            double offset = fraction * range - halfRange;
            offset = offset < 0 ? 1.0 + offset : offset;
            sortedStops.add(new Stop(offset, stopMap.get(fraction)));
        }
        return sortedStops;
    }

    public void setSymbolPath(final double SCALE_X, final double SCALE_Y, final String PATH) {
        if (PATH.isEmpty()) {
            symbol.setVisible(false);
        } else {
            symbol.setStyle(new StringBuilder().append("-fx-scale-x:").append(clamp(0.0, 1.0, SCALE_X)).append(";")
                                               .append("-fx-scale-y:").append(clamp(0.0, 1.0, SCALE_Y)).append(";")
                                               .append("-fx-shape:\"").append(PATH).append("\";")
                                               .toString());
            symbol.setVisible(true);
        }
        symbol.setCache(false);
        resize();
        symbol.setCache(true);
    }

    public void setIcon(final Ikon ICON) {
        icon.setIconCode(ICON);
        resize();
    }

    private <T extends Number> T clamp(final T MIN, final T MAX, final T VALUE) {
        if (VALUE.doubleValue() < MIN.doubleValue()) return MIN;
        if (VALUE.doubleValue() > MAX.doubleValue()) return MAX;
        return VALUE;
    }

    private void adjustTextSize(final Text TEXT, final double MAX_WIDTH, double fontSize) {
        final String FONT_NAME = TEXT.getFont().getName();
        while (TEXT.getLayoutBounds().getWidth() > MAX_WIDTH && fontSize > 0) {
            fontSize -= 0.005;
            TEXT.setFont(new Font(FONT_NAME, fontSize));
        }
    }

    private void touchRotate(final double X, final double Y) {
        Point2D p      = sceneToLocal(X, Y);
        double  deltaX = p.getX() - (pane.getLayoutX() + size * 0.5);
        double  deltaY = p.getY() - (pane.getLayoutY() + size * 0.5);
        double  radius = Math.sqrt((deltaX * deltaX) + (deltaY * deltaY));
        double  nx     = deltaX / radius;
        double  ny     = deltaY / radius;
        double  theta  = Math.atan2(ny, nx);
        theta         = Double.compare(theta, 0.0) >= 0 ? Math.toDegrees(theta) : Math.toDegrees((theta)) + 360.0;
        double angle  = (theta + 230) % 360;
        if (angle > 320 && angle < 360) {
            angle = 0;
        } else if (angle <= 320 && angle > ANGLE_RANGE) {
            angle = ANGLE_RANGE;
        }
        setTargetValue(angle / angleStep + minValue.get());
    }


    // ******************** Resizing ******************************************
    private void rotate(final double VALUE) {
        indicatorRotate.setAngle((VALUE - minValue.get()) * angleStep - ANGLE_RANGE * 0.5);
        targetText.setText(String.format(Locale.US, formatString, VALUE));
        adjustTextSize(targetText, size * 0.24, size * 0.216);
        targetText.setLayoutX((size - targetText.getLayoutBounds().getWidth()) * 0.5);
    }

    private void setText(final double VALUE) {
        text.setText(String.format(Locale.US, formatString, VALUE));
        adjustTextSize(text, size * 0.48, size * 0.216);
        text.setLayoutX((size - text.getLayoutBounds().getWidth()) * 0.5);
    }

    private void drawBar(final double VALUE) {
        barArc.setLength(-(VALUE - minValue.get()) * angleStep);
    }

    private void drawOverlayBar(final double VALUE) {
        overlayBarArc.setLength(-(VALUE - minValue.get()) * angleStep);
    }

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

        if (width > 0 && height > 0) {
            pane.setMaxSize(size, size);
            pane.setPrefSize(size, size);
            pane.relocate((getWidth() - size) * 0.5, (getHeight() - size) * 0.5);

            barArc.setCache(false);
            barArc.setCenterX(size * 0.5);
            barArc.setCenterY(size * 0.5);
            barArc.setRadiusX(size * 0.46);
            barArc.setRadiusY(size * 0.46);
            barArc.setStrokeWidth(size * 0.04);
            barArc.setStroke(barGradient.getImagePattern(new Rectangle(0, 0, size, size)));
            drawBar(maxValue.get());
            barArc.setCache(true);
            barArc.setCacheHint(CacheHint.SPEED);

            overlayBarArc.setCenterX(size * 0.5);
            overlayBarArc.setCenterY(size * 0.5);
            overlayBarArc.setRadiusX(size * 0.46);
            overlayBarArc.setRadiusY(size * 0.46);
            overlayBarArc.setStrokeWidth(size * 0.03);
            drawOverlayBar(currentValue.get());

            double shadowRadius = clamp(1.0, 2.0, size * 0.004);
            dropShadow.setRadius(shadowRadius);
            dropShadow.setOffsetY(shadowRadius);
            highlight.setRadius(shadowRadius);
            highlight.setOffsetY(shadowRadius);
            innerShadow.setRadius(shadowRadius);
            innerShadow.setOffsetY(-shadowRadius);

            double center = size * 0.5;
            scaleFactor = size / PREFERRED_WIDTH;
            ring.setCache(false);
            ring.getTransforms().setAll(new Scale(scaleFactor, scaleFactor, 0, 0));
            ring.setCache(true);
            ring.setCacheHint(CacheHint.SPEED);

            mainCircle.setCache(false);
            mainCircle.setRadius(size * 0.3);
            mainCircle.setCenterX(center); mainCircle.setCenterY(center);
            mainCircle.setCache(true);
            mainCircle.setCacheHint(CacheHint.SPEED);

            text.setFont(Fonts.robotoMedium(size * 0.216));
            text.relocate((size - text.getLayoutBounds().getWidth()) * 0.5, size * 0.33);

            targetText.setFont(Fonts.robotoLight(size * 0.082));
            targetText.relocate((size - targetText.getLayoutBounds().getWidth()) * 0.5, size * 0.23);

            indicatorGlow.setRadius(size * 0.02);
            indicatorInnerShadow.setRadius(size * 0.008);
            indicatorInnerShadow.setOffsetY(size * 0.006);
            indicatorHighlight.setRadius(size * 0.008);
            indicatorHighlight.setOffsetY(-size * 0.004);

            indicator.setRadius(size * 0.032);
            indicator.setCenterX(center);
            indicator.setCenterY(size * 0.148);

            indicatorRotate.setPivotX(center);
            indicatorRotate.setPivotY(center);

            icon.setIconSize((int) (size * 0.112));

            iconPane.setPrefSize(size * 0.112, size * 0.112);
            iconPane.relocate((size - iconPane.getPrefWidth()) * 0.5, size * 0.62);

            redraw();
        }
    }

    private void redraw() {
        pane.setBackground(new Background(new BackgroundFill(color.get().darker(), new CornerRadii(1024), Insets.EMPTY)));
        mainCircle.setFill(color.get().darker().darker());
        ring.setFill(color.get());
        indicator.setFill(isSelected() ? indicatorColor.get() : color.get().darker());
        indicator.setStroke(isSelected() ? indicatorColor.get().darker().darker() : color.get().darker().darker());
        symbol.setBackground(new Background(new BackgroundFill(symbolColor.get(), CornerRadii.EMPTY, Insets.EMPTY)));
        icon.setFill(iconColor.get());
        targetText.setFill(textColor.get().darker());
        text.setFill(textColor.get());
        rotate(targetValue.get());
        setText(currentValue.get());
    }


    // ******************** Event Handling ************************************
    public void setOnTargetSet(final EventHandler<RegulatorEvent> HANDLER) { addEventHandler(RegulatorEvent.TARGET_SET, HANDLER); }
    public void removeOnTargetSet(final EventHandler<RegulatorEvent> HANDLER) { removeEventHandler(RegulatorEvent.TARGET_SET, HANDLER); }

    public void setOnAdjusting(final EventHandler<RegulatorEvent> HANDLER) { addEventHandler(RegulatorEvent.ADJUSTING, HANDLER); }
    public void removeOnAdjusting(final EventHandler<RegulatorEvent> HANDLER) { removeEventHandler(RegulatorEvent.ADJUSTING, HANDLER); }

    public void setOnAdjusted(final EventHandler<RegulatorEvent> HANDLER) { addEventHandler(RegulatorEvent.ADJUSTED, HANDLER); }
    public void removeOnAdjusted(final EventHandler<RegulatorEvent> HANDLER) { removeEventHandler(RegulatorEvent.ADJUSTED, HANDLER); }
}