/**
 * 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, record, element) {
         *             tooltip.setHtml('Customer: ' + record.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);
        }
    }
 
});
="meta arguments js">         * 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: '',
 
    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();
    },
 
    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();
        }
    },
 
    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.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;
 
        if (me.isVisible() && !me.lastShowWasPointer) {
            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('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: {
        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();
        },
 
        getConstrainRegion: function() {
            return this.callParent().adjust(5, -5, -5, 5);
        },
 
        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);
                // Move inside a delegate with no currentTarget
                if (newTarget && Ext.fly(newTarget).contains(e.relatedTarget)) {
                    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.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() && 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.pointerType !== 'mouse' ? 0 : me.getShowDelay(), 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(-mouseOffset[1], mouseOffset[0], mouseOffset[1], -mouseOffset[0]),
                anchor = me.getAnchor(),
                align;
 
            // The anchor must point to the mouse
            if (me.getAnchor()) {
                align = me.getAlign();
            }
 
            if (!align) {
                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);
        },
 
        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);
            }
        }
    }
});