(function() {
  var getViewPortSize
    , isNum
    , toPixels
    , Carousel
    ;

  /* get size of viewport in pixels */
  getViewPortSize = function() {
    // the more standards compliant browsers (mozilla/netscape/opera/IE7) use window.innerWidth and window.innerHeight
    if (typeof window.innerWidth != 'undefined') {
      return {width: window.innerWidth, height: window.innerHeight};
    }
    // IE6 in standards compliant mode (i.e. with a valid doctype as the first line in the document)
    else if (typeof document.documentElement != 'undefined'
        && typeof document.documentElement.clientWidth !=
        'undefined' && document.documentElement.clientWidth != 0) {
      return {width: document.documentElement.clientWidth, height: document.documentElement.clientHeight};
    } else { // older versions of IE
      return {width: document.getElementsByTagName('body')[0].clientWidth, height: document.getElementsByTagName('body')[0].clientHeight};
    }
  };

  /* returns true if n is a number, false if it's a string, object, or something else */
  isNum = function(n) {
    return !isNaN(parseFloat(n)) && isFinite(n);
  };

  /* casts string in the form XX.XXpx to an integer. Truncates decimals */
  toPixels = function(styleString) {
    return parseInt((/(-?[0-9]+).?[0-9]*px/i).exec(styleString),10);
  };

  /* Functional constructor for a roundrobin one-at-a-time horizontal carousel,
   * uses closure to encapsulate internals, returning module-pattern style public API
   */
  Carousel = function(containerEl, itemEls, previousEl, nextEl, config) {
    // Private Functions
    var endpointsX
      , positionAllItems
      , hideElements
      , getShownIndices
      , setPoints
      , setStep
      , isWithin
      , centerFirst
      , animate
      , advanceRoundrobin
      , animateRoundrobin
      , previousRoundrobin
      , nextRoundrobin
      , setupRoundrobin
    // Private Members
      , intervalId
      , isForward
      , fps = 15
      , stepX
    ;

    centerFirst = function() {
      itemEls[0].style.left = ((document.viewport.getWidth()/2) - (itemEls[0].clientWidth/2)) + "px";
    };

    endpointsX = function(isBound, isForward, itemEls) {
      var firstWidth = itemEls[0].clientWidth
        , secondWidth = itemEls[1].clientWidth
        , viewportWidth = getViewPortSize().width
      ;
      return (isBound && isForward) ?  {startX: firstWidth,                                   endX: 0}:
             (isBound && !isForward) ? {startX: -firstWidth,                                  endX: 0}:
             (isForward) ?             {startX: viewportWidth/2 + firstWidth/2,               endX: viewportWidth/2 - secondWidth/2}:
                                       {startX: viewportWidth/2 - secondWidth/2 - firstWidth, endX: viewportWidth/2 - firstWidth/2};
    };

    positionAllItems = function(itemEls, leftPos) {
      var itemIdx = 0;
      for(itemIdx; itemIdx < itemEls.length; itemIdx++) {
        itemEls[itemIdx].style.left = leftPos + "px";
        leftPos += itemEls[itemIdx].clientWidth;
      }
    };

    showHideElements = function(itemEls, showHides) {
      var itemIdx = 0
        , shownIdx = 0
      ;
      outer: for(; itemIdx < itemEls.length; itemIdx++) {
        for(; shownIdx < showHides.shown.length; shownIdx++) {
          if(parseInt(itemIdx, 10) === parseInt(showHides.shown[shownIdx], 10)) {
            itemEls[itemIdx].show();
            continue outer;
          }
        }
        itemEls[itemIdx].hide();
      }
    };

    setPoints = function(points) {
      pointsX = points;
      return points;
    };

    getShownIndices = function(numShown) {
      var i = 0
        , shown = [];
      for(; i < numShown; i++) {
        shown.push(i);
      }
      return shown;
    };

    /* Calculate how far the items should move with each step of the animation
     *
     * @post stepX variable will be populated with correct value for this carouel
     */
    setStep = function() {
      var diffX = pointsX.endX - pointsX.startX;
      stepX = Math.ceil(Math.abs(diffX)/((config.duration*1000)/(1000/fps)));
      stepX = (diffX > 0)? stepX: -stepX;
      return stepX;
    };

    isWithin = function(item, bounds) {
      return (!("left" in bounds) || (("left" in bounds) && isNum(bounds.left) && (toPixels(item.style.left) >= bounds.left)))
             &&
             (!("right" in bounds) || (("right" in bounds) && isNum(bounds.right) && (toPixels(item.style.left) + item.clientWidth <= bounds.right)));
    };

    animate = function(fn, fps) {
        intervalId = setInterval(fn, 500/fps);
    };

    /* One frame of the animation */
    advanceRoundrobin = function() {
     	var itemIdx = 0;
		var bannerSpace = 0;

		//Adjust position for home page banner size.
		if (("unbound" in config) && (config.unbound == true)) {
			(isForward == false) ? bannerSpace = -90 : bannerSpace = 90;
		}

      	// Position all items one more step along
		for(; itemIdx < itemEls.length; itemIdx++) {
			itemEls[itemIdx].style.left = (toPixels(itemEls[itemIdx].style.left) + stepX) + bannerSpace + "px";
		}


		if(((isForward === false) && isWithin(itemEls[0], {left: pointsX.endX})) ||
			((isForward === true)  && isWithin(itemEls[0], {right: pointsX.endX}))) {
			clearInterval(intervalId);
		intervalId = null;
		if(isForward === true) {
		  	// Move front to end
		  	itemEls.push(itemEls.shift());
		  	itemEls[itemEls.length - 1].style.left = (toPixels(itemEls[itemEls.length - 2].style.left) + itemEls[itemEls.length - 1].clientWidth) + "px";
		}
		showHideElements(itemEls, {shown: getShownIndices(config.numShown)});
		}
    };

    /* Setup the animation */
    animateRoundrobin = function() {
      setStep(setPoints(endpointsX(!config.unbound, isForward, itemEls)));
      animate(advanceRoundrobin, fps);
    };

    /* Rotates the items by adding the end item to the front.
     *
     * Positions of the elements on the page are updated via style.left, and the correct order of the items is stored in mutable itemEls array
     * Adjusts display properties to show/hide as needed
     * Prevents clicking next/prev while animation is proceeding with locking variable
     *
     * Starts animation
     */
    previousRoundrobin = function() {
      if(!intervalId) {
        isForward = false;
        itemEls[itemEls.length - 1].show();
        // Move end to front
        itemEls.unshift(itemEls.pop());
        itemEls[0].style.left = (toPixels(itemEls[1].style.left) - itemEls[0].clientWidth) + "px";
        animateRoundrobin();
      }
    };

    /* Rotates the items by adding the front item to the end.
     *
     * Positions of the elements on the page are updated via style.left, and the correct order of the items is stored in mutable itemEls array
     * Adjusts display properties to show/hide as needed
     * Prevents clicking next/prev while animation is proceeding with locking variable
     *
     * Starts animation
     */
    nextRoundrobin = function() {
      if(!intervalId) {
        isForward = true;
        itemEls[1].show();
        animateRoundrobin();
      }
    };

    /* Setup the default carousel configuration - animates the transition one-item-at-a-time when previous or next are clicked.
     * - Hides next and previous buttons if there are one or fewer items to display
     *                                   or if the items don't fill the width of the container
     * - Attaches handlers to next and previous elements
     * - Positions all the items so they sit flush against each other,
     *   handles screen resize when elements are unbound
     * - Hides all items that are not within the container bounds
     *                            or are beyond the parameterized amount
     * - Halts execution for click next or previous to resume
     * - Sets timer to auto slide on load of carousel
     * - Auto slide stops after arrow is clicked
     */
    setupRoundrobin = function() {
		var lastItem = itemEls[itemEls.length - 1];
		var autoSlide = 0;
		if(previousEl) {
			previousEl.observe("click", previousRoundrobin);
			previousEl.observe("click", function() {
				clearInterval ( autoSlide );
			});
		}
      	(nextEl || containerEl).observe("click", nextRoundrobin);
	 	(nextEl || containerEl).observe("click", function() {
			clearInterval ( autoSlide );
		});

	  	autoSlide = setInterval ( nextRoundrobin, 10000 );

		if(("unbound" in config) && (config.unbound === true)) {
			$$("html").first()
			    .addClassName("home");
			$$("body").first()
			    .addClassName("home")
			    .insert(Element.remove(containerEl.down("ul")));
			Event.observe(document.onresize ? document : window, "resize", centerFirst);
			centerFirst();
			positionAllItems(itemEls, getViewPortSize().width/2 - itemEls[0].clientWidth/2);
			// Hide next/prev arrows if we don't need them`
			if(!itemEls || itemEls.length <= 1) {
			  nextEl.hide();
			  previousEl.hide();
			}
		} else {
			positionAllItems(itemEls, 0);
			// Hide next/prev arrows if we don't need them`
			if((toPixels(lastItem.style.left) + lastItem.clientWidth) < containerEl.clientWidth) {
			  nextEl.hide();
			  previousEl.hide();
			}
		}

      	showHideElements(itemEls, {shown: getShownIndices(config.numShown)});
    };

    // Call the constructor
    setupRoundrobin();

    /* PUBLIC API */
    return { previous: previousRoundrobin
           , next:     nextRoundrobin
           };
  };

  /* Example of parasitic functional inheritance
  RoundrobinCarousel = function(extend, els, config) {
    var extended, setup, next, previous;

    next = function() {

    };

    previous = function() {

    };

    setup = function() {
      extended = extend();
    };

    // Call the constructor
    setup();

    return { previous: previous
           , next:     next
           };
  }
  */

  /* Builds different kinds of carousel based on the parameters. Currently all of this logic is within Carousel.*/
  var CarouselBuilder = function() {
    var container, previousEl, nextEl, itemEls, config;
    return { container:   function(el)  { containerEl = el;  return this; }
           , navPrevious: function(el)  { previousEl  = el;  return this; }
           , navNext:     function(el)  { nextEl      = el;  return this; }
           , items:       function(els) { itemEls     = els; return this; }
           , build:       function(config) {
                            /* inject inheritance relationship
                            var els = {containerEl: containerEl, itemEls: itemEls, previousEl: previousEl, nextEl: nextEl};
                            return RoundrobinCarousel(Carousel, els, config);
                            */
                            return Carousel(containerEl, itemEls, previousEl, nextEl, config);
                          }
           };
  };

  // Carousel Initializer
  /* Scrapes the page looking for elements with class="carousel ..."
   * When found, it looks for additional classnames secifying parameters for the carousel.
   * All parameters are optional. Defaults are specified below.
   *   Parameter Syntax                 Description                                                 Default
   *   carousel-transition-XX.XXs       Spends XX.XX seconds animating next and previous commands   2 seconds
   *   carousel-show-XX                 Only show XX items at a time                                1
   *   carousel-unbound                 Removes the carousel UL or OL from the containing element
   *                                    and appends it as an immediate child of body.               false
   *
   * The carousel is driven by next and previous buttons, both can be any kinds of element, place anywhere
   * within the container, and must contain class="carousel-prevous" or class="carousel-next"
   *
   * The items of the carousel may be any kind of block element, and must contain class="carousel-item"
   *
   * All items in the carousel must be the same width
   */
  document.observe("dom:loaded", function() {
    // Activate all the carousels on the page
    $$('.carousel').each(function(item) {
      // Build the carousel
      CarouselBuilder()
          .container(item)
          .navPrevious(item.down('.carousel-previous'))
          .navNext(item.down('.carousel-next'))
          .items(item.select('.carousel-item'))
          // Get the configuration parameters from the classnames
          .build(item.classNames().inject({}, function(config, value) {
                   /* Scrape:  from className (if matches)                                                             existing           default*/
            return { duration: parseFloat(((/carousel-transition-([0-9]+.?[0-9]*)s/i).exec(value) || [0, null])[1]) || config.duration || 2
                   , numShown: parseInt(((/carousel-show-([0-9]+)/i).exec(value) || [0, null])[1], 10)              || config.numShown || 1
                   , unbound:  (/carousel-unbound/i).test(value)                                                    || config.unbound  || false
                   };
          }));
    });
  });
})();
