/*
 * 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.clock.skin;

import eu.hansolo.enzo.clock.Clock;
import eu.hansolo.enzo.common.Fonts;
import javafx.animation.AnimationTimer;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.geometry.VPos;
import javafx.scene.Group;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.effect.BlurType;
import javafx.scene.effect.DropShadow;
import javafx.scene.effect.InnerShadow;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontPosture;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import javafx.scene.transform.Rotate;
import javafx.util.Duration;

import java.time.LocalDateTime;
import java.time.temporal.ChronoField;
import java.util.ArrayList;
import java.util.List;


/**
 * User: hansolo
 * Date: 31.10.12
 * Time: 14:18
 */
public class ClockSkin extends SkinBase<Clock> implements Skin<Clock> {
    private static final long   INTERVAL         = 20_000_000l;
    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 Pane                pane;
    private String              nightDayStyleClass;
    private Region              background;
    private Canvas              logoLayer;
    private GraphicsContext     ctx;
    private Region              hourPointer;
    private Region              hourPointerFlour;
    private Region              minutePointer;
    private Region              minutePointerFlour;
    private Region              secondPointer;
    private Region              centerKnob;
    private Region              foreground;
    private double              size;
    private double              hourPointerWidthFactor;
    private double              hourPointerHeightFactor;
    private double              minutePointerWidthFactor;
    private double              minutePointerHeightFactor;
    private double              secondPointerWidthFactor;
    private double              secondPointerHeightFactor;
    private double              majorTickWidthFactor;
    private double              majorTickHeightFactor;
    private double              minorTickWidthFactor;
    private double              minorTickHeightFactor;
    private double              majorTickOffset;
    private double              minorTickOffset;
    private Rotate              hourAngle;
    private Rotate              minuteAngle;
    private Rotate              secondAngle;
    private List<Region>        ticks;
    private List<Text>          tickLabels;
    private Group               tickMarkGroup;
    private Group               tickLabelGroup;
    private Group               pointerGroup;
    private Group               secondPointerGroup;
    private Font                tickLabelFont;
    private DoubleProperty      currentMinuteAngle;
    private DoubleProperty      minute;
    private Timeline            timeline;
    private long                lastTimerCall;
    private AnimationTimer      timer;


    // ******************** Constructors **************************************
    public ClockSkin(final Clock CONTROL) {
        super(CONTROL);
        nightDayStyleClass        = getSkinnable().isNightMode() ? "night-mode" : "day-mode";

        hourPointerWidthFactor    = 0.04;
        hourPointerHeightFactor   = 0.55;
        minutePointerWidthFactor  = 0.04;
        minutePointerHeightFactor = 0.4;
        secondPointerWidthFactor  = 0.075;
        secondPointerHeightFactor = 0.46;

        majorTickWidthFactor  = 0.04;
        majorTickHeightFactor = 0.12;
        minorTickWidthFactor  = 0.01;
        minorTickHeightFactor = 0.05;

        majorTickOffset = 0.018;
        minorTickOffset = 0.05;

        //tickLabelFont      = Font.loadFont(getClass().getResourceAsStream("/eu/hansolo/enzo/fonts/helvetica.ttf"), 12);
        tickLabelFont      = Font.loadFont(getClass().getResourceAsStream("/eu/hansolo/enzo/fonts/bebasneue.ttf"), 12);
        minute             = new SimpleDoubleProperty(0);
        currentMinuteAngle = new SimpleDoubleProperty(0);

        hourAngle = new Rotate();
        hourAngle.angleProperty().bind(currentMinuteAngle);
        minuteAngle = new Rotate();
        secondAngle = new Rotate();

        ticks      = new ArrayList<>(60);
        tickLabels = new ArrayList<>(12);

        timeline = new Timeline();
        timer = new AnimationTimer() {
            @Override public void handle(final long NOW) {
                if (NOW >= lastTimerCall + INTERVAL) {
                    updateTime(LocalDateTime.now().plus(getSkinnable().getOffset()));                    
                    lastTimerCall = NOW;
                }
            }
        };
        minute.addListener(observable -> moveMinutePointer(minute.get()) );

        init();
        initGraphics();
        registerListeners();
        if (getSkinnable().isRunning()) {
            timer.start();
        } else {
            updateTime(LocalDateTime.now().plus(getSkinnable().getOffset()));
        }
    }


    // ******************** 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() {
        pane    = new Pane();

        background = new Region();
        if (Clock.Design.IOS6 == getSkinnable().getDesign()) {
            background.getStyleClass().setAll("background-ios6");
        } else if (Clock.Design.DB == getSkinnable().getDesign()) {
            background.getStyleClass().setAll("background-db");
        } else if (Clock.Design.BRAUN == getSkinnable().getDesign()) {
            background.getStyleClass().setAll("background-braun");
        } else if (Clock.Design.BOSCH == getSkinnable().getDesign()) {
            background.getStyleClass().setAll("background-bosch");
        }

        logoLayer = new Canvas(PREFERRED_WIDTH, PREFERRED_HEIGHT);
        ctx       = logoLayer.getGraphicsContext2D();

        String majorTickStyleClass;
        String minorTickStyleClass;
        if (Clock.Design.IOS6 == getSkinnable().getDesign()) {
            majorTickStyleClass = "major-tick-ios6";
            minorTickStyleClass = "minor-tick-ios6";
        } else if (Clock.Design.DB == getSkinnable().getDesign()) {
            majorTickStyleClass = "major-tick-db";
            minorTickStyleClass = "minor-tick-db";
        } else if (Clock.Design.BOSCH == getSkinnable().getDesign()) {
            majorTickStyleClass = "major-tick-bosch";
            minorTickStyleClass = "minor-tick-bosch";
        } else {
            majorTickStyleClass = "major-tick-braun";
            minorTickStyleClass = "minor-tick-braun";
        }

        int tickLabelCounter = 1;
        for (double angle = 0 ; angle < 360 ; angle += 6) {
            Region tick = new Region();
            if (angle % 30 == 0) {
                tick.getStyleClass().setAll(majorTickStyleClass);
                Text tickLabel = new Text(Integer.toString(tickLabelCounter));
                tickLabel.getStyleClass().setAll("tick-label-braun");
                tickLabels.add(tickLabel);
                tickLabelCounter++;
            } else {
                tick.getStyleClass().setAll(minorTickStyleClass);
            }
            ticks.add(tick);
        }

        DropShadow dropShadow = new DropShadow();
        dropShadow.setColor(Color.rgb(0, 0, 0, 0.65));
        dropShadow.setRadius(1.5);
        dropShadow.setBlurType(BlurType.TWO_PASS_BOX);
        dropShadow.setOffsetY(1);

        tickMarkGroup = new Group();
        tickMarkGroup.setEffect(dropShadow);
        tickMarkGroup.getChildren().setAll(ticks);

        tickLabelGroup = new Group();
        tickLabelGroup.setEffect(dropShadow);
        tickLabelGroup.getChildren().setAll(tickLabels);
        tickLabelGroup.setOpacity(Clock.Design.BRAUN == getSkinnable().getDesign() ? 1 : 0);

        hourPointer = new Region();
        if (Clock.Design.IOS6 == getSkinnable().getDesign()) {
            hourPointer.getStyleClass().setAll("hour-pointer-ios6");
        } else if (Clock.Design.DB == getSkinnable().getDesign()) {
            hourPointer.getStyleClass().setAll("hour-pointer-db");
        } else if (Clock.Design.BRAUN == getSkinnable().getDesign()) {
            hourPointer.getStyleClass().setAll("hour-pointer-braun");
        } else if (Clock.Design.BOSCH == getSkinnable().getDesign()) {
            hourPointer.getStyleClass().setAll("hour-pointer-bosch");
        }
        hourPointer.getTransforms().setAll(hourAngle);

        hourPointerFlour = new Region();
        hourPointerFlour.getStyleClass().setAll("hour-pointer-braun-flour");
        if (Clock.Design.BRAUN == getSkinnable().getDesign()) {
            hourPointerFlour.setOpacity(1);
        } else {
            hourPointerFlour.setOpacity(0);
        }
        hourPointerFlour.getTransforms().setAll(hourAngle);

        minutePointer = new Region();
        if (Clock.Design.IOS6 == getSkinnable().getDesign()) {
            minutePointer.getStyleClass().setAll("minute-pointer-ios6");
        } else if (Clock.Design.DB == getSkinnable().getDesign()) {
            minutePointer.getStyleClass().setAll("minute-pointer-db");
        } else if (Clock.Design.BRAUN == getSkinnable().getDesign()) {
            minutePointer.getStyleClass().setAll("minute-pointer-braun");
        } else if (Clock.Design.BOSCH == getSkinnable().getDesign()) {
            minutePointer.getStyleClass().setAll("minute-pointer-bosch");
        }
        minutePointer.getTransforms().setAll(minuteAngle);

        minutePointerFlour = new Region();
        minutePointerFlour.getStyleClass().setAll("minute-pointer-braun-flour");
        if (Clock.Design.BRAUN == getSkinnable().getDesign()) {
            minutePointerFlour.setOpacity(1);
        } else {
            minutePointerFlour.setOpacity(0);
        }
        minutePointerFlour.getTransforms().setAll(minuteAngle);

        DropShadow pointerShadow = new DropShadow();
        pointerShadow.setColor(Color.rgb(0, 0, 0, 0.45));
        pointerShadow.setRadius(12);
        pointerShadow.setBlurType(BlurType.TWO_PASS_BOX);
        pointerShadow.setOffsetY(6);

        pointerGroup = new Group();
        pointerGroup.setEffect(pointerShadow);
        pointerGroup.getChildren().setAll(minutePointerFlour, minutePointer, hourPointerFlour, hourPointer);

        secondPointer = new Region();
        secondPointer.setOpacity(1);
        if (Clock.Design.IOS6 == getSkinnable().getDesign()) {
            secondPointer.getStyleClass().setAll("second-pointer-ios6");
        } else if (Clock.Design.DB == getSkinnable().getDesign()) {
            secondPointer.getStyleClass().setAll("second-pointer-db");
        } else if (Clock.Design.BRAUN == getSkinnable().getDesign()) {
            secondPointer.getStyleClass().setAll("second-pointer-braun");
        } else if (Clock.Design.BOSCH == getSkinnable().getDesign()) {
            secondPointer.setOpacity(0);
        }
        secondPointer.getTransforms().setAll(secondAngle);

        InnerShadow secondPointerInnerShadow = new InnerShadow();
        secondPointerInnerShadow.setColor(Color.rgb(0, 0, 0, 0.3));
        secondPointerInnerShadow.setRadius(1);
        secondPointerInnerShadow.setBlurType(BlurType.TWO_PASS_BOX);
        secondPointerInnerShadow.setOffsetY(-1);

        InnerShadow secondPointerInnerHighlight = new InnerShadow();
        secondPointerInnerHighlight.setColor(Color.rgb(255, 255, 255, 0.3));
        secondPointerInnerHighlight.setRadius(1);
        secondPointerInnerHighlight.setBlurType(BlurType.TWO_PASS_BOX);
        secondPointerInnerHighlight.setOffsetY(1);
        secondPointerInnerHighlight.setInput(secondPointerInnerShadow);

        DropShadow secondPointerShadow = new DropShadow();
        secondPointerShadow.setColor(Color.rgb(0, 0, 0, 0.45));
        secondPointerShadow.setRadius(12);
        secondPointerShadow.setBlurType(BlurType.TWO_PASS_BOX);
        secondPointerShadow.setOffsetY(6);
        secondPointerShadow.setInput(secondPointerInnerHighlight);

        secondPointerGroup = new Group();
        secondPointerGroup.setEffect(secondPointerShadow);
        secondPointerGroup.getChildren().setAll(secondPointer);
        secondPointerGroup.setOpacity(getSkinnable().isSecondPointerVisible() ? 1 : 0);

        centerKnob = new Region();
        if (Clock.Design.IOS6 == getSkinnable().getDesign()) {
            centerKnob.getStyleClass().setAll("center-knob-ios6");
        } else if (Clock.Design.DB == getSkinnable().getDesign()) {
            centerKnob.getStyleClass().setAll("center-knob-db");
        } else if (Clock.Design.BRAUN == getSkinnable().getDesign()) {
            centerKnob.getStyleClass().setAll("center-knob-braun");
        } else if (Clock.Design.BOSCH == getSkinnable().getDesign()) {
            centerKnob.getStyleClass().setAll("center-knob-bosch");
        }

        foreground = new Region();
        if (Clock.Design.IOS6 == getSkinnable().getDesign()) {
            foreground.getStyleClass().setAll("foreground-ios6");
        } else if (Clock.Design.DB == getSkinnable().getDesign()) {
            foreground.getStyleClass().setAll("foreground-db");
        } else if (Clock.Design.BRAUN == getSkinnable().getDesign()) {
            foreground.getStyleClass().setAll("foreground-braun");
        } else if (Clock.Design.BOSCH == getSkinnable().getDesign()) {
            foreground.getStyleClass().setAll("foreground-bosch");
        }
        foreground.setOpacity(getSkinnable().isHighlightVisible() ? 1 : 0);

        pane.getChildren().setAll(background, logoLayer, tickMarkGroup, tickLabelGroup, pointerGroup, secondPointerGroup, centerKnob, foreground);

        getChildren().setAll(pane);

        updateDesign();
    }

    private void registerListeners() {
        getSkinnable().widthProperty().addListener(observable -> handleControlPropertyChanged("RESIZE") );
        getSkinnable().heightProperty().addListener(observable -> handleControlPropertyChanged("RESIZE") );
        getSkinnable().secondPointerVisibleProperty().addListener(observable -> handleControlPropertyChanged("SECOND_POINTER_VISIBLE") );
        getSkinnable().nightModeProperty().addListener(observable -> handleControlPropertyChanged("DESIGN") );
        getSkinnable().designProperty().addListener(observable -> handleControlPropertyChanged("DESIGN") );
        getSkinnable().highlightVisibleProperty().addListener(observable -> handleControlPropertyChanged("DESIGN") );
        getSkinnable().dateTimeProperty().addListener(observable -> handleControlPropertyChanged("DATE_TIME"));
        getSkinnable().runningProperty().addListener(observable -> handleControlPropertyChanged("RUNNING"));
    }


    // ******************** Methods *******************************************
    protected void handleControlPropertyChanged(final String PROPERTY) {
        if ("RESIZE".equals(PROPERTY)) {
            resize();
        } else if ("DESIGN".equals(PROPERTY)) {
            updateDesign();
        } else if ("SECOND_POINTER_VISIBLE".equals(PROPERTY)) {
            secondPointerGroup.setOpacity(getSkinnable().isSecondPointerVisible() ? 1 : 0);
        } else if ("DATE_TIME".equals(PROPERTY)) {
            if (getSkinnable().isRunning()) updateTime(getSkinnable().getDateTime().plus(getSkinnable().getOffset()));    
        } else if ("RUNNING".equals(PROPERTY)) {
            if (getSkinnable().isRunning()) {
                timer.start();
            } else {
                timer.stop();                
            }
        }
    }

    @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 void updateTime(final LocalDateTime DATE_TIME) {                
        // Seconds
        if (getSkinnable().isDiscreteSecond()) {
            secondAngle.setAngle(DATE_TIME.getSecond() * 6);
        } else {
            secondAngle.setAngle(DATE_TIME.getSecond() * 6 + DATE_TIME.get(ChronoField.MILLI_OF_SECOND) * 0.006);
        }
        // Minutes
        minute.set(DATE_TIME.getMinute() * 6);
        // Hours
        minuteAngle.setAngle(DATE_TIME.getHour() * 30 + 0.5 * DATE_TIME.getMinute());
        
        if (getSkinnable().isAutoNightMode()) checkForNight(DATE_TIME);
    }
    
    private void checkForNight(final LocalDateTime DATE_TIME) {
        int hour   = DATE_TIME.getHour();
        int minute = DATE_TIME.getMinute();
        
        if (0 <= hour && minute >= 0 && hour <= 5 && minute <= 59|| 17 <= hour && minute <= 59 && hour <= 23 && minute <= 59) {
            getSkinnable().setNightMode(true);
        } else {
            getSkinnable().setNightMode(false);
        }
    }
    
    private void drawLogoLayer() {
        ctx.clearRect(0, 0, size, size);
        if (Clock.Design.BOSCH == getSkinnable().getDesign()) {
            ctx.setFill(getSkinnable().isNightMode() ? Color.rgb(240, 240, 240) : Color.rgb(10, 10, 10));
            ctx.fillRect(size * 0.5 - 1, size * 0.18, 2, size * 0.27);
            ctx.fillRect(size * 0.5 - 1, size * 0.55, 2, size * 0.27);
            ctx.fillRect(size * 0.18, size * 0.5 - 1, size * 0.27, 2);
            ctx.fillRect(size * 0.55, size * 0.5 - 1, size * 0.27, 2);
        }
        if (getSkinnable().getText().isEmpty()) return;
        ctx.setFill(getSkinnable().isNightMode() ? Color.WHITE : Color.BLACK);
        ctx.setFont(Fonts.opensansSemiBold(size * 0.05));
        ctx.setTextBaseline(VPos.CENTER);
        ctx.setTextAlign(TextAlignment.CENTER);        
        ctx.fillText(getSkinnable().getText(), size * 0.5, size * 0.675, size * 0.8);
    }

    private void updateDesign() {
        // Set day or night mode
        nightDayStyleClass = getSkinnable().isNightMode() ? "night-mode" : "day-mode";
        // Set Styles for each component
        if (Clock.Design.IOS6 == getSkinnable().getDesign()) {
            background.getStyleClass().setAll(nightDayStyleClass, "background-ios6");
            int index = 0;
            for (double angle = 0 ; angle < 360 ; angle += 6) {
                Region tick = ticks.get(index);
                if (angle % 30 == 0) {
                    tick.getStyleClass().setAll(nightDayStyleClass, "major-tick-ios6");
                } else {
                    tick.getStyleClass().setAll(nightDayStyleClass, "minor-tick-ios6");
                }
                ticks.add(tick);
                index++;
            }
            hourPointer.getStyleClass().setAll(nightDayStyleClass, "hour-pointer-ios6");
            minutePointer.getStyleClass().setAll(nightDayStyleClass, "minute-pointer-ios6");
            secondPointer.getStyleClass().setAll(nightDayStyleClass, "second-pointer-ios6");
            centerKnob.getStyleClass().setAll(nightDayStyleClass, "center-knob-ios6");
            foreground.getStyleClass().setAll(nightDayStyleClass, "foreground-ios6");
        } else if (Clock.Design.BRAUN == getSkinnable().getDesign()) {
            nightDayStyleClass = getSkinnable().isNightMode() ? "night-mode-braun" : "day-mode-braun";
            background.getStyleClass().setAll(nightDayStyleClass, "background-braun");
            int index = 0;
            for (double angle = 0 ; angle < 360 ; angle += 6) {
                if (angle % 30 == 0) {
                    ticks.get(index).getStyleClass().setAll(nightDayStyleClass, "major-tick-braun");
                } else {
                    ticks.get(index).getStyleClass().setAll(nightDayStyleClass, "minor-tick-braun");
                }
                index++;
            }
            for (index = 0 ; index < 12 ; index++) {
                tickLabels.get(index).getStyleClass().setAll(nightDayStyleClass, "tick-label-braun");
            }
            hourPointer.getStyleClass().setAll(nightDayStyleClass, "hour-pointer-braun");
            minutePointer.getStyleClass().setAll(nightDayStyleClass, "minute-pointer-braun");
            secondPointer.getStyleClass().setAll(nightDayStyleClass, "second-pointer-braun");
            centerKnob.getStyleClass().setAll(nightDayStyleClass, "center-knob-braun");
            foreground.getStyleClass().setAll(nightDayStyleClass, "foreground-braun");
        } else if (Clock.Design.BOSCH == getSkinnable().getDesign()) {
            nightDayStyleClass = getSkinnable().isNightMode() ? "night-mode-bosch" : "day-mode-bosch";
            background.getStyleClass().setAll(nightDayStyleClass, "background-bosch");
            int index = 0;
            for (double angle = 0 ; angle < 360 ; angle += 6) {
                Region tick = ticks.get(index);
                if (angle % 30 == 0) {
                    tick.getStyleClass().setAll(nightDayStyleClass, "major-tick-bosch");
                } else {
                    tick.getStyleClass().setAll(nightDayStyleClass, "minor-tick-bosch");
                }
                ticks.add(tick);
                index++;
            }
            hourPointer.getStyleClass().setAll(nightDayStyleClass, "hour-pointer-bosch");
            minutePointer.getStyleClass().setAll(nightDayStyleClass, "minute-pointer-bosch");
            secondPointer.getStyleClass().setAll(nightDayStyleClass, "second-pointer-bosch");
            centerKnob.getStyleClass().setAll(nightDayStyleClass, "center-knob-bosch");
            foreground.getStyleClass().setAll(nightDayStyleClass, "foreground-bosch");
        } else {
            background.getStyleClass().setAll(nightDayStyleClass, "background-db");
            int index = 0;
            for (double angle = 0 ; angle < 360 ; angle += 6) {
                Region tick = ticks.get(index);
                if (angle % 30 == 0) {
                    tick.getStyleClass().setAll(nightDayStyleClass, "major-tick-db");
                } else {
                    tick.getStyleClass().setAll(nightDayStyleClass, "minor-tick-db");
                }
                ticks.add(tick);
                index++;
            }
            hourPointer.getStyleClass().setAll(nightDayStyleClass, "hour-pointer-db");
            minutePointer.getStyleClass().setAll(nightDayStyleClass, "minute-pointer-db");
            secondPointer.getStyleClass().setAll(nightDayStyleClass, "second-pointer-db");
            centerKnob.getStyleClass().setAll(nightDayStyleClass, "center-knob-db");
            foreground.getStyleClass().setAll(nightDayStyleClass, "foreground-db");
        }
        tickLabelGroup.setOpacity(Clock.Design.BRAUN == getSkinnable().getDesign() ? 1 : 0);
        foreground.setOpacity(getSkinnable().isHighlightVisible() ? 1 : 0);
        resize();
    }

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

        logoLayer.setWidth(size);
        logoLayer.setHeight(size);

        if (size > 0) {
            background.setPrefSize(size, size);

            // TODO: hourPointer and minutePointer have to be vice versa...wrong scaling here !!!

            if (Clock.Design.IOS6 == getSkinnable().getDesign()) {
                hourPointerWidthFactor    = 0.04;
                hourPointerHeightFactor   = 0.55;
                minutePointerWidthFactor  = 0.04;
                minutePointerHeightFactor = 0.4;
                secondPointerWidthFactor  = 0.075;
                secondPointerHeightFactor = 0.46;
                majorTickWidthFactor      = 0.04;
                majorTickHeightFactor     = 0.12;
                minorTickWidthFactor      = 0.01;
                minorTickHeightFactor     = 0.05;
                majorTickOffset           = 0.018;
                minorTickOffset           = 0.05;
                hourAngle.setPivotX(size * 0.5 * hourPointerWidthFactor);
                hourAngle.setPivotY(size * 0.76 * hourPointerHeightFactor);
                minuteAngle.setPivotX(size * 0.5 * minutePointerWidthFactor);
                minuteAngle.setPivotY(size * 0.66 * minutePointerHeightFactor);
                secondAngle.setPivotX(size * 0.5 * secondPointerWidthFactor);
                secondAngle.setPivotY(size * 0.7341040462 * secondPointerHeightFactor);
            } else if (Clock.Design.BRAUN == getSkinnable().getDesign()) {
                hourPointerWidthFactor    = 0.105;
                hourPointerHeightFactor   = 0.485;
                minutePointerWidthFactor  = 0.105;
                minutePointerHeightFactor = 0.4;
                secondPointerWidthFactor  = 0.09;
                secondPointerHeightFactor = 0.53;
                majorTickWidthFactor      = 0.015;
                majorTickHeightFactor     = 0.045;
                minorTickWidthFactor      = 0.0075;
                minorTickHeightFactor     = 0.0225;
                majorTickOffset           = 0.012;
                minorTickOffset           = 0.02;
                hourAngle.setPivotX(size * 0.5 * hourPointerWidthFactor);
                hourAngle.setPivotY(size * 0.895 * hourPointerHeightFactor);
                minuteAngle.setPivotX(size * 0.5 * minutePointerWidthFactor);
                minuteAngle.setPivotY(size * 0.87 * minutePointerHeightFactor);
                secondAngle.setPivotX(size * 0.5 * secondPointerWidthFactor);
                secondAngle.setPivotY(size * 0.8125 * secondPointerHeightFactor);
            } else if (Clock.Design.BOSCH == getSkinnable().getDesign()) {
                hourPointerWidthFactor    = 0.04;
                hourPointerHeightFactor   = 0.54;  // ToDo: switch with minutePointerHeightFactor
                minutePointerWidthFactor  = 0.04;
                minutePointerHeightFactor = 0.38;  // ToDo: switch with hourPointerHeightFactor
                secondPointerWidthFactor  = 0.09;
                secondPointerHeightFactor = 0.53;
                majorTickWidthFactor      = 0.02;
                majorTickHeightFactor     = 0.145;
                minorTickWidthFactor      = 0.006;
                minorTickHeightFactor     = 0.07;
                majorTickOffset           = 0.005;
                minorTickOffset           = 0.04;
                hourAngle.setPivotX(size * 0.5 * hourPointerWidthFactor);
                hourAngle.setPivotY(size * 0.8240740741 * hourPointerHeightFactor);
                minuteAngle.setPivotX(size * 0.5 * minutePointerWidthFactor);
                minuteAngle.setPivotY(size * 0.75 * minutePointerHeightFactor);
                secondAngle.setPivotX(size * 0.5 * secondPointerWidthFactor);
                secondAngle.setPivotY(size * 0.8125 * secondPointerHeightFactor);
            } else {
                hourPointerWidthFactor    = 0.04;
                hourPointerHeightFactor   = 0.47;
                minutePointerWidthFactor  = 0.055;
                minutePointerHeightFactor = 0.33;
                secondPointerWidthFactor  = 0.1;
                secondPointerHeightFactor = 0.455;
                majorTickWidthFactor      = 0.04;
                majorTickHeightFactor     = 0.12;
                minorTickWidthFactor      = 0.025;
                minorTickHeightFactor     = 0.04;
                majorTickOffset           = 0.018;
                minorTickOffset           = 0.06;
                hourAngle.setPivotX(size * 0.5 * hourPointerWidthFactor);
                hourAngle.setPivotY(size * hourPointerHeightFactor);
                minuteAngle.setPivotX(size * 0.5 * minutePointerWidthFactor);
                minuteAngle.setPivotY(size * minutePointerHeightFactor);
                secondAngle.setPivotX(size * 0.5 * secondPointerWidthFactor);
                secondAngle.setPivotY(size * secondPointerHeightFactor);
            }

            drawLogoLayer();

            double radius = 0.4;
            double sinValue;
            double cosValue;
            int index = 0;
            for (double angle = 0 ; angle < 360 ; angle += 6) {
                sinValue = Math.sin(Math.toRadians(angle));
                cosValue = Math.cos(Math.toRadians(angle));
                Region tick = ticks.get(index);
                if (angle % 30 == 0) {
                    tick.setPrefWidth(size * majorTickWidthFactor);
                    tick.setPrefHeight(size * majorTickHeightFactor);
                    tick.setTranslateX(size * 0.5 + ((size * (radius + majorTickOffset) * sinValue) - (size * (majorTickWidthFactor) * 0.5)));
                    tick.setTranslateY(size * 0.5 + ((size * (radius + majorTickOffset) * cosValue) - (size * (majorTickHeightFactor) * 0.5)));
                } else {
                    tick.setPrefWidth(size * minorTickWidthFactor);
                    tick.setPrefHeight(size * minorTickHeightFactor);
                    tick.setTranslateX(size * 0.5 + ((size * (radius + minorTickOffset) * sinValue) - (size * (minorTickWidthFactor) * 0.5)));
                    tick.setTranslateY(size * 0.5 + ((size * (radius + minorTickOffset) * cosValue) - (size * (minorTickHeightFactor) * 0.5)));
                }
                tick.setRotate(-angle);
                index++;
            }

            if (Clock.Design.BRAUN == getSkinnable().getDesign()) {
                int tickLabelCounter = 0;
                //tickLabelFont = Font.loadFont(getClass().getResourceAsStream("/eu/hansolo/enzo/fonts/helvetica.ttf"), (0.075 * size));
                tickLabelFont = Font.font("Bebas Neue", FontWeight.THIN, FontPosture.REGULAR, 0.09 * size);
                for (double angle = 0 ; angle < 360 ; angle += 30.0) {
                    double x = 0.34 * size * Math.sin(Math.toRadians(150 - angle));
                    double y = 0.34 * size * Math.cos(Math.toRadians(150 - angle));
                    tickLabels.get(tickLabelCounter).setFont(tickLabelFont);
                    tickLabels.get(tickLabelCounter).setX(size * 0.5 + x - tickLabels.get(tickLabelCounter).getLayoutBounds().getWidth() * 0.5);
                    tickLabels.get(tickLabelCounter).setY(size * 0.5 + y);
                    tickLabels.get(tickLabelCounter).setTextOrigin(VPos.CENTER);
                    tickLabels.get(tickLabelCounter).setTextAlignment(TextAlignment.CENTER);
                    tickLabelCounter++;
                }
            }

            hourPointer.setPrefSize(size * hourPointerWidthFactor, size * hourPointerHeightFactor);
            if (Clock.Design.IOS6 == getSkinnable().getDesign()) {
                hourPointer.setTranslateX(size * 0.5 - (hourPointer.getPrefWidth() * 0.5));
                hourPointer.setTranslateY(size * 0.5 - (hourPointer.getPrefHeight()) + (hourPointer.getPrefHeight() * 0.24));
            } else if (Clock.Design.BRAUN == getSkinnable().getDesign()) {
                hourPointer.setTranslateX(size * 0.5 - (hourPointer.getPrefWidth() * 0.5));
                hourPointer.setTranslateY(size * 0.5 - (hourPointer.getPrefHeight()) + (hourPointer.getPrefHeight() * 0.108));
                hourPointerFlour.setPrefSize(size * hourPointerWidthFactor, size * hourPointerHeightFactor);
                hourPointerFlour.setTranslateX(size * 0.5 - (hourPointer.getPrefWidth() * 0.5));
                hourPointerFlour.setTranslateY(size * 0.5 - (hourPointer.getPrefHeight()) + (hourPointer.getPrefHeight() * 0.108));
            } else if (Clock.Design.BOSCH == getSkinnable().getDesign()) {
                hourPointer.setTranslateX(size * 0.5 - (hourPointer.getPrefWidth() * 0.5));
                hourPointer.setTranslateY(size * 0.5 - (hourPointer.getPrefHeight()) + (hourPointer.getPrefHeight() * 0.1759259259));
            } else {
                hourPointer.setTranslateX(size * 0.5 - (hourPointer.getPrefWidth() * 0.5));
                hourPointer.setTranslateY(size * 0.5 - hourPointer.getPrefHeight());
            }

            minutePointer.setPrefSize(size * minutePointerWidthFactor, size * minutePointerHeightFactor);
            if (Clock.Design.IOS6 == getSkinnable().getDesign()) {
                minutePointer.setTranslateX(size * 0.5 - (minutePointer.getPrefWidth() * 0.5));
                minutePointer.setTranslateY(size * 0.5 - (minutePointer.getPrefHeight()) + (minutePointer.getPrefHeight() * 0.34));
            } else if (Clock.Design.BRAUN == getSkinnable().getDesign()) {
                minutePointer.setTranslateX(size * 0.5 - (minutePointer.getPrefWidth() * 0.5));
                minutePointer.setTranslateY(size * 0.5 - (minutePointer.getPrefHeight()) + (minutePointer.getPrefHeight() * 0.128));
                minutePointerFlour.setPrefSize(size * minutePointerWidthFactor, size * minutePointerHeightFactor);
                minutePointerFlour.setTranslateX(size * 0.5 - (minutePointer.getPrefWidth() * 0.5));
                minutePointerFlour.setTranslateY(size * 0.5 - (minutePointer.getPrefHeight()) + (minutePointer.getPrefHeight() * 0.128));
            } else if (Clock.Design.BOSCH == getSkinnable().getDesign()) {
                minutePointer.setTranslateX(size * 0.5 - (minutePointer.getPrefWidth() * 0.5));
                minutePointer.setTranslateY(size * 0.5 - (minutePointer.getPrefHeight()) + (minutePointer.getPrefHeight() * 0.25));
            } else {
                minutePointer.setTranslateX(size * 0.5 - (minutePointer.getPrefWidth() * 0.5));
                minutePointer.setTranslateY(size * 0.5 - minutePointer.getPrefHeight());
            }

            secondPointer.setPrefSize(size * secondPointerWidthFactor, size * secondPointerHeightFactor);
            if (Clock.Design.IOS6 == getSkinnable().getDesign()) {
                secondPointer.setTranslateX(size * 0.5 - (secondPointer.getPrefWidth() * 0.5));
                secondPointer.setTranslateY(size * 0.5 - (secondPointer.getPrefHeight()) + (secondPointer.getPrefHeight() * 0.2658959538));
            } else if (Clock.Design.BRAUN == getSkinnable().getDesign()) {
                secondPointer.setTranslateX(size * 0.5 - (secondPointer.getPrefWidth() * 0.5));
                secondPointer.setTranslateY(size * 0.5 - secondPointer.getPrefHeight() + (secondPointer.getPrefHeight() * 0.189));
            } else {
                secondPointer.setTranslateX(size * 0.5 - (secondPointer.getPrefWidth() * 0.5));
                secondPointer.setTranslateY(size * 0.5 - secondPointer.getPrefHeight());
            }

            if (Clock.Design.IOS6 == getSkinnable().getDesign()) {
                centerKnob.setPrefSize(size * 0.015, size * 0.015);
            } else if (Clock.Design.BRAUN == getSkinnable().getDesign()) {
                centerKnob.setPrefSize(size * 0.085, size * 0.085);
            } else if (Clock.Design.BOSCH == getSkinnable().getDesign()) {
                centerKnob.setPrefSize(size * 0.035, size * 0.035);
            } else {
                centerKnob.setPrefSize(size * 0.1, size * 0.1);
            }
            centerKnob.setTranslateX(size * 0.5 - (centerKnob.getPrefWidth() * 0.5));
            centerKnob.setTranslateY(size * 0.5 - (centerKnob.getPrefHeight() * 0.5));

            foreground.setPrefSize(size * 0.955, size * 0.495);
            foreground.setTranslateX(size * 0.5 - (foreground.getPrefWidth() * 0.5));
            foreground.setTranslateY(size * 0.01);
        }
    }


    // ******************** Drawing related ***********************************
    private void moveMinutePointer(double newAngle) {
        final KeyValue kv = new KeyValue(currentMinuteAngle, newAngle, Interpolator.SPLINE(0.5, 0.4, 0.4, 1.0));
        final KeyFrame kf = new KeyFrame(Duration.millis(200), kv);
        timeline = new Timeline();
        timeline.getKeyFrames().add(kf);
        timeline.play();
    }
}
