/**
Copyright (c) 2011 Geoff Green (http://labs.kojo.com.au/author/geoff/)

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

Version 1.0
*/
(function($) { 
    $.fn.mtouch = function(options) {

        var INPUT_TYPE = {
            MOUSE: 1,
            TOUCH: 2
        };

        /**
            var e = {
                start: {
                    x: 0,
                    y: 0,
                    time: 0
                },
                end: {
                    x: 0,
                    y: 0,
                    time: 0
                },
                delta: {
                    x: 0,
                    y: 0,
                    xy: 0,
                    time: 0,
                    velocity: 0
                },
                direction: {
                    radians: 0,
                    degrees: 0,
                    cardinal: 'n',
                    basic: {
                        horizontal: 'left' | 'right' | 'none',
                        vertical: 'up' | 'down' | 'none',
                        major: 'left' | 'right' | 'up' | 'down'
                    }
                }
            };

            var f = {
                startFrame: 0,
                startTime: 0,
                currentFrame: 0,
                roundedCurrentFrame: 0,
                currentFrameRate: 0,
                initialVelocity: 0,
                currentVelocity: 0,
                frameIntervalHandler: null,
                animationIntervalHandler: null
            };
         */
        var settings = {
            animate: function(e, f) { },
            wipe: function(e) { },
            wipeLeft: function(e) { },
            wipeRight: function(e) { },
            wipeUp: function(e) { },
            wipeDown: function(e) { },
            wipeEnd: function(e) { },
            dragged: function (e) { },
            draggedUp: function (e) { },
            draggedDown: function (e) { },
            draggedLeft: function(e) { },
            draggedRight: function(e) { },
            grabbed: function(x, y) { },
            
            /**
             * Minimum/Maximum the cursor/touch needs to have moved in order
             * to fire an event
             */
            minimumMoveX: 0,
            minimumMoveY: 0,
            
            /**
             * Every other touch input plugin had this. All the cool kids were
             * doing it :(
             * 
             * No, but seriously this will call preventDefault() with the mouse
             * move/touch inputs
             */
            preventDefaultEvents: true,
            
            /**
             * cardinalSnap string
             * 'all': snap to any of the 16 points of a compass
             * 'ordinal': snap to any of the cardinal (N,E,S,W) and ordinal (NW, SW, etc.)
             * 'cardinal': snap to any of the cardinal points (N,E,S,W)
             */
            cardinalSnap: 'all',
            /**
             * maxGestureTimeout int (ms)
             * How many milliseconds the start and end of a gesture must be in
             * order to qualify for a swipe check
             */
            maxGestureTimeout: 100,
            /**
             * internalTrackingTick int (ms)
             * How often the internal tracking tick should fire. Higher ms for better
             * performance, but degraded tracking.. lower ms for more precision but
             * degraded performance.
             */
            internalTrackingTick: 20,
            
            /**
             * @todo keep documenting..
             */
            
            /**
             * wipeMinimumFrame int
             * The lowest frame to start from. Usually 1 or 0, but can be any
             * other int depending on what you're animating.
             */
            wipeMinimumFrame: 1,
            
            /**
             * wipeMaximumFrame int
             * The maximum frame to return during the animation. The number of
             * frames in your animation.
             */
            wipeMaximumFrame: 124,
            
            /**
             * wipeFrameLooparound boolean
             * If after hitting the maximum or minimum, should it look back
             * around so the the animation start/end feels connected. 
             * 
             * If true and on frame wipeMinimumFrame, then going back a frame
             * will set the current frame to wipeMaximumFrame. If true and on
             * frame wipeMaximumFrame, then going forward a frame will set the
             * current frame to wipeMinimumFrame
             */
            wipeFrameLooparound: true,
            
            /**
             * wipeDecayConstant float
             * How rate at which the animation should slow down after a wipe
             */
            wipeDecayConstant: 0.005,
            /**
             * The minimum velocity before canceling the animation. If set very
             * low then the animation could still be going through all entire
             * process, but not visibly moving
             */
            wipeCancelSpeed: 0.05,
            
            /**
             * The number of frames to animate at.
             */
            wipeFps: 60,
            
            containerWidth: this.width(),
            containerHeight: this.height(),
            
            /**
             * containerAnchor string
             * 'none': Animation will be stepped on all axis of movement
             * 'horizontal': Animation will be stepped on left/right movement
             * 'vertical': Animation will be stepped on up/down movement
             */
            containerAnchor: 'horizontal'
            
        };

        if (options) $.extend(settings, options);

        this.each(function() {

            var intent = {
                start: {
                    x: 0,
                    y: 0,
                    time: new Date().getTime()
                },
                current: {
                    x: 0,
                    y: 0
                },
                isMoving: false,
                isGrabbed: false
            };

            var swipeAnimation = {
                startFrame: 0,
                startTime: 0,
                currentFrame: 0,
                roundedCurrentFrame: 0,
                currentFrameRate: 0,
                initialVelocity: 0,
                currentVelocity: 0,
                frameIntervalHandler: null,
                animationIntervalHandler: null
            };

            var trackingIntervalHandler = null;
            var trackingIntervals = new Array();
            
            var inputType = INPUT_TYPE.TOUCH;

            function formalizeDirections(e) {
                
                e.direction = { };
                
                // Radians and rotate right
                e.direction.radians = Math.atan2(e.delta.x, e.delta.y) + (Math.PI / 2);
                if (e.direction.radians < 0) {
                    e.direction.radians += 2 * Math.PI;
                }

                e.direction.degrees = e.direction.radians * (180 / Math.PI);
                
                var snappedDegree = 0;
                
                // var degreeRemainder = 0;
                if (settings.cardinalSnap == 'all') {
                    // degreeRemainder = e.direction.degrees % (360 / 16);
                    snappedDegree = Math.round((e.direction.degrees) / 22.5 ) * 22.5;
                }
                else if (settings.cardinalSnap == 'ordinal') {
                    // degreeRemainder = e.direction.degrees % (360 / 8);
                    snappedDegree = Math.round((e.direction.degrees) / 45 ) * 45;
                }
                else {
                    snappedDegree = Math.round((e.direction.degrees) / 90 ) * 90;
                }
                
                switch(snappedDegree) {
                    case 0:e.direction.cardinal = 'e';break;
                    case 22.5:e.direction.cardinal = 'ene';break;
                    case 45:e.direction.cardinal = 'ne';break;
                    case 67.5:e.direction.cardinal = 'nne';break;
                    case 90:e.direction.cardinal = 'n';break;
                    case 112.5:e.direction.cardinal = 'nnw';break;
                    case 135:e.direction.cardinal = 'nw';break;
                    case 157.5:e.direction.cardinal = 'wnw';break;
                    case 180:e.direction.cardinal = 'w';break;
                    case 202.5:e.direction.cardinal = 'wsw';break;
                    case 225:e.direction.cardinal = 'sw';break;
                    case 247.5:e.direction.cardinal = 'ssw';break;
                    case 270:e.direction.cardinal = 's';break;
                    case 292.5:e.direction.cardinal = 'sse';break;
                    case 315:e.direction.cardinal = 'se';break;
                    case 337.5:e.direction.cardinal = 'ese';break;
                    case 360:e.direction.cardinal = 'e';break; 
                }

                e.direction.basic = {};
                e.direction.basic.horizontal = '';
                e.direction.basic.vertical = '';
                e.direction.basic.major = '';
                
                if (e.direction.degrees < 180) {
                    e.direction.basic.vertical = 'up';
                }
                else if (e.direction.degrees > 180) {
                    e.direction.basic.vertical = 'down';
                }
                else {
                    e.direction.basic.vertical = 'none';
                }

                if (e.direction.degrees > 90 && e.direction.degrees < 270) {
                    e.direction.basic.horizontal = 'left';
                }
                else if (e.direction.degrees < 90 && e.direction.degrees > 270) {
                    e.direction.basic.horizontal = 'right';
                }
                else {
                    e.direction.basic.horizontal = 'none';
                }

                if (e.direction.degrees >= 45 && e.direction.degrees < 135) {
                    e.direction.basic.major = 'up';
                }
                else if (e.direction.degrees >= 135 && e.direction.degrees < 225) {
                    e.direction.basic.major = 'left';
                }
                else if (e.direction.degrees >= 225 && e.direction.degrees < 315) {
                    e.direction.basic.major = 'down';
                }
                else {
                    e.direction.basic.major = 'right';
                }

                return e;
            }
            
            function cancelIntent() {
                this.removeEventListener('touchmove', onTouchMove);
                this.removeEventListener('mousemove', onMouseMove);
                // startX = null;
                intent.isMoving = false;
                intent.isGrabbed = false;

                if (trackingIntervalHandler != null) {
                    clearTimeout(trackingIntervalHandler);
                    trackingIntervalHandler = null;
                }
                
                trackingIntervals = new Array();
            }

            function cancelWipeAnimation() {
                if (swipeAnimation.frameIntervalHandler != null) {
                    clearTimeout(swipeAnimation.frameIntervalHandler);
                    swipeAnimation.frameIntervalHandler = null;
                }
                
                if (swipeAnimation.animationIntervalHandler != null) {
                    clearTimeout(swipeAnimation.animationIntervalHandler);
                    swipeAnimation.animationIntervalHandler = null;
                }
            }

            function beginWipeAnimation(e) {
                var lr = e.direction.basic.major == 'left'  ? -1 : 1;

                swipeAnimation.startTime = new Date().getTime();
                swipeAnimation.initialVelocity = e.delta.velocity;

                cancelWipeAnimation();

                swipeAnimation.frameIntervalHandler = setInterval(function() {
                    swipeAnimation.currentVelocity = swipeAnimation.initialVelocity * Math.exp((-1 * settings.wipeDecayConstant * (new Date().getTime() - swipeAnimation.startTime)) );

                    if (swipeAnimation.currentVelocity <= settings.wipeCancelSpeed) {
                        cancelWipeAnimation();
                    }
                    
                    // Frame skip?
                    swipeAnimation.currentFrameRate = Math.round( lr * settings.wipeMaximumFrame * swipeAnimation.currentVelocity );
                }, 10);
                
                // 67 = 15fps
                // 17 = 60fps
                swipeAnimation.animationIntervalHandler = setInterval(function() {
                    
                    if (settings.wipeFrameLooparound == true) {
                        swipeAnimation.currentFrame = (swipeAnimation.currentFrame + (swipeAnimation.currentFrameRate / (1000 / settings.wipeFps))) % settings.wipeMaximumFrame;
                        
                        if (swipeAnimation.currentFrame <= settings.wipeMinimumFrame) {
                            swipeAnimation.currentFrame = settings.wipeMaximumFrame;
                        }
                    }
                    else {
                        swipeAnimation.currentFrame = (swipeAnimation.currentFrame + (swipeAnimation.currentFrameRate / (1000 / settings.wipeFps)));
                    }
                    
                    swipeAnimation.roundedCurrentFrame = Math.round(swipeAnimation.currentFrame);

                    if (settings.wipeFrameLooparound == true && swipeAnimation.roundedCurrentFrame == 0) {
                        if (lr == -1) {
                            swipeAnimation.roundedCurrentFrame = settings.wipeMaximumFrame;
                        }
                        else {
                            swipeAnimation.roundedCurrentFrame = settings.wipeMinimumFrame;
                        }
                    }

                    settings.animate(e, swipeAnimation);
                }, (1000 / settings.wipeFps));
            }

            function fireWipeEvent(e) {
                cancelIntent();
                
                e = formalizeDirections(e);
                
                settings.wipe(e);
                
                if (e.direction.basic.major == 'up') {
                    settings.wipeUp(e);
                }
                else if (e.direction.basic.major == 'left') {
                    settings.wipeLeft(e);
                }
                else if (e.direction.basic.major == 'down') {
                    settings.wipeDown(e);
                }
                else {
                    settings.wipeRight(e);
                }
                
                beginWipeAnimation(e);
            }
         
            /**
             * Checks if the latest action is a swipe
             * 
             */
            function isSwipeIntent() {

                onInternalTick();

                if (trackingIntervals.length > 0) {
                    var start = trackingIntervals[trackingIntervals.length - Math.min(5, trackingIntervals.length)];
                    var end = trackingIntervals[trackingIntervals.length - 1];

                    var delta = {
                        x: start.x - end.x,
                        y: start.y - end.y,
                        xy: Math.sqrt(Math.pow(start.x - end.x,2) + Math.pow(start.y - end.y,2)),
                        time: end.time - start.time
                    };

                    delta.velocity = delta.time == 0 ? 0 : delta.xy / delta.time;

                    if(delta.velocity > 0 && (Math.abs(delta.x) >= settings.minimumMoveX || Math.abs(delta.y) >= settings.minimumMoveY)) {
                        fireWipeEvent({
                            start: start,
                            end: end,
                            delta: delta
                        });
                    }
                }
            }
            
            function beginDragAnimation(e) {

                var lr = 0;
                var dFrame = 0;
                
                if (settings.containerAnchor == 'horizontal') {
                    lr = e.direction.basic.horizontal == 'left' ? -1 : 1;
                    
                    dFrame = (e.delta.x / settings.containerWidth * settings.wipeMaximumFrame);
                }
                else if (settings.containerAnchor == 'vertical') {
                    lr = e.direction.basic.vertical == 'up' ? -1 : 1;
                    
                    dFrame = (e.delta.y / settings.containerHeight * settings.wipeMaximumFrame);
                }
                else { // == 'none'
                    lr = (e.direction.basic.major == 'up' || e.direction.basic.major == 'left') ? -1 : 1;
                    
                    dFrame = Math.sqrt(Math.pow(e.delta.x,2) + Math.pow(e.delta.y,2)) * settings.wipeMaximumFrame;
                }
                
                cancelWipeAnimation();

                if (lr > 0) {
                    // Right
                    if (settings.wipeFrameLooparound == true) {
                        swipeAnimation.currentFrame = (swipeAnimation.startFrame - dFrame) % settings.wipeMaximumFrame;
                    }
                    else {
                        swipeAnimation.currentFrame = (swipeAnimation.startFrame - dFrame);
                    }
                }
                else {
                    if (settings.wipeFrameLooparound == true) {
                        swipeAnimation.currentFrame = (swipeAnimation.startFrame - dFrame) % settings.wipeMaximumFrame;

                        if (swipeAnimation.currentFrame <= 0) {
                            swipeAnimation.currentFrame = settings.wipeMaximumFrame + swipeAnimation.currentFrame;
                        }
                    }
                    else {
                        swipeAnimation.currentFrame = (swipeAnimation.startFrame - dFrame);
                    }
                }

                swipeAnimation.roundedCurrentFrame = Math.round(swipeAnimation.currentFrame);

                if (settings.wipeFrameLooparound == true && swipeAnimation.roundedCurrentFrame == 0) {
                    if (lr == -1) {
                        swipeAnimation.roundedCurrentFrame = settings.wipeMaximumFrame;
                    }
                    else {
                        swipeAnimation.roundedCurrentFrame = settings.wipeMinimumFrame;
                    }
                }

                settings.animate(e, swipeAnimation);
            }

            function fireDragEvent(e) {
                e = formalizeDirections(e);
                
                settings.dragged(e);

                if (e.delta.x > 0) {
                    settings.draggedLeft(e);
                }
                else {
                    settings.draggedRight(e);
                }
                
                if (e.delta.y > 0) {
                    settings.draggedDown(e);
                }
                else {
                    settings.draggedUp(e);
                }
                
                beginDragAnimation(e);
            }

            function onMove(e, pageX, pageY) {
                if(settings.preventDefaultEvents) {
                    e.preventDefault();
                }
                
                intent.current.x = pageX;
                intent.current.y = pageY;

                var delta = {
                    x: intent.start.x - pageX,
                    y: intent.start.y - pageY,
                    xy: Math.sqrt(Math.pow(intent.start.x - pageX,2) + Math.pow(intent.start.y - pageY,2)),
                    time: (new Date()).getTime() - intent.start.time
                };

                if (inputType == INPUT_TYPE.TOUCH) {
                    fireDragEvent({
                        start: intent.start,
                        end: {
                            x: pageX,
                            y: pageY,
                            time: new Date().getTime()
                        },
                        delta: delta
                    });
                }
                else {

                    if(intent.isMoving == true && intent.isGrabbed == false) {
                        // onInternalTick();

                        isSwipeIntent();
                    }
                    else if (intent.isGrabbed == true) {
                        
                        fireDragEvent({
                            start: intent.start,
                            end: {
                                x: pageX,
                                y: pageY,
                                time: new Date().getTime()
                            },
                            delta: delta
                        });
                    }
                }
            }

            function onTouchMove(e) {
                onMove(e, e.touches[0].pageX, e.touches[0].pageY);
            }
            
            function onMouseMove(e) {
                onMove(e, e.pageX, e.pageY);
            }
    	 
            function onGrabbed(pageX, pageY) {
                // Stop all animation
                cancelWipeAnimation();
                swipeAnimation.startFrame = swipeAnimation.currentFrame;
                
                // Reset on grab if the event returns true.
                if (settings.grabbed(pageX, pageY) == true) {
                    swipeAnimation = {
                        startFrame: 0,
                        startTime: 0,
                        currentFrame: 0,
                        roundedCurrentFrame: 0,
                        currentFrameRate: 0,
                        initialVelocity: 0,
                        currentVelocity: 0,
                        frameIntervalHandler: null,
                        animationIntervalHandler: null
                    };
                }
            }
         
            /**
             * Fired whenever a mouse down or touchstart is fired for any common
             * functionality.
             */
            function onIntentBegin(pageX, pageY) {
                cancelIntent();
                
                onGrabbed(pageX, pageY);

                intent.start.x = pageX;
                intent.start.y = pageY

                intent.current.x = pageX;
                intent.current.y = pageY;

                intent.isMoving = true;
                intent.isGrabbed = true;

                trackingIntervals = new Array();
                onInternalTick();
                trackingIntervalHandler = setInterval(onInternalTick, settings.internalTrackingTick);
            }
         
            /**
             * Fired once per settings.internalTrackingTick (default: 20 ms)
             * whenever the user has the mouse/touch down
             */
            function onInternalTick() {
                trackingIntervals.push({
                    x: intent.current.x,
                    y: intent.current.y,
                    time: new Date().getTime()
                });

                // purge anything older than the maximum gesture timeout
                if (trackingIntervals.length > parseInt(settings.maxGestureTimeout / settings.internalTrackingTick)) {
                    trackingIntervals = trackingIntervals.slice(trackingIntervals.length - Math.min(parseInt(settings.maxGestureTimeout / settings.internalTrackingTick), trackingIntervals.length), trackingIntervals.length - 1);
                }
            }
         
            function onTouchStart(e) {
                if (e.touches.length == 1) {
                    if(settings.preventDefaultEvents) {
                        e.preventDefault();
                    }
                    
                    this.addEventListener('touchmove', onTouchMove, false);
                    
                    onIntentBegin(e.touches[0].pageX, e.touches[0].pageY);
                }
            }    

            function onMouseDown(e) {
                if(settings.preventDefaultEvents) {
                    e.preventDefault();
                }
                
                this.addEventListener('mousemove', onMouseMove, false);
                
                onIntentBegin(e.pageX, e.pageY);
            }
            
            function onTouchEnd(e) {
                intent.isGrabbed = false;
                
                intent.current.x = e.changedTouches[0].pageX;
                intent.current.y = e.changedTouches[0].pageY;

                isSwipeIntent();
            }
            
            /**
             * Just let the controller know the user has let go. Anything after
             * this is considered a wipe on the browser
             */
            function onMouseUp(e) {
                intent.isGrabbed = false;
            }

            /**
             * Cancel it, like they were mouse upping, then check if it was wiped
             * or not.
             */
            function onMouseOut(e) {
                intent.isGrabbed = false;
                
                onMove(e, e.pageX, e.pageY);
            }

            if ('ontouchstart' in document.documentElement) {
                isTouchInput = true;
                this.addEventListener('touchstart', onTouchStart, false);
                this.addEventListener('touchend', onTouchEnd, false);
            } 
            else {
                inputType = INPUT_TYPE.MOUSE;
                // isTouchInput = false;
                this.addEventListener('mousedown', onMouseDown, false);
                this.addEventListener('mouseup', onMouseUp, false);
                this.addEventListener('mouseout', onMouseOut, false);
            }
        });
 
        return this;
    };
 
})(jQuery);

