// DOM.event.move
//
// 2.0.0
//
// Stephen Band
//
// Triggers 'movestart', 'move' and 'moveend' events after
// mousemoves following a mousedown cross a distance threshold,
// similar to the native 'dragstart', 'drag' and 'dragend' events.
// Move events are throttled to animation frames. Move event objects
// have the properties:
//
// pageX:
// pageY:     Page coordinates of pointer.
// startX:
// startY:    Page coordinates of pointer at movestart.
// distX:
// distY:     Distance the pointer has moved since movestart.
// deltaX:
// deltaY:    Distance the finger has moved since last event.
// velocityX:
// velocityY: Average velocity over last few events.


(function(fn) {
	if (typeof define === 'function' && define.amd) {
		define( [], fn );
	} else if ((typeof module !== "undefined" && module !== null) && module.exports) {
		module.exports = fn;
	} else {
		fn();
	}
})(
	function(){
		var assign = Object.assign || window.jQuery && jQuery.extend;

		// Number of pixels a pressed pointer travels before movestart
		// event is fired.
		var threshold = 8;

		// Shim for requestAnimationFrame, falling back to timer. See:
		// see http://paulirish.com/2011/requestanimationframe-for-smart-animating/
		var requestFrame = (function(){
			return (
			window.requestAnimationFrame ||
			window.webkitRequestAnimationFrame ||
			window.mozRequestAnimationFrame ||
			window.oRequestAnimationFrame ||
			window.msRequestAnimationFrame ||
			function(fn, element){
				return window.setTimeout(
					function(){
						fn();
					},
					25
				);
			}
			);
		})();

		// Shim for customEvent
		// see https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
		(function () {
			if ( typeof window.CustomEvent === "function" ) {
				return false;
			}
			function CustomEvent ( event, params ) {
				params  = params || { bubbles: false, cancelable: false, detail: undefined };
				var evt = document.createEvent( 'CustomEvent' );
				evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
				return evt;
			}

			CustomEvent.prototype = window.Event.prototype;
			window.CustomEvent    = CustomEvent;
		})();

		var ignoreTags = {
			textarea: true,
			input: true,
			select: true,
			button: true
		};

		var mouseevents = {
			move:   'mousemove',
			cancel: 'mouseup dragstart',
			end:    'mouseup'
		};

		var touchevents = {
			move:   'touchmove',
			cancel: 'touchend',
			end:    'touchend'
		};

		var rspaces = /\s+/;

		// DOM Events

		var eventOptions = { bubbles: true, cancelable: true };

		var eventsSymbol = typeof Symbol === "function" ? Symbol( 'events' ) : {};

		function createEvent(type) {
			return new CustomEvent( type, eventOptions );
		}

		function getEvents(node) {
			return node[eventsSymbol] || (node[eventsSymbol] = {});
		}

		function on(node, types, fn, data, selector) {
			types = types.split( rspaces );

			var events = getEvents( node );
			var i      = types.length;
			var handlers, type;

			function handler(e) { fn( e, data ); }

			while (i--) {
				type     = types[i];
				handlers = events[type] || (events[type] = []);
				handlers.push( [fn, handler] );
				node.addEventListener( type, handler );
			}
		}

		function off(node, types, fn, selector) {
			types = types.split( rspaces );

			var events = getEvents( node );
			var i      = types.length;
			var type, handlers, k;

			if ( ! events) {
				return; }

			while (i--) {
				type     = types[i];
				handlers = events[type];
				if ( ! handlers) {
					continue; }
				k = handlers.length;
				while (k--) {
					if (handlers[k][0] === fn) {
						node.removeEventListener( type, handlers[k][1] );
						handlers.splice( k, 1 );
					}
				}
			}
		}

		function trigger(node, type, properties) {
			// Don't cache events. It prevents you from triggering an event of a
			// given type from inside the handler of another event of that type.
			var event = createEvent( type );
			if (properties) {
				assign( event, properties ); }
			node.dispatchEvent( event );
		}

		// Constructors

		function Timer(fn){
			var callback = fn,
			active       = false,
			running      = false;

			function trigger(time) {
				if (active) {
					callback();
					requestFrame( trigger );
					running = true;
					active  = false;
				} else {
					running = false;
				}
			}

			this.kick = function(fn) {
				active = true;
				if ( ! running) {
					trigger(); }
			};

			this.end = function(fn) {
				var cb = callback;

				if ( ! fn) {
					return; }

				// If the timer is not running, simply call the end callback.
				if ( ! running) {
					fn();
				}
				// If the timer is running, and has been kicked lately, then
				// queue up the current callback and the end callback, otherwise
				// just the end callback.
				else {
					callback = active ?
					function(){ cb(); fn(); } :
					fn;

					active = true;
				}
			};
		}

		// Functions

		function noop() {}

		function preventDefault(e) {
			e.preventDefault();
		}

		function isIgnoreTag(e) {
			return ! ! ignoreTags[e.target.tagName.toLowerCase()];
		}

		function isPrimaryButton(e) {
			// Ignore mousedowns on any button other than the left (or primary)
			// mouse button, or when a modifier key is pressed.
			return (e.which === 1 && ! e.ctrlKey && ! e.altKey);
		}

		function identifiedTouch(touchList, id) {
			var i, l;

			if (touchList.identifiedTouch) {
				return touchList.identifiedTouch( id );
			}

			// touchList.identifiedTouch() does not exist in
			// webkit yet… we must do the search ourselves...

			i = -1;
			l = touchList.length;

			while (++i < l) {
				if (touchList[i].identifier === id) {
					return touchList[i];
				}
			}
		}

		function changedTouch(e, data) {
			var touch = identifiedTouch( e.changedTouches, data.identifier );

			// This isn't the touch you're looking for.
			if ( ! touch) {
				return; }

			// Chrome Android (at least) includes touches that have not
			// changed in e.changedTouches. That's a bit annoying. Check
			// that this touch has changed.
			if (touch.pageX === data.pageX && touch.pageY === data.pageY) {
				return; }

			return touch;
		}

		// Handlers that decide when the first movestart is triggered

		function mousedown(e){
			// Ignore non-primary buttons
			if ( ! isPrimaryButton( e )) {
				return; }

			// Ignore form and interactive elements
			if (isIgnoreTag( e )) {
				return; }

			on( document, mouseevents.move, mousemove, e );
			on( document, mouseevents.cancel, mouseend, e );
		}

		function mousemove(e, data){
			checkThreshold( e, data, e, removeMouse );
		}

		function mouseend(e, data) {
			removeMouse();
		}

		function removeMouse() {
			off( document, mouseevents.move, mousemove );
			off( document, mouseevents.cancel, mouseend );
		}

		function touchstart(e) {
			// Don't get in the way of interaction with form elements
			if (ignoreTags[e.target.tagName.toLowerCase()]) {
				return; }

			var touch = e.changedTouches[0];

			// iOS live updates the touch objects whereas Android gives us copies.
			// That means we can't trust the touchstart object to stay the same,
			// so we must copy the data. This object acts as a template for
			// movestart, move and moveend event objects.
			var data = {
				target:     touch.target,
				pageX:      touch.pageX,
				pageY:      touch.pageY,
				identifier: touch.identifier,

				// The only way to make handlers individually unbindable is by
				// making them unique.
				touchmove:  function(e, data) { touchmove( e, data ); },
				touchend:   function(e, data) { touchend( e, data ); }
			};

			on( document, touchevents.move, data.touchmove, data );
			on( document, touchevents.cancel, data.touchend, data );
		}

		function touchmove(e, data) {
			var touch = changedTouch( e, data );
			if ( ! touch) {
				return; }
			checkThreshold( e, data, touch, removeTouch );
		}

		function touchend(e, data) {
			var touch = identifiedTouch( e.changedTouches, data.identifier );
			if ( ! touch) {
				return; }
			removeTouch( data );
		}

		function removeTouch(data) {
			off( document, touchevents.move, data.touchmove );
			off( document, touchevents.cancel, data.touchend );
		}

		function checkThreshold(e, data, touch, fn) {
			var distX = touch.pageX - data.pageX;
			var distY = touch.pageY - data.pageY;

			// Do nothing if the threshold has not been crossed.
			if ((distX * distX) + (distY * distY) < (threshold * threshold)) {
				return; }

			triggerStart( e, data, touch, distX, distY, fn );
		}

		function triggerStart(e, data, touch, distX, distY, fn) {
			var touches = e.targetTouches;
			var time    = e.timeStamp - data.timeStamp;

			// Create a movestart object with some special properties that
			// are passed only to the movestart handlers.
			var template = {
				altKey:     e.altKey,
				ctrlKey:    e.ctrlKey,
				shiftKey:   e.shiftKey,
				startX:     data.pageX,
				startY:     data.pageY,
				distX:      distX,
				distY:      distY,
				deltaX:     distX,
				deltaY:     distY,
				pageX:      touch.pageX,
				pageY:      touch.pageY,
				velocityX:  distX / time,
				velocityY:  distY / time,
				identifier: data.identifier,
				targetTouches: touches,
				finger: touches ? touches.length : 1,
				enableMove: function() {
					this.moveEnabled = true;
					this.enableMove  = noop;
					e.preventDefault();
				}
			};

			// Trigger the movestart event.
			trigger( data.target, 'movestart', template );

			// Unbind handlers that tracked the touch or mouse up till now.
			fn( data );
		}

		// Handlers that control what happens following a movestart

		function activeMousemove(e, data) {
			var timer = data.timer;

			data.touch     = e;
			data.timeStamp = e.timeStamp;
			timer.kick();
		}

		function activeMouseend(e, data) {
			var target = data.target;
			var event  = data.event;
			var timer  = data.timer;

			removeActiveMouse();

			endEvent(
				target,
				event,
				timer,
				function() {
					// Unbind the click suppressor, waiting until after mouseup
					// has been handled.
					setTimeout(
						function(){
							off( target, 'click', preventDefault );
						},
						0
					);
				}
			);
		}

		function removeActiveMouse() {
			off( document, mouseevents.move, activeMousemove );
			off( document, mouseevents.end, activeMouseend );
		}

		function activeTouchmove(e, data) {
			var event = data.event;
			var timer = data.timer;
			var touch = changedTouch( e, event );

			if ( ! touch) {
				return; }

			// Stop the interface from gesturing
			e.preventDefault();

			event.targetTouches = e.targetTouches;
			data.touch          = touch;
			data.timeStamp      = e.timeStamp;

			timer.kick();
		}

		function activeTouchend(e, data) {
			var target = data.target;
			var event  = data.event;
			var timer  = data.timer;
			var touch  = identifiedTouch( e.changedTouches, event.identifier );

			// This isn't the touch you're looking for.
			if ( ! touch) {
				return; }

			removeActiveTouch( data );
			endEvent( target, event, timer );
		}

		function removeActiveTouch(data) {
			off( document, touchevents.move, data.activeTouchmove );
			off( document, touchevents.end, data.activeTouchend );
		}

		// Logic for triggering move and moveend events

		function updateEvent(event, touch, timeStamp) {
			var time = timeStamp - event.timeStamp;

			event.distX  = touch.pageX - event.startX;
			event.distY  = touch.pageY - event.startY;
			event.deltaX = touch.pageX - event.pageX;
			event.deltaY = touch.pageY - event.pageY;

			// Average the velocity of the last few events using a decay
			// curve to even out spurious jumps in values.
			event.velocityX = 0.3 * event.velocityX + 0.7 * event.deltaX / time;
			event.velocityY = 0.3 * event.velocityY + 0.7 * event.deltaY / time;
			event.pageX     = touch.pageX;
			event.pageY     = touch.pageY;
		}

		function endEvent(target, event, timer, fn) {
			timer.end(
				function(){
					trigger( target, 'moveend', event );
					return fn && fn();
				}
			);
		}

		// Set up the DOM

		function movestart(e) {
			if (e.defaultPrevented) {
				return; }
			if ( ! e.moveEnabled) {
				return; }

			var event = {
				startX:        e.startX,
				startY:        e.startY,
				pageX:         e.pageX,
				pageY:         e.pageY,
				distX:         e.distX,
				distY:         e.distY,
				deltaX:        e.deltaX,
				deltaY:        e.deltaY,
				velocityX:     e.velocityX,
				velocityY:     e.velocityY,
				identifier:    e.identifier,
				targetTouches: e.targetTouches,
				finger:        e.finger
			};

			var data = {
				target:    e.target,
				event:     event,
				timer:     new Timer( update ),
				touch:     undefined,
				timeStamp: e.timeStamp
			};

			function update(time) {
				updateEvent( event, data.touch, data.timeStamp );
				trigger( data.target, 'move', event );
			}

			if (e.identifier === undefined) {
				// We're dealing with a mouse event.
				// Stop clicks from propagating during a move
				on( e.target, 'click', preventDefault );
				on( document, mouseevents.move, activeMousemove, data );
				on( document, mouseevents.end, activeMouseend, data );
			} else {
				// In order to unbind correct handlers they have to be unique
				data.activeTouchmove = function(e, data) { activeTouchmove( e, data ); };
				data.activeTouchend  = function(e, data) { activeTouchend( e, data ); };

				// We're dealing with a touch.
				on( document, touchevents.move, data.activeTouchmove, data );
				on( document, touchevents.end, data.activeTouchend, data );
			}
		}

		on( document, 'mousedown', mousedown );
		on( document, 'touchstart', touchstart );
		on( document, 'movestart', movestart );

		// jQuery special events
		//
		// jQuery event objects are copies of DOM event objects. They need
		// a little help copying the move properties across.

		if ( ! window.jQuery) {
			return; }

		var properties = ("startX startY pageX pageY distX distY deltaX deltaY velocityX velocityY").split( ' ' );

		function enableMove1(e) { e.enableMove(); }
		function enableMove2(e) { e.enableMove(); }
		function enableMove3(e) { e.enableMove(); }

		function add(handleObj) {
			var handler = handleObj.handler;

			handleObj.handler = function(e) {
				// Copy move properties across from originalEvent
				var i = properties.length;
				var property;

				while (i--) {
					property    = properties[i];
					e[property] = e.originalEvent[property];
				}

				handler.apply( this, arguments );
			};
		}

		jQuery.event.special.movestart = {
			setup: function() {
				// Movestart must be enabled to allow other move events
				on( this, 'movestart', enableMove1 );

				// Do listen to DOM events
				return false;
			},

			teardown: function() {
				off( this, 'movestart', enableMove1 );
				return false;
			},

			add: add
		};

		jQuery.event.special.move = {
			setup: function() {
				on( this, 'movestart', enableMove2 );
				return false;
			},

			teardown: function() {
				off( this, 'movestart', enableMove2 );
				return false;
			},

			add: add
		};

		jQuery.event.special.moveend = {
			setup: function() {
				on( this, 'movestart', enableMove3 );
				return false;
			},

			teardown: function() {
				off( this, 'movestart', enableMove3 );
				return false;
			},

			add: add
		};
	}
);
