/*
 * Copyright (c) 2010, 2014, 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;


import com.sun.javafx.beans.IDProperty;
import com.sun.javafx.css.StyleManager;

import javafx.css.SimpleStyleableBooleanProperty;
import javafx.css.SimpleStyleableDoubleProperty;
import javafx.css.SimpleStyleableObjectProperty;
import javafx.css.StyleOrigin;
import javafx.css.StyleableObjectProperty;
import javafx.css.StyleableStringProperty;

import com.sun.javafx.css.converters.BooleanConverter;
import com.sun.javafx.css.converters.EnumConverter;
import com.sun.javafx.css.converters.SizeConverter;
import com.sun.javafx.css.converters.StringConverter;
import com.sun.javafx.scene.control.skin.TooltipSkin;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.property.*;
import javafx.beans.value.WritableValue;
import javafx.css.CssMetaData;
import javafx.css.FontCssMetaData;
import javafx.css.Styleable;
import javafx.css.StyleableProperty;
import javafx.event.EventHandler;
import javafx.geometry.NodeOrientation;
import javafx.scene.AccessibleRole;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.text.Font;
import javafx.scene.text.TextAlignment;
import javafx.stage.Window;
import javafx.util.Duration;


/**
 * Tooltips are common UI elements which are typically used for showing
 * additional information about a Node in the scenegraph when the Node is 
 * hovered over by the mouse. Any Node can show a tooltip. In most cases a 
 * Tooltip is created and its {@link #textProperty() text} property is modified
 * to show plain text to the user. However, a Tooltip is able to show within it
 * an arbitrary scenegraph of nodes - this is done by creating the scenegraph
 * and setting it inside the Tooltip {@link #graphicProperty() graphic} 
 * property. 
 * 
 * <p>You use the following approach to set a Tooltip on any node:
 *
 * <pre>
 * Rectangle rect = new Rectangle(0, 0, 100, 100);
 * Tooltip t = new Tooltip("A Square");
 * Tooltip.install(rect, t);
 * </pre>
 *
 * This tooltip will then participate with the typical tooltip semantics (i.e.
 * appearing on hover, etc). Note that the Tooltip does not have to be 
 * uninstalled: it will be garbage collected when it is not referenced by any
 * Node. It is possible to manually uninstall the tooltip, however.
 *
 * <p>A single tooltip can be installed on multiple target nodes or multiple
 * controls.
 * 
 * <p>Because most Tooltips are shown on UI controls, there is special API
 * for all controls to make installing a Tooltip less verbose. The example below 
 * shows how to create a tooltip for a Button control:
 *
 * <pre>
 * import javafx.scene.control.Tooltip;
 * import javafx.scene.control.Button;
 *
 * Button button = new Button("Hover Over Me");
 * button.setTooltip(new Tooltip("Tooltip for Button"));
 * </pre>
 * @since JavaFX 2.0
 */
@IDProperty("id")
public class Tooltip extends PopupControl {
    private static String TOOLTIP_PROP_KEY = "javafx.scene.control.Tooltip";

    // RT-31134 : the tooltip style includes a shadow around the tooltip with a
    // width of 9 and height of 5. This causes mouse events to not reach the control
    // underneath resulting in losing hover state on the control while the tooltip is showing.
    // Displaying the tooltip at an offset indicated below resolves this issue.
    // RT-37107: The y-offset was upped to 7 to ensure no overlaps when the tooltip
    // is shown near the right edge of the screen.
    private static int TOOLTIP_XOFFSET = 10;
    private static int TOOLTIP_YOFFSET = 7;

    private static TooltipBehavior BEHAVIOR = new TooltipBehavior(
        new Duration(1000), new Duration(5000), new Duration(200), false);

    /**
     * Associates the given {@link Tooltip} with the given {@link Node}. The tooltip
     * can then behave similar to when it is set on any {@link Control}. A single
     * tooltip can be associated with multiple nodes.
     * @see Tooltip
     */
    public static void install(Node node, Tooltip t) {
        BEHAVIOR.install(node, t);
    }

    /**
     * Removes the association of the given {@link Tooltip} on the specified
     * {@link Node}. Hence hovering on the node will no longer result in showing of the
     * tooltip.
     * @see Tooltip
     */
    public static void uninstall(Node node, Tooltip t) {
        BEHAVIOR.uninstall(node);
    }
    
    /***************************************************************************
     *                                                                         *
     * Constructors                                                            *
     *                                                                         *
     **************************************************************************/
    
    /**
     * Creates a tooltip with an empty string for its text.
     */
    public Tooltip() {
        this(null);
    }

    /**
     * Creates a tooltip with the specified text.
     *
     * @param text A text string for the tooltip.
     */
    public Tooltip(String text) {
        super();
        if (text != null) setText(text);
        bridge = new CSSBridge();
        getContent().setAll(bridge);
        getStyleClass().setAll("tooltip");
    }

    /***************************************************************************
     *                                                                         *
     * Properties                                                              *
     *                                                                         *
     **************************************************************************/
    /**
     * The text to display in the tooltip. If the text is set to null, an empty
     * string will be displayed, despite the value being null.
     */
    public final StringProperty textProperty() { return text; }
    public final void setText(String value) {
        if (isShowing() && value != null && !value.equals(getText())) {
            //Dynamic tooltip content is location-dependant.
            //Chromium trick.
            setAnchorX(BEHAVIOR.lastMouseX);
            setAnchorY(BEHAVIOR.lastMouseY);
        }
        textProperty().setValue(value);
    }
    public final String getText() { return text.getValue() == null ? "" : text.getValue(); }
    private final StringProperty text = new SimpleStringProperty(this, "text", "");

    /**
     * Specifies the behavior for lines of text <em>when text is multiline</em>.
     * Unlike {@link #contentDisplayProperty() contentDisplay} which affects the 
     * graphic and text, this setting only affects multiple lines of text 
     * relative to the text bounds.
     */
    public final ObjectProperty<TextAlignment> textAlignmentProperty() {  
        return textAlignment;
    }
    public final void setTextAlignment(TextAlignment value) {
        textAlignmentProperty().setValue(value);
    }
    public final TextAlignment getTextAlignment() {
        return textAlignmentProperty().getValue();
    }
    private final ObjectProperty<TextAlignment> textAlignment =
            new SimpleStyleableObjectProperty<>(TEXT_ALIGNMENT, this, "textAlignment", TextAlignment.LEFT);;

    /**
     * Specifies the behavior to use if the text of the {@code Tooltip}
     * exceeds the available space for rendering the text.
     */
    public final ObjectProperty<OverrunStyle> textOverrunProperty() {
        return textOverrun;
    }
    public final void setTextOverrun(OverrunStyle value) {
        textOverrunProperty().setValue(value);
    }
    public final OverrunStyle getTextOverrun() {
        return textOverrunProperty().getValue();
    }
    private final ObjectProperty<OverrunStyle> textOverrun =
            new SimpleStyleableObjectProperty<OverrunStyle>(TEXT_OVERRUN, this, "textOverrun", OverrunStyle.ELLIPSIS);

    /**
     * If a run of text exceeds the width of the Tooltip, then this variable
     * indicates whether the text should wrap onto another line.
     */
    public final BooleanProperty wrapTextProperty() {
        return wrapText;
    }
    public final void setWrapText(boolean value) {
        wrapTextProperty().setValue(value);
    }
    public final boolean isWrapText() {
        return wrapTextProperty().getValue();
    }
    private final BooleanProperty wrapText =
            new SimpleStyleableBooleanProperty(WRAP_TEXT, this, "wrapText", false);


    /**
     * The default font to use for text in the Tooltip. If the Tooltip's text is
     * rich text then this font may or may not be used depending on the font
     * information embedded in the rich text, but in any case where a default
     * font is required, this font will be used.
     */
    public final ObjectProperty<Font> fontProperty() {
        return font;
    }
    public final void setFont(Font value) {
        fontProperty().setValue(value);
    }
    public final Font getFont() {
        return fontProperty().getValue();
    }
    private final ObjectProperty<Font> font = new StyleableObjectProperty<Font>(Font.getDefault()) {
        private boolean fontSetByCss = false;

        @Override public void applyStyle(StyleOrigin newOrigin, Font value) {
            // RT-20727 - if CSS is setting the font, then make sure invalidate doesn't call impl_reapplyCSS
            try {
                // super.applyStyle calls set which might throw if value is bound.
                // Have to make sure fontSetByCss is reset.
                fontSetByCss = true;
                super.applyStyle(newOrigin, value);
            } catch(Exception e) {
                throw e;
            } finally {
                fontSetByCss = false;
            }
        }

        @Override public void set(Font value) {
            final Font oldValue = get();
            if (value != null ? !value.equals(oldValue) : oldValue != null) {
                super.set(value);
            }
        }

        @Override protected void invalidated() {
            // RT-20727 - if font is changed by calling setFont, then
            // css might need to be reapplied since font size affects
            // calculated values for styles with relative values
            if(fontSetByCss == false) {
                Tooltip.this.bridge.impl_reapplyCSS();
            }
        }

        @Override public CssMetaData<Tooltip.CSSBridge,Font> getCssMetaData() {
            return FONT;
        }

        @Override public Object getBean() {
            return Tooltip.this;
        }

        @Override public String getName() {
            return "font";
        }
    };

    /**
     * An optional icon for the Tooltip. This can be positioned relative to the
     * text by using the {@link #contentDisplayProperty() content display} 
     * property.
     * The node specified for this variable cannot appear elsewhere in the
     * scene graph, otherwise the {@code IllegalArgumentException} is thrown.
     * See the class description of {@link javafx.scene.Node Node} for more detail.
     */
    public final ObjectProperty<Node> graphicProperty() {
        return graphic;
    }
    public final void setGraphic(Node value) {
        graphicProperty().setValue(value);
    }
    public final Node getGraphic() {
        return graphicProperty().getValue();
    }
    private final ObjectProperty<Node> graphic = new StyleableObjectProperty<Node>() {
        // The graphic is styleable by css, but it is the
        // imageUrlProperty that handles the style value.
        @Override public CssMetaData getCssMetaData() {
            return GRAPHIC;
        }

        @Override public Object getBean() {
            return Tooltip.this;
        }

        @Override public String getName() {
            return "graphic";
        }

    };

    private StyleableStringProperty imageUrlProperty() {
        if (imageUrl == null) {
            imageUrl = new StyleableStringProperty() {
                // If imageUrlProperty is invalidated, this is the origin of the style that
                // triggered the invalidation. This is used in the invaildated() method where the
                // value of super.getStyleOrigin() is not valid until after the call to set(v) returns,
                // by which time invalidated will have been called.
                // This value is initialized to USER in case someone calls set on the imageUrlProperty, which
                // is possible:
                //     CssMetaData metaData = ((StyleableProperty)labeled.graphicProperty()).getCssMetaData();
                //     StyleableProperty prop = metaData.getStyleableProperty(labeled);
                //     prop.set(someUrl);
                //
                // TODO: Note that prop != labeled, which violates the contract between StyleableProperty and CssMetaData.
                StyleOrigin origin = StyleOrigin.USER;

                @Override public void applyStyle(StyleOrigin origin, String v) {

                    this.origin = origin;

                    // Don't want applyStyle to throw an exception which would leave this.origin set to the wrong value
                    if (graphic == null || graphic.isBound() == false) super.applyStyle(origin, v);

                    // Origin is only valid for this invocation of applyStyle, so reset it to USER in case someone calls set.
                    this.origin = StyleOrigin.USER;
                }

                @Override protected void invalidated() {

                    // need to call super.get() here since get() is overridden to return the graphicProperty's value
                    final String url = super.get();

                    if (url == null) {
                        ((StyleableProperty<Node>)(WritableValue<Node>)graphicProperty()).applyStyle(origin, null);
                    } else {
                        // RT-34466 - if graphic's url is the same as this property's value, then don't overwrite.
                        final Node graphicNode = Tooltip.this.getGraphic();
                        if (graphicNode instanceof ImageView) {
                            final ImageView imageView = (ImageView)graphicNode;
                            final Image image = imageView.getImage();
                            if (image != null) {
                                final String imageViewUrl = image.impl_getUrl();
                                if (url.equals(imageViewUrl)) return;
                            }

                        }

                        final Image img = StyleManager.getInstance().getCachedImage(url);

                        if (img != null) {
                            // Note that it is tempting to try to re-use existing ImageView simply by setting
                            // the image on the current ImageView, if there is one. This would effectively change
                            // the image, but not the ImageView which means that no graphicProperty listeners would
                            // be notified. This is probably not what we want.

                            // Have to call applyStyle on graphicProperty so that the graphicProperty's
                            // origin matches the imageUrlProperty's origin.
                            ((StyleableProperty<Node>)(WritableValue<Node>)graphicProperty()).applyStyle(origin, new ImageView(img));
                        }
                    }
                }

                @Override public String get() {
                    // The value of the imageUrlProperty is that of the graphicProperty.
                    // Return the value in a way that doesn't expand the graphicProperty.
                    final Node graphic = getGraphic();
                    if (graphic instanceof ImageView) {
                        final Image image = ((ImageView)graphic).getImage();
                        if (image != null) {
                            return image.impl_getUrl();
                        }
                    }
                    return null;
                }

                @Override public StyleOrigin getStyleOrigin() {
                    // The origin of the imageUrlProperty is that of the graphicProperty.
                    // Return the origin in a way that doesn't expand the graphicProperty.
                    return graphic != null ? ((StyleableProperty<Node>)(WritableValue<Node>)graphic).getStyleOrigin() : null;
                }

                @Override public Object getBean() {
                    return Tooltip.this;
                }

                @Override public String getName() {
                    return "imageUrl";
                }

                @Override public CssMetaData<Tooltip.CSSBridge,String> getCssMetaData() {
                    return GRAPHIC;
                }
            };
        }
        return imageUrl;
    }

    private StyleableStringProperty imageUrl = null;

    /**
     * Specifies the positioning of the graphic relative to the text.
     */
    public final ObjectProperty<ContentDisplay> contentDisplayProperty() {
        return contentDisplay;
    }
    public final void setContentDisplay(ContentDisplay value) {
        contentDisplayProperty().setValue(value);
    }
    public final ContentDisplay getContentDisplay() {
        return contentDisplayProperty().getValue();
    }
    private final ObjectProperty<ContentDisplay> contentDisplay =
            new SimpleStyleableObjectProperty<>(CONTENT_DISPLAY, this, "contentDisplay", ContentDisplay.LEFT);

    /**
     * The amount of space between the graphic and text
     */
    public final DoubleProperty graphicTextGapProperty() {
        return graphicTextGap;
    }
    public final void setGraphicTextGap(double value) {
        graphicTextGapProperty().setValue(value);
    }
    public final double getGraphicTextGap() {
        return graphicTextGapProperty().getValue();
    }
    private final DoubleProperty graphicTextGap =
            new SimpleStyleableDoubleProperty(GRAPHIC_TEXT_GAP, this, "graphicTextGap", 4d);

    /**
     * Typically, the tooltip is "activated" when the mouse moves over a Control.
     * There is usually some delay between when the Tooltip becomes "activated"
     * and when it is actually shown. The details (such as the amount of delay, etc)
     * is left to the Skin implementation.
     */
    private final ReadOnlyBooleanWrapper activated = new ReadOnlyBooleanWrapper(this, "activated");
    final void setActivated(boolean value) { activated.set(value); }
    public final boolean isActivated() { return activated.get(); }
    public final ReadOnlyBooleanProperty activatedProperty() { return activated.getReadOnlyProperty(); }



    /***************************************************************************
     *                                                                         *
     * Methods                                                                 *
     *                                                                         *
     **************************************************************************/

    /** {@inheritDoc} */
    @Override protected Skin<?> createDefaultSkin() {
        return new TooltipSkin(this);
    }



    /***************************************************************************
     *                                                                         *
     *                         Stylesheet Handling                             *
     *                                                                         *
     **************************************************************************/


    private static final CssMetaData<Tooltip.CSSBridge,Font> FONT =
            new FontCssMetaData<Tooltip.CSSBridge>("-fx-font", Font.getDefault()) {

                @Override
                public boolean isSettable(Tooltip.CSSBridge cssBridge) {
                    return !cssBridge.tooltip.fontProperty().isBound();
                }

                @Override
                public StyleableProperty<Font> getStyleableProperty(Tooltip.CSSBridge cssBridge) {
                    return (StyleableProperty<Font>)(WritableValue<Font>)cssBridge.tooltip.fontProperty();
                }
            };

    private static final CssMetaData<Tooltip.CSSBridge,TextAlignment> TEXT_ALIGNMENT =
            new CssMetaData<Tooltip.CSSBridge,TextAlignment>("-fx-text-alignment",
                    new EnumConverter<TextAlignment>(TextAlignment.class),
                    TextAlignment.LEFT) {

                @Override
                public boolean isSettable(Tooltip.CSSBridge cssBridge) {
                    return !cssBridge.tooltip.textAlignmentProperty().isBound();
                }

                @Override
                public StyleableProperty<TextAlignment> getStyleableProperty(Tooltip.CSSBridge cssBridge) {
                    return (StyleableProperty<TextAlignment>)(WritableValue<TextAlignment>)cssBridge.tooltip.textAlignmentProperty();
                }
            };

    private static final CssMetaData<Tooltip.CSSBridge,OverrunStyle> TEXT_OVERRUN =
            new CssMetaData<Tooltip.CSSBridge,OverrunStyle>("-fx-text-overrun",
                    new EnumConverter<OverrunStyle>(OverrunStyle.class),
                    OverrunStyle.ELLIPSIS) {

                @Override
                public boolean isSettable(Tooltip.CSSBridge cssBridge) {
                    return !cssBridge.tooltip.textOverrunProperty().isBound();
                }

                @Override
                public StyleableProperty<OverrunStyle> getStyleableProperty(Tooltip.CSSBridge cssBridge) {
                    return (StyleableProperty<OverrunStyle>)(WritableValue<OverrunStyle>)cssBridge.tooltip.textOverrunProperty();
                }
            };

    private static final CssMetaData<Tooltip.CSSBridge,Boolean> WRAP_TEXT =
            new CssMetaData<Tooltip.CSSBridge,Boolean>("-fx-wrap-text",
                    BooleanConverter.getInstance(), Boolean.FALSE) {

                @Override
                public boolean isSettable(Tooltip.CSSBridge cssBridge) {
                    return !cssBridge.tooltip.wrapTextProperty().isBound();
                }

                @Override
                public StyleableProperty<Boolean> getStyleableProperty(Tooltip.CSSBridge cssBridge) {
                    return (StyleableProperty<Boolean>)(WritableValue<Boolean>)cssBridge.tooltip.wrapTextProperty();
                }
            };

    private static final CssMetaData<Tooltip.CSSBridge,String> GRAPHIC =
            new CssMetaData<Tooltip.CSSBridge,String>("-fx-graphic",
                    StringConverter.getInstance()) {

                @Override
                public boolean isSettable(Tooltip.CSSBridge cssBridge) {
                    return !cssBridge.tooltip.graphicProperty().isBound();
                }

                @Override
                public StyleableProperty<String> getStyleableProperty(Tooltip.CSSBridge cssBridge) {
                    return (StyleableProperty<String>)cssBridge.tooltip.imageUrlProperty();
                }
            };

    private static final CssMetaData<Tooltip.CSSBridge,ContentDisplay> CONTENT_DISPLAY =
            new CssMetaData<Tooltip.CSSBridge,ContentDisplay>("-fx-content-display",
                    new EnumConverter<ContentDisplay>(ContentDisplay.class),
                    ContentDisplay.LEFT) {

                @Override
                public boolean isSettable(Tooltip.CSSBridge cssBridge) {
                    return !cssBridge.tooltip.contentDisplayProperty().isBound();
                }

                @Override
                public StyleableProperty<ContentDisplay> getStyleableProperty(Tooltip.CSSBridge cssBridge) {
                    return (StyleableProperty<ContentDisplay>)(WritableValue<ContentDisplay>)cssBridge.tooltip.contentDisplayProperty();
                }
            };

    private static final CssMetaData<Tooltip.CSSBridge,Number> GRAPHIC_TEXT_GAP =
            new CssMetaData<Tooltip.CSSBridge,Number>("-fx-graphic-text-gap",
                    SizeConverter.getInstance(), 4.0) {

                @Override
                public boolean isSettable(Tooltip.CSSBridge cssBridge) {
                    return !cssBridge.tooltip.graphicTextGapProperty().isBound();
                }

                @Override
                public StyleableProperty<Number> getStyleableProperty(Tooltip.CSSBridge cssBridge) {
                    return (StyleableProperty<Number>)(WritableValue<Number>)cssBridge.tooltip.graphicTextGapProperty();
                }
            };

    private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
    static {
        final List<CssMetaData<? extends Styleable, ?>> styleables =
                new ArrayList<CssMetaData<? extends Styleable, ?>>(PopupControl.getClassCssMetaData());
        styleables.add(FONT);
        styleables.add(TEXT_ALIGNMENT);
        styleables.add(TEXT_OVERRUN);
        styleables.add(WRAP_TEXT);
        styleables.add(GRAPHIC);
        styleables.add(CONTENT_DISPLAY);
        styleables.add(GRAPHIC_TEXT_GAP);
        STYLEABLES = Collections.unmodifiableList(styleables);
    }

    /**
     * @return The CssMetaData associated with this class, which may include the
     * CssMetaData of its super classes.
     * @since JavaFX 8.0
     */
    public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
        return STYLEABLES;
    }

    /**
     * {@inheritDoc}
     * @since JavaFX 8.0
     */
    @Override
    public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
        return getClassCssMetaData();
    }

    @Override public Styleable getStyleableParent() {
        if (BEHAVIOR.hoveredNode == null) {
            return super.getStyleableParent();
        }
        return BEHAVIOR.hoveredNode;
    }



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

    private final class CSSBridge extends PopupControl.CSSBridge {
        private Tooltip tooltip = Tooltip.this;

        CSSBridge() {
            super();
            setAccessibleRole(AccessibleRole.TOOLTIP);
        }
    }


    private static class TooltipBehavior {
        /*
         * There are two key concepts with Tooltip: activated and visible. A Tooltip
         * is activated as soon as a mouse move occurs over the target node. When it
         * becomes activated, we start off the ACTIVATION_TIMER. If the
         * ACTIVATION_TIMER expires before another mouse event occurs, then we will
         * show the popup. This timer typically lasts about 1 second.
         *
         * Once visible, we reset the ACTIVATION_TIMER and start the HIDE_TIMER.
         * This second timer will allow the tooltip to remain visible for some time
         * period (such as 5 seconds). If the mouse hasn't moved, and the HIDE_TIMER
         * expires, then the tooltip is hidden and the tooltip is no longer
         * activated.
         *
         * If another mouse move occurs, the ACTIVATION_TIMER starts again, and the
         * same rules apply as above.
         *
         * If a mouse exit event occurs while the HIDE_TIMER is ticking, we reset
         * the HIDE_TIMER. Thus, the tooltip disappears after 5 seconds from the
         * last mouse move.
         *
         * If some other mouse event occurs while the HIDE_TIMER is running, other
         * than mouse move or mouse enter/exit (such as a click), then the tooltip
         * is hidden, the HIDE_TIMER stopped, and activated set to false.
         *
         * If a mouse exit occurs while the HIDE_TIMER is running, we stop the
         * HIDE_TIMER and start the LEFT_TIMER, and immediately hide the tooltip.
         * This timer is very short, maybe about a 1/2 second. If the mouse enters a
         * new node which also has a tooltip before LEFT_TIMER expires, then the
         * second tooltip is activated and shown immediately (the ACTIVATION_TIMER
         * having been bypassed), and the HIDE_TIMER is started. If the LEFT_TIMER
         * expires and there is no mouse movement over a control with a tooltip,
         * then we are back to the initial steady state where the next mouse move
         * over a node with a tooltip installed will start the ACTIVATION_TIMER.
         */

        private Timeline activationTimer = new Timeline();
        private Timeline hideTimer = new Timeline();
        private Timeline leftTimer = new Timeline();

        /**
         * The Node with a tooltip over which the mouse is hovering. There can
         * only be one of these at a time.
         */
        private Node hoveredNode;

        /**
         * The tooltip that is currently activated. There can only be one
         * of these at a time.
         */
        private Tooltip activatedTooltip;

        /**
         * The tooltip that is currently visible. There can only be one
         * of these at a time.
         */
        private Tooltip visibleTooltip;

        /**
         * The last position of the mouse, in screen coordinates.
         */
        private double lastMouseX;
        private double lastMouseY;

        private boolean hideOnExit;

        TooltipBehavior(Duration openDelay, Duration visibleDuration, Duration closeDelay, final boolean hideOnExit) {
            this.hideOnExit = hideOnExit;

            activationTimer.getKeyFrames().add(new KeyFrame(openDelay));
            activationTimer.setOnFinished(event -> {
                // Show the currently activated tooltip and start the
                // HIDE_TIMER.
                assert activatedTooltip != null;
                final Window owner = getWindow(hoveredNode);
                final boolean treeVisible = isWindowHierarchyVisible(hoveredNode);

                // If the ACTIVATED tooltip is part of a visible window
                // hierarchy, we can go ahead and show the tooltip and
                // start the HIDE_TIMER.
                //
                // If the owner is null or invisible, then it either means a
                // bug in our code, the node was removed from a scene or
                // window or made invisible, or the node is not part of a
                // visible window hierarchy. In that case, we don't show the
                // tooltip, and we don't start the HIDE_TIMER. We simply let
                // ACTIVATED_TIMER expire, and wait until the next mouse
                // the movement to start it again.
                if (owner != null && owner.isShowing() && treeVisible) {
                    double x = lastMouseX;
                    double y = lastMouseY;

                    // The tooltip always inherits the nodeOrientation of
                    // the Node that it is attached to (see RT-26147). It
                    // is possible to override this for the Tooltip content
                    // (but not the popup placement) by setting the
                    // nodeOrientation on tooltip.getScene().getRoot().
                    NodeOrientation nodeOrientation = hoveredNode.getEffectiveNodeOrientation();
                    activatedTooltip.getScene().setNodeOrientation(nodeOrientation);
                    if (nodeOrientation == NodeOrientation.RIGHT_TO_LEFT) {
                        x -= activatedTooltip.getWidth();
                    }

                    activatedTooltip.show(owner, x+TOOLTIP_XOFFSET, y+TOOLTIP_YOFFSET);

                    // RT-37107: Ensure the tooltip is displayed in a position
                    // where it will not be under the mouse, even when the tooltip
                    // is near the edge of the screen
                    if ((y+TOOLTIP_YOFFSET) > activatedTooltip.getAnchorY()) {
                        // the tooltip has been shifted vertically upwards,
                        // most likely to be underneath the mouse cursor, so we
                        // need to shift it further by hiding and reshowing
                        // in another location
                        activatedTooltip.hide();

                        y -= activatedTooltip.getHeight();
                        activatedTooltip.show(owner, x+TOOLTIP_XOFFSET, y);
                    }

                    visibleTooltip = activatedTooltip;
                    hoveredNode = null;
                    hideTimer.playFromStart();
                }

                // Once the activation timer has expired, the tooltip is no
                // longer in the activated state, it is only in the visible
                // state, so we go ahead and set activated to false
                activatedTooltip.setActivated(false);
                activatedTooltip = null;
            });

            hideTimer.getKeyFrames().add(new KeyFrame(visibleDuration));
            hideTimer.setOnFinished(event -> {
                // Hide the currently visible tooltip.
                assert visibleTooltip != null;
                visibleTooltip.hide();
                visibleTooltip = null;
                hoveredNode = null;
            });

            leftTimer.getKeyFrames().add(new KeyFrame(closeDelay));
            leftTimer.setOnFinished(event -> {
                if (!hideOnExit) {
                    // Hide the currently visible tooltip.
                    assert visibleTooltip != null;
                    visibleTooltip.hide();
                    visibleTooltip = null;
                    hoveredNode = null;
                }
            });
        }

        /**
         * Registers for mouse move events only. When the mouse is moved, this
         * handler will detect it and decide whether to start the ACTIVATION_TIMER
         * (if the ACTIVATION_TIMER is not started), restart the ACTIVATION_TIMER
         * (if ACTIVATION_TIMER is running), or skip the ACTIVATION_TIMER and just
         * show the tooltip (if the LEFT_TIMER is running).
         */
        private EventHandler<MouseEvent> MOVE_HANDLER = (MouseEvent event) -> {
            //Screen coordinates need to be actual for dynamic tooltip.
            //See Tooltip.setText

            lastMouseX = event.getScreenX();
            lastMouseY = event.getScreenY();

            // If the HIDE_TIMER is running, then we don't want this event
            // handler to do anything, or change any state at all.
            if (hideTimer.getStatus() == Timeline.Status.RUNNING) {
                return;
            }

            // Note that the "install" step will both register this handler
            // with the target node and also associate the tooltip with the
            // target node, by stashing it in the client properties of the node.
            hoveredNode = (Node) event.getSource();
            Tooltip t = (Tooltip) hoveredNode.getProperties().get(TOOLTIP_PROP_KEY);
            if (t != null) {
                // In theory we should never get here with an invisible or
                // non-existant window hierarchy, but might in some cases where
                // people are feeding fake mouse events into the hierarchy. So
                // we'll guard against that case.
                final Window owner = getWindow(hoveredNode);
                final boolean treeVisible = isWindowHierarchyVisible(hoveredNode);
                if (owner != null && treeVisible) {
                    // Now we know that the currently HOVERED node has a tooltip
                    // and that it is part of a visible window Hierarchy.
                    // If LEFT_TIMER is running, then we make this tooltip
                    // visible immediately, stop the LEFT_TIMER, and start the
                    // HIDE_TIMER.
                    if (leftTimer.getStatus() == Timeline.Status.RUNNING) {
                        if (visibleTooltip != null) visibleTooltip.hide();
                        visibleTooltip = t;
                        t.show(owner, event.getScreenX()+TOOLTIP_XOFFSET,
                                event.getScreenY()+TOOLTIP_YOFFSET);
                        leftTimer.stop();
                        hideTimer.playFromStart();
                    } else {
                        // Start / restart the timer and make sure the tooltip
                        // is marked as activated.
                        t.setActivated(true);
                        activatedTooltip = t;
                        activationTimer.stop();
                        activationTimer.playFromStart();
                    }
                }
            } else {
                // TODO should deregister, no point being here anymore!
            }
        };

        /**
         * Registers for mouse exit events. If the ACTIVATION_TIMER is running then
         * this will simply stop it. If the HIDE_TIMER is running then this will
         * stop the HIDE_TIMER, hide the tooltip, and start the LEFT_TIMER.
         */
        private EventHandler<MouseEvent> LEAVING_HANDLER = (MouseEvent event) -> {
            // detect bogus mouse exit events, if it didn't really move then ignore it
            if (activationTimer.getStatus() == Timeline.Status.RUNNING) {
                activationTimer.stop();
            } else if (hideTimer.getStatus() == Timeline.Status.RUNNING) {
                assert visibleTooltip != null;
                hideTimer.stop();
                if (hideOnExit) visibleTooltip.hide();
                leftTimer.playFromStart();
            }

            hoveredNode = null;
            activatedTooltip = null;
            if (hideOnExit) visibleTooltip = null;
        };

        /**
         * Registers for mouse click, press, release, drag events. If any of these
         * occur, then the tooltip is hidden (if it is visible), it is deactivated,
         * and any and all timers are stopped.
         */
        private EventHandler<MouseEvent> KILL_HANDLER = (MouseEvent event) -> {
            activationTimer.stop();
            hideTimer.stop();
            leftTimer.stop();
            if (visibleTooltip != null) visibleTooltip.hide();
            hoveredNode = null;
            activatedTooltip = null;
            visibleTooltip = null;
        };

        private void install(Node node, Tooltip t) {
            // Install the MOVE_HANDLER, LEAVING_HANDLER, and KILL_HANDLER on
            // the given node. Stash the tooltip in the node's client properties
            // map so that it is not gc'd. The handlers must all be installed
            // with a TODO weak reference so as not to cause a memory leak
            if (node == null) return;
            node.addEventHandler(MouseEvent.MOUSE_MOVED, MOVE_HANDLER);
            node.addEventHandler(MouseEvent.MOUSE_EXITED, LEAVING_HANDLER);
            node.addEventHandler(MouseEvent.MOUSE_PRESSED, KILL_HANDLER);
            node.getProperties().put(TOOLTIP_PROP_KEY, t);
        }

        private void uninstall(Node node) {
            if (node == null) return;
            node.removeEventHandler(MouseEvent.MOUSE_MOVED, MOVE_HANDLER);
            node.removeEventHandler(MouseEvent.MOUSE_EXITED, LEAVING_HANDLER);
            node.removeEventHandler(MouseEvent.MOUSE_PRESSED, KILL_HANDLER);
            Tooltip t = (Tooltip)node.getProperties().get(TOOLTIP_PROP_KEY);
            if (t != null) {
                node.getProperties().remove(TOOLTIP_PROP_KEY);
                if (t.equals(visibleTooltip) || t.equals(activatedTooltip)) {
                    KILL_HANDLER.handle(null);
                }
            }
        }

        /**
         * Gets the top level window associated with this node.
         * @param node the node
         * @return the top level window
         */
        private Window getWindow(final Node node) {
            final Scene scene = node == null ? null : node.getScene();
            return scene == null ? null : scene.getWindow();
        }

        /**
         * Gets whether the entire window hierarchy is visible for this node.
         * @param node the node to check
         * @return true if entire hierarchy is visible
         */
        private boolean isWindowHierarchyVisible(Node node) {
            boolean treeVisible = node != null;
            Parent parent = node == null ? null : node.getParent();
            while (parent != null && treeVisible) {
                treeVisible = parent.isVisible();
                parent = parent.getParent();
            }
            return treeVisible;
        }
    }
}
