/*
 * 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.XYItem;
import javafx.beans.DefaultProperty;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.StringProperty;
import javafx.beans.property.StringPropertyBase;
import javafx.collections.ObservableList;
import javafx.geometry.Orientation;
import javafx.scene.Node;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Region;

import java.util.Arrays;
import java.util.List;


/**
 * User: hansolo
 * Date: 26.07.17
 * Time: 16:52
 */
@DefaultProperty("children")
public class XYChart<T extends XYItem> extends Region {
    private static final double         PREFERRED_WIDTH  = 400;
    private static final double         PREFERRED_HEIGHT = 250;
    private static final double         MINIMUM_WIDTH    = 50;
    private static final double         MINIMUM_HEIGHT   = 50;
    private static final double         MAXIMUM_WIDTH    = 4096;
    private static final double         MAXIMUM_HEIGHT   = 4096;
    private              double         width;
    private              double         height;
    private              XYPane<T>      xyPane;
    private              List<Axis>     axis;
    private              Axis           yAxisL;
    private              Axis           yAxisC;
    private              Axis           yAxisR;
    private              Axis           xAxisT;
    private              Axis           xAxisC;
    private              Axis           xAxisB;
    private              double         topAxisHeight;
    private              double         rightAxisWidth;
    private              double         bottomAxisHeight;
    private              double         leftAxisWidth;
    private              Grid           grid;
    private              boolean        hasLeftYAxis;
    private              boolean        hasCenterYAxis;
    private              boolean        hasRightYAxis;
    private              boolean        hasTopXAxis;
    private              boolean        hasCenterXAxis;
    private              boolean        hasBottomXAxis;
    private              String         _title;
    private              StringProperty title;
    private              String         _subTitle;
    private              StringProperty subTitle;
    private              AnchorPane     pane;
    private              BooleanBinding showing;


    // ******************** Constructors **************************************
    public XYChart(final XYPane<T> XY_PANE, final Axis... AXIS) {
        this(XY_PANE, null, AXIS);
    }
    public XYChart(final XYPane<T> XY_PANE, final Grid GRID, final Axis... AXIS) {
        if (null == XY_PANE) { throw new IllegalArgumentException("XYPane has not to be null"); }
        if (XY_PANE.containsPolarChart()) { throw new IllegalArgumentException("XYPane contains Polar chart type"); }
        xyPane = XY_PANE;
        axis   = Arrays.asList(AXIS);
        grid   = GRID;
        width  = PREFERRED_WIDTH;
        height = PREFERRED_HEIGHT;
        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);
            }
        }

        checkForAxis();

        adjustChartRange();

        adjustAxisAnchors();

        pane = new AnchorPane(xyPane);
        pane.getChildren().addAll(axis);
        setGrid(grid);

        getChildren().setAll(pane);
    }

    private void registerListeners() {
        widthProperty().addListener(o -> resize());
        heightProperty().addListener(o -> resize());
        if (null != getScene()) {
            setupBinding();
        } else {
            sceneProperty().addListener((o1, ov1, nv1) -> {
                if (null == nv1) { return; }
                if (null != getScene().getWindow()) {
                    setupBinding();
                } else {
                    sceneProperty().get().windowProperty().addListener((o2, ov2, nv2) -> {
                        if (null == nv2) { return; }
                        setupBinding();
                    });
                }
            });
        }
    }


    // ******************** Methods *******************************************
    @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; }

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

    public String getTitle() { return null == title ? _title : title.get(); }
    public void setTitle(final String TITLE) {
        if (null == title) {
            _title = TITLE;
            xyPane.redraw();
        } else {
            title.set(TITLE);
        }
    }
    public StringProperty titleProperty() {
        if (null == title) {
            title = new StringPropertyBase(_title) {
                @Override protected void invalidated() { xyPane.redraw(); }
                @Override public Object getBean() { return XYChart.this; }
                @Override public String getName() { return "title"; }
            };
            _title = null;
        }
        return title;
    }

    public String getSubTitle() { return null == subTitle ? _subTitle : subTitle.get(); }
    public void setSubTitle(final String SUB_TITLE) {
        if (null == subTitle) {
            _subTitle = SUB_TITLE;
            xyPane.redraw();
        } else {
            subTitle.set(SUB_TITLE);
        }
    }
    public StringProperty subTitleProperty() {
        if (null == subTitle) {
            subTitle = new StringPropertyBase(_subTitle) {
                @Override protected void invalidated() { xyPane.redraw(); }
                @Override public Object getBean() { return XYChart.this; }
                @Override public String getName() { return "subTitle"; }
            };
            _subTitle = null;
        }
        return subTitle;
    }

    public boolean isReferenceZero() { return xyPane.isReferenceZero(); }
    public void setReferenceZero(final boolean IS_ZERO) { xyPane.setReferenceZero(IS_ZERO); }

    public void setGrid(final Grid GRID) {
        if (null == GRID) return;
        if (null != grid) { pane.getChildren().remove(grid); }
        grid = GRID;
        pane.getChildren().add(0, grid);
        adjustGridAnchors();
    }

    public XYPane<T> getXYPane() { return xyPane; }

    public void refresh() { xyPane.redraw(); }

    private void checkForAxis() {
        axis.forEach(axis -> {
            Position position = axis.getPosition();
            switch (axis.getOrientation()) {
                case HORIZONTAL:
                    switch(position) {
                        case TOP:
                            hasTopXAxis    = true;
                            topAxisHeight  = axis.getPrefHeight();
                            xAxisT         = axis;
                            break;
                        case CENTER:
                            hasCenterXAxis = true;
                            xAxisC         = axis;
                            break;
                        case BOTTOM:
                            hasBottomXAxis   = true;
                            bottomAxisHeight = axis.getPrefHeight();
                            xAxisB           = axis;
                            break;
                        default:
                            hasTopXAxis    = false;
                            hasCenterXAxis = false;
                            hasBottomXAxis = false;
                            break;
                    }
                    break;
                case VERTICAL:
                    switch(position) {
                        case LEFT:
                            hasLeftYAxis   = true;
                            leftAxisWidth  = axis.getPrefWidth();
                            yAxisL         = axis;
                            break;
                        case CENTER:
                            hasCenterYAxis = true;
                            yAxisC         = axis;
                            break;
                        case RIGHT:
                            hasRightYAxis  = true;
                            rightAxisWidth = axis.getPrefWidth();
                            yAxisR         = axis;
                            break;
                        default:
                            hasLeftYAxis   = false;
                            hasCenterYAxis = false;
                            hasRightYAxis  = false;
                            break;
                    }
                    break;
            }
        });
    }

    private void adjustChartRange() {
        if (hasBottomXAxis) {
            xyPane.setLowerBoundX(xAxisB.getMinValue());
            xyPane.setUpperBoundX(xAxisB.getMaxValue());
        } else if (hasTopXAxis) {
            xyPane.setLowerBoundX(xAxisT.getMinValue());
            xyPane.setUpperBoundX(xAxisT.getMaxValue());
        } else if (hasCenterXAxis) {
            xyPane.setLowerBoundX(xAxisC.getMinValue());
            xyPane.setUpperBoundX(xAxisC.getMaxValue());
        }

        if (hasLeftYAxis) {
            xyPane.setLowerBoundY(yAxisL.getMinValue());
            xyPane.setUpperBoundY(yAxisL.getMaxValue());
        } else if (hasRightYAxis) {
            xyPane.setLowerBoundY(yAxisR.getMinValue());
            xyPane.setUpperBoundY(yAxisR.getMaxValue());
        } else if (hasCenterYAxis) {
            xyPane.setLowerBoundY(yAxisC.getMinValue());
            xyPane.setUpperBoundY(yAxisC.getMaxValue());
        }
    }

    private void adjustAxisAnchors() {
        axis.forEach(axis -> {
            if (Orientation.HORIZONTAL == axis.getOrientation()) {
                AnchorPane.setLeftAnchor(axis, hasLeftYAxis ? leftAxisWidth : 0d);
                AnchorPane.setRightAnchor(axis, hasRightYAxis ? rightAxisWidth : 0d);

                AnchorPane.setLeftAnchor(xyPane, hasLeftYAxis ? leftAxisWidth : 0d);
                AnchorPane.setRightAnchor(xyPane, hasRightYAxis ? rightAxisWidth : 0d);
            } else {
                AnchorPane.setTopAnchor(axis, hasTopXAxis ? topAxisHeight : 0d);
                AnchorPane.setBottomAnchor(axis, hasBottomXAxis ? bottomAxisHeight : 0d);

                AnchorPane.setTopAnchor(xyPane, hasTopXAxis ? topAxisHeight : 0d);
                AnchorPane.setBottomAnchor(xyPane, hasBottomXAxis ? bottomAxisHeight : 0d);
            }
        });
    }

    private void adjustCenterAxisAnchors() {
        if (hasCenterYAxis) {
            if (hasBottomXAxis) {
                if (hasLeftYAxis) {
                    AnchorPane.setLeftAnchor(yAxisC, xAxisB.getZeroPosition() + yAxisL.getWidth());
                } else if (hasRightYAxis) {
                    AnchorPane.setLeftAnchor(yAxisC, xAxisB.getZeroPosition());
                } else if (hasCenterXAxis) {
                    AnchorPane.setLeftAnchor(yAxisC, xAxisC.getZeroPosition());
                }
            } else {
                if (hasLeftYAxis) {
                    AnchorPane.setLeftAnchor(yAxisC, xAxisT.getZeroPosition() + yAxisL.getWidth());
                } else if (hasRightYAxis) {
                    AnchorPane.setLeftAnchor(yAxisC, xAxisT.getZeroPosition());
                }  else if (hasCenterXAxis) {
                    AnchorPane.setLeftAnchor(yAxisC, xAxisC.getZeroPosition());
                }
            }
        }
        if (hasCenterXAxis) {
            if (hasLeftYAxis) {
                if (hasTopXAxis) {
                    AnchorPane.setTopAnchor(xAxisC, yAxisL.getZeroPosition() + xAxisT.getHeight());
                } else if (hasBottomXAxis) {
                    AnchorPane.setTopAnchor(xAxisC, yAxisL.getZeroPosition());
                } else if (hasCenterYAxis) {
                    AnchorPane.setTopAnchor(xAxisC, yAxisC.getZeroPosition());
                }
            } else {
                if (hasTopXAxis) {
                    AnchorPane.setTopAnchor(xAxisC, yAxisR.getZeroPosition() + xAxisT.getHeight());
                } else if (hasBottomXAxis) {
                    AnchorPane.setTopAnchor(xAxisC, yAxisR.getZeroPosition());
                } else if (hasCenterYAxis) {
                    AnchorPane.setTopAnchor(xAxisC, yAxisC.getZeroPosition());
                }
            }
        }
    }

    private void adjustGridAnchors() {
        if (null == grid) return;
        AnchorPane.setLeftAnchor(grid, hasLeftYAxis ? leftAxisWidth : 0d);
        AnchorPane.setRightAnchor(grid, hasRightYAxis ? rightAxisWidth : 0d);
        AnchorPane.setTopAnchor(grid, hasTopXAxis ? topAxisHeight : 0d);
        AnchorPane.setBottomAnchor(grid, hasBottomXAxis ? bottomAxisHeight : 0d);
    }

    private void setupBinding() {
        showing = Bindings.selectBoolean(sceneProperty(), "window", "showing");
        showing.addListener((o, ov, nv) -> { if (nv) { adjustCenterAxisAnchors(); } });
    }


    // ******************** Resizing ******************************************
    private void resize() {
        width  = getWidth() - getInsets().getLeft() - getInsets().getRight();
        height = getHeight() - getInsets().getTop() - getInsets().getBottom();

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

            adjustCenterAxisAnchors();
        }
    }
}