/**
 * @private
 */
Ext.define('Ext.event.publisher.TouchGesture', {

    extend: 'Ext.event.publisher.Dom',

    requires: [
        'Ext.util.Point',
        'Ext.event.Touch'
    ],

    handledEvents: ['touchstart', 'touchmove', 'touchend', 'touchcancel'],

    moveEventName: 'touchmove',

    config: {
        moveThrottle: 1,
        buffering: {
            enabled: false,
            interval: 10
        },
        recognizers: {}
    },

    currentTouchesCount: 0,

    constructor: function(config) {
        this.processEvents = Ext.Function.bind(this.processEvents, this);

        this.eventProcessors = {
            touchstart: this.onTouchStart,
            touchmove: this.onTouchMove,
            touchend: this.onTouchEnd,
            touchcancel: this.onTouchEnd
        };

        this.eventToRecognizerMap = {};

        this.activeRecognizers = [];

        this.currentRecognizers = [];

        this.currentTargets = {};

        this.currentTouches = {};

        this.buffer = [];

        this.initConfig(config);

        return this.callParent();
    },

    applyBuffering: function(buffering) {
        if (buffering.enabled === true) {
            this.bufferTimer = setInterval(this.processEvents, buffering.interval);
        }
        else {
            clearInterval(this.bufferTimer);
        }

        return buffering;
    },

    applyRecognizers: function(recognizers) {
        var i, recognizer;

        for (i in recognizers) {
            if (recognizers.hasOwnProperty(i)) {
                recognizer = recognizers[i];

                if (recognizer) {
                    this.registerRecognizer(recognizer);
                }
            }
        }

        return recognizers;
    },

    handles: function(eventName) {
        return this.callParent(arguments) || this.eventToRecognizerMap.hasOwnProperty(eventName);
    },

    doesEventBubble: function() {
        // All touch events bubble
        return true;
    },

    eventLogs: [],

    onEvent: function(e) {
        var buffering = this.getBuffering();

        e = new Ext.event.Touch(e);

        if (buffering.enabled) {
            this.buffer.push(e);
        }
        else {
            this.processEvent(e);
        }
    },

    processEvents: function() {
        var buffer = this.buffer,
            ln = buffer.length,
            moveEvents = [],
            events, event, i;

        if (ln > 0) {
            events = buffer.slice(0);
            buffer.length = 0;

            for (i = 0; i < ln; i++) {
                event = events[i];
                if (event.type === this.moveEventName) {
                    moveEvents.push(event);
                }
                else {
                    if (moveEvents.length > 0) {
                        this.processEvent(this.mergeEvents(moveEvents));
                        moveEvents.length = 0;
                    }

                    this.processEvent(event);
                }
            }

            if (moveEvents.length > 0) {
                this.processEvent(this.mergeEvents(moveEvents));
                moveEvents.length = 0;
            }
        }
    },

    mergeEvents: function(events) {
        var changedTouchesLists = [],
            ln = events.length,
            i, event, targetEvent;

        targetEvent = events[ln - 1];

        if (ln === 1) {
            return targetEvent;
        }

        for (i = 0; i < ln; i++) {
            event = events[i];
            changedTouchesLists.push(event.changedTouches);
        }

        targetEvent.changedTouches = this.mergeTouchLists(changedTouchesLists);

        return targetEvent;
    },

    mergeTouchLists: function(touchLists) {
        var touches = {},
            list = [],
            i, ln, touchList, j, subLn, touch, identifier;

        for (i = 0,ln = touchLists.length; i < ln; i++) {
            touchList = touchLists[i];

            for (j = 0,subLn = touchList.length; j < subLn; j++) {
                touch = touchList[j];
                identifier = touch.identifier;
                touches[identifier] = touch;
            }
        }

        for (identifier in touches) {
            if (touches.hasOwnProperty(identifier)) {
                list.push(touches[identifier]);
            }
        }

        return list;
    },

    registerRecognizer: function(recognizer) {
        var map = this.eventToRecognizerMap,
            activeRecognizers = this.activeRecognizers,
            handledEvents = recognizer.getHandledEvents(),
            i, ln, eventName;

        recognizer.setOnRecognized(this.onRecognized);
        recognizer.setCallbackScope(this);

        for (i = 0,ln = handledEvents.length; i < ln; i++) {
            eventName = handledEvents[i];

            map[eventName] = recognizer;
        }

        activeRecognizers.push(recognizer);

        return this;
    },

    onRecognized: function(eventName, e, touches, info) {
        var targetGroups = [],
            ln = touches.length,
            targets, i, touch;

        if (ln === 1) {
            return this.publish(eventName, touches[0].targets, e, info);
        }

        for (i = 0; i < ln; i++) {
            touch = touches[i];
            targetGroups.push(touch.targets);
        }

        targets = this.getCommonTargets(targetGroups);

        this.publish(eventName, targets, e, info);
    },

    publish: function(eventName, targets, event, info) {
        event.set(info);

        return this.callParent([eventName, targets, event]);
    },

    getCommonTargets: function(targetGroups) {
        var firstTargetGroup = targetGroups[0],
            ln = targetGroups.length;

        if (ln === 1) {
            return firstTargetGroup;
        }

        var commonTargets = [],
            i = 1,
            target, targets, j;

        while (true) {
            target = firstTargetGroup[firstTargetGroup.length - i];

            if (!target) {
                return commonTargets;
            }

            for (j = 1; j < ln; j++) {
                targets = targetGroups[j];

                if (targets[targets.length - i] !== target) {
                    return commonTargets;
                }
            }

            commonTargets.unshift(target);
            i++;
        }

        return commonTargets;
    },

    invokeRecognizers: function(methodName, e) {
        var recognizers = this.activeRecognizers,
            ln = recognizers.length,
            i, recognizer;

        if (methodName === 'onStart') {
            for (i = 0; i < ln; i++) {
                recognizers[i].isActive = true;
            }
        }

        for (i = 0; i < ln; i++) {
            recognizer = recognizers[i];
            if (recognizer.isActive && recognizer[methodName].call(recognizer, e) === false) {
                recognizer.isActive = false;
            }
        }
    },

    getActiveRecognizers: function() {
        return this.activeRecognizers;
    },

    processEvent: function(e) {
        this.eventProcessors[e.type].call(this, e);
    },

    onTouchStart: function(e) {
        var currentTargets = this.currentTargets,
            currentTouches = this.currentTouches,
            currentTouchesCount = this.currentTouchesCount,
            changedTouches = e.changedTouches,
            touches = e.touches,
            touchesLn = touches.length,
            currentIdentifiers = {},
            ln = changedTouches.length,
            i, touch, identifier, fakeEndEvent;

        currentTouchesCount += ln;

        if (currentTouchesCount > touchesLn) {
            for (i = 0; i < touchesLn; i++) {
                touch = touches[i];
                identifier = touch.identifier;
                currentIdentifiers[identifier] = true;
            }

            for (identifier in currentTouches) {
                if (currentTouches.hasOwnProperty(identifier)) {
                    if (!currentIdentifiers[identifier]) {
                        currentTouchesCount--;
                        fakeEndEvent = e.clone();
                        touch = currentTouches[identifier];
                        touch.targets = this.getBubblingTargets(this.getElementTarget(touch.target));
                        fakeEndEvent.changedTouches = [touch];
                        this.onTouchEnd(fakeEndEvent);
                    }
                }
            }

            // Fix for a bug found in Motorola Droid X (Gingerbread) and similar
            // where there are 2 touchstarts but just one touchend
            if (currentTouchesCount > touchesLn) {
                return;
            }
        }

        for (i = 0; i < ln; i++) {
            touch = changedTouches[i];
            identifier = touch.identifier;

            if (!currentTouches.hasOwnProperty(identifier)) {
                this.currentTouchesCount++;
            }

            currentTouches[identifier] = touch;
            currentTargets[identifier] = this.getBubblingTargets(this.getElementTarget(touch.target));
        }

        e.setTargets(currentTargets);

        for (i = 0; i < ln; i++) {
            touch = changedTouches[i];

            this.publish('touchstart', touch.targets, e, {touch: touch});
        }

        if (!this.isStarted) {
            this.isStarted = true;
            this.invokeRecognizers('onStart', e);
        }

        this.invokeRecognizers('onTouchStart', e);
    },

    onTouchMove: function(e) {
        if (!this.isStarted) {
            return;
        }

        var currentTargets = this.currentTargets,
            currentTouches = this.currentTouches,
            moveThrottle = this.getMoveThrottle(),
            changedTouches = e.changedTouches,
            stillTouchesCount = 0,
            i, ln, touch, point, oldPoint, identifier;

        e.setTargets(currentTargets);

        for (i = 0,ln = changedTouches.length; i < ln; i++) {
            touch = changedTouches[i];
            identifier = touch.identifier;
            point = touch.point;

            oldPoint = currentTouches[identifier].point;

            if (moveThrottle && point.isCloseTo(oldPoint, moveThrottle)) {
                stillTouchesCount++;
                continue;
            }

            currentTouches[identifier] = touch;

            this.publish('touchmove', touch.targets, e, {touch: touch});
        }

        if (stillTouchesCount < ln) {
            this.invokeRecognizers('onTouchMove', e);
        }
    },

    onTouchEnd: function(e) {
        if (!this.isStarted) {
            return;
        }

        var currentTargets = this.currentTargets,
            currentTouches = this.currentTouches,
            changedTouches = e.changedTouches,
            ln = changedTouches.length,
            isEnded, identifier, i, touch;

        e.setTargets(currentTargets);

        this.currentTouchesCount -= ln;

        isEnded = (this.currentTouchesCount === 0);

        if (isEnded) {
            this.isStarted = false;
        }

        for (i = 0; i < ln; i++) {
            touch = changedTouches[i];
            identifier = touch.identifier;

            delete currentTouches[identifier];
            delete currentTargets[identifier];

            this.publish('touchend', touch.targets, e, {touch: touch});
        }

        this.invokeRecognizers('onTouchEnd', e);

        if (isEnded) {
            this.invokeRecognizers('onEnd', e);
        }
    }

}, function() {
    if (!Ext.feature.has.Touch) {
        this.override({
            moveEventName: 'mousemove',

            map: {
                mouseToTouch: {
                    mousedown: 'touchstart',
                    mousemove: 'touchmove',
                    mouseup: 'touchend'
                },

                touchToMouse: {
                    touchstart: 'mousedown',
                    touchmove: 'mousemove',
                    touchend: 'mouseup'
                }
            },

            attachListener: function(eventName) {
                eventName = this.map.touchToMouse[eventName];

                if (!eventName) {
                    return;
                }

                return this.callOverridden([eventName]);
            },

            lastEventType: null,

            onEvent: function(e) {
                if ('button' in e && e.button !== 0) {
                    return;
                }

                var type = e.type,
                    touchList = [e];

                // Temporary fix for a recent Chrome bugs where events don't seem to bubble up to document
                // when the element is being animated
                // with webkit-transition (2 mousedowns without any mouseup)
                if (type === 'mousedown' && this.lastEventType && this.lastEventType !== 'mouseup') {
                    var fixedEvent = document.createEvent("MouseEvent");
                        fixedEvent.initMouseEvent('mouseup', e.bubbles, e.cancelable,
                            document.defaultView, e.detail, e.screenX, e.screenY, e.clientX,
                            e.clientY, e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, e.metaKey,
                            e.button, e.relatedTarget);

                    this.onEvent(fixedEvent);
                }

                if (type !== 'mousemove') {
                    this.lastEventType = type;
                }

                e.identifier = 1;
                e.touches = (type !== 'mouseup') ? touchList : [];
                e.targetTouches = (type !== 'mouseup') ? touchList : [];
                e.changedTouches = touchList;

                return this.callOverridden([e]);
            },

            processEvent: function(e) {
                this.eventProcessors[this.map.mouseToTouch[e.type]].call(this, e);
            }
        });
    }
});