// 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.

try {

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

        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 = String('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
        };
    });

} catch (a) {
    console.log('error jquery move');
    console.log(a);
}