/**
 * @fileOverview The Vision Apps Carousel Class
 * @author <a href="mailto:matt.haynes@bbc.co.uk">Matt Haynes</a>
 */

/**
	<p>Creates a new Carousel object. The Carousel is a scrolling list of HTML elements.</p>
	
    @class Create a new Carousel object
    
    @param {String}  parentNodeId 		The ID of the HTML element to act as the holder for the carousel 
    @param {Object}  [opts]		  		An object of options
    @param {Boolean} [opts.vertical=false]	Is the Carousel a vertical one.
    @param {Boolean} [opts.loop=true]		Should the Carousel loop when it reaches the end.
    @param {Number}  [opts.visibleItems=5]  How many items are displayed in the carousel.
    @param {Object|Boolean} [opts.anim=glow.anim.Animation(0.5)]  A glow animation object for animated scroll. If false movement is not animated.
    @param {String}  [opts.itemFilter]    A CSS selector to only include certain elemnts in the carousel.

	@example var myCarousel = new Carousel("myParentDiv", {loop : false, itemFilter : "li"});

*/ 
function Carousel () {
	this._init(Carousel.arguments);
}

Carousel.prototype = {

	_init : function (argv) {
	
		// Set default properties
		this.vertical = false;
		this.loop = true;
		this.visibleItems = 4;
		this.anim = new glow.anim.Animation(0.5);
		
		// Set user opts
		glow.lang.apply(this, argv[1]);	
		
		// Apply class to holding elem
		this.holder = glow.dom.get(argv[0]);				
		this.holder.addClass("visionAppsCarousel");		

		// Get items for carousel
		this.items = (this.itemFilter) ? this.holder.get(this.itemFilter) : this.holder.children();		
		
		// Find item width and height. it's width / height + margin.
		var firstItem = this.items.slice(0, 0+1);		
		var width  = firstItem.width()  + parseInt(firstItem.css("margin-left")) +  parseInt(firstItem.css("margin-right"));
		var height = firstItem.height() + parseInt(firstItem.css("margin-top"))  +  parseInt(firstItem.css("margin-bottom"));
		
		// Set translation axis and multiplier
		this.axis = (this.vertical) ? "top" : "left";
		this.mult = (!this.vertical) ? width : height;		

		this.currentVisibleItems = [];

		// Animation stuff
		if (this.anim) {
			// Holds how far we have animated
			this.distanceToMove = 0;
		
			// Anim event listeners
			
			// Move a little on each frame
			glow.events.addListener(this.anim, "frame", function() {
					var dist = Math.ceil(this.distanceToMove * this.anim.value);
					this._moveItems(dist);					
					this.distanceToMove = this.distanceToMove - dist;
			}, this);
			
			// Tidy up on stop, ensures we have moved as far as has been requested.
			// Pixel perfect positioning
			glow.events.addListener(this.anim, "stop", function() {
					this._moveItems(this.distanceToMove);
					this.distanceToMove = 0;
			}, this);			

		}
			
		// Position items to default		
		this._setDefaultPositions();
		this._moveItems(0);
		
	},
		
		
	// PRIVATE(ISH) METHODS -----------------------------------
	// --------------------------------------------------------
	// --------------------------------------------------------
	
	
	/**
	 * Checks whether we are at an end point when we increment the items by a distance.
	 *
	 * @param {Number} dist The distance that our items will move.
	 *
	 * @returns Returns false if we are not at an end point or looping is on. Otherwise true.
	 *
	 */
	_atEndPoint : function (dist) {
	
		if (this.loop) return false;

		var curFirstItemPos = this.items[0].cache[this.axis] + dist;
		var maxFirstItemPos = 0;
		
		if (curFirstItemPos > maxFirstItemPos) {
			glow.events.fire(this, 'cantDoPrevious');	
			return true;
		}
		
		var curLastItemPos = this.items[this.items.length -1].cache[this.axis] + dist;
		var maxLastItemPos = (this.visibleItems * this.mult) - this.mult;		
		
		if (curLastItemPos < maxLastItemPos) {
			glow.events.fire(this, 'cantDoNext');				
			return true;
		}
				
		if (dist != 0) {			
			glow.events.fire(this, 'canDoPrevious');
			glow.events.fire(this, 'canDoNext');
		}
		
		return false;
		
	},
	
	/**
	 * Checks wether or not we should wrap the current item, if so update the items axis
	 *
	 * @param {Object} item The item to check wrapping against.
	 *
	 * @returns Returns false if we are not looping
	 *
	 */	
	_checkWrap : function (item) {
	
		// Wrap only features if we're looping
		if (!this.loop) return false;
					
		// Is the item off screen (position <= items width or height * -1)
		if (item.cache[this.axis] <= (this.mult * -1)) {
			item.cache[this.axis] = item.cache[this.axis] + (this.mult * this.items.length);	
			return;
		}
		
		// Is the item off screen in the other direction?
		// 
		if (item.cache[this.axis] >= (this.mult * (this.items.length -1))) {
			item.cache[this.axis] = item.cache[this.axis] - (this.mult * this.items.length);	
			return;
		}
		
	},
	
	/**
	 * Calculate the distance of a particular item from an on screen position
	 *
	 * @param {Object} item The item to claculate distance for.
	 * @param {Number} pos The position to calulate distance from
	 * @returns {Number} Number of pixels in distance.
	 *
	 */
	_distanceFrom : function(item, pos) {
		
		var itemPosition = this.items[item].cache[this.axis];
	
		var distance = itemPosition - (pos * this.mult);

		return distance;
	},	
	
	/**
	 * Returns an array containing the indexes of all visible items, the array is
	 * sorted so the first item is at position 0.
	 * @returns {Array} An array containing the index of each visible item
     */	
	_getVisibleItems : function () {
		var that = this;
		var sorted = this.currentVisibleItems.sort(
							function(a,b) {
								var a = that.items[a].cache[that.axis];
								var b = that.items[b].cache[that.axis];														
								return a - b;
							});
		return sorted;
	},
	
	/**
	 * Returns an array containing the indexes of specified non-visible items.
     *
	 * @param {Number} amount The amount of items to get.
	 * @param {Number} dir The direction to fetch items from -1 = previous, 1 = next
	 */
	_getNonVisibleItems : function (amount, dir) {
		
		if (amount > 0) {
			var curItems = this._getVisibleItems();
			var start = (dir > 0) ? curItems[curItems.length -1] : curItems[0];	
			var items = [];
			
			for (var i=1;i<=amount;i++) {
				
				var sum = (dir > 0) ? start + i : start - i;
				var ind;
				
				if (this.loop) {
					var mod = this.items.length;				
					var res = sum % mod;
					ind = (res < 0) ? mod + res : res;
				} else {
					if (sum >= 0 && sum < this.items.length) {
						ind = sum;
					} else {
						ind = null;
					}
				}
				
				if (ind != undefined) items.push(ind);
			}
			
			return items;
		}
		
		return [];
		
	},
	
	/**
	 * Move all items by a specified distance.
	 * @param {Number} dist The number of pixels to move.
	 */
	_moveItems : function (dist) {	
	
		if (this._atEndPoint(dist))	return;	
				
		var that = this;
		
		// Save indexes of visible items
		var currentVisibleItems = [];
		var min = 0;
		var max = this.mult * this.visibleItems;
		
		this.items.each(function(i) {
		
			// Update new axis value, check if we need to wrap or are at the end?
			that._updateAxis(this, this.cache[that.axis] + dist);
			that._checkWrap(this);		
			
			// If item is visible then put it in array
			if (this.cache[that.axis] >= min && this.cache[that.axis] < max) {
				currentVisibleItems.push(i);
			}
			
			// Set CSS positions
			var item = glow.dom.get(this);
			item.css("position", "absolute");					
			item.css(that.axis, this.cache[that.axis] + "px");																									
			
		});
		
		this.currentVisibleItems = currentVisibleItems;

	},	

	/**
	 * Sets the axis for each item, item 0 is always first on screen.
	 */
	_setDefaultPositions : function () {
		var that = this;
		
		this.items.each(function(i) {	
				this.cache = {};
				that._updateAxis(this, that.mult *i);
			});

	},		
	
	/**
	 * Updates an items axis position
	 * @param {Object} item The item to update
	 * @param {Number} val The value of the position
	 *
	 */
	_updateAxis : function (item, val) {
		item.cache[this.axis] = val;
	},

	// PUBLIC METHODS (API) -----------------------------------
	// --------------------------------------------------------
	// --------------------------------------------------------	
	
	// Movement

	/**
	 * <p>Moves the carousel in either direction by a specified numkber of items</p>
	 * @param {Number} items The number of items to move. Using positive and negative numbers conrtolls the direction
	 * @param {Object} [opts] An object of options
	 * @param {Boolean} [opts.usePixels=false] Use pixels instead of items as value to move by.
	 * @param {Boolean} [opts.anim=true] Animate this movement
	 *
	 * @example
	 * myCarousel.move(-1); // Move carousel 1 item backwards
	 * myCarousel.move(300, {usePixels : true);	// Move carousel 300px forwards
	 *
	 */
	move : function (items, opts) {					
			
		var opts = opts || {anim:true};
		
		var distance = (opts["usePixels"]) ? items : items * this.mult;	
		
		if (this.anim && opts["anim"]) {

			// Stop any other animations, and tidy whats left.
			this.anim.stop()

			// Start the animation
			this.distanceToMove = distance;
			this.anim.start();

		} else {
			this._moveItems(distance);		
		}
	},
	
	/** 
	 * <p>Moves a particular item to a particular position. If looping is set to false then an item
	 *    may not be able to move to a particular position. I.E. The first item will not be able to move to position 10</p>
	 *
	 * @param {Number} item Zero based index identifying the item to move
	 * @param {Number} item Zero based index identifying the position to move to
	 * @param {Object} [opts] An object of options	 
	 * @param {Boolean} [opts.anim=true] Animate this movement	 
	 *
	 * @example myCarousel.moveTo(0, 10);
	 *
	 */
	moveTo : function (item, pos, opts) {
		
		var opts = opts || {anim:true};

		// How far do we need to move ?
		var distance = this._distanceFrom(item, pos) * -1;
		
		this.move(distance, {usePixels : true, anim : opts["anim"]}); 
	},
	
	/**
	 * <p>Resets all items to original positions.</p>
	 */
	 resetPositions : function() {
	 	this._setDefaultPositions();
	 	this._moveItems(0);
	 },
	
	/**
	 * <p>Returns a glow nodeList represtenting the items in the carousel.</p>
	 *
	 * @returns {Object} A glow NodeList of all items in carousel
	 *
	 */
	getItems : function () {
		return this.items;	
	},

	
	/**
	 * <p>Returns an array containing the indexes of all visible items.</p>
	 * @returns {Array} An array containing the index of each visible item
	 * @example 
	 * // Returns something like [2,3,4,5,6]
	 * myCarousel.getVisibleIndexes(); 
	 */
	getVisibleItemsIndexes : function () {
		return this._getVisibleItems();
	},

	getNextVisibleItemsIndexes : function (amount) {
		var amount = (amount) ? amount : this.visibleItems;
		return this._getNonVisibleItems(amount, 1);	
	},

	getPrevVisibleItemsIndexes : function (amount) {
		var amount = (amount) ? amount : this.visibleItems;
		return this._getNonVisibleItems(amount, -1);
	},	
	
	/**
	 * <p>Adds a class name to a specifed item or items</p>
	 * @param {String|Number} item If "*" adds class to all items. If an integer it will add the class to the item at that index.
	 * @param {String} className The class to add to the item / items.
	 */
	addClass : function (item, className) {
		if (item == "*") {
			this.items.addClass(className);
		} else {
			this.items.slice(item, item + 1).addClass(className);
		}
	},

	/**
	 * <p>Removes a class name to a specifed item or items</p>
	 * @param {String|Number} item If "*" removes class from all items. If an integer it will remove the class from the item at that index.
	 * @param {String} className The class to remove from the item / items.
	 */	
	removeClass : function (item, className) {
		if (item == "*") {
			this.items.removeClass(className);
		} else {
			this.items.slice(item, item + 1).removeClass(className);
		}
	},
	
	// Add Item
	addItem : function (item, position) {
	
	}, 
	
	removeItem : function (position) {
	
	} 	

}