// This plugin is an experiment for abstracting away the touch and mouse
// events so that developers don't have to worry about which method of input
// the device their document is loaded on supports.
//
// The idea here is to allow the developer to register listeners for the
// basic mouse events, such as mousedown, mousemove, mouseup, and click,
// and the plugin will take care of registering the correct listeners
// behind the scenes to invoke the listener at the fastest possible time
// for that device, while still retaining the order of event firing in
// the traditional mouse environment, should multiple handlers be registered
// on the same element for different events.
//
// The current version exposes the following virtual events to jQuery bind methods:
// "vmouseover vmousedown vmousemove vmouseup vclick vmouseout vmousecancel"

(function( $, window, document, undefined ) {

var dataPropertyName = "virtualMouseBindings",
    touchTargetPropertyName = "virtualTouchID",
    virtualEventNames = "vmouseover vmousedown vmousemove vmouseup vclick vmouseout vmousecancel".split( " " ),
    touchEventProps = "clientX clientY pageX pageY screenX screenY".split( " " ),
    mouseHookProps = $.event.mouseHooks ? $.event.mouseHooks.props : [],
    mouseEventProps = $.event.props.concat( mouseHookProps ),
    activeDocHandlers = {},
    resetTimerID = 0,
    startX = 0,
    startY = 0,
    didScroll = false,
    clickBlockList = [],
    blockMouseTriggers = false,
    blockTouchTriggers = false,
    eventCaptureSupported = "addEventListener" in document,
    $document = $( document ),
    nextTouchID = 1,
    lastTouchID = 0, threshold;

$.vmouse = {
    moveDistanceThreshold: 10,
    clickDistanceThreshold: 10,
    resetTimerDuration: 1500
};

function getNativeEvent( event ) {

    while ( event && typeof event.originalEvent !== "undefined" ) {
        event = event.originalEvent;
    }
    return event;
}

function createVirtualEvent( event, eventType ) {

    var t = event.type,
        oe, props, ne, prop, ct, touch, i, j, len;

    event = $.Event( event );
    event.type = eventType;

    oe = event.originalEvent;
    props = $.event.props;

    // addresses separation of $.event.props in to $.event.mouseHook.props and Issue 3280
    // https://github.com/jquery/jquery-mobile/issues/3280
    if ( t.search( /^(mouse|click)/ ) > -1 ) {
        props = mouseEventProps;
    }

    // copy original event properties over to the new event
    // this would happen if we could call $.event.fix instead of $.Event
    // but we don't have a way to force an event to be fixed multiple times
    if ( oe ) {
        for ( i = props.length, prop; i; ) {
            prop = props[ --i ];
            event[ prop ] = oe[ prop ];
        }
    }

    // make sure that if the mouse and click virtual events are generated
    // without a .which one is defined
    if ( t.search(/mouse(down|up)|click/) > -1 && !event.which ) {
        event.which = 1;
    }

    if ( t.search(/^touch/) !== -1 ) {
        ne = getNativeEvent( oe );
        t = ne.touches;
        ct = ne.changedTouches;
        touch = ( t && t.length ) ? t[0] : ( ( ct && ct.length ) ? ct[ 0 ] : undefined );

        if ( touch ) {
            for ( j = 0, len = touchEventProps.length; j < len; j++) {
                prop = touchEventProps[ j ];
                event[ prop ] = touch[ prop ];
            }
        }
    }

    return event;
}

function getVirtualBindingFlags( element ) {

    var flags = {},
        b, k;

    while ( element ) {

        b = $.data( element, dataPropertyName );

        for (  k in b ) {
            if ( b[ k ] ) {
                flags[ k ] = flags.hasVirtualBinding = true;
            }
        }
        element = element.parentNode;
    }
    return flags;
}

function getClosestElementWithVirtualBinding( element, eventType ) {
    var b;
    while ( element ) {

        b = $.data( element, dataPropertyName );

        if ( b && ( !eventType || b[ eventType ] ) ) {
            return element;
        }
        element = element.parentNode;
    }
    return null;
}

function enableTouchBindings() {
    blockTouchTriggers = false;
}

function disableTouchBindings() {
    blockTouchTriggers = true;
}

function enableMouseBindings() {
    lastTouchID = 0;
    clickBlockList.length = 0;
    blockMouseTriggers = false;

    // When mouse bindings are enabled, our
    // touch bindings are disabled.
    disableTouchBindings();
}

function disableMouseBindings() {
    // When mouse bindings are disabled, our
    // touch bindings are enabled.
    enableTouchBindings();
}

function startResetTimer() {
    clearResetTimer();
    resetTimerID = setTimeout( function() {
        resetTimerID = 0;
        enableMouseBindings();
    }, $.vmouse.resetTimerDuration );
}

function clearResetTimer() {
    if ( resetTimerID ) {
        clearTimeout( resetTimerID );
        resetTimerID = 0;
    }
}

function triggerVirtualEvent( eventType, event, flags ) {
    var ve;

    if ( ( flags && flags[ eventType ] ) ||
                ( !flags && getClosestElementWithVirtualBinding( event.target, eventType ) ) ) {

        ve = createVirtualEvent( event, eventType );

        $( event.target).trigger( ve );
    }

    return ve;
}

function mouseEventCallback( event ) {
    var touchID = $.data( event.target, touchTargetPropertyName );

    if ( !blockMouseTriggers && ( !lastTouchID || lastTouchID !== touchID ) ) {
        var ve = triggerVirtualEvent( "v" + event.type, event );
        if ( ve ) {
            if ( ve.isDefaultPrevented() ) {
                event.preventDefault();
            }
            if ( ve.isPropagationStopped() ) {
                event.stopPropagation();
            }
            if ( ve.isImmediatePropagationStopped() ) {
                event.stopImmediatePropagation();
            }
        }
    }
}

function handleTouchStart( event ) {

    var touches = getNativeEvent( event ).touches,
        target, flags;

    if ( touches && touches.length === 1 ) {

        target = event.target;
        flags = getVirtualBindingFlags( target );

        if ( flags.hasVirtualBinding ) {

            lastTouchID = nextTouchID++;
            $.data( target, touchTargetPropertyName, lastTouchID );

            clearResetTimer();

            disableMouseBindings();
            didScroll = false;

            var t = getNativeEvent( event ).touches[ 0 ];
            startX = t.pageX;
            startY = t.pageY;

            triggerVirtualEvent( "vmouseover", event, flags );
            triggerVirtualEvent( "vmousedown", event, flags );
        }
    }
}

function handleScroll( event ) {
    if ( blockTouchTriggers ) {
        return;
    }

    if ( !didScroll ) {
        triggerVirtualEvent( "vmousecancel", event, getVirtualBindingFlags( event.target ) );
    }

    didScroll = true;
    startResetTimer();
}

function handleTouchMove( event ) {
    if ( blockTouchTriggers ) {
        return;
    }

    var t = getNativeEvent( event ).touches[ 0 ],
        didCancel = didScroll,
        moveThreshold = $.vmouse.moveDistanceThreshold,
        flags = getVirtualBindingFlags( event.target );

        didScroll = didScroll ||
            ( Math.abs( t.pageX - startX ) > moveThreshold ||
                Math.abs( t.pageY - startY ) > moveThreshold );


    if ( didScroll && !didCancel ) {
        triggerVirtualEvent( "vmousecancel", event, flags );
    }

    triggerVirtualEvent( "vmousemove", event, flags );
    startResetTimer();
}

function handleTouchEnd( event ) {
    if ( blockTouchTriggers ) {
        return;
    }

    disableTouchBindings();

    var flags = getVirtualBindingFlags( event.target ),
        t;
    triggerVirtualEvent( "vmouseup", event, flags );

    if ( !didScroll ) {
        var ve = triggerVirtualEvent( "vclick", event, flags );
        if ( ve && ve.isDefaultPrevented() ) {
            // The target of the mouse events that follow the touchend
            // event don't necessarily match the target used during the
            // touch. This means we need to rely on coordinates for blocking
            // any click that is generated.
            t = getNativeEvent( event ).changedTouches[ 0 ];
            clickBlockList.push({
                touchID: lastTouchID,
                x: t.clientX,
                y: t.clientY
            });

            // Prevent any mouse events that follow from triggering
            // virtual event notifications.
            blockMouseTriggers = true;
        }
    }
    triggerVirtualEvent( "vmouseout", event, flags);
    didScroll = false;

    startResetTimer();
}

function hasVirtualBindings( ele ) {
    var bindings = $.data( ele, dataPropertyName ),
        k;

    if ( bindings ) {
        for ( k in bindings ) {
            if ( bindings[ k ] ) {
                return true;
            }
        }
    }
    return false;
}

function dummyMouseHandler() {}

function getSpecialEventObject( eventType ) {
    var realType = eventType.substr( 1 );

    return {
        setup: function( data, namespace ) {
            // If this is the first virtual mouse binding for this element,
            // add a bindings object to its data.

            if ( !hasVirtualBindings( this ) ) {
                $.data( this, dataPropertyName, {} );
            }

            // If setup is called, we know it is the first binding for this
            // eventType, so initialize the count for the eventType to zero.
            var bindings = $.data( this, dataPropertyName );
            bindings[ eventType ] = true;

            // If this is the first virtual mouse event for this type,
            // register a global handler on the document.

            activeDocHandlers[ eventType ] = ( activeDocHandlers[ eventType ] || 0 ) + 1;

            if ( activeDocHandlers[ eventType ] === 1 ) {
                $document.bind( realType, mouseEventCallback );
            }

            // Some browsers, like Opera Mini, won't dispatch mouse/click events
            // for elements unless they actually have handlers registered on them.
            // To get around this, we register dummy handlers on the elements.

            $( this ).bind( realType, dummyMouseHandler );

            // For now, if event capture is not supported, we rely on mouse handlers.
            if ( eventCaptureSupported ) {
                // If this is the first virtual mouse binding for the document,
                // register our touchstart handler on the document.

                activeDocHandlers[ "touchstart" ] = ( activeDocHandlers[ "touchstart" ] || 0) + 1;

                if ( activeDocHandlers[ "touchstart" ] === 1 ) {
                    $document.bind( "touchstart", handleTouchStart )
                        .bind( "touchend", handleTouchEnd )

                        // On touch platforms, touching the screen and then dragging your finger
                        // causes the window content to scroll after some distance threshold is
                        // exceeded. On these platforms, a scroll prevents a click event from being
                        // dispatched, and on some platforms, even the touchend is suppressed. To
                        // mimic the suppression of the click event, we need to watch for a scroll
                        // event. Unfortunately, some platforms like iOS don't dispatch scroll
                        // events until *AFTER* the user lifts their finger (touchend). This means
                        // we need to watch both scroll and touchmove events to figure out whether
                        // or not a scroll happenens before the touchend event is fired.

                        .bind( "touchmove", handleTouchMove )
                        .bind( "scroll", handleScroll );
                }
            }
        },

        teardown: function( data, namespace ) {
            // If this is the last virtual binding for this eventType,
            // remove its global handler from the document.

            --activeDocHandlers[ eventType ];

            if ( !activeDocHandlers[ eventType ] ) {
                $document.unbind( realType, mouseEventCallback );
            }

            if ( eventCaptureSupported ) {
                // If this is the last virtual mouse binding in existence,
                // remove our document touchstart listener.

                --activeDocHandlers[ "touchstart" ];

                if ( !activeDocHandlers[ "touchstart" ] ) {
                    $document.unbind( "touchstart", handleTouchStart )
                        .unbind( "touchmove", handleTouchMove )
                        .unbind( "touchend", handleTouchEnd )
                        .unbind( "scroll", handleScroll );
                }
            }

            var $this = $( this ),
                bindings = $.data( this, dataPropertyName );

            // teardown may be called when an element was
            // removed from the DOM. If this is the case,
            // jQuery core may have already stripped the element
            // of any data bindings so we need to check it before
            // using it.
            if ( bindings ) {
                bindings[ eventType ] = false;
            }

            // Unregister the dummy event handler.

            $this.unbind( realType, dummyMouseHandler );

            // If this is the last virtual mouse binding on the
            // element, remove the binding data from the element.

            if ( !hasVirtualBindings( this ) ) {
                $this.removeData( dataPropertyName );
            }
        }
    };
}

// Expose our custom events to the jQuery bind/unbind mechanism.

for ( var i = 0; i < virtualEventNames.length; i++ ) {
    $.event.special[ virtualEventNames[ i ] ] = getSpecialEventObject( virtualEventNames[ i ] );
}

// Add a capture click handler to block clicks.
// Note that we require event capture support for this so if the device
// doesn't support it, we punt for now and rely solely on mouse events.
if ( eventCaptureSupported ) {
    document.addEventListener( "click", function( e ) {
        var cnt = clickBlockList.length,
            target = e.target,
            x, y, ele, i, o, touchID;

        if ( cnt ) {
            x = e.clientX;
            y = e.clientY;
            threshold = $.vmouse.clickDistanceThreshold;

            // The idea here is to run through the clickBlockList to see if
            // the current click event is in the proximity of one of our
            // vclick events that had preventDefault() called on it. If we find
            // one, then we block the click.
            //
            // Why do we have to rely on proximity?
            //
            // Because the target of the touch event that triggered the vclick
            // can be different from the target of the click event synthesized
            // by the browser. The target of a mouse/click event that is syntehsized
            // from a touch event seems to be implementation specific. For example,
            // some browsers will fire mouse/click events for a link that is near
            // a touch event, even though the target of the touchstart/touchend event
            // says the user touched outside the link. Also, it seems that with most
            // browsers, the target of the mouse/click event is not calculated until the
            // time it is dispatched, so if you replace an element that you touched
            // with another element, the target of the mouse/click will be the new
            // element underneath that point.
            //
            // Aside from proximity, we also check to see if the target and any
            // of its ancestors were the ones that blocked a click. This is necessary
            // because of the strange mouse/click target calculation done in the
            // Android 2.1 browser, where if you click on an element, and there is a
            // mouse/click handler on one of its ancestors, the target will be the
            // innermost child of the touched element, even if that child is no where
            // near the point of touch.

            ele = target;

            while ( ele ) {
                for ( i = 0; i < cnt; i++ ) {
                    o = clickBlockList[ i ];
                    touchID = 0;

                    if ( ( ele === target && Math.abs( o.x - x ) < threshold && Math.abs( o.y - y ) < threshold ) ||
                                $.data( ele, touchTargetPropertyName ) === o.touchID ) {
                        // XXX: We may want to consider removing matches from the block list
                        //      instead of waiting for the reset timer to fire.
                        e.preventDefault();
                        e.stopPropagation();
                        return;
                    }
                }
                ele = ele.parentNode;
            }
        }
    }, true);
}
})( jQuery, window, document );