/**
 * TODO animation
 * TODO prefetching auch für items aus dem html
 * @author soeren
 */


(function ($) {
	
	
	/**
	 * Global state of all galleries.
	 * Later contains Gallery class merged with these properties!
	 */
	var commonData = {
		
		/**
		 * Add functions to this hash, if you need to retrieve elements by other means than through link-tags or
		 * data objects with properties like: full_src, thumb_src, title, caption, copyright, type.
		 */
		providers: {
			
			/**
			 * Retrieve data from a list of links. 
			 * this is bound to the gallery object
			 * this.links is a jq object retrieved from the gallery html for the given config
			 */
			fromHtml: function () {
				var obj = this;
				this.links.each(function (i) {
					var link = $(this);
					obj.data.push({
						full_src: link.attr('href'),
						thumb_src: obj.thumbs.eq(i).attr('src'),
						title: link.attr('title'),
						caption: obj.captions.eq(i).clone(true),
						type: 'TODO item type',
						copyright: 'TODO copyright'
					});
				});
			},
			
			/**
			 * Append a list of data objects if given with the config hash.
			 */
			fromOptions: function () {
				var obj = this;
				if (this.opt.data) $.each(obj.opt.data, function (i) {
					obj.data.push(this);
					obj.slider.append(obj.generateItem(this));
				});
			}
			
		},
		
		
		/**
		 * Default options, change these to change all subsequent configurations.
		 */
		options: {
			
			namespace: '.gallery', // the namespace to use for data storage/event handling
			selectSlider: 'ul.galleryItems', // selector pointing to the slide container
			selectListItem: 'ul.galleryItems li', // selector pointing to slide elements
			selectLink: 'a', // selector for link tags inside slide elts
			selectThumb: 'img', // selector for thumbs inside slide elts
			selectCaption: 'div.caption', // selector for captions inside slide elts
			selectZoom: 'div.bigImgContainer img', // selector pointing to the zoom image in this gallery
			selectNext: 'a.next', // selector pointing to buttons scrolling forward
			selectBack: 'a.back', // selector pointing to buttons scrolling backwards
			classSelected: 'selected', // class for selected news items
			classInactive: 'inactive', // class for deactivated controls
			selectCaptionDisplay: 'div.description', // target element for the selected caption
			
			groupedSliderItems: 3, // minimal number of slider-items to be moved in one step
			
			oninit: function () {}, 
			onchange: function () {}, 
			onupdate: function () {}, 
			onmove: function () {}, 
			onselect: function () {}, 
			
			api: false // true, if Gallery instance should be returned instead
			
		},
		
		// exposes all gallery instances
		galleries: []
		
	};
	
	
	
	
	/**
	 * @class gallery on given element el with options opt.
	 * 
	 * @param jQuery el
	 * @param Object opt
	 */
	var Gallery = function (el, opt) {
		
		var obj = $.extend(this, {
			
			// save arguments
			opt: $.extend({}, opt),
			el: el,
			
			// retrieve controls and thumbs
			slider: $(opt.selectSlider, el),
			listItems: $(opt.selectListItem, el),
			thumbs: $(opt.selectListItem, el).find(opt.selectThumb),
			links: $(opt.selectListItem, el).find(opt.selectLink),
			zoom: $(opt.selectZoom, el),
			zoomHelper: $(opt.selectZoom, el).clone(false),
			captions: $(opt.selectListItem, el).find(opt.selectCaption),
			next: $(opt.selectNext, el),
			back: $(opt.selectBack, el),
			selectCaptionDisplay: $(opt.selectCaptionDisplay, el),
			
			// get the selected element
			selected: $(opt.selectListItem, el).filter('.' + opt.classSelected),
			
			data: [],
			currentIndex: 0,
			sliderIndex: 0,
			hasBeenChanged: false,
			firstImageHasLoaded: false
		});
		
		// store instance
		commonData.galleries.push(obj);
		el.data('instance' + opt.namespace);
		
		// load data into this.data
		obj.appendData();
		
		// bind events to notify of gallery changes
		el.bind('init' + opt.namespace, opt.oninit);
		el.bind('change' + opt.namespace, opt.onchange);
		el.bind('update' + opt.namespace, opt.onupdate);
		el.bind('move' + opt.namespace, opt.onmove);
		el.bind('select' + opt.namespace, opt.onselect);
		
		// wait for the first time a zoom occurs and set a flag
		el.one('change', function (e) {
			obj.hasBeenChanged = true;
		});
		
		// bind load event of zoom image after initial load event occurs
		obj.zoom.bind('load' + opt.namespace, function (e) {
			
			obj.el.trigger('select', {
				gallery: obj
			});
			
			if (!obj.firstImageHasLoaded) {
				return;
			}
			
			el.trigger('zoom', {
				gallery: obj
			});
		});
		
		// bind click handlers for forward scrolling 
		obj.next.each(function (numLink) {
			var link = $(this);
			var data = link.metadata();
			var steps = data.steps;
			var noChange = data.noChange;
			link.bind('click' + opt.namespace, function(e) {
				obj.doNext(steps, noChange);
				obj.doPrefetch(1, opt.groupedSliderItems, noChange);
				e.preventDefault();
			}).bind('mouseover' + opt.namespace, function (e) {
				obj.doPrefetch(1, steps, noChange);
				e.preventDefault();
			});
		});
		
		// bind click handlers for backward scrolling
		obj.back.each(function (numLink) {
			var link = $(this);
			var data = link.metadata();
			var steps = data.steps;
			var noChange = data.noChange;
			link.bind('click' + opt.namespace, function(e) {
				obj.doBack(steps, noChange);
				obj.doPrefetch(-1, opt.groupedSliderItems, noChange);
				e.preventDefault();
			}).bind('mouseover' + opt.namespace, function (e) {
				obj.doPrefetch(-1, steps, noChange);
				e.preventDefault();
			});
		});
		
		// bind click handlers to sldie items 
		obj.links.bind('click' + opt.namespace, function (e) {
			obj.selectItem(obj.links.index(this));
			e.preventDefault();
		});
		
		// retrieve selection state from html, if possible
		obj.determineSelectedIndex();
		
		obj.setControlStates();
		
		// init the gallery on load of the currently visible item
		var loadImg = new Image();
		loadImg.onload = function () {
			obj.firstImageHasLoaded = true;
			el.trigger('init', {
				gallery: obj
			});
		};
		loadImg.src = this.data[this.currentIndex].full_src;
		
	};
	
	/**
	 * Adds methods to Gallery class
	 */
	$.extend(Gallery.prototype, {
		
		/**
		 * Clones first list item and sets its properties/offsprings according to given data
		 * 
		 * @param {Object} data
		 * @return jQuery
		 */
		generateItem: function (data) {
			
			var listItem = this.listItems.eq(0).clone(false).removeClass(this.opt.classSelected);
			
			// add "prefetching"
			var prefetched = false;
			listItem.bind('prefetch' + this.opt.namespace, function (e) {
				if (prefetched) {
					return;
				}
				prefetched = true;
				thumb.attr({
					src: data.thumb_src
				});
				new Image().src = data.full_src;
			});
			
			// set link attribs
			var link = listItem.find(this.opt.selectLink).attr({
				href: data.full_src,
				title: data.title
			});
			
			// set thumb attribs
			var thumb = listItem.find(this.opt.selectThumb).removeAttr('src').attr({
				title: data.title,
				alt: data.title
			}).removeAttr('width').removeAttr('height');
			
			// insert caption
			var caption = listItem.find(this.opt.selectCaption).html(data.caption);
			
			// push this item into respective jquery objects
			this.links.push(link.get(0));
			this.thumbs.push(thumb.get(0));
			this.captions.push(caption.get(0));
			this.listItems.push(listItem.get(0));
			
			this.el.trigger('update', {
				gallery: this
			});
			
			return listItem;
			
		},
		
		/**
		 * Set class on selected item
		 * 
		 * @param {Object} oldIndex
		 * @param {Object} newIndex
		 */
		updateSliderItem: function (oldIndex, newIndex) {
			this.listItems.removeClass(this.opt.classSelected).eq(newIndex).addClass(this.opt.classSelected);
		},
		
		/**
		 * Returns the image, currently displayed in full.
		 */
		getCurrentImage: function () {
			return this.data[this.currentIndex];
		},
		
		/**
		 * Move slider for given current and future index
		 * 
		 * @param {Object} oldIndex
		 * @param {Object} newIndex
		 */
		moveSlider: function (oldIndex, newIndex) {
			// save scope
			var obj = this;
			
			// determine visible slider portion
			var oldPos = this.getSliderPage(oldIndex) * this.opt.groupedSliderItems;
			var newPos = this.getSliderPage(newIndex) * this.opt.groupedSliderItems;

			// if slider has not moved, return
			if (oldPos == newPos) {
				return false;
			}

			this.el.trigger('move', {
				gallery: this,
				oldIndex: oldIndex,
				newIndex: newIndex
			});
			
			// get scroll width
			var width = 0;
			this.listItems.each(function (i) {
				if (i >= newIndex || Math.floor(i / obj.opt.groupedSliderItems) * obj.opt.groupedSliderItems == newPos) {
					return false;
				}
				width += $(this).outerWidth(true);
			});
			
			// scroll the slider
			this.slider.animate({
				marginLeft: -width
			});
			
			return true;
		},
		
		/**
		 * Walk the global data retrieval helpers to get data from current gallery element.
		 */
		appendData: function () {
			for (var i in commonData.providers) {
				commonData.providers[i].call(this);
			}
		},
		
		/**
		 * Find the item selected by initial HTML.
		 */
		determineSelectedIndex: function () {
			var index = 0;
			index = this.listItems.index(this.selected);
			this.currentIndex = this.sliderIndex = index = (index < 1 ? 0 : index);
			return index;
		},
		
		/**
		 * Set the new caption.
		 * @param {Object} data
		 */
		updateCaption: function (data) {
			this.selectCaptionDisplay.html(data.caption);
		},
		
		/**
		 * Set the new zoom image.
		 * @param {Object} data
		 */
		updateZoom: function (data) {
			if (this.zoom.attr('src') == data.full_src && this.zoom.attr('title') == data.title) {
				this.el.trigger('zoom', {
					gallery: this
				});
				return;
			}
			this.zoom.removeAttr('width').removeAttr('height').attr({
				src: data.full_src,
				title: data.title,
				alt: data.title
			});
		},
		
		/**
		 * Get the next item index for given direction and step size.
		 * @param {Object} dir
		 * @param {Object} steps
		 */
		getNextIndex: function (dir, steps, noChange) {
			steps = steps || 1;
			var compare = noChange ? this.sliderIndex : this.currentIndex;
			var index = 0;
			if (dir > 0) {
				index = compare + steps;
			} else {
				index = compare - steps;
			}
			index = Math.min(index, this.data.length - 1);
			index = Math.max(index, 0);
			return index;
		},
		
		/**
		 * Retrieve item for item index.
		 * @param {Object} i
		 */
		getData: function (i) {
			return this.data[i];
		},
		
		/**
		 * Update all visible elements of the gallery for a given index.
		 * @param {Object} index
		 * @param {Object} noChange
		 */
		updateGallery: function (index, noChange) {
			
			this.moveSlider(this.sliderIndex, index);
			
			this.sliderIndex = index;
			
			if (!noChange) {
				this.updateSliderItem(this.sliderIndex, index);
				var data = this.getData(index);
				this.updateCaption(data);
				this.updateZoom(data);
				
				if (this.currentIndex != index) {
					this.el.trigger('change', {
						gallery: this,
						oldIndex: this.currentIndex,
						newIndex: index
					});
				}
				
				this.currentIndex = index;
			}
			
			this.setControlStates();
			
		},
		
		/**
		 * Selects a specific item in the gallery.
		 * @param {Object} index
		 */
		selectItem: function (index) {
			this.updateGallery(index);
		},
		
		/**
		 * Scroll gallery forward for given step size.
		 * @param {Object} steps
		 * @param Boolean noChange
		 */
		doNext: function (steps, noChange) {
			var index = this.getNextIndex(1, steps, noChange);
			this.updateGallery(index, noChange);
		},
		
		
		/**
		 * Scroll gallery backwards for given step size.
		 * @param {Object} steps
		 * @param Boolean noChange
		 */
		doBack: function (steps, noChange) {
			var index = this.getNextIndex(-1, steps, noChange);
			this.updateGallery(index, noChange);
		},
		
		/**
		 * Deactivate nav buttons according to the current position.
		 * @param {Object} index
		 */
		setControlStates: function () {
			
			var obj = this;
			
			var iterate = function (i) {
				var link = $(this);
				var data = link.metadata();
				var steps = data.steps || 1;
				var noChange = !!data.noChange;
				var compare = noChange ? obj.sliderIndex : obj.currentIndex;
				if (check(obj, steps, noChange, compare)) {
					link.addClass('inactive');
				} else {
					link.removeClass('inactive');
				}
			};
			
			var check = function (obj, steps, noChange, compare) {
				return (steps == 1 && compare + steps >= obj.data.length) || (steps > 1 && obj.getSliderPage(compare + steps) == obj.getSliderPage(compare))
			};
			this.next.each(iterate);
			
			check = function (obj, steps, noChange, compare) {
				return compare - steps < 0;
			};
			this.back.each(iterate);
			
		},
		
		/**
		 * Get page for given index
		 * @param {Object} index
		 */
		getSliderPage: function (index) {
			return Math.floor(Math.min((this.data.length - 1) / this.opt.groupedSliderItems, index / this.opt.groupedSliderItems));
		},
		
		/**
		 * Prefetch upcoming images
		 * @param {Object} dir
		 * @param {Object} steps
		 * @param {Object} noChange
		 */
		doPrefetch: function (dir, steps, noChange) {
			noChange = !!noChange;
			steps = steps || 1;
			dir = dir || 1;
			var compare = noChange ? this.sliderIndex :  this.currentIndex;
			var page = this.getSliderPage(compare) + dir;
			var lower = page * this.opt.groupedSliderItems - 1;
			var upper = (page + 1) * this.opt.groupedSliderItems;
			this.listItems.filter(':gt(' + lower + '):lt(' + upper + ')').trigger('prefetch', {
				gallery: this
			});
		}
		
	});
	
	
	
	// expose class and config
	$.gallery = commonData = $.extend(Gallery, commonData);
	
	
	
	
	/**
	 * Implement a jQuery plugin that walks the given jquery object and instantiates a Gallery each.
	 * Uses metadata-plugin to configure scrool-width of nav-links.
	 * @param {Object} opt
	 * @param {Object} data
	 */
	$.fn.gallery = function (opt, data) {
		var firstGallery = null;
		data = data || [];
		opt = $.extend({}, commonData.options, opt);
		opt.data = data;
		
		this.each(function (i) {
			var item = $(this);
			var gallery = item.data('instance' + opt.namespace);
			
			// prevent reinitialization
			if (!gallery) {
				gallery = new Gallery(item, opt);
			}
			
			if (i == 0) {
				firstGallery = gallery;
			}
			
		});
		
		// return gallery instance instead?
		if (opt.api) {
			return firstGallery;
		} else {
			return this;
		}
		
	};
	
	
	
})(jQuery);

