/**
 * @preserve Galleria v 1.2.5 2011-08-03
 * http://galleria.aino.se
 *
 * Copyright (c) 2011, Aino
 * Licensed under the MIT license.
 */

/*global jQuery, navigator, Galleria:true, Image */

(function ($) {

	// some references
	var undef,
    window = this,
    doc = window.document,
    $doc = $(doc),
    $win = $(window),

	// internal constants
    VERSION = 1.25,
    DEBUG = true,
    TIMEOUT = 30000,
    DUMMY = false,
    NAV = navigator.userAgent.toLowerCase(),
    HASH = window.location.hash.replace(/#\//, ''),
    IE = (function () {

    	var v = 3,
            div = doc.createElement('div'),
            all = div.getElementsByTagName('i');

    	do {
    		div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->';
    	} while (all[0]);

    	return v > 4 ? v : undef;

    } ()),
    DOM = function () {
    	return {
    		html: doc.documentElement,
    		body: doc.body,
    		head: doc.getElementsByTagName('head')[0],
    		title: doc.title
    	};
    },

	// list of Galleria events
    _eventlist = 'data ready thumbnail loadstart loadfinish image play pause progress ' +
              'fullscreen_enter fullscreen_exit idle_enter idle_exit rescale ' +
              'lightbox_open lightbox_close lightbox_image',

    _events = (function () {

    	var evs = [];

    	$.each(_eventlist.split(' '), function (i, ev) {
    		evs.push(ev);

    		// legacy events
    		if (/_/.test(ev)) {
    			evs.push(ev.replace(/_/g, ''));
    		}
    	});

    	return evs;

    } ()),

	// legacy options
	// allows the old my_setting syntax and converts it to camel case

    _legacyOptions = function (options) {

    	var n;

    	if (typeof options !== 'object') {

    		// return whatever it was...
    		return options;
    	}

    	$.each(options, function (key, value) {
    		if (/^[a-z]+_/.test(key)) {
    			n = '';
    			$.each(key.split('_'), function (i, k) {
    				n += i > 0 ? k.substr(0, 1).toUpperCase() + k.substr(1) : k;
    			});
    			options[n] = value;
    			delete options[key];
    		}
    	});

    	return options;
    },

    _patchEvent = function (type) {

    	// allow 'image' instead of Galleria.IMAGE
    	if ($.inArray(type, _events) > -1) {
    		return Galleria[type.toUpperCase()];
    	}

    	return type;
    },

	// the internal timeouts object
	// provides helper methods for controlling timeouts
    _timeouts = {

    	trunk: {},

    	add: function (id, fn, delay, loop) {
    		loop = loop || false;
    		this.clear(id);
    		if (loop) {
    			var old = fn;
    			fn = function () {
    				old();
    				_timeouts.add(id, fn, delay);
    			};
    		}
    		this.trunk[id] = window.setTimeout(fn, delay);
    	},

    	clear: function (id) {

    		var del = function (i) {
    			window.clearTimeout(this.trunk[i]);
    			delete this.trunk[i];
    		}, i;

    		if (!!id && id in this.trunk) {
    			del.call(_timeouts, id);

    		} else if (typeof id === 'undefined') {
    			for (i in this.trunk) {
    				if (this.trunk.hasOwnProperty(i)) {
    					del.call(_timeouts, i);
    				}
    			}
    		}
    	}
    },

	// the internal gallery holder
    _galleries = [],

	// the internal instance holder
    _instances = [],

	// flag for errors
    _hasError = false,

	// canvas holder
    _canvas = false,

	// instance pool, holds the galleries until themeLoad is triggered
    _pool = [],

	// themeLoad trigger
    _themeLoad = function (theme) {
    	Galleria.theme = theme;

    	// run the instances we have in the pool
    	$.each(_pool, function (i, instance) {
    		instance._init.call(instance);
    	});
    },

	// the Utils singleton
    Utils = (function () {

    	return {

    		array: function (obj) {
    			return Array.prototype.slice.call(obj, 0);
    		},

    		create: function (className, nodeName) {
    			nodeName = nodeName || 'div';
    			var elem = doc.createElement(nodeName);
    			elem.className = className;
    			return elem;
    		},

    		getScriptPath: function (src) {

    			// the currently executing script is always the last
    			src = src || $('script:last').attr('src');
    			var slices = src.split('/');

    			if (slices.length == 1) {
    				return '';
    			}

    			slices.pop();

    			return slices.join('/') + '/';
    		},

    		// CSS3 transitions, added in 1.2.4
    		animate: (function () {

    			// detect transition
    			var transition = (function (style) {
    				var props = 'transition WebkitTransition MozTransition OTransition'.split(' '),
                        i;

    				// disable css3 animations in opera until stable
    				if (window.opera) {
    					return false;
    				}

    				for (i = 0; props[i]; i++) {
    					if (typeof style[props[i]] !== 'undefined') {
    						return props[i];
    					}
    				}
    				return false;
    			} ((doc.body || doc.documentElement).style));

    			// map transitionend event
    			var endEvent = {
    				MozTransition: 'transitionend',
    				OTransition: 'oTransitionEnd',
    				WebkitTransition: 'webkitTransitionEnd',
    				transition: 'transitionend'
    			}[transition];

    			// map bezier easing conversions
    			var easings = {
    				_default: [0.25, 0.1, 0.25, 1],
    				galleria: [0.645, 0.045, 0.355, 1],
    				galleriaIn: [0.55, 0.085, 0.68, 0.53],
    				galleriaOut: [0.25, 0.46, 0.45, 0.94],
    				ease: [0.25, 0, 0.25, 1],
    				linear: [0.25, 0.25, 0.75, 0.75],
    				'ease-in': [0.42, 0, 1, 1],
    				'ease-out': [0, 0, 0.58, 1],
    				'ease-in-out': [0.42, 0, 0.58, 1]
    			};

    			// function for setting transition css for all browsers
    			var setStyle = function (elem, value, suffix) {
    				var css = {};
    				suffix = suffix || 'transition';
    				$.each('webkit moz ms o'.split(' '), function () {
    					css['-' + this + '-' + suffix] = value;
    				});
    				elem.css(css);
    			};

    			// clear styles
    			var clearStyle = function (elem) {
    				setStyle(elem, 'none', 'transition');
    				if (Galleria.WEBKIT && Galleria.TOUCH) {
    					setStyle(elem, 'translate3d(0,0,0)', 'transform');
    					if (elem.data('revert')) {
    						elem.css(elem.data('revert'));
    						elem.data('revert', null);
    					}
    				}
    			};

    			// various variables
    			var change, strings, easing, syntax, revert, form, css;

    			// the actual animation method
    			return function (elem, to, options) {

    				// extend defaults
    				options = $.extend({
    					duration: 400,
    					complete: function () { },
    					stop: false
    				}, options);

    				// cache jQuery instance
    				elem = $(elem);

    				if (!options.duration) {
    					elem.css(to);
    					options.complete.call(elem[0]);
    					return;
    				}

    				// fallback to jQuery's animate if transition is not supported
    				if (!transition) {
    					elem.animate(to, options);
    					return;
    				}

    				// stop
    				if (options.stop) {
    					// clear the animation
    					elem.unbind(endEvent);
    					clearStyle(elem);
    				}

    				// see if there is a change
    				change = false;
    				$.each(to, function (key, val) {
    					css = elem.css(key);
    					if (Utils.parseValue(css) != Utils.parseValue(val)) {
    						change = true;
    					}
    					// also add computed styles for FF
    					elem.css(key, css);
    				});
    				if (!change) {
    					window.setTimeout(function () {
    						options.complete.call(elem[0]);
    					}, options.duration);
    					return;
    				}

    				// the css strings to be applied
    				strings = [];

    				// the easing bezier
    				easing = options.easing in easings ? easings[options.easing] : easings._default;

    				// the syntax
    				syntax = ' ' + options.duration + 'ms' + ' cubic-bezier(' + easing.join(',') + ')';

    				// add a tiny timeout so that the browsers catches any css changes before animating
    				window.setTimeout(function () {

    					// attach the end event
    					elem.one(endEvent, (function (elem) {
    						return function () {
    							// clear the animation
    							clearStyle(elem);

    							// run the complete method
    							options.complete.call(elem[0]);
    						};
    					} (elem)));

    					// do the webkit translate3d for better performance on iOS
    					if (Galleria.WEBKIT && Galleria.TOUCH) {

    						revert = {};
    						form = [0, 0, 0];

    						$.each(['left', 'top'], function (i, m) {
    							if (m in to) {
    								form[i] = (Utils.parseValue(to[m]) - Utils.parseValue(elem.css(m))) + 'px';
    								revert[m] = to[m];
    								delete to[m];
    							}
    						});

    						if (form[0] || form[1]) {

    							elem.data('revert', revert);

    							strings.push('-webkit-transform' + syntax);

    							// 3d animate
    							setStyle(elem, 'translate3d(' + form.join(',') + ')', 'transform');
    						}
    					}

    					// push the animation props
    					$.each(to, function (p, val) {
    						strings.push(p + syntax);
    					});

    					// set the animation styles
    					setStyle(elem, strings.join(','));

    					// animate
    					elem.css(to);

    				}, 1);
    			};
    		} ()),

    		removeAlpha: function (elem) {
    			if (IE < 9 && elem) {

    				var style = elem.style,
                        currentStyle = elem.currentStyle,
                        filter = currentStyle && currentStyle.filter || style.filter || "";

    				if (/alpha/.test(filter)) {
    					style.filter = filter.replace(/alpha\([^)]*\)/i, '');
    				}
    			}
    		},

    		forceStyles: function (elem, styles) {
    			elem = $(elem);
    			if (elem.attr('style')) {
    				elem.data('styles', elem.attr('style')).removeAttr('style');
    			}
    			elem.css(styles);
    		},

    		revertStyles: function () {
    			$.each(Utils.array(arguments), function (i, elem) {

    				elem = $(elem);
    				elem.removeAttr('style');

    				elem.attr('style', ''); // "fixes" webkit bug

    				if (elem.data('styles')) {
    					elem.attr('style', elem.data('styles')).data('styles', null);
    				}
    			});
    		},

    		moveOut: function (elem) {
    			Utils.forceStyles(elem, {
    				position: 'absolute',
    				left: -10000
    			});
    		},

    		moveIn: function () {
    			Utils.revertStyles.apply(Utils, Utils.array(arguments));
    		},

    		elem: function (elem) {
    			if (elem instanceof $) {
    				return {
    					$: elem,
    					dom: elem[0]
    				};
    			} else {
    				return {
    					$: $(elem),
    					dom: elem
    				};
    			}
    		},

    		hide: function (elem, speed, callback) {

    			callback = callback || function () { };

    			var el = Utils.elem(elem),
                    $elem = el.$;

    			elem = el.dom;

    			// save the value if not exist
    			if (!$elem.data('opacity')) {
    				$elem.data('opacity', $elem.css('opacity'));
    			}

    			// always hide
    			var style = { opacity: 0 };

    			if (speed) {

    				var complete = IE < 9 && elem ? function () {
    					Utils.removeAlpha(elem);
    					elem.style.visibility = 'hidden';
    					callback.call(elem);
    				} : callback;

    				Utils.animate(elem, style, {
    					duration: speed,
    					complete: complete,
    					stop: true
    				});
    			} else {
    				if (IE < 9 && elem) {
    					Utils.removeAlpha(elem);
    					elem.style.visibility = 'hidden';
    				} else {
    					$elem.css(style);
    				}
    			}
    		},

    		show: function (elem, speed, callback) {

    			callback = callback || function () { };

    			var el = Utils.elem(elem),
                    $elem = el.$;

    			elem = el.dom;

    			// bring back saved opacity
    			var saved = parseFloat($elem.data('opacity')) || 1,
                    style = { opacity: saved };

    			// animate or toggle
    			if (speed) {

    				if (IE < 9) {
    					$elem.css('opacity', 0);
    					elem.style.visibility = 'visible';
    				}

    				var complete = IE < 9 && elem ? function () {
    					if (style.opacity == 1) {
    						Utils.removeAlpha(elem);
    					}
    					callback.call(elem);
    				} : callback;

    				Utils.animate(elem, style, {
    					duration: speed,
    					complete: complete,
    					stop: true
    				});
    			} else {
    				if (IE < 9 && style.opacity == 1 && elem) {
    					Utils.removeAlpha(elem);
    					elem.style.visibility = 'visible';
    				} else {
    					$elem.css(style);
    				}
    			}
    		},


    		// enhanced click for mobile devices
    		// we bind a touchend and hijack any click event in the bubble
    		// then we execute the click directly and save it in a separate data object for later
    		optimizeTouch: (function () {

    			var node,
                    evs,
                    fakes,
                    travel,
                    evt = {},
                    handler = function (e) {
                    	e.preventDefault();
                    	evt = $.extend({}, e, true);
                    },
                    attach = function () {
                    	this.evt = evt;
                    },
                    fake = function () {
                    	this.handler.call(node, this.evt);
                    };

    			return function (elem) {

    				$(elem).bind('touchend', function (e) {

    					node = e.target;
    					travel = true;

    					while (node.parentNode && node != e.currentTarget && travel) {

    						evs = $(node).data('events');
    						fakes = $(node).data('fakes');

    						if (evs && 'click' in evs) {

    							travel = false;
    							e.preventDefault();

    							// fake the click and save the event object
    							$(node).click(handler).click();

    							// remove the faked click
    							evs.click.pop();

    							// attach the faked event
    							$.each(evs.click, attach);

    							// save the faked clicks in a new data object
    							$(node).data('fakes', evs.click);

    							// remove all clicks
    							delete evs.click;

    						} else if (fakes) {

    							travel = false;
    							e.preventDefault();

    							// fake all clicks
    							$.each(fakes, fake);
    						}

    						// bubble
    						node = node.parentNode;
    					}
    				});
    			};
    		} ()),

    		addTimer: function () {
    			_timeouts.add.apply(_timeouts, Utils.array(arguments));
    			return this;
    		},

    		clearTimer: function () {
    			_timeouts.clear.apply(_timeouts, Utils.array(arguments));
    			return this;
    		},

    		wait: function (options) {
    			options = $.extend({
    				until: function () { return false; },
    				success: function () { },
    				error: function () { Galleria.raise('Could not complete wait function.'); },
    				timeout: 3000
    			}, options);

    			var start = Utils.timestamp(),
                    elapsed,
                    now,
                    fn = function () {
                    	now = Utils.timestamp();
                    	elapsed = now - start;
                    	if (options.until(elapsed)) {
                    		options.success();
                    		return false;
                    	}

                    	if (now >= start + options.timeout) {
                    		options.error();
                    		return false;
                    	}
                    	window.setTimeout(fn, 10);
                    };

    			window.setTimeout(fn, 10);
    		},

    		toggleQuality: function (img, force) {

    			if ((IE !== 7 && IE !== 8) || !img) {
    				return;
    			}

    			if (typeof force === 'undefined') {
    				force = img.style.msInterpolationMode === 'nearest-neighbor';
    			}

    			img.style.msInterpolationMode = force ? 'bicubic' : 'nearest-neighbor';
    		},

    		insertStyleTag: function (styles) {
    			var style = doc.createElement('style');
    			DOM().head.appendChild(style);

    			if (style.styleSheet) { // IE
    				style.styleSheet.cssText = styles;
    			} else {
    				var cssText = doc.createTextNode(styles);
    				style.appendChild(cssText);
    			}
    		},

    		// a loadscript method that works for local scripts
    		loadScript: function (url, callback) {

    			var done = false,
                    script = $('<scr' + 'ipt>').attr({
                    	src: url,
                    	async: true
                    }).get(0);

    			// Attach handlers for all browsers
    			script.onload = script.onreadystatechange = function () {
    				if (!done && (!this.readyState ||
                       this.readyState === 'loaded' || this.readyState === 'complete')) {

    					done = true;

    					// Handle memory leak in IE
    					script.onload = script.onreadystatechange = null;

    					if (typeof callback === 'function') {
    						callback.call(this, this);
    					}
    				}
    			};

    			DOM().head.appendChild(script);
    		},

    		// parse anything into a number
    		parseValue: function (val) {
    			if (typeof val === 'number') {
    				return val;
    			} else if (typeof val === 'string') {
    				var arr = val.match(/\-?\d|\./g);
    				return arr && arr.constructor === Array ? arr.join('') * 1 : 0;
    			} else {
    				return 0;
    			}
    		},

    		// timestamp abstraction
    		timestamp: function () {
    			return new Date().getTime();
    		},

    		// this is pretty crap, but works for now
    		// it will add a callback, but it can't guarantee that the styles can be fetched
    		// using getComputedStyle further checking needed, possibly a dummy element
    		loadCSS: function (href, id, callback) {

    			var link,
                    ready = false,
                    length;

    			// look for manual css
    			$('link[rel=stylesheet]').each(function () {
    				if (new RegExp(href).test(this.href)) {
    					link = this;
    					return false;
    				}
    			});

    			if (typeof id === 'function') {
    				callback = id;
    				id = undef;
    			}

    			callback = callback || function () { }; // dirty

    			// if already present, return
    			if (link) {
    				callback.call(link, link);
    				return link;
    			}

    			// save the length of stylesheets to check against
    			length = doc.styleSheets.length;

    			// add timestamp if DEBUG is true
    			if (DEBUG) {
    				href += '?' + Utils.timestamp();
    			}

    			// check for existing id
    			if ($('#' + id).length) {
    				$('#' + id).attr('href', href);
    				length--;
    				ready = true;
    			} else {
    				link = $('<link>').attr({
    					rel: 'stylesheet',
    					href: href,
    					id: id
    				}).get(0);

    				window.setTimeout(function () {
    					var styles = $('link[rel="stylesheet"], style');
    					if (styles.length) {
    						styles.get(0).parentNode.insertBefore(link, styles[0]);
    					} else {
    						DOM().head.appendChild(link);
    					}

    					if (IE) {

    						// IE has a limit of 31 stylesheets in one document
    						if (length >= 31) {
    							Galleria.raise('You have reached the browser stylesheet limit (31)', true);
    							return;
    						}

    						// todo: test if IE really needs the readyState
    						link.onreadystatechange = function (e) {
    							if (!ready && (!this.readyState ||
                                    this.readyState === 'loaded' || this.readyState === 'complete')) {
    								ready = true;
    							}
    						};
    					} else {
    						// final test via ajax if not local
    						if (!(new RegExp('file://', 'i').test(href))) {
    							$.ajax({
    								url: href,
    								success: function () {
    									ready = true;
    								},
    								error: function (e) {
    									// pass if origin is rejected in chrome for some reason
    									if (e.isRejected() && Galleria.WEBKIT) {
    										ready = true;
    									}
    								}
    							});
    						} else {
    							ready = true;
    						}
    					}
    				}, 10);
    			}

    			if (typeof callback === 'function') {

    				Utils.wait({
    					until: function () {
    						return ready && doc.styleSheets.length > length;
    					},
    					success: function () {
    						window.setTimeout(function () {
    							callback.call(link, link);
    						}, 100);
    					},
    					error: function () {
    						Galleria.raise('Theme CSS could not load', true);
    					},
    					timeout: 10000
    				});
    			}
    			return link;
    		}
    	};
    } ()),

	// the transitions holder
    _transitions = (function () {

    	var _slide = function (params, complete, fade, door) {

    		var easing = this.getOptions('easing'),
                distance = this.getStageWidth(),
                from = { left: distance * (params.rewind ? -1 : 1) },
                to = { left: 0 };

    		if (fade) {
    			from.opacity = 0;
    			to.opacity = 1;
    		}

    		$(params.next).css(from);

    		Utils.animate(params.next, to, {
    			duration: params.speed,
    			complete: (function (elems) {
    				return function () {
    					complete();
    					elems.css({
    						left: 0
    					});
    				};
    			} ($(params.next).add(params.prev))),
    			queue: false,
    			easing: easing
    		});

    		if (door) {
    			params.rewind = !params.rewind;
    		}

    		if (params.prev) {

    			from = { left: 0 };
    			to = { left: distance * (params.rewind ? 1 : -1) };

    			if (fade) {
    				from.opacity = 1;
    				to.opacity = 0;
    			}

    			$(params.prev).css(from);
    			Utils.animate(params.prev, to, {
    				duration: params.speed,
    				queue: false,
    				easing: easing,
    				complete: function () {
    					$(this).css('opacity', 0);
    				}
    			});
    		}
    	};

    	return {

    		fade: function (params, complete) {
    			$(params.next).css('opacity', 0).show();
    			Utils.animate(params.next, {
    				opacity: 1
    			}, {
    				duration: params.speed,
    				complete: complete
    			});
    			if (params.prev) {
    				$(params.prev).css('opacity', 1).show();
    				Utils.animate(params.prev, {
    					opacity: 0
    				}, {
    					duration: params.speed
    				});
    			}
    		},

    		flash: function (params, complete) {
    			$(params.next).css('opacity', 0);
    			if (params.prev) {
    				Utils.animate(params.prev, {
    					opacity: 0
    				}, {
    					duration: params.speed / 2,
    					complete: function () {
    						Utils.animate(params.next, {
    							opacity: 1
    						}, {
    							duration: params.speed,
    							complete: complete
    						});
    					}
    				});
    			} else {
    				Utils.animate(params.next, {
    					opacity: 1
    				}, {
    					duration: params.speed,
    					complete: complete
    				});
    			}
    		},

    		pulse: function (params, complete) {
    			if (params.prev) {
    				$(params.prev).hide();
    			}
    			$(params.next).css('opacity', 0).show();
    			Utils.animate(params.next, {
    				opacity: 1
    			}, {
    				duration: params.speed,
    				complete: complete
    			});
    		},

    		slide: function (params, complete) {
    			_slide.apply(this, Utils.array(arguments));
    		},

    		fadeslide: function (params, complete) {
    			_slide.apply(this, Utils.array(arguments).concat([true]));
    		},

    		doorslide: function (params, complete) {
    			_slide.apply(this, Utils.array(arguments).concat([false, true]));
    		}
    	};
    } ());

	/**
	The main Galleria class

	@class
	@constructor

	@example var gallery = new Galleria();

	@author http://aino.se

	@requires jQuery

	*/

	Galleria = function () {

		var self = this;

		// the theme used
		this._theme = undef;

		// internal options
		this._options = {};

		// flag for controlling play/pause
		this._playing = false;

		// internal interval for slideshow
		this._playtime = 5000;

		// internal variable for the currently active image
		this._active = null;

		// the internal queue, arrayified
		this._queue = { length: 0 };

		// the internal data array
		this._data = [];

		// the internal dom collection
		this._dom = {};

		// the internal thumbnails array
		this._thumbnails = [];

		// the internal layers array
		this._layers = [];

		// internal init flag
		this._initialized = false;

		// internal firstrun flag
		this._firstrun = false;

		// global stagewidth/height
		this._stageWidth = 0;
		this._stageHeight = 0;

		// target holder
		this._target = undef;

		// instance id
		this._id = Math.random();

		// add some elements
		var divs = 'container stage images image-nav image-nav-left image-nav-right ' +
                'info info-text info-title info-description ' +
                'thumbnails thumbnails-list thumbnails-container thumb-nav-left thumb-nav-right ' +
                'loader counter tooltip',
        spans = 'current total';

		$.each(divs.split(' '), function (i, elemId) {
			self._dom[elemId] = Utils.create('galleria-' + elemId);
		});

		$.each(spans.split(' '), function (i, elemId) {
			self._dom[elemId] = Utils.create('galleria-' + elemId, 'span');
		});

		// the internal keyboard object
		// keeps reference of the keybinds and provides helper methods for binding keys
		var keyboard = this._keyboard = {

			keys: {
				'UP': 38,
				'DOWN': 40,
				'LEFT': 37,
				'RIGHT': 39,
				'RETURN': 13,
				'ESCAPE': 27,
				'BACKSPACE': 8,
				'SPACE': 32
			},

			map: {},

			bound: false,

			press: function (e) {
				var key = e.keyCode || e.which;
				if (key in keyboard.map && typeof keyboard.map[key] === 'function') {
					keyboard.map[key].call(self, e);
				}
			},

			attach: function (map) {

				var key, up;

				for (key in map) {
					if (map.hasOwnProperty(key)) {
						up = key.toUpperCase();
						if (up in keyboard.keys) {
							keyboard.map[keyboard.keys[up]] = map[key];
						} else {
							keyboard.map[up] = map[key];
						}
					}
				}
				if (!keyboard.bound) {
					keyboard.bound = true;
					$doc.bind('keydown', keyboard.press);
				}
			},

			detach: function () {
				keyboard.bound = false;
				keyboard.map = {};
				$doc.unbind('keydown', keyboard.press);
			}
		};

		// internal controls for keeping track of active / inactive images
		var controls = this._controls = {

			0: undef,

			1: undef,

			active: 0,

			swap: function () {
				controls.active = controls.active ? 0 : 1;
			},

			getActive: function () {
				return controls[controls.active];
			},

			getNext: function () {
				return controls[1 - controls.active];
			}
		};

		// internal carousel object
		var carousel = this._carousel = {

			// shortcuts
			next: self.$('thumb-nav-right'),
			prev: self.$('thumb-nav-left'),

			// cache the width
			width: 0,

			// track the current position
			current: 0,

			// cache max value
			max: 0,

			// save all hooks for each width in an array
			hooks: [],

			// update the carousel
			// you can run this method anytime, f.ex on window.resize
			update: function () {
				var w = 0,
                h = 0,
                hooks = [0];

				$.each(self._thumbnails, function (i, thumb) {
					if (thumb.ready) {
						w += thumb.outerWidth || $(thumb.container).outerWidth(true);
						hooks[i + 1] = w;
						h = Math.max(h, thumb.outerHeight || $(thumb.container).outerHeight(true));
					}
				});

				self.$('thumbnails').css({
					width: w,
					height: h
				});

				carousel.max = w;
				carousel.hooks = hooks;
				carousel.width = self.$('thumbnails-list').width();
				carousel.setClasses();

				self.$('thumbnails-container').toggleClass('galleria-carousel', w > carousel.width);

				// one extra calculation
				carousel.width = self.$('thumbnails-list').width();

				// todo: fix so the carousel moves to the left
			},

			bindControls: function () {

				var i;

				carousel.next.bind('click', function (e) {
					e.preventDefault();

					if (self._options.carouselSteps === 'auto') {

						for (i = carousel.current; i < carousel.hooks.length; i++) {
							if (carousel.hooks[i] - carousel.hooks[carousel.current] > carousel.width) {
								carousel.set(i - 2);
								break;
							}
						}

					} else {
						carousel.set(carousel.current + self._options.carouselSteps);
					}
				});

				carousel.prev.bind('click', function (e) {
					e.preventDefault();

					if (self._options.carouselSteps === 'auto') {

						for (i = carousel.current; i >= 0; i--) {
							if (carousel.hooks[carousel.current] - carousel.hooks[i] > carousel.width) {
								carousel.set(i + 2);
								break;
							} else if (i === 0) {
								carousel.set(0);
								break;
							}
						}
					} else {
						carousel.set(carousel.current - self._options.carouselSteps);
					}
				});
			},

			// calculate and set positions
			set: function (i) {
				i = Math.max(i, 0);
				while (carousel.hooks[i - 1] + carousel.width >= carousel.max && i >= 0) {
					i--;
				}
				carousel.current = i;
				carousel.animate();
			},

			// get the last position
			getLast: function (i) {
				return (i || carousel.current) - 1;
			},

			// follow the active image
			follow: function (i) {

				//don't follow if position fits
				if (i === 0 || i === carousel.hooks.length - 2) {
					carousel.set(i);
					return;
				}

				// calculate last position
				var last = carousel.current;
				while (carousel.hooks[last] - carousel.hooks[carousel.current] <
                   carousel.width && last <= carousel.hooks.length) {
					last++;
				}

				// set position
				if (i - 1 < carousel.current) {
					carousel.set(i - 1);
				} else if (i + 2 > last) {
					carousel.set(i - last + carousel.current + 2);
				}
			},

			// helper for setting disabled classes
			setClasses: function () {
				carousel.prev.toggleClass('disabled', !carousel.current);
				carousel.next.toggleClass('disabled', carousel.hooks[carousel.current] + carousel.width >= carousel.max);
			},

			// the animation method
			animate: function (to) {
				carousel.setClasses();
				var num = carousel.hooks[carousel.current] * -1;

				if (isNaN(num)) {
					return;
				}

				Utils.animate(self.get('thumbnails'), {
					left: num
				}, {
					duration: self._options.carouselSpeed,
					easing: self._options.easing,
					queue: false
				});
			}
		};

		// tooltip control
		// added in 1.2
		var tooltip = this._tooltip = {

			initialized: false,

			open: false,

			init: function () {

				tooltip.initialized = true;

				var css = '.galleria-tooltip{padding:3px 8px;max-width:50%;background:#ffe;color:#000;z-index:3;position:absolute;font-size:11px;line-height:1.3' +
                      'opacity:0;box-shadow:0 0 2px rgba(0,0,0,.4);-moz-box-shadow:0 0 2px rgba(0,0,0,.4);-webkit-box-shadow:0 0 2px rgba(0,0,0,.4);}';

				Utils.insertStyleTag(css);

				self.$('tooltip').css('opacity', 0.8);
				Utils.hide(self.get('tooltip'));

			},

			// move handler
			move: function (e) {
				var mouseX = self.getMousePosition(e).x,
                mouseY = self.getMousePosition(e).y,
                $elem = self.$('tooltip'),
                x = mouseX,
                y = mouseY,
                height = $elem.outerHeight(true) + 1,
                width = $elem.outerWidth(true),
                limitY = height + 15;

				var maxX = self.$('container').width() - width - 2,
                maxY = self.$('container').height() - height - 2;

				if (!isNaN(x) && !isNaN(y)) {

					x += 10;
					y -= 30;

					x = Math.max(0, Math.min(maxX, x));
					y = Math.max(0, Math.min(maxY, y));

					if (mouseY < limitY) {
						y = limitY;
					}

					$elem.css({ left: x, top: y });
				}
			},

			// bind elements to the tooltip
			// you can bind multiple elementIDs using { elemID : function } or { elemID : string }
			// you can also bind single DOM elements using bind(elem, string)
			bind: function (elem, value) {

				// todo: revise if alternative tooltip is needed for mobile devices
				if (Galleria.TOUCH) {
					return;
				}

				if (!tooltip.initialized) {
					tooltip.init();
				}

				var hover = function (elem, value) {

					tooltip.define(elem, value);

					$(elem).hover(function () {

						Utils.clearTimer('switch_tooltip');
						self.$('container').unbind('mousemove', tooltip.move).bind('mousemove', tooltip.move).trigger('mousemove');
						tooltip.show(elem);

						Galleria.utils.addTimer('tooltip', function () {
							self.$('tooltip').stop().show().animate({
								opacity: 1
							});
							tooltip.open = true;

						}, tooltip.open ? 0 : 500);

					}, function () {

						self.$('container').unbind('mousemove', tooltip.move);
						Utils.clearTimer('tooltip');

						self.$('tooltip').stop().animate({
							opacity: 0
						}, 200, function () {

							self.$('tooltip').hide();

							Utils.addTimer('switch_tooltip', function () {
								tooltip.open = false;
							}, 1000);
						});
					});
				};

				if (typeof value === 'string') {
					hover((elem in self._dom ? self.get(elem) : elem), value);
				} else {
					// asume elemID here
					$.each(elem, function (elemID, val) {
						hover(self.get(elemID), val);
					});
				}
			},

			show: function (elem) {

				elem = $(elem in self._dom ? self.get(elem) : elem);

				var text = elem.data('tt'),
                mouseup = function (e) {

                	// attach a tiny settimeout to make sure the new tooltip is filled
                	window.setTimeout((function (ev) {
                		return function () {
                			tooltip.move(ev);
                		};
                	} (e)), 10);

                	elem.unbind('mouseup', mouseup);

                };

				text = typeof text === 'function' ? text() : text;

				if (!text) {
					return;
				}

				self.$('tooltip').html(text.replace(/\s/, '&nbsp;'));

				// trigger mousemove on mouseup in case of click
				elem.bind('mouseup', mouseup);
			},

			define: function (elem, value) {

				// we store functions, not strings
				if (typeof value !== 'function') {
					var s = value;
					value = function () {
						return s;
					};
				}

				elem = $(elem in self._dom ? self.get(elem) : elem).data('tt', value);

				tooltip.show(elem);

			}
		};

		// internal fullscreen control
		// added in 1.195
		// still kind of experimental
		var fullscreen = this._fullscreen = {

			scrolled: 0,

			crop: self._options.imageCrop,

			active: false,

			keymap: self._keyboard.map,

			enter: function (callback) {

				fullscreen.active = true;

				// hide the image until rescale is complete
				Utils.hide(self.getActiveImage());

				self.$('container').addClass('fullscreen');

				fullscreen.scrolled = $win.scrollTop();

				// begin styleforce
				Utils.forceStyles(self.get('container'), {
					position: 'fixed',
					top: 0,
					left: 0,
					width: '100%',
					height: '100%',
					zIndex: 10000
				});

				var htmlbody = {
					height: '100%',
					overflow: 'hidden',
					margin: 0,
					padding: 0
				},

                data = self.getData();

				Utils.forceStyles(DOM().html, htmlbody);
				Utils.forceStyles(DOM().body, htmlbody);

				// temporarily attach some keys
				// save the old ones first in a cloned object
				fullscreen.keymap = $.extend({}, self._keyboard.map);

				self.attachKeyboard({
					escape: self.exitFullscreen,
					right: self.next,
					left: self.prev
				});

				if (self._options.fullscreenCrop !== undef) {
					self._options.imageCrop = self._options.fullscreenCrop;
				}

				// swap to big image if it's different from the display image

				if (data && data.big && data.image !== data.big) {

					//SETFIX (vymaze cache cimz vynuti reload)

					var big = new Galleria.Picture(),
                    cached = big.isCached(data.big, true),
                    index = self.getIndex(),
                    thumb = self._thumbnails[index];

					self.trigger({
						type: Galleria.LOADSTART,
						cached: cached,
						rewind: false,
						index: index,
						imageTarget: self.getActiveImage(),
						thumbTarget: thumb
					});

					big.load(data.big, function (big) {
						self._scaleImage(big, {
							complete: function (big) {
								self.trigger({
									type: Galleria.LOADFINISH,
									cached: cached,
									index: index,
									rewind: false,
									imageTarget: big.image,
									thumbTarget: thumb
								});
								var image = self._controls.getActive().image;
								if (image) {
									$(image).width(big.image.width).height(big.image.height)
                                    .attr('style', $(big.image).attr('style'))
                                    .attr('src', big.image.src);
								}
							}
						});
					});
				}

				// init the first rescale and attach callbacks
				self.rescale(function () {

					Utils.addTimer('fullscreen_enter', function () {
						// show the image after 50 ms
						Utils.show(self.getActiveImage());

						if (typeof callback === 'function') {
							callback.call(self);
						}

					}, 100);

					self.trigger(Galleria.FULLSCREEN_ENTER);
				});

				// bind the scaling to the resize event
				$win.resize(function () {
					fullscreen.scale();
				});
			},

			scale: function () {
				self.rescale();
			},

			exit: function (callback) {

				fullscreen.active = false;

				Utils.hide(self.getActiveImage());

				self.$('container').removeClass('fullscreen');

				// revert all styles
				Utils.revertStyles(self.get('container'), DOM().html, DOM().body);

				// scroll back
				window.scrollTo(0, fullscreen.scrolled);

				// detach all keyboard events and apply the old keymap
				self.detachKeyboard();
				self.attachKeyboard(fullscreen.keymap);

				if (self._options.fullscreenCrop !== undef) {
					self._options.imageCrop = fullscreen.crop;
				}

				self.rescale(function () {
					Utils.addTimer('fullscreen_exit', function () {

						// show the image after 50 ms
						Utils.show(self.getActiveImage());

						if (typeof callback === 'function') {
							callback.call(self);
						}

					}, 50);

					self.trigger(Galleria.FULLSCREEN_EXIT);
				});

				$win.unbind('resize', fullscreen.scale);
			}
		};

		// the internal idle object for controlling idle states
		var idle = this._idle = {

			trunk: [],

			bound: false,

			add: function (elem, to) {
				if (!elem) {
					return;
				}
				if (!idle.bound) {
					idle.addEvent();
				}
				elem = $(elem);

				var from = {},
                style;

				for (style in to) {
					if (to.hasOwnProperty(style)) {
						from[style] = elem.css(style);
					}
				}
				elem.data('idle', {
					from: from,
					to: to,
					complete: true,
					busy: false
				});
				idle.addTimer();
				idle.trunk.push(elem);
			},

			remove: function (elem) {

				elem = jQuery(elem);

				$.each(idle.trunk, function (i, el) {
					if (el.length && !el.not(elem).length) {
						self._idle.show(elem);
						self._idle.trunk.splice(i, 1);
					}
				});

				if (!idle.trunk.length) {
					idle.removeEvent();
					Utils.clearTimer('idle');
				}
			},

			addEvent: function () {
				idle.bound = true;
				self.$('container').bind('mousemove click', idle.showAll);
			},

			removeEvent: function () {
				idle.bound = false;
				self.$('container').unbind('mousemove click', idle.showAll);
			},

			addTimer: function () {
				Utils.addTimer('idle', function () {
					self._idle.hide();
				}, self._options.idleTime);
			},

			hide: function () {

				if (!self._options.idleMode) {
					return;
				}

				self.trigger(Galleria.IDLE_ENTER);

				$.each(idle.trunk, function (i, elem) {

					var data = elem.data('idle');

					if (!data) {
						return;
					}

					elem.data('idle').complete = false;

					Utils.animate(elem, data.to, {
						duration: self._options.idleSpeed
					});
				});
			},

			showAll: function () {

				Utils.clearTimer('idle');

				$.each(self._idle.trunk, function (i, elem) {
					self._idle.show(elem);
				});
			},

			show: function (elem) {

				var data = elem.data('idle');

				if (!data.busy && !data.complete) {

					data.busy = true;

					self.trigger(Galleria.IDLE_EXIT);

					Utils.clearTimer('idle');

					Utils.animate(elem, data.from, {
						duration: self._options.idleSpeed / 2,
						complete: function () {
							$(this).data('idle').busy = false;
							$(this).data('idle').complete = true;
						}
					});

				}
				idle.addTimer();
			}
		};

		// internal lightbox object
		// creates a predesigned lightbox for simple popups of images in galleria
		var lightbox = this._lightbox = {

			width: 0,

			height: 0,

			initialized: false,

			active: null,

			image: null,

			elems: {},

			keymap: false,

			init: function () {

				// trigger the event
				self.trigger(Galleria.LIGHTBOX_OPEN);

				if (lightbox.initialized) {
					return;
				}
				lightbox.initialized = true;

				// create some elements to work with
				var elems = 'overlay box content shadow title info close prevholder prev nextholder next counter image',
                el = {},
                op = self._options,
                css = '',
                abs = 'position:absolute;',
                prefix = 'lightbox-',
                cssMap = {
                	overlay: 'position:fixed;display:none;opacity:' + op.overlayOpacity + ';filter:alpha(opacity=' + (op.overlayOpacity * 100) +
                                ');top:0;left:0;width:100%;height:100%;background:' + op.overlayBackground + ';z-index:99990',
                	box: 'position:fixed;display:none;width:400px;height:400px;top:50%;left:50%;margin-top:-200px;margin-left:-200px;z-index:99991',
                	shadow: abs + 'background:#000;width:100%;height:100%;',
                	content: abs + 'background-color:#fff;top:10px;left:10px;right:10px;bottom:10px;overflow:hidden',
                	info: abs + 'bottom:10px;left:10px;right:10px;color:#444;font:11px/13px arial,sans-serif;height:13px',
                	close: abs + 'top:10px;right:10px;height:20px;width:20px;background:#fff;text-align:center;cursor:pointer;color:#444;font:16px/22px arial,sans-serif;z-index:99999',
                	image: abs + 'top:10px;left:10px;right:10px;bottom:30px;overflow:hidden;display:block;',
                	prevholder: abs + 'width:50%;top:0;bottom:40px;cursor:pointer;',
                	nextholder: abs + 'width:50%;top:0;bottom:40px;right:-1px;cursor:pointer;',
                	prev: abs + 'top:50%;margin-top:-20px;height:40px;width:30px;background:#fff;left:20px;display:none;text-align:center;color:#000;font:bold 16px/36px arial,sans-serif',
                	next: abs + 'top:50%;margin-top:-20px;height:40px;width:30px;background:#fff;right:20px;left:auto;display:none;font:bold 16px/36px arial,sans-serif;text-align:center;color:#000',
                	title: 'float:left',
                	counter: 'float:right;margin-left:8px;'
                },
                hover = function (elem) {
                	return elem.hover(
                        function () { $(this).css('color', '#bbb'); },
                        function () { $(this).css('color', '#444'); }
                    );
                },
                appends = {};

				// IE8 fix for IE's transparent background event "feature"
				if (IE === 8) {
					cssMap.nextholder += 'background:#000;filter:alpha(opacity=0);';
					cssMap.prevholder += 'background:#000;filter:alpha(opacity=0);';
				}

				// create and insert CSS
				$.each(cssMap, function (key, value) {
					css += '.galleria-' + prefix + key + '{' + value + '}';
				});

				Utils.insertStyleTag(css);

				// create the elements
				$.each(elems.split(' '), function (i, elemId) {
					self.addElement('lightbox-' + elemId);
					el[elemId] = lightbox.elems[elemId] = self.get('lightbox-' + elemId);
				});

				// initiate the image
				lightbox.image = new Galleria.Picture();

				// append the elements
				$.each({
					box: 'shadow content close prevholder nextholder',
					info: 'title counter',
					content: 'info image',
					prevholder: 'prev',
					nextholder: 'next'
				}, function (key, val) {
					var arr = [];
					$.each(val.split(' '), function (i, prop) {
						arr.push(prefix + prop);
					});
					appends[prefix + key] = arr;
				});

				self.append(appends);

				$(el.image).append(lightbox.image.container);

				$(DOM().body).append(el.overlay, el.box);

				Utils.optimizeTouch(el.box);

				// add the prev/next nav and bind some controls

				hover($(el.close).bind('click', lightbox.hide).html('&#215;'));

				$.each(['Prev', 'Next'], function (i, dir) {

					var $d = $(el[dir.toLowerCase()]).html(/v/.test(dir) ? '&#8249;&nbsp;' : '&nbsp;&#8250;'),
                    $e = $(el[dir.toLowerCase() + 'holder']);

					$e.bind('click', function () {
						lightbox['show' + dir]();
					});

					// IE7 and touch devices will simply show the nav
					if (IE < 8 || Galleria.TOUCH) {
						$d.show();
						return;
					}

					$e.hover(function () {
						$d.show();
					}, function (e) {
						$d.stop().fadeOut(200);
					});

				});
				$(el.overlay).bind('click', lightbox.hide);

				// the lightbox animation is slow on ipad
				if (Galleria.IPAD) {
					self._options.lightboxTransitionSpeed = 0;
				}

			},

			rescale: function (event) {

				// calculate
				var width = Math.min($win.width() - 40, lightbox.width),
                height = Math.min($win.height() - 60, lightbox.height),
                ratio = Math.min(width / lightbox.width, height / lightbox.height),
                destWidth = Math.round(lightbox.width * ratio) + 40,
                destHeight = Math.round(lightbox.height * ratio) + 60,
                to = {
                	width: destWidth,
                	height: destHeight,
                	'margin-top': Math.ceil(destHeight / 2) * -1,
                	'margin-left': Math.ceil(destWidth / 2) * -1
                };

				// if rescale event, don't animate
				if (event) {
					$(lightbox.elems.box).css(to);
				} else {
					$(lightbox.elems.box).animate(to, {
						duration: self._options.lightboxTransitionSpeed,
						easing: self._options.easing,
						complete: function () {
							var image = lightbox.image,
                            speed = self._options.lightboxFadeSpeed;

							self.trigger({
								type: Galleria.LIGHTBOX_IMAGE,
								imageTarget: image.image
							});

							$(image.container).show();

							Utils.show(image.image, speed);
							Utils.show(lightbox.elems.info, speed);
						}
					});
				}
			},

			hide: function () {

				// remove the image
				lightbox.image.image = null;

				$win.unbind('resize', lightbox.rescale);

				$(lightbox.elems.box).hide();

				Utils.hide(lightbox.elems.info);

				self.detachKeyboard();
				self.attachKeyboard(lightbox.keymap);

				lightbox.keymap = false;

				Utils.hide(lightbox.elems.overlay, 200, function () {
					$(this).hide().css('opacity', self._options.overlayOpacity);
					self.trigger(Galleria.LIGHTBOX_CLOSE);
				});
			},

			showNext: function () {
				lightbox.show(self.getNext(lightbox.active));
			},

			showPrev: function () {
				lightbox.show(self.getPrev(lightbox.active));
			},

			show: function (index) {

				lightbox.active = index = typeof index === 'number' ? index : self.getIndex();

				if (!lightbox.initialized) {
					lightbox.init();
				}

				// temporarily attach some keys
				// save the old ones first in a cloned object
				if (!lightbox.keymap) {

					lightbox.keymap = $.extend({}, self._keyboard.map);

					self.attachKeyboard({
						escape: lightbox.hide,
						right: lightbox.showNext,
						left: lightbox.showPrev
					});
				}

				$win.unbind('resize', lightbox.rescale);

				var data = self.getData(index),
                total = self.getDataLength();

				Utils.hide(lightbox.elems.info);

				lightbox.image.load(data.big || data.image, function (image) {

					lightbox.width = image.original.width;
					lightbox.height = image.original.height;

					$(image.image).css({
						width: '100.5%',
						height: '100.5%',
						top: 0,
						zIndex: 99998
					});

					Utils.hide(image.image);

					lightbox.elems.title.innerHTML = data.title || '';
					lightbox.elems.counter.innerHTML = (index + 1) + ' / ' + total;
					$win.resize(lightbox.rescale);
					lightbox.rescale();
				});

				$(lightbox.elems.overlay).show();
				$(lightbox.elems.box).show();
			}
		};

		return this;
	};

	// end Galleria constructor

	Galleria.prototype = {

		// bring back the constructor reference

		constructor: Galleria,

		/**
		Use this function to initialize the gallery and start loading.
		Should only be called once per instance.

		@param {HTMLElement} target The target element
		@param {Object} options The gallery options

		@returns Instance
		*/

		init: function (target, options) {

			var self = this;

			options = _legacyOptions(options);

			// save the original ingredients
			this._original = {
				target: target,
				options: options,
				data: null
			};

			// save the target here
			this._target = this._dom.target = target.nodeName ? target : $(target).get(0);

			// push the instance
			_instances.push(this);

			// raise error if no target is detected
			if (!this._target) {
				Galleria.raise('Target not found.', true);
				return;
			}

			// apply options
			this._options = {
				autoplay: false,
				carousel: true,
				carouselFollow: true,
				carouselSpeed: 400,
				carouselSteps: 'auto',
				clicknext: false,
				dataConfig: function (elem) { return {}; },
				dataSelector: 'img',
				dataSource: this._target,
				debug: undef,
				dummy: undef, /* 1.2.5 */
				easing: 'galleria',
				extend: function (options) { },
				fullscreenCrop: undef, // 1.2.5
				fullscreenDoubleTap: true, // 1.2.4 toggles fullscreen on double-tap for touch devices
				fullscreenTransition: undef, // 1.2.5
				height: 'auto',
				idleMode: true, // 1.2.4 toggles idleMode
				idleTime: 3000,
				idleSpeed: 200,
				imageCrop: false,
				imageMargin: 0,
				imagePan: false,
				imagePanSmoothness: 12,
				imagePosition: '50%',
				imageTimeout: undef, // 1.2.5
				initialTransition: undef, // 1.2.4, replaces transitionInitial
				keepSource: false,
				layerFollow: true, // 1.2.5
				lightbox: false, // 1.2.3
				lightboxFadeSpeed: 200,
				lightboxTransitionSpeed: 200,
				linkSourceImages: true,
				maxScaleRatio: undef,
				minScaleRatio: undef,
				overlayOpacity: 0.85,
				overlayBackground: '#0b0b0b',
				pauseOnInteraction: true,
				popupLinks: false,
				preload: 2,
				queue: true,
				show: 0,
				showInfo: true,
				showCounter: true,
				showImagenav: true,
				swipe: true, // 1.2.4
				thumbCrop: true,
				thumbEventType: 'click',
				thumbFit: true,
				thumbMargin: 0,
				thumbQuality: 'auto',
				thumbnails: true,
				touchTransition: undef, // 1.2.5
				transition: 'fade',
				transitionInitial: undef, // legacy, deprecate in 1.3. Use initialTransition instead.
				transitionSpeed: 400,
				useCanvas: false, // 1.2.4
				width: 'auto'
			};

			// legacy support for transitionInitial
			this._options.initialTransition = this._options.initialTransition || this._options.transitionInitial;

			// turn off debug
			if (options && options.debug === false) {
				DEBUG = false;
			}

			// set timeout
			if (options && typeof options.imageTimeout === 'number') {
				TIMEOUT = options.imageTimeout;
			}

			// set dummy
			if (options && typeof options.dummy === 'string') {
				DUMMY = options.dummy;
			}

			// hide all content
			$(this._target).children().hide();

			// now we just have to wait for the theme...
			if (typeof Galleria.theme === 'object') {
				this._init();
			} else {
				// push the instance into the pool and run it when the theme is ready
				_pool.push(this);
			}

			return this;
		},

		// this method should only be called once per instance
		// for manipulation of data, use the .load method

		_init: function () {
			var self = this;
			if (this._initialized) {
				Galleria.raise('Init failed: Gallery instance already initialized.');
				return this;
			}

			this._initialized = true;

			if (!Galleria.theme) {
				Galleria.raise('Init failed: No theme found.');
				return this;
			}

			// merge the theme & caller options
			$.extend(true, this._options, Galleria.theme.defaults, this._original.options);

			// check for canvas support
			(function (can) {

				if (!('getContext' in can)) {
					can = null;
					return;
				}

				_canvas = _canvas || {
					elem: can,
					context: can.getContext('2d'),
					cache: {},
					length: 0
				};

			} (doc.createElement('canvas')));

			// bind the gallery to run when data is ready
			this.bind(Galleria.DATA, function () {

				// save the new data
				this._original.data = this._data;

				// lets show the counter here
				this.get('total').innerHTML = this.getDataLength();

				// cache the container
				var $container = this.$('container');

				// the gallery is ready, let's just wait for the css
				var num = { width: 0, height: 0 };
				var testHeight = function () {
					return self.$('stage').height();
				};

				// check container and thumbnail height
				Utils.wait({
					until: function () {

						// keep trying to get the value
						$.each(['width', 'height'], function (i, m) {

							// first check if options is set

							if (self._options[m] && typeof self._options[m] === 'number') {
								num[m] = self._options[m];
							} else {

								// else extract the measures from different sources and grab the highest value
								num[m] = Math.max(
                                Utils.parseValue($container.css(m)),         // 1. the container css
                                Utils.parseValue(self.$('target').css(m)), // 2. the target css
                                $container[m](),                               // 3. the container jQuery method
                                self.$('target')[m]()                        // 4. the container jQuery method
                            );
							}

							// apply the new measures
							$container[m](num[m]);

						});
						return testHeight() && num.width && num.height > 10;

					},
					success: function () {

						// for some strange reason, webkit needs a single setTimeout to play ball
						if (Galleria.WEBKIT) {
							window.setTimeout(function () {
								self._run();
							}, 1);
						} else {

							self._run();
						}
					},
					error: function () {

						// Height was probably not set, raise hard errors

						if (testHeight()) {
							Galleria.raise('Could not extract sufficient width/height of the gallery container. Traced measures: width:' + num.width + 'px, height: ' + num.height + 'px.', true);
						} else {
							Galleria.raise('Could not extract a stage height from the CSS. Traced height: ' + testHeight() + 'px.', true);
						}
					},
					timeout: 2000
				});
			});

			// build the gallery frame
			this.append({
				'info-text':
                ['info-title', 'info-description'],
				'info':
                ['info-text'],
				'image-nav':
                ['image-nav-right', 'image-nav-left'],
				'stage':
                ['images', 'loader', 'counter', 'image-nav'],
				'thumbnails-list':
                ['thumbnails'],
				'thumbnails-container':
                ['thumb-nav-left', 'thumbnails-list', 'thumb-nav-right'],
				'container':
                ['stage', 'thumbnails-container', 'info', 'tooltip']
			});

			Utils.hide(this.$('counter').append(
            this.get('current'),
            ' / ',
            this.get('total')
        ));

			this.setCounter('&#8211;');

			Utils.hide(self.get('tooltip'));

			// add a notouch class on the container to prevent unwanted :hovers on touch devices
			this.$('container').addClass(Galleria.TOUCH ? 'touch' : 'notouch');

			// add images to the controls
			$.each(new Array(2), function (i) {

				// create a new Picture instance
				var image = new Galleria.Picture();

				// apply some styles, create & prepend overlay
				$(image.container).css({
					position: 'absolute',
					top: 0,
					left: 0
				}).prepend(self._layers[i] = $(Utils.create('galleria-layer')).css({
					position: 'absolute',
					top: 0, left: 0, right: 0, bottom: 0,
					zIndex: 2
				})[0]);

				// append the image
				self.$('images').append(image.container);

				// reload the controls
				self._controls[i] = image;

			});

			// some forced generic styling
			this.$('images').css({
				position: 'relative',
				top: 0,
				left: 0,
				width: '100%',
				height: '100%'
			});

			this.$('thumbnails, thumbnails-list').css({
				overflow: 'hidden',
				position: 'relative'
			});

			// bind image navigation arrows
			this.$('image-nav-right, image-nav-left').bind('click', function (e) {

				// tune the clicknext option
				if (self._options.clicknext) {
					e.stopPropagation();
				}

				// pause if options is set
				if (self._options.pauseOnInteraction) {
					self.pause();
				}

				// navigate
				var fn = /right/.test(this.className) ? 'next' : 'prev';
				self[fn]();

			});

			// hide controls if chosen to
			$.each(['info', 'counter', 'image-nav'], function (i, el) {
				if (self._options['show' + el.substr(0, 1).toUpperCase() + el.substr(1).replace(/-/, '')] === false) {
					Utils.moveOut(self.get(el.toLowerCase()));
				}
			});

			// load up target content
			this.load();

			// now it's usually safe to remove the content
			// IE will never stop loading if we remove it, so let's keep it hidden for IE (it's usually fast enough anyway)
			if (!this._options.keep_source && !IE) {
				this._target.innerHTML = '';
			}

			// re-append the errors, if they happened before clearing
			if (this.get('errors')) {
				this.appendChild('target', 'errors');
			}

			// append the gallery frame
			this.appendChild('target', 'container');

			// parse the carousel on each thumb load
			if (this._options.carousel) {
				var count = 0,
                show = this._options.show;
				this.bind(Galleria.THUMBNAIL, function () {
					this.updateCarousel();
					if (++count == this.getDataLength() && typeof show == 'number' && show > 0) {
						this._carousel.follow(show);
					}
				});
			}

			// bind swipe gesture
			if (this._options.swipe) {

				(function (images) {

					var swipeStart = [0, 0],
                    swipeStop = [0, 0],
                    limitX = 30,
                    limitY = 100,
                    multi = false,
                    tid = 0,
                    data,
                    ev = {
                    	start: 'touchstart',
                    	move: 'touchmove',
                    	stop: 'touchend'
                    },
                    getData = function (e) {
                    	return e.originalEvent.touches ? e.originalEvent.touches[0] : e;
                    },
                    moveHandler = function (e) {

                    	if (e.originalEvent.touches && e.originalEvent.touches.length > 1) {
                    		return;
                    	}

                    	data = getData(e);
                    	swipeStop = [data.pageX, data.pageY];

                    	if (!swipeStart[0]) {
                    		swipeStart = swipeStop;
                    	}

                    	if (Math.abs(swipeStart[0] - swipeStop[0]) > 10) {
                    		e.preventDefault();
                    	}
                    },
                    upHandler = function (e) {

                    	images.unbind(ev.move, moveHandler);

                    	// if multitouch (possibly zooming), abort
                    	if ((e.originalEvent.touches && e.originalEvent.touches.length) || multi) {
                    		multi = !multi;
                    		return;
                    	}

                    	if (Utils.timestamp() - tid < 1000 &&
                             Math.abs(swipeStart[0] - swipeStop[0]) > limitX &&
                             Math.abs(swipeStart[1] - swipeStop[1]) < limitY) {

                    		e.preventDefault();
                    		self[swipeStart[0] > swipeStop[0] ? 'next' : 'prev']();
                    	}

                    	swipeStart = swipeStop = [0, 0];
                    };

					images.bind(ev.start, function (e) {

						if (e.originalEvent.touches && e.originalEvent.touches.length > 1) {
							return;
						}

						data = getData(e);
						tid = Utils.timestamp();
						swipeStart = swipeStop = [data.pageX, data.pageY];
						images.bind(ev.move, moveHandler).one(ev.stop, upHandler);

					});

				} (self.$('images')));

				// double-tap/click fullscreen toggle

				if (this._options.fullscreenDoubleTap) {

					this.$('stage').bind('touchstart', (function () {
						var last, cx, cy, lx, ly, now,
                        getData = function (e) {
                        	return e.originalEvent.touches ? e.originalEvent.touches[0] : e;
                        };
						return function (e) {
							now = Galleria.utils.timestamp();
							cx = getData(e).pageX;
							cy = getData(e).pageY;
							if ((now - last < 500) && (cx - lx < 20) && (cy - ly < 20)) {
								self.toggleFullscreen();
								e.preventDefault();
								self.$('stage').unbind('touchend', arguments.callee);
								return;
							}
							last = now;
							lx = cx;
							ly = cy;
						};
					} ()));
				}

			}

			// optimize touch for container
			Utils.optimizeTouch(this.get('container'));

			return this;
		},

		// Creates the thumbnails and carousel
		// can be used at any time, f.ex when the data object is manipulated

		_createThumbnails: function () {

			this.get('total').innerHTML = this.getDataLength();

			var i,
            src,
            thumb,
            data,

            $container,

            self = this,
            o = this._options,

			// get previously active thumbnail, if exists
            active = (function () {
            	var a = self.$('thumbnails').find('.active');
            	if (!a.length) {
            		return false;
            	}
            	return a.find('img').attr('src');
            } ()),

			// cache the thumbnail option
            optval = typeof o.thumbnails === 'string' ? o.thumbnails.toLowerCase() : null,

			// move some data into the instance
			// for some reason, jQuery cant handle css(property) when zooming in FF, breaking the gallery
			// so we resort to getComputedStyle for browsers who support it
            getStyle = function (prop) {
            	return doc.defaultView && doc.defaultView.getComputedStyle ?
                    doc.defaultView.getComputedStyle(thumb.container, null)[prop] :
                    $container.css(prop);
            },

            fake = function (image, index, container) {
            	return function () {
            		$(container).append(image);
            		self.trigger({
            			type: Galleria.THUMBNAIL,
            			thumbTarget: image,
            			index: index
            		});
            	};
            },

            onThumbEvent = function (e) {

            	// pause if option is set
            	if (o.pauseOnInteraction) {
            		self.pause();
            	}

            	// extract the index from the data
            	var index = $(e.currentTarget).data('index');
            	if (self.getIndex() !== index) {
            		self.show(index);
            	}

            	e.preventDefault();
            },

            onThumbLoad = function (thumb) {

            	// scale when ready
            	thumb.scale({
            		width: thumb.data.width,
            		height: thumb.data.height,
            		crop: o.thumbCrop,
            		margin: o.thumbMargin,
            		canvas: o.useCanvas,
            		complete: function (thumb) {

            			// shrink thumbnails to fit
            			var top = ['left', 'top'],
                            arr = ['Width', 'Height'],
                            m,
                            css;

            			// calculate shrinked positions
            			$.each(arr, function (i, measure) {
            				m = measure.toLowerCase();
            				if ((o.thumbCrop !== true || o.thumbCrop === m) && o.thumbFit) {
            					css = {};
            					css[m] = thumb[m];
            					$(thumb.container).css(css);
            					css = {};
            					css[top[i]] = 0;
            					$(thumb.image).css(css);
            				}

            				// cache outer measures
            				thumb['outer' + measure] = $(thumb.container)['outer' + measure](true);
            			});

            			// set high quality if downscale is moderate
            			Utils.toggleQuality(thumb.image,
                            o.thumbQuality === true ||
                            (o.thumbQuality === 'auto' && thumb.original.width < thumb.width * 3)
                        );

            			// trigger the THUMBNAIL event
            			self.trigger({
            				type: Galleria.THUMBNAIL,
            				thumbTarget: thumb.image,
            				index: thumb.data.order
            			});
            		}
            	});
            };

			this._thumbnails = [];

			this.$('thumbnails').empty();

			// loop through data and create thumbnails
			for (i = 0; this._data[i]; i++) {

				data = this._data[i];

				if (o.thumbnails === true) {

					// add a new Picture instance
					thumb = new Galleria.Picture(i);

					// get source from thumb or image
					src = data.thumb || data.image;

					// append the thumbnail
					this.$('thumbnails').append(thumb.container);

					// cache the container
					$container = $(thumb.container);

					thumb.data = {
						width: Utils.parseValue(getStyle('width')),
						height: Utils.parseValue(getStyle('height')),
						order: i
					};

					// grab & reset size for smoother thumbnail loads
					if (o.thumbFit && o.thumbCrop !== true) {
						$container.css({ width: 0, height: 0 });
					} else {
						$container.css({ width: thumb.data.width, height: thumb.data.height });
					}

					// load the thumbnail
					thumb.load(src, onThumbLoad);

					// preload all images here
					if (o.preload === 'all') {
						thumb.preload(data.image);
					}

					// create empty spans if thumbnails is set to 'empty'
				} else if (optval === 'empty' || optval === 'numbers') {

					thumb = {
						container: Utils.create('galleria-image'),
						image: Utils.create('img', 'span'),
						ready: true
					};

					// create numbered thumbnails
					if (optval === 'numbers') {
						$(thumb.image).text(i + 1);
					}

					this.$('thumbnails').append(thumb.container);

					// we need to "fake" a loading delay before we append and trigger
					// 50+ should be enough

					window.setTimeout((fake)(thumb.image, i, thumb.container), 50 + (i * 20));

					// create null object to silent errors
				} else {
					thumb = {
						container: null,
						image: null
					};
				}

				// add events for thumbnails
				// you can control the event type using thumb_event_type
				// we'll add the same event to the source if it's kept

				$(thumb.container).add(o.keepSource && o.linkSourceImages ? data.original : null)
                .data('index', i).bind(o.thumbEventType, onThumbEvent);

				if (active === src) {
					$(thumb.container).addClass('active');
				}

				this._thumbnails.push(thumb);
			}
		},

		// the internal _run method should be called after loading data into galleria
		// makes sure the gallery has proper measurements before postrun & ready
		_run: function () {

			var self = this;

			self._createThumbnails();

			// make sure we have a stageHeight && stageWidth

			Utils.wait({

				until: function () {

					// Opera crap
					if (Galleria.OPERA) {
						self.$('stage').css('display', 'inline-block');
					}

					self._stageWidth = self.$('stage').width();
					self._stageHeight = self.$('stage').height();

					return (self._stageWidth &&
                        self._stageHeight > 50); // what is an acceptable height?
				},

				success: function () {

					// save the instance
					_galleries.push(self);

					// postrun some stuff after the gallery is ready

					// show counter
					Utils.show(self.get('counter'));

					// bind carousel nav
					if (self._options.carousel) {
						self._carousel.bindControls();
					}

					// start autoplay
					if (self._options.autoplay) {

						self.pause();

						if (typeof self._options.autoplay === 'number') {
							self._playtime = self._options.autoplay;
						}

						self.trigger(Galleria.PLAY);
						self._playing = true;
					}
					// if second load, just do the show and return
					if (self._firstrun) {
						if (typeof self._options.show === 'number') {
							self.show(self._options.show);
						}
						return;
					}

					self._firstrun = true;

					// bind clicknext
					if (self._options.clicknext && !Galleria.TOUCH) {
						$.each(self._data, function (i, data) {
							delete data.link;
						});
						self.$('stage').css({ cursor: 'pointer' }).bind('click', function (e) {
							// pause if options is set
							if (self._options.pauseOnInteraction) {
								self.pause();
							}
							self.next();
						});
					}

					// initialize the History plugin
					if (Galleria.History) {

						// bind the show method
						Galleria.History.change(function (value) {

							// if ID is NaN, the user pressed back from the first image
							// return to previous address
							if (isNaN(value)) {
								window.history.go(-1);

								// else show the image
							} else {
								self.show(value, undef, true);
							}
						});
					}

					// Trigger Galleria.ready
					$.each(Galleria.ready.callbacks, function () {
						this.call(self, self._options);
					});

					self.trigger(Galleria.READY);

					// call the theme init method
					Galleria.theme.init.call(self, self._options);

					// call the extend option
					self._options.extend.call(self, self._options);

					// show the initial image
					// first test for permalinks in history
					if (/^[0-9]{1,4}$/.test(HASH) && Galleria.History) {
						self.show(HASH, undef, true);

					} else if (self._data[self._options.show]) {
						self.show(self._options.show);
					}
				},

				error: function () {
					Galleria.raise('Stage width or height is too small to show the gallery. Traced measures: width:' + self._stageWidth + 'px, height: ' + self._stageHeight + 'px.', true);
				}

			});
		},

		/**
		Loads data into the gallery.
		You can call this method on an existing gallery to reload the gallery with new data.

		@param {Array|string} source Optional JSON array of data or selector of where to find data in the document.
		Defaults to the Galleria target or dataSource option.

		@param {string} selector Optional element selector of what elements to parse.
		Defaults to 'img'.

		@param {Function} [config] Optional function to modify the data extraction proceedure from the selector.
		See the data_config option for more information.

		@returns Instance
		*/

		load: function (source, selector, config) {

			var self = this;

			// empty the data array
			this._data = [];

			// empty the thumbnails
			this._thumbnails = [];
			this.$('thumbnails').empty();

			// shorten the arguments
			if (typeof selector === 'function') {
				config = selector;
				selector = null;
			}

			// use the source set by target
			source = source || this._options.dataSource;

			// use selector set by option
			selector = selector || this._options.dataSelector;

			// use the data_config set by option
			config = config || this._options.dataConfig;

			// if source is a true object, make it into an array
			if (/^function Object/.test(source.constructor)) {
				source = [source];
			}

			// check if the data is an array already
			if (source.constructor === Array) {
				if (this.validate(source)) {

					this._data = source;
					this._parseData().trigger(Galleria.DATA);

				} else {
					Galleria.raise('Load failed: JSON Array not valid.');
				}
				return this;
			}

			// loop through images and set data
			$(source).find(selector).each(function (i, img) {
				img = $(img);
				var data = {},
                parent = img.parent(),
                href = parent.attr('href'),
                rel = parent.attr('rel');

				if (href) {
					data.image = data.big = href;
				}
				if (rel) {
					data.big = rel;
				}

				// mix default extractions with the hrefs and config
				// and push it into the data array
				self._data.push($.extend({

					title: img.attr('title') || '',
					thumb: img.attr('src'),
					image: img.attr('src'),
					big: img.attr('src'),
					description: img.attr('alt') || '',
					link: img.attr('longdesc'),
					original: img.get(0) // saved as a reference

				}, data, config(img)));

			});
			// trigger the DATA event and return
			if (this.getDataLength()) {
				this.trigger(Galleria.DATA);
			} else {
				Galleria.raise('Load failed: no data found.');
			}
			return this;

		},

		// make sure the data works properly
		_parseData: function () {

			var self = this;

			$.each(this._data, function (i, data) {
				// copy image as thumb if no thumb exists
				if ('thumb' in data === false) {
					self._data[i].thumb = data.image;
				}
				// copy image as big image if no biggie exists
				if (!'big' in data) {
					self._data[i].big = data.image;
				}
			});

			return this;
		},

		/**
		Adds and/or removes images from the gallery
		Works just like Array.splice
		https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/splice

		@example this.splice( 2, 4 ); // removes 4 images after the second image

		@returns Instance
		*/

		splice: function () {
			Array.prototype.splice.apply(this._data, Utils.array(arguments));
			return this._parseData()._createThumbnails();
		},

		/**
		Append images to the gallery
		Works just like Array.push
		https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/push

		@example this.push({ image: 'image1.jpg' }); // appends the image to the gallery

		@returns Instance
		*/

		push: function () {
			Array.prototype.push.apply(this._data, Utils.array(arguments));
			return this._parseData()._createThumbnails();
		},

		_getActive: function () {
			return this._controls.getActive();
		},

		validate: function (data) {
			// todo: validate a custom data array
			return true;
		},

		/**
		Bind any event to Galleria

		@param {string} type The Event type to listen for
		@param {Function} fn The function to execute when the event is triggered

		@example this.bind( 'image', function() { Galleria.log('image shown') });

		@returns Instance
		*/

		bind: function (type, fn) {

			// allow 'image' instead of Galleria.IMAGE
			type = _patchEvent(type);

			this.$('container').bind(type, this.proxy(fn));
			return this;
		},

		/**
		Unbind any event to Galleria

		@param {string} type The Event type to forget

		@returns Instance
		*/

		unbind: function (type) {

			type = _patchEvent(type);

			this.$('container').unbind(type);
			return this;
		},

		/**
		Manually trigger a Galleria event

		@param {string} type The Event to trigger

		@returns Instance
		*/

		trigger: function (type) {

			type = typeof type === 'object' ?
            $.extend(type, { scope: this }) :
            { type: _patchEvent(type), scope: this };

			this.$('container').trigger(type);

			return this;
		},

		/**
		Assign an "idle state" to any element.
		The idle state will be applied after a certain amount of idle time
		Useful to hide f.ex navigation when the gallery is inactive

		@param {HTMLElement|string} elem The Dom node or selector to apply the idle state to
		@param {Object} styles the CSS styles to apply

		@example addIdleState( this.get('image-nav'), { opacity: 0 });
		@example addIdleState( '.galleria-image-nav', { top: -200 });

		@returns Instance
		*/

		addIdleState: function (elem, styles) {
			this._idle.add.apply(this._idle, Utils.array(arguments));
			return this;
		},

		/**
		Removes any idle state previously set using addIdleState()

		@param {HTMLElement|string} elem The Dom node or selector to remove the idle state from.

		@returns Instance
		*/

		removeIdleState: function (elem) {
			this._idle.remove.apply(this._idle, Utils.array(arguments));
			return this;
		},

		/**
		Force Galleria to enter idle mode.

		@returns Instance
		*/

		enterIdleMode: function () {
			this._idle.hide();
			return this;
		},

		/**
		Force Galleria to exit idle mode.

		@returns Instance
		*/

		exitIdleMode: function () {
			this._idle.showAll();
			return this;
		},

		/**
		Enter FullScreen mode

		@param {Function} callback the function to be executed when the fullscreen mode is fully applied.

		@returns Instance
		*/

		enterFullscreen: function (callback) {
			this._fullscreen.enter.apply(this, Utils.array(arguments));
			return this;
		},

		/**
		Exits FullScreen mode

		@param {Function} callback the function to be executed when the fullscreen mode is fully applied.

		@returns Instance
		*/

		exitFullscreen: function (callback) {
			this._fullscreen.exit.apply(this, Utils.array(arguments));
			return this;
		},

		/**
		Toggle FullScreen mode

		@param {Function} callback the function to be executed when the fullscreen mode is fully applied or removed.

		@returns Instance
		*/

		toggleFullscreen: function (callback) {
			this._fullscreen[this.isFullscreen() ? 'exit' : 'enter'].apply(this, Utils.array(arguments));
			return this;
		},

		/**
		Adds a tooltip to any element.
		You can also call this method with an object as argument with elemID:value pairs to apply tooltips to (see examples)

		@param {HTMLElement} elem The DOM Node to attach the event to
		@param {string|Function} value The tooltip message. Can also be a function that returns a string.

		@example this.bindTooltip( this.get('thumbnails'), 'My thumbnails');
		@example this.bindTooltip( this.get('thumbnails'), function() { return 'My thumbs' });
		@example this.bindTooltip( { image_nav: 'Navigation' });

		@returns Instance
		*/

		bindTooltip: function (elem, value) {
			this._tooltip.bind.apply(this._tooltip, Utils.array(arguments));
			return this;
		},

		/**
		Note: this method is deprecated. Use refreshTooltip() instead.

		Redefine a tooltip.
		Use this if you want to re-apply a tooltip value to an already bound tooltip element.

		@param {HTMLElement} elem The DOM Node to attach the event to
		@param {string|Function} value The tooltip message. Can also be a function that returns a string.

		@returns Instance
		*/

		defineTooltip: function (elem, value) {
			this._tooltip.define.apply(this._tooltip, Utils.array(arguments));
			return this;
		},

		/**
		Refresh a tooltip value.
		Use this if you want to change the tooltip value at runtime, f.ex if you have a play/pause toggle.

		@param {HTMLElement} elem The DOM Node that has a tooltip that should be refreshed

		@returns Instance
		*/

		refreshTooltip: function (elem) {
			this._tooltip.show.apply(this._tooltip, Utils.array(arguments));
			return this;
		},

		/**
		Open a pre-designed lightbox with the currently active image.
		You can control some visuals using gallery options.

		@returns Instance
		*/

		openLightbox: function () {
			this._lightbox.show.apply(this._lightbox, Utils.array(arguments));
			return this;
		},

		/**
		Close the lightbox.

		@returns Instance
		*/

		closeLightbox: function () {
			this._lightbox.hide.apply(this._lightbox, Utils.array(arguments));
			return this;
		},

		/**
		Get the currently active image element.

		@returns {HTMLElement} The image element
		*/

		getActiveImage: function () {
			return this._getActive().image || undef;
		},

		/**
		Get the currently active thumbnail element.

		@returns {HTMLElement} The thumbnail element
		*/

		getActiveThumb: function () {
			return this._thumbnails[this._active].image || undef;
		},

		/**
		Get the mouse position relative to the gallery container

		@param e The mouse event

		@example

		var gallery = this;
		$(document).mousemove(function(e) {
		console.log( gallery.getMousePosition(e).x );
		});

		@returns {Object} Object with x & y of the relative mouse postion
		*/

		getMousePosition: function (e) {
			return {
				x: e.pageX - this.$('container').offset().left,
				y: e.pageY - this.$('container').offset().top
			};
		},

		/**
		Adds a panning effect to the image

		@param img The optional image element. If not specified it takes the currently active image

		@returns Instance
		*/

		addPan: function (img) {

			if (this._options.imageCrop === false) {
				return;
			}

			img = $(img || this.getActiveImage());

			// define some variables and methods
			var self = this,
            x = img.width() / 2,
            y = img.height() / 2,
            destX = parseInt(img.css('left'), 10),
            destY = parseInt(img.css('top'), 10),
            curX = destX || 0,
            curY = destY || 0,
            distX = 0,
            distY = 0,
            active = false,
            ts = Utils.timestamp(),
            cache = 0,
            move = 0,

			// positions the image
            position = function (dist, cur, pos) {
            	if (dist > 0) {
            		move = Math.round(Math.max(dist * -1, Math.min(0, cur)));
            		if (cache !== move) {

            			cache = move;

            			if (IE === 8) { // scroll is faster for IE
            				img.parent()['scroll' + pos](move * -1);
            			} else {
            				var css = {};
            				css[pos.toLowerCase()] = move;
            				img.css(css);
            			}
            		}
            	}
            },

			// calculates mouse position after 50ms
            calculate = function (e) {
            	if (Utils.timestamp() - ts < 50) {
            		return;
            	}
            	active = true;
            	x = self.getMousePosition(e).x;
            	y = self.getMousePosition(e).y;
            },

			// the main loop to check
            loop = function (e) {

            	if (!active) {
            		return;
            	}

            	distX = img.width() - self._stageWidth;
            	distY = img.height() - self._stageHeight;
            	destX = x / self._stageWidth * distX * -1;
            	destY = y / self._stageHeight * distY * -1;
            	curX += (destX - curX) / self._options.imagePanSmoothness;
            	curY += (destY - curY) / self._options.imagePanSmoothness;

            	position(distY, curY, 'Top');
            	position(distX, curX, 'Left');

            };

			// we need to use scroll in IE8 to speed things up
			if (IE === 8) {

				img.parent().scrollTop(curY * -1).scrollLeft(curX * -1);
				img.css({
					top: 0,
					left: 0
				});

			}

			// unbind and bind event
			this.$('stage').unbind('mousemove', calculate).bind('mousemove', calculate);

			// loop the loop
			Utils.addTimer('pan', loop, 50, true);

			return this;
		},

		/**
		Brings the scope into any callback

		@param fn The callback to bring the scope into
		@param scope Optional scope to bring

		@example $('#fullscreen').click( this.proxy(function() { this.enterFullscreen(); }) )

		@returns {Function} Return the callback with the gallery scope
		*/

		proxy: function (fn, scope) {
			if (typeof fn !== 'function') {
				return function () { };
			}
			scope = scope || this;
			return function () {
				return fn.apply(scope, Utils.array(arguments));
			};
		},

		/**
		Removes the panning effect set by addPan()

		@returns Instance
		*/

		removePan: function () {

			// todo: doublecheck IE8

			this.$('stage').unbind('mousemove');

			Utils.clearTimer('pan');

			return this;
		},

		/**
		Adds an element to the Galleria DOM array.
		When you add an element here, you can access it using element ID in many API calls

		@param {string} id The element ID you wish to use. You can add many elements by adding more arguments.

		@example addElement('mybutton');
		@example addElement('mybutton','mylink');

		@returns Instance
		*/

		addElement: function (id) {

			var dom = this._dom;

			$.each(Utils.array(arguments), function (i, blueprint) {
				dom[blueprint] = Utils.create('galleria-' + blueprint);
			});

			return this;
		},

		/**
		Attach keyboard events to Galleria

		@param {Object} map The map object of events.
		Possible keys are 'UP', 'DOWN', 'LEFT', 'RIGHT', 'RETURN', 'ESCAPE', 'BACKSPACE', and 'SPACE'.

		@example

		this.attachKeyboard({
		right: this.next,
		left: this.prev,
		up: function() {
		console.log( 'up key pressed' )
		}
		});

		@returns Instance
		*/

		attachKeyboard: function (map) {
			this._keyboard.attach.apply(this._keyboard, Utils.array(arguments));
			return this;
		},

		/**
		Detach all keyboard events to Galleria

		@returns Instance
		*/

		detachKeyboard: function () {
			this._keyboard.detach.apply(this._keyboard, Utils.array(arguments));
			return this;
		},

		/**
		Fast helper for appending galleria elements that you added using addElement()

		@param {string} parentID The parent element ID where the element will be appended
		@param {string} childID the element ID that should be appended

		@example this.addElement('myElement');
		this.appendChild( 'info', 'myElement' );

		@returns Instance
		*/

		appendChild: function (parentID, childID) {
			this.$(parentID).append(this.get(childID) || childID);
			return this;
		},

		/**
		Fast helper for prepending galleria elements that you added using addElement()

		@param {string} parentID The parent element ID where the element will be prepended
		@param {string} childID the element ID that should be prepended

		@example

		this.addElement('myElement');
		this.prependChild( 'info', 'myElement' );

		@returns Instance
		*/

		prependChild: function (parentID, childID) {
			this.$(parentID).prepend(this.get(childID) || childID);
			return this;
		},

		/**
		Remove an element by blueprint

		@param {string} elemID The element to be removed.
		You can remove multiple elements by adding arguments.

		@returns Instance
		*/

		remove: function (elemID) {
			this.$(Utils.array(arguments).join(',')).remove();
			return this;
		},

		// a fast helper for building dom structures
		// leave this out of the API for now

		append: function (data) {
			var i, j;
			for (i in data) {
				if (data.hasOwnProperty(i)) {
					if (data[i].constructor === Array) {
						for (j = 0; data[i][j]; j++) {
							this.appendChild(i, data[i][j]);
						}
					} else {
						this.appendChild(i, data[i]);
					}
				}
			}
			return this;
		},

		// an internal helper for scaling according to options
		_scaleImage: function (image, options) {

			image = image || this._controls.getActive();

			var self = this,

            complete,

            scaleLayer = function (img) {
            	$(img.container).children(':first').css({
            		top: Math.max(0, Utils.parseValue(img.image.style.top)),
            		left: Math.max(0, Utils.parseValue(img.image.style.left)),
            		width: Utils.parseValue(img.image.width),
            		height: Utils.parseValue(img.image.height)
            	});
            };

			options = $.extend({
				width: this._stageWidth,
				height: this._stageHeight,
				crop: this._options.imageCrop,
				max: this._options.maxScaleRatio,
				min: this._options.minScaleRatio,
				margin: this._options.imageMargin,
				position: this._options.imagePosition
			}, options);

			if (this._options.layerFollow && this._options.imageCrop !== true) {

				if (typeof options.complete == 'function') {
					complete = options.complete;
					options.complete = function () {
						complete.call(image, image);
						scaleLayer(image);
					};
				} else {
					options.complete = scaleLayer;
				}

			} else {
				$(image.container).children(':first').css({ top: 0, left: 0 });
			}

			image.scale(options);
			return this;
		},

		/**
		Updates the carousel,
		useful if you resize the gallery and want to re-check if the carousel nav is needed.

		@returns Instance
		*/

		updateCarousel: function () {
			this._carousel.update();
			return this;
		},

		/**
		Rescales the gallery

		@param {number} width The target width
		@param {number} height The target height
		@param {Function} complete The callback to be called when the scaling is complete

		@returns Instance
		*/

		rescale: function (width, height, complete) {

			var self = this;

			// allow rescale(fn)
			if (typeof width === 'function') {
				complete = width;
				width = undef;
			}

			var scale = function () {

				// set stagewidth
				self._stageWidth = width || self.$('stage').width();
				self._stageHeight = height || self.$('stage').height();

				// scale the active image
				self._scaleImage();

				if (self._options.carousel) {
					self.updateCarousel();
				}

				self.trigger(Galleria.RESCALE);

				if (typeof complete === 'function') {
					complete.call(self);
				}
			};

			if (Galleria.WEBKIT && !width && !height) {
				Utils.addTimer('scale', scale, 10); // webkit is too fast
			} else {
				scale.call(self);
			}

			return this;
		},

		/**
		Refreshes the gallery.
		Useful if you change image options at runtime and want to apply the changes to the active image.

		@returns Instance
		*/

		refreshImage: function () {
			this._scaleImage();
			if (this._options.imagePan) {
				this.addPan();
			}
			return this;
		},

		/**
		Shows an image by index

		@param {number|boolean} index The index to show
		@param {Boolean} rewind A boolean that should be true if you want the transition to go back

		@returns Instance
		*/

		show: function (index, rewind, _history) {

			// do nothing if index is false or queue is false and transition is in progress
			if (index === false || (!this._options.queue && this._queue.stalled)) {
				return;
			}

			index = Math.max(0, Math.min(parseInt(index, 10), this.getDataLength() - 1));

			rewind = typeof rewind !== 'undefined' ? !!rewind : index < this.getIndex();

			_history = _history || false;

			// do the history thing and return
			if (!_history && Galleria.History) {
				Galleria.History.set(index.toString());
				return;
			}

			this._active = index;

			Array.prototype.push.call(this._queue, {
				index: index,
				rewind: rewind
			});
			if (!this._queue.stalled) {
				this._show();
			}

			return this;
		},

		// the internal _show method does the actual showing
		_show: function () {

			// shortcuts
			var self = this,
            queue = this._queue[0],
            data = this.getData(queue.index);

			if (!data) {
				return;
			}

			var src = this.isFullscreen() && 'big' in data ? data.big : data.image, // use big image if fullscreen mode
            active = this._controls.getActive(),
            next = this._controls.getNext(),
            cached = next.isCached(src),
            thumb = this._thumbnails[queue.index];

			// to be fired when loading & transition is complete:
			var complete = (function (data, next, active, queue, thumb) {

				return function () {

					var win;

					// remove stalled
					self._queue.stalled = false;

					// optimize quality
					Utils.toggleQuality(next.image, self._options.imageQuality);

					// remove old layer
					self._layers[self._controls.active].innerHTML = '';

					// swap
					$(active.container).css({
						zIndex: 0,
						opacity: 0
					}).show();

					$(next.container).css({
						zIndex: 1
					}).show();

					self._controls.swap();

					// add pan according to option
					if (self._options.imagePan) {
						self.addPan(next.image);
					}

					// make the image link or add lightbox
					// link takes precedence over lightbox if both are detected
					if (data.link || self._options.lightbox) {

						$(next.image).css({
							cursor: 'pointer'
						}).bind('mouseup', function () {

							// popup link
							if (data.link) {
								if (self._options.popupLinks) {
									win = window.open(data.link, '_blank');
								} else {
									window.location.href = data.link;
								}
								return;
							}

							self.openLightbox();

						});
					}

					// remove the queued image
					Array.prototype.shift.call(self._queue);

					// if we still have images in the queue, show it
					if (self._queue.length) {
						self._show();
					}

					// check if we are playing
					self._playCheck();

					// trigger IMAGE event
					self.trigger({
						type: Galleria.IMAGE,
						index: queue.index,
						imageTarget: next.image,
						thumbTarget: thumb.image
					});
				};
			} (data, next, active, queue, thumb));

			// let the carousel follow
			if (this._options.carousel && this._options.carouselFollow) {
				this._carousel.follow(queue.index);
			}

			// preload images
			if (this._options.preload) {

				var p, i,
                n = this.getNext(),
                ndata;

				try {
					for (i = this._options.preload; i > 0; i--) {
						p = new Galleria.Picture();
						ndata = self.getData(n);
						p.preload(this.isFullscreen() && 'big' in ndata ? ndata.big : ndata.image);
						n = self.getNext(n);
					}
				} catch (e) { }
			}

			// show the next image, just in case
			Utils.show(next.container);

			// add active classes
			$(self._thumbnails[queue.index].container)
            .addClass('active')
            .siblings('.active')
            .removeClass('active');

			// trigger the LOADSTART event
			self.trigger({
				type: Galleria.LOADSTART,
				cached: cached,
				index: queue.index,
				rewind: queue.rewind,
				imageTarget: next.image,
				thumbTarget: thumb.image
			});

			// begin loading the next image
			next.load(src, function (next) {

				// add layer HTML
				$(self._layers[1 - self._controls.active]).html(data.layer || '').hide();

				self._scaleImage(next, {

					complete: function (next) {

						// toggle low quality for IE
						if ('image' in active) {
							Utils.toggleQuality(active.image, false);
						}
						Utils.toggleQuality(next.image, false);

						// stall the queue
						self._queue.stalled = true;

						// remove the image panning, if applied
						// TODO: rethink if this is necessary
						self.removePan();

						// set the captions and counter
						self.setInfo(queue.index);
						self.setCounter(queue.index);

						// show the layer now
						if (data.layer) {
							$(self._layers[1 - self._controls.active]).show();
						}

						// trigger the LOADFINISH event
						self.trigger({
							type: Galleria.LOADFINISH,
							cached: cached,
							index: queue.index,
							rewind: queue.rewind,
							imageTarget: next.image,
							thumbTarget: self._thumbnails[queue.index].image
						});

						var transition = self._options.transition;

						// can JavaScript loop through objects in order? yes.
						$.each({
							initial: active.image === null,
							touch: Galleria.TOUCH,
							fullscreen: self.isFullscreen()
						}, function (type, arg) {
							if (arg && self._options[type + 'Transition'] !== undef) {
								transition = self._options[type + 'Transition'];
								return false;
							}
						});

						// validate the transition
						if (transition in _transitions === false) {
							complete();
						} else {
							var params = {
								prev: active.container,
								next: next.container,
								rewind: queue.rewind,
								speed: self._options.transitionSpeed || 400
							};

							// call the transition function and send some stuff
							_transitions[transition].call(self, params, complete);

						}
					}
				});
			});
		},

		/**
		Gets the next index

		@param {number} base Optional starting point

		@returns {number} the next index, or the first if you are at the first (looping)
		*/

		getNext: function (base) {
			base = typeof base === 'number' ? base : this.getIndex();
			return base === this.getDataLength() - 1 ? 0 : base + 1;
		},

		/**
		Gets the previous index

		@param {number} base Optional starting point

		@returns {number} the previous index, or the last if you are at the first (looping)
		*/

		getPrev: function (base) {
			base = typeof base === 'number' ? base : this.getIndex();
			return base === 0 ? this.getDataLength() - 1 : base - 1;
		},

		/**
		Shows the next image in line

		@returns Instance
		*/

		next: function () {
			if (this.getDataLength() > 1) {
				this.show(this.getNext(), false);
			}
			return this;
		},

		/**
		Shows the previous image in line

		@returns Instance
		*/

		prev: function () {
			if (this.getDataLength() > 1) {
				this.show(this.getPrev(), true);
			}
			return this;
		},

		/**
		Retrieve a DOM element by element ID

		@param {string} elemId The delement ID to fetch

		@returns {HTMLElement} The elements DOM node or null if not found.
		*/

		get: function (elemId) {
			return elemId in this._dom ? this._dom[elemId] : null;
		},

		/**
		Retrieve a data object

		@param {number} index The data index to retrieve.
		If no index specified it will take the currently active image

		@returns {Object} The data object
		*/

		getData: function (index) {
			return index in this._data ?
            this._data[index] : this._data[this._active];
		},

		/**
		Retrieve the number of data items

		@returns {number} The data length
		*/
		getDataLength: function () {
			return this._data.length;
		},

		/**
		Retrieve the currently active index

		@returns {number|boolean} The active index or false if none found
		*/

		getIndex: function () {
			return typeof this._active === 'number' ? this._active : false;
		},

		/**
		Retrieve the stage height

		@returns {number} The stage height
		*/

		getStageHeight: function () {
			return this._stageHeight;
		},

		/**
		Retrieve the stage width

		@returns {number} The stage width
		*/

		getStageWidth: function () {
			return this._stageWidth;
		},

		/**
		Retrieve the option

		@param {string} key The option key to retrieve. If no key specified it will return all options in an object.

		@returns option or options
		*/

		getOptions: function (key) {
			return typeof key === 'undefined' ? this._options : this._options[key];
		},

		/**
		Set options to the instance.
		You can set options using a key & value argument or a single object argument (see examples)

		@param {string} key The option key
		@param {string} value the the options value

		@example setOptions( 'autoplay', true )
		@example setOptions({ autoplay: true });

		@returns Instance
		*/

		setOptions: function (key, value) {
			if (typeof key === 'object') {
				$.extend(this._options, key);
			} else {
				this._options[key] = value;
			}
			return this;
		},

		/**
		Starts playing the slideshow

		@param {number} delay Sets the slideshow interval in milliseconds.
		If you set it once, you can just call play() and get the same interval the next time.

		@returns Instance
		*/

		play: function (delay) {

			this._playing = true;

			this._playtime = delay || this._playtime;

			this._playCheck();

			this.trigger(Galleria.PLAY);

			return this;
		},

		/**
		Stops the slideshow if currently playing

		@returns Instance
		*/

		pause: function () {

			this._playing = false;

			this.trigger(Galleria.PAUSE);

			return this;
		},

		/**
		Toggle between play and pause events.

		@param {number} delay Sets the slideshow interval in milliseconds.

		@returns Instance
		*/

		playToggle: function (delay) {
			return (this._playing) ? this.pause() : this.play(delay);
		},

		/**
		Checks if the gallery is currently playing

		@returns {Boolean}
		*/

		isPlaying: function () {
			return this._playing;
		},

		/**
		Checks if the gallery is currently in fullscreen mode

		@returns {Boolean}
		*/

		isFullscreen: function () {
			return this._fullscreen.active;
		},

		_playCheck: function () {
			var self = this,
            played = 0,
            interval = 20,
            now = Utils.timestamp(),
            timer_id = 'play' + this._id;

			if (this._playing) {

				Utils.clearTimer(timer_id);

				var fn = function () {

					played = Utils.timestamp() - now;
					if (played >= self._playtime && self._playing) {
						Utils.clearTimer(timer_id);
						self.next();
						return;
					}
					if (self._playing) {

						// trigger the PROGRESS event
						self.trigger({
							type: Galleria.PROGRESS,
							percent: Math.ceil(played / self._playtime * 100),
							seconds: Math.floor(played / 1000),
							milliseconds: played
						});

						Utils.addTimer(timer_id, fn, interval);
					}
				};
				Utils.addTimer(timer_id, fn, interval);
			}
		},

		/**
		Modify the slideshow delay

		@param {number} delay the number of milliseconds between slides,

		@returns Instance
		*/

		setPlaytime: function (delay) {
			this._playtime = delay;
			return this;
		},

		setIndex: function (val) {
			this._active = val;
			return this;
		},

		/**
		Manually modify the counter

		@param {number} index Optional data index to fectch,
		if no index found it assumes the currently active index

		@returns Instance
		*/

		setCounter: function (index) {

			if (typeof index === 'number') {
				index++;
			} else if (typeof index === 'undefined') {
				index = this.getIndex() + 1;
			}

			this.get('current').innerHTML = index;

			if (IE) { // weird IE bug

				var count = this.$('counter'),
                opacity = count.css('opacity');

				if (parseInt(opacity, 10) === 1) {
					Utils.removeAlpha(count[0]);
				} else {
					this.$('counter').css('opacity', opacity);
				}

			}

			return this;
		},

		/**
		Manually set captions

		@param {number} index Optional data index to fectch and apply as caption,
		if no index found it assumes the currently active index

		@returns Instance
		*/

		setInfo: function (index) {

			var self = this,
            data = this.getData(index);

			$.each(['title', 'description'], function (i, type) {

				var elem = self.$('info-' + type);

				if (!!data[type]) {
					elem[data[type].length ? 'show' : 'hide']().html(data[type]);
				} else {
					elem.empty().hide();
				}
			});

			return this;
		},

		/**
		Checks if the data contains any captions

		@param {number} index Optional data index to fectch,
		if no index found it assumes the currently active index.

		@returns {boolean}
		*/

		hasInfo: function (index) {

			var check = 'title description'.split(' '),
            i;

			for (i = 0; check[i]; i++) {
				if (!!this.getData(index)[check[i]]) {
					return true;
				}
			}
			return false;

		},

		jQuery: function (str) {

			var self = this,
            ret = [];

			$.each(str.split(','), function (i, elemId) {
				elemId = $.trim(elemId);

				if (self.get(elemId)) {
					ret.push(elemId);
				}
			});

			var jQ = $(self.get(ret.shift()));

			$.each(ret, function (i, elemId) {
				jQ = jQ.add(self.get(elemId));
			});

			return jQ;

		},

		/**
		Converts element IDs into a jQuery collection
		You can call for multiple IDs separated with commas.

		@param {string} str One or more element IDs (comma-separated)

		@returns jQuery

		@example this.$('info,container').hide();
		*/

		$: function (str) {
			return this.jQuery.apply(this, Utils.array(arguments));
		}

	};

	// End of Galleria prototype

	// Add events as static variables
	$.each(_events, function (i, ev) {

		// legacy events
		var type = /_/.test(ev) ? ev.replace(/_/g, '') : ev;

		Galleria[ev.toUpperCase()] = 'galleria.' + type;

	});

	$.extend(Galleria, {

		// Browser helpers
		IE9: IE === 9,
		IE8: IE === 8,
		IE7: IE === 7,
		IE6: IE === 6,
		IE: IE,
		WEBKIT: /webkit/.test(NAV),
		SAFARI: /safari/.test(NAV),
		CHROME: /chrome/.test(NAV),
		QUIRK: (IE && doc.compatMode && doc.compatMode === "BackCompat"),
		MAC: /mac/.test(navigator.platform.toLowerCase()),
		OPERA: !!window.opera,
		IPHONE: /iphone/.test(NAV),
		IPAD: /ipad/.test(NAV),
		ANDROID: /android/.test(NAV),
		TOUCH: ('ontouchstart' in doc)

	});

	// Galleria static methods

	/**
	Adds a theme that you can use for your Gallery

	@param {Object} theme Object that should contain all your theme settings.
	<ul>
	<li>name - name of the theme</li>
	<li>author - name of the author</li>
	<li>css - css file name (not path)</li>
	<li>defaults - default options to apply, including theme-specific options</li>
	<li>init - the init function</li>
	</ul>

	@returns {Object} theme
	*/

	Galleria.addTheme = function (theme) {

		// make sure we have a name
		if (!theme.name) {
			Galleria.raise('No theme name specified');
		}

		if (typeof theme.defaults !== 'object') {
			theme.defaults = {};
		} else {
			theme.defaults = _legacyOptions(theme.defaults);
		}

		var css = false,
        reg;

		if (typeof theme.css === 'string') {

			// look for manually added CSS
			$('link').each(function (i, link) {
				reg = new RegExp(theme.css);
				if (reg.test(link.href)) {

					// we found the css
					css = true;

					// the themeload trigger
					_themeLoad(theme);

					return false;
				}
			});

			// else look for the absolute path and load the CSS dynamic
			if (!css) {

				$('script').each(function (i, script) {

					// look for the theme script
					reg = new RegExp('galleria\\.' + theme.name.toLowerCase() + '\\.');
					if (reg.test(script.src)) {

						// we have a match
						css = script.src.replace(/[^\/]*$/, '') + theme.css;

						Utils.addTimer("css", function () {
							Utils.loadCSS(css, 'galleria-theme', function () {

								// the themeload trigger
								_themeLoad(theme);

							});
						}, 1);

					}
				});
			}

			if (!css) {
				Galleria.raise('No theme CSS loaded');
			}
		} else {

			// pass
			_themeLoad(theme);
		}
		return theme;
	};

	/**
	loadTheme loads a theme js file and attaches a load event to Galleria

	@param {string} src The relative path to the theme source file

	@param {Object} [options] Optional options you want to apply
	*/

	Galleria.loadTheme = function (src, options) {

		var loaded = false,
        length = _galleries.length,
        err = window.setTimeout(function () {
        	Galleria.raise("Theme at " + src + " could not load, check theme path.", true);
        }, 5000);

		// first clear the current theme, if exists
		Galleria.theme = undef;

		// load the theme
		Utils.loadScript(src, function () {

			window.clearTimeout(err);

			// check for existing galleries and reload them with the new theme
			if (length) {

				// temporary save the new galleries
				var refreshed = [];

				// refresh all instances
				// when adding a new theme to an existing gallery, all options will be resetted but the data will be kept
				// you can apply new options as a second argument
				$.each(Galleria.get(), function (i, instance) {

					// mix the old data and options into the new instance
					var op = $.extend(instance._original.options, {
						data_source: instance._data
					}, options);

					// remove the old container
					instance.$('container').remove();

					// create a new instance
					var g = new Galleria();

					// move the id
					g._id = instance._id;

					// initialize the new instance
					g.init(instance._original.target, op);

					// push the new instance
					refreshed.push(g);
				});

				// now overwrite the old holder with the new instances
				_galleries = refreshed;
			}

		});
	};

	/**
	Retrieves a Galleria instance.

	@param {number} [index] Optional index to retrieve.
	If no index is supplied, the method will return all instances in an array.

	@returns Instance or Array of instances
	*/

	Galleria.get = function (index) {
		if (!!_instances[index]) {
			return _instances[index];
		} else if (typeof index !== 'number') {
			return _instances;
		} else {
			Galleria.raise('Gallery index ' + index + ' not found');
		}
	};

	/**
	Creates a transition to be used in your gallery

	@param {string} name The name of the transition that you will use as an option

	@param {Function} fn The function to be executed in the transition.
	The function contains two arguments, params and complete.
	Use the params Object to integrate the transition, and then call complete when you are done.

	*/

	Galleria.addTransition = function (name, fn) {
		_transitions[name] = fn;
	};

	/**
	The Galleria utilites
	*/

	Galleria.utils = Utils;

	/**
	A helper metod for cross-browser logging.
	It uses the console log if available otherwise it falls back to alert

	@example Galleria.log("hello", document.body, [1,2,3]);
	*/

	Galleria.log = (function () {
		if ('console' in window && 'log' in window.console) {
			return window.console.log;
		} else {
			return function () {
				window.alert(Utils.array(arguments).join(', '));
			};
		}
	} ());

	/**
	A ready method for adding callbacks when a gallery is ready
	Each method is call before the extend option for every instance

	@param {function} callback The function to call
	*/

	Galleria.ready = function (fn) {
		$.each(_galleries, function (i, gallery) {
			fn.call(gallery, gallery._options);
		});
		Galleria.ready.callbacks.push(fn);
	};

	Galleria.ready.callbacks = [];

	/**
	Method for raising errors

	@param {string} msg The message to throw

	@param {boolean} [fatal] Set this to true to override debug settings and display a fatal error
	*/

	Galleria.raise = function (msg, fatal) {

		var type = fatal ? 'Fatal error' : 'Error',

        self = this,

        echo = function (msg) {

        	var html = '<div style="padding:4px;margin:0 0 2px;background:#' +
                (fatal ? '811' : '222') + '";>' +
                (fatal ? '<strong>' + type + ': </strong>' : '') +
                msg + '</div>';

        	$.each(_instances, function () {

        		var cont = this.$('errors'),
                    target = this.$('target');

        		if (!cont.length) {

        			target.css('position', 'relative');

        			cont = this.addElement('errors').appendChild('target', 'errors').$('errors').css({
        				color: '#fff',
        				position: 'absolute',
        				top: 0,
        				left: 0,
        				zIndex: 100000
        			});
        		}

        		cont.append(html);

        	});
        };

		// if debug is on, display errors and throw exception if fatal
		if (DEBUG) {
			echo(msg);
			if (fatal) {
				throw new Error(type + ': ' + msg);
			}

			// else just echo a silent generic error if fatal
		} else if (fatal) {
			if (_hasError) {
				return;
			}
			_hasError = true;
			fatal = false;
			echo('Image gallery could not load.');
		}
	};

	// Add the version
	Galleria.version = VERSION;

	/**
	A method for checking what version of Galleria the user has installed and throws a readable error if the user needs to upgrade.
	Useful when building plugins that requires a certain version to function.

	@param {number} version The minimum version required

	@param {string} [msg] Optional message to display. If not specified, Galleria will throw a generic error.
	*/

	Galleria.requires = function (version, msg) {
		msg = msg || 'You need to upgrade Galleria to version ' + version + ' to use one or more components.';
		if (Galleria.version < version) {
			Galleria.raise(msg, true);
		}
	};

	/**
	Adds preload, cache, scale and crop functionality

	@constructor

	@requires jQuery

	@param {number} [id] Optional id to keep track of instances
	*/

	Galleria.Picture = function (id) {

		// save the id
		this.id = id || null;

		// the image should be null until loaded
		this.image = null;

		// Create a new container
		this.container = Utils.create('galleria-image');

		// add container styles
		$(this.container).css({
			overflow: 'hidden',
			position: 'relative' // for IE Standards mode
		});

		// saves the original measurements
		this.original = {
			width: 0,
			height: 0
		};

		// flag when the image is ready
		this.ready = false;

		// placeholder for the timeout
		this.tid = null;

	};

	Galleria.Picture.prototype = {

		// the inherited cache object
		cache: {},

		// show the image on stage
		show: function () {
			Utils.show(this.image);
		},

		// hide the image
		hide: function () {
			Utils.moveOut(this.image);
		},

		clear: function () {
			this.image = null;
		},

		/**
		Checks if an image is in cache

		@param {string} src The image source path, ex '/path/to/img.jpg'

		@returns {boolean}
		*/

		//SETFIX (vymaze cache cimz vynuti reload)

		isCached: function (src, forceDelete) {
			//if (forceDelete) delete this.cache[src];
			return !!this.cache[src];
		},

		/**
		Preloads an image into the cache

		@param {string} src The image source path, ex '/path/to/img.jpg'

		@returns Galleria.Picture
		*/

		preload: function (src) {
			$(new Image()).load((function (src, cache) {
				return function () {
					cache[src] = src;
				};
			} (src, this.cache))).attr('src', src);
		},

		/**
		Loads an image and call the callback when ready.
		Will also add the image to cache.

		@param {string} src The image source path, ex '/path/to/img.jpg'
		@param {Function} callback The function to be executed when the image is loaded & scaled

		@returns The image container (jQuery object)
		*/

		load: function (src, callback) {

			// set a load timeout for debugging
			this.tid = window.setTimeout((function (src) {
				return function () {
					Galleria.raise('Image not loaded in ' + Math.round(TIMEOUT / 1000) + ' seconds: ' + src);
				};
			} (src)), TIMEOUT);

			this.image = new Image();

			var i = 0,
            reload = false,

			// some jquery cache
            $container = $(this.container),
            $image = $(this.image),

			// the onload method
            onload = (function (self, callback, src) {

            	return function () {

            		var complete = function () {

            			// save the original size
            			self.original = {
            				height: this.height,
            				width: this.width
            			};

            			self.cache[src] = src; // will override old cache

            			// clear the debug timeout
            			window.clearTimeout(self.tid);

            			if (typeof callback == 'function') {
            				window.setTimeout(function () {
            					callback.call(self, self);
            				}, 1);
            			}
            		};

            		// Delay the callback to "fix" the Adblock Bug
            		// http://code.google.com/p/adblockforchrome/issues/detail?id=3701
            		if ((!this.width || !this.height)) {
            			window.setTimeout((function (img) {
            				return function () {
            					if (img.width && img.height) {
            						complete.call(img);
            					} else {
            						Galleria.raise('Could not extract width/height from image: ' + img.src +
                                        '. Traced measures: width:' + img.width + 'px, height: ' + img.height + 'px.');
            					}
            				};
            			} (this)), 2);
            		} else {
            			complete.call(this);
            		}
            	};
            } (this, callback, src));

			// remove any previous images
			$container.find('img').remove();

			// append the image
			$image.css('display', 'block').appendTo(this.container);

			// hide it for now
			Utils.hide(this.image);

			if (this.cache[src]) {

				// no need to load if the image is cached, just call onload and set source
				this.image.src = src;

				onload.call(this.image);
				return this.container;
			}

			// begin load and insert in cache when done
			$(this.image).load(onload).error(function () {
				if (!reload) {
					reload = true;
					// reload the image with a timestamp
					window.setTimeout((function (image, src) {
						return function () {
							image.attr('src', src + '?' + Utils.timestamp());
						};
					} ($(this), src)), 50);
				} else {
					// apply the dummy image if it exists
					if (DUMMY) {
						$(this).attr('src', DUMMY);
					} else {
						Galleria.raise('Image not found: ' + src);
					}
				}
			}).attr('src', src);

			// return the container
			return this.container;
		},

		/**
		Scales and crops the image

		@param {Object} options The method takes an object with a number of options:

		<ul>
		<li>width - width of the container</li>
		<li>height - height of the container</li>
		<li>min - minimum scale ratio</li>
		<li>max - maximum scale ratio</li>
		<li>margin - distance in pixels from the image border to the container</li>
		<li>complete - a callback that fires when scaling is complete</li>
		<li>position - positions the image, works like the css background-image property.</li>
		<li>crop - defines how to crop. Can be true, false, 'width' or 'height'</li>
		<li>canvas - set to true to try a canvas-based rescale</li>
		</ul>

		@returns The image container object (jQuery)
		*/

		scale: function (options) {

			// extend some defaults
			options = $.extend({
				width: 0,
				height: 0,
				min: undef,
				max: undef,
				margin: 0,
				complete: function () { },
				position: 'center',
				crop: false,
				canvas: false
			}, options);

			// return the element if no image found
			if (!this.image) {
				return this.container;
			}

			// store locale variables
			var width,
            height,
            self = this,
            $container = $(self.container),
            data;

			// wait for the width/height
			Utils.wait({
				until: function () {
					width = options.width ||
                         $container.width() ||
                         Utils.parseValue($container.css('width'));

					height = options.height ||
                         $container.height() ||
                         Utils.parseValue($container.css('height'));

					return width && height;
				},
				success: function () {
					// calculate some cropping
					var newWidth = (width - options.margin * 2) / self.original.width,
                    newHeight = (height - options.margin * 2) / self.original.height,
                    cropMap = {
                    	'true': Math.max(newWidth, newHeight),
                    	'width': newWidth,
                    	'height': newHeight,
                    	'false': Math.min(newWidth, newHeight)
                    },
                    ratio = cropMap[options.crop.toString()],
                    canvasKey = '';

					// allow max_scale_ratio
					if (options.max) {
						ratio = Math.min(options.max, ratio);
					}

					// allow min_scale_ratio
					if (options.min) {
						ratio = Math.max(options.min, ratio);
					}

					$.each(['width', 'height'], function (i, m) {
						$(self.image)[m](self[m] = self.image[m] = Math.round(self.original[m] * ratio));
					});

					$(self.container).width(width).height(height);

					if (options.canvas && _canvas) {

						_canvas.elem.width = self.width;
						_canvas.elem.height = self.height;

						canvasKey = self.image.src + ':' + self.width + 'x' + self.height;

						self.image.src = _canvas.cache[canvasKey] || (function (key) {

							_canvas.context.drawImage(self.image, 0, 0, self.original.width * ratio, self.original.height * ratio);

							try {

								data = _canvas.elem.toDataURL();
								_canvas.length += data.length;
								_canvas.cache[key] = data;
								return data;

							} catch (e) {
								return self.image.src;
							}

						} (canvasKey));

					}

					// calculate image_position
					var pos = {},
                    mix = {},
                    getPosition = function (value, measure, margin) {
                    	var result = 0;
                    	if (/\%/.test(value)) {
                    		var flt = parseInt(value, 10) / 100,
                                m = self.image[measure] || $(self.image)[measure]();

                    		result = Math.ceil(m * -1 * flt + margin * flt);
                    	} else {
                    		result = Utils.parseValue(value);
                    	}
                    	return result;
                    },
                    positionMap = {
                    	'top': { top: 0 },
                    	'left': { left: 0 },
                    	'right': { left: '100%' },
                    	'bottom': { top: '100%' }
                    };

					$.each(options.position.toLowerCase().split(' '), function (i, value) {
						if (value === 'center') {
							value = '50%';
						}
						pos[i ? 'top' : 'left'] = value;
					});

					$.each(pos, function (i, value) {
						if (positionMap.hasOwnProperty(value)) {
							$.extend(mix, positionMap[value]);
						}
					});

					pos = pos.top ? $.extend(pos, mix) : mix;

					pos = $.extend({
						top: '50%',
						left: '50%'
					}, pos);

					// apply position
					$(self.image).css({
						position: 'absolute',
						top: getPosition(pos.top, 'height', height),
						left: getPosition(pos.left, 'width', width)
					});

					// show the image
					self.show();

					// flag ready and call the callback
					self.ready = true;
					options.complete.call(self, self);

				},
				error: function () {
					Galleria.raise('Could not scale image: ' + self.image.src);
				},
				timeout: 1000
			});
			return this;
		}
	};

	// our own easings
	$.extend($.easing, {

		galleria: function (_, t, b, c, d) {
			if ((t /= d / 2) < 1) {
				return c / 2 * t * t * t + b;
			}
			return c / 2 * ((t -= 2) * t * t + 2) + b;
		},

		galleriaIn: function (_, t, b, c, d) {
			return c * (t /= d) * t + b;
		},

		galleriaOut: function (_, t, b, c, d) {
			return -c * (t /= d) * (t - 2) + b;
		}

	});

	// the plugin initializer
	$.fn.galleria = function (options) {

		return this.each(function () {
			$(this).data('galleria', new Galleria().init(this, options));
		});

	};

	// phew

} (jQuery));
