/*
 * 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 org.point85.tilesfx.skins;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;

import org.point85.tilesfx.Tile;
import org.point85.tilesfx.Tile.ChartType;
import org.point85.tilesfx.chart.ChartData;
import org.point85.tilesfx.events.ChartDataEventListener;
import org.point85.tilesfx.events.TileEvent;
import org.point85.tilesfx.events.TileEvent.EventType;
import org.point85.tilesfx.fonts.Fonts;
import org.point85.tilesfx.tools.Helper;
import org.point85.tilesfx.tools.Point;

import javafx.animation.FadeTransition;
import javafx.animation.PauseTransition;
import javafx.animation.SequentialTransition;
import javafx.collections.ListChangeListener;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Point2D;
import javafx.geometry.VPos;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Tooltip;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Circle;
import javafx.scene.shape.ClosePath;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.PathElement;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import javafx.scene.text.TextFlow;
import javafx.util.Duration;


/**
 * Created by hansolo on 09.06.17.
 */
public class SmoothAreaChartTileSkin extends TileSkin {
    private Text                          titleText;
    private Text                          valueText;
    private Text                          unitText;
    private TextFlow                      valueUnitFlow;
    private int                           dataSize;
    private double                        maxValue;
    private List<Point>                   points;
    private Path                          fillPath;
    private Path                          strokePath;
    private Canvas                        canvas;
    private GraphicsContext               ctx;
    private boolean                       dataPointsVisible;
    private boolean                       smoothing;
    private double                        hStepSize;
    private double                        vStepSize;
    private Circle                        selector;
    private Tooltip                       selectorTooltip;
    private SequentialTransition          fadeInFadeOut;
    private Rectangle                     fillClip;
    private Rectangle                     strokeClip;
    private ChartDataEventListener        chartEventListener;
    private ListChangeListener<ChartData> chartDataListener;
    private EventHandler<MouseEvent>      clickHandler;
    private EventHandler<ActionEvent>     endOfTransformationHandler;


    // ******************** Constructors **************************************
    public SmoothAreaChartTileSkin(final Tile TILE) {
        super(TILE);
    }


    // ******************** Initialization ************************************
    @Override protected void initGraphics() {
        super.initGraphics();

        chartEventListener         = e -> handleData();
        chartDataListener          = c -> {
            while (c.next()) {
                if (c.wasAdded()) {
                    c.getAddedSubList().forEach(addedItem -> addedItem.addChartDataEventListener(chartEventListener));
                } else if (c.wasRemoved()) {
                    c.getRemoved().forEach(removedItem -> removedItem.removeChartDataEventListener(chartEventListener));
                }
            }
            handleData();
        };
        clickHandler               = e -> select(e);
        endOfTransformationHandler = e -> selectorTooltip.hide();

        smoothing = tile.isSmoothing();

        titleText = new Text();
        titleText.setFill(tile.getTitleColor());
        Helper.enableNode(titleText, !tile.getTitle().isEmpty());

        fillClip   = new Rectangle(0, 0, PREFERRED_WIDTH, PREFERRED_HEIGHT);
        strokeClip = new Rectangle(0, 0, PREFERRED_WIDTH, PREFERRED_HEIGHT);

        points = new ArrayList<>();

        fillPath = new Path();
        fillPath.setStroke(null);
        fillPath.setClip(fillClip);

        strokePath = new Path();
        strokePath.setFill(null);
        strokePath.setStroke(tile.getBarColor());
        strokePath.setClip(strokeClip);
        strokePath.setMouseTransparent(true);

        Helper.enableNode(fillPath, ChartType.AREA == tile.getChartType());

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

        dataPointsVisible = tile.getDataPointsVisible();

        valueText = new Text(String.format(locale, formatString, ((tile.getValue() - minValue) / range * 100)));
        valueText.setFill(tile.getValueColor());
        valueText.setTextOrigin(VPos.BASELINE);
        Helper.enableNode(valueText, tile.isValueVisible());

        unitText = new Text(" " + tile.getUnit());
        unitText.setFill(tile.getUnitColor());
        unitText.setTextOrigin(VPos.BASELINE);
        Helper.enableNode(unitText, !tile.getUnit().isEmpty());

        valueUnitFlow = new TextFlow(valueText, unitText);
        valueUnitFlow.setTextAlignment(TextAlignment.RIGHT);

        selector        = new Circle();
        selectorTooltip = new Tooltip("");
        selectorTooltip.setWidth(60);
        selectorTooltip.setHeight(48);
        Tooltip.install(selector, selectorTooltip);

        FadeTransition fadeIn = new FadeTransition(Duration.millis(100), selector);
        fadeIn.setFromValue(0);
        fadeIn.setToValue(1);

        FadeTransition fadeOut = new FadeTransition(Duration.millis(100), selector);
        fadeOut.setFromValue(1);
        fadeOut.setToValue(0);

        fadeInFadeOut = new SequentialTransition(fadeIn, new PauseTransition(Duration.millis(3000)), fadeOut);

        handleData();

        getPane().getChildren().addAll(titleText, fillPath, strokePath, canvas, valueUnitFlow, selector);
    }

    @Override protected void registerListeners() {
        super.registerListeners();
        tile.getChartData().forEach(chartData -> chartData.addChartDataEventListener(chartEventListener));
        tile.getChartData().addListener(chartDataListener);
        fillPath.addEventHandler(MouseEvent.MOUSE_PRESSED, clickHandler);
        fadeInFadeOut.setOnFinished(endOfTransformationHandler);
    }

    @Override public void dispose() {
        tile.getChartData().removeListener(chartDataListener);
        tile.getChartData().forEach(chartData -> chartData.removeChartDataEventListener(chartEventListener));
        fillPath.removeEventHandler(MouseEvent.MOUSE_PRESSED, clickHandler);
        endOfTransformationHandler = null;
        fadeInFadeOut.setOnFinished(null);
        super.dispose();
    }


    // ******************** Methods *******************************************
    @Override protected void handleEvents(final String EVENT_TYPE) {
        super.handleEvents(EVENT_TYPE);

        if ("VISIBILITY".equals(EVENT_TYPE)) {
            Helper.enableNode(titleText, !tile.getTitle().isEmpty());
            Helper.enableNode(valueText, tile.isValueVisible());
            Helper.enableNode(unitText, !tile.getUnit().isEmpty());
            dataPointsVisible = tile.getDataPointsVisible();
            if (dataPointsVisible) { drawChart(points); } else { ctx.clearRect(0, 0, width, height); }
        } else if ("SERIES".equals(EVENT_TYPE)) {
            Helper.enableNode(fillPath, ChartType.AREA == tile.getChartType());
        }
    }

    private void handleData() {
        selectorTooltip.hide();
        selector.setVisible(false);
        List<ChartData> data = tile.getChartData();
        if (null == data || data.isEmpty()) { return; }
        Optional<ChartData> lastDataEntry = data.stream().reduce((first, second) -> second);
        if (lastDataEntry.isPresent()) {
            valueText.setText(String.format(locale, formatString, lastDataEntry.get().getValue()));
            tile.setValue(lastDataEntry.get().getValue());
            resizeDynamicText();
        }
        dataSize  = data.size();
        maxValue  = data.stream().max(Comparator.comparing(c -> c.getValue())).get().getValue();
        hStepSize = width / (dataSize - 1);
        vStepSize = (height * 0.5) / maxValue;

        points.clear();
        for (int i = 0 ; i < dataSize ; i++) {
            points.add(new Point((i) * hStepSize, height - data.get(i).getValue() * vStepSize));
        }
        drawChart(points);
    }

    private void drawChart(final List<Point> POINTS) {
        if (POINTS.isEmpty()) return;
        Point[] points = smoothing ? Helper.subdividePoints(POINTS.toArray(new Point[0]), 8) : POINTS.toArray(new Point[0]);

        fillPath.getElements().clear();
        fillPath.getElements().add(new MoveTo(0, height));

        strokePath.getElements().clear();
        strokePath.getElements().add(new MoveTo(points[0].getX(), points[0].getY()));

        for (Point p : points) {
            fillPath.getElements().add(new LineTo(p.getX(), p.getY()));
            strokePath.getElements().add(new LineTo(p.getX(), p.getY()));
        }

        fillPath.getElements().add(new LineTo(width, height));
        fillPath.getElements().add(new LineTo(0, height));
        fillPath.getElements().add(new ClosePath());

        if (dataPointsVisible) { drawDataPoints(POINTS, tile.isFillWithGradient() ? tile.getGradientStops().get(0).getColor() : tile.getBarColor()); }
    }

    private void drawDataPoints(final List<Point> DATA, final Color COLOR) {
        if (DATA.isEmpty()) { return; }
        final double LOWER_BOUND_X = 0;
        final double LOWER_BOUND_Y = tile.getMinValue();
        ctx.clearRect(0, 0, width, height);
        for (Point point : DATA) {
            double x = (point.getX() - LOWER_BOUND_X);
            double y = (point.getY() - LOWER_BOUND_Y);
            drawDataPoint(x, y, COLOR);
        }
    }

    private void drawDataPoint(final double X, final double Y, final Color COLOR) {
        double borderSize     = size * 0.06;
        double symbolSize     = size * 0.04;
        double halfBorderSize = borderSize * 0.5;
        double halfSymbolSize = symbolSize * 0.5;
        ctx.save();
        ctx.setFill(tile.getBackgroundColor());
        ctx.fillOval(X -halfBorderSize, Y - halfBorderSize, borderSize, borderSize);
        ctx.setFill(COLOR);
        ctx.fillOval(X - halfSymbolSize, Y - halfSymbolSize, symbolSize, symbolSize);
        ctx.restore();
    }

    private void select(final MouseEvent EVT) {
        final double EVENT_X     = EVT.getX();
        final double CHART_X     = 0;
        final double CHART_WIDTH = width;

        if (Double.compare(EVENT_X, CHART_X) < 0 || Double.compare(EVENT_X, CHART_WIDTH) > 0) { return; }

        double            upperBound   = tile.getChartData().stream().max(Comparator.comparing(ChartData::getValue)).get().getValue();
        double            range        = upperBound - tile.getMinValue();
        double            factor       = range / (height * 0.5);
        List<PathElement> elements     = strokePath.getElements();
        int               noOfElements = elements.size();
        PathElement       lastElement  = elements.get(0);

        if (tile.isSnapToTicks()) {
            double    reverseFactor    = (height * 0.5) / range;
            int       noOfDataElements = tile.getChartData().size();
            double    interval         = width / (double) (noOfDataElements - 1);
            int       selectedIndex    = Helper.roundDoubleToInt(EVENT_X / interval);
            ChartData selectedData     = tile.getChartData().get(selectedIndex);
            double    selectedValue    = selectedData.getValue();

            selector.setCenterX(interval * selectedIndex);
            selector.setCenterY(height - selectedValue * reverseFactor);
            selector.setVisible(true);
            fadeInFadeOut.playFrom(Duration.millis(0));

            String tooltipText = new StringBuilder(selectedData.getName()).append("\n").append(String.format(locale, formatString, selectedValue)).toString();

            Point2D popupLocation = tile.localToScreen(selector.getCenterX() - selectorTooltip.getWidth() * 0.5, selector.getCenterY() - size * 0.025 - selectorTooltip.getHeight());
            selectorTooltip.setText(tooltipText);
            selectorTooltip.setX(popupLocation.getX());
            selectorTooltip.setY(popupLocation.getY());
            selectorTooltip.show(tile.getScene().getWindow());

            tile.fireTileEvent(new TileEvent(EventType.SELECTED_CHART_DATA, selectedData));
        } else {
            for (int i = 1; i < noOfElements; i++) {
                PathElement element = elements.get(i);

                double[] xy  = getXYFromPathElement(lastElement);
                double[] xy1 = getXYFromPathElement(element);

                if (EVENT_X > xy[0] && EVENT_X < xy1[0]) {
                    double deltaX        = xy1[0] - xy[0];
                    double deltaY        = xy1[1] - xy[1];
                    double m             = deltaY / deltaX;
                    double y             = m * (EVT.getX() - xy[0]) + xy[1];
                    double selectedValue = upperBound - (y - (height * 0.5)) * factor;

                    selector.setCenterX(EVT.getX());
                    selector.setCenterY(y);
                    selector.setVisible(true);
                    fadeInFadeOut.playFrom(Duration.millis(0));

                    Point2D popupLocation = tile.localToScreen(EVT.getX() - selectorTooltip.getWidth() * 0.5, selector.getCenterY() - size * 0.025 - selectorTooltip.getHeight());
                    selectorTooltip.setText(String.format(locale, formatString, selectedValue));
                    selectorTooltip.setX(popupLocation.getX());
                    selectorTooltip.setY(popupLocation.getY());
                    selectorTooltip.show(tile.getScene().getWindow());

                    tile.fireTileEvent(new TileEvent(EventType.SELECTED_CHART_DATA, new ChartData(selectedValue)));
                    break;
                }
                lastElement = element;
            }
        }
    }

    private double[] getXYFromPathElement(final PathElement ELEMENT) {
        if (ELEMENT instanceof MoveTo) {
            return new double[]{ ((MoveTo) ELEMENT).getX(), ((MoveTo) ELEMENT).getY() };
        } else {
            return new double[] { ((LineTo) ELEMENT).getX(), ((LineTo) ELEMENT).getY() };
        }
    }


    // ******************** Resizing ******************************************
    @Override protected void resizeDynamicText() {
        double maxWidth = unitText.isVisible() ? width - size * 0.275 : width - size * 0.1;
        double fontSize = size * 0.24;
        valueText.setFont(Fonts.latoRegular(fontSize));
        if (valueText.getLayoutBounds().getWidth() > maxWidth) { Helper.adjustTextSize(valueText, maxWidth, fontSize); }
    }
    @Override protected void resizeStaticText() {
        double maxWidth = width - size * 0.1;
        double fontSize = size * textSize.factor;

        titleText.setFont(Fonts.latoRegular(fontSize));
        if (titleText.getLayoutBounds().getWidth() > maxWidth) { Helper.adjustTextSize(titleText, maxWidth, fontSize); }

        switch(tile.getTitleAlignment()) {
            default    :
            case LEFT  : titleText.relocate(size * 0.05, size * 0.05); break;
            case CENTER: titleText.relocate((width - titleText.getLayoutBounds().getWidth()) * 0.5, size * 0.05); break;
            case RIGHT : titleText.relocate(width - (size * 0.05) - titleText.getLayoutBounds().getWidth(), size * 0.05); break;
        }
    }

    @Override protected void resize() {
        super.resize();

        valueUnitFlow.setPrefWidth(contentBounds.getWidth());
        valueUnitFlow.relocate(contentBounds.getX(), contentBounds.getY());

        hStepSize = width / dataSize;
        vStepSize = (height * 0.5) / maxValue;

        selector.setRadius(size * 0.02);
        selector.setStrokeWidth(size * 0.01);

        handleData();
        strokePath.setStrokeWidth(size * 0.02);

        canvas.setWidth(width);
        canvas.setHeight(height);

        double cornerRadius = tile.getRoundedCorners() ? size * 0.05 : 0;

        fillClip.setX(0);
        fillClip.setY(0);
        fillClip.setWidth(tile.getWidth());
        fillClip.setHeight(tile.getHeight());
        fillClip.setArcWidth(cornerRadius);
        fillClip.setArcHeight(cornerRadius);

        strokeClip.setX(0);
        strokeClip.setY(0);
        strokeClip.setWidth(tile.getWidth());
        strokeClip.setHeight(tile.getHeight());
        strokeClip.setArcWidth(cornerRadius);
        strokeClip.setArcHeight(cornerRadius);
    }

    @Override protected void redraw() {
        super.redraw();

        smoothing = tile.isSmoothing();

        titleText.setText(tile.getTitle());

        valueText.setText(String.format(locale, formatString, tile.getCurrentValue()));
        unitText.setText(tile.getUnit());

        resizeDynamicText();
        resizeStaticText();

        titleText.setFill(tile.getTitleColor());
        valueText.setFill(tile.getValueColor());
        unitText.setFill(tile.getUnitColor());
        selector.setStroke(tile.getForegroundColor());
        selector.setFill(tile.getBackgroundColor());
        Color fillPathColor1 = Helper.getColorWithOpacity(tile.getBarColor(), 0.7);
        Color fillPathColor2 = Helper.getColorWithOpacity(tile.getBarColor(), 0.1);
        if (tile.isFillWithGradient() && !tile.getGradientStops().isEmpty()) {
            fillPath.setFill(new LinearGradient(0, 0, 0, 1, true, CycleMethod.NO_CYCLE, tile.getGradientStops()));
            strokePath.setStroke(tile.getGradientStops().get(0).getColor());
            if (dataPointsVisible) { drawDataPoints(points, tile.getGradientStops().get(0).getColor()); }
        } else {
            fillPath.setFill(new LinearGradient(0, 0, 0, 1, true, CycleMethod.NO_CYCLE, new Stop(0, fillPathColor1), new Stop(1, fillPathColor2)));
            strokePath.setStroke(tile.getBarColor());
            if (dataPointsVisible) { drawDataPoints(points, tile.getBarColor()); }
        }
        drawChart(points);
    }
}