/**
 * Acts as a mediator between sources and targets. 
 * 
 * Typically this class will not be used in user code.
 *
 * @private
 */
Ext.define('Ext.drag.Manager', {
    singleton: true,

    /**
     * @property {String} dragCls
     * A class added to the body while a drag is active.
     *
     * @private
     */
    dragCls: Ext.baseCSSPrefix + 'drag-body',

    // If we need to use the current mousemove target to find the over el,
    // but pointer-events is not supported, AND the delta position does not place the mouse outside of the dragEl,
    // temporarily move the dragEl away, and fake the mousemove target by using document.elementFromPoint
    // while it's out of the way.
    // The pointer events implementation is bugged in IE9/10 and opera, so fallback even if they report that they support it.
    // IE8m do not support it so they will auto fall back
    pointerBug: Ext.isTouch || (!Ext.supports.CSSPointerEvents || Ext.isIE10m || Ext.isOpera),

    constructor: function() {
        this.targets = {};
        this.nativeTargets = [];
        Ext.onReady(this.init, this);
    },

    init: function() {
        // The purpose of listening for these events is to track when a
        // native drag enters the document so we can create and maintain
        // a single drag.Info object for it. Need to use a "stack-like" mechanism
        // to track while elements are being entered/left, keeping a count is
        // not sufficient because Gecko can fire multiple events for the
        // same element in some instances. So just keep pushing/removing the
        // element from the tracking array. Once we hit 0, the drag is out
        // of the document. On drop, we clear it manually because there is
        // no longer an active drag.
        Ext.getDoc().on({
            scope: this,
            dragenter: {
                capture: true,
                fn: 'onNativeDragEnter'
            },
            dragleave: 'onNativeDragLeave',
            dragover: 'onNativeDragOver',
            drop: 'onNativeDrop'
        });
    },

    destroy: function() {
        var me = this,
            targets = me.targets,
            key;

        me.destroying = true;

        for (key in targets) {
            targets[key].destroy();
        }

        me.targets = null;

        me.callParent();
        me.destroying = false;
    },

    privates: {
        /**
         * A shim for elementFromPoint to allow RTL behaviour.
         * @param {Number} x The x coordinate.
         * @param {Number} y The y coordinate
         * @return {HTMLElement} The element.
         *
         * @private
         */
        elementFromPoint: function(x, y) {
            if (Ext.rootInheritedState.rtl) {
                x = Ext.Element.getViewportWidth() - x;
            }

            return Ext.dom.Element.fromPagePoint(x, y, true);
        },

        /**
         * Get the matching target (if any) at a particular point.
         * @param {Ext.drag.Info} info The drag info.
         * @return {Ext.drag.Target} The matching target, `null` if not found.
         *
         * @private
         */
        getAtPoint: function(info) {
            var current = info.cursor.current,
                elementMap = info.elementMap,
                isUnderCursor = info.proxy.isUnderCursor,
                proxyEl = this.pointerBug && isUnderCursor ? info.proxy.element.dom : null,
                target, el;

            if (proxyEl) {
                proxyEl.style.visibility = 'hidden';
            }
            el = this.elementFromPoint(current.x, current.y);
            if (proxyEl) {
                proxyEl.style.visibility = 'visible';
            }

            while (el) {
                target = elementMap[el.id];
                if (target) {
                    return target;
                }

                el = el.parentNode;
            }

            return null;
        },

        /**
         * Spins up an info object based on a native drag.
         * @param {Ext.event.Event} e The event.
         * @return {Ext.drag.Info} The info. Cached for a single drag.
         *
         * @private
         */
        getNativeDragInfo: function(e) {
            var info = this.nativeDragInfo;
            if (!info) {
                this.nativeDragInfo = info = new Ext.drag.Info();
                info.isNative = true;
            }
            return info;
        },

        /**
         * Called on drag cancel.
         * @param {Ext.drag.Info} info The drag info.
         * @param {Ext.event.Event} e The event.
         *
         * @private
         */
        onDragCancel: function() {
            Ext.getBody().removeCls(this.dragCls);
        },

        /**
         * Called when drag completes.
         * @param {Ext.drag.Info} info The drag info.
         * @param {Ext.event.Event} e The event.
         *
         * @private
         */
        onDragEnd: function(info, e) {
            info.finalize();
            Ext.getBody().removeCls(this.dragCls);
        },

        /**
         * Called for each drag movement.
         * @param {Ext.drag.Info} info The drag info.
         * @param {Ext.event.Event} e The event.
         *
         * @private
         */
        onDragMove: function(info, e) {
            this.processDrag(info);
        },

        /**
         * Called when drag starts.
         * @param {Ext.drag.Info} info The drag info.
         * @param {Ext.event.Event} e The event.
         *
         * @private
         */
        onDragStart: function(info, e) {
            var me = this,
                source = info.source,
                targets = me.targets,
                groups = source.getGroups(),
                targetMap = {},
                possibleTargets = {},
                elementMap = {},
                id, target, targetGroups, 
                groupMap, groupOk, len, i;

            elementMap = {};
            possibleTargets = {};

            if (groups) {
                groupMap = Ext.Array.toMap(groups);
            }

            // Exclude any invalid targets so they don't get used during
            // a drag. This means targets that are locked or have groups that don't
            // match
            for (id in targets) {
                target = targets[id];

                if (!target.isDisabled()) {
                    groupOk = false;
                    targetGroups = target.getGroups();

                    // If neither has groups, proceed. Otherwise, it
                    // can only be correct if both have groups, then we
                    // need to check if they intersect. If one has groups
                    // and not the other it's not possible to intersect.
                    if (!groupMap && !targetGroups) {
                        groupOk = true;
                    } else if (groupMap && targetGroups) {
                        for (i = 0, len = targetGroups.length; i < len; ++i) {
                            if (groupMap[targetGroups[i]]) {
                                groupOk = true;
                                break;
                            }
                        }
                    }

                    if (groupOk) {
                        possibleTargets[id] = target;
                    }
                }
                targetMap[id] = target;
                elementMap[target.getElement().id] = target;
            }

            info.possibleTargets = possibleTargets;
            info.targetMap = targetMap;
            info.elementMap = elementMap;

            Ext.getBody().addCls(me.dragCls);

            me.processDrag(info);
        },

        /**
         * Handle a native dragenter event from outside the browser.
         * @param {Ext.event.Event} e The event.
         *
         * @private
         */
        onNativeDragEnter: function(e) {
            var nativeTargets = this.nativeTargets,
                target = e.target;

            // Need to preventDefault to stop browser navigating to the dropped item.
            e.preventDefault();
            if (nativeTargets[nativeTargets.length - 1] !== target) {
                nativeTargets.push(target);
            }

        },

        /**
         * Handle a native dragleave event.
         * @param {Ext.event.Event} e The event.
         *
         * @private
         */
        onNativeDragLeave: function(e) {
            var nativeTargets = this.nativeTargets;
            Ext.Array.remove(nativeTargets, e.target);
            if (nativeTargets.length === 0) {
                this.nativeDragInfo = null;
            }
        },

        /**
         * Handle a native dragover event.
         * @param {Ext.event.Event} e The event.
         *
         * @private
         */
        onNativeDragOver: function(e) {
            // Need to preventDefault to stop browser navigating to the dropped item.
            e.preventDefault();
        },

        /**
         * Handle a native drop event.
         * @param {Ext.event.Event} e The event.
         *
         * @private
         */
        onNativeDrop: function(e) {
            // Need to preventDefault to stop browser navigating to the dropped item.
            e.preventDefault();
            this.nativeTargets.length = 0;
            this.nativeDragInfo = null;
        },

        /**
         * Process a drag movement.
         * @param {Ext.drag.Info} info The drag info.
         *
         * @private
         */
        processDrag: function(info) {
            info.setActive(this.getAtPoint(info));
        },

        /**
         * Register a target with this group. This is intended to
         * be called by the target.
         * @param {Ext.drag.Target} target The target.
         *
         * @private
         */
        register: function(target) {
            this.targets[target.getId()] = target;
        },

        /**
         * Unregister a target with this group. This is intended to
         * be called by the target.
         * @param {Ext.drag.Target} target The target.
         *
         * @private
         */
        unregister: function(target) {
            if (this.destroying) {
                return;
            }

            delete this.targets[target.getId()];
        }
    }
});