var XCarousel;

(function($) {
     var config_defaults = {
         /**
          * start_slide - Slide to begin on
          */
         start_slide: 0,

         /**
          * transition - Transition to use. More transitions can be
          * added to XCarousel.transitions
          */
         transition: 'fade',

         /**
          * axis - this only aplies to directional transitions
          * (like 'slide'), but indicates the direction they should slide in
          */
         axis: 'horizontal',

         /**
          * align - Where to line the current slide up with the slide
          * holder. Valid values for transition 'slide' carousels are 'left',
          * 'right', and 'center'. Valid values for 'lslide' carousels with
          * direction 'vertical are 'top', 'bottom' and 'middle'
          */
         align: 'center',

         /**
          * infinite - whether the carousel should go on forever, or
          * should hit the end. Currently this only works for horizontal slide
          * scrollers
          */
         infinite: false,

         /**
          * click_to_slide - when set to true, will grab the click
          * event on slides, and slide to the clicked one on click. This will
          * prevent the browser from performing whatever default action it
          * would have on click
          */

         /**
          * holder - jQuery array (with one element) or selector for
          * slide holder, required to be defined. The holder is the viewport in
          * which the slides are displayed
          */
         holder: null, // required parameter with no default

         /**
          * resize_holder - if set true, the slide holder will be
          * resized to fit the current slide
          */
         resize_holder: false,

         /**
          * wrapper - optional pre-defined wrapper for slides. If this
          * is not set, then transitions that need a wrapper will add
          * a new wrapper div inside the holder, and move all the
          * slides into it.
          */
         wrapper: null,

         /**
          * slides - jQuery array or selector for slide objects. If a
          * selector is given, the search will happen in the context of the
          * holder parameter.
          */
         slides: '.slide',

         /**
          * next - jQuery array or selector for the next slide button.
          */
         next: '.next',

         /**
          * prev - jQeury array or selector for the previous slide button.
          */
         prev: '.prev',

         /**
          * per_page - the number of slides to show per page. This
          * setting is only used by transition methods for which it makse sense
          * to show more than one slide at a time, i.e. the sliding transition.
          */
         per_page: 1,

         /**
          * step - the number of slides to move when next or previous is clicked
          */
         step: 1,

         /**
          * callbacks - object filled with callbacks for various
          * events. currently only 'gotoSlide' is supported. Each callback can
          * either be a single function or an array of functions to be bound.
          */
         callbacks: {}
     };

     if (window.console && console.log) {
         var log = console.log;
     }
     else {
         var log = function() { }; // empty log function for those without firebug
     }

     /**
      * Helper function to handle a click event and call the callback
      * as if it were called from a specific context.
      */
     function click_wrap(context, fn) {
         return function(e) {
             e.preventDefault();
             fn.apply(context);
         };
     }

     /**
      * Helper function to make sure a number is greater than -1 and
      * less than max, by wrapping that number back into the wrange
      */
     function wrap_number(number, max)  {
         if (number >= max) number %= max;
         else while (number < 0) number += max;
         return number;
     }

     /**
      * XCarousel - Create a new XCarousel object.
      *
      * @param {Object} config The config for the carousel.
      *
      * Any extra parameters passed in will be passed along to the transition
      * setup function.
      *
      */
     XCarousel = function(config) {
         if (this == window) return new XCarousel(config);

         this.callbacks = {};
         if ($.isArray(config)) {
             this.config = $.extend({}, config_defaults, { transition: 'multi' });
             this.holder = $([]);
             this.slides = $([]);
         }
         else {
             this.config = $.extend({}, config_defaults, config);
             $.each(this.config.callbacks, function(callback, event) {
                 this.bind(event, callback);
             });
             this.setup();
         }
         if (!this.holder.length && this.config.transition != 'multi') return false;
         var transition = this.config['transition'] || '';
         if (!XCarousel.transitions[transition]) {
             throw 'Undefined transition "' + transition + '"';
         }

         this.current_slide = config.start_slide || 0;
         this.transition = XCarousel.transitions[transition];
         this.transition.setup.apply(this, arguments);

         return this;
     };

     XCarousel.prototype = {
         setup: function() {
             this.holder = $(this.config.holder);
             this.slides = (typeof this.config.slides == 'string')
                 ? this.holder.find(this.config.slides)
                 : $(this.config.slides);
             this.next = (typeof this.config.next == 'string')
                 ? this.holder.find(this.config.next)
                 : $(this.config.next);
             this.prev = (typeof this.config.prev == 'string')
                 ? this.holder.find(this.config.prev)
                 : $(this.config.prev);
             this.next.unbind().click(click_wrap(this, this.nextSlide));
             this.prev.unbind().click(click_wrap(this, this.prevSlide));
         },

         update: function() {
             this.setup();
             if (this.transition.update) this.transition.update.apply(this);
         },

         remove: function() {
             if (this.transition.remove) this.transition.remove.apply(this);
             this.next.unbind();
             this.prev.unbind();
         },

         bind: function(event, callback) {
             // if it's an array, add them all
             if ($.isArray(callback)) {
                 var self = this;
                 $.each(callback, function() {
                     self.bind(event, this);
                 });
             }
             else if (this.callbacks[event]) {
                 this.callbacks[event].push(callback);
             }
             else {
                 this.callbacks[event] = [callback];
             }
         },

         trigger: function(event) {
             if (this.callbacks[event]) {
                 var self = this;
                 var args = Array.prototype.slice.apply(arguments, [1]);
                 $.each(this.callbacks[event], function(i, callback) {
                     this.apply(self, args);
                 });
             }
         },

         gotoSlide: function(slide_num, no_trigger) {
             if (!no_trigger && this.callbacks['gotoSlide']) {
                 this.trigger('gotoSlide', slide_num);
             }
             this.transition.transition.call(this, slide_num);
         },

         nextSlide: function() {
             this.gotoSlide(this.current_slide + this.config.step);
         },

         prevSlide: function() {
             this.gotoSlide(this.current_slide - this.config.step);
         }
     };

     XCarousel.transitions = {
         multi: {
             setup: function(children_config) {
                 var self = this;
                 this.children = $.map(children_config, function(config) {
                     var child = new XCarousel(config);
                     child.bind('gotoSlide', function(slide_num) {
                         $.each(self.children, function() {
                             if (this != child) this.gotoSlide(slide_num, true);
                         });
                         self.current_slide = slide_num;
                     });
                     return child;
                 });
             },
             update: function() {
                 var self = this;
                 $.each(this.children, function() {
                     this.update.apply(self);
                 });
             },
             transition: function(slide_num) {

             }
         },

         fade: {
             setup: function(config) {
                 this.holder.css('position', 'relative');
                 this.slides.css({position: 'absolute', top: 0, left: 0, display: 'none'})
                     .eq(this.current_slide).show();
                 if (this.config.resize_holder) {
                     this.holder.css({height: this.slides.eq(this.current_slide).outerHeight()});
                 }

             },
             remove: function() {
                 this.holder.removeAttr('style');
                 this.slides.removeAttr('style');
             },
             transition: function(slide_num) {
                 if (slide_num == this.current_slide) return;

                 // wrap around
                 slide_num = wrap_number(slide_num, this.slides.length);

                 var new_slide = this.slides.eq(slide_num);
                 if (this.config.resize_holder) {
                     this.holder.animate({height: new_slide.outerHeight()});
                 }
                 this.slides.eq(this.current_slide).fadeOut();
                 new_slide.fadeIn();
                 this.current_slide = slide_num;
             }
         },

         prev_next: {
             setup: function(config) {
                 this.holder.css('position', 'relative');
                 this.slides.css({position: 'absolute', top: 0, left: 0, display: 'none'});
                 this.slides.eq(wrap_number(this.current_slide-1, this.slides.length))
                     .css(this.prev.position())
                     .show();
                 this.slides.eq(wrap_number(this.current_slide+1, this.slides.length))
                     .css(this.next.position())
                     .show();
             },
             remove: function() {
                 this.holder.removeAttr('style');
                 this.slides.removeAttr('style');
             },
             transition: function(slide_num) {
                 this.slides.filter(':visible').fadeOut();
                 this.current_slide = slide_num;
                 this.slides.eq(wrap_number(this.current_slide-1, this.slides.length))
                     .css(this.prev.position())
                     .fadeIn();
                 this.slides.eq(wrap_number(this.current_slide+1, this.slides.length))
                     .css(this.next.position())
                     .fadeIn();
             }
         },

         slide: {
             setup: function(config) {
                 if (!this.config.axis) this.config.axis = 'horizontal';
                 this.holder.css('position', 'relative');

                 var total_width = 0, total_height = 0;
                 var self = this;
                 this.slides.show().each(function(i) {
                     var $this = $(this);
                     total_width += $this.outerWidth() + parseInt($this.css('margin-left'))
                         + parseInt($this.css('margin-right'));
                     total_height += $this.outerHeight() + parseInt($this.css('margin-top'))
                         + parseInt($this.css('margin-bottom'));

                     if (self.config.click_to_slide) {
                         $this.click(function(e) {
                             e.preventDefault();
                             var copy = $(this).closest('.xccopy');
                             var slide = i;
                             if (copy.length) {
                                 // if the copy is off to the left
                                 if (self.wrapper.children().index(copy) == 0) {
                                     slide = i - self.slides.length;
                                 }
                                 else {
                                     slide = i + self.slides.length;
                                 }
                             }
                             self.gotoSlide(slide);
                         });
                     }
                 });
                 if (this.config.wrapper) {
                     this.wrapper = $(this.config.wrapper);
                 }
                 else {
                     this.wrapper = $('<div>').appendTo(this.holder);
                     this.wrapper.append(this.slides);
                 }
                 this.slides.css('position', 'relative');
                 if (this.config.axis == 'horizontal') {
                     this.wrapper.css({
                         width: total_width,
                         position: 'absolute'
                     });
                 }
                 else {
                     this.wrapper.css({
                         height: total_height,
                         position: 'absolute'
                     });
                 }
                 if (this.config.infinite) {
                     this.total_width = total_width;
                     this.middle = Math.round(this.slides.length / 2);

                     this.wrapper_copy = $('<div class="xccopy"></div>')
                         .append(this.wrapper.children().clone(true));

                     this.wrapper.append(this.wrapper_copy).prepend(this.wrapper_copy.clone(true));
                     if (this.config.axis == 'horizontal') {
                         this.wrapper.width(this.wrapper.width() * 3);
                     }
                     else {
                         this.wrapper.height(this.wrapper.height() * 3);
                     }
                 }
                 var slide = this.slides.eq(this.current_slide);

                 var css = this.transition.calculate_offset.call(this, this.current_slide);
                 this.wrapper.css(css);

             },
             remove: function() {
                 this.holder.removeAttr('style');
                 if (!this.config.wrapper) {
                     this.holder.append(this.slides);
                     this.wrapper.remove();
                 }
             },

             calculate_offset: function(slide_num) {
                 var slide = this.slides.eq(slide_num);
                 var align = this.config.align;
                 if (this.config.axis == 'horizontal') {
                     var left;
                     if (align != 'right') {
                         if (!slide.length) left = 0;
                         else if (align == 'center') {
                             left = slide.position().left + slide.width()/2
                                 + parseInt(slide.css('marginLeft'))
                                 - this.holder.width()/2;
                         }
                         else {
                             left = slide.position().left - parseInt(slide.css('margin-left'));
                         }
                         var css = {'left': -left};
                     }
                     else {
                         var right;
                         if (!slide.length) right = 0;
                         else {
                             var left = slide.position().left + slide.width();
                             right = this.wrapper.width() - left;
                         }
                         var css = {'right': -right};
                     }
                 }
                 else {
                     if (align != 'bottom') {
                         var top;
                         if (!slide.length) top = 0;
                         else if (align == 'middle') {
                             top = slide.position().top + slide.height()/2
                                 + parseInt(slide.css('margin-top'))
                                 - this.holder.height()/2;
                         }
                         else {
                             top = slide.position().top - parseInt(slide.css('margin-top'));
                         }
                         var css = {'top': -top};
                     }
                     else {
                         var bottom;
                         if (!slide.length) bottom = 0;
                         else {
                             var top = slide.position().top + slide.height();
                             bottom = this.wrapper.height() - top;
                         }
                         var css = {'bottom': -bottom};
                     }
                 }
                 return css;
             },

             transition: function(slide_num) {
                 if (slide_num == this.current_slide) return;

                 var css;
                 // wrap around
                 if (slide_num >= this.slides.length) {
                     // about to wrap to the right
                     if (this.config.infinite) {
                         var current_left = parseInt(this.wrapper.css('left'));
                         this.wrapper.css('left', current_left + this.total_width);
                     }
                     slide_num %= this.slides.length;
                 }
                 else if (slide_num < 0) {
                     // about to wrap to the left
                     if (this.config.infinite) {
                         var current_left = parseInt(this.wrapper.css('left'));
                         this.wrapper.css('left', current_left - this.total_width);
                     }
                     while (slide_num < 0) slide_num += this.slides.length;
                 }

                 css = this.transition.calculate_offset.call(this, slide_num);
                 this.wrapper.animate(css);
                 this.current_slide = slide_num;
             }
         }
     };
})(jQuery);