/*
 * Copyright 2015 Laurent Pellegrino
 *
 * 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.controlsfx.yfiton;

import javafx.animation.Animation.Status;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.stage.Screen;
import javafx.util.Duration;

import org.controlsfx.control.NotificationPane;
import org.controlsfx.control.action.Action;
import org.controlsfx.control.action.ActionUtils;

/**
 * Copied and adapted from controlsfx source code.
 */
@SuppressWarnings("deprecation")
public abstract class NotificationBar extends Region {

    private static final double MIN_HEIGHT = 40;

    final Label label;
    private final GridPane pane;
    // --- animation timeline code
    private final Duration TRANSITION_DURATION = new Duration(350.0);
    public DoubleProperty transition = new SimpleDoubleProperty() {
        @Override
        protected void invalidated() {
            requestContainerLayout();
        }
    };
    Label title;
    ButtonBar actionsBar;
    Button closeBtn;
    EventHandler<ActionEvent> onCloseAction;
    private Timeline timeline;
    private double transitionStartValue;

    public NotificationBar() {
        getStyleClass().add("notification-bar"); //$NON-NLS-1$

        setVisible(isShowing());

        pane = new GridPane();
        pane.getStyleClass().add("pane"); //$NON-NLS-1$
        pane.setAlignment(Pos.BASELINE_LEFT);
        getChildren().setAll(pane);

        // initialise title area, if one is set
        String titleStr = getTitle();
        if (titleStr != null && !titleStr.isEmpty()) {
            title = new Label();
            title.getStyleClass().add("title"); //$NON-NLS-1$
            title.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
            GridPane.setHgrow(title, Priority.ALWAYS);

            title.setText(titleStr);
            title.opacityProperty().bind(transition);
        }

        // initialise label area
        label = new Label();
        label.setWrapText(true);

        label.setMaxSize(0.4 * Screen.getPrimary().getVisualBounds().getWidth(), Double.MAX_VALUE);
        GridPane.setVgrow(label, Priority.ALWAYS);
        GridPane.setHgrow(label, Priority.ALWAYS);

        label.setText(getText());
        label.setGraphic(getGraphic());
        label.opacityProperty().bind(transition);

        // initialise actions area
        getActions().addListener(new InvalidationListener() {
            @Override
            public void invalidated(Observable arg0) {
                updatePane();
            }
        });

        // initialise close button area
        closeBtn = new Button();
        closeBtn.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent arg0) {
                hide();
                if (onCloseAction != null) {
                    onCloseAction.handle(arg0);
                }
            }
        });
        closeBtn.getStyleClass().setAll("close-button"); //$NON-NLS-1$
        StackPane graphic = new StackPane();
        graphic.getStyleClass().setAll("graphic"); //$NON-NLS-1$
        closeBtn.setGraphic(graphic);
        closeBtn.setMinSize(17, 17);
        closeBtn.setPrefSize(17, 17);
        closeBtn.setFocusTraversable(false);
        closeBtn.opacityProperty().bind(transition);
        GridPane.setMargin(closeBtn, new Insets(0, 0, 0, 8));

        // position the close button in the best place, depending on the height
        double minHeight = minHeight(-1);
        GridPane.setValignment(closeBtn, minHeight == MIN_HEIGHT ? VPos.CENTER : VPos.TOP);

        // put it all together
        updatePane();
    }

    public void requestContainerLayout() {
        layoutChildren();
    }

    public String getTitle() {
        return ""; //$NON-NLS-1$
    }

    public boolean isCloseButtonVisible() {
        return true;
    }

    public abstract String getText();

    public abstract Node getGraphic();

    public abstract ObservableList<Action> getActions();

    public abstract void hide();

    public abstract boolean isShowing();

    public abstract boolean isShowFromTop();

    public abstract double getContainerHeight();

    public abstract void relocateInParent(double x, double y);

    void updatePane() {
        actionsBar = ActionUtils.createButtonBar(getActions());
        actionsBar.opacityProperty().bind(transition);
        GridPane.setHgrow(actionsBar, Priority.SOMETIMES);
        pane.getChildren().clear();

        int row = 0;

        if (title != null) {
            pane.add(title, 0, row++);
        }

        pane.add(label, 0, row);
        pane.add(actionsBar, 1, row);

        if (isCloseButtonVisible()) {
            pane.add(closeBtn, 2, 0, 1, row + 1);
        }
    }

    @Override
    protected void layoutChildren() {
        final double w = getWidth();
        final double h = computePrefHeight(-1);

        final double notificationBarHeight = prefHeight(w);
        final double notificationMinHeight = minHeight(w);

        if (isShowFromTop()) {
            // place at top of area
            pane.resize(w, h);
            relocateInParent(0, (transition.get() - 1) * notificationMinHeight);
        } else {
            // place at bottom of area
            pane.resize(w, notificationBarHeight);
            relocateInParent(0, getContainerHeight() - notificationBarHeight);
        }
    }

    @Override
    protected double computeMinHeight(double width) {
        return Math.max(super.computePrefHeight(width), MIN_HEIGHT);
    }

    @Override
    protected double computePrefHeight(double width) {
        return Math.max(pane.prefHeight(width), minHeight(width)) * transition.get();
    }

    public void doShow() {
        transitionStartValue = 0;
        doAnimationTransition();
    }

    public void doHide() {
        transitionStartValue = 1;
        doAnimationTransition();
    }

    public void setOnCloseAction(EventHandler<ActionEvent> onAction) {
        this.onCloseAction = onAction;
    }

    private void doAnimationTransition() {
        Duration duration;

        if (timeline != null && (timeline.getStatus() != Status.STOPPED)) {
            duration = timeline.getCurrentTime();

            // fix for #70 - the notification pane freezes up as it has zero
            // duration to expand / contract
            duration = duration == Duration.ZERO ? TRANSITION_DURATION : duration;
            transitionStartValue = transition.get();
            // --- end of fix

            timeline.stop();
        } else {
            duration = TRANSITION_DURATION;
        }

        timeline = new Timeline();
        timeline.setCycleCount(1);

        KeyFrame k1, k2;

        if (isShowing()) {
            k1 = new KeyFrame(
                    Duration.ZERO,
                    new EventHandler<ActionEvent>() {
                        @Override
                        public void handle(ActionEvent event) {
                            // start expand
                            setCache(true);
                            setVisible(true);

                            pane.fireEvent(new Event(NotificationPane.ON_SHOWING));
                        }
                    },
                    new KeyValue(transition, transitionStartValue)
            );

            k2 = new KeyFrame(
                    duration,
                    new EventHandler<ActionEvent>() {
                        @Override
                        public void handle(ActionEvent event) {
                            // end expand
                            pane.setCache(false);

                            pane.fireEvent(new Event(NotificationPane.ON_SHOWN));
                        }
                    },
                    new KeyValue(transition, 1, Interpolator.EASE_OUT)

            );
        } else {
            k1 = new KeyFrame(
                    Duration.ZERO,
                    new EventHandler<ActionEvent>() {
                        @Override
                        public void handle(ActionEvent event) {
                            // Start collapse
                            pane.setCache(true);

                            pane.fireEvent(new Event(NotificationPane.ON_HIDING));
                        }
                    },
                    new KeyValue(transition, transitionStartValue)
            );

            k2 = new KeyFrame(
                    duration,
                    new EventHandler<ActionEvent>() {
                        @Override
                        public void handle(ActionEvent event) {
                            // end collapse
                            setCache(false);
                            setVisible(false);

                            pane.fireEvent(new Event(NotificationPane.ON_HIDDEN));
                        }
                    },
                    new KeyValue(transition, 0, Interpolator.EASE_IN)
            );
        }

        timeline.getKeyFrames().setAll(k1, k2);
        timeline.play();
    }
}