/**
 * Copyright (c) 2014, 2021, ControlsFX
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution.
 *     * Neither the name of ControlsFX, any associated website, nor the
 * names of its contributors may be used to endorse or promote products
 * derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package impl.org.controlsfx.skin;

import impl.org.controlsfx.ImplUtils;
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.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.collections.ObservableList;
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.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
import org.controlsfx.control.NotificationPane;
import org.controlsfx.control.action.Action;
import org.controlsfx.control.action.ActionUtils;

@SuppressWarnings("deprecation")
public abstract class NotificationBar extends Region {

    private static final double MIN_HEIGHT = 40;

    final Label label;
    Label title;
    ButtonBar actionsBar;
    Button closeBtn;

    private final GridPane pane;
    
    public DoubleProperty transition = new SimpleDoubleProperty() {
        @Override protected void invalidated() {
            requestContainerLayout();
        }
    };

    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);

    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.setFocusTraversable(false);
            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.setFocusTraversable(false);
        label.setMaxSize(Double.MAX_VALUE, 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((InvalidationListener) o -> updatePane());

        // initialise close button area
        closeBtn = new Button();
        closeBtn.setFocusTraversable(false);
        closeBtn.setOnAction(arg0 -> hide());
        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.opacityProperty().bind(transition);
        GridPane.setMargin(closeBtn, new Insets(0, 0, 0, 8));

        GridPane.setValignment(closeBtn, VPos.TOP);

        // put it all together
        updatePane();

        focusedProperty().addListener((o, ov, hasFocus) -> {
            if (hasFocus) {
                actionsBar.requestFocus();
            }
        });

        EventHandler<KeyEvent> keyPressEventHandler = event -> {
            if (KeyCode.TAB.equals(event.getCode())) {
                handleTraversal(event);
            }
            event.consume();
        };
        addEventHandler(KeyEvent.KEY_PRESSED, keyPressEventHandler);
    }

    void updatePane() {
        actionsBar = ActionUtils.createButtonBar(getActions());
        actionsBar.opacityProperty().bind(transition);
        actionsBar.focusedProperty().addListener((o, ov, hasFocus) -> {
            if (!actionsBar.getButtons().isEmpty()) {
                actionsBar.getButtons().get(0).requestFocus();
            }
        });
        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);
        }
    }
    
    public void handleTraversal(KeyEvent event) {
        if (actionsBar != null && actionsBar.getButtons().size() > 0) {
            if (event.isShiftDown()) {
                if (actionsBar.getButtons().get(0).isFocused()) {
                    ImplUtils.focusPreviousSibling(getParent());
                }
            } else {
                if (actionsBar.getButtons().get(actionsBar.getButtons().size() - 1).isFocused()) {
                    ImplUtils.focusNextSibling(getParent());
                }
            }
        }
    }

    @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();
    }

    // --- animation timeline code
    private final Duration TRANSITION_DURATION = new Duration(350.0);
    private Timeline timeline;
    private double transitionStartValue;
    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, event -> {
                        // start expand
                        setCache(true);
                        setVisible(true);
                        pane.fireEvent(new Event(NotificationPane.ON_SHOWING));
                    },
                    new KeyValue(transition, transitionStartValue)
            );

            k2 = new KeyFrame(duration, 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, event -> {
                        // Start collapse
                        pane.setCache(true);
                        pane.fireEvent(new Event(NotificationPane.ON_HIDING));
                    },
                    new KeyValue(transition, transitionStartValue)
            );

            k2 = new KeyFrame(duration, 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();
    }
}

