/*
* A basic carousel like slider, responsive out of the box
* depends on jQuery, may work with zepto in the future.
*
* v 0.1 (alpha)
* Usage:
*
*	var myCarousel = new arbitraryCarousel();
*	myCarousel.init('mySelector');
*
* Options
* ===========
*	modulus: number of rows.
*	$firstVisisbleItem : a jQuery object containing the first visible object, it's assumed to be the first
*	pagination: Do you want me to add pagination? Defaults to true.
*	threshold: how much of an image needs to be inside the viewport for it to register in the pagination ( expects decimal, ie. 0.2) defaults to 1
*
* Methods
* ===========
*	init() : initializes the slider
*
*	moveSliderToElement($element) : moves the slider so that spedified element is in the left hand corner. Expects a jQuery object
*
*	moveSlider(direction) : Moves the slider one viewport width. Requires either "forward" or "backward"
*
* Callbacks:
* ===========
*
*	onMoveStart : runs before any movement
*	onMoveEnd : runs at the end of movement animations
*
*
*
*/
(function (window, $) {
	var ua = navigator.userAgent.toLowerCase();
	isWebkit = !!ua.match(/applewebkit/i);
	var supportsTouch = false;
	try {
		document.createEvent("TouchEvent");
		supportsTouch = true;
	} catch (e) {}

	var ac = function () {},
		arbitraryCarousel = function () { // this is all here for touch events down the line, you'll see
			return new ac();
		};
	ac.prototype.init = function (target, options) {


		this.settings = $.extend({
			columns : [], // assuming we're moving columns, this will be filled with the columns, columns need to be of equal width at the moment.
			colCount : 0,
			targetSelector : null, // the outer wrapper of the slider.
			viewportWidth : null,
			startingSlide : null, // move to slide number
			nextSelector : '.next',
			prevSelector : '.prev',

			//slideControls
			pagination : true, // do you want me to add pagination?
			slideControlsContainerSelector : null, //where do you want the controls to go? if no selector is given the controls will be appended after the slider

			callbacks : {}, // any callbacks can go in here
			itemSelector : null, // the selector to grab individual elements in the carousel
			slider : null, // this is the bit that moves. We expect it to be the first child unless ovewritten, must have an ID
			modulus : null, // in the case of sliding columns of a given selector, we'll only select the nth item, must be an integer
			$firstVisisbleItem : null, // The first visible item in the carousel
			viewPortLeft : 0, // the left hand side of the viewport, assumning we're at the begining
			firstVisibleCol : 0, // assuming we're at the begining // this should be redundant if we're given $firstVisisbleItem
			threshold : 1 // the percentage of an element to be within the viewport for it to register in the pagination.
		}, options);

		this.settings.slider = $(target).children().eq(0);
		this.settings.targetSelector = target;

		// if we weren't initialized with a selector, assume we're at the begining of the slider
		this.settings.$firstVisisbleItem = this.settings.$firstVisisbleItem || $(this.settings.targetSelector).find(this.settings.itemSelector).eq(0);

		// how many columns are there?
		this.settings.colCount = this.settings.modulus ? Math.ceil($(target).find(this.settings.itemSelector).length / this.settings.modulus)
														: $(target).find(this.settings.itemSelector).length;

		// help keep track of what this is that, or was it that is this. Wait. Um.
		var that = this,
			dStyle = document.body.style,
			position; // used in column calculations

		this.settings.tStyle = (dStyle.webkitTransition !== undefined)  ? "-webkit-transition":
				(dStyle.mozTransition !== undefined)  ? "-moz-transition":
				(dStyle.msTransition !== undefined)  ? "-ms-transition":
				(dStyle.oTransition !== undefined)  ? "-o-transition":
				(dStyle.transition !== undefined)  ? "transition":
											null; // nothing doing? then we'll use jQuery animation
		if (this.settings.tStyle) {
			tStyle = this.settings.tStyle;
			that.settings.slider.css(tStyle, "margin 800ms ease-in-out");
		}
		// Asses viewportWidth
		this.updateViewportWidth();
		$(window).resize(function () { // consider using Ben Alman's viewport throttling for older browsers.
			that.updateViewportWidth();
			that.findTrailingElements();
			that.updatePagination();
		});
		$(this.settings.nextSelector).live('click', function (e) {
			that.moveSlider("forward");
			e.preventDefault();
			return false;

		});
		$(this.settings.prevSelector).live('click', function (e) {
			that.moveSlider("backward");
			e.preventDefault();
			return false;
		});

		this.settings = $.extend(this.settings, that.settings);

		// wait till all images have a width before calculating their positions.
		this.settings.slider.imagesLoaded( function() {
			setTimeout(function () {that.parseElements.apply(that)}, that.settings.slider.find('img').length * 100);
		});

	};

	ac.prototype.parseElements = function () {

		var that = this,
			target = that.settings.targetSelector;
		
		that.settings.columnLeftValues = []; // an array of left values used to determine what column an item is in we're using this later;;

		$(target).find(that.settings.itemSelector).each(function (i) {
			var eachThat = this;
			position = $(eachThat).position();

			// setup the initial position
			$(eachThat)
				.css({
					"left" : position.left,
					"top" : position.top
				});

			whichCol = jQuery.inArray(position.left, that.settings.columnLeftValues); // columnLeftValues.indexOf(position.left); // indexOf only supported in IE9, still supporting IE7/8
			if (whichCol < 0) { // we have a new column if this left position has not been found before
				that.settings.columnLeftValues.push(position.left);
				whichCol = that.settings.columnLeftValues.length - 1; // remember arrays are zero indexed.
			}

			// while we're iterating, let's put everything into columns
			if (typeof(that.settings.columns[whichCol]) == "undefined") {

				that.settings.columns[whichCol] = [$(eachThat)];

			} else {
				that.settings.columns[whichCol].push($(eachThat));
			}

		});
		if (that.settings.modulus > 1) {
			$(target).find(that.settings.itemSelector).css("position", "absolute"); // need to apply absolute outside the each.
		}
		// create pagination
		this.createPagination();

		this.settings = $.extend(this.settings, that.settings);

		// option to move to specific slide on initialization, maybe useful for hashtags/history state stuff.
		if (!isNaN(this.settings.startingSlide && this.settings.startingSlide)) {
			this.moveSliderToElement(this.settings.startingSlide);
		}

	};

	ac.prototype.createPagination = function () {
		var that = this,
			$page = null,
			$li = null,
			i = 0,
			$slides = that.settings.slider.find('li.slide'),
			classes = "slider-control";

		this.$pagination = $('<ul />', {
			'class' : 'slidecontrols'
		});
		
		while (i < this.settings.colCount) {
			if ($slides.eq(i).hasClass('textSlide')) {
				classes = "slider-control text";
			} else {
				classes = "slider-control";
			}
			$li  =  $('<li />', {'class': classes});
			$page = $('<a />', {
				href : '#slide' + i, /// maybe use that.settings.targetSelector, // point the link to the slider
				click : function (e) { // jsHint screams at this. But what are ya gonna do?
					that.moveSliderToElement(parseInt($(this).html(), 10));
					that.findTrailingElements();
					that.updatePagination();
					e.preventDefault(); // stop it from going anywhere
				},
				role : "tab",
				tabindex : "-1",
				"aria-selected" : "false",
				text : i
			});
			$page.appendTo($li);
			$li.appendTo(this.$pagination);

			i += 1;
		}
		if (this.settings.slideControlsContainerSelector) {
			this.$pagination.appendTo($(this.settings.slideControlsContainerSelector));
		} else {
			this.$pagination.insertAfter($(this.settings.targetSelector));
		}

		// figure out where we are
		that.findTrailingElements();

	};

	ac.prototype.updatePagination = function () {
		var $liElements = this.$pagination.find('li'),
			that = this;

		$liElements
			// this is everything before the live content
			.slice(0, that.settings.firstVisibleCol)
			.removeClass('active slider-control-future slider-control-active')
			.addClass('slider-control-past')
			.attr({
				tabindex : "-1",
				"aria-selected" : "false"
			});

		$liElements
			// this is everything after the live content
			.slice(that.settings.lastElement + 1)
			.removeClass('active slider-control-past slider-control-active')
			.addClass('slider-control-future')
			.attr({
				tabindex : "-1",
				"aria-selected" : "false"
			});
			
		if (that.settings.firstVisibleCol > that.settings.lastElement) {
			$liElements
				// this is the live content
				.slice(that.settings.firstVisibleCol, that.settings.firstVisibleCol + 1)
				.removeClass('slider-control-past slider-control-future')
				.addClass('slider-control-active')
				.attr({
					'tabindex' : 0,
					'aria-selected' : true
				});
				

		} else {
			$liElements
				// this is the live content
				.slice(that.settings.firstVisibleCol, that.settings.lastElement + 1)
				.removeClass('slider-control-past slider-control-future')
				.addClass('slider-control-active')
				.attr({
					'tabindex' : 0,
					'aria-selected' : true
				});

		}



		// if all pagination elements are active, then the pagination is irrelevant
		if ($liElements.length == $liElements.filter('.slider-control-active').length) {
			this.$pagination.filter(':visible').fadeOut();
		} else {
			this.$pagination.filter(':hidden').fadeIn();
		}
	};

	ac.prototype.updateViewportWidth = function () {

		var vpWidth = parseFloat($(this.settings.targetSelector).width()),
			winWidth = parseFloat($(window).width());

		// if the window width is smaller than the viewport, then it's our viewport by default
		vpWidth = (vpWidth <= winWidth) ? vpWidth : winWidth;

		this.settings.viewportWidth = vpWidth;
	};

	// find out if the target is inside the viewport (currently only checks horizontally)
	ac.prototype.viewPortTest = function ($t, threshold) {

		var that = this,
			left = that.settings.viewPortLeft,
			threshold = threshold || that.settings.threshold,
			right = left + that.settings.viewportWidth,

		// object left is a tricky one to define.
		// left position of element, plus any margin plus some percentage of the object's width
		objectLeft = parseFloat($t.position().left) + parseFloat($t.css("margin-left")) + parseFloat($t.width() * threshold);

		if (objectLeft >= left && objectLeft <= right) {
			return true; // in viewPort
		} else {
			return false; // not in viewPort
		}

	};
	// find the first slider item we want in the viewport
	// returns an object with two properties,
	// .element is the jQuery object
	// .column is the column number
	ac.prototype.findFirstElement = function (firstVisibleCol, threshold) {

		var inViewPort = false,
			i = firstVisibleCol,
			threshold = threshold || this.settings.threshold,
			totalNumberOfColumns = this.settings.columns.length,
			first = {};

		while (inViewPort === false && i < totalNumberOfColumns) {

			inViewPort = this.viewPortTest(this.settings.columns[i][0], threshold);

			if (inViewPort === false) {
				i += 1;
			}

		}

		if (typeof(this.settings.columns[i]) != 'undefined') {
			first.column = i;
			first.element = this.settings.columns[i][0];
			return first;
		} else {
			// Loop until we find something.
			if (threshold > 0 ) {
				return this.findFirstElement(firstVisibleCol, threshold - 0.1);
			} else {
				// Didn't find anything!
				// prevent an infinite loop
				return false;
			}
			
		}

	};


	// move the view port
	ac.prototype.moveViewPort = function (direction) {
		this.settings.viewPortLeft = (direction === "forward") ? this.settings.viewPortLeft + this.settings.viewportWidth
																: this.settings.viewPortLeft - this.settings.viewportWidth;
	};

	// find out if there are any elements to the right of the viewport
	// if there are we may need to manage the controls.
	ac.prototype.findTrailingElements = function () {
		var that = this,
			$firstElement = null;

		// temporarily move the viewport on...
		that.moveViewPort("forward");

		$firstElement = (that.findFirstElement(that.settings.firstVisibleCol)).element;

		// are we at the end?
		if (! $firstElement || 
			$firstElement.index() == this.settings.colCount || 
			! $firstElement.next().length && $firstElement.width() > that.settings.viewportWidth) { // the last item is not registering because it's wider than the viewport
			this.settings.lastElement = this.settings.colCount - 1;

			// TODO: make these prev/next selectors work with multiple carousels on the same page
			// we're at the end, so hide the next button
			$(that.settings.nextSelector).hide();

		} else {

			// looks like we're not at the end
			this.settings.lastElement = $firstElement.index() - 1;

			// this bit is just to help keep track of things
			//$(this.settings.itemSelector).css('border', 0);
			//(this.settings.columns[this.settings.lastElement][0]).css('border', '1px solid red');

			// if there isn't a next button then there should be
			// TODO: make these prev/next selectors work with multiple carousels on the same page
			$(that.settings.nextSelector + ':hidden').fadeIn();
		}

		// update the pagination
		// TODO, findTrailingElements gets called a lot. Is there a better place to update the pagination?
		this.updatePagination();

		// reset the viewport to the current setup
		that.moveViewPort("backward");
	};

	// moveSliderToElement moves the slider (!)
	ac.prototype.moveSliderToElement = function (element) {

		var that = this,

			// if we're given a number, grab the first element in the columm column
			// otheriwse grab the element passed.
			$element = (typeof (element) == "undefined") ? false :
						(typeof (element) == "number") ? that.settings.columns[element][0] : element,

			// this is where we're going to store the position of the element we're going to
			leftPosition;

		if (! $element) {
			return; // TODO: add proper error reporting here.
		}

		// this is where the slider is heading to
		leftPosition = $element.position().left;

		// the item we're moving to is now the first visible element
		this.settings.$firstVisisbleItem = $element;
		this.settings.viewPortLeft = leftPosition;

		// move the slider
		if (this.settings.tStyle) {
			$(this.settings.slider).css("margin-left", "-" + (leftPosition + 2 + "px"));
			setTimeout(function () {
				that.findTrailingElements();
			}, 900);
		} else {
			$(this.settings.slider).animate({
					"margin-left": "-" + (leftPosition + 2 + "px")
				}, 800, function () {
					that.findTrailingElements();
				});
		}

										// if we're given a number, assume that number is the column number
		this.settings.firstVisibleCol = (typeof (element) == "number") ? element :

										// if there was a modulus, we can't use index, so let's refer to the column array from parseElements
										// TODO: Allow resizing of elements in multi-row situations.
										(that.settings.modulus > 1) ? jQuery.inArray(leftPosition, that.settings.columnLeftValues) :

										// if there are no rows, to allow people to resize the elements, the simplest is to use the index
										$element.index();

		if (leftPosition === 0) {
			// TODO: make these prev/next selectors work with multiple carousels on the same page
			$('.prev').fadeOut(); // we're at the begining, hide the prev button
			return false; // stop the script from assessing anything else. We're done!
		} else {
			$('.prev:hidden').fadeIn(); // if the prev button's hidden, we can show it
		}

		// not at the beginning? Ok, let's see if we're at the end
		that.findTrailingElements();
	};

		// find out where we're going.
	ac.prototype.moveSlider = function (direction) {
		var that = this,
			index = that.settings.$firstVisisbleItem.index(),
			whichRow = parseInt(index / that.settings.colCount, 10),
			backupIndex = (index < that.settings.colCount) ? index + 1 : index,
			startingCol = (direction == "forward") ? parseInt(backupIndex - (whichRow * this.settings.colCount), 10) : 0,
			oppositeDirection = (direction == "forward") ? "backward" : "forward";

		// run any callback required before we start to move
		// this has to be here, instead of inside moveSliderToElement
		// so that it effects movement calculations.
		if (typeof(this.settings.callbacks.onMoveStart) == "function") {
			this.settings.callbacks.onMoveStart();
		}

		// create a virtual viewport where we're heading.
		that.moveViewPort(direction);

		// find the item we'd be moving to.
		var newPosition = (that.findFirstElement(startingCol));

		this.settings.firstVisibleCol = newPosition.column;

		// is the bio a different one to the one we're already at?
		if (that.settings.$firstVisisbleItem === newPosition.element) {

			// yes? Then it looks like we didn't go anywhere

			// Clean up after ourselves and reset the viewport to where it was
			that.moveViewPort(oppositeDirection);

		} else {

			// move the slider to the first new first visible biography.
			that.moveSliderToElement(newPosition.element);

			// update the pagination
			// TODO, findTrailingElements gets called a lot. Is there a better place to update the pagination?
			this.updatePagination();
		}
	};


	window.arbitraryCarousel = arbitraryCarousel;

})(window, jQuery);

