/*
 * 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.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
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.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.Text;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Scale;

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


/**
 * Created by hansolo on 03.03.16.
 */
public class ColorRegulator 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 static final double         MIN_VALUE        = 0.0;
    private static final double         MAX_VALUE        = 100.0;
    private              double         BAR_START_ANGLE  = -130;
    private              double         ANGLE_RANGE      = 280;
    private final        RegulatorEvent TARGET_SET_EVENT = new RegulatorEvent(RegulatorEvent.TARGET_SET);
    private double                      size;
    private Arc                         barArc;
    private Arc                         buttonOn;
    private Arc                         buttonOff;
    private Shape                       ring;
    private Shape                       innerRing;
    private Circle                      mainCircle;
    private Text                        textOn;
    private Text                        textOff;
    private Circle                      indicator;
    private Circle                      currentColorCircle;
    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 Color                       baseColor;
    private DoubleProperty              targetValue;
    private ObjectProperty<Color>       targetColor;
    private ObjectProperty<Color>       textColor;
    private ObjectProperty<Color>       color;
    private ObjectProperty<Color>       indicatorColor;
    private BooleanProperty             selected;
    private BooleanProperty             on;
    private DoubleProperty              brightness;
    private double                      angleStep;
    private ConicalGradient             barGradient;
    private GradientLookup              gradientLookup;


    // ******************** Constructors **************************************
    public ColorRegulator() {
        scaleFactor    = 1.0;
        baseColor      = Color.YELLOW;
        targetValue    = new DoublePropertyBase(0) {
            @Override protected void invalidated() { setOn(Double.compare(get(), 0) != 0); }
            @Override public void set(final double VALUE) {
                super.set(clamp(MIN_VALUE, MAX_VALUE, VALUE));
            }
            @Override public Object getBean() { return ColorRegulator.this; }
            @Override public String getName() { return "targetValue"; }
        };
        targetColor    = new ObjectPropertyBase<Color>(baseColor) {
            @Override protected void invalidated() {
                super.set(null == get() ? Color.BLACK : get());
                currentColorCircle.setFill(get());
                indicatorRotate.setAngle(((gradientLookup.getValueFrom(baseColor) * 100.0) - MIN_VALUE) * angleStep - ANGLE_RANGE * 0.5);
            }
            @Override public Object getBean() { return ColorRegulator.this; }
            @Override public String getName() { return "targetColor"; }
        };
        textColor      = new ObjectPropertyBase<Color>(Color.WHITE) {
            @Override protected void invalidated() {
                super.set(null == get() ? Color.WHITE:  get());
                redraw();
            }
            @Override public Object getBean() { return ColorRegulator.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 ColorRegulator.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 ColorRegulator.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 ColorRegulator.this; }
            @Override public String getName() { return "selected"; }
        };
        on             = new BooleanPropertyBase(false) {
            @Override protected void invalidated() { currentColorCircle.setVisible(get()); }
            @Override public Object getBean() { return ColorRegulator.this; }
            @Override public String getName() { return "on"; }
        };
        brightness     = new DoublePropertyBase(1.0) {
            @Override protected void invalidated() {
                set(clamp(0.0, 1.0, get()));
                targetColor.set(baseColor.deriveColor(0, 1, get(), 1));
            }
            @Override public Object getBean() { return ColorRegulator.this; }
            @Override public String getName() { return "brightness"; }
        };
        angleStep      = ANGLE_RANGE / (MAX_VALUE - MIN_VALUE);

        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(255,255,0)),
                         new Stop(0.125, Color.rgb(255,0,0)),
                         new Stop(0.375, Color.rgb(255,0,255)),
                         new Stop(0.5, Color.rgb(0,0,255)),
                         new Stop(0.625, Color.rgb(0,255,255)),
                         new Stop(0.875, Color.rgb(0,255,0)),
                         new Stop(1.0, Color.rgb(255,255,0)) };

        List<Stop> reorderedStops = reorderStops(stops);

        gradientLookup = new GradientLookup(stops);

        barGradient = new ConicalGradient(reorderedStops);
        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)));

        buttonOn = new Arc(PREFERRED_WIDTH * 0.5, PREFERRED_HEIGHT * 0.5, PREFERRED_WIDTH * 0.46, PREFERRED_HEIGHT * 0.46, -125, 34.75);
        buttonOn.setFill(null);
        buttonOn.setStroke(color.get());
        buttonOn.setStrokeLineCap(StrokeLineCap.BUTT);
        buttonOn.setStrokeWidth(PREFERRED_WIDTH * 0.072);
        buttonOn.setEffect(dropShadow);

        buttonOff = new Arc(PREFERRED_WIDTH * 0.5, PREFERRED_HEIGHT * 0.5, PREFERRED_WIDTH * 0.46, PREFERRED_HEIGHT * 0.46, -89.75, 34.75);
        buttonOff.setFill(null);
        buttonOff.setStroke(color.get());
        buttonOff.setStrokeLineCap(StrokeLineCap.BUTT);
        buttonOff.setStrokeWidth(PREFERRED_WIDTH * 0.072);
        buttonOff.setEffect(dropShadow);

        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(highlight);

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

        textOn = new Text("ON");
        textOn.setFill(textColor.get());
        textOn.setTextOrigin(VPos.CENTER);
        textOn.setMouseTransparent(true);
        textOn.setRotate(17);

        textOff = new Text("OFF");
        textOff.setFill(textColor.get());
        textOff.setTextOrigin(VPos.CENTER);
        textOff.setMouseTransparent(true);
        textOff.setRotate(-17);

        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);

        innerRing = Shape.subtract(new Circle(center, center, PREFERRED_WIDTH * 0.24),
                                   new Circle(center, center, PREFERRED_WIDTH * 0.2));
        innerRing.setFill(color.get());

        currentColorCircle = new Circle();
        currentColorCircle.setFill(targetColor.get());
        currentColorCircle.setVisible(isOn());

        pane = new Pane(barArc, ring, mainCircle, currentColorCircle, innerRing, indicatorGroup, buttonOn, textOn, buttonOff, textOff);
        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()));
        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); });
        buttonOn.setOnMousePressed(e -> { if (isDisabled()) return; buttonOnPressed(true); });
        buttonOn.setOnMouseReleased(e -> { if (isDisabled()) return; buttonOnPressed(false); });
        buttonOff.setOnMousePressed(e -> { if (isDisabled()) return; buttonOffPressed(true); });
        buttonOff.setOnMouseReleased(e -> { if (isDisabled()) return; buttonOffPressed(false); });
    }


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

    public Color getTargetColor() { return targetColor.get(); }
    public void setTargetColor(final Color COLOR) { targetColor.set(COLOR); }
    public ObjectProperty<Color> targetColorProperty() { return targetColor; }

    @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) {
        gradientLookup.setStops(STOPS);
        barGradient = new ConicalGradient(reorderStops(STOPS));
        barArc.setStroke(barGradient.getImagePattern(new Rectangle(0, 0, PREFERRED_WIDTH, PREFERRED_HEIGHT)));
    }

    public boolean isOn() { return on.get(); }
    public void setOn(final boolean IS_ON) { on.set(IS_ON); }
    public BooleanProperty onProperty() { return on; }

    public double getBrightness() { return brightness.get(); }
    public void setBrightness(final double BRIGHTNESS) { brightness.set(BRIGHTNESS); }
    public DoubleProperty brightnessProperty() { return brightness; }

    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;
    }

    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 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 + MIN_VALUE);
    }


    // ******************** Resizing ******************************************
    private void rotate(final double VALUE) {
        indicatorRotate.setAngle((VALUE - MIN_VALUE) * angleStep - ANGLE_RANGE * 0.5);
        baseColor = gradientLookup.getColorAt(VALUE / 100.0);
        baseColor.deriveColor(0, 1, getBrightness(), 1);
        targetColor.set(baseColor.deriveColor(0, 1, getBrightness(), 1));
        currentColorCircle.setFill(targetColor.get());
    }

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

    private void buttonOnPressed(final boolean PRESSED) {
        buttonOn.setEffect(PRESSED ? innerShadow : dropShadow);
        textOn.relocate(buttonOn.getLayoutBounds().getMinX() + (buttonOn.getLayoutBounds().getWidth() - textOn.getLayoutBounds().getWidth()) * 0.5, PRESSED ? size * 0.913 : size * 0.91);
        setOn(true);
    }
    private void buttonOffPressed(final boolean PRESSED) {
        buttonOff.setEffect(PRESSED ? innerShadow : dropShadow);
        textOff.relocate(buttonOff.getLayoutBounds().getMinX() + (buttonOff.getLayoutBounds().getWidth() - textOff.getLayoutBounds().getWidth()) * 0.5, PRESSED ? size * 0.913 : size * 0.91);
        setOn(false);
    }

    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) {
            double center = size * 0.5;

            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(MAX_VALUE);
            barArc.setCache(true);
            barArc.setCacheHint(CacheHint.SPEED);

            double buttonRadius = size * 0.46;
            double buttonWidth  = size * 0.072;
            buttonOn.setCenterX(center);
            buttonOn.setCenterY(center);
            buttonOn.setRadiusX(buttonRadius);
            buttonOn.setRadiusY(buttonRadius);
            buttonOn.setStrokeWidth(buttonWidth);

            buttonOff.setCenterX(center);
            buttonOff.setCenterY(center);
            buttonOff.setRadiusX(buttonRadius);
            buttonOff.setRadiusY(buttonRadius);
            buttonOff.setStrokeWidth(buttonWidth);

            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);

            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);

            double fontSize = size * 0.04;
            textOn.setFont(Fonts.robotoLight(fontSize));
            textOn.relocate(buttonOn.getLayoutBounds().getMinX() + (buttonOn.getLayoutBounds().getWidth() - textOn.getLayoutBounds().getWidth()) * 0.5, size * 0.91);

            textOff.setFont(Fonts.robotoLight(fontSize));
            textOff.relocate(buttonOff.getLayoutBounds().getMinX() + (buttonOff.getLayoutBounds().getWidth() - textOff.getLayoutBounds().getWidth()) * 0.5, size * 0.91);

            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);

            currentColorCircle.setCenterX(center);
            currentColorCircle.setCenterY(center);
            currentColorCircle.setRadius(size * 0.2);

            innerRing.setCache(false);
            innerRing.getTransforms().setAll(new Scale(scaleFactor, scaleFactor, 0, 0));
            innerRing.setCache(true);
            innerRing.setCacheHint(CacheHint.SPEED);

            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());
        innerRing.setFill(color.get());
        buttonOn.setStroke(color.get());
        buttonOff.setStroke(color.get());
        textOn.setFill(textColor.get());
        textOff.setFill(textColor.get());
        indicator.setFill(isSelected() ? indicatorColor.get() : color.get().darker());
        indicator.setStroke(isSelected() ? indicatorColor.get().darker().darker() : color.get().darker().darker());
        rotate(targetValue.get());
    }


    // ******************** Event Handling ************************************
    public void setOnButtonOnPressed(final EventHandler<MouseEvent> HANDLER) { buttonOn.addEventHandler(MouseEvent.MOUSE_PRESSED, HANDLER); }
    public void removeOnButtonOnPressed(final EventHandler<MouseEvent> HANDLER) { buttonOn.removeEventHandler(MouseEvent.MOUSE_PRESSED, HANDLER); }

    public void setOnButtonOnReleased(final EventHandler<MouseEvent> HANDLER) { buttonOn.addEventHandler(MouseEvent.MOUSE_RELEASED, HANDLER); }
    public void removeOnButtonOnReleased(final EventHandler<MouseEvent> HANDLER) { buttonOn.removeEventHandler(MouseEvent.MOUSE_RELEASED, HANDLER); }

    public void setOnButtonOffPressed(final EventHandler<MouseEvent> HANDLER) { buttonOff.addEventHandler(MouseEvent.MOUSE_PRESSED, HANDLER); }
    public void removeOnButtonOffPressed(final EventHandler<MouseEvent> HANDLER) { buttonOff.removeEventHandler(MouseEvent.MOUSE_PRESSED, HANDLER); }

    public void setOnButtonOffReleased(final EventHandler<MouseEvent> HANDLER) { buttonOff.addEventHandler(MouseEvent.MOUSE_RELEASED, HANDLER); }
    public void removeOnButtonOffReleased(final EventHandler<MouseEvent> HANDLER) { buttonOff.removeEventHandler(MouseEvent.MOUSE_RELEASED, HANDLER); }

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