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

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

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

    isNotPreventable: /^(select|a)$/i,

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

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

    lastEventType: null,

    config: {
        moveThrottle: 0,
        recognizers: {}
    },

    constructor: function(config) {
        var me = this;

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

        this.eventToRecognizerMap = {};

        this.activeRecognizers = [];

        this.touchesMap = {};

        this.currentIdentifiers = [];

        if (Ext.browser.is.Chrome && Ext.os.is.Android) {
            this.screenPositionRatio = Ext.browser.version.gt('18') ? 1 : 1 / window.devicePixelRatio;
        }
        else if (Ext.browser.is.AndroidStock4) {
            this.screenPositionRatio = 1;
        }
        else if (Ext.os.is.BlackBerry) {
            this.screenPositionRatio = 1 / window.devicePixelRatio;
        }
        else if (Ext.browser.engineName == 'WebKit' && Ext.os.is.Desktop) {
            this.screenPositionRatio = 1;
        }
        else {
            this.screenPositionRatio = window.innerWidth / window.screen.width;
        }
        this.initConfig(config);

        if (Ext.feature.has.Touch) {
            // bind handlers that are only invoked when the browser has touchevents
            me.onTargetTouchMove = me.onTargetTouchMove.bind(me);
            me.onTargetTouchEnd = me.onTargetTouchEnd.bind(me);
        }

        return this.callSuper();
    },

    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.callSuper(arguments) || this.eventToRecognizerMap.hasOwnProperty(eventName);
    },

    doesEventBubble: function() {
        // All touch events bubble
        return true;
    },
    onEvent: function(e) {
        var type = e.type,
            lastEventType = this.lastEventType,
            touchList = [e];

        if (this.eventProcessors[type]) {
            this.eventProcessors[type].call(this, e);
            return;
        }

        if ('button' in e && e.button > 0) {
            return;
        }
        else {
            // 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' && lastEventType && 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;

            this.eventProcessors[this.mouseToTouchMap[type]].call(this, e);
        }
    },

    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.callSuper([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;
    },

    updateTouch: function(touch) {
        var identifier = touch.identifier,
            currentTouch = this.touchesMap[identifier],
            target, x, y;

        if (!currentTouch) {
            target = this.getElementTarget(touch.target);

            this.touchesMap[identifier] = currentTouch = {
                identifier: identifier,
                target: target,
                targets: this.getBubblingTargets(target)
            };

            this.currentIdentifiers.push(identifier);
        }

        x  = touch.pageX;
        y  = touch.pageY;

        if (x === currentTouch.pageX && y === currentTouch.pageY) {
            return false;
        }

        currentTouch.pageX = x;
        currentTouch.pageY = y;
        currentTouch.timeStamp = touch.timeStamp;
        currentTouch.point = new Ext.util.Point(x, y);

        return currentTouch;
    },

    updateTouches: function(touches) {
        var i, ln, touch,
            changedTouches = [];

        for (i = 0, ln = touches.length; i < ln; i++) {
            touch = this.updateTouch(touches[i]);
            if (touch) {
                changedTouches.push(touch);
            }
        }

        return changedTouches;
    },

    factoryEvent: function(e) {
        return new Ext.event.Touch(e, null, this.touchesMap, this.currentIdentifiers);
    },

    onTouchStart: function(e) {
        var changedTouches = e.changedTouches,
            target = e.target,
            ln = changedTouches.length,
            isNotPreventable = this.isNotPreventable,
            isTouch = (e.type === 'touchstart'),
            me = this,
            i, touch, parent;

        this.updateTouches(changedTouches);

        e = this.factoryEvent(e);
        changedTouches = e.changedTouches;

        // TOUCH-3934
        // Android event system will not dispatch touchend for any multitouch
        // event that has not been preventDefaulted.
        if(Ext.browser.is.AndroidStock && this.currentIdentifiers.length >= 2) {
            e.preventDefault();
        }

        // If targets are destroyed while touches are active on them
        // we need these listeners to sync up our internal TouchesMap
        if (isTouch) {
            target.addEventListener('touchmove', me.onTargetTouchMove);
            target.addEventListener('touchend', me.onTargetTouchEnd);
            target.addEventListener('touchcancel', me.onTargetTouchEnd);
        }

        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);

        parent = target.parentNode || {};
    },

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

        if (!this.animationQueued) {
            this.animationQueued = true;
            Ext.AnimationQueue.start('onAnimationFrame', this);
        }

        this.lastMoveEvent = e;
    },

    onAnimationFrame: function() {
        var event = this.lastMoveEvent;

        if (event) {
            this.lastMoveEvent = null;
            this.doTouchMove(event);
        }
    },

    doTouchMove: function(e) {
        var changedTouches, i, ln, touch;

        changedTouches = this.updateTouches(e.changedTouches);

        ln = changedTouches.length;

        e = this.factoryEvent(e);

        for (i = 0; i < ln; i++) {
            touch = changedTouches[i];
            this.publish('touchmove', touch.targets, e, {touch: touch});
        }

        if (ln > 0) {
            this.invokeRecognizers('onTouchMove', e);
        }
    },

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

        if (this.lastMoveEvent) {
            this.onAnimationFrame();
        }

        var touchesMap = this.touchesMap,
            currentIdentifiers = this.currentIdentifiers,
            changedTouches = e.changedTouches,
            ln = changedTouches.length,
            identifier, i, touch;

        this.updateTouches(changedTouches);

        changedTouches = e.changedTouches;

        for (i = 0; i < ln; i++) {
            Ext.Array.remove(currentIdentifiers, changedTouches[i].identifier);
        }

        e = this.factoryEvent(e);

        for (i = 0; i < ln; i++) {
            identifier = changedTouches[i].identifier;
            touch = touchesMap[identifier];
            delete touchesMap[identifier];
            this.publish('touchend', touch.targets, e, {touch: touch});
        }

        this.invokeRecognizers('onTouchEnd', e);

        // This previously was set to e.touches.length === 1 to catch errors in syncing
        // this has since been addressed to keep proper sync and now this is a catch for
        // a sync error in touches to reset our internal maps
        if (e.touches.length === 0 && currentIdentifiers.length) {
            currentIdentifiers.length = 0;
            this.touchesMap = {};
        }

        if (currentIdentifiers.length === 0) {
            this.isStarted = false;
            this.invokeRecognizers('onEnd', e);
            if (this.animationQueued) {
                this.animationQueued = false;
                Ext.AnimationQueue.stop('onAnimationFrame', this);
            }
        }
    },

    onTargetTouchMove: function(e) {
        if (!Ext.getBody().contains(e.target)) {
            this.onTouchMove(e);
        }
    },

    onTargetTouchEnd: function(e) {
        var me = this,
            target = e.target,
            touchCount=0,
            touchTarget;

        // Determine how many active touches there are on this target
        for (identifier in this.touchesMap) {
            touchTarget = this.touchesMap[identifier].target;
            if (touchTarget === target ) {
                touchCount++;
            }
        }

        // If this is the last active touch on the target remove the target listeners
        if (touchCount <= 1) {
            target.removeEventListener('touchmove', me.onTargetTouchMove);
            target.removeEventListener('touchend', me.onTargetTouchEnd);
            target.removeEventListener('touchcancel', me.onTargetTouchEnd);
        }

        if (!Ext.getBody().contains(target)) {
            me.onTouchEnd(e);
        }
    }

}, function() {
    if (Ext.feature.has.Pointer) {
        this.override({
            pointerToTouchMap: {
                MSPointerDown: 'touchstart',
                MSPointerMove: 'touchmove',
                MSPointerUp: 'touchend',
                MSPointerCancel: 'touchcancel',
                pointerdown: 'touchstart',
                pointermove: 'touchmove',
                pointerup: 'touchend',
                pointercancel: 'touchcancel'
            },

            touchToPointerMap: {
                touchstart: 'MSPointerDown',
                touchmove: 'MSPointerMove',
                touchend: 'MSPointerUp',
                touchcancel: 'MSPointerCancel'
            },

            attachListener: function(eventName, doc) {
                eventName = this.touchToPointerMap[eventName];

                if (!eventName) {
                    return;
                }

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

            onEvent: function(e) {
                var type = e.type;
                if (
                        this.currentIdentifiers.length === 0 &&
                        // This is for IE 10 and IE 11
                        (e.pointerType === e.MSPOINTER_TYPE_TOUCH || e.pointerType === "touch") &&
                        // This is for IE 10 and IE 11
                        (type === "MSPointerMove" || type === "pointermove")
                    ) {
                    type = "MSPointerDown";
                }

                if ('button' in e && e.button > 0) {
                    return;
                }

                type = this.pointerToTouchMap[type];
                e.identifier = e.pointerId;
                e.changedTouches = [e];

                this.eventProcessors[type].call(this, e);
            }
        });
    }
    else if (!Ext.browser.is.Ripple && (Ext.os.is.ChromeOS || !Ext.feature.has.Touch)) {
        this.override({
            handledEvents: ['touchstart', 'touchmove', 'touchend', 'touchcancel', 'mousedown', 'mousemove', 'mouseup']
        });
    }
});