(function($) {

  Cycler = function(options) {
    // Cycle v0.1, Copyright Colin Aarts <colinaarts.com>
    // Feel free to do anything you like with this, as long as this line and the above copyright message are left intact.
    
    // Based on jQuery 1.2.6 (not tested with other versions), but not really using jQuery plugin syntax. Call it with "var myCycle = new Cycle(options);"
    
    /* Public methods:
       void play()
       void pause()
       void fixDimensions() ~ see below for explanation
    */
    
    // Note: This is a fade-in/fade-out cycle with optional slide effect, it does not support a literal cycle effect at this time.
    
    // Note: when operating on variable width and/or height items, it is advisable to set a fixed width and/or height on the cycle container anyway. If you do not do this, elements around the cycle might shift around and the slide effect, if used, might not work as intended on line boxes within the cycle items. A public method, 'fixDimensions', is provided for this.
    
    // TODO: Re-think and re-work the pausedOverlay functionality. The way it works now, is fairly limiting.
    // TODO: Allow for more than one initial item to be present.
    
    // START
    
    // Creating a new instance of Cycle with a wrapper element that is already in use as a Cycle instance will lead to issues...
    if($(options.wrapper).add($(options.wrapper).parents()).hasClass('cycle-wrapper')) return false;
    $(options.wrapper).addClass('cycle-wrapper');
    
    // Options defaults.
    options.controls         = typeof options.controls         == 'undefined' ? true    : options.controls;         // bool
    options.controlsLoc      = typeof options.controlsLoc      == 'undefined' ? 'after' : options.controlsLoc;      // string [ before | after ]
    options.timer            = typeof options.timer            == 'undefined' ? 5000    : options.timer;            // int milliseconds
    options.setDefaultStyles = typeof options.setDefaultStyles == 'undefined' ? true    : options.setDefaultStyles; // bool
    options.pauseOnHover     = typeof options.pauseOnHover     == 'undefined' ? true    : options.pauseOnHover;     // bool
    options.pausedOverlay    = typeof options.pausedOverlay    == 'undefined' ? false   : options.pausedOverlay;    // bool // todo: implement this
    options.animation        = typeof options.animation        == 'undefined' ? 'fade'  : options.animation;        // string [ fade | left | right | top | bottom ]
    
    var containers = {
      cycle: $(options.wrapper).find('.cycle')
    };
    
    if(options.controls) {
      // Create and inject the container for the controls.
      var html = $('<div class="controls"></div>');
      switch(options.controlsLoc) {
        case 'before': $(containers.cycle).before(html); break;
        case 'after' : $(containers.cycle).after(html);  break;
      }
      containers.controls = html;
    }
    
    if(options.pausedOverlay) {
      var html = $('<div class="paused-overlay">paused</div>').appendTo(options.wrapper).css('opacity', '0');
      containers.pausedOverlay = html;
    }
    
    var items = {
      initial: containers.cycle.find('li:first'),
      adds   : options.items,
      all    : containers.cycle.find('li:first').add($(options.items)),
      current: 0
    };
    
    var timeout = null,
        paused  = 0; // 1 = soft paused, 2 = hard paused
    
    var styles = {
      visible: {
        opacity: 1,
        top: 0,
        left: 0
      },
      hidden: {
        opacity: 0,
        left: 0,
        top: 0,
        'z-index': 0 // IE threw a fit with zIndex
      }
    };
    
    switch(options.animation) {
      case 'left'  : styles.hidden.left    = '-100%'; break;
      case 'top'   : styles.hidden.top     = '-100%'; break;
      case 'right' : styles.hidden.left    =  '100%'; break;
      case 'bottom': styles.hidden.top     =  '100%'; break;
    }
    
    var setDefaultStyles = function() {
      // Sometimes it's desirable to not have to deal with the absolute positioning setup required for the cycle when JS isn't available.
      // This function will add the necessary styling.
      
      // Wrapper styles.
      if(options.pausedOverlay) {
        $(options.wrapper).css({
          position: 'relative'
        });
      }
      // Container styles.
      containers.cycle.css({
        position: 'relative',
        overflow: 'hidden'
      });
      // Cycle item styles.
      $(items.all).css({
        position: 'absolute',
        top: 0,
        left: 0
      });
      // Initially present cycle item styles.
      $(items.initial).css({
        zIndex: 1
      });
    }
    
    var addItems = function() {
      // This function injects the additional cycle items, as passed through the options object, into the cycle.
      // Note that at this time, it is not possible to inject more items after the cycle has initialized.
      // Also note that no sanity checking of passed items is done.
      $(items.adds).each(function(i, el) {
        $(el).css(styles.hidden).appendTo(containers.cycle);
      });
      // Update items.all to reflect the new DOM Node status of the adds.
      items.all = containers.cycle.find('li');
    };
    
    var paint = function() {
      items.current = (items.current + 1) < containers.cycle.children().length ? items.current + 1 : 0;
      containers.cycle.children(':visible').animate(styles.hidden).css('zIndex', 0);
      containers.cycle.children().eq(items.current).animate(styles.visible).css('zIndex', 1);
      
      // set active class on controls
      if(options.controls) {
        $(containers.controls).find('ol li').removeClass('active');
        $(containers.controls).find('ol li').eq(items.current).addClass('active');
      }
    }
    
    var play = this.play = function() {
      // This function starts (or resumes) the cycle.
      // If the cycle has been paused — either by triggering the pause control or by pauseOnHover — this will cause the cycle to auto-resume, as opposed to starting from the first item.
      
      clearInterval(timeout);
      paused = 0;
      if(options.controls) {
        containers.controls.find('.pause').removeClass('active');
      }
      if(options.pausedOverlay) {
        containers.pausedOverlay.animate({ opacity: 0 });
      }
      
      timeout = setInterval(function() {
        paint();
      }, options.timer);
      
    };
    
    var advance = this.advance = function() {
      // This function advances the cycle to the next item only
      paint();
    }
    
    var pause = this.pause = function(type) {
      // This function, quite obviously, pauses the cycle. It does this by simply clearing the interval that keeps the cycle going.
      clearInterval(timeout);
      paused = typeof type == 'undefined' ? 2 : type; // default to 2
      
      if(options.controls)  {
        containers.controls.find('.pause').addClass('active');
      }
      
      if(options.pausedOverlay) {
        containers.pausedOverlay.animate({ opacity: .7 });
      }
    };
    
    this.fixDimensions = function() {
      var width = $(containers.cycle).width()
      var height = 0;
      items.all.each(function(i, el) {
        var h = $(el).height();
        height = h > height ? h : height;
      });
      items.all.add(containers.cycle).css({
        height: height,
        width: width
      });
    }
    
    var init = function() {
      
      // Bind events to pause the cycle on hover.
      if(options.pauseOnHover) {
        containers.cycle.bind('mouseenter mouseleave', function(e) {
          if(paused == 1) play();
          else if(paused == 0) pause(1);
        });
      }
      
      // Build the HTML for the controls for the cycle.
      if(options.controls) {
        var html = '';
        html += '<ol>'
        $(items.all).each(function(i, el) {
          html += '<li title="' + (i+1) + ' of ' + items.all.length + '">' + (i+1) + '</li>';
        });
        html += '</ol><span class="pause" title="Pause">Pause</span>';
        
        // Inject controls.
        $(html).appendTo(containers.controls);
        
        // Set initial active item in controls.
        $(containers.controls).find('ol li:first').addClass('active');
        
        // Attach event for the pause/play control.
        $(containers.controls).find('.pause').bind('click', function(e) {
          if(paused) play();
          else pause(2);
        });
        
        // Attach events for the items list.
        $(containers.controls).find('ol li').click(function(e) {
          items.current = $(this).parent().children().index($(this)) - 1;
          advance();
          if(!paused) play();
        });
      } // end if(options.controls)
      
      // Add additional items to the cycle, as passed via options.
      addItems();
      
      // Apply default styles if set in options.
      if(options.setDefaultStyles) setDefaultStyles();
      
      // Start the cycle.
      play();
      
    }; // end init()
    
    // Call init to get things started.
    init();
    
  };
  
})(jQuery);
