/**
 * This class provides extra contextual information for components/elements by
 * attaching to a {@link #target}. The tip will show based on mouseover (or touch, 
 * depending on the environment) and {@link #align} itself to the {@link #target}.
 *
 * Typically, tooltips will be created via {@link Ext.Component#tooltip components}, however
 * it is possible to create instances directly.
 *
 *     new Ext.tip.ToolTip({
 *         target: myComponent,
 *         html: 'Here is some help text about this!'
 *     });
 *
 * # Shared instance
 * New instances of tooltips do not need to be created for every item that requires
 * a tooltip. In most cases it is sufficient to use a single shared instance across 
 * the application, which provides a performance benefit. See {@link Ext.tip.Manager} 
 * for an explanation of how shared tips are used.
 *
 * # Delegation
 * 
 * It is common to want to show a tooltip for a repeated view and dynamically update
 * the content based on the current item within this view. This can be achieved using
 * the {@link #delegate} configuration. This means that the tip will only activate
 * when over an item inside the target that matches the {@link #delegate}. After this,
 * the {@link #currentTarget} can be interrogated to get contextual information about which
 * delegate item triggered the show.
 *
 *     var el = Ext.getBody().createChild({
 *         html: '<div data-num="1" class="item">Foo</div>' +
 *               '<div data-num="2" class="item">Bar</div>' +
 *               '<div data-num="3" class="item">Baz</div>' +
 *               '<div class="notip">No tip here</div>'
 *     });
 *
 *     new Ext.tip.ToolTip({
 *         target: el,
 *         delegate: '.item',
 *         listeners: {
 *             beforeshow: function(tip) {
 *                 var current = tip.currentTarget.dom;
 *                 tip.setHtml('Item #' + current.getAttribute('data-num'));
 *             }
 *         }
 *     });
 *
 * # Alignment
 *
 * The following configuration properties allow control over how the ToolTip is aligned relative to
 * the target element and/or mouse pointer:
 *
 * - {@link #anchor}
 * - {@link #anchorToTarget}
 * - {@link #trackMouse}
 * - {@link #mouseOffset}
 *
 * # Showing/Hiding
 *
 * The following configuration properties allow control over how and when the ToolTip is automatically
 * shown and hidden:
 *
 * - {@link #autoHide}
 * - {@link #showDelay}
 * - {@link #hideDelay}
 * - {@link #dismissDelay}
 * 
 * 
 * @since 6.2.0
 */
Ext.define('Ext.tip.ToolTip', {
    extend: 'Ext.Panel',
    xtype: 'tooltip',
 
    floated: true,
    
    ui: 'tooltip',
 
    hidden: true,
 
    shadow: true,
 
    border: true,
 
    anchor: false,
 
    closeAction: 'hide',
 
    config: {
        /**
         * @cfg {String} [align]
         * A string which specifies how this ToolTip is to align with regard to its
         * {@link #currentTarget} by means of identifying the point of the tooltip to
         * join to the point of the target.
         *
         * By default, the tooltip shows at {@link #mouseOffset} pixels from the
         * triggering pointer event. Using this config anchors the ToolTip to its target
         * instead.
         *
         * This may take the following forms:
         * 
         * - **Blank**: Defaults to aligning the element's top-left corner to the target's
         *   bottom-left corner ("tl-bl").
         * - **Two anchors**: If two values from the table below are passed separated by a dash,
         *   the first value is used as the element's anchor point, and the second value is
         *   used as the target's anchor point.
         * - **Two edge/offset descriptors:** An edge/offset descriptor is an edge initial
         *   (`t`/`r`/`b`/`l`) followed by a percentage along that side. This describes a
         *   point to align with a similar point in the target. So `'t0-b0'` would be
         *   the same as `'tl-bl'`, `'l0-r50'` would place the top left corner of this item
         *   halfway down the right edge of the target item. This allows more flexibility
         *   and also describes which two edges are considered adjacent when positioning a tip pointer. 
         *
         * In addition to the anchor points, the position parameter also supports the "?"
         * character. If "?" is passed at the end of the position string, the element will
         * attempt to align as specified, but the position will be adjusted to constrain to
         * the viewport if necessary. Note that the element being aligned might be swapped to
         * align to a different position than that specified in order to enforce the viewport
         * constraints. Following are all of the supported anchor positions:
         *
         *      Value  Description
         *      -----  -----------------------------
         *      tl     The top left corner
         *      t      The center of the top edge
         *      tr     The top right corner
         *      l      The center of the left edge
         *      c      The center
         *      r      The center of the right edge
         *      bl     The bottom left corner
         *      b      The center of the bottom edge
         *      br     The bottom right corner
         *
         * Example Usage:
         *
         *     // align the top left corner of the tooltip with the top right corner of its target
         *     // (constrained to viewport).
         *     align: 'tl-tr?'
         *
         *     // align the bottom right corner of the tooltip with the center left edge of its target.
         *     align: 'br-l?'
         *
         *     // align the top center of the tooltip with the bottom left corner of its target.
         *     align: 't-bl'
         *
         *     // align the 25% point on the bottom edge of this tooltip
         *     // with the 75% point on the top edge of its target.
         *     align: 'b25-c75'
         */
        align: 'l-r?',
 
        /**
         * @cfg {String} [alignDelegate]
         * A selector which identifies child elements of the  {@link #currentTarget} to
         * align to upon show.
         */
        alignDelegate: null,
 
        /**
         * @cfg {Boolean} [allowOver=false]
         * Set to `true` to allow mouse exiting the target, but moving into the ToolTip to
         * keep the ToolTip visible. This may be useful for interactive tips.
         *
         * While the mouse is over the tip, the {@link #dismissDelay dismiss timer} is
         * inactive, so the tip will not {@link #autoHide}.
         *
         * On touch platforms, a touch on the tooltip is the equivalent, and this cancels
         * the dismiss timer so that a tap outside is then necessary to hide the tip.
         *
         * This is incompatible with the {@link #cfg-trackMouse} config.
         */
        allowOver: null,
 
        /**
         * @cfg {Boolean} [anchorToTarget]
         * By default, the {@link #align} config aligns to the {@link #target}.
         *
         * Configure this as `false` if an anchor is required, but positioning is still
         * relative to the pointer position on show.
         */
        anchorToTarget: true,
 
        /**
         * @cfg {Boolean} [autoHide]
         * True to automatically hide the tooltip after the mouse exits the target element
         * or after the `{@link #dismissDelay}` has expired if set.
         *
         * If `{@link #closable} = true` a close tool button will be rendered into the
         * tooltip header.
         */
        autoHide: true,
 
        /**
         * @cfg {String} [delegate]
         * A selector which identifies child elements of the target which trigger showing
         * this ToolTip. The {@link #currentTarget} property is set to the triggering
         * element.
         */
        delegate: null,
 
        /**
         * @cfg {Number} [dismissDelay]
         * Delay in milliseconds before the tooltip automatically hides.
         *
         * Set this value to `0` to disable automatic hiding.
         */
        dismissDelay: 5000,
 
        /**
         * @cfg {Number} [hideDelay]
         * Delay in milliseconds after the mouse exits the target element but before the
         * tooltip actually hides.
         *
         * Set to `0` for the tooltip to hide immediately.
         */
        hideDelay: 300,
 
        /**
         * @cfg {Number[]} [mouseOffset]
         * An XY offset from the triggering pointer event position where the tooltip
         * should be shown unless aligned to the target element.
         */
        mouseOffset: [15, 18],
 
        /**
         * @cfg {Number} [quickShowInterval]
         * If a show is triggered within this number of milliseconds of this ToolTip being
         * hidden, it shows immediately regardless of the delay. If rapidly moving from
         * target to target, this ensure that each separate target does not get its own
         * delay.
         */
        quickShowInterval: 250,
 
        /**
         * @cfg {Number} [showDelay]
         * Delay in milliseconds before the tooltip displays after the mouse enters the
         * target element.
         *
         * On touch platforms, if {@link #showOnTap} is `true`, a tap on the target shows
         * the tip, and this timer is ignored - the tip is shown immediately.
         */
        showDelay: 500,
 
        /**
         * @cfg {Boolean/String[]} [showOnTap=false]
         * `true` to show this tip on a tap event on the target. If specified as a string,
         * it should be the {@link Ext.event.Event#pointerType} of the event that should
         * trigger a show. Typically, this will be `touch`.
         *
         * This is useful for adding tips on elements which do not have tap listeners. It
         * would not be appropriate for a ToolTip on a {@link Ext.Button Button}.
         */
        showOnTap: null,
 
        /**
         * @cfg {Ext.Component/Ext.dom.Element} target
         * The target that should trigger showing this ToolTip.
         */
        target: null,
 
        /**
         * @cfg {Boolean} [trackMouse]
         * True to have the tooltip follow the mouse as it moves over the target element.
         *
         * Only effective on platforms with pointing devices, this does not address touch
         * events.
         */
        trackMouse: false
    },
 
    closeToolText: null,
 
    constructor: function (config) {
        /**
         * @property {Ext.dom.Fly} currentTarget
         * Only attached to a DOM element  when this ToolTip is active. The current target.
         * This is usually the {@link #cfg-target}, but if the {@link #cfg-delegate} option
         * is used, it may be a child element of the main target.
         * @readonly
         */
        this.currentTarget = new Ext.dom.Fly();
 
        this.callParent([config]);
 
        this.attachTargetListeners();
    },
 
    getRefOwner: function() {
        var target = this.getTarget();
        return (target && target.isComponent) ? target : this.callParent();
    },
 
    updateAnchor: function() {
        this.doRealignToTarget();
    },
 
    applyAlign: function(align) {
        var lastChar = align[align.length - 1];
 
        // Tooltips constrain themselves. 
        if (lastChar !== '?' && lastChar !== '!') {
            align += '?';
        }
        return align;
    },
 
    updateAlign: function() {
        this.doRealignToTarget();
    },
 
    updateAllowOver: function(allowOver) {
        var me = this;
 
        me.overListeners = Ext.destroy(me.overListeners);
 
        // Use the mouseleave and mouseenter events because we do not need delegation 
        if (allowOver) {
            me.overListeners = me.el.on({
                mouseenter: 'onTipOver',
                mouseleave: 'onTipOut',
                scope: me,
                destroyable: true
            });
        }
    },
 
    applyTarget: function(target) {
        if (target) {
            if (!target.isComponent) {
                target = Ext.get(target.el || target);
            }
        }
        return target;
    },
 
    updateTarget: function(target, oldTarget) {
        var me = this;
 
        if (!me.isConfiguring) {
            me.targetListeners = Ext.destroy(me.targetListeners);
            me.attachTargetListeners();
        }
    },
 
    updateTrackMouse: function(trackMouse) {
        // If tracking mouse, allow mouse to enter the tooltip without triggering dismiss 
        if (!this.getAnchor()) {
            this.setAllowOver(trackMouse);
        }
    },
 
    updateDisabled: function(disabled, oldDisabled) {
        var me = this,
            val;
 
        me.callParent([disabled, oldDisabled]);
        if (disabled) {
            me.clearTimers();
            me.hide();
            val = null;
        }
        // If we pass null, it won't attempt to attach listeners 
        me.attachTargetListeners(val);
    },
    
    updateShowOnTap: function(showOnTap) {
        if (!this.isConfiguring) {
            this.attachTargetListeners();
        }
    },
 
    /**
     * Realign this tip to the current target if it is currently visible.
     *
     * @since 6.2.1
     */
    realignToTarget: function() {
        this.doRealignToTarget();
    },
 
    showBy: function(target, alignment, passedOptions) {
        var me = this,
            alignDelegate = me.getAlignDelegate();
 
        // If we are trackMouse: true, we will be asked to show by a pointer event 
        if (target.isEvent) {
            me.alignToEvent(target);
        } else {
            if (target.isWidget) {
                me.updateCurrentTarget(target.element.dom);
            } else if (target.isElement) {
                me.updateCurrentTarget(target.dom);
            } else if (target.nodeType) {
                me.updateCurrentTarget(target);
            }
            me.callParent([alignDelegate ? target.child(alignDelegate, true) : target, alignment || me.getAlign(), passedOptions]);
        }
    },
    
    onViewportResize: function() {
        var me = this,
            currentTarget = me.currentTarget;
 
        if (me.isVisible() && !me.lastShowWasPointer && currentTarget.dom) {
            me.showByTarget(me.currentTarget);
        }
    },
 
    show: function() {
        var me = this,
            dismissDelay = me.getDismissDelay();
 
        // A programmatic show should align to the target 
        if (!me.currentTarget.dom && (me.pointerEvent || me.getTarget())) {
            return me.showByTarget(me.getElFromTarget());
        }
        me.callParent();
        me.clearTimer('show');
        if (dismissDelay && me.getAutoHide()) {
            me.dismissTimer = Ext.defer(me.hide, dismissDelay, me);
        }
        me.toFront();
        Ext.getDoc().on('mousedown', me.onDocMouseDown, me);
    },
 
    hide: function() {
        var me = this;
 
        me.clearTimer('hide');
        me.clearTimer('dismiss');
        me.callParent();
        me.lastHidden = new Date();
        me.updateCurrentTarget(null);
        Ext.getDoc().un('mousedown', me.onDocMouseDown, me);
    },
 
    doDestroy: function() {
        var me = this;
 
        me.clearTimers();
        me.setTarget(null);
        me.overListeners = null;
        me.callParent();
    },
 
    privates: {
        allowRealign: true,
        
        onDocMouseDown: function(e) {
            var me = this,
                delegate = me.getDelegate();
 
            if (e.within(me.el.dom)) {
                // A real touch event inside the tip is the equivalent of 
                // mousing over the tip to keep it visible, so cancel the 
                // dismiss timer. 
                if (e.pointerType !== 'mouse' && me.getAllowOver()) {
                    me.clearTimer('dismiss');
                }
            }
            // Only respond to the mousedown if it's not on this tip, and it's not on a target. 
            // If it's on a target, onTargetTap will handle it. 
            else if (!me.getClosable()) {
                if (e.within(me.getElFromTarget()) && (!delegate || e.getTarget(delegate))) {
                    me.delayHide();
                } else {
                    me.disable();
                    me.enableTimer = Ext.defer(me.enable, 100, me);
                }
            }
        },
 
        forceTargetOver: function(e, newTarget) {
            this.pointerEvent = e;
            this.updateCurrentTarget(newTarget);
            this.handleTargetOver();
        },
 
        onTargetOver: function(e) {
            var me = this,
                myTarget = me.getElFromTarget(),
                delegate = me.getDelegate(),
                currentTarget = me.currentTarget,
                myListeners = me.hasListeners,
                newTarget;
 
            if (me.getDisabled()) {
                return;
            }
 
            if (delegate) {
                // Moving inside a delegate 
                if (currentTarget.contains(e.target)) {
                    return;
                }
                newTarget = e.getTarget(delegate);
 
                // Mouseovers while within a target do nothing 
                if (newTarget && e.getRelatedTarget(delegate) === newTarget) {
                    return;
                }
            }
            // Moved from outside the target 
            else if (!myTarget.contains(e.relatedTarget)) {
                newTarget = myTarget.dom;
            }
            // Moving inside the target 
            else {
                return;
            }
 
            // If pointer entered the target or a delegate child, then show. 
            if (newTarget) {
                // If users need to see show events on target change, we must hide. 
                if ((myListeners.beforeshow || myListeners.show) && me.isVisible()) {
                    me.hide();
                    me.hiddenByTargetOver = me.isHidden();
                }
 
                me.forceTargetOver(e, newTarget);
            }
            // If over a non-delegate child, behave as in target out 
            else if (currentTarget.dom) {
                me.handleTargetOut();
            }
        },
 
        handleTargetOver: function() {
            // Separated from onTargetOver so that subclasses can handle target over in any way. 
            this.delayShow(this.currentTarget);
        },
 
        onTargetTap: function(e) {
            // On hybrid mouse/touch systems, we want to show the tip on touch, but 
            // we don't want to show it if this is coming from a click event, because 
            // the mouse is already hovered. 
            if (e.pointerType !== 'mouse') {
                this.onTargetOver(e);
            }
        },
 
        onTargetOut: function(e) {
            // We have exited the current target 
            if (this.currentTarget.dom && !this.currentTarget.contains(e.relatedTarget)) {
                this.handleTargetOut();
            }
        },
 
        handleTargetOut: function() {
            // Separated from onTargetOut so that subclasses can handle target out in any way. 
            var me = this;
 
            if (me.showTimer) {
                me.clearTimer('show');
            }
            if (me.isVisible() && me.getAutoHide()) {
                me.delayHide();
            }
        },
 
        onTipOver: function() {
            this.clearTimer('hide');
            this.clearTimer('dismiss');
        },
 
        onTipOut: function() {
            this.handleTargetOut();
        },
 
        onMouseMove: function(e) {
            var me = this,
                dismissDelay = me.getDismissDelay();
 
            // Always update pointerEvent, so that if there's a delayed show 
            // scheduled, it gets the latest pointer to align to. 
            me.pointerEvent = e;
            if (me.isVisible() && me.currentTarget.contains(e.target)) {
                // If they move the mouse, restart the dismiss delay 
                if (dismissDelay && me.getAutoHide() !== false) {
                    me.clearTimer('dismiss');
                    me.dismissTimer = Ext.defer(me.hide, dismissDelay, me);
                }
 
                if (me.getTrackMouse()) {
                    me.alignToEvent(e);
                }
             }
        },
 
        delayShow: function(target) {
            var me = this;
 
            me.clearTimer('hide');
            if (me.getHidden() && !me.showTimer) {
                // Allow rapid movement from delegate to delegate to show immediately 
                if ((me.getDelegate() || me.hiddenByTargetOver) && Ext.Date.getElapsed(me.lastHidden) < me.getQuickShowInterval()) {
                    me.hiddenByTargetOver = false;
                    me.showByTarget(target);
                } else {
                    // If a tap event triggered, do not wait. Show immediately. 
                    me.showTimer = Ext.defer(me.showByTarget, (!me.pointerEvent || me.pointerEvent.pointerType === 'mouse') ? me.getShowDelay() : 0, me, [target]);
                }
            }
            else if (!me.getHidden() && me.getAutoHide() !== false) {
                me.showByTarget(target);
            }
        },
 
        showByTarget: function(target) {
            var me = this,
                isTarget = me.getAnchorToTarget() && !me.getTrackMouse();
 
            me.lastShowWasPointer = !isTarget;
            // Show by the correct thing. 
            // If trackMouse, or we are not anchored to the target, then it's the current pointer event. 
            // Otherwise it's either the current target, or the alignDelegate within that. 
            me.showBy(isTarget ? target : me.pointerEvent, me.getAlign(), {overlap: me.getTrackMouse() && !me.getAnchor()});
        },
 
        delayHide: function() {
            var me = this;
 
            if (!me.isHidden() && !me.hideTimer) {
                me.clearTimer('dismiss');
                me.hideTimer = Ext.defer(me.hide, me.getHideDelay(), me);
            }
        },
 
        alignToEvent: function(event) {
            var me = this,
                options = {
                    // Allow the "exclusion area", the zone of mouseOffset 
                    // created as a Region around the mouse to overlap 
                    // the tip position. 
                    overlap: me.getTrackMouse() && !me.getAnchor()
                },
                mouseOffset = me.getMouseOffset(),
                target = event.getPoint().adjust(-Math.abs(mouseOffset[1]), Math.abs(mouseOffset[0]), Math.abs(mouseOffset[1]), -Math.abs(mouseOffset[0])),
                align = me.getAnchor() ? me.getAlign() : null;
 
            if (!align && mouseOffset) {
                if (mouseOffset[0] > 0) {
                    if (mouseOffset[1] > 0) {
                        align = 'tl-br?';
                    } else {
                        align = 'bl-tr?';
                    }
                } else {
                    if (mouseOffset[1] > 0) {
                        align = 'tr-bl?';
                    } else {
                        align = 'br-tl?';
                    }
                }
            }
 
            if (me.isVisible()) {
                me.alignTo(target, align, options);
            } else {
                me.showBy(target, align, options);
            }
        },
 
        _timerNames: {},
 
        clearTimer: function (name) {
            var me = this,
                names = me._timerNames,
                propName = names[name] || (names[name] = name + 'Timer'),
                timer = me[propName];
 
            if (timer) {
                clearTimeout(timer);
                me[propName] = null;
 
                // We were going to show against the target, but now not. 
                if (name === 'show' && me.isHidden()) {
                    me.updateCurrentTarget(null);
                }
            }
        },
 
        /**
         * @private
         */
        clearTimers: function() {
            var me = this;
            me.clearTimer('show');
            me.clearTimer('dismiss');
            me.clearTimer('hide');
            me.clearTimer('enable');
        },
 
        clipTo: function(clippingEl, sides) {
        // Override because we also need to clip the anchor 
            var clippingRegion;
 
            // Allow a Region to be passed 
            if (clippingEl.isRegion) {
                clippingRegion = clippingEl;
            } else {
                clippingRegion = (clippingEl.isComponent ? clippingEl.el : Ext.fly(clippingEl)).getConstrainRegion();
            }
 
            // this method is borrowed by the Widget override 
            // @noOptimize.callParent 
            this.callParent([clippingRegion, sides]);
 
            // Clip the anchor to the same bounds 
            this.tipElement.clipTo(clippingRegion, sides);
        },
 
        doRealignToTarget: function() {
            var me = this,
                currentTarget = me.currentTarget,
                dom = currentTarget && currentTarget.dom;
 
            me.clearTimers();
            if (me.allowRealign && me.isVisible() && dom) {
                me.showByTarget(me.pointerEvent || dom);
            }
        },
 
        updateCurrentTarget: function (dom) {
            var me = this,
                currentTarget = me.currentTarget,
                was = currentTarget.dom;
 
            currentTarget.attach(dom);
 
            if (!me.isConfiguring) {
                me.fireEvent('hovertarget', me, currentTarget, was);
            }
        },
 
        getElFromTarget: function() {
            var target = this.getTarget();
            if (target) {
                if (target.isComponent) {
                    target = target.element;
                }
            }
            return target;
        },
 
        attachTargetListeners: function(target) {
            var me = this,
                listeners;
 
            if (target !== null) {
                target = me.getElFromTarget();
            }
 
            me.targetListeners = Ext.destroy(me.targetListeners);
 
            if (target) {
                listeners = {
                    mouseover: 'onTargetOver',
                    mouseout: 'onTargetOut',
                    mousemove: 'onMouseMove',
                    scope: me,
                    destroyable: true
                };
 
                if (me.getShowOnTap()) {
                    listeners.tap = 'onTargetTap';
                }
                me.targetListeners = target.on(listeners);
            }
        }
    }
});