/*
 *         ________                                                            ________
 *         ______(_)_____ ____  __________________  __ _____________________   ______(_)_______
 *         _____  /_  __ `/  / / /  _ \_  ___/_  / / / ___  __ \  _ \__  __ \  _____  /__  ___/
 *         ____  / / /_/ // /_/ //  __/  /   _  /_/ /____  /_/ /  __/_  /_/ /______  / _(__  )
 *         ___  /  \__, / \__,_/ \___//_/    _\__, /_(_)  .___/\___/_  .___/_(_)__  /  /____/
 *         /___/     /_/                     /____/    /_/          /_/        /___/
 *
 *        http://pep.briangonzalez.org
 *        Kinetic drag for mobile/desktop.
 *
 *        Copyright (c) 2013 Brian Gonzalez
 *        Licensed under the MIT license.
 *
 *        Title generated using "Speed" @
 *        http://patorjk.com/software/taag/#p=display&f=Speed&t=jquery.pep.js
 */

;(function ( $, window, undefined ) {

    "use strict";

    //  create the defaults once
    var pluginName = 'pep';
    var defaults   = {

        // Options
        // ----------------------------------------------------------------------------------------------
        // See ** https://github.com/briangonzalez/jquery.pep.js ** for fully documented options.
        // It was too hard to manage options here and in the readme.
        // ----------------------------------------------------------------------------------------------
        initiate:                       function(){},
        start:                          function(){},
        drag:                           function(){},
        stop:                           function(){},
        rest:                           function(){},
        moveTo:                         false,
        callIfNotStarted:               ['stop', 'rest'],
        startThreshold:                 [0,0],
        grid:                           [1,1],
        debug:                          false,
        activeClass:                    'pep-active',
        multiplier:                     1,
        velocityMultiplier:             2.5,
        shouldPreventDefault:           true,
        allowDragEventPropagation:      true,
        stopEvents:                     '',
        hardwareAccelerate:             true,
        useCSSTranslation:              true,
        disableSelect:                  true,
        cssEaseString:                  "cubic-bezier(0.190, 1.000, 0.220, 1.000)",
        cssEaseDuration:                1000,
        shouldEase:                     true,
        droppable:                      false,
        droppableActiveClass:           'pep-dpa',
        overlapFunction:                false,
        constrainTo:                    false,
        removeMargins:                  true,
        place:                          true,
        deferPlacement:                 false,
        axis:                           null,
        forceNonCSS3Movement:           false,
        elementsWithInteraction:        'input',
        revert:                         false,
        revertAfter:                    'stop',
        revertIf:                       function(){ return true; },
        ignoreRightClick:               true
    };

    //  ---------------------------------
    //  -----  Our main Pep object  -----
    //  ---------------------------------
    function Pep( el, options ) {

        this.name = pluginName;

        // reference to our DOM object
        // and it's jQuery equivalent.
        this.el  = el;
        this.$el = $(el);

        //  merge in defaults
        this.options    = $.extend( {}, defaults, options) ;

        // store document/body so we don't need to keep grabbing them
        // throughout the code
        this.$document  = $(this.$el[0].ownerDocument);
        this.$body      = this.$document.find('body');

        //  Create our triggers based on touch/click device
        this.moveTrigger        = "MSPointerMove touchmove mousemove";
        this.startTrigger       = "MSPointerDown touchstart mousedown";
        this.stopTrigger        = "MSPointerUp touchend mouseup";
        this.startTriggerArray  = this.startTrigger.split(' ');
        this.moveTriggerArray   = this.moveTrigger.split(' ');
        this.stopTriggerArray   = this.stopTrigger.split(' ');
        this.stopEvents         = [ this.stopTrigger, this.options.stopEvents ].join(' ');

        if ( this.options.constrainTo === 'window' )
            this.$container = this.$document;
        else if ( this.options.constrainTo && (this.options.constrainTo !== 'parent') )
            this.$container = $(this.options.constrainTo);
        else
            this.$container = this.$el.parent();

        // IE need this
        if ( this.isPointerEventCompatible() )
            this.applyMSDefaults();

        this.CSSEaseHash    = this.getCSSEaseHash();
        this.scale          = 1;
        this.started        = false;
        this.disabled       = false;
        this.activeDropRegions = [];
        this.resetVelocityQueue();

        this.init();
    }

    //  init();
    //    initialization logic
    //    you already have access to the DOM el and the options via the instance,
    //    e.g., this.el and this.options
    Pep.prototype.init = function () {

        if ( this.options.debug )
            this.buildDebugDiv();

        if ( this.options.disableSelect )
            this.disableSelect();

        // position the parent & place the object, if necessary.
        if ( this.options.place && !this.options.deferPlacement ) {
            this.positionParent();
            this.placeObject();
        }

        this.ev = {};       // to store our event movements
        this.pos = {};      // to store positions
        this.subscribe();
    };

    //  subscribe();
    //    useful in the event we want to programmatically
    //    interact with our Pep object.
    //      e.g.:     $('#pep').trigger('stop')
    Pep.prototype.subscribe = function () {
        var self = this;

        // Subscribe to our start event
        this.onStartEvent = function(ev){ self.handleStart(ev); };
        this.$el.on(this.startTrigger, this.onStartEvent);

        // Prevent start events from being gobbled by elements that should allow user interaction
        this.onStartEventOnElementsWithInteraction = function(ev){ ev.stopPropagation(); };
        this.$el.on(
            this.startTrigger,
            this.options.elementsWithInteraction,
            this.onStartEventOnElementsWithInteraction
        );

        // Subscribe to our stop event
        this.onStopEvents = function(ev) { self.handleStop(ev); };
        this.$document.on(this.stopEvents, this.onStopEvents);

        // Subscribe to our move event
        this.onMoveEvents = function(ev){ self.moveEvent = ev; };
        this.$document.on(this.moveTrigger, this.onMoveEvents);
    };

    Pep.prototype.unsubscribe = function() {
        this.$el.off(this.startTrigger, this.onStartEvent);
        this.$el.off(
            this.startTrigger,
            this.options.elementsWithInteraction,
            this.onStartEventOnElementsWithInteraction
        );
        this.$document.off(this.stopEvents, this.onStopEvents);
        this.$document.off(this.moveTrigger, this.onMoveEvents);
    };

    //  handleStart();
    //    once this.startTrigger occurs, handle all of the logic
    //    that must go on. This is where Pep's heavy lifting is done.
    Pep.prototype.handleStart = function(ev) {
        var self = this;

        // only continue chugging if our start event is a valid move event.
        if ( this.isValidMoveEvent(ev) && !this.disabled ){

            if( !(this.options.ignoreRightClick && ev.which === 3) ) {

                // IE10 Hack. Me not happy.
                if ( this.isPointerEventCompatible() && ev.preventManipulation )
                    ev.preventManipulation();

                // normalize event
                ev = this.normalizeEvent(ev);

                // position the parent & place the object, if necessary.
                if ( this.options.place && this.options.deferPlacement ) {
                    this.positionParent();
                    this.placeObject();
                }

                // log it
                this.log({ type: 'event', event: ev.type });

                // hardware accelerate, if necessary.
                if ( this.options.hardwareAccelerate && !this.hardwareAccelerated ) {
                    this.hardwareAccelerate();
                    this.hardwareAccelerated = true;
                }

                // fire user's initiate event.
                var shouldContinue = this.options.initiate.call(this, ev, this);
                if ( shouldContinue === false )
                    return;


                // cancel the rest timeout
                clearTimeout( this.restTimeout );

                // add active class and reset css animation, if necessary
                this.$el.addClass( this.options.activeClass );
                this.removeCSSEasing();

                // store event's x & y values for later use
                this.startX = this.ev.x = ev.pep.x;
                this.startY = this.ev.y = ev.pep.y;

                // store initial offset.
                this.initialPosition = this.initialPosition || this.$el.position();

                // store the initial touch/click event, used to calculate the inital delta values.
                this.startEvent = this.moveEvent = ev;

                // make object active, so watchMoveLoop starts looping.
                this.active     = true;

                // preventDefault(), is necessary
                if ( this.options.shouldPreventDefault )
                    ev.preventDefault();

                // allow / disallow event bubbling
                if ( !this.options.allowDragEventPropagation )
                    ev.stopPropagation();

                // animation loop to ensure we don't fire
                // too many unneccessary repaints
                (function watchMoveLoop(){
                    if ( !self.active ) return;
                    self.handleMove();
                    self.requestAnimationFrame( watchMoveLoop );
                })($, self);
            }
        }
    };

    //  handleMove();
    //    the logic for when the move events occur
    Pep.prototype.handleMove = function() {

        // setup our event object
        if ( typeof(this.moveEvent) === 'undefined' )
            return;

        // get our move event's x & y
        var ev      = this.normalizeEvent( this.moveEvent );
        var curX    = window.parseInt(ev.pep.x / this.options.grid[0]) * this.options.grid[0];
        var curY    = window.parseInt(ev.pep.y / this.options.grid[1]) * this.options.grid[1];

        // last in, first out (LIFO) queue to help us manage velocity
        this.addToLIFO( { time: ev.timeStamp, x: curX, y: curY } );

        // calculate values necessary to moving
        var dx, dy;

        if ( $.inArray( ev.type, this.startTriggerArray ) > -1  ){
            dx = 0;
            dy = 0;
        } else{
            dx = curX - this.ev.x;
            dy = curY - this.ev.y;
        }

        this.dx   = dx;
        this.dy   = dy;
        this.ev.x = curX;
        this.ev.y = curY;

        // no movement in either direction -- so return
        if (dx === 0 && dy === 0){
            this.log({ type: 'event', event: '** stopped **' });
            return;
        }

        // check if object has moved past X/Y thresholds
        // if so, fire users start event
        var initialDx  = Math.abs(this.startX - curX);
        var initialDy  = Math.abs(this.startY - curY);
        if ( !this.started && ( initialDx > this.options.startThreshold[0] || initialDy > this.options.startThreshold[1] ) ){
            this.started = true;
            this.$el.addClass('pep-start');
            this.options.start.call(this, this.startEvent, this);
        }

        // Calculate our drop regions
        if ( this.options.droppable ) {
            this.calculateActiveDropRegions();
        }

        // fire user's drag event.
        var continueDrag = this.options.drag.call(this, ev, this);

        if ( continueDrag === false ) {
            this.resetVelocityQueue();
            return;
        }

        // log the move trigger & event position
        this.log({ type: 'event', event: ev.type });
        this.log({ type: 'event-coords', x: this.ev.x, y: this.ev.y });
        this.log({ type: 'velocity' });

        this.doMoveTo(dx, dy);
    };

    Pep.prototype.doMoveTo = function(dx, dy) {
        var hash = this.handleConstraint(dx, dy);
        var xOp, yOp;

        // if using not using CSS transforms, move object via absolute position
        if ( typeof this.options.moveTo === 'function') {
            xOp     = ( dx >= 0 ) ? "+=" + Math.abs(dx/this.scale)*this.options.multiplier : "-=" + Math.abs(dx/this.scale)*this.options.multiplier;
            yOp     = ( dy >= 0 ) ? "+=" + Math.abs(dy/this.scale)*this.options.multiplier : "-=" + Math.abs(dy/this.scale)*this.options.multiplier;

            if ( this.options.constrainTo ) {
                xOp = (hash.x !== false) ? hash.x : xOp;
                yOp = (hash.y !== false) ? hash.y : yOp;
            }

            // only move along single axis, if necessary
            if ( this.options.axis  === 'x' ) yOp = hash.y;
            if ( this.options.axis  === 'y' ) xOp = hash.x;

            this.options.moveTo.call(this, xOp, yOp);
        } else if ( !this.shouldUseCSSTranslation() ){
            xOp     = ( dx >= 0 ) ? "+=" + Math.abs(dx/this.scale)*this.options.multiplier : "-=" + Math.abs(dx/this.scale)*this.options.multiplier;
            yOp     = ( dy >= 0 ) ? "+=" + Math.abs(dy/this.scale)*this.options.multiplier : "-=" + Math.abs(dy/this.scale)*this.options.multiplier;

            if ( this.options.constrainTo ) {
                xOp = (hash.x !== false) ? hash.x : xOp;
                yOp = (hash.y !== false) ? hash.y : yOp;
            }

            // only move along single axis, if necessary
            if ( this.options.axis  === 'x' ) yOp = hash.y;
            if ( this.options.axis  === 'y' ) xOp = hash.x;

            this.moveTo(xOp, yOp);
        }
        else {

            dx = (dx/this.scale)*this.options.multiplier;
            dy = (dy/this.scale)*this.options.multiplier;

            if ( this.options.constrainTo ) {
                dx = (hash.x === false) ? dx : 0 ;
                dy = (hash.y === false) ? dy : 0 ;
            }

            // only move along single axis, if necessary
            if ( this.options.axis  === 'x' ) dy = 0;
            if ( this.options.axis  === 'y' ) dx = 0;

            this.moveToUsingTransforms( dx, dy );
        }
    };

    //  handleStop();
    //    the logic for when the stop events occur
    Pep.prototype.handleStop = function(ev) {

        // no need to handle stop event if we're not active
        if (!this.active)
            return;

        // log it
        this.log({ type: 'event', event: ev.type });

        // make object inactive, so watchMoveLoop returns
        this.active = false;

        // remove our start class
        this.$el.removeClass('pep-start')
            .addClass('pep-ease');

        // Calculate our drop regions
        if ( this.options.droppable ) {
            this.calculateActiveDropRegions();
        }

        // fire user's stop event.
        if ( this.started || (!this.started &&  $.inArray('stop', this.options.callIfNotStarted) > -1 ) ) {
            this.options.stop.call(this, ev, this);
        }

        // ease the object, if necessary.
        if (this.options.shouldEase) {
            this.ease(ev, this.started);
        } else {
            this.removeActiveClass();
        }

        if ( this.options.revert && (this.options.revertAfter === 'stop' || !this.options.shouldEase) && ( this.options.revertIf && this.options.revertIf.call(this) ) ) {
            this.revert();
        }

        // this must be set to false after
        // the user's stop event is called, so the dev
        // has access to it.
        this.started = false;

        // reset the velocity queue
        this.resetVelocityQueue();

    };

    //  ease();
    //    used in conjunction with the LIFO queue
    //    to ease the object after stop
    Pep.prototype.ease = function(ev, started){

        var pos       = this.$el.position();
        var vel       = this.velocity();
        var dt        = this.dt;
        var x         = (vel.x/this.scale) * this.options.multiplier;
        var y         = (vel.y/this.scale) * this.options.multiplier;

        var hash      = this.handleConstraint(x, y, true);

        // ✪  Apply the CSS3 animation easing magic  ✪
        if ( this.cssAnimationsSupported() )
            this.$el.css( this.getCSSEaseHash() );

        var xOp = ( vel.x > 0 ) ? "+=" + x : "-=" + Math.abs(x);
        var yOp = ( vel.y > 0 ) ? "+=" + y : "-=" + Math.abs(y);

        if ( this.options.constrainTo ) {
            xOp = (hash.x !== false) ? hash.x : xOp;
            yOp = (hash.y !== false) ? hash.y : yOp;
        }

        if ( this.options.axis  === 'x' ) yOp = "+=0";
        if ( this.options.axis  === 'y' ) xOp = "+=0";

        // ease it via JS, the last true tells it to animate.
        var jsAnimateFallback = !this.cssAnimationsSupported() || this.options.forceNonCSS3Movement;
        if (typeof this.options.moveTo === 'function') {
            this.options.moveTo.call(this, xOp, yOp);
        } else {
            this.moveTo(xOp, yOp, jsAnimateFallback);
        }

        // when the rest occurs, remove active class and call
        // user's rest event.
        var self = this;
        this.restTimeout = setTimeout( function(){

            // Calculate our drop regions
            if ( self.options.droppable ) {
                self.calculateActiveDropRegions();
            }

            // call users rest event.
            if ( started || ( !started && $.inArray('rest', self.options.callIfNotStarted) > -1 ) ) {
                self.options.rest.call(self, ev, self);
            }

            // revert thy self!
            if ( self.options.revert && (self.options.revertAfter === 'ease' && self.options.shouldEase) && ( self.options.revertIf && self.options.revertIf.call(self) ) ) {
                self.revert();
            }

            // remove active class
            self.removeActiveClass();

        }, this.options.cssEaseDuration );

    };

    // normalizeEvent()
    Pep.prototype.normalizeEvent = function(ev) {
        ev.pep        = {};

        if ( this.isPointerEventCompatible() || !this.isTouch(ev) ) {

            if ( ev.pageX  ) {
                ev.pep.x      = ev.pageX;
                ev.pep.y      = ev.pageY;
            } else {
                ev.pep.x      = ev.originalEvent.pageX;
                ev.pep.y      = ev.originalEvent.pageY;
            }

            ev.pep.type   = ev.type;

        }
        else {
            ev.pep.x      = ev.originalEvent.touches[0].pageX;
            ev.pep.y      = ev.originalEvent.touches[0].pageY;
            ev.pep.type   = ev.type;
        }

        return ev;
    };

    // resetVelocityQueue()
    //
    Pep.prototype.resetVelocityQueue = function() {
        this.velocityQueue = new Array(5);
    };

    //  moveTo();
    //    move the object to an x and/or y value
    //    using jQuery's .css function -- this fxn uses the
    //    .css({top: "+=20", left: "-=30"}) syntax
    Pep.prototype.moveTo = function(x,y, animate) {

        this.log({ type: 'delta', x: x, y: y });
        if ( animate ) {
            this.$el.animate({ top: y, left: x }, 0, 'easeOutQuad', {queue: false});
        } else{
            this.$el.stop(true, false).css({ top: y , left: x });
        }

    };

    //  moveToUsingTransforms();
    //    move the object to an x and/or y value
    Pep.prototype.moveToUsingTransforms = function(x,y) {

        // Check for our initial values if we don't have them.
        var matrixArray  = this.matrixToArray( this.matrixString() );
        if ( !this.cssX )
            this.cssX = this.xTranslation( matrixArray );

        if ( !this.cssY )
            this.cssY = this.yTranslation( matrixArray );

        // CSS3 transforms are additive from current position
        this.cssX = this.cssX + x;
        this.cssY = this.cssY + y;

        this.log({ type: 'delta', x: x, y: y });

        matrixArray[4]    = this.cssX;
        matrixArray[5]    = this.cssY;

        this.translation  = this.arrayToMatrix( matrixArray );
        this.transform( this.translation );
    };

    Pep.prototype.transform = function(value) {
        this.$el.css({
            '-webkit-transform': value,
            '-moz-transform': value,
            '-ms-transform': value,
            '-o-transform': value,
            'transform': value  });
    };

    Pep.prototype.xTranslation = function(matrixArray) {
        matrixArray  = matrixArray || this.matrixToArray( this.matrixString() );
        return parseInt(matrixArray[4], 10);
    };

    Pep.prototype.yTranslation = function(matrixArray) {
        matrixArray  = matrixArray || this.matrixToArray( this.matrixString() );
        return parseInt(matrixArray[5], 10);
    };


    // 3 helper functions for working with the
    // objects CSS3 transforms
    // matrixString
    // matrixToArray
    // arrayToMatrix
    Pep.prototype.matrixString = function() {

        var validMatrix = function(o){
            return !( !o || o === 'none' || o.indexOf('matrix') < 0  );
        };

        var matrix = "matrix(1, 0, 0, 1, 0, 0)";

        if ( validMatrix( this.$el.css('-webkit-transform') ) )
            matrix = this.$el.css('-webkit-transform');

        if ( validMatrix( this.$el.css('-moz-transform') ) )
            matrix = this.$el.css('-moz-transform');

        if ( validMatrix( this.$el.css('-ms-transform') ) )
            matrix = this.$el.css('-ms-transform');

        if ( validMatrix( this.$el.css('-o-transform') ) )
            matrix = this.$el.css('-o-transform');

        if ( validMatrix( this.$el.css('transform') ) )
            matrix = this.$el.css('transform');

        return matrix;
    };

    Pep.prototype.matrixToArray = function(str) {
        return str.split('(')[1].split(')')[0].split(',');
    };

    Pep.prototype.arrayToMatrix = function(array) {
        return "matrix(" +  array.join(',')  + ")";
    };

    //  addToLIFO();
    //    a Last-In/First-Out array of the 5 most recent
    //    velocity points, which is used for easing
    Pep.prototype.addToLIFO = function(val){
        // last in, first out
        var arr = this.velocityQueue;
        arr = arr.slice(1, arr.length);
        arr.push(val);
        this.velocityQueue = arr;
    };

    //  velocity();
    //    using the LIFO, calculate velocity and return
    //    velocity in each direction (x & y)
    Pep.prototype.velocity = function(){
        var sumX = 0;
        var sumY = 0;

        for ( var i = 0; i < this.velocityQueue.length -1; i++  ){
            if ( this.velocityQueue[i] ){
                sumX        += (this.velocityQueue[i+1].x - this.velocityQueue[i].x);
                sumY        += (this.velocityQueue[i+1].y - this.velocityQueue[i].y);
                this.dt     = ( this.velocityQueue[i+1].time - this.velocityQueue[i].time );
            }
        }

        // return velocity in each direction.
        return { x: sumX*this.options.velocityMultiplier, y: sumY*this.options.velocityMultiplier};
    };

    Pep.prototype.revert = function() {
        if ( this.shouldUseCSSTranslation() ){
            this.moveToUsingTransforms(-this.xTranslation(),-this.yTranslation());
        }

        this.moveTo(this.initialPosition.left, this.initialPosition.top);
    };

    //  requestAnimationFrame();
    //    requestAnimationFrame Polyfill
    //    More info:
    //    http://paulirish.com/2011/requestanimationframe-for-smart-animating/
    Pep.prototype.requestAnimationFrame = function(callback) {
        return  window.requestAnimationFrame        && window.requestAnimationFrame(callback)         ||
            window.webkitRequestAnimationFrame  && window.webkitRequestAnimationFrame(callback)   ||
            window.mozRequestAnimationFrame     && window.mozRequestAnimationFrame(callback)      ||
            window.oRequestAnimationFrame       && window.mozRequestAnimationFrame(callback)      ||
            window.msRequestAnimationFrame      && window.msRequestAnimationFrame(callback)       ||
            window.setTimeout(callback, 1000 / 60);
    };

    //  positionParent();
    //    add the right positioning to the parent object
    Pep.prototype.positionParent = function() {

        if ( !this.options.constrainTo || this.parentPositioned )
            return;

        this.parentPositioned = true;

        // make `relative` parent if necessary
        if ( this.options.constrainTo === 'parent' ) {
            this.$container.css({ position: 'relative' });
        } else if ( this.options.constrainTo === 'window'             &&
            this.$container.get(0).nodeName !== "#document"   &&
            this.$container.css('position') !== 'static' )
        {
            this.$container.css({ position: 'static' });
        }

    };

    //  placeObject();
    //    add the right positioning to the object
    Pep.prototype.placeObject = function() {

        if ( this.objectPlaced )
            return;

        this.objectPlaced = true;

        this.offset = (this.options.constrainTo === 'parent' || this.hasNonBodyRelative() ) ?
            this.$el.position() : this.$el.offset();

        // better to leave absolute position alone if
        // it already has one.
        if ( parseInt( this.$el.css('left'), 10 ) )
            this.offset.left = this.$el.css('left');

        if ( parseInt( this.$el.css('top'), 10 ) )
            this.offset.top = this.$el.css('top');

        if ( this.options.removeMargins )
            this.$el.css({margin: 0});

        this.$el.css({
            position:   'absolute',
            top:        this.offset.top,
            left:       this.offset.left
        });

    };

    //  hasNonBodyRelative()
    //    returns true if any parent other than the body
    //    has relative positioning
    Pep.prototype.hasNonBodyRelative = function() {
        return this.$el.parents().filter(function() {
            var $this = $(this);
            return $this.is('body') || $this.css('position') === 'relative';
        }).length > 1;
    };

    //  setScale()
    //    set the scale of the object being moved.
    Pep.prototype.setScale = function(val) {
        this.scale = val;
    };

    //  setMultiplier()
    //    set the multiplier of the object being moved.
    Pep.prototype.setMultiplier = function(val) {
        this.options.multiplier = val;
    };

    //  removeCSSEasing();
    //    remove CSS easing properties, if necessary
    Pep.prototype.removeCSSEasing = function() {
        if ( this.cssAnimationsSupported() )
            this.$el.css( this.getCSSEaseHash(true) );
    };

    //  disableSelect();
    //    add the property which causes the object
    //    to not be selected user drags over text areas
    Pep.prototype.disableSelect = function() {

        this.$el.css({
            '-webkit-touch-callout' : 'none',
            '-webkit-user-select' : 'none',
            '-khtml-user-select' : 'none',
            '-moz-user-select' : 'none',
            '-ms-user-select' : 'none',
            'user-select' : 'none'
        });

    };

    // removeActiveClass()
    //  Removes the active class.
    Pep.prototype.removeActiveClass = function() {
        this.$el.removeClass( [this.options.activeClass, 'pep-ease'].join(' ') );
    };

    //  handleConstraint();
    //    returns a hash of where to move to
    //    when we constrain to parent/window
    Pep.prototype.handleConstraint = function(dx, dy, accountForTranslation) {
        var pos               = this.$el.position();
        this.pos.x            = pos.left;
        this.pos.y            = pos.top;

        var hash              = { x: false, y: false };

        var upperYLimit, upperXLimit, lowerXLimit, lowerYLimit;

        // log our positions
        this.log({ type: "pos-coords", x: this.pos.x, y: this.pos.y});

        if ( $.isArray( this.options.constrainTo ) ) {

            if ( this.options.constrainTo[3] !== undefined && this.options.constrainTo[1] !== undefined ) {
                upperXLimit     = this.options.constrainTo[1] === false ?  Infinity : this.options.constrainTo[1];
                lowerXLimit     = this.options.constrainTo[3] === false ? -Infinity : this.options.constrainTo[3];
            }
            if ( this.options.constrainTo[0] !== false && this.options.constrainTo[2] !== false ) {
                upperYLimit       = this.options.constrainTo[2] === false ?  Infinity : this.options.constrainTo[2];
                lowerYLimit       = this.options.constrainTo[0] === false ? -Infinity : this.options.constrainTo[0];
            }

            // is our object trying to move outside lower X & Y limits?
            if ( this.pos.x + dx < lowerXLimit)     hash.x = lowerXLimit;
            if ( this.pos.y + dy < lowerYLimit)     hash.y = lowerYLimit;

        } else if ( typeof this.options.constrainTo === 'string' ) {
            lowerXLimit       = 0;
            lowerYLimit       = 0;
            upperXLimit       = this.$container.width()  - this.$el.outerWidth();
            upperYLimit       = this.$container.height() - this.$el.outerHeight();

            // is our object trying to move outside lower X & Y limits?
            if ( this.pos.x + dx < 0 )              hash.x = 0;
            if ( this.pos.y + dy < 0 )              hash.y = 0;
        }

        // is our object trying to move outside upper X & Y limits?
        if ( this.pos.x + dx > upperXLimit )    hash.x = upperXLimit;
        if ( this.pos.y + dy > upperYLimit )    hash.y = upperYLimit;

        // Account for translation, which makes movement a little tricky.
        if ( this.shouldUseCSSTranslation() && accountForTranslation ){
            if (hash.x === lowerXLimit && this.xTranslation() ) hash.x = lowerXLimit - this.xTranslation();
            if (hash.x === upperXLimit && this.xTranslation() ) hash.x = upperXLimit - this.xTranslation();

            if (hash.y === lowerYLimit && this.yTranslation() ) hash.y = lowerYLimit - this.yTranslation();
            if (hash.y === upperYLimit && this.yTranslation() ) hash.y = upperYLimit - this.yTranslation();
        }

        return hash;
    };

    //  getCSSEaseHash();
    //    returns a hash of params used in conjunction
    //    with this.options.cssEaseString
    Pep.prototype.getCSSEaseHash = function(reset){
        if ( typeof(reset) === 'undefined' ) reset = false;

        var cssEaseString;
        if (reset){
            cssEaseString = '';
        } else if ( this.CSSEaseHash ) {
            return this.CSSEaseHash;
        } else {
            cssEaseString = ['all', this.options.cssEaseDuration + 'ms', this.options.cssEaseString].join(' ');
        }

        return {
            '-webkit-transition'   : cssEaseString,   // chrome, safari, etc.
            '-moz-transition'   : cssEaseString,   // firefox
            '-ms-transition'   : cssEaseString,   // microsoft
            '-o-transition'   : cssEaseString,   // opera
            'transition'   : cssEaseString    // future
        };
    };

    // calculateActiveDropRegions()
    //    sets parent droppables of this.
    Pep.prototype.calculateActiveDropRegions = function() {
        var self = this;
        this.activeDropRegions.length = 0;

        $.each( $(this.options.droppable), function(idx, el){
            var $el = $(el);
            if ( self.isOverlapping($el, self.$el) ){
                $el.addClass(self.options.droppableActiveClass);
                self.activeDropRegions.push($el);
            } else {
                $el.removeClass(self.options.droppableActiveClass);
            }
        });

    };

    //  isOverlapping();
    //    returns true if element a over
    Pep.prototype.isOverlapping = function($a,$b) {

        if ( this.options.overlapFunction ) {
            return this.options.overlapFunction($a,$b);
        }

        var rect1 = $a[0].getBoundingClientRect();
        var rect2 = $b[0].getBoundingClientRect();

        return !( rect1.right   < rect2.left  ||
            rect1.left    > rect2.right ||
            rect1.bottom  < rect2.top   ||
            rect1.top     > rect2.bottom  );
    };

    //  isTouch();
    //    returns whether or not event is a touch event
    Pep.prototype.isTouch = function(ev){
        return ev.type.search('touch') > -1;
    };

    // isPointerEventCompatible();
    //    return whether or note our device is pointer
    //    event compatible; typically means where on a
    //    touch Win8 device
    Pep.prototype.isPointerEventCompatible = function() {
        return ("MSPointerEvent" in window);
    };

    // applyMSDefaults();
    Pep.prototype.applyMSDefaults = function(first_argument) {
        this.$el.css({
            '-ms-touch-action' :    'none',
            'touch-action' :        'none',
            '-ms-scroll-chaining':  'none',
            '-ms-scroll-limit':     '0 0 0 0'
        });
    };

    //  isValidMoveEvent();
    //    returns true if we're on a non-touch device -- or --
    //    if the event is **single** touch event on a touch device
    Pep.prototype.isValidMoveEvent = function(ev){
        return ( !this.isTouch(ev) || ( this.isTouch(ev) && ev.originalEvent && ev.originalEvent.touches && ev.originalEvent.touches.length === 1 ) );
    };

    //  shouldUseCSSTranslation();
    //    return true if we should use CSS transforms for move the object
    Pep.prototype.shouldUseCSSTranslation = function() {

        if ( this.options.forceNonCSS3Movement )
            return false;

        if ( typeof(this.useCSSTranslation) !== "undefined" )
            return this.useCSSTranslation;

        var useCSSTranslation = false;

        if ( !this.options.useCSSTranslation || ( typeof(Modernizr) !== "undefined" && !Modernizr.csstransforms)){
            useCSSTranslation = false;
        }
        else{
            useCSSTranslation = true;
        }

        this.useCSSTranslation =  useCSSTranslation;
        return useCSSTranslation;
    };

    //  cssAnimationsSupported():
    //    returns true if the browser supports CSS animations
    //    which are used for easing..
    Pep.prototype.cssAnimationsSupported = function() {

        if ( typeof(this.cssAnimationsSupport) !== "undefined" ){
            return this.cssAnimationsSupport;
        }

        // If the page has Modernizr, let them do the heavy lifting.
        if ( ( typeof(Modernizr) !== "undefined" && Modernizr.cssanimations) ){
            this.cssAnimationsSupport = true;
            return true;
        }

        var animation = false,
            elm = document.createElement('div'),
            animationstring = 'animation',
            keyframeprefix = '',
            domPrefixes = 'Webkit Moz O ms Khtml'.split(' '),
            pfx  = '';

        if( elm.style.animationName ) { animation = true; }

        if( animation === false ) {
            for( var i = 0; i < domPrefixes.length; i++ ) {
                if( elm.style[ domPrefixes[i] + 'AnimationName' ] !== undefined ) {
                    pfx = domPrefixes[ i ];
                    animationstring = pfx + 'Animation';
                    keyframeprefix = '-' + pfx.toLowerCase() + '-';
                    animation = true;
                    break;
                }
            }
        }

        this.cssAnimationsSupport = animation;
        return animation;
    };

    //  hardwareAccelerate();
    //    add fool-proof CSS3 hardware acceleration.
    Pep.prototype.hardwareAccelerate = function() {
        this.$el.css({
            '-webkit-perspective':          1000,
            'perspective':                  1000,
            '-webkit-backface-visibility':  'hidden',
            'backface-visibility':          'hidden'
        });
    };

    //  getMovementValues();
    //    returns object pos, event position, and velocity in each direction.
    Pep.prototype.getMovementValues = function() {
        return { ev: this.ev, pos: this.pos, velocity: this.velocity() };
    };

    //  buildDebugDiv();
    //    Create a little div in the lower right corner of the window
    //    for extra info about the object currently moving
    Pep.prototype.buildDebugDiv = function() {

        // Build the debugDiv and it's inner HTML -- if necessary
        var $debugDiv;
        if ( $('#pep-debug').length === 0 ){
            $debugDiv = $('<div></div>');
            $debugDiv
                .attr('id', 'pep-debug')
                .append("<div style='font-weight:bold; background: red; color: white;'>DEBUG MODE</div>")
                .append("<div id='pep-debug-event'>no event</div>")
                .append("<div id='pep-debug-ev-coords'>event coords: <span class='pep-x'>-</span>, <span class='pep-y'>-</span></div>")
                .append("<div id='pep-debug-pos-coords'>position coords: <span class='pep-x'>-</span>, <span class='pep-y'>-</span></div>")
                .append("<div id='pep-debug-velocity'>velocity: <span class='pep-x'>-</span>, <span class='pep-y'>-</span></div>")
                .append("<div id='pep-debug-delta'>&Delta; movement: <span class='pep-x'>-</span>, <span class='pep-y'>-</span></div>")
                .css({
                    position:   'fixed',
                    bottom:     5,
                    right:      5,
                    zIndex:     99999,
                    textAlign:  'right',
                    fontFamily: 'Arial, sans',
                    fontSize:   10,
                    border:     '1px solid #DDD',
                    padding:    '3px',
                    background: 'white',
                    color:      '#333'
                });
        }

        var self = this;
        setTimeout(function(){
            self.debugElements = {
                $event:      $("#pep-debug-event"),
                $velocityX:  $("#pep-debug-velocity .pep-x"),
                $velocityY:  $("#pep-debug-velocity .pep-y"),
                $dX:         $("#pep-debug-delta .pep-x"),
                $dY:         $("#pep-debug-delta .pep-y"),
                $evCoordsX:  $("#pep-debug-ev-coords .pep-x"),
                $evCoordsY:  $("#pep-debug-ev-coords .pep-y"),
                $posCoordsX: $("#pep-debug-pos-coords .pep-x"),
                $posCoordsY: $("#pep-debug-pos-coords .pep-y")
            };
        }, 0);

        $('body').append( $debugDiv );
    };

    // log()
    Pep.prototype.log = function(opts) {
        if ( !this.options.debug ) return;

        switch (opts.type){
            case "event":
                this.debugElements.$event.text(opts.event);
                break;
            case "pos-coords":
                this.debugElements.$posCoordsX.text(opts.x);
                this.debugElements.$posCoordsY.text(opts.y);
                break;
            case "event-coords":
                this.debugElements.$evCoordsX.text(opts.x);
                this.debugElements.$evCoordsY.text(opts.y);
                break;
            case "delta":
                this.debugElements.$dX.text(opts.x);
                this.debugElements.$dY.text(opts.y);
                break;
            case "velocity":
                var vel = this.velocity();
                this.debugElements.$velocityX.text( Math.round(vel.x) );
                this.debugElements.$velocityY.text( Math.round(vel.y) );
                break;
        }
    };

    // toggle()
    //  toggle the pep object
    Pep.prototype.toggle = function(on) {
        if ( typeof(on) === "undefined"){
            this.disabled = !this.disabled;
        }
        else {
            this.disabled = !on;
        }
    };

    //  *** Special Easings functions ***
    //    Used for JS easing fallback
    //    We can use any of these for a
    //    good intertia ease
    $.extend($.easing,
        {
            easeOutQuad: function (x, t, b, c, d) {
                return -c *(t/=d)*(t-2) + b;
            },
            easeOutCirc: function (x, t, b, c, d) {
                return c * Math.sqrt(1 - (t=t/d-1)*t) + b;
            },
            easeOutExpo: function (x, t, b, c, d) {
                return (t===d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b;
            }
        });

    //  wrap it
    //    A really lightweight plugin wrapper around the constructor,
    //    preventing against multiple instantiations.
    $.fn[pluginName] = function ( options ) {
        return this.each(function () {
            if (!$.data(this, 'plugin_' + pluginName)) {
                var pepObj = new Pep( this, options );
                $.data(this, 'plugin_' + pluginName, pepObj);
                $.pep.peps.push(pepObj);
            }
        });
    };

    //  The   _   ___ ___
    //       /_\ | _ \_ _|
    //      / _ \|  _/| |
    //     /_/ \_\_| |___|
    //
    $.pep = {};
    $.pep.peps = [];
    $.pep.toggleAll = function(on){
        $.each(this.peps, function(index, pepObj){
            pepObj.toggle(on);
        });
    };

    $.pep.unbind = function($obj){
        var pep = $obj.data('plugin_' + pluginName);

        if ( typeof pep === 'undefined' )
            return;

        pep.toggle(false);
        pep.unsubscribe();
        $obj.removeData('plugin_' + pluginName);

    };

}(jQuery, window));



