(function( window, $, undefined ) {

    // http://www.netcu.de/jquery-touchwipe-iphone-ipad-library
    $.fn.touchwipe = function(settings) {

        var config = {
            min_move_x: 20,
            min_move_y: 20,
            wipeLeft: function() { },
            wipeRight: function() { },
            wipeUp: function() { },
            wipeDown: function() { },
            preventDefaultEvents: true
        };

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

        this.each(function() {
            var startX;
            var startY;
            var isMoving = false;

            function cancelTouch() {
                this.removeEventListener('touchmove', onTouchMove);
                startX = null;
                isMoving = false;
            }

            function onTouchMove(e) {
                if(config.preventDefaultEvents) {
                    e.preventDefault();
                }
                if(isMoving) {
                    var x = e.touches[0].pageX;
                    var y = e.touches[0].pageY;
                    var dx = startX - x;
                    var dy = startY - y;
                    if(Math.abs(dx) >= config.min_move_x) {
                        cancelTouch();
                        if(dx > 0) {
                            config.wipeLeft();
                        }
                        else {
                            config.wipeRight();
                        }
                    }
                    else if(Math.abs(dy) >= config.min_move_y) {
                        cancelTouch();
                        if(dy > 0) {
                            config.wipeDown();
                        }
                        else {
                            config.wipeUp();
                        }
                    }
                }
            }

            function onTouchStart(e)
            {
                if (e.touches.length == 1) {
                    startX = e.touches[0].pageX;
                    startY = e.touches[0].pageY;
                    isMoving = true;
                    this.addEventListener('touchmove', onTouchMove, false);
                }
            }
            if ('ontouchstart' in document.documentElement) {
                this.addEventListener('touchstart', onTouchStart, false);
            }
        });

        return this;
    };

    $.elastislide 				= function( options, element ) {
        this.$el	= $( element );
        this._init( options );
    };

    $.elastislide.defaults 		= {
        speed		: 450,	// animation speed
        easing		: '',	// animation easing effect
        imageW		: 90,	// the images width
        margin		: 5,	// image margin right
        border		: 0,	// image border
        minItems	: 5,	// the minimum number of items to show.
        // when we resize the window, this will make sure minItems are always shown
        // (unless of course minItems is higher than the total number of elements)
        current		: 0,	// index of the current item
        // when we resize the window, the carousel will make sure this item is visible
        onClick		: function() {
            return false;
        } // click item callback
    };

    $.elastislide.prototype 	= {
        _init 				: function( options ) {

            this.options 		= $.extend( true, {}, $.elastislide.defaults, options );

            // <ul>
            this.$slider		= this.$el.find('ul');

            // <li>
            this.$items			= this.$slider.children('li');

            // total number of elements / images
            this.itemsCount		= this.$items.length;

            // cache the <ul>'s parent, since we will eventually need to recalculate its width on window resize
            this.$esCarousel	= this.$slider.parent();

            // validate options
            this._validateOptions();

            // set sizes and initialize some vars...
            this._configure();

            // add navigation buttons
            this._addControls();

            // initialize the events
            this._initEvents();

            // show the <ul>
            this.$slider.show();

            // slide to current's position
            this._slideToCurrent( false );

        },
        _validateOptions	: function() {

            if( this.options.speed < 0 )
                this.options.speed = 450;
            if( this.options.margin < 0 )
                this.options.margin = 4;
            if( this.options.border < 0 )
                this.options.border = 1;
            if( this.options.minItems < 1 || this.options.minItems > this.itemsCount )
                this.options.minItems = 1;
            if( this.options.current > this.itemsCount - 1 )
                this.options.current = 0;

        },
        _configure			: function() {

            // current item's index
            this.current		= this.options.current;

            // the ul's parent's (div.es-carousel) width is the "visible" width
            this.visibleWidth	= this.$esCarousel.width();

            // test to see if we need to initially resize the items
            if( this.visibleWidth < this.options.minItems * ( this.options.imageW + 2 * this.options.border ) + ( this.options.minItems - 1 ) * this.options.margin ) {
                this._setDim( ( this.visibleWidth - ( this.options.minItems - 1 ) * this.options.margin ) / this.options.minItems );
                this._setCurrentValues();
                // how many items fit with the current width
                this.fitCount	= this.options.minItems;
            }
            else {
                this._setDim();
                this._setCurrentValues();
            }

            // set the <ul> width
            this.$slider.css({
                width	: this.sliderW
            });

        },
        _setDim				: function( elW ) {

            // <li> style
            this.$items.css({
                marginRight	: this.options.margin,
                width		: ( elW ) ? elW : this.options.imageW + 2 * this.options.border
            }).children('a').css({ // <a> style
                borderWidth		: this.options.border
            });

        },
        _setCurrentValues	: function() {

            // the total space occupied by one item
            this.itemW			= this.$items.outerWidth(true);

            // total width of the slider / <ul>
            // this will eventually change on window resize
            this.sliderW		= this.itemW * this.itemsCount;

            // the ul parent's (div.es-carousel) width is the "visible" width
            this.visibleWidth	= this.$esCarousel.width();

            // how many items fit with the current width
            this.fitCount		= Math.floor( this.visibleWidth / this.itemW );

        },
        _addControls		: function() {

            this.$navNext	= $('<span class="es-nav-next">Next</span>');
            this.$navPrev	= $('<span class="es-nav-prev">Previous</span>');
            $('<div class="es-nav"/>')
            .append( this.$navPrev )
            .append( this.$navNext )
            .appendTo( this.$el );

        //this._toggleControls();

        },
        _toggleControls		: function( dir, status ) {

            // show / hide navigation buttons
            if( dir && status ) {
                if( status === 1 )
                    ( dir === 'right' ) ? this.$navNext.show() : this.$navPrev.show();
                else
                    ( dir === 'right' ) ? this.$navNext.hide() : this.$navPrev.hide();
            }
            else if( this.current === this.itemsCount - 1 || this.fitCount >= this.itemsCount )
                this.$navNext.hide();

        },
        _initEvents			: function() {

            var instance	= this;

            // window resize
            $(window).bind('resize.elastislide', function( event ) {

                // set values again
                instance._setCurrentValues();

                // need to resize items
                if( instance.visibleWidth < instance.options.minItems * ( instance.options.imageW + 2 * instance.options.border ) + ( instance.options.minItems - 1 ) * instance.options.margin ) {
                    instance._setDim( ( instance.visibleWidth - ( instance.options.minItems - 1 ) * instance.options.margin ) / instance.options.minItems );
                    instance._setCurrentValues();
                    instance.fitCount	= instance.options.minItems;
                }
                else{
                    instance._setDim();
                    instance._setCurrentValues();
                }

                instance.$slider.css({
                    width	: instance.sliderW + 10 // TODO: +10px seems to solve a firefox "bug" :S
                });

                // slide to the current element
                clearTimeout( instance.resetTimeout );
                instance.resetTimeout	= setTimeout(function() {
                    instance._slideToCurrent();
                }, 200);

            });

            // navigation buttons events
            this.$navNext.bind('click.elastislide', function( event ) {
                instance._slide('right');
            });

            this.$navPrev.bind('click.elastislide', function( event ) {
                instance._slide('left');
            });

            // item click event
            this.$items.bind('click.elastislide', function( event ) {
                instance.options.onClick( $(this) );
                return false;
            });

            // touch events
            instance.$slider.touchwipe({
                wipeLeft			: function() {
                    instance._slide('right');
                },
                wipeRight			: function() {
                    instance._slide('left');
                }
            });

        },
        _slide: function( dir, val, anim, callback ) {

            // if animating return
            if( this.$slider.is(':animated') )
                return false;

            // current margin left
            var ml		= parseFloat( this.$slider.css('margin-left') );

            // val is just passed when we want an exact value for the margin left (used in the _slideToCurrent function)
            if( val === undefined ) {

                // how much to slide?
                var amount	= this.fitCount * this.itemW, val;

                if( amount < 0 ) return false;

                // make sure not to leave a space between the last item / first item and the end / beggining of the slider available width
                if( dir === 'right' && this.sliderW - ( Math.abs( ml ) + amount ) < this.visibleWidth ) {
                    amount	= this.sliderW - ( Math.abs( ml ) + this.visibleWidth ) - this.options.margin; // decrease the margin left
                    // show / hide navigation buttons
                    this._toggleControls( 'right', -1 );
                    this._toggleControls( 'left', 1 );
                }
                else if( dir === 'left' && Math.abs( ml ) - amount < 0 ) {
                    amount	= Math.abs( ml );
                    // show / hide navigation buttons
                    this._toggleControls( 'left', -1 );
                    this._toggleControls( 'right', 1 );
                }
                else {
                    var fml; // future margin left
                    ( dir === 'right' )
                    ? fml = Math.abs( ml ) + this.options.margin + Math.abs( amount )
                    : fml = Math.abs( ml ) - this.options.margin - Math.abs( amount );

                    // show / hide navigation buttons
                    if( fml > 0 )
                        this._toggleControls( 'left', 1 );
                    else
                        this._toggleControls( 'left', -1 );

                    if( fml < this.sliderW - this.visibleWidth )
                        this._toggleControls( 'right', 1 );
                    else
                        this._toggleControls( 'right', -1 );

                }

                ( dir === 'right' ) ? val = '-=' + amount : val = '+=' + amount

            }
            else {
                var fml	= Math.abs(val); // future margin left

                if( Math.max( this.sliderW, this.visibleWidth ) - fml < this.visibleWidth ) {
                    val	= - ( Math.max( this.sliderW, this.visibleWidth ) - this.visibleWidth );
                    if( val !== 0 )
                        val += this.options.margin;	// decrease the margin left if not on the first position

                    // show / hide navigation buttons
                    this._toggleControls( 'right', -1 );
                    fml	= Math.abs( val );
                }

                // show / hide navigation buttons
                if( fml > 0 )
                    this._toggleControls( 'left', 1 );
                else
                    this._toggleControls( 'left', -1 );

                if( Math.max( this.sliderW, this.visibleWidth ) - this.visibleWidth > fml + this.options.margin )
                    this._toggleControls( 'right', 1 );
                else
                    this._toggleControls( 'right', -1 );

            }

            $.fn.applyStyle = ( anim === undefined ) ? $.fn.animate : $.fn.css;

            var sliderCSS	= {
                marginLeft : val
            };

            var instance	= this;

            this.$slider.applyStyle( sliderCSS, $.extend( true, [], {
                duration : this.options.speed,
                easing : this.options.easing,
                complete : function() {
                    if( callback ) callback.call();
                }
            } ) );

        },
        _slideToCurrent: function( anim ) {

            // how much to slide?
            //var amount	= this.current * this.itemW;
            var amount = (this.itemsCount - 1) * this.itemW;
            this._slide('right', -amount, anim );
        },
        add: function( $newelems, callback ) {

            // adds new items to the carousel
            this.$items 		= this.$items.add( $newelems );
            this.itemsCount		= this.$items.length;
            this._setDim();
            this._setCurrentValues();
            this.$slider.css({
                width	: this.sliderW
            });
            this._slideToCurrent();

            if ( callback ) callback.call( $newelems );

        },
        remove_item: function (callback) {

            // adds new items to the carousel
            this.$items 		= this.$slider.children('li');
            this.itemsCount		= this.$items.length;
            this._setDim();
            this._setCurrentValues();
            this.$slider.css({
                width	: this.sliderW
            });
            this._slideToCurrent();

            if ( callback ) callback.call( $newelems );
        },
        destroy				: function( callback ) {
            this._destroy( callback );
        },
        _destroy 			: function( callback ) {
            this.$el.unbind('.elastislide').removeData('elastislide');
            $(window).unbind('.elastislide');
            if ( callback ) callback.call();
        }
    };

    var logError = function( message ) {
        if ( this.console ) {
            console.error( message );
        }
    };

    $.fn.elastislide = function( options ) {
        if ( typeof options === 'string' ) {
            var args = Array.prototype.slice.call( arguments, 1 );

            this.each(function() {
                var instance = $.data( this, 'elastislide' );
                if ( !instance ) {
                    logError( "cannot call methods on elastislide prior to initialization; " +
                        "attempted to call method '" + options + "'" );
                    return;
                }
                if ( !$.isFunction( instance[options] ) || options.charAt(0) === "_" ) {
                    logError( "no such method '" + options + "' for elastislide instance" );
                    return;
                }
                instance[ options ].apply( instance, args );
            });
        }
        else {
            this.each(function() {
                var instance = $.data( this, 'elastislide' );
                if ( !instance ) {
                    $.data( this, 'elastislide', new $.elastislide( options, this ) );
                }
            });
        }
        return this;
    };

})( window, jQuery );
