/*
 * Copyright (c) 2010, 2021, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package javafx.scene.control.skin;

import com.sun.javafx.PlatformUtil;
import com.sun.javafx.scene.control.skin.Utils;
import javafx.animation.Animation.Status;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.geometry.HPos;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Control;
import javafx.scene.control.TitledPane;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;

import com.sun.javafx.scene.control.behavior.TitledPaneBehavior;
import javafx.beans.binding.DoubleBinding;
import javafx.geometry.Insets;
import javafx.scene.control.Accordion;
import javafx.scene.control.Labeled;
import javafx.scene.control.ContextMenu;
import javafx.scene.input.MouseButton;
import javafx.scene.text.Font;

/**
 * Default skin implementation for the {@link TitledPane} control.
 *
 * @see TitledPane
 * @since 9
 */
public class TitledPaneSkin extends LabeledSkinBase<TitledPane>  {

    /* *************************************************************************
     *                                                                         *
     * Static fields                                                           *
     *                                                                         *
     **************************************************************************/

    private static final Duration TRANSITION_DURATION = new Duration(350.0);

    // caching results in poorer looking text (it is blurry), so we don't do it
    // unless on a low powered device (admittedly the test below isn't a great
    // indicator of power, but it'll do for now).
    private static final boolean CACHE_ANIMATION = PlatformUtil.isEmbedded();



    /* *************************************************************************
     *                                                                         *
     * Private fields                                                          *
     *                                                                         *
     **************************************************************************/

    private final TitledPaneBehavior behavior;

    private final TitleRegion titleRegion;
    private final StackPane contentContainer;
    private Node content;
    private Timeline timeline;
    private double transitionStartValue;
    private Rectangle clipRect;
    private Pos pos;
    private HPos hpos;
    private VPos vpos;



    /* *************************************************************************
     *                                                                         *
     * Constructors                                                            *
     *                                                                         *
     **************************************************************************/

    /**
     * Creates a new TitledPaneSkin instance, installing the necessary child
     * nodes into the Control {@link Control#getChildren() children} list, as
     * well as the necessary input mappings for handling key, mouse, etc events.
     *
     * @param control The control that this skin should be installed onto.
     */
    public TitledPaneSkin(final TitledPane control) {
        super(control);

        // install default input map for the TitledPane control
        this.behavior = new TitledPaneBehavior(control);
//        control.setInputMap(behavior.getInputMap());

        clipRect = new Rectangle();

        transitionStartValue = 0;
        titleRegion = new TitleRegion();

        content = getSkinnable().getContent();
        contentContainer = new StackPane() {
            {
                getStyleClass().setAll("content");

                if (content != null) {
                    getChildren().setAll(content);
                }
            }
        };
        contentContainer.setClip(clipRect);
        updateClip();

        if (control.isExpanded()) {
            setTransition(1.0f);
            setExpanded(control.isExpanded());
        } else {
            setTransition(0.0f);
            if (content != null) {
                content.setVisible(false);
            }
        }

        getChildren().setAll(contentContainer, titleRegion);

        registerChangeListener(control.contentProperty(), e -> {
            content = getSkinnable().getContent();
            if (content == null) {
                contentContainer.getChildren().clear();
            } else {
                contentContainer.getChildren().setAll(content);
            }
        });
        registerChangeListener(control.expandedProperty(), e -> setExpanded(getSkinnable().isExpanded()));
        registerChangeListener(control.collapsibleProperty(), e -> titleRegion.update());
        registerChangeListener(control.alignmentProperty(), e -> {
            pos = getSkinnable().getAlignment();
            hpos = pos.getHpos();
            vpos = pos.getVpos();
        });
        registerChangeListener(control.widthProperty(), e -> updateClip());
        registerChangeListener(control.heightProperty(), e -> updateClip());
        registerChangeListener(titleRegion.alignmentProperty(), e -> {
            pos = titleRegion.getAlignment();
            hpos = pos.getHpos();
            vpos = pos.getVpos();
        });

        pos = control.getAlignment();
        hpos = pos == null ? HPos.LEFT   : pos.getHpos();
        vpos = pos == null ? VPos.CENTER : pos.getVpos();
    }



    /* *************************************************************************
     *                                                                         *
     * Properties                                                              *
     *                                                                         *
     **************************************************************************/

    private DoubleProperty transition;
    private final void setTransition(double value) { transitionProperty().set(value); }
    private final double getTransition() { return transition == null ? 0.0 : transition.get(); }
    private final DoubleProperty transitionProperty() {
        if (transition == null) {
            transition = new SimpleDoubleProperty(this, "transition", 0.0) {
                @Override protected void invalidated() {
                    contentContainer.requestLayout();
                }
            };
        }
        return transition;
    }



    /* *************************************************************************
     *                                                                         *
     * Public API                                                              *
     *                                                                         *
     **************************************************************************/

    /** {@inheritDoc} */
    @Override public void dispose() {
        super.dispose();

        if (behavior != null) {
            behavior.dispose();
        }
    }

    // Override LabeledSkinBase updateChildren because
    // it removes all the children.  The update() in TitleRegion
    // will replace this method.
    /** {@inheritDoc} */
    @Override protected void updateChildren() {
        if (titleRegion != null) {
            titleRegion.update();
        }
    }

    /** {@inheritDoc} */
    @Override protected void layoutChildren(final double x, double y,
                                            final double w, final double h) {

        // header
        double headerHeight = snapSizeY(titleRegion.prefHeight(-1));

        titleRegion.resize(w, headerHeight);
        positionInArea(titleRegion, x, y,
                w, headerHeight, 0, HPos.LEFT, VPos.CENTER);
        titleRegion.requestLayout();

        // content
        double contentHeight = (h - headerHeight) * getTransition();
        if (isInsideAccordion()) {
            if (prefHeightFromAccordion != 0) {
                contentHeight = (prefHeightFromAccordion - headerHeight) * getTransition();
            }
        }
        contentHeight = snapSizeY(contentHeight);

        y += headerHeight;
        contentContainer.resize(w, contentHeight);
        clipRect.setHeight(contentHeight);
        positionInArea(contentContainer, x, y,
                w, contentHeight, /*baseline ignored*/0, HPos.CENTER, VPos.CENTER);
    }

    /** {@inheritDoc} */
    @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
        double titleWidth = snapSizeX(titleRegion.prefWidth(height));
        double contentWidth = snapSizeX(contentContainer.minWidth(height));
        return Math.max(titleWidth, contentWidth) + leftInset + rightInset;
    }

    /** {@inheritDoc} */
    @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
        double headerHeight = snapSizeY(titleRegion.prefHeight(width));
        double contentHeight = contentContainer.minHeight(width) * getTransition();
        return headerHeight + snapSizeY(contentHeight) + topInset + bottomInset;
    }

    /** {@inheritDoc} */
    @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
        double titleWidth = snapSizeX(titleRegion.prefWidth(height));
        double contentWidth = snapSizeX(contentContainer.prefWidth(height));
        return Math.max(titleWidth, contentWidth) + leftInset + rightInset;
    }

    /** {@inheritDoc} */
    @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
        double headerHeight = snapSizeY(titleRegion.prefHeight(width));
        double contentHeight = contentContainer.prefHeight(width) * getTransition();
        return headerHeight + snapSizeY(contentHeight) + topInset + bottomInset;
    }

    /** {@inheritDoc} */
    @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
        return Double.MAX_VALUE;
    }



    /* *************************************************************************
     *                                                                         *
     * Private implementation                                                  *
     *                                                                         *
     **************************************************************************/

    private void updateClip() {
        clipRect.setWidth(getSkinnable().getWidth());
        clipRect.setHeight(contentContainer.getHeight());
    }

    private void setExpanded(boolean expanded) {
        if (! getSkinnable().isCollapsible()) {
            setTransition(1.0f);
            return;
        }

        // we need to perform the transition between expanded / hidden
        if (getSkinnable().isAnimated()) {
            transitionStartValue = getTransition();
            doAnimationTransition();
        } else {
            if (expanded) {
                setTransition(1.0f);
            } else {
                setTransition(0.0f);
            }
            if (content != null) {
                content.setVisible(expanded);
             }
            getSkinnable().requestLayout();
        }
    }

    private boolean isInsideAccordion() {
        return getSkinnable().getParent() != null && getSkinnable().getParent() instanceof Accordion;
    }

    double getTitleRegionSize(double width) {
        return snapSizeY(titleRegion.prefHeight(width)) + snappedTopInset() + snappedBottomInset();
    }

    private double prefHeightFromAccordion = 0;
    void setMaxTitledPaneHeightForAccordion(double height) {
        this.prefHeightFromAccordion = height;
    }

    double getTitledPaneHeightForAccordion() {
        double headerHeight = snapSizeY(titleRegion.prefHeight(-1));
        double contentHeight = (prefHeightFromAccordion - headerHeight) * getTransition();
        return headerHeight + snapSizeY(contentHeight) + snappedTopInset() + snappedBottomInset();
    }

    private void doAnimationTransition() {
        if (content == null) {
            return;
        }

        Duration duration;
        if (timeline != null && (timeline.getStatus() != Status.STOPPED)) {
            duration = timeline.getCurrentTime();
            timeline.stop();
        } else {
            duration = TRANSITION_DURATION;
        }

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

        KeyFrame k1, k2;

        if (getSkinnable().isExpanded()) {
            k1 = new KeyFrame(
                Duration.ZERO,
                    event -> {
                        // start expand
                        if (CACHE_ANIMATION) content.setCache(true);
                        content.setVisible(true);
                    },
                new KeyValue(transitionProperty(), transitionStartValue)
            );

            k2 = new KeyFrame(
                duration,
                    event -> {
                        // end expand
                        if (CACHE_ANIMATION) content.setCache(false);
                    },
                new KeyValue(transitionProperty(), 1, Interpolator.LINEAR)

            );
        } else {
            k1 = new KeyFrame(
                Duration.ZERO,
                    event -> {
                        // Start collapse
                        if (CACHE_ANIMATION) content.setCache(true);
                    },
                new KeyValue(transitionProperty(), transitionStartValue)
            );

            k2 = new KeyFrame(
                duration,
                    event -> {
                        // end collapse
                        content.setVisible(false);
                        if (CACHE_ANIMATION) content.setCache(false);
                    },
                new KeyValue(transitionProperty(), 0, Interpolator.LINEAR)
            );
        }

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



    /* *************************************************************************
     *                                                                         *
     * Support classes                                                         *
     *                                                                         *
     **************************************************************************/

    class TitleRegion extends StackPane {
        private final StackPane arrowRegion;

        public TitleRegion() {
            getStyleClass().setAll("title");
            arrowRegion = new StackPane();
            arrowRegion.setId("arrowRegion");
            arrowRegion.getStyleClass().setAll("arrow-button");

            StackPane arrow = new StackPane();
            arrow.setId("arrow");
            arrow.getStyleClass().setAll("arrow");
            arrowRegion.getChildren().setAll(arrow);

            // RT-13294: TitledPane : add animation to the title arrow
            arrow.rotateProperty().bind(new DoubleBinding() {
                { bind(transitionProperty()); }

                @Override protected double computeValue() {
                    return -90 * (1.0 - getTransition());
                }
            });

            setAlignment(Pos.CENTER_LEFT);

            setOnMouseReleased(e -> {
                if( e.getButton() != MouseButton.PRIMARY ) return;
                ContextMenu contextMenu = getSkinnable().getContextMenu() ;
                if (contextMenu != null) {
                    contextMenu.hide() ;
                }
                if (getSkinnable().isCollapsible() && getSkinnable().isFocused()) {
                    behavior.toggle();
                }
            });

            // title region consists of the title and the arrow regions
            update();
        }

        private void update() {
            getChildren().clear();
            final TitledPane titledPane = getSkinnable();

            if (titledPane.isCollapsible()) {
                getChildren().add(arrowRegion);
            }

            // Only in some situations do we want to have the graphicPropertyChangedListener
            // installed. Since updateChildren() is not called much, we'll just remove it always
            // and reinstall it later if it is necessary to do so.
            if (graphic != null) {
                graphic.layoutBoundsProperty().removeListener(graphicPropertyChangedListener);
            }
            // Now update the graphic (since it may have changed)
            graphic = titledPane.getGraphic();
            // Now update the children (and add the graphicPropertyChangedListener as necessary)
            if (isIgnoreGraphic()) {
                if (titledPane.getContentDisplay() == ContentDisplay.GRAPHIC_ONLY) {
                    getChildren().clear();
                    getChildren().add(arrowRegion);
                } else {
                    getChildren().add(text);
                }
            } else {
                graphic.layoutBoundsProperty().addListener(graphicPropertyChangedListener);
                if (isIgnoreText()) {
                    getChildren().add(graphic);
                } else {
                    getChildren().addAll(graphic, text);
                }
            }
            setCursor(getSkinnable().isCollapsible() ? Cursor.HAND : Cursor.DEFAULT);
        }

        @Override protected double computePrefWidth(double height) {
            double left = snappedLeftInset();
            double right = snappedRightInset();
            double arrowWidth = 0;
            double labelPrefWidth = labelPrefWidth(height);

            if (arrowRegion != null) {
                arrowWidth = snapSizeX(arrowRegion.prefWidth(height));
            }

            return left + arrowWidth + labelPrefWidth + right;
        }

        @Override protected double computePrefHeight(double width) {
            double top = snappedTopInset();
            double bottom = snappedBottomInset();
            double arrowHeight = 0;
            double labelPrefHeight = labelPrefHeight(width);

            if (arrowRegion != null) {
                arrowHeight = snapSizeY(arrowRegion.prefHeight(width));
            }

            return top + Math.max(arrowHeight, labelPrefHeight) + bottom;
        }

        @Override protected void layoutChildren() {
            final double top = snappedTopInset();
            final double bottom = snappedBottomInset();
            final double left = snappedLeftInset();
            final double right = snappedRightInset();
            double width = getWidth() - (left + right);
            double height = getHeight() - (top + bottom);
            double arrowWidth = snapSizeX(arrowRegion.prefWidth(-1));
            double arrowHeight = snapSizeY(arrowRegion.prefHeight(-1));
            double labelWidth = snapSizeX(Math.min(width - arrowWidth / 2.0, labelPrefWidth(-1)));
            double labelHeight = snapSizeY(labelPrefHeight(-1));

            double x = left + arrowWidth + Utils.computeXOffset(width - arrowWidth, labelWidth, hpos);
            if (HPos.CENTER == hpos) {
                // We want to center the region based on the entire width of the TitledPane.
                x = left + Utils.computeXOffset(width, labelWidth, hpos);
            }
            double y = top + Utils.computeYOffset(height, Math.max(arrowHeight, labelHeight), vpos);

            arrowRegion.resize(arrowWidth, arrowHeight);
            positionInArea(arrowRegion, left, top, arrowWidth, height,
                    /*baseline ignored*/0, HPos.CENTER, VPos.CENTER);

            layoutLabelInArea(x, y, labelWidth, height, pos);
        }

        // Copied from LabeledSkinBase because the padding from TitledPane was being
        // applied to the Label when it should not be.
        private double labelPrefWidth(double height) {
            // Get the preferred width of the text
            final Labeled labeled = getSkinnable();
            final Font font = text.getFont();
            final String string = labeled.getText();
            boolean emptyText = string == null || string.isEmpty();
            Insets labelPadding = labeled.getLabelPadding();
            double widthPadding = labelPadding.getLeft() + labelPadding.getRight();
            double textWidth = emptyText ? 0 : Utils.computeTextWidth(font, string, 0);

            // Now add on the graphic, gap, and padding as appropriate
            final Node graphic = labeled.getGraphic();
            if (isIgnoreGraphic()) {
                return textWidth + widthPadding;
            } else if (isIgnoreText()) {
                return graphic.prefWidth(-1) + widthPadding;
            } else if (labeled.getContentDisplay() == ContentDisplay.LEFT
                    || labeled.getContentDisplay() == ContentDisplay.RIGHT) {
                return textWidth + labeled.getGraphicTextGap() + graphic.prefWidth(-1) + widthPadding;
            } else {
                return Math.max(textWidth, graphic.prefWidth(-1)) + widthPadding;
            }
        }

        // Copied from LabeledSkinBase because the padding from TitledPane was being
        // applied to the Label when it should not be.
        private double labelPrefHeight(double width) {
            final Labeled labeled = getSkinnable();
            final Font font = text.getFont();
            final ContentDisplay contentDisplay = labeled.getContentDisplay();
            final double gap = labeled.getGraphicTextGap();
            final Insets labelPadding = labeled.getLabelPadding();
            final double widthPadding = snappedLeftInset() + snappedRightInset() + labelPadding.getLeft() + labelPadding.getRight();

            String str = labeled.getText();
            if (str != null && str.endsWith("\n")) {
                // Strip ending newline so we don't count another row.
                str = str.substring(0, str.length() - 1);
            }

            if (!isIgnoreGraphic() &&
                (contentDisplay == ContentDisplay.LEFT || contentDisplay == ContentDisplay.RIGHT)) {
                width -= (graphic.prefWidth(-1) + gap);
            }

            width -= widthPadding;

            // TODO figure out how to cache this effectively.
            final double textHeight = Utils.computeTextHeight(font, str,
                    labeled.isWrapText() ? width : 0, text.getBoundsType());

            // Now we want to add on the graphic if necessary!
            double h = textHeight;
            if (!isIgnoreGraphic()) {
                final Node graphic = labeled.getGraphic();
                if (contentDisplay == ContentDisplay.TOP || contentDisplay == ContentDisplay.BOTTOM) {
                    h = graphic.prefHeight(-1) + gap + textHeight;
                } else {
                    h = Math.max(textHeight, graphic.prefHeight(-1));
                }
            }

            return h + labelPadding.getTop() + labelPadding.getBottom();
        }
    }
}
