/**
 * Pageable: generic behaviour for things with pagination
 * @author Josh Johnston josh@xhtmlized.com
 * @namespace
 */
var Pageable = function(){
	/*global	*/

	var self = /** @scope Pageable */{
		/**
		 * Create an instance
		 * @param {hash} settings
		 */
		create: function(settings) {
			var instance = settings;

			// set default callbacks
			instance.callbacks.createTween = instance.callbacks.createTween || self.createTween;
			instance.callbacks.createPageMap = instance.callbacks.createPageMap || self.createPageMap;
			instance.callbacks.createNavigation = instance.callbacks.createNavigation || self.createNavigation;

			// set the current page to default
			instance.current_page = 0;

			// get item widths
			instance.item_widths = self.getItemWidths(instance);

			// group items into pages
			instance.page_map = instance.callbacks.createPageMap(instance);

			// more than 1 page? setup behaviour
			if (instance.page_map.length > 0) {
				self.setup(instance);
			}

			return instance;
		},

		/**
		 * Setup pageable behaviour
		 * @param {hash} instance
		 */
		setup: function(instance) {
			// position items
			self.positionItems(instance);

			// make the holder big enough to fit all pages
			var viewportWidth = instance.config.viewport_width;
			var totalWidth = instance.page_map.length * viewportWidth;
			instance.itemholderElm.setStyle({width: totalWidth+'px'});

			// enter state: "has-pages"
			if (instance.page_map.length > 1)
			{
				instance.baseElm.addClassName('has-pages');
			}

			// show only items of the current page
			self.showSinglePage(instance, instance.current_page);

			// create and update navigation
			if (instance.navListElm) {
				instance.callbacks.createNavigation(instance);
			}
		},

		/**
		 * Create a map of items to pages
		 * @param {hash} instance
		 * @return {array}
		 */
		createPageMap: function(instance) {
			var viewportWidth = instance.config.viewport_width;
			var spaceBetweenItems = instance.config.space_between_items;
			var items = instance.itemholderElm.childElements();
			var itemWidths = instance.item_widths;
			var pageWidth = 0;
			var pageMap = [[]];
			var pageCounter = 0;
			var width;
			for (var i=0,len=items.length; i<len; i++) {
				// calculate the item's width and spacing
				width = itemWidths[i] + spaceBetweenItems;

				// will this item fit on the current page?
				if (pageWidth + width <= viewportWidth) {
					// keep track of page width
					pageWidth += width;
				}
				// start a new page
				else {
					pageWidth = width;
					pageCounter ++;
					pageMap[pageCounter] = [];
				}

				// add item to the page map
				pageMap[pageCounter].push(i);
			}

			return pageMap;
		},

		/**
		 * Get an array of item widths
		 * @param {hash} instance
		 */
		getItemWidths: function(instance) {
			var items = instance.itemholderElm.childElements();
			var itemWidths = [];
			for (var i=0, len=items.length; i<len; i++) {
				itemWidths[i] = items[i].getDimensions().width;
			}
			return itemWidths;
		},

		/**
		 * Position items in a horizontal row
		 * @param {hash} instance
		 */
		positionItems: function(instance) {
			var items = instance.itemholderElm.childElements();
			var itemWidths = instance.item_widths;
			var viewportWidth = instance.config.viewport_width;
			var spaceBetweenItems = instance.config.space_between_items;
			var pageMap = instance.page_map;

			var pageCounter, itemCounter;
			var numPages = pageMap.length;
			var numItems;
			var itemLeft;
			var currentLeft = 0;
			var i=0;
			for (pageCounter=0; pageCounter<numPages; pageCounter++) {
				for (itemCounter=0, numItems = pageMap[pageCounter].length; itemCounter<numItems; itemCounter++) {
					// first item in the page
					if (itemCounter == 0) {
						// position the item at the start of the new page
						currentLeft = viewportWidth * pageCounter;
					}

					// position the item
					items[i].setStyle({left: currentLeft+'px'});

					// move to the next position
					currentLeft += itemWidths[i] + spaceBetweenItems;

					// update the item index
					i++;
				}
			}
		},

		/**
		 * Show the items of a single page
		 * @param {hash} instance
		 * @param {int} pageIndex
		 */
		showSinglePage: function(instance, pageIndex) {
			var numItems = instance.page_map[pageIndex].length;
			var startIndex = instance.page_map[pageIndex][0];
			var endIndex = instance.page_map[pageIndex][numItems-1];
			self.showItemsInRange(instance, startIndex, endIndex);
		},

		/**
		 * Show items within a certain range
		 * @param {hash} instance
		 * @param {index} startIndex
		 * @param {index} endIndex
		 */
		showItemsInRange: function(instance, startIndex, endIndex) {
			var holderElm = instance.itemholderElm;
			var isAnimating = instance.baseElm.hasClassName('animating');
			holderElm.childElements().each(function(elm, index) {
												 if (index >= startIndex && index <= endIndex) {
													 elm.setStyle({visibility: 'visible'});
												 }
												 // if animation is in progress, don't hide anything
												 else if (!isAnimating) {
													 elm.setStyle({visibility: 'hidden'});
												 }
											 });
		},

    /**
		 * Go to the page containing the given element
		 * @param {hash} instance
		 * @param {DOMElement} elm
		 */
    gotoElementPage: function(instance, elm) {
      var page = 0;
      var pages = instance.page_map;
      var index = instance.itemholderElm.childElements().indexOf(elm);
      var page_length;
      while (page < pages.length && index > (page_length = pages[page].length))
      {
        index -= page_length;
        page ++;
      }
      if (page >= pages.length) return;

      // Move to this page without animation
			instance.itemholderElm.style.left = -(instance.config.viewport_width * page) + 'px';
      instance.current_page = page;
      self.showSinglePage(instance, page);
    },

		/**
		 * Set the current page
		 * @param {hash} instance
		 * @param {int} pageIndex
		 */
		setCurrentPage: function(instance, pageIndex) {
			// no change? do nothing
			if (pageIndex == instance.current_page) {
				return;
			}

			self.animatePageChange(instance, instance.current_page, pageIndex);
			instance.current_page = pageIndex;

			if (instance.navListElm) {
				instance.callbacks.updateNavigation(instance);
			}
		},

		/**
		 * Wrap a page index so it's always in legal bounds
		 * @param {hash} instance
		 * @param {int} pageIndex
		 * @return {int}
		 */
		wrapPageIndex: function(instance, pageIndex) {
			var numPages = instance.page_map.length;
			if (pageIndex < 0) {
				return self.wrapPageIndex(instance, pageIndex+numPages);
			}
			else if (pageIndex >= numPages) {
				return self.wrapPageIndex(instance, pageIndex-numPages);
			}
			else {
				return pageIndex;
			}
		},

		/**
		 * Create the navigation
		 * @param {hash} instance
		 */
		createNavigation: function(instance) {
			var elms = instance.navListElm.select('li');
			var numPages = instance.page_map.length;
			var i;

			// use the first element as a template
			var fragment = document.createDocumentFragment();
			var tpl = elms[0].innerHTML;
			var newElm;

			// delete some items
			if (elms.length > numPages) {
				// delete redundant item
				for (i=numPages; i<elms.length; i++) {
					elms[i].remove();
				}
			}
			// create some items
			else {
				for (i=elms.length; i<numPages; i++) {
					newElm = document.createElement('li');
					newElm.innerHTML = tpl.replace(/(href=\".*?\#)(\d+)(\")/, '$1'+(i+1)+'$3');
					fragment.appendChild(newElm);
				}
				elms[0].parentNode.appendChild(fragment);
			}

			// Set up other navigation callbacks
			instance.callbacks.updateNavigation = self.updateNavigation;
			if (instance.navListElm) {
				self.updateNavigation(instance);
				// setup click behaviour
				instance.navListElm.observe('click', function(event) { self.handleNavClick(event, instance); });
			}
		},

		/**
		 * Update the navigation
		 * @param {hash} instance
		 */
		updateNavigation: function(instance) {
			var elms = instance.navListElm.select('li');
			elms.each(function(elm) {
							elm.removeClassName('active');
						});
			elms[instance.current_page].addClassName('active');
		},

		/**
		 * Handle a click on a navigation button
		 * @param {DOMevent} event
		 * @param {hash} instance
		 */
		handleNavClick: function(event, instance) {
			var elm = event.element();

			// work out which nav button was clicked
			if (elm.nodeName != 'LI') {
				elm = elm.up('li');
			}

			if (!elm) {
				return;
			}

			// stop the click
			event.stop();
			var pageIndex = instance.navListElm.select('li').indexOf(elm);
			self.setCurrentPage(instance, pageIndex);
		},

		/**
		 * Create arrow navigation
		 * @param {hash} instance
		 */
		createArrowNavigation: function(instance) {
			// Set up other arrow navigation callbacks
			instance.callbacks.updateNavigation = self.updateArrowNavigation;
			if (instance.navListElm) {
				self.updateArrowNavigation(instance);
				// setup click behaviour
				instance.navListElm.observe('click', function(event) { self.handleArrowNavClick(event, instance); });
			}
		},

		/**
		 * Update arrow navigation
		 * @param {hash} instance
		 */
		updateArrowNavigation: function(instance) {
		},

		/**
		 * Handle a click on an arrow navigation button
		 * @param {DOMevent} event
		 * @param {hash} instance
		 */
		handleArrowNavClick: function(event, instance) {
			var elm = event.element();

			// work out which nav button was clicked
			if (elm.nodeName != 'LI') {
				elm = elm.up('li');
			}
			elm = elm.select('a')[0];

			if (!elm) {
				return;
			}

			// stop the click
			event.stop();

			var delta = elm.hasClassName('prev') ? -1 : 1;
			self.setCurrentPage(instance, self.wrapPageIndex(instance, instance.current_page + delta));
		},

		/**
		 * Get the index of the first item in the page
		 * @param {hash} instance
		 * @param {int} pageIndex
		 */
		getFirstItemIndex: function(instance, pageIndex) {
			return instance.page_map[pageIndex][0];
		},

		/**
		 * Get the index of the last item in the page
		 * @param {hash} instance
		 * @param {int} pageIndex
		 */
		getLastItemIndex: function(instance, pageIndex) {
			var numItems = instance.page_map[pageIndex].length;
			return instance.page_map[pageIndex][numItems-1];
		},

		/**
		 * Animate the change in pages
		 * @param {hash} instance
		 * @param {int} currentPage Current page index
		 * @param {int} newPage New page index
		 */
		animatePageChange: function(instance, currentPage, newPage) {
			var duration = instance.config.tween_duration;
			var itemDelay = instance.config.tween_item_delay;

			// already animating? queue up the change
			if (instance.is_animating) {
				instance.queued_change = {
					from_page: currentPage,
					to_page: newPage
				};
				return;
			}

			// enter animating state
			instance.is_animating = true;

			self.transitionPageOut(instance, currentPage, duration, itemDelay);

			var outTotalDelay = duration + instance.page_map[currentPage].length * itemDelay;
			var inTotalDelay = duration + instance.page_map[currentPage].length * itemDelay;
			window.setTimeout(function(){
									self.showSinglePage(instance, newPage);
									self.transitionPageIn(instance, newPage, duration, itemDelay);
								}, outTotalDelay*1000);

			// no longer animating
			window.setTimeout(function() {
									var queuedChange = instance.queued_change;

									// exit animating state
									instance.is_animating = false;

									// is there a queued change? run and clear it
									if (queuedChange) {
										instance.queued_change = null;
										self.animatePageChange(instance, queuedChange.from_page, queuedChange.to_page);
									}
								}, (outTotalDelay + inTotalDelay) * 1000);
		},

		transitionPageOut: function(instance, pageIndex, duration, delay) {
			var items = instance.itemholderElm.childElements();
			var currentPageMap = instance.page_map[pageIndex];
			var startWidth;
			var tween;
			var itemIndex;
			currentPageMap.each(function(itemIndex, i) {
									window.setTimeout(function() {
															startWidth = instance.item_widths[itemIndex];
															tween = new Tween(items[itemIndex].style, 'width', Tween.regularEaseInOut, startWidth, 0, duration, 'px');
															tween.start();
														}, delay*1000*i);
								});
		},

		transitionPageIn: function(instance, pageIndex, duration, delay) {
			var items = instance.itemholderElm.childElements();
			var currentPageMap = instance.page_map[pageIndex];
			var itemIndex;
			var endWidth;
			var tween;
			var numItems = currentPageMap.length;

			// set all items to have 0 width
			for (var i=0; i<numItems; i++) {
				items[currentPageMap[i]].style.width = '0px';
			}

			// reposition the holder
			instance.itemholderElm.style.left = -(instance.config.viewport_width * pageIndex) + 'px';

			// start the tween
			currentPageMap.each(function(itemIndex, i) {
									window.setTimeout(function() {
															endWidth = instance.item_widths[itemIndex];
															tween = new Tween(items[itemIndex].style, 'width', Tween.regularEaseInOut, 0, endWidth, duration, 'px');
															tween.start();
														}, delay*1000*i);
								});
		}
	};

	return self;
}();
