/**
Script: Slideshow.js
	Slideshow - A javascript class for Mootools to stream and animate the presentation of images on your website.

License:
	MIT-style license.

Copyright:
	Copyright (c) 2011 [Aeron Glemann](http://www.electricprism.com/aeron/).

Dependencies:
	Mootools 1.3.1 Core: Fx.Morph, Fx.Tween, Selectors, Element.Dimensions.
	Mootools 1.3.1.1 More: Assets.
*/
(function(){
	WhenPaused = 1 << 0;
	WhenPlaying = 1 << 1;
	OnStart = 1 << 2;
	
	Slideshow = new Class({
		Implements: [Chain, Events, Options],

		options: {/*
			onComplete: $empty,
			onEnd: $empty,
			onStart: $empty,*/
		    accesskeys: {'first': {'key': 'shift left', 'label': 'Shift + Leftwards Arrow'}, 'prev': {'key': 'left', 'label': 'Leftwards Arrow'}, 'pause': {'key': 'p', 'label': 'P'}, 'next': {'key': 'right', 'label': 'Rightwards Arrow'}, 'last': {'key': 'shift right', 'label': 'Shift + Rightwards Arrow'}},
			captions: true,
			center: true,
			classes: [/*'slideshow', 'first', 'prev', 'play', 'pause', 'next', 'last', 'images', 'captions', 'controller', 'thumbnails', 'hidden', 'visible', 'inactive', 'active', 'loader'*/],
			controller: true,
			data: null,
			delay: 2000,
			duration: 1000,
			fast: false,
			height: false,
			href: '',
			hu: '',
			linked: false,
			loader: true,
			loop: true,
			match: /\?slide=(\d+)$/,
			overlap: true,
			paused: false,
			random: false,
			replace: [/(\.[^\.]+)$/, 't$1'],
			resize: 'fill',
			slide: 0,
			thumbnails: true,
			titles: false,
			transition: 'sine:in:out',
			width: false
		},

	/**
	Constructor: initialize
		Creates an instance of the Slideshow class.

	Arguments:
		element - (element) The wrapper element.
		data - (array or object) The images and optional thumbnails, captions and links for the show.
		options - (object) The options below.

	Syntax:
		var myShow = new Slideshow(element, data, options);
	*/

		initialize: function(el, data, options){
			this.setOptions(options);
			this.el = document.id(el);
			if (!this.el) 
				return;
			var match = window.location.href.match(this.options.match);
			this.slide = this._slide = this.options.match && match ? match[1].toInt() 
				: this.options.slide;
			this.counter = this.timeToNextTransition = this.timeToTransitionComplete = 0;
			this.direction = 'left';
			this.cache = {};
			this.paused = false;
			if (!this.options.overlap)
				this.options.duration *= 2;
			var anchor = this.el.getElement('a') || new Element('a');
			if (!this.options.href)
				this.options.href = anchor.get('href') || '';
			if (this.options.hu.length && !this.options.hu.test(/\/$/)) 
				this.options.hu += '/';
			if (this.options.fast === true)
				this.options.fast = WhenPaused | WhenPlaying;

			// styles

			var keys = 'slideshow first prev play pause next last images captions controller thumbnails hidden visible inactive active loader'.split(' '),
				values = keys.map(function(key, i){
					return this.options.classes[i] || key;
				}, this);
			this.classes = values.associate(keys);
			this.classes.get = function(){
				var str = '.' + this.slideshow;
				for (var i = 0, l = arguments.length; i < l; i++)
					str += '-' + this[arguments[i]];
				return str;
			}.bind(this.classes);

			// data	
			
			if (!data){
				this.options.hu = '';
				data = {};
				var thumbnails = this.el.getElements(this.classes.get('thumbnails') + ' img');
				this.el.getElements(this.classes.get('images') + ' img').each(function(img, i){
					var src = img.src,
						caption = img.alt || img.title,
						href = img.getParent().href,
						thumbnail = thumbnails[i] ? thumbnails[i].src 
							: '';
					data[src] = {'caption': caption, 'href': href, 'thumbnail': thumbnail};
				});
			}
			var loaded = this.load(data);
			if (!loaded)
				return; 

			// events

			this.events = {};
			this.events.push = function(type, fn){
				if (!this[type])
					this[type] = [];
				this[type].push(fn);
				document.addEvent(type, fn);
				return this;
			}.bind(this.events);

			this.accesskeys = {};
			for (action in this.options.accesskeys){
				var obj = this.options.accesskeys[action];
				this.accesskeys[action] = accesskey = {'label': obj.label};
				['shift', 'control', 'alt'].each(function(modifier){
					var re = new RegExp(modifier, 'i');
					accesskey[modifier] = obj.key.test(re);
					obj.key = obj.key.replace(re, '');
				});
				accesskey.key = obj.key.trim();
			}

			this.events.push('keyup', function(e){
				Object.each(this.accesskeys, function(accesskey, action){
					if (e.key == accesskey.key && e.shift == accesskey.shift && e.control == accesskey.control && e.alt == accesskey.alt)
						this[action]();
				}, this);			
			}.bind(this));	 

			// required elements

			var el = this.el.getElement(this.classes.get('images')),
				img = this.el.getElement('img') || new Element('img'),
				images = el ? el.empty() 
					: new Element('div', {'class': this.classes.get('images').substr(1)}).inject(this.el),
				div = images.getSize();
			this.height = this.options.height || div.y;		
			this.width = this.options.width || div.x;
			images.set({'styles': {'height': this.height, 'width': this.width}});
			this.el.store('images', images);
			this.a = this.image = img;
			if (Browser.ie && Browser.version >= 7)
				this.a.style.msInterpolationMode = 'bicubic';
			this.a.set('styles', {'display': 'none'});
			this.b = this.a.clone();
			[this.a, this.b].each(function(img){
				anchor.clone().cloneEvents(anchor).grab(img).inject(images);
			});

			// optional elements

			this.options.captions && new Caption(this);
			this.options.controller && new Controller(this);
			this.options.loader && new Loader(this);
			this.options.thumbnails && new Thumbnails(this);

			// begin show

			this._preload(this.options.fast & OnStart);
		},

	/**
	Public method: go
		Jump directly to a slide in the show.

	Arguments:
		n - (integer) The index number of the image to jump to, 0 being the first image in the show.

	Syntax:
		myShow.go(n);	
	*/

		go: function(n, direction){
			var nextSlide = (this.slide + this.data.images.length) % this.data.images.length;
			if (n == nextSlide || Date.now() < this.timeToTransitionComplete)
				return;		
			clearTimeout(this.timer);
			this.timeToNextTransition = 0;		
			this.direction = direction ? direction 
				: n < this._slide ? 'right' 
				: 'left';
			this.slide = this._slide = n;
			if (this.preloader) 
				this.preloader = this.preloader.destroy();
			this._preload((this.options.fast & WhenPlaying) || (this.paused && this.options.fast & WhenPaused));
		},

	/**
	Public method: first
		Goes to the first image in the show.

	Syntax:
		myShow.first();	
	*/

		first: function(){
			this.prev(true); 
		},

	/**
	Public method: prev
		Goes to the previous image in the show.

	Syntax:
		myShow.prev();	
	*/

		prev: function(first){
			var n = 0;
			if (!first){
				if (this.options.random){
					if (this.showed.i < 2)
						return;
					this.showed.i -= 2;
					n = this.showed.array[this.showed.i];
				}
				else
					n = (this.slide - 1 + this.data.images.length) % this.data.images.length;									
			}
			this.go(n, 'right');
		},

	/**
	Public method: pause
		Toggles play / pause state of the show.

	Arguments:
		p - (undefined, 1 or 0) Call pause with no arguments to toggle the pause state. Call pause(1) to force pause, or pause(0) to force play.

	Syntax:
		myShow.pause(p);	
	*/

		pause: function(p){
			if (p != undefined)
				this.paused = p ? false 
					: true;
			if (this.paused){ // play
				this.paused = false;
				this.timeToTransitionComplete = Date.now() + this.timeToTransitionComplete;		
				this.timer = this._preload.delay(50, this);
				[this.a, this.b].each(function(img){
					['morph', 'tween'].each(function(p){
						if (this.retrieve(p)) this.get(p).resume();
					}, img);
				});
				this.controller && this.el.retrieve('pause').getParent().removeClass(this.classes.play);
			} 
			else { // pause
				this.paused = true;
				this.timeToTransitionComplete = this.timeToTransitionComplete - Date.now();
				clearTimeout(this.timer);
				[this.a, this.b].each(function(img){
					['morph', 'tween'].each(function(p){
						if (this.retrieve(p)) this.get(p).pause();
					}, img);
				});
				this.controller && this.el.retrieve('pause').getParent().addClass(this.classes.play);
			}
		},

	/**
	Public method: next
		Goes to the next image in the show.

	Syntax:
		myShow.next();	
	*/

		next: function(last){
			var n = last ? this.data.images.length - 1 
				: this._slide;
			this.go(n, 'left');
		},

	/**
	Public method: last
		Goes to the last image in the show.

	Syntax:
		myShow.last();	
	*/

		last: function(){
			this.next(true); 
		},

	/**
	Public method: load
		Loads a new data set into the show: will stop the current show, rewind and rebuild thumbnails if applicable.

	Arguments:
		data - (array or object) The images and optional thumbnails, captions and links for the show.

	Syntax:
		myShow.load(data);
	*/

		load: function(data){
			this.firstrun = true;
			this.showed = {'array': [], 'i': 0};
			if (typeOf(data) == 'array'){
				this.options.captions = false;			
				data = new Array(data.length).associate(data.map(function(image, i){ return image + '?' + i })); 
			}
			this.data = {'images': [], 'captions': [], 'hrefs': [], 'thumbnails': [], 'targets': [], 'titles': []};
			for (var image in data){
				var obj = data[image] || {},
					image = this.options.hu + image,
					caption = obj.caption ? obj.caption.trim() 
						: '',
					href = obj.href ? obj.href.trim() 
						: this.options.linked ? image 
						: this.options.href,
					target = obj.target ? obj.target.trim() 
						: '_self',
					thumbnail = obj.thumbnail ? this.options.hu + obj.thumbnail.trim() 
						: image.replace(this.options.replace[0], this.options.replace[1]),
					title = caption.replace(/<.+?>/gm, '').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, "'");
				this.data.images.push(image);
				this.data.captions.push(caption);
				this.data.hrefs.push(href);
				this.data.targets.push(target);
				this.data.thumbnails.push(thumbnail);
				this.data.titles.push(title);
			}
			if (this.options.random)
				this.slide = this._slide = Number.random(0, this.data.images.length - 1);

			// only run when data is loaded dynamically into an existing slideshow instance

			if (this.options.thumbnails && this.el.retrieve('thumbnails'))
				this._thumbnails();
			if (this.el.retrieve('images')){
				[this.a, this.b].each(function(img){
					['morph', 'tween'].each(function(p){
						if (this.retrieve(p)) this.get(p).cancel();
					}, img);
				});
				this.slide = this._slide = this.timeToTransitionComplete = 0;
				this.go(0);		
			}
			return this.data.images.length;
		},

	/**
	Public method: destroy
		Destroys a Slideshow instance.

	Arguments:
		p - (string) The images and optional thumbnails, captions and links for the show.

	Syntax:
		myShow.destroy(p);
	*/

		destroy: function(p){
			Object.each(this.events, function(array, e){
				if ('each' in array)
					array.each(function(fn){ document.removeEvent(e, fn); });
			});
			this.pause(1);
			'caption loader thumbnails'.split(' ').each(function(i, timer){
				this.options[i] && (timer = this[i].retrieve('timer')) && clearTimeout(timer);
			}, this);
			typeOf(this.el[p]) == 'function' && this.el[p]();
			delete this.el.uid;
		},

	/**
	Private method: preload
		Preloads the next slide in the show, once loaded triggers the show, updates captions, thumbnails, etc.
	*/

		_preload: function(fast){
			var src = this.data.images[this._slide].replace(/([^?]+).*/, '$1'),
				cached = loaded = !!this.cache[src];
			if (!cached){
				if (!this.preloader)
				 	this.preloader = new Asset.image(src, {
						'onerror': function(){
							// do something
						},
						'onload': function(){
							this.store('loaded', true);
						}
					});
				loaded = this.preloader.retrieve('loaded') && this.preloader.get('width');
			}
			if (loaded && Date.now() > this.timeToNextTransition && Date.now() > this.timeToTransitionComplete){
				var src = this.data.images[this._slide].replace(/([^?]+).*/, '$1');
				if (this.preloader){
					this.cache[src] = {
						'height': this.preloader.get('height'),
						'src': src,
						'width': this.preloader.get('width')
					}
				}
				if (this.stopped){
					if (this.options.captions)
						this.caption.get('morph').cancel().start(this.classes.get('captions', 'hidden'));
					this.pause(1);
					if (this.end)
						this.fireEvent('end');
					this.stopped = this.end = false;
					return;				
				}
				this.image = this.counter % 2 ? this.b 
					: this.a;
				this.image.set('styles', {'display': 'block', 'height': null, 'visibility': 'hidden', 'width': null, 'zIndex': this.counter});
				this.image.set(this.cache[src]);
				this.image.width = this.cache[src].width;
				this.image.height = this.cache[src].height;
				this.options.resize && this._resize(this.image);
				this.options.center && this._center(this.image);
				var anchor = this.image.getParent();
				if (this.data.hrefs[this._slide]){
					anchor.set('href', this.data.hrefs[this._slide]);
					anchor.set('target', this.data.targets[this._slide]);			
				} 
				else {
					anchor.erase('href');
					anchor.erase('target');
				}
				var title = this.data.titles[this._slide];
				this.image.set('alt', title);		
				this.options.titles && anchor.set('title', title);
				this.options.loader && this.loader.fireEvent('hide');
				this.options.captions && this.caption.fireEvent('update', fast);				
				this.options.thumbnails && this.thumbnails.fireEvent('update', fast); 			
				this._show(fast);
				this._loaded(fast);
			} 
			else {
				if (Date.now() > this.timeToNextTransition && this.options.loader)
					this.loader.fireEvent('show');
				this.timer = this._preload.delay(50, this, fast); 
			}
		},

	/**
	Private method: show
		Does the slideshow effect.
	*/

		_show: function(fast){
			if (!this.image.retrieve('morph')){
				var options =  this.options.overlap ? {'link': 'cancel'} : {'link': 'chain'};
				$$(this.a, this.b).set('morph', Object.merge(options, {'duration': this.options.duration, 'onStart': this._start.bind(this), 'onComplete': this._complete.bind(this), 'transition': this.options.transition}));
			}
			var hidden = this.classes.get('images', (this.direction == 'left' ? 'next' 
				: 'prev')),
				visible = this.classes.get('images', 'visible'),
				img = this.counter % 2 ? this.a 
					: this.b;
			if (fast){			
				img.get('morph').cancel().set(hidden);
				this.image.get('morph').cancel().set(visible); 			
			} 
			else {
				if (this.options.overlap){
					img.get('morph').set(visible);
					this.image.get('morph').set(hidden).start(visible);
				} 
				else {
					var fn = function(visible){
						this.image.get('morph').start(visible);
					}.pass(visible, this);
					if (this.firstrun)
						return fn();
					hidden = this.classes.get('images', (this.direction == 'left' ? 'prev' 
						: 'next'));
					this.image.get('morph').set(hidden);				
					img.get('morph').set(visible).start(hidden).chain(fn);
				}
			}
		},

	/**
	Private method: loaded
		Run after the current image has been loaded, sets up the next image to be shown.
	*/

		_loaded: function(fast){
			this.counter++;
			this.timeToNextTransition = Date.now() + this.options.duration + this.options.delay;
			this.direction = 'left';			
			this.timeToTransitionComplete = fast ? 0 
				: Date.now() + this.options.duration;
			if (this._slide == (this.data.images.length - 1) && !this.options.loop && !this.options.random)
				this.stopped = this.end = true;
			if (this.options.random){
				this.showed.i++;
				if (this.showed.i >= this.showed.array.length){
					var n = this._slide;
					if (this.showed.array.getLast() != n) this.showed.array.push(n);
					while (this._slide == n)
						this.slide = this._slide = Number.random(0, this.data.images.length - 1);				
				}
				else
					this.slide = this._slide = this.showed.array[this.showed.i];
			}
			else {
				this.slide = this._slide;
				this._slide = (this.slide + 1) % this.data.images.length;
			}
			if (this.image.getStyle('visibility') != 'visible')
				(function(){ this.image.setStyle('visibility', 'visible'); }).delay(1, this);			
			if (this.preloader) 
				this.preloader = this.preloader.destroy();
			this.paused || this._preload();
		},

	/**
	Private method: center
		Center an image.
	*/

		_center: function(img){
			var size = img.getSize(), 
				h = size.y, w = size.x; 
			img.set('styles', {'left': (w - this.width) / -2, 'top': (h - this.height) / -2});
		},

	/**
	Private method: resize
		Resizes an image.
	*/

		_resize: function(img){
			var h = img.get('height').toFloat(), w = img.get('width').toFloat(),
				dh = this.height / h, dw = this.width / w;
			if (this.options.resize == 'fit')
				dh = dw = dh > dw ? dw 
					: dh;
			if (this.options.resize == 'fill')
				dh = dw = dh > dw ? dh 
					: dw;
			img.set('styles', {'height': Math.ceil(h * dh), 'width': Math.ceil(w * dw)});
		},

	/**
	Private method: start
		Callback on start of slide change.
	*/

		_start: function(){		
			this.fireEvent('start');
		},

	/**
	Private method: complete
		Callback on start of slide change.
	*/

		_complete: function(){
			if (this.firstrun && this.options.paused)
				this.pause(1);
			this.firstrun = false;
			this.fireEvent('complete');
		}	
	});

	/**
	Private method: captions
		Builds the optional caption element, adds interactivity.
		This method can safely be removed if the captions option is not enabled.
	*/

	var Caption = new Class({
		Implements: [Chain, Events, Options],

		options: {/*
			duration: 500,
			fps: 50,
			transition: 'sine:in:out',
			unit: false, */
			delay: 0,
			link: 'cancel'			
		},

		initialize: function(slideshow){
			if (!slideshow)
				return;
			var options = slideshow.options.captions;
			if (options === true)
				options = {};
			this.setOptions(options);
			var el = slideshow.el.getElement(slideshow.classes.get('captions')),
				caption = el ? el.dispose().empty()
					: new Element('div', {'class': slideshow.classes.get('captions').substr(1)});
			slideshow.caption = caption;
			caption.set({
		    	'aria-busy': false,
		    	'aria-hidden': false,
				'events': { 'update': this.update.bind(slideshow) },
				'morph': this.options,
				'role': 'description'
			}).store('delay', this.options.delay);
		    if (!caption.get('id'))
		    	caption.set('id', 'Slideshow-' + Date.now());
		    slideshow.el.retrieve('images').set('aria-labelledby', caption.get('id'));
			caption.inject(slideshow.el);
		},

		update: function(fast){
		    var empty = !this.data.captions[this._slide].length, timer;
			if (timer = this.caption.retrieve('timer'))
				clearTimeout(timer);
		    if (fast){
		      var p = empty ? 'hidden' 
				: 'visible';
		      this.caption.set({'aria-hidden': empty, 'html': this.data.captions[this._slide]}).get('morph').cancel().set(this.classes.get('captions', p));
		    }
		    else {
		      var fn1 = empty ? function(){} 
				: function(caption){
				this.caption.store('timer', setTimeout(function(caption){
			        this.caption.set('html', caption).morph(this.classes.get('captions', 'visible'));
				}.pass(caption, this), this.caption.retrieve('delay')));
		      }.pass(this.data.captions[this._slide], this);    
		      var fn2 = function(){ 
		        this.caption.set('aria-busy', false); 
		      }.bind(this);
		      this.caption.set('aria-busy', true).get('morph').cancel().start(this.classes.get('captions', 'hidden')).chain(fn1, fn2);
		    }
		}
	});

	/**
	Private method: controller
	  Builds the optional controller element, adds interactivity.
	  This method can safely be removed if the controller option is not enabled.
	*/

	var Controller = new Class({
		Implements: [Chain, Events, Options],

		options: {/*
			duration: 500,
			fps: 50,
			transition: 'sine:in:out',
			unit: false, */
			link: 'cancel'			
		},

		initialize: function(slideshow){
			if (!slideshow)
				return;
			var options = slideshow.options.captions;
			if (options === true)
				options = {};
			this.setOptions(options);
			var el = slideshow.el.getElement(slideshow.classes.get('controller')),
				controller = el ? el.dispose().empty()
					: new Element('div', {'class': slideshow.classes.get('controller').substr(1)});
			slideshow.controller = controller;
			controller.set({
				'aria-hidden': false, 
				'role': 'menubar'
			});
			var ul = new Element('ul', {'role': 'menu'}).inject(controller),
				i = 0;
			Object.each(slideshow.accesskeys, function(accesskey, action){
				var li = new Element('li', {
					'class': (action == 'pause' && this.options.paused) ? this.classes.play + ' ' + this.classes[action] 
						: this.classes[action]
				}).inject(ul);
				var a = this.el.retrieve(action, new Element('a', {
					'role': 'menuitem', 'tabindex': i++, 'title': accesskey.label
				}).inject(li));
				a.set('events', {
					'click': function(action){
						this[action]()
					}.pass(action, this),
					'mouseenter': function(active){
						this.addClass(active)
					}.pass(this.classes.active, a),
					'mouseleave': function(active){
						this.removeClass(active)
					}.pass(this.classes.active, a)
				});		
			}, slideshow);
			controller.set({
				'events': {
					'hide': this.hide.pass(slideshow.classes.get('controller', 'hidden'), controller),
					'show': this.show.pass(slideshow.classes.get('controller', 'visible'), controller)
				},
				'morph': this.options
			}).store('hidden', false);
			slideshow.events
				.push('keydown', this.keydown.bind(slideshow))
				.push('keyup',	this.keyup.bind(slideshow))
				.push('mousemove',	this.mousemove.bind(slideshow));
			controller.inject(slideshow.el).fireEvent('hide');
		},

		hide: function(hidden){
			if (this.get('aria-hidden') == 'false')
				this.set('aria-hidden', true).morph(hidden);
		},

		keydown: function(e){
			Object.each(this.accesskeys, function(accesskey, action){
				if (e.key == accesskey.key && e.shift == accesskey.shift && e.control == accesskey.control && e.alt == accesskey.alt){
					if (this.controller.get('aria-hidden') == 'true')
						this.controller.get('morph').set(this.classes.get('controller', 'visible'));
					this.el.retrieve(action).fireEvent('mouseenter');
				}					
			}, this);		
		},

		keyup: function(e){
			Object.each(this.accesskeys, function(accesskey, action){
				if (e.key == accesskey.key && e.shift == accesskey.shift && e.control == accesskey.control && e.alt == accesskey.alt){
					if (this.controller.get('aria-hidden') == 'true')
						this.controller.set('aria-hidden', false).fireEvent('hide'); 
					this.el.retrieve(action).fireEvent('mouseleave');
				}					
			}, this);			
		},

		mousemove: function(e){
			var images = this.el.retrieve('images').getCoordinates(),
				action = (e.page.x > images.left && e.page.x < images.right && e.page.y > images.top && e.page.y < images.bottom) ? 'show' 
					: 'hide';
			this.controller.fireEvent(action);
		},

		show: function(visible){
			if (this.get('aria-hidden') == 'true')
				this.set('aria-hidden', false).morph(visible);
		}
	});

	/**
	Private method: loader
		Builds the optional loader element, adds interactivity.
		This method can safely be removed if the loader option is not enabled.
	*/

	var Loader = new Class({
		Implements: [Chain, Events, Options],

		options: {/*
			duration: 500,
			transition: 'sine:in:out',
			unit: false, */
			fps: 20,
			link: 'cancel'			
		},

		initialize: function(slideshow){
			if (!slideshow)
				return;
			var options = slideshow.options.loader;
			if (options === true)
				options = {};
			this.setOptions(options);
			var loader = new Element('div', {
				'aria-hidden': false,
				'class': slideshow.classes.get('loader').substr(1),				
				'morph': this.options,
				'role': 'progressbar'
			}).store('animate', false).store('i', 0).store('delay', 1000 / this.options.fps).inject(slideshow.el);
			slideshow.loader = loader;
			var url = loader.getStyle('backgroundImage').replace(/url\(['"]?(.*?)['"]?\)/, '$1').trim();
			if (url){
				if (url.test(/\.png$/) && Browser.ie && Browser.version < 7)
					loader.setStyles({'backgroundImage': 'none', 'filter': 'progid:DXImageTransform.Microsoft.AlphaImageLoader(src="' + url + '", sizingMethod="crop")'});					
				new Asset.image(url, {'onload': function(){
					var size = loader.getSize(),
						width = this.get('width'), 
						height = this.get('height');
					if (width > size.x)
						loader.store('x', size.x).store('animate', 'x').store('frames', (width / size.x).toInt());
					if (height > size.y)
						loader.store('y', size.y).store('animate', 'y').store('frames', (height / size.y).toInt());
				}});
			}
			loader.set('events', {
				'animate': this.animate.bind(loader),
				'hide': this.hide.pass(slideshow.classes.get('loader', 'hidden'), loader),
				'show': this.show.pass(slideshow.classes.get('loader', 'visible'), loader)
			});
			loader.fireEvent('hide');
		},

		animate: function(){
			var animate = this.retrieve('animate');
			if (!animate)
				return;
			var i = (this.retrieve('i').toInt() + 1) % this.retrieve('frames');
			this.store('i', i);
			var n = (i * this.retrieve(animate)) + 'px';
			if (animate == 'x')
				this.setStyle('backgroundPosition', n + ' 0px');			
			if (animate == 'y')
				this.setStyle('backgroundPosition', '0px ' + n);			
		},

		hide: function(hidden){
			if (this.get('aria-hidden') == 'false'){
				this.set('aria-hidden', true).morph(hidden);
				if (this.retrieve('animate'))
					clearTimeout(this.retrieve('timer'));
			}
		},

		show: function(visible){
			if (this.get('aria-hidden') == 'true'){
				this.set('aria-hidden', false).morph(visible);
				if (this.retrieve('animate')){
					this.store('timer', function(){ 
						this.fireEvent('animate') 
					}.periodical(this.retrieve('delay'), this));			
				}
			}
		}
	});

	/**
	Private method: thumbnails
		Builds the optional thumbnails element, adds interactivity.
		This method can safely be removed if the thumbnails option is not enabled.
	*/

	var Thumbnails = new Class({
		Implements: [Chain, Events, Options],

		options: {/*
			duration: 500,
			transition: 'sine:in:out',
			unit: false, */
			columns: null,
			fps: 50,
			link: 'cancel',
			position: null,
			rows: null,
			scroll: null
		},

		initialize: function(slideshow){
			var options = (slideshow.options.thumbnails === true) ? {} 
				: slideshow.options.thumbnails;
			this.setOptions(options);
			var el = slideshow.el.getElement(slideshow.classes.get('thumbnails')),
				thumbnails = el ? el.empty() 
					: new Element('div', {'class': slideshow.classes.get('thumbnails').substr(1)});
			slideshow.thumbnails = thumbnails;
			thumbnails.set({'role': 'menubar', 'styles': {'overflow': 'hidden'}});
			var uid = thumbnails.retrieve('uid', 'Slideshow-' + Date.now()),
				ul = new Element('ul', {'role': 'menu', 'styles': {'left': 0, 'position': 'absolute', 'top': 0}, 'tween': {'link': 'cancel'}}).inject(thumbnails);
			slideshow.data.thumbnails.each(function(thumbnail, i){
				var li = new Element('li', {'id': uid + i}).inject(ul),
					a = new Element('a', {
						'class': slideshow.classes.get('thumbnails', 'hidden').substr(1),
						'events': {
							'click': this.click.pass(i, slideshow)
						},
						'href': slideshow.data.images[i],
						'morph': this.options,
						'role': 'menuitem',
						'tabindex': i
					}).store('uid', i).inject(li);
				if (slideshow.options.titles)
					a.set('title', slideshow.data.titles[i]);
				new Asset.image(thumbnail, {
					'onload': this.onload.pass(i, slideshow)
				}).inject(a);
			}, this);
			thumbnails.set('events', {
				'scroll': this.scroll.bind(thumbnails),
				'update': this.update.bind(slideshow)
			});
			var coords = thumbnails.getCoordinates();
			if (!options.scroll)
				options.scroll = (coords.height > coords.width) ? 'y' 
					: 'x';
			var props = (options.scroll == 'y') ? 'top bottom height y width'.split(' ') 
				: 'left right width x height'.split(' ');
			thumbnails.store('props', props).store('delay', 1000 / this.options.fps);
			slideshow.events.push('mousemove', this.mousemove.bind(thumbnails));
			thumbnails.inject(slideshow.el);
		},

		click: function(i){
			this.go(i); 
			return false; 
		},

		mousemove: function(e){
			var coords = this.getCoordinates();
			if (e.page.x > coords.left && e.page.x < coords.right && e.page.y > coords.top && e.page.y < coords.bottom){
				this.store('page', e.page);			
				if (!this.retrieve('mouseover')){
					this.store('mouseover', true);
					this.store('timer', function(){this.fireEvent('scroll');}.periodical(this.retrieve('delay'), this));
				}
			}
			else {
				if (this.retrieve('mouseover')){
					this.store('mouseover', false);				
					clearTimeout(this.retrieve('timer'));
				}
			}			
		},

		onload: function(i){
			var thumbnails = this.thumbnails,
				a = thumbnails.getElements('a')[i];
			if (a){
				(function(a){
					var visible = i == this.slide ? 'active' 
						: 'inactive';					
					a.store('loaded', true).get('morph').set(this.classes.get('thumbnails', 'hidden')).start(this.classes.get('thumbnails', visible));	
				}).delay(Math.max(1000 / this.data.thumbnails.length, 100), this, a);
			}					
			if (thumbnails.retrieve('limit'))
				return;
			var props = thumbnails.retrieve('props'), 
				options = this.options.thumbnails,
				pos = props[1], 
				length = props[2], 
				width = props[4],
				li = thumbnails.getElement('li:nth-child(' + (i + 1) + ')').getCoordinates();
			if (options.columns || options.rows){
				thumbnails.setStyles({'height': this.height, 'width': this.width});
				if (options.columns.toInt())
					thumbnails.setStyle('width', li.width * options.columns.toInt());
				if (options.rows.toInt())
					thumbnails.setStyle('height', li.height * options.rows.toInt());
			}
			var div = thumbnails.getCoordinates();
			if (options.position){
				if (options.position.test(/bottom|top/))
					thumbnails.setStyles({'bottom': 'auto', 'top': 'auto'}).setStyle(options.position, -div.height);
				if (options.position.test(/left|right/))
					thumbnails.setStyles({'left': 'auto', 'right': 'auto'}).setStyle(options.position, -div.width);
			}
			var units = Math.floor(div[width] / li[width]),
				x = Math.ceil(this.data.images.length / units),
				r = this.data.images.length % units,
				len = x * li[length],
				ul = thumbnails.getElement('ul').setStyle(length, len);
			ul.getElements('li').setStyles({'height': li.height, 'width': li.width});
			thumbnails.store('limit', div[length] - len);
		},

		scroll: function(n, fast){
			var div = this.getCoordinates(),
				ul = this.getElement('ul').getPosition(),
				props = this.retrieve('props'),
				axis = props[3], delta, pos = props[0], size = props[2], value,			
				tween = this.getElement('ul').set('tween', {'property': pos}).get('tween');	
			if (n != undefined){
				var uid = this.retrieve('uid'),
					li = document.id(uid + n).getCoordinates();
				delta = div[pos] + (div[size] / 2) - (li[size] / 2) - li[pos];
				value = (ul[axis] - div[pos] + delta).limit(this.retrieve('limit'), 0);
				tween[fast ? 'set' : 'start'](value);
			}
			else{
				var area = div[props[2]] / 3, 
					page = this.retrieve('page'), 
					velocity = -(this.retrieve('delay') * 0.01);			
				if (page[axis] < (div[pos] + area))
					delta = (page[axis] - div[pos] - area) * velocity;
				else if (page[axis] > (div[pos] + div[size] - area))
					delta = (page[axis] - div[pos] - div[size] + area) * velocity;			
				if (delta){			
					value = (ul[axis] - div[pos] + delta).limit(this.retrieve('limit'), 0);
					tween.set(value);
				}
			}				
		},

		update: function(fast){
			var thumbnails = this.thumbnails,
				uid = thumbnails.retrieve('uid');
			thumbnails.getElements('a').each(function(a, i){
				if (a.retrieve('loaded')){
					if (a.retrieve('uid') == this._slide){
						if (!a.retrieve('active', false)){
							a.store('active', true);
							var active = this.classes.get('thumbnails', 'active');							
							if (fast) a.get('morph').set(active);
							else a.morph(active);
						}
					} 
					else {
						if (a.retrieve('active', true)){
							a.store('active', false);
							var inactive = this.classes.get('thumbnails', 'inactive');						
							if (fast) a.get('morph').set(inactive);
							else a.morph(inactive);
						}
					}
				}
			}, this);
			if (!thumbnails.retrieve('mouseover'))
				thumbnails.fireEvent('scroll', [this._slide, fast]);
		}
	});
})();