/**
 * The ToolTip mixin is used to add {@link Ext.tip.ToolTip tooltip} support to various
 * D3 components.
 */
Ext.define('Ext.d3.mixin.ToolTip', {
    extend: 'Ext.Mixin',
    mixinConfig: {
        id: 'd3tooltip'
    },
    
    requires: [
        'Ext.tip.ToolTip'
    ],
 
    config: {
        /**
         * @cfg {Ext.tip.ToolTip} tooltip
         * The {@link Ext.tip.ToolTip} class config object with one extra supported
         * property: `renderer`.
         * @cfg {Function} tooltip.renderer
         * For example:
         *
         *     tooltip: {
         *         renderer: function (component, tooltip, node, element) {
         *             tooltip.setHtml('Customer: ' + node.data.get('name'));
         *         }
         *     }
         */
        tooltip: null
    },
 
    applyTooltip: function (tooltip, oldTooltip) {
        var config = Ext.merge({
                renderer: Ext.emptyFn,
                target: this.el,
                constrainPosition: true,
                shrinkWrapDock: true,
                autoHide: true,
                trackMouse: true,
                showOnTap: true
            }, tooltip),
            result;
 
        Ext.destroy(oldTooltip);
 
        result = new Ext.tip.ToolTip(config);
        result.on('hovertarget', 'onTargetHover', this);
 
        return result;
    },
 
    onTargetHover: function (tooltip, element) {
        if (element.dom) {
            Ext.callback(tooltip.renderer, tooltip.scope,
                [this, tooltip, element.dom.__data__, element], 0, this);
        }
    }
 
});
hod-call js">         *   (`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
    },
 
    classCls: Ext.baseCSSPrefix + 'tooltip',
 
    headerCls: Ext.baseCSSPrefix + 'tooltipheader',
    titleCls: Ext.baseCSSPrefix + 'tooltiptitle',
    toolCls: [
        Ext.baseCSSPrefix + 'paneltool',
        Ext.baseCSSPrefix + 'tooltiptool'
    ],
 
    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]);
        }
    },
 
    beforeShow: function(options) {
        var me = this,
            result = me.callParent(arguments);
 
        // Show is going ahead. Ensure there is alignment if a raw show() call used. 
        // Cancel an impending hide. 
        if (result !== false) {
            // A call to show with no alignment specified should align to the target 
            if (!options.alignment && (me.pointerEvent || me.getTarget())) {
                options.alignment = {
                    component: me.getElFromTarget(),
                    alignment: me.getAlign(),
                    options: {
                        overlap: me.getTrackMouse() && !me.getAnchor()
                    }
                };
            }
            me.clearTimer('dismiss');
        }
    },
 
    afterShow: function() {
        var me = this,
            dismissDelay = me.getDismissDelay();
 
        me.callParent(arguments);
        me.clearTimer('show');
        if (dismissDelay && me.getAutoHide()) {
            me.dismissTimer = Ext.defer(me.hide, dismissDelay, me);
        }
        me.toFront();
        me.mousedownListener = Ext.on({
            mousedown: 'onDocMouseDown',
            scope: me,
            destroyable: true
        });
    },
 
    hide: function() {
        var me = this;
 
        me.clearTimer('hide');
        me.clearTimer('dismiss');
        me.callParent();
        me.lastHidden = new Date();
        me.updateCurrentTarget(null);
        Ext.destroy(me.mousedownListener);
    },
 
    doDestroy: function() {
        var me = this;
 
        me.clearTimers();
        me.setTarget(null);
        me.destroyMembers('mousedownListener', 'overListeners');
        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);
                }
            }
        },
 
        onTargetOver: function(e) {
            var me = this,
                myTarget = me.getElFromTarget(),
                delegate = me.getDelegate(),
                currentTarget = me.currentTarget,
                newTarget;
 
            if (me.getDisabled()) {
                return;
            }
 
            // If mouse moves over the tip, ignore it if that is allowed. 
            if (me.getAllowOver() && me.el.contains(e.target)) {
                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) {
                me.handleTargetOver(e, newTarget);
            }
            // If over a non-delegate child, behave as in target out 
            else if (currentTarget.dom) {
                me.handleTargetOut();
            }
        },
 
        handleTargetOver: function(e, newTarget) {
            var me = this,
                myListeners = me.hasListeners;
 
            me.pointerEvent = e;
            me.updateCurrentTarget(newTarget);
 
            // We are over a new target. If we are still visible, we 
            // do not want to hide to avoid flickering. But if there is a 
            // beforeshow listener which may mutate us, we still have to 
            // consult it. If it returns a veto, then we do in fact hide. 
            // Under normal circumstances we continue with no delay into 
            // showByTarget in a visible state. 
            if (me.isVisible()) {
                if (myListeners.beforeshow && me.fireEvent('beforeshow', me) === false) {
                    return me.hide();
                }
                me.clearTimer('hide');
                me.clearTimer('dismiss');
                me.showByTarget(newTarget);
                if (myListeners.show) {
                    me.fireEvent('show', me);
                }
            } else {
                me.delayShow(newTarget);
            }
        },
 
        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. Tap occasionally hides - eg: pickers, menus. 
            if (e.pointerType !== 'mouse' && Ext.fly(e.target).isVisible(true)) {
                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() {
            // A mouseout of the tip itself is only a mouseout if the pointer has already moved 
            // outside the target and we have no current target, or the mouseout point is outside 
            // of the target. 
            if (!this.currentTarget.dom || !this.pointerEvent.getPoint().isContainedBy(this.currentTarget.getRegion())) {
                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. 
            if (!me.el.contains(e.target)) {
                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() && Ext.Date.getElapsed(me.lastHidden) < me.getQuickShowInterval()) {
                    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.clearTimer('hide');
                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) {
                // Realign, overriding possibly stale alignment 
                me.realign(null, me.getAlign());
            }
        },
 
        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);
            }
        }
    }
});