/*
 * SPDX-License-Identifier: Apache-2.0
 *
 * Copyright 2016-2021 Gerrit Grunwald.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package eu.hansolo.medusa.skins;

import eu.hansolo.medusa.Fonts;
import eu.hansolo.medusa.Gauge;
import eu.hansolo.medusa.LcdDesign;
import eu.hansolo.medusa.LcdFont;
import eu.hansolo.medusa.Section;
import eu.hansolo.medusa.tools.Helper;
import javafx.beans.InvalidationListener;
import javafx.geometry.Insets;
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.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.BorderWidths;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Paint;
import javafx.scene.paint.Stop;
import javafx.scene.shape.ClosePath;
import javafx.scene.shape.CubicCurveTo;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;

import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

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


/**
 * Created by hansolo on 21.01.16.
 */
public class LcdSkin extends GaugeSkinBase {
    protected static final double              PREFERRED_WIDTH    = 220;//275;
    protected static final double              PREFERRED_HEIGHT   = 100;
    protected static final double              MINIMUM_WIDTH      = 5;
    protected static final double              MINIMUM_HEIGHT     = 5;
    protected static final double              MAXIMUM_WIDTH      = 1024;
    protected static final double              MAXIMUM_HEIGHT     = 1024;
    private static final Color                 DARK_NOISE_COLOR   = Color.rgb(100, 100, 100, 0.10);
    private static final Color                 BRIGHT_NOISE_COLOR = Color.rgb(200, 200, 200, 0.05);
    private static final DropShadow            FOREGROUND_SHADOW  = new DropShadow();
    private static       double                aspectRatio        = 0.45454545;
    private static       Text                  oneSegment         = new Text("8");
    private static       Text                  dotSegment         = new Text(".");
    private              double                width;
    private              double                height;
    private              Pane                  pane;
    private              Paint                 lcdPaint;
    private              Paint                 lcdFramePaint;
    private              ImageView             crystalOverlay;
    private              Image                 crystalImage;
    private              Rectangle             crystalClip;
    private              InnerShadow           mainInnerShadow0;
    private              InnerShadow           mainInnerShadow1;
    private              Path                  threshold;
    private              Path                  average;
    private              Text                  valueText;
    private              Text                  backgroundText;
    private              Text                  unitText;
    private              Text                  title;
    private              Text                  lowerRightText;
    private              Text                  upperLeftText;
    private              Text                  upperRightText;
    private              Text                  lowerCenterText;
    private              double                valueOffsetLeft;
    private              double                valueOffsetRight;
    private              double                digitalFontSizeFactor;
    private              Font                  valueFont;
    private              Font                  unitFont;
    private              Font                  titleFont;
    private              Font                  smallFont;
    private              double                oneSegmentWidth;
    private              double                dotSegmentWidth;
    private              double                widthOfDecimals;
    private              double                availableWidth;
    private              int                   noOfSegments;
    private              StringBuilder         backgroundTextBuilder;
    private              Group                 shadowGroup;
    private              String                valueFormatString;
    private              String                otherFormatString;
    private              Locale                locale;
    private              List<Section>         sections;
    private              Map<Section, Color[]> sectionColorMap;
    private              InvalidationListener  currentValueListener;


    // ******************** Constructors **************************************
    public LcdSkin(Gauge gauge) {
        super(gauge);
        width                 = PREFERRED_WIDTH;
        height                = PREFERRED_HEIGHT;
        valueOffsetLeft       = 0.0;
        valueOffsetRight      = 0.0;
        digitalFontSizeFactor = 1.0;
        backgroundTextBuilder = new StringBuilder();
        valueFormatString     = new StringBuilder("%.").append(gauge.getDecimals()).append("f").toString();
        otherFormatString     = new StringBuilder("%.").append(gauge.getTickLabelDecimals()).append("f").toString();
        locale                = gauge.getLocale();
        sections              = gauge.getSections();
        sectionColorMap       = new HashMap<>(sections.size());
        currentValueListener  = o -> handleEvents("REDRAW");
        updateSectionColors();
        FOREGROUND_SHADOW.setOffsetX(0);
        FOREGROUND_SHADOW.setOffsetY(1);
        FOREGROUND_SHADOW.setColor(Color.rgb(0, 0, 0, 0.5));
        FOREGROUND_SHADOW.setBlurType(BlurType.TWO_PASS_BOX);
        FOREGROUND_SHADOW.setRadius(2);

        initGraphics();
        registerListeners();
    }


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

        mainInnerShadow0 = new InnerShadow();
        mainInnerShadow0.setOffsetX(0.0);
        mainInnerShadow0.setOffsetY(0.0);
        mainInnerShadow0.setRadius(0.0625 * PREFERRED_HEIGHT);
        mainInnerShadow0.setColor(Color.rgb(255, 255, 255, 0.5));
        mainInnerShadow0.setBlurType(BlurType.TWO_PASS_BOX);

        mainInnerShadow1 = new InnerShadow();
        mainInnerShadow1.setOffsetX(0.0);
        mainInnerShadow1.setOffsetY(1.0);
        mainInnerShadow1.setRadius(0.04166667 * PREFERRED_HEIGHT);
        mainInnerShadow1.setColor(Color.rgb(0, 0, 0, 0.65));
        mainInnerShadow1.setBlurType(BlurType.TWO_PASS_BOX);
        mainInnerShadow1.setInput(mainInnerShadow0);

        crystalClip = new Rectangle(0, 0, width, height);
        crystalClip.setArcWidth(5);
        crystalClip.setArcHeight(5);

        crystalImage   = Helper.createNoiseImage(PREFERRED_WIDTH, PREFERRED_HEIGHT, DARK_NOISE_COLOR, BRIGHT_NOISE_COLOR, 8);
        crystalOverlay = new ImageView(crystalImage);
        crystalOverlay.setClip(crystalClip);
        boolean crystalEnabled = gauge.isLcdCrystalEnabled();
        Helper.enableNode(crystalOverlay, crystalEnabled);

        threshold = new Path();
        threshold.setStroke(null);
        Helper.enableNode(threshold, gauge.isThresholdVisible());

        average = new Path();
        average.setStroke(null);
        Helper.enableNode(average, gauge.isAverageVisible());

        backgroundText = new Text(String.format(locale, valueFormatString, gauge.getCurrentValue()));
        backgroundText.setFill(gauge.getLcdDesign().lcdBackgroundColor);
        backgroundText.setOpacity((LcdFont.LCD == gauge.getLcdFont() || LcdFont.ELEKTRA == gauge.getLcdFont()) ? 1 : 0);

        valueText = new Text(String.format(locale, valueFormatString, gauge.getCurrentValue()));
        valueText.setFill(gauge.getLcdDesign().lcdForegroundColor);

        unitText = new Text(gauge.getUnit());
        unitText.setFill(gauge.getLcdDesign().lcdForegroundColor);
        Helper.enableNode(unitText, !gauge.getUnit().isEmpty());

        title = new Text(gauge.getTitle());
        title.setFill(gauge.getLcdDesign().lcdForegroundColor);
        Helper.enableNode(title, !gauge.getTitle().isEmpty());

        lowerRightText = new Text(gauge.getSubTitle());
        lowerRightText.setFill(gauge.getLcdDesign().lcdForegroundColor);
        Helper.enableNode(lowerRightText, !gauge.getSubTitle().isEmpty());

        upperLeftText = new Text(String.format(locale, otherFormatString, gauge.getMinMeasuredValue()));
        upperLeftText.setFill(gauge.getLcdDesign().lcdForegroundColor);
        Helper.enableNode(upperLeftText, gauge.isMinMeasuredValueVisible());

        upperRightText = new Text(String.format(locale, otherFormatString, gauge.getMaxMeasuredValue()));
        upperRightText.setFill(gauge.getLcdDesign().lcdForegroundColor);
        Helper.enableNode(upperRightText, gauge.isMaxMeasuredValueVisible());

        lowerCenterText = new Text(String.format(locale, otherFormatString, gauge.getOldValue()));
        lowerCenterText.setFill(gauge.getLcdDesign().lcdForegroundColor);
        Helper.enableNode(lowerCenterText, gauge.isOldValueVisible());

        shadowGroup = new Group();
        shadowGroup.setEffect(gauge.isShadowsEnabled() ? FOREGROUND_SHADOW : null);
        shadowGroup.getChildren().setAll(threshold,
                                         average,
                                         valueText,
                                         unitText,
                                         title,
                                         lowerRightText,
                                         upperLeftText,
                                         upperRightText,
                                         lowerCenterText);

        pane = new Pane(crystalOverlay, backgroundText, shadowGroup);
        pane.setEffect(gauge.isShadowsEnabled() ? mainInnerShadow1 : null);
        getChildren().setAll(pane);
    }

    @Override protected void registerListeners() {
        super.registerListeners();
        gauge.currentValueProperty().addListener(currentValueListener);
    }


    // ******************** Methods *******************************************
    @Override protected void handleEvents(final String EVENT_TYPE) {
        if ("REDRAW".equals(EVENT_TYPE)) {
            pane.setEffect(gauge.isShadowsEnabled() ? mainInnerShadow1 : null);
            shadowGroup.setEffect(gauge.isShadowsEnabled() ? FOREGROUND_SHADOW : null);
            updateLcdDesign(height);
            redraw();
        } else if ("RESIZE".equals(EVENT_TYPE)) {
            aspectRatio = gauge.getPrefHeight() / gauge.getPrefWidth();
            resize();
            redraw();
        } else if ("LCD".equals(EVENT_TYPE)) {
            updateLcdDesign(height);
        } else if ("VISIBILITY".equals(EVENT_TYPE)) {
            Helper.enableNode(crystalOverlay, gauge.isLcdCrystalEnabled());
            Helper.enableNode(title, !gauge.getTitle().isEmpty());
            Helper.enableNode(unitText, !gauge.getUnit().isEmpty());
            Helper.enableNode(upperLeftText, gauge.isMinMeasuredValueVisible());
            Helper.enableNode(upperRightText, gauge.isMaxMeasuredValueVisible());
            Helper.enableNode(lowerRightText, !gauge.getSubTitle().isEmpty());
            Helper.enableNode(lowerCenterText, gauge.isOldValueVisible());
            Helper.enableNode(average, gauge.isAverageVisible());
            Helper.enableNode(threshold, gauge.isThresholdVisible());
            resize();
            redraw();
        } else if ("SECTION".equals(EVENT_TYPE)) {
            sections = gauge.getSections();
            updateSectionColors();
            resize();
            redraw();
        }
    }

    @Override public void dispose() {
        gauge.currentValueProperty().removeListener(currentValueListener);
            super.dispose();
    }


    // ******************** Private Methods ***********************************
    private boolean isNoOfDigitsInvalid() {
        final double AVAILABLE_WIDTH = width - 2 - valueOffsetLeft - valueOffsetRight;
        final double NEEDED_WIDTH    = valueText.getLayoutBounds().getWidth();
        return Double.compare(AVAILABLE_WIDTH, NEEDED_WIDTH) < 0;
    }

    private void updateLcdDesign(final double HEIGHT) {
        LcdDesign lcdDesign = gauge.getLcdDesign();
        Color[]   lcdColors = lcdDesign.getColors();

        if (LcdDesign.SECTIONS == lcdDesign) {
            double currentValue = gauge.getCurrentValue();
            int listSize = sections.size();
            for (int i = 0 ; i < listSize ; i++) {
                Section section = sections.get(i);
                if (section.contains(currentValue)) {
                    lcdColors = sectionColorMap.get(section);
                    break;
                }
            }
        }

        lcdPaint = new LinearGradient(0, 1, 0, HEIGHT - 1,
                                      false, CycleMethod.NO_CYCLE,
                                      new Stop(0, lcdColors[0]),
                                      new Stop(0.03, lcdColors[1]),
                                      new Stop(0.5, lcdColors[2]),
                                      new Stop(0.5, lcdColors[3]),
                                      new Stop(1.0, lcdColors[4]));
        if (lcdDesign.name().startsWith("FLAT")) {
            lcdFramePaint = gauge.getBorderPaint();

            lcdPaint      = gauge.getBackgroundPaint();

            Color lcdForegroundColor = (Color) gauge.getForegroundPaint();
            backgroundText.setFill(Color.color(lcdForegroundColor.getRed(), lcdForegroundColor.getGreen(), lcdForegroundColor.getBlue(), 0.1));
            valueText.setFill(lcdForegroundColor);
            upperLeftText.setFill(lcdForegroundColor);
            title.setFill(lcdForegroundColor);
            upperRightText.setFill(lcdForegroundColor);
            unitText.setFill(lcdForegroundColor);
            lowerRightText.setFill(lcdForegroundColor);
            lowerCenterText.setFill(lcdForegroundColor);
            threshold.setFill(lcdForegroundColor);
            average.setFill(lcdForegroundColor);
        } else {
            lcdFramePaint = new LinearGradient(0, 0.02083333 * height, 0, HEIGHT - 0.02083333 * HEIGHT,
                                               false, CycleMethod.NO_CYCLE,
                                               new Stop(0.0, Color.rgb(26, 26, 26)),
                                               new Stop(0.015, Color.rgb(77, 77, 77)),
                                               new Stop(0.985, Color.rgb(77, 77, 77)),
                                               new Stop(1.0, Color.rgb(221, 221, 221)));

            lcdPaint = new LinearGradient(0, 1, 0, HEIGHT - 1,
                                          false, CycleMethod.NO_CYCLE,
                                          new Stop(0, lcdColors[0]),
                                          new Stop(0.03, lcdColors[1]),
                                          new Stop(0.5, lcdColors[2]),
                                          new Stop(0.5, lcdColors[3]),
                                          new Stop(1.0, lcdColors[4]));

            backgroundText.setFill(lcdDesign.lcdBackgroundColor);
            valueText.setFill(lcdDesign.lcdForegroundColor);
            upperLeftText.setFill(lcdDesign.lcdForegroundColor);
            title.setFill(lcdDesign.lcdForegroundColor);
            upperRightText.setFill(lcdDesign.lcdForegroundColor);
            unitText.setFill(lcdDesign.lcdForegroundColor);
            lowerRightText.setFill(lcdDesign.lcdForegroundColor);
            lowerCenterText.setFill(lcdDesign.lcdForegroundColor);
            threshold.setFill(lcdDesign.lcdForegroundColor);
            average.setFill(lcdDesign.lcdForegroundColor);
        }

        pane.setBackground(new Background(new BackgroundFill(lcdPaint, new CornerRadii(0.10416667 * HEIGHT), Insets.EMPTY)));
        pane.setBorder(new Border(new BorderStroke(lcdFramePaint, BorderStrokeStyle.SOLID, new CornerRadii(0.05 * HEIGHT), new BorderWidths(0.02083333 * HEIGHT))));
    }

    private void updateSectionColors() {
        int listSize = sections.size();
        sectionColorMap.clear();
        for (int i = 0 ; i < listSize ; i++) {
            Color sectionColor = sections.get(i).getColor();
            Color lcdForegroundColor;
            if (Helper.isMonochrome(sectionColor)) {
                lcdForegroundColor = Helper.isDark(sectionColor) ? Color.WHITE : Color.BLACK;
            } else {
                lcdForegroundColor = Color.hsb(sectionColor.getHue(), sectionColor.getSaturation(), sectionColor.getBrightness() * 0.3);
            }
            Color lcdBackgroundColor = Color.color(sectionColor.getRed(), sectionColor.getGreen(), sectionColor.getBlue(), 0.1);
            sectionColorMap.put(sections.get(i), getSectionColors(lcdBackgroundColor, lcdForegroundColor));
        }
    }

    private Color[] getSectionColors(final Color LCD_BACKGROUND_COLOR, final Color LCD_FOREGROUND_COLOR) {
        double hue = LCD_BACKGROUND_COLOR.getHue();
        double sat = LCD_BACKGROUND_COLOR.getSaturation();

        Color[] colors;
        if (Helper.isMonochrome(LCD_BACKGROUND_COLOR)) {
            // Section color is monochrome
            colors = new Color[]{
                Color.hsb(hue, 0, 0.69),
                Color.hsb(hue, 0, 1.0),
                Color.hsb(hue, 0, 0.76),
                Color.hsb(hue, 0, 0.76),
                Color.hsb(hue, sat, 0.69),
                Helper.isDark(LCD_BACKGROUND_COLOR) ? Color.WHITE : Color.BLACK,
                Helper.isDark(LCD_BACKGROUND_COLOR) ? Color.rgb(255, 255, 255, 0.1) : Color.rgb(0, 0, 0, 0.1)
            };
        } else {
            // Section color is not monochrome
            colors = new Color[]{
                Color.hsb(hue, sat, 0.69),
                Color.hsb(hue, sat, 1.0),
                Color.hsb(hue, sat, 0.76),
                Color.hsb(hue, sat, 0.76),
                Color.hsb(hue, sat, 0.69),
                LCD_FOREGROUND_COLOR,
                Color.color(LCD_BACKGROUND_COLOR.getRed(), LCD_BACKGROUND_COLOR.getGreen(), LCD_BACKGROUND_COLOR.getBlue(), 0.1)
            };
        }
        return colors;
    }
    
    private void updateFonts() {
        digitalFontSizeFactor = 1.0;
        switch(gauge.getLcdFont()) {
            case LCD:
                valueFont = Fonts.digital(0.6 * height);
                digitalFontSizeFactor = 1.4;
                break;
            case DIGITAL:
                valueFont = Fonts.digitalReadout(0.5833333333 * height);
                break;
            case DIGITAL_BOLD:
                valueFont = Fonts.digitalReadoutBold(0.5833333333 * height);
                break;
            case ELEKTRA:
                valueFont = Fonts.elektra(0.62 * height);
                break;
            case STANDARD:
            default:
                valueFont = Fonts.robotoMedium(0.5 * height);
                break;
        }
        backgroundText.setFont(valueFont);
        backgroundText.setOpacity((LcdFont.LCD == gauge.getLcdFont() ||
                                   LcdFont.DIGITAL == gauge.getLcdFont() ||
                                   LcdFont.DIGITAL_BOLD == gauge.getLcdFont() ||
                                   LcdFont.ELEKTRA == gauge.getLcdFont()) ? 1 : 0);
        valueText.setFont(valueFont);
        unitFont  = Fonts.latoBold(0.26 * height);
        titleFont = Fonts.latoBold(0.1666666667 * height);
        smallFont = Fonts.latoBold(0.1666666667 * height);
    }

    private void updateBackgroundText() {
        // Setup the semitransparent background text
        backgroundText.setCache(false);
        backgroundText.setTextOrigin(VPos.BASELINE);
        backgroundText.setTextAlignment(TextAlignment.RIGHT);

        // Setup the semitransparent background text
        // Width of one segment
        oneSegment.setFont(valueFont);
        dotSegment.setText(".");
        if (LcdFont.LCD == gauge.getLcdFont()) {
            oneSegment.setText("8");
        } else if (LcdFont.DIGITAL == gauge.getLcdFont()) {
            oneSegment.setText("_");
        } else if (LcdFont.DIGITAL_BOLD == gauge.getLcdFont()) {
            oneSegment.setText("_");
        } else if (LcdFont.ELEKTRA == gauge.getLcdFont()) {
            oneSegment.setText("_");
        }
        oneSegmentWidth = oneSegment.getLayoutBounds().getWidth();
        dotSegmentWidth = dotSegment.getLayoutBounds().getWidth();
        
        // Width of decimals
        widthOfDecimals = 0 == gauge.getDecimals() ? 0 : gauge.getDecimals() * oneSegmentWidth + (LcdFont.LCD == gauge.getLcdFont() ? oneSegmentWidth : dotSegmentWidth);

        // Available width
        availableWidth = width - (0.0151515152 * width) - (0.0416666667 * height) - 2 - valueOffsetRight - widthOfDecimals;

        // Number of segments
        noOfSegments = (int) Math.floor(availableWidth / oneSegmentWidth);

        // Add segments to background text
        backgroundTextBuilder.setLength(0);
        for (int i = 0 ; i < gauge.getDecimals() ; i++) {
            backgroundTextBuilder.append(oneSegment.getText());
        }

        if (gauge.getDecimals() != 0) {
            backgroundTextBuilder.insert(0, ".");
        }

        for (int i = 0 ; i < noOfSegments ; i++) {
            backgroundTextBuilder.insert(0, oneSegment.getText());
        }
        backgroundText.setText(backgroundTextBuilder.toString());
        
        backgroundText.setCache(true);
        backgroundText.setCacheHint(CacheHint.SCALE);
    }

    @Override protected void resize() {
        width  = gauge.getWidth() - gauge.getInsets().getLeft() - gauge.getInsets().getRight();
        height = gauge.getHeight() - gauge.getInsets().getTop() - gauge.getInsets().getBottom();

        if (aspectRatio * width > height) {
            width = 1 / (aspectRatio / height);
        } else if (1 / (aspectRatio / height) > width) {
            height = aspectRatio * width;
        }

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

            updateLcdDesign(height);

            mainInnerShadow0.setRadius(0.0625 * height);
            mainInnerShadow1.setRadius(0.04166667 * height);

            if (crystalOverlay.isVisible()) {
                double frameWidth = 0.02083333 * height;
                double imgWidth   = width - (2 * frameWidth);
                double imgHeight  = height - (2 * frameWidth);

                crystalClip.setWidth(imgWidth);
                crystalClip.setHeight(imgHeight);
                crystalOverlay.setImage(Helper.createNoiseImage(width, height, DARK_NOISE_COLOR, BRIGHT_NOISE_COLOR, 8));
                crystalOverlay.setCache(true);
                crystalOverlay.relocate(frameWidth, frameWidth);
            }

            double tSize = 0.2 * height;
            threshold.getElements().clear();
            threshold.getElements().add(new MoveTo(0.41666667 * tSize, 0.75 * tSize));
            threshold.getElements().add(new LineTo(0.583333333333333 * tSize, 0.75 * tSize));
            threshold.getElements().add(new LineTo(0.583333333333333 * tSize, 0.916666666666667 * tSize));
            threshold.getElements().add(new LineTo(0.416666666666667 * tSize, 0.916666666666667 * tSize));
            threshold.getElements().add(new LineTo(0.416666666666667 * tSize, 0.75 * tSize));
            threshold.getElements().add(new ClosePath());
            threshold.getElements().add(new MoveTo(0.416666666666667 * tSize, 0.333333333333333 * tSize));
            threshold.getElements().add(new LineTo(0.583333333333333 * tSize, 0.333333333333333 * tSize));
            threshold.getElements().add(new LineTo(0.583333333333333 * tSize, 0.666666666666667 * tSize));
            threshold.getElements().add(new LineTo(0.416666666666667 * tSize, 0.666666666666667 * tSize));
            threshold.getElements().add(new LineTo(0.416666666666667 * tSize, 0.333333333333333 * tSize));
            threshold.getElements().add(new ClosePath());
            threshold.getElements().add(new MoveTo(tSize, tSize));
            threshold.getElements().add(new LineTo(0.5 * tSize, 0));
            threshold.getElements().add(new LineTo(0, tSize));
            threshold.getElements().add(new LineTo(tSize, tSize));
            threshold.getElements().add(new ClosePath());
            threshold.relocate(0.027961994662429348 * width, 0.75 * height);

            double aSize = 0.12 * height;
            average.getElements().clear();
            average.getElements().add(new MoveTo(0.5 * aSize, 0.0));
            average.getElements().add(new CubicCurveTo(0.22727272727272727 * aSize, 0.0,
                                                       0.022727272727272728 * aSize, 0.22727272727272727 * aSize,
                                                       0.022727272727272728 * aSize, 0.5 * aSize));
            average.getElements().add(new CubicCurveTo(0.022727272727272728 * aSize, 0.6136363636363636 * aSize,
                                                       0.045454545454545456 * aSize, 0.7045454545454546 * aSize,
                                                       0.11363636363636363 * aSize, 0.7954545454545454 * aSize));
            average.getElements().add(new CubicCurveTo(0.11363636363636363 * aSize, 0.7954545454545454 * aSize,
                                                       0.0, 0.8863636363636364 * aSize,
                                                       0.0, 0.8863636363636364 * aSize));
            average.getElements().add(new LineTo(0.09090909090909091 * aSize, 0.9772727272727273 * aSize));
            average.getElements().add(new CubicCurveTo(0.09090909090909091 * aSize, 0.9772727272727273 * aSize,
                                                       0.18181818181818182 * aSize, 0.8863636363636364 * aSize,
                                                       0.18181818181818182 * aSize, 0.8863636363636364 * aSize));
            average.getElements().add(new CubicCurveTo(0.2727272727272727 * aSize, 0.9545454545454546 * aSize,
                                                       0.38636363636363635 * aSize, aSize,
                                                       0.5 * aSize, aSize));
            average.getElements().add(new CubicCurveTo(0.7727272727272727 * aSize, aSize,
                                                       0.9772727272727273 * aSize, 0.7727272727272727 * aSize,
                                                       0.9772727272727273 * aSize, 0.5 * aSize));
            average.getElements().add(new CubicCurveTo(0.9772727272727273 * aSize, 0.38636363636363635 * aSize,
                                                       0.9545454545454546 * aSize, 0.29545454545454547 * aSize,
                                                       0.8863636363636364 * aSize, 0.20454545454545456 * aSize));
            average.getElements().add(new CubicCurveTo(0.8863636363636364 * aSize, 0.20454545454545456 * aSize,
                                                       aSize, 0.09090909090909091 * aSize,
                                                       aSize, 0.09090909090909091 * aSize));
            average.getElements().add(new LineTo(0.9090909090909091 * aSize, 0.0));
            average.getElements().add(new CubicCurveTo(0.9090909090909091 * aSize, 0.0,
                                                       0.7954545454545454 * aSize, 0.09090909090909091 * aSize,
                                                       0.7954545454545454 * aSize, 0.09090909090909091 * aSize));
            average.getElements().add(new CubicCurveTo(0.7045454545454546 * aSize, 0.045454545454545456 * aSize,
                                                       0.6136363636363636 * aSize, 0.0,
                                                       0.5 * aSize, 0.0));
            average.getElements().add(new ClosePath());
            average.getElements().add(new MoveTo(0.7954545454545454 * aSize, 0.29545454545454547 * aSize));
            average.getElements().add(new CubicCurveTo(0.8181818181818182 * aSize, 0.36363636363636365 * aSize,
                                                       0.8409090909090909 * aSize, 0.4318181818181818 * aSize,
                                                       0.8409090909090909 * aSize, 0.5 * aSize));
            average.getElements().add(new CubicCurveTo(0.8409090909090909 * aSize, 0.7045454545454546 * aSize,
                                                       0.7045454545454546 * aSize, 0.8863636363636364 * aSize,
                                                       0.5 * aSize, 0.8863636363636364 * aSize));
            average.getElements().add(new CubicCurveTo(0.4090909090909091 * aSize, 0.8863636363636364 * aSize,
                                                       0.3409090909090909 * aSize, 0.8636363636363636 * aSize,
                                                       0.2727272727272727 * aSize, 0.7954545454545454 * aSize));
            average.getElements().add(new CubicCurveTo(0.2727272727272727 * aSize, 0.7954545454545454 * aSize,
                                                       0.7954545454545454 * aSize, 0.29545454545454547 * aSize,
                                                       0.7954545454545454 * aSize, 0.29545454545454547 * aSize));
            average.getElements().add(new ClosePath());
            average.getElements().add(new MoveTo(0.5 * aSize, 0.11363636363636363 * aSize));
            average.getElements().add(new CubicCurveTo(0.5909090909090909 * aSize, 0.11363636363636363 * aSize,
                                                       0.6590909090909091 * aSize, 0.13636363636363635 * aSize,
                                                       0.7045454545454546 * aSize, 0.18181818181818182 * aSize));
            average.getElements().add(new CubicCurveTo(0.7045454545454546 * aSize, 0.18181818181818182 * aSize,
                                                       0.20454545454545456 * aSize, 0.6818181818181818 * aSize,
                                                       0.20454545454545456 * aSize, 0.6818181818181818 * aSize));
            average.getElements().add(new CubicCurveTo(0.18181818181818182 * aSize, 0.6363636363636364 * aSize,
                                                       0.1590909090909091 * aSize, 0.5681818181818182 * aSize,
                                                       0.1590909090909091 * aSize, 0.5 * aSize));
            average.getElements().add(new CubicCurveTo(0.1590909090909091 * aSize, 0.29545454545454547 * aSize,
                                                       0.29545454545454547 * aSize, 0.11363636363636363 * aSize,
                                                       0.5 * aSize, 0.11363636363636363 * aSize));
            average.getElements().add(new ClosePath());
            average.relocate(0.32 * width, 0.82 * height);

            updateFonts();

            // Setup the lcd unit
            unitText.setFont(unitFont);
            unitText.setTextOrigin(VPos.BASELINE);
            unitText.setTextAlignment(TextAlignment.RIGHT);

            unitText.setText(gauge.getUnit());
            if (unitText.visibleProperty().isBound()) {
                unitText.visibleProperty().unbind();
            }

            valueOffsetLeft = height * 0.04;

            if (gauge.getUnit().isEmpty()) {
                valueOffsetRight = height * 0.0833333333;
                valueText.setX((width - valueText.getLayoutBounds().getWidth()) - valueOffsetRight);
            } else {
                unitText.setX((width - unitText.getLayoutBounds().getWidth()) - height * 0.04);
                unitText.setY(height - (valueText.getLayoutBounds().getHeight() * digitalFontSizeFactor) * 0.5);
                valueOffsetRight = (unitText.getLayoutBounds().getWidth() + height * 0.0833333333); // distance between value and unit
                valueText.setX(width - 2 - valueText.getLayoutBounds().getWidth() - valueOffsetRight);
            }
            valueText.setY(height - (valueText.getLayoutBounds().getHeight() * digitalFontSizeFactor) * 0.5);

            // Visualize the lcd semitransparent background text
            updateBackgroundText();

            if (gauge.getUnit().isEmpty()) {
                backgroundText.setX((width - backgroundText.getLayoutBounds().getWidth()) - valueOffsetRight);
            } else {
                backgroundText.setX(width - 2 - backgroundText.getLayoutBounds().getWidth() - valueOffsetRight);
            }
            backgroundText.setY(height - (backgroundText.getLayoutBounds().getHeight() * digitalFontSizeFactor) * 0.5);

            // Setup the font for the lcd title, number system, min measured, max measure and former value
            // Title
            title.setFont(titleFont);
            title.setTextOrigin(VPos.BASELINE);
            title.setTextAlignment(TextAlignment.CENTER);
            title.setText(gauge.getTitle());
            title.setX((width - title.getLayoutBounds().getWidth()) * 0.5);
            title.setY(height * 0.18);

            // Lower Right Text
            lowerRightText.setFont(smallFont);
            lowerRightText.setTextOrigin(VPos.BASELINE);
            lowerRightText.setTextAlignment(TextAlignment.RIGHT);
            lowerRightText.setText(gauge.getSubTitle());
            lowerRightText.setX(pane.getLayoutBounds().getMinX() + (pane.getLayoutBounds().getWidth() - lowerRightText.getLayoutBounds().getWidth()) * 0.5);
            lowerRightText.setY(height * 0.94);

            // Min measured value
            upperLeftText.setFont(smallFont);
            upperLeftText.setTextOrigin(VPos.BASELINE);
            upperLeftText.setTextAlignment(TextAlignment.RIGHT);
            upperLeftText.setX(pane.getLayoutBounds().getMinX() + 0.0416666667 * height);
            upperLeftText.setY(height * 0.18);

            // Max measured value
            upperRightText.setFont(smallFont);
            upperRightText.setTextOrigin(VPos.BASELINE);
            upperRightText.setTextAlignment(TextAlignment.RIGHT);
            upperRightText.setY(height * 0.18);

            // Lower Center Text
            lowerCenterText.setFont(smallFont);
            lowerCenterText.setTextOrigin(VPos.BASELINE);
            lowerCenterText.setTextAlignment(TextAlignment.CENTER);
            lowerCenterText.setX((width - lowerCenterText.getLayoutBounds().getWidth()) * 0.5);
            lowerCenterText.setY(height * 0.94);
        }
    }

    @Override protected void redraw() {
        locale            = gauge.getLocale();
        valueFormatString = new StringBuilder("%.").append(gauge.getDecimals()).append("f").toString();
        otherFormatString = new StringBuilder("%.").append(gauge.getTickLabelDecimals()).append("f").toString();

        if (gauge.isThresholdVisible()) { threshold.setVisible(Double.compare(gauge.getCurrentValue(), gauge.getThreshold()) >= 0); }

        //valueText.setText(isNoOfDigitsInvalid() ? "-E-" : formatNumber(gauge.getLocale(), gauge.getFormatString(), gauge.getDecimals(), gauge.getCurrentValue()));
        valueText.setText(isNoOfDigitsInvalid() ? "-E-" : String.format(Locale.US, valueFormatString, gauge.getCurrentValue()));

        updateBackgroundText();

        // Visualize the lcd semitransparent background text
        if (gauge.getUnit().isEmpty()) {
            backgroundText.setX((width - backgroundText.getLayoutBounds().getWidth()) - valueOffsetRight);
        } else {
            backgroundText.setX(width - 2 - backgroundText.getLayoutBounds().getWidth() - valueOffsetRight);
        }
        backgroundText.setY(height - (backgroundText.getLayoutBounds().getHeight() * digitalFontSizeFactor) * 0.5);

        if (gauge.getUnit().isEmpty()) {
            valueText.setX((width - valueText.getLayoutBounds().getWidth()) - valueOffsetRight);
        } else {
            valueText.setX((width - 2 - valueText.getLayoutBounds().getWidth()) - valueOffsetRight);
        }

        // Update the title
        title.setText(gauge.getTitle());
        title.setX((width - title.getLayoutBounds().getWidth()) * 0.5);

        // Update the upper left text
        upperLeftText.setText(String.format(locale, otherFormatString, gauge.getMinMeasuredValue()));
        if (upperLeftText.getX() + upperLeftText.getLayoutBounds().getWidth() > title.getX()) {
            upperLeftText.setText("...");
        }

        // Update the upper right text
        upperRightText.setText(String.format(locale, otherFormatString, gauge.getMaxMeasuredValue()));
        upperRightText.setX(width - upperRightText.getLayoutBounds().getWidth() - 0.0416666667 * height);
        if (upperRightText.getX() < title.getX() + title.getLayoutBounds().getWidth()) {
            upperRightText.setText("...");
            upperRightText.setX(width - upperRightText.getLayoutBounds().getWidth() - 0.0416666667 * height);
        }

        // Update the lower center text
        if (gauge.isAverageVisible()) {
            lowerCenterText.setText(String.format(locale, otherFormatString, gauge.getAverage()));
        } else {
            lowerCenterText.setText(String.format(locale, otherFormatString, gauge.getOldValue()));
        }
        lowerCenterText.setX((width - lowerCenterText.getLayoutBounds().getWidth()) * 0.5);
        lowerCenterText.setY(0.94 * height);

        average.relocate(lowerCenterText.getX() - 0.2 * height, 0.82 * height);

        // Update the lower right text
        lowerRightText.setText(gauge.getSubTitle());
        lowerRightText.setX(width - lowerRightText.getLayoutBounds().getWidth() - 0.0416666667 * height);
        lowerRightText.setY(height * 0.94);
        if (lowerRightText.getX() < lowerCenterText.getX() + lowerCenterText.getLayoutBounds().getWidth()) {
            lowerRightText.setText("...");
            lowerRightText.setX(width - lowerRightText.getLayoutBounds().getWidth() - 0.0416666667 * height);
        }
    }
}
