/*
 * Copyright (c) 2017 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.fx.charts;

import eu.hansolo.fx.charts.data.PlotItem;
import eu.hansolo.fx.charts.event.ItemEventListener;
import eu.hansolo.fx.charts.font.Fonts;
import eu.hansolo.fx.charts.tools.Helper;
import eu.hansolo.fx.charts.tools.Point;
import eu.hansolo.fx.geometry.Path;
import javafx.beans.DefaultProperty;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.BooleanPropertyBase;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.DoublePropertyBase;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.IntegerPropertyBase;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.shape.ArcType;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;

import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;


/**
 * User: hansolo
 * Date: 17.11.17
 * Time: 10:41
 */
@DefaultProperty("children")
public class CircularPlot extends Region {
    private static final double                       PREFERRED_WIDTH            = 500;
    private static final double                       PREFERRED_HEIGHT           = 500;
    private static final double                       MINIMUM_WIDTH              = 50;
    private static final double                       MINIMUM_HEIGHT             = 50;
    private static final double                       MAXIMUM_WIDTH              = 4096;
    private static final double                       MAXIMUM_HEIGHT             = 4096;
    private static final double                       DEFAULT_SEGMENT_GAP        = 4;
    private static final double                       DEFAULT_CONNECTION_OPACITY = 0.65;
    private static final double                       MAJOR_TICK_MARK_LENGTH     = 0.0125;
    private static final double                       MEDIUM_TICK_MARK_LENGTH    = 0.01;
    private static final double                       MINOR_TICK_MARK_LENGTH     = 0.0075;
    private static final double                       TICK_MARK_WIDTH            = 0.001;
    private static final double                       ANGLE_OFFSET               = 90;
    private              double                       size;
    private              double                       width;
    private              double                       height;
    private              Canvas                       canvas;
    private              GraphicsContext              ctx;
    private              double                       mainLineWidth;
    private              double                       outgoingLineWidth;
    private              double                       tickMarkWidth;
    private              double                       chartSize;
    private              double                       chartOffset;
    private              double                       innerChartSize;
    private              double                       innerChartOffset;
    private              double                       centerX;
    private              double                       centerY;
    private              Color                        _tickMarkColor;
    private              ObjectProperty<Color>        tickMarkColor;
    private              Color                        _textColor;
    private              ObjectProperty<Color>        textColor;
    private              int                          _decimals;
    private              IntegerProperty              decimals;
    private              double                       _segmentGap;
    private              DoubleProperty               segmentGap;
    private              boolean                      _showFlowDirection;
    private              BooleanProperty              showFlowDirection;
    private              boolean                      _minorTickMarksVisible;
    private              boolean                      _mediumTickMarksVisible;
    private              boolean                      _majorTickMarksVisible;
    private              boolean                      _tickLabelsVisible;
    private              TickLabelOrientation         _tickLabelOrientation;
    private              boolean                      _onlyFirstAndLastTickLabelVisible;
    private              double                       _connectionOpacity;
    private              DoubleProperty               connectionOpacity;
    private              Locale                       _locale;
    private              ObjectProperty<Locale>       locale;
    private              ObservableList<PlotItem>     items;
    private              ItemEventListener            itemListener;
    private              ListChangeListener<PlotItem> itemListListener;
    private              Map<Path, String>            paths;
    private              Tooltip                      tooltip;
    private              String                       formatString;


    // ******************** Constructors **************************************
    public CircularPlot() {
        _tickMarkColor                    = Color.BLACK;
        _textColor                        = Color.BLACK;
        _segmentGap                       = DEFAULT_SEGMENT_GAP;
        _decimals                         = 0;
        _showFlowDirection                = false;
        _minorTickMarksVisible            = true;
        _mediumTickMarksVisible           = true;
        _majorTickMarksVisible            = true;
        _tickLabelsVisible                = true;
        _tickLabelOrientation             = TickLabelOrientation.TANGENT;
        _onlyFirstAndLastTickLabelVisible = true;
        _connectionOpacity                = DEFAULT_CONNECTION_OPACITY;
        _locale                           = Locale.getDefault();
        items                             = FXCollections.observableArrayList();
        itemListener                      = e -> redraw();
        itemListListener                  = c -> {
            while (c.next()) {
                if (c.wasAdded()) {
                    c.getAddedSubList().forEach(addedItem -> addedItem.setOnItemEvent(itemListener));
                } else if (c.wasRemoved()) {
                    c.getRemoved().forEach(removedItem -> removedItem.removeItemEventListener(itemListener));
                }
            }
            validateData();
            redraw();
        };

        formatString                      = "%." + _decimals + "f";

        paths                             = new LinkedHashMap<>();

        initGraphics();
        registerListeners();
    }


    // ******************** Initialization ************************************
    private void initGraphics() {
        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);
            }
        }

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

        ctx.setLineCap(StrokeLineCap.BUTT);

        tooltip = new Tooltip();
        tooltip.setAutoHide(true);

        getChildren().setAll(canvas);
    }

    private void registerListeners() {
        widthProperty().addListener(o -> resize());
        heightProperty().addListener(o -> resize());
        items.addListener(itemListListener);
        canvas.setOnMouseClicked(e -> {
            paths.forEach((path, tooltipText) -> {
                double eventX = e.getX();
                double eventY = e.getY();
                if (path.contains(eventX, eventY)) {
                    double tooltipX = eventX + canvas.getScene().getX() + canvas.getScene().getWindow().getX();
                    double tooltipY = eventY + canvas.getScene().getY() + canvas.getScene().getWindow().getY() - 25;
                    tooltip.setText(tooltipText);
                    tooltip.setX(tooltipX);
                    tooltip.setY(tooltipY);
                    tooltip.show(getScene().getWindow());
                }
            });
        });
    }


    // ******************** Methods *******************************************
    @Override public void layoutChildren() {
        super.layoutChildren();
    }

    @Override protected double computeMinWidth(final double HEIGHT) { return MINIMUM_WIDTH; }
    @Override protected double computeMinHeight(final double WIDTH) { return MINIMUM_HEIGHT; }
    @Override protected double computePrefWidth(final double HEIGHT) { return super.computePrefWidth(HEIGHT); }
    @Override protected double computePrefHeight(final double WIDTH) { return super.computePrefHeight(WIDTH); }
    @Override protected double computeMaxWidth(final double HEIGHT) { return MAXIMUM_WIDTH; }
    @Override protected double computeMaxHeight(final double WIDTH) { return MAXIMUM_HEIGHT; }

    private void handleControlPropertyChanged(final String PROPERTY) {
        if ("".equals(PROPERTY)) {

        }
    }

    @Override public ObservableList<Node> getChildren() { return super.getChildren(); }

    public void dispose() {
        items.forEach(item -> item.removeItemEventListener(itemListener));
        items.removeListener(itemListListener);
    }

    public Color getTickMarkColor() { return null == tickMarkColor ? _tickMarkColor : tickMarkColor.get(); }
    public void setTickMarkColor(final Color COLOR) {
        if (null == tickMarkColor) {
            _tickMarkColor = COLOR;
            redraw();
        } else {
            tickMarkColor.set(COLOR);
        }
    }
    public ObjectProperty<Color> tickMarkColorProperty() {
        if (null == tickMarkColor) {
            tickMarkColor = new ObjectPropertyBase<Color>(_tickMarkColor) {
                @Override protected void invalidated() { redraw(); }
                @Override public Object getBean() { return CircularPlot.this; }
                @Override public String getName() { return "tickMarkColor"; }
            };
            _tickMarkColor = null;
        }
        return tickMarkColor;
    }

    public Color getTextColor() { return null == textColor ? _textColor : textColor.get(); }
    public void setTextColor(final Color COLOR) {
        if (null == textColor) {
            _textColor = COLOR;
            redraw();
        } else {
            textColor.set(COLOR);
        }
    }
    public ObjectProperty<Color> textColorProperty() {
        if (null == textColor) {
            textColor = new ObjectPropertyBase<Color>(_textColor) {
                @Override protected void invalidated() { redraw(); }
                @Override public Object getBean() { return CircularPlot.this; }
                @Override public String getName() { return "textColor"; }
            };
            _textColor = null;
        }
        return textColor;
    }

    public int getDecimals() { return null == decimals ? _decimals : decimals.get(); }
    public void setDecimals(final int DECIMALS) {
        if (null == decimals) {
            _decimals = Helper.clamp(0, 6, DECIMALS);
            formatString = new StringBuilder("%.").append(getDecimals()).append("f").toString();
            redraw();
        } else {
            decimals.set(DECIMALS);
        }
    }
    public IntegerProperty decimalsProperty() {
        if (null == decimals) {
            decimals = new IntegerPropertyBase(_decimals) {
                @Override protected void invalidated() {
                    set(Helper.clamp(0, 6, get()));
                    formatString = new StringBuilder("%.").append(get()).append("f").toString();
                    redraw();
                }
                @Override public Object getBean() { return CircularPlot.this; }
                @Override public String getName() { return "decimals"; }
            };
        }
        return decimals;
    }

    public double getSegmentGap() { return null == segmentGap ? _segmentGap : segmentGap.get(); }
    public void setSegmentGap(final double GAP) {
        if (null == segmentGap) {
            _segmentGap = Helper.clamp(0, 10, GAP);
            redraw();
        } else {
            segmentGap.set(GAP);
        }
    }
    public DoubleProperty segmentGapProperty() {
        if (null == segmentGap) {
            segmentGap = new DoublePropertyBase(_segmentGap) {
                @Override protected void invalidated() {
                    set(Helper.clamp(0, 10, get()));
                    redraw();
                }
                @Override public Object getBean() { return CircularPlot.this; }
                @Override public String getName() { return "segmentGap"; }
            };
        }
        return segmentGap;
    }

    public boolean getShowFlowDirection() { return null == showFlowDirection ? _showFlowDirection : showFlowDirection.get(); }
    public void setShowFlowDirection(final boolean SHOW) {
        if (null == showFlowDirection) {
            _showFlowDirection = SHOW;
            drawChart();
        } else {
            showFlowDirection.set(SHOW);
        }
    }
    public BooleanProperty showFlowDirectionProperty() {
        if (null == showFlowDirection) {
            showFlowDirection = new BooleanPropertyBase(_showFlowDirection) {
                @Override protected void invalidated() { drawChart(); }
                @Override public Object getBean() { return CircularPlot.this; }
                @Override public String getName() { return "showFlowDirection"; }
            };
        }
        return showFlowDirection;
    }

    public boolean getMinorTickMarksVisible() { return _minorTickMarksVisible; }
    public void setMinorTickMarksVisible(final boolean VISIBLE) {
        _minorTickMarksVisible = VISIBLE;
        redraw();
    }

    public boolean getMediumTickMarksVisible() { return _mediumTickMarksVisible; }
    public void setMediumTickMarksVisible(final boolean VISIBLE) {
        _mediumTickMarksVisible = VISIBLE;
        redraw();
    }

    public boolean getMajorTickMarksVisible() { return _majorTickMarksVisible; }
    public void setMajorTickMarksVisible(final boolean VISIBLE) {
        _majorTickMarksVisible = VISIBLE;
        redraw();
    }

    public boolean getTickLabelsVisible() { return _tickLabelsVisible; }
    public void setTickLabelsVisible(final boolean VISIBLE) {
        _tickLabelsVisible = VISIBLE;
        redraw();
    }

    public TickLabelOrientation getTickLabelOrientation() { return _tickLabelOrientation; }
    public void setTickLabelOrientation(final TickLabelOrientation ORIENTATION) {
        _tickLabelOrientation = ORIENTATION;
        redraw();
    }

    public boolean isOnlyFirstAndLastTickLabelVisible() { return _onlyFirstAndLastTickLabelVisible; }
    public void setOnlyFirstAndLastTickLabelVisible(final boolean VISIBLE) {
        _onlyFirstAndLastTickLabelVisible = VISIBLE;
        redraw();
    }

    public double getConnectionOpacity() { return null == connectionOpacity ? _connectionOpacity : connectionOpacity.get(); }
    public void setConnectionOpacity(final double OPACITY) {
        if (null == connectionOpacity) {
            _connectionOpacity = Helper.clamp(0.1, 1.0, OPACITY);
            redraw();
        } else {
            connectionOpacity.set(OPACITY);
        }
    }
    public DoubleProperty connectionOpacityProperty() {
        if (null == connectionOpacity) {
            connectionOpacity = new DoublePropertyBase(_connectionOpacity) {
                @Override protected void invalidated() {
                    set(Helper.clamp(0.1, 1.0, get()));
                    redraw();
                }
                @Override public Object getBean() { return CircularPlot.this; }
                @Override public String getName() { return "connectionOpacity"; }
            };
        }
        return connectionOpacity;
    }

    public Locale getLocale() { return null == locale ? _locale : locale.get(); }
    public void setLocale(final Locale LOCALE) {
        if (null == locale) {
            _locale = LOCALE;
            redraw();
        } else {
            locale.set(LOCALE);
        }
    }
    public ObjectProperty<Locale> localeProperty() {
        if (null == locale) {
            locale = new ObjectPropertyBase<Locale>(_locale) {
                @Override protected void invalidated() { redraw(); }
                @Override public Object getBean() { return CircularPlot.this; }
                @Override public String getName() { return "locale"; }
            };
        }
        _locale = null;
        return locale;
    }

    public List<PlotItem> getItems() { return items; }
    public void setItems(final PlotItem... ITEMS) { setItems(Arrays.asList(ITEMS)); }
    public void setItems(final List<PlotItem> ITEMS) {
        items.setAll(ITEMS);
        validateData();
    }
    public void addItem(final PlotItem ITEM) {
        if (!items.contains(ITEM)) { items.add(ITEM); }
        validateData();
    }
    public void removeItem(final PlotItem ITEM) { if (items.contains(ITEM)) { items.remove(ITEM); } }

    public void sortAscending() {
        Collections.sort(getItems(), Comparator.comparingDouble(PlotItem::getValue));
    }
    public void sortDescending() {
        Collections.sort(getItems(), (item1, item2) -> Double.compare(item2.getValue(), item1.getValue()));
    }

    private void validateData() {
        Map<PlotItem, Double> incoming = new HashMap<>(getItems().size());
        for (PlotItem item : getItems()) {
            item.getOutgoing().forEach((outgoingItem, value) -> {
                if (incoming.containsKey(outgoingItem)) {
                    incoming.put(outgoingItem, incoming.get(outgoingItem) + value);
                } else {
                    incoming.put(outgoingItem, value);
                }
            });
        }
        for (PlotItem item : getItems()) {
            if (incoming.containsKey(item)) {
                double sumOfIncoming = incoming.get(item);
                if (item.getValue() < sumOfIncoming) {
                    item.setValue(sumOfIncoming);
                }
            }
        }
    }

    private void drawChart() {
        paths.clear();

        TickLabelOrientation tickLabelOrientation = getTickLabelOrientation();
        if (TickLabelOrientation.ORTHOGONAL == tickLabelOrientation) {
            chartSize         = size * 0.75;
            mainLineWidth     = chartSize * 0.045;
            outgoingLineWidth = chartSize * 0.015;
            tickMarkWidth     = chartSize * TICK_MARK_WIDTH;
            chartOffset       = (size - chartSize) * 0.5;
            innerChartOffset  = chartOffset + chartSize * 0.032;
            innerChartSize    = chartSize - chartSize * 0.064;
            centerX           = size * 0.5;
            centerY           = size * 0.5;
        }

        ctx.clearRect(0, 0, size, size);

        double sum       = items.stream().mapToDouble(PlotItem::getValue).sum();
        int    noOfItems = items.size();

        Map<PlotItem, ChartItemParameter> parameterMap = new HashMap<>(items.size());

        // Draw outer circle segments and tickmarks
        double angleStep = (360.0 - (noOfItems * getSegmentGap())) / sum;
        double angle     = -ANGLE_OFFSET;
        for (int i = 0 ; i < noOfItems ; i++) {
            PlotItem item          = items.get(i);
            double   angleRange    = item.getValue() * angleStep;
            double   sumOfOutgoing = item.getOutgoing().values().stream().mapToDouble(Double::doubleValue).sum();

            // Store item specific angle and angleRange for later use
            parameterMap.put(item, new ChartItemParameter(angle + ANGLE_OFFSET, angleRange));

            // Draw outer circle segments
            ctx.setLineWidth(mainLineWidth);
            ctx.setStroke(item.getFill());
            ctx.strokeArc(chartOffset, chartOffset, chartSize, chartSize, -angle, -angleRange, ArcType.OPEN);

            // Draw sum of outgoing at the end of the segment
            double outgoingAngleRange = sumOfOutgoing * angleStep;
            ctx.setLineWidth(outgoingLineWidth);
            ctx.strokeArc(innerChartOffset, innerChartOffset, innerChartSize, innerChartSize, -angle - angleRange + outgoingAngleRange, -outgoingAngleRange, ArcType.OPEN);

            // Draw tickmarks
            ctx.setLineWidth(tickMarkWidth);
            ctx.setStroke(getTickMarkColor());
            ctx.strokeArc(chartOffset - mainLineWidth * 0.5, chartOffset - mainLineWidth * 0.5, chartSize + mainLineWidth, chartSize + mainLineWidth, -angle, -angleRange, ArcType.OPEN);
            drawTickMarks(item, angle, angleRange);

            // Increment angle
            angle += angleRange + getSegmentGap();
        }

        // Draw incoming and outgoing
        double sinValue;
        double cosValue;
        double innerRingRadius  = chartSize * 0.462;
        double innerRingRadius2 = chartSize * 0.475;
        double outerPointRadius = chartSize * 0.26;
        double innerPointRadius = chartSize * 0.20;
        for (int i = 0 ; i < noOfItems ; i++) {
            PlotItem           item           = items.get(i);
            ChartItemParameter itemParameter  = parameterMap.get(item);
            double             itemStartAngle = itemParameter.getStartAngle();
            double             itemAngleRange = itemParameter.getAngleRange();
            double             itemEndAngle   = itemParameter.getEndAngle();

            // Draw item name
            ctx.save();
            ctx.setFill(getTextColor());
            ctx.setFont(Fonts.latoRegular(size * 0.02));
            ctx.setTextAlign(TextAlignment.CENTER);
            ctx.setTextBaseline(VPos.CENTER);
            sinValue = Math.sin(Math.toRadians(-itemStartAngle - itemAngleRange * 0.5 - 180));
            cosValue = Math.cos(Math.toRadians(-itemStartAngle - itemAngleRange * 0.5 - 180));
            double itemNamePointX = centerX + chartSize * 0.56 * sinValue;
            double itemNamePointY = centerY + chartSize * 0.56 * cosValue;

            if (TickLabelOrientation.ORTHOGONAL == tickLabelOrientation) {
                Font font = Fonts.latoRegular(size * 0.02);
                Text measureText = new Text(item.getName());
                measureText.setFont(font);
                double textWidth = measureText.getLayoutBounds().getWidth();
                itemNamePointX += textWidth * 0.33 * sinValue;
                itemNamePointY += textWidth * 0.33 * cosValue;
            }

            ctx.translate(itemNamePointX, itemNamePointY);
            rotateContextForText(ctx, -itemStartAngle, -itemAngleRange * 0.5 + ANGLE_OFFSET, tickLabelOrientation);
            ctx.fillText(item.getName(), 0, 0);
            ctx.restore();

            // Draw connections between items
            for (PlotItem outgoingItem : item.getOutgoing().keySet()) {
                ChartItemParameter outgoingItemParameter = parameterMap.get(outgoingItem);
                double             outgoingValue         = item.getOutgoing().get(outgoingItem);
                double             outgoingAngleRange    = outgoingValue * angleStep;

                int indexDelta   = items.indexOf(item) - items.indexOf(outgoingItem);
                outerPointRadius = outerPointRadius / (Math.abs(indexDelta) + 0.75);
                innerPointRadius = innerPointRadius / (Math.abs(indexDelta) + 0.75);

                // Points in source chart item
                sinValue = Math.sin(Math.toRadians(-itemEndAngle + 180 + itemParameter.getNextOutgoingStartAngle()));
                cosValue = Math.cos(Math.toRadians(-itemEndAngle + 180 + itemParameter.getNextOutgoingStartAngle()));
                Point p0 = new Point(centerX + innerRingRadius * sinValue, centerY + innerRingRadius * cosValue);

                sinValue = Math.sin(Math.toRadians(-itemEndAngle + 180 + outgoingAngleRange + itemParameter.getNextOutgoingStartAngle()));
                cosValue = Math.cos(Math.toRadians(-itemEndAngle + 180 + outgoingAngleRange + itemParameter.getNextOutgoingStartAngle()));
                Point p1 = new Point(centerX + innerRingRadius * sinValue, centerY + innerRingRadius * cosValue);

                // Point between p0 and p1
                sinValue  = Math.sin(Math.toRadians(-itemEndAngle + outgoingAngleRange * 0.5 + 180 + itemParameter.getNextOutgoingStartAngle()));
                cosValue  = Math.cos(Math.toRadians(-itemEndAngle + outgoingAngleRange * 0.5 + 180 + itemParameter.getNextOutgoingStartAngle()));
                Point p01 = new Point(centerX + innerRingRadius * sinValue, centerY + innerRingRadius * cosValue);

                // Points in target chart item
                sinValue = Math.sin(Math.toRadians(-outgoingItemParameter.getNextIncomingStartAngle() + 180));
                cosValue = Math.cos(Math.toRadians(-outgoingItemParameter.getNextIncomingStartAngle() + 180));
                Point p2 = new Point(centerX + innerRingRadius * sinValue, centerY + innerRingRadius * cosValue);

                sinValue = Math.sin(Math.toRadians(-outgoingItemParameter.getNextIncomingStartAngle() - outgoingAngleRange + 180));
                cosValue = Math.cos(Math.toRadians(-outgoingItemParameter.getNextIncomingStartAngle() - outgoingAngleRange + 180));
                Point p3 = new Point(centerX + innerRingRadius * sinValue, centerY + innerRingRadius * cosValue);

                // Point between p2 and p3
                sinValue  = Math.sin(Math.toRadians(-outgoingItemParameter.getNextIncomingStartAngle() - outgoingAngleRange * 0.5 + 180));
                cosValue  = Math.cos(Math.toRadians(-outgoingItemParameter.getNextIncomingStartAngle() - outgoingAngleRange * 0.5 + 180));
                Point p23;
                if (getShowFlowDirection()) {
                    p23 = new Point(centerX + innerRingRadius2 * sinValue, centerY + innerRingRadius2 * cosValue);
                } else {
                    p23 = new Point(centerX + innerRingRadius * sinValue, centerY + innerRingRadius * cosValue);
                }

                // Points between source and target chart item
                sinValue = Math.sin(Math.toRadians((-itemEndAngle - outgoingItemParameter.getNextIncomingStartAngle()) * 0.5 + 180 + itemParameter.getNextOutgoingStartAngle()));
                cosValue = Math.cos(Math.toRadians((-itemEndAngle - outgoingItemParameter.getNextIncomingStartAngle()) * 0.5 + 180 + itemParameter.getNextOutgoingStartAngle()));
                Point p4, p5;
                if (indexDelta < 0) {
                    p4 = new Point(centerX + outerPointRadius * sinValue, centerY + outerPointRadius * cosValue);
                    p5 = new Point(centerX + innerPointRadius * sinValue, centerY + innerPointRadius * cosValue);
                } else {
                    p4 = new Point(centerX + innerPointRadius * sinValue, centerY + innerPointRadius * cosValue);
                    p5 = new Point(centerX + outerPointRadius * sinValue, centerY + outerPointRadius * cosValue);
                }

                // Store next incoming start angle
                outgoingItemParameter.setNextIncomingStartAngle(outgoingItemParameter.getNextIncomingStartAngle() + outgoingAngleRange);

                // Store next outgoing start angle
                itemParameter.setNextOutgoingStartAngle(itemParameter.getNextOutgoingStartAngle() + outgoingAngleRange);

                // Draw flow
                Path path = new Path();
                path.setFill(Helper.getColorWithOpacity(item.getFill(), getConnectionOpacity()));
                path.moveTo(p0.getX(), p0.getY());
                path.quadraticCurveTo(p4.getX(), p4.getY(), p2.getX(), p2.getY());             // curve from p4 -> p4 -> p2
                if (getShowFlowDirection()) {
                    path.lineTo(p23.getX(), p23.getY());                                       // line from p2 -> p23
                    path.lineTo(p3.getX(), p3.getY());                                         // line from p23 -> p3
                } else {
                    path.quadraticCurveTo(p23.getX(), p23.getY(), p3.getX(), p3.getY());       // curve from p2 -> p23 -> p3
                }
                path.quadraticCurveTo(p5.getX(), p5.getY(), p1.getX(), p1.getY());             // curve from p3 -> p5 -> p1
                path.quadraticCurveTo(p01.getX(), p01.getY(), p0.getX(), p0.getY());           // curve from p1 -> p01 -> p0
                path.closePath();

                path.draw(ctx, true, false);

                String tooltipText = new StringBuilder().append(item.getName())
                                                        .append(" -> ")
                                                        .append(outgoingItem.getName())
                                                        .append(" ")
                                                        .append(String.format(getLocale(), formatString, outgoingValue))
                                                        .toString();
                paths.put(path, tooltipText);

                /*
                ctx.setFill(Helper.getColorWithOpacity(item.getFillColor(), getConnectionOpacity()));
                ctx.beginPath();
                ctx.moveTo(p0.getX(), p0.getY());
                ctx.quadraticCurveTo(p4.getX(), p4.getY(), p2.getX(), p2.getY());             // curve from p4 -> p4 -> p2
                if (getShowFlowDirection()) {
                    ctx.lineTo(p23.getX(), p23.getY());                                       // line from p2 -> p23
                    ctx.lineTo(p3.getX(), p3.getY());                                         // line from p23 -> p3
                } else {
                    ctx.quadraticCurveTo(p23.getX(), p23.getY(), p3.getX(), p3.getY());       // curve from p2 -> p23 -> p3
                }
                ctx.quadraticCurveTo(p5.getX(), p5.getY(), p1.getX(), p1.getY());             // curve from p3 -> p5 -> p1
                ctx.quadraticCurveTo(p01.getX(), p01.getY(), p0.getX(), p0.getY());           // curve from p1 -> p01 -> p0
                ctx.closePath();
                ctx.fill();
                */
            }
        }
    }

    private void drawTickMarks(final PlotItem ITEM, final double START_ANGLE, final double ANGLE_RANGE) {
        double        sinValue;
        double        cosValue;
        double[]      scaleParameters              = Helper.calcAutoScale(0, ITEM.getValue());
        double        minorTickSpace               = scaleParameters[0];
        double        majorTickSpace               = scaleParameters[1];
        double        minValue                     = 0; //scaleParameters[2];
        double        maxValue                     = ITEM.getValue(); //scaleParameters[3];
        double        range                        = maxValue - minValue;
        double        angleStep                    = (ANGLE_RANGE / range) * minorTickSpace;
        BigDecimal    minorTickSpaceBD             = BigDecimal.valueOf(minorTickSpace);
        BigDecimal    majorTickSpaceBD             = BigDecimal.valueOf(majorTickSpace);
        BigDecimal    mediumCheck2                 = BigDecimal.valueOf(2 * minorTickSpace);
        BigDecimal    mediumCheck5                 = BigDecimal.valueOf(5 * minorTickSpace);
        BigDecimal    counterBD                    = BigDecimal.valueOf(0);
        double        counter                      = 0;

        boolean       majorTickMarksVisible        = getMajorTickMarksVisible();
        boolean       mediumTickMarksVisible       = getMediumTickMarksVisible();
        boolean       minorTickMarksVisible        = getMinorTickMarksVisible();
        boolean       tickLabelsVisible            = getTickLabelsVisible();
        boolean       onlyFirstAndLastLabelVisible = isOnlyFirstAndLastTickLabelVisible();

        double        orthTextFactor               = 0.542;
        double        tickLabelFontSize            = getDecimals() == 0 ? 0.018 * chartSize : 0.017 * chartSize;
        double        tickLabelOrientationFactor   = TickLabelOrientation.HORIZONTAL == getTickLabelOrientation() ? 0.9 : 1.0;
        Font          tickLabelFont                = Fonts.latoRegular(tickLabelFontSize * tickLabelOrientationFactor);

        // Variables needed for tickmarks
        double innerPointX, innerPointY;
        double outerPointX, outerPointY;
        double outerMediumPointX, outerMediumPointY;
        double outerMinorPointX, outerMinorPointY;
        double textPointX, textPointY;

        // Set the general tickmark color
        ctx.setStroke(getTickMarkColor());
        ctx.setFill(getTickMarkColor());
        ctx.setLineCap(StrokeLineCap.BUTT);
        ctx.setLineWidth(size * TICK_MARK_WIDTH);
        ctx.setTextAlign(TextAlignment.CENTER);
        ctx.setTextBaseline(VPos.CENTER);

        // Main loop
        BigDecimal tmpStepBD = new BigDecimal(angleStep);
        tmpStepBD            = tmpStepBD.setScale(3, BigDecimal.ROUND_HALF_UP);
        double tmpStep       = tmpStepBD.doubleValue();
        double angle         = 0;

        for (double i = 0 ; Double.compare(-ANGLE_RANGE - tmpStep, i) <= 0 ; i -= tmpStep) {
            sinValue          = Math.sin(Math.toRadians(-START_ANGLE + angle + ANGLE_OFFSET));
            cosValue          = Math.cos(Math.toRadians(-START_ANGLE + angle + ANGLE_OFFSET));

            innerPointX       = centerX + chartSize * 0.5225 * sinValue;
            innerPointY       = centerY + chartSize * 0.5225 * cosValue;
            outerPointX       = centerX + chartSize * (0.5175 + MAJOR_TICK_MARK_LENGTH) * sinValue;
            outerPointY       = centerY + chartSize * (0.5175 + MAJOR_TICK_MARK_LENGTH) * cosValue;
            outerMediumPointX = centerX + chartSize * (0.5175 + MEDIUM_TICK_MARK_LENGTH) * sinValue;
            outerMediumPointY = centerY + chartSize * (0.5175 + MEDIUM_TICK_MARK_LENGTH) * cosValue;
            outerMinorPointX  = centerX + chartSize * (0.5175 + MINOR_TICK_MARK_LENGTH) * sinValue;
            outerMinorPointY  = centerY + chartSize * (0.5175 + MINOR_TICK_MARK_LENGTH) * cosValue;
            textPointX        = centerX + chartSize * orthTextFactor * sinValue;
            textPointY        = centerY + chartSize * orthTextFactor * cosValue;

            if (Double.compare(counterBD.remainder(majorTickSpaceBD).doubleValue(), 0.0) == 0) {
                // Draw major tick mark
                if (majorTickMarksVisible) {
                    ctx.strokeLine(innerPointX, innerPointY, outerPointX, outerPointY);
                } else if (minorTickMarksVisible) {
                    ctx.strokeLine(innerPointX, innerPointY, outerMinorPointX, outerMinorPointY);
                }

                // Draw tick label text
                if (tickLabelsVisible) {
                    ctx.save();
                    ctx.translate(textPointX, textPointY);

                    rotateContextForText(ctx, -START_ANGLE, angle, getTickLabelOrientation());
                    ctx.setFont(tickLabelFont);

                    if (!onlyFirstAndLastLabelVisible) {
                        ctx.setFill(getTextColor());
                    } else {
                        if ((Double.compare(counter, minValue) == 0 ||
                             counter + majorTickSpace > maxValue)) {
                             //Double.compare(counter, maxValue) == 0)) { // only if nice min-max values will be used
                            ctx.setFill(getTextColor());
                        } else {
                            ctx.setFill(Color.TRANSPARENT);
                        }
                    }
                    ctx.fillText(Helper.format(counter, getDecimals(), getLocale()), 0, 0);
                    ctx.restore();
                }
            } else if (mediumTickMarksVisible &&
                       Double.compare(minorTickSpaceBD.remainder(mediumCheck2).doubleValue(), 0.0) != 0.0 &&
                       Double.compare(counterBD.remainder(mediumCheck5).doubleValue(), 0.0) == 0.0) {
                // Draw medium tick mark
                ctx.strokeLine(innerPointX, innerPointY, outerMediumPointX, outerMediumPointY);
            } else if (minorTickMarksVisible && Double.compare(counterBD.remainder(minorTickSpaceBD).doubleValue(), 0.0) == 0) {
                // Draw minor tick mark
                ctx.strokeLine(innerPointX, innerPointY, outerMinorPointX, outerMinorPointY);
            }
            counterBD = counterBD.add(minorTickSpaceBD);
            counter   = counterBD.doubleValue();
            if (counter > maxValue) break;
            angle = (angle - angleStep);
        }
    }

    private void rotateContextForText(final GraphicsContext CTX, final double START_ANGLE, final double TEXT_ANGLE, final TickLabelOrientation ORIENTATION) {
        switch (ORIENTATION) {
            case ORTHOGONAL:
                if ((360 - START_ANGLE - TEXT_ANGLE) % 360 > 90 && (360 - START_ANGLE - TEXT_ANGLE) % 360 < 270) {
                    CTX.rotate((180 - START_ANGLE - TEXT_ANGLE) % 360);
                } else {
                    CTX.rotate((360 - START_ANGLE - TEXT_ANGLE) % 360);
                }
                break;
            case TANGENT:
                if ((360 - START_ANGLE - TEXT_ANGLE - 90) % 360 > 90 && (360 - START_ANGLE - TEXT_ANGLE - 90) % 360 < 270) {
                    CTX.rotate((90 - START_ANGLE - TEXT_ANGLE) % 360);
                } else {
                    CTX.rotate((270 - START_ANGLE - TEXT_ANGLE) % 360);
                }
                break;
            case HORIZONTAL:
            default:
                break;
        }
    }


    // ******************** Resizing ******************************************
    private void resize() {
        width             = getWidth() - getInsets().getLeft() - getInsets().getRight();
        height            = getHeight() - getInsets().getTop() - getInsets().getBottom();
        size              = width < height ? width : height;
        chartSize         = size * 0.85;
        mainLineWidth     = chartSize * 0.045;
        outgoingLineWidth = chartSize * 0.015;
        tickMarkWidth     = chartSize * TICK_MARK_WIDTH;
        chartOffset       = (size - chartSize) * 0.5;
        innerChartOffset  = chartOffset + chartSize * 0.032;
        innerChartSize    = chartSize - chartSize * 0.064;
        centerX           = size * 0.5;
        centerY           = size * 0.5;

        if (width > 0 && height > 0) {
            canvas.setWidth(size);
            canvas.setHeight(size);
            canvas.relocate((getWidth() - size) * 0.5, (getHeight() - size) * 0.5);

            redraw();
        }
    }

    private void redraw() {
        drawChart();
    }


    // ******************** Inner Classes *************************************
    private class ChartItemParameter {
        private double startAngle;
        private double angleRange;
        private double endAngle;
        private double nextIncomingStartAngle;
        private double nextOutgoingStartAngle;


        // ******************** Constructors **********************************
        public ChartItemParameter() {
            this(0, 0);
        }
        public ChartItemParameter(final double START_ANGLE, final double ANGLE_RANGE) {
            startAngle             = START_ANGLE;
            angleRange             = ANGLE_RANGE;
            endAngle               = START_ANGLE + ANGLE_RANGE;
            nextIncomingStartAngle = START_ANGLE;
            nextOutgoingStartAngle = 0;
        }


        // ******************** Methods ***************************************
        public double getStartAngle() { return startAngle; }
        public void setStartAngle(final double START_ANGLE) {
            startAngle             = START_ANGLE;
            endAngle               = startAngle + getAngleRange();
            nextIncomingStartAngle = startAngle;
            nextOutgoingStartAngle = 0;
        }

        public double getAngleRange() { return angleRange; }
        public void setAngleRange(final double ANGLE_RANGE) {
            angleRange             = ANGLE_RANGE;
            endAngle               = getStartAngle() + angleRange;
            nextOutgoingStartAngle = 0;
        }

        public double getEndAngle() { return endAngle; }

        public double getNextIncomingStartAngle() { return nextIncomingStartAngle; }
        public void setNextIncomingStartAngle(final double ANGLE) {
            nextIncomingStartAngle = ANGLE;
        }

        public double getNextOutgoingStartAngle() { return nextOutgoingStartAngle; }
        public void setNextOutgoingStartAngle(final double ANGLE) { nextOutgoingStartAngle = ANGLE; }
    }
}