/**
 * @author Vlad Yakovlev (red.scorpix@gmail.com)
 * @link www.scorpix.ru
 * @copyright Art.Lebedev Studio (http://www.artlebedev.ru)
 * @version 0.2.12
 * @date 2009-12-04
 * @requires jQuery
 * @requires jTweener
 */

/**
 * @type changelog
 *
 * Version 0.2.12 (2009-12-04)
 * Исправлен баг в uGallery.history. Теперь в него можно передавать параметр firstId — первое текущее изображение. Текущая картинка не будет подгружаться еще раз.
 */

/**
 * События
 *
 * loaded — галерея готова к работе.
 *
 * pause — запаузить галерею. Ее можно будет скрывать.
 *
 * resume — возобновить работу галереи. Галерея должна быть display: block;
 *
 * imageSelect — выбор изображения (перемещение полосы превьюшек к нему, выбор нового).
 * index — индекс выбранного изображения.
 * imageInfo — информация о выбранном изображении.
 *
 * imageSelected — выбрано новое текущее изображение (приходит после imageSelect).
 * index — индекс выбранного изображения.
 * imageInfo — информация о выбранном изображении.
 *
 * imageLoaded — загружено изображение.
 * index — индекс загруженного изображения.
 *
 * previewsUpdate — нужно обновить контент.
 *
 * previewsUpdated — контент с превьюшками обновлен.
 * moveX — длина и направление, куда сдвинут контент.
 *
 * previewsMove — контент нужно переместить.
 * toLeft — конечная позиция перемещения.
 * [speed] — начальная скорость.
 * [onComplete] — функция, выполняемая по окончании перемещения.
 *
 * previewClick - кликнули на превьюшку в полосе превьюшек.
 * current - информация о новой текущей фотографии.
 * prev - информация о новой предыдущей фотографии.
 * next - информация о новой следующей фотографии.
 */

var uGallery = {};


/**
 * Ядро галереи, хранящее все основные параметры.
 * @return {uGallery.core}
 */
uGallery.core = function(options) {

	var localCriterion = 'file:///';

	var state = function(className) {
		var state = false;

		return function(newState) {
			if (undefined === newState) return state;

			state = newState;

			if (state) {
				this.previewEl.addClass(className);
				this.viewEl.addClass(className);
			} else {
				this.previewEl.removeClass(className);
				this.viewEl.removeClass(className);
			}
		};
	};

	var previewLeft = function() {
		var pos = 0;

		return function(newPos, notUpdateEl) {
			if (undefined === newPos) return pos;

			pos = parseInt(newPos, 10);

			if (!notUpdateEl) {
				this.previewContentEl.css('left', pos);
			}
		};
	};

	var defOptions = {
		/**
		 * Главный элемент полосы превьюшек.
		 * @type {jQuery}
		 */
		previewEl: null,
		/**
		 * Элемент контейнера полосы превьюшек — видимая часть.
		 * @type {jQuery}
		 */
		previewContainerEl: null,
		/**
		 * Элемент контента полосы превьюшек — длиннющий блок.
		 * @type {jQuery}
		 */
		previewContentEl: null,
		/**
		 * Контейнер изображений-превьюшек — равен по ширине всем превьюшкам.
		 * @type {jQuery}
		 */
		previewPicturesEl: null,
		/**
		 * Контейнер больших изображений.
		 * @type {jQuery}
		 */
		viewEl: null,
		/**
		 * Флаг анимации.
		 * @type {Boolean}
		 */
		animated: state('animate'),
		/** Диспетчер событий галереи. */
		eventDispatcher: null,
		/** Геттер и сеттер текущего горизонтального положения блока с превьюшками, в пикселях. */
		previewLeft: previewLeft(),
		/** Объект, отвечающий за загрузку и хранение информации об изображениях галереи. */
		loader: null,
		/** Локальный скрипт или на сервере. Для отладочных целей. */
		isLocal: localCriterion == location.href.substr(0, localCriterion.length),
		/** Геттер и сеттер занятости галереи. Если галерея занята, то ничего делать с нею нельзя. */
		busy: state('busy'),
		/** Ширина прелоадера в полосе превьюшек. */
		preloaderWidth: 0,
		/** Геттер и сеттер того, что галерея находится на паузе. */
		paused: state('pause')
	};

	return $.extend({}, defOptions, options ? options : {});
};


uGallery.abstractLoader = function(core) {
	var
		/** Индекс текущего изображения. Индекс может быть и отрицательным числом. Ноль имеет первое загруженное изображение. */
		selIndex = 0,
		loaded = [],
		/** Навигация по массиву <code>loaded</code>. */
		loadedIndexes = {},
		/** Название свойства — идентификатор изображения, значение — индекс изображения. */
		loadedIds = {},
		/** Индекс первой превьюшки, данные о которой загружены. */
		loadedStart = 0;

	function selectedIndex(newIndex) {
		if (undefined === newIndex) return selIndex;

		selIndex = newIndex;
	}

	function selected() {
		return item(selIndex);
	}

	function insert(data, isPrev) {
		loaded.push(data);

		var index;

		if (isPrev) {
			loadedStart--;
			index = loadedStart;
		} else {
			index = loadedStart + loaded.length - 1;
		}

		loaded[loaded.length - 1].index = index;
		loadedIndexes[index] = loaded.length - 1;
		loadedIds[item(index).id] = index;
		uGallery.loadImage(item(index).preview.src);
	}

	function item(index) {
		return undefined === loadedIndexes[index] ? false : loaded[loadedIndexes[index]];
	}

	function itemById(id) {
		return undefined === loadedIds[id] ? false : item(loadedIds[id]);
	}

	function indexById(id) {
		return undefined === loadedIds[id] ? false : loadedIds[id];
	}

	return {
		count: function() { return loaded.length; },
		first: function() { return loadedStart; },
		indexById: indexById,
		insert: insert,
		item: item,
		itemById: itemById,
		selectedIndex: selectedIndex,
		selected: selected
	};
};


/**
 * Загрузчик данных об изображениях и их превьюшках в формате XML.
 *
 * @param {uGallery.core} core
 * @param {Object} options Настройки:
 * @option {String} ajaxUrl Адрес, откуда запрашивать.
 * @option {Number} imageId Идентификатор изображения, по которому производится первый запрос.
 * @option {Number} [limit = 50] Количество изображений, о которых запрашиваются данные в одном запросе.
 * @option {Object} [ajaxParams] Дополнительные параметры для запроса.
 * @option {Number} [tryCount = 3] - количество попыток при неудачном окончании Ajax-запроса.
 * @return {uGallery.ajaxLoader}
 */
uGallery.ajaxLoader = function(core, options) {

	options = $.extend({}, {
		tryCount: 3,
		ajaxParams: {},
		limit: 30
	}, options);

	var
		/** Данные о первом запросе. */
		firstRequest,
		/** Данные о запросе для предыдущих изображений. */
		prevRequest,
		/** Данные о запросе для последующих изображений. */
		nextRequest,
		/** Флаг того, что изображений слева больше нет. */
		isFirst = false,
		/** Флаг того, что изображений справа больше нет. */
		isLast = false,
		onInited,
		onLoaded,
		parent = uGallery.abstractLoader(core);

	function init(onInit, onLoad) {
		onInited = onInit;
		onLoaded = onLoad;
		options.ajaxParams.limit = options.limit;

		/**
		 * Первый запрос с фоткой посередине.
		 */

		var params = {};
		$.extend(params, options.ajaxParams);
		params.id = options.imageId;
		params.gallery_id = options.galleryId;

		firstRequest = {
			id: params.id,
			gallery_id: params.gallery_id,
			counter: 1,
			params: params
		};
		load(params);
	}

	/**
	 * Отправляет запрос на предыдущие изображения.
	 *
	 * @return {Boolean} Запрос пошел.
	 */
	function loadPrev() {
		if (0 >= parent.count()) return false;
		// Аналогичный запрос в процессе или больше нет данных слева.
		if (firstRequest || prevRequest || isFirst) return false;

		var image = parent.item(parent.first());
		if (!image) return;

		var params = {};
		$.extend(params, options.ajaxParams);
		params.id = image.id;
		params.direction = -1;

		prevRequest = {
			id: params.id,
			counter: 1,
			params: params
		};
		load(params);

		return true;
	}

	/**
	 * Отправляет запрос на следующие изображения.
	 *
	 * @return {Boolean} Запрос пошел.
	 */
	function loadNext() {
		if (0 >= parent.count()) return false;
		// Аналогичный запрос в процессе или больше нет данных справа.
		if (firstRequest || nextRequest || isLast) return false;

		var image = parent.item(parent.first() + parent.count() - 1);
		if (!image) return;

		var params = {};
		$.extend(params, options.ajaxParams);
		params.id = image.id;
		params.direction = 1;

		nextRequest = {
			id: params.id,
			counter: 1,
			params: params
		};
		load(params);

		return true;
	}

	/**
	 * Запрос по параметрам.
	 *
	 * @param {Object} params Ajax-параметры.
	 */
	function load(params) {
		try {
			$.ajax( {
				imageId: params.id,
				data: params,
				dataType: 'text',
				error: onLoadError,
				success: onLoadSuccess,
				timeout: 2000,
				type: 'GET',
				url: core.isLocal ? './i/temp/gallery_ajax_' + params.id + 'xml.htm' : options.ajaxUrl
			});
		} catch (e) { }
	}

	/**
	 * Вызывается при удачном Ajax-запросе.
	 *
	 * @param {Object} data
	 * @param {String} textStatus
	 */
	function onLoadSuccess(data, textStatus) {
		if ('success' != textStatus) return;
		if (!data) return;

		var
			responseEl = $('root', uGallery.getXml(data.replace('&', '&amp;'))).find('>response'),
			els = responseEl.find('>d'),
			imagesLength = els.length,
			previewSrcPrefix = responseEl.attr('p_src_prefix'),
			imageSrcPrefix = responseEl.attr('i_src_prefix');

		if (prevRequest && prevRequest.id == this.imageId) {
			prevRequest = false;

			for (var i = els.length - 1; i >= 0; i--) {
				parent.insert(fillInfo(els.eq(i), previewSrcPrefix, imageSrcPrefix), true);
			}

			if (1 != parseInt(responseEl.attr('has_prev'), 10)) {
				isFirst = true;
			}

			onLoaded && onLoaded();

		} else if (nextRequest && nextRequest.id == this.imageId) {
			nextRequest = false;

			els.each(function() {
				parent.insert(fillInfo($(this), previewSrcPrefix, imageSrcPrefix));
			});

			if (1 != parseInt(responseEl.attr('has_next'), 10)) {
				isLast = true;
			}

			onLoaded && onLoaded();

		} else if (firstRequest && firstRequest.id == this.imageId) {
			firstRequest = false;

			els.each(function() {
				parent.insert(fillInfo($(this), previewSrcPrefix, imageSrcPrefix));
			});

			if (1 != parseInt(responseEl.attr('has_prev'), 10)) {
				isFirst = true;
			}
			if (1 != parseInt(responseEl.attr('has_next'), 10)) {
				isLast = true;
			}

			onInited && onInited();
		}
	}

	/**
	 * Вызывается при ошибке. Пытается снова отправить запрос.
	 */
	function onLoadError() {
		if (prevRequest && this.imageId == prevRequest.id) {
			if (options.tryCount >= ++prevRequest.counter) {
				load(prevRequest.params);
			} else {
				prevRequest = false;
				isFirst = true;
			}
		} else if (nextRequest && this.imageId == nextRequest.id) {
			if (options.tryCount >= ++nextRequest.counter) {
				load(nextRequest.params);
			} else {
				nextRequest = false;
				isLast = true;
			}
		}
	}

	function fillInfo(el, previewSrcPrefix, imageSrcPrefix) {
		var
			previewEl = el.find('>p'),
			imageEl = el.find('>i'),
			id = el.attr('id');

		return {
			id: id,
			href: el.attr('href'),
			preview: {
				src: previewSrcPrefix + previewEl.attr('src'),
				width: parseInt(previewEl.attr('w'), 10),
				height: parseInt(previewEl.attr('h'), 10)
			},
			image: {
				src: imageSrcPrefix + imageEl.attr('src'),
				width: parseInt(imageEl.attr('w'), 10),
				height: parseInt(imageEl.attr('h'), 10)
			},
			info: el[0]
		};
	}

	return {
		count: parent.count,
		first: parent.first,
		init: init,
		indexById: parent.indexById,
		isLeftBorder: function() { return isFirst; },
		isRightBorder: function() { return isLast; },
		item: parent.item,
		itemById: parent.itemById,
		loadNext: loadNext,
		loadPrev: loadPrev,
		selectedIndex: parent.selectedIndex,
		selected: parent.selected
	};
};


/**
 * Загрузчик данных об изображениях и их превьюшках в формате JSON.
 *
 * @param {uGallery.core} core
 * @param {Object} options Настройки:
 *   <ul>
 *     <li>String ajaxUrl - адрес, откуда запрашивать,</li>
 *     <li>Number imageId - идентификатор изображения, по которому производится первый запрос,</li>
 *     <li>Number [limit = 50] - количество изображений, о которых запрашиваются данные в одном запросе,</li>
 *     <li>Object [ajaxParams] - дополнительные параметры для запроса,</li>
 *     <li>Number [tryCount = 3] - количество попыток при неудачном окончании Ajax-запроса.</li>
 *   </ul>
 * @return {uGallery.jsonLoader}
 */
uGallery.jsonLoader = function(core, options) {

	options = $.extend({}, {
		tryCount: 3,
		ajaxParams: {},
		limit: 30
	}, options);

	var
		/** Данные о первом запросе. */
		firstRequest,
		/** Данные о запросе для предыдущих изображений. */
		prevRequest,
		/** Данные о запросе для последующих изображений. */
		nextRequest,
		/** Флаг того, что изображений слева больше нет. */
		isFirst = false,
		/** Флаг того, что изображений справа больше нет. */
		isLast = false,
		onInited,
		onLoaded,
		parent = uGallery.abstractLoader(core);

	function init(onInit, onLoad) {
		onInited = onInit;
		onLoaded = onLoad;
		options.ajaxParams.limit = options.limit;

		/**
		 * Первый запрос с фоткой посередине.
		 */

		var params = {};
		$.extend(params, options.ajaxParams);
		params.id = options.imageId;

		firstRequest = {
			id: params.id,
			counter: 1,
			params: params
		};
		load(params);
	}

	/**
	 * Отправляет запрос на предыдущие изображения.
	 *
	 * @return {Boolean} Запрос пошел.
	 */
	function loadPrev() {
		if (0 >= parent.count()) return false;
		// Аналогичный запрос в процессе или больше нет данных слева.
		if (firstRequest || prevRequest || isFirst) return false;

		var image = parent.item(parent.first());
		if (!image) return;

		var params = {};
		$.extend(params, options.ajaxParams);
		params.id = image.id;
		params.direction = -1;

		prevRequest = {
			id: params.id,
			counter: 1,
			params: params
		};
		load(params);

		return true;
	}

	/**
	 * Отправляет запрос на следующие изображения.
	 *
	 * @return {Boolean} Запрос пошел.
	 */
	function loadNext() {
		if (0 >= parent.count()) return false;
		// Аналогичный запрос в процессе или больше нет данных справа.
		if (firstRequest || nextRequest || isLast) return false;

		var image = parent.item(parent.first() + parent.count() - 1);
		if (!image) return;

		var params = {};
		$.extend(params, options.ajaxParams);
		params.id = image.id;
		params.direction = 1;

		nextRequest = {
			id: params.id,
			counter: 1,
			params: params
		};
		load(params);

		return true;
	}

	/**
	 * Запрос по параметрам.
	 *
	 * @param {Object} params Ajax-параметры.
	 */
	function load(params) {
		try {
			$.ajax( {
				imageId: params.id,
				data: params,
				dataType: 'json',
				error: onLoadError,
				success: onLoadSuccess,
				timeout: 2000,
				type: 'GET',
				url: core.isLocal ? './i/temp/gallery_ajax_' + params.id + '.htm' : options.ajaxUrl
			});
		} catch (e) { }
	}

	/**
	 * Вызывается при удачном Ajax-запросе.
	 *
	 * @param {Object} data
	 * @param {String} textStatus
	 */
	function onLoadSuccess(data, textStatus) {
		if ('success' != textStatus) return;
		if (!data) return;

		var els = data.images;

		if (prevRequest && prevRequest.id == this.imageId) {
			prevRequest = false;

			els.reverse();

			$.each(els, function() {
				parent.insert(fillInfo(this, data.pSrcPrefix, data.iSrcPrefix), true);
			});

			if (1 != data.hasPrev) {
				isFirst = true;
			}

			onLoaded && onLoaded();

		} else if (nextRequest && nextRequest.id == this.imageId) {
			nextRequest = false;

			$.each(els, function() {
				parent.insert(fillInfo(this, data.pSrcPrefix, data.iSrcPrefix));
			});

			if (1 != data.hasNext) {
				isLast = true;
			}

			onLoaded && onLoaded();

		} else if (firstRequest && firstRequest.id == this.imageId) {
			firstRequest = false;

			$.each(els, function() {
				parent.insert(fillInfo(this, data.pSrcPrefix, data.iSrcPrefix));
			});

			if (1 != data.hasPrev) {
				isFirst = true;
			}
			if (1 != data.hasNext) {
				isLast = true;
			}

			onInited && onInited();
		}
	}

	/**
	 * Вызывается при ошибке. Пытается снова отправить запрос.
	 */
	function onLoadError() {
		if (prevRequest && this.imageId == prevRequest.id) {
			if (options.tryCount >= ++prevRequest.counter) {
				load(prevRequest.params);
			} else {
				prevRequest = false;
				isFirst = true;
			}
		} else if (nextRequest && this.imageId == nextRequest.id) {
			if (options.tryCount >= ++nextRequest.counter) {
				load(nextRequest.params);
			} else {
				nextRequest = false;
				isLast = true;
			}
		}
	}

	function fillInfo(el, previewSrcPrefix, imageSrcPrefix) {

		if (previewSrcPrefix) {
			el.preview.src = previewSrcPrefix + el.preview.src;
		}
		if (imageSrcPrefix) {
			el.image.src = imageSrcPrefix + el.image.src;
		}

		return el;
	}

	return {
		count: parent.count,
		first: parent.first,
		indexById: parent.indexById,
		init: init,
		isLeftBorder: function() { return isFirst; },
		isRightBorder: function() { return isLast; },
		item: parent.item,
		itemById: parent.itemById,
		loadPrev: loadPrev,
		loadNext: loadNext,
		selectedIndex: parent.selectedIndex,
		selected: parent.selected
	};
};


/**
 * Загрузчик данных об изображениях и их превьюшках.
 *
 * @param {uGallery.core} core
 * @param {Object} imageData
 * @return {uGallery.arrayLoader}
 */
uGallery.arrayLoader = function(core, imageData) {

	var parent = uGallery.abstractLoader(core);

	var loadedIds = {};
	/** Индекс текущего изображения. Ноль имеет первое загруженное изображение. */
	var selIndex = 0;

	function init(onInit) {

		$.each(imageData, function(i) {
			if (undefined === this.id) {
				this.id = i;
			}

			parent.insert(this);
		});

		onInit && onInit();
	}

	return {
		count: parent.count,
		first: parent.first,
		indexById: parent.indexById,
		init: init,
		isLeftBorder: function() { return true; },
		isRightBorder: function() { return true; },
		item: parent.item,
		itemById: parent.itemById,
		loadPrev: function() { return false; },
		loadNext: function() { return false; },
		selectedIndex: parent.selectedIndex,
		selected: parent.selected
	};
};


/**
 * Отвечает за смену html-элементов превьюшек.
 * @param {uGallery.core} core Ядро галереи.
 * @param {Number} imageId Идентификатор изображения, на котором загрузилась галерея.
 * @param {Object} options Параметры:
 *   <ul>
 *     <li>{Number} maxWidth — максимальная ширина превьюшки,</li>
 *     <li>{Number} [margin = 0] — суммарный отступ каждой превьюшки (padding и margin),</li>
 *     <li>{Number} [maxDeviation = 150] — отклонение, при котором не требуется коррекция расположения блоков. Лучше всего, оно в 2 раза больше максимальной ширины превьюшки,</li>
 *     <li>{Number} [screens = 4] - количество экранов превьюшек в html; если 0, то все превьюшки хранятся в html.
 *   </ul>
 * @return {uGallery.state}
 */
uGallery.state = function(core, imageId, options) {

	var temp = 0;

	options = $.extend({}, {
		maxDeviation: 150,
		margin: 0,
		screens: 4
	}, options ? options : {});

	core.busy(true);
	core.loader.init(load, update);

	/**
	 * Загружает в галерею превьюшки. После этого с галереей можно будет работать.
	 */
	function load() {

		core.eventDispatcher.bind('previewsUpdate', function() {
			update();
		});

		core.eventDispatcher.bind('imageSelect', function(evt) {
			animateToImage(evt.data.index, function() {
				core.eventDispatcher.dispatch('imageSelected', {
					index: evt.data.index,
					imageInfo: evt.data.imageInfo
				});
			});
		});

		core.eventDispatcher.bind('pause', function() {
			core.paused(true);
		});
		core.eventDispatcher.bind('resume', function() {
			core.paused(false);
			update();
		});

		$(window).resize(update);
		update();

		core.busy(false);

		var index = core.loader.indexById(imageId);

		if (false !== index) {
			core.eventDispatcher.dispatch('previewClick', {
				current: core.loader.item(index),
				prev: core.loader.item(index - 1),
				next: core.loader.item(index + 1)
			});
		}

		core.eventDispatcher.dispatch('loaded');
	}

	/**
	 * Обновляет расположение блоков и данные в них.
	 */
	function update() {
		if (core.animated() || core.paused()) return;

		var containerWidth = core.previewContainerEl.width();

		if (!containerWidth) return;

		/** Количество превьюшек, которое должно быть в ленте. */
		var maxElCount;

		if (options.screens) {
			maxElCount = Math.floor(containerWidth * options.screens / (options.maxWidth + options.margin));

			if (core.loader.count() < maxElCount && core.loader.isLeftBorder() && core.loader.isRightBorder()) {
				maxElCount = core.loader.count();
			}
		} else {
			maxElCount = core.loader.count();
		}

		fillContent(maxElCount);

		var
			previewsWidth = core.previewPicturesEl.width(),
			elCount = core.previewPicturesEl.find('img').length,
			elStartIndex = elCount
				? core.previewPicturesEl.find('img:first').data('uIndex')
				: core.loader.first();

		// Если ширина всех изображений в галерее меньше, чем блок галереи.
		var minLeft = 0 > containerWidth - previewsWidth ? containerWidth - previewsWidth : 0;
		var maxLeft = 0;

		if (core.loader.first() + core.loader.count() > elStartIndex + elCount || !core.loader.isRightBorder()) {
			minLeft -= core.preloaderWidth;
		}
		if (core.loader.first() < elStartIndex || !core.loader.isLeftBorder()) {
			maxLeft += core.preloaderWidth;
		}

		if (core.previewLeft() < minLeft) {
			core.previewLeft(minLeft);
		} else if (maxLeft < core.previewLeft()) {
			// Если превьюшки уехали вправо за максимальные пределы.
			core.previewLeft(maxLeft);
		}

		var deviation = core.previewLeft() + Math.round((previewsWidth - containerWidth) / 2);
		var moveX = 0;

		if (options.maxDeviation < deviation) {
			moveX = fromEndToBegin(deviation, maxElCount);
		} else if (-options.maxDeviation > deviation) {
			moveX = fromBeginToEnd(deviation);
		}

		core.eventDispatcher.dispatch('previewsUpdated', { moveX: moveX });
	}

	/**
	 * Приводит количество превьюшек к нужному в зависимости от ширины окна браузера.
	 */
	function fillContent(maxElCount) {
		var elCount = core.previewPicturesEl.find('img').length;
		var elStartIndex = elCount
			? core.previewPicturesEl.find('img:first').data('uIndex')
			: core.loader.first();

		/**
		 * Количество блоков, которое нужно добавить (если положительное) или удалить (если
		 * отрицательное).
		 */
		var operationCount;

		if (options.screens) {
			operationCount = maxElCount - elCount;

			if (0 < operationCount) {
				// Нужно добавить превьюшки.

				/** Индекс, с которого обращаемся к изображениям. */
				var startImage = elStartIndex + core.previewPicturesEl.find('.active').length;

				// Если у нас загружены данные не для всех изображений, которые нам понадобятся,
				// отправляем Ajax-запрос.
				if (core.loader.first() + core.loader.count() < startImage + operationCount) {
					operationCount = core.loader.first() + core.loader.count() - startImage;
					core.loader.loadNext();
				}

				// Добавляем блоки.
				for ( var i = 0; i < operationCount; i++) {
					var el = $('<img />').appendTo(core.previewPicturesEl);
					fillBlock(el, startImage + i);
				}
			} else if (0 > operationCount) {
				// Нужно удалить превьюшки.
				core.previewPicturesEl.find('img').eq(maxElCount - 1).nextAll('img').remove();
			}
		} else {
			// Неограниченное число элементов.

			/** Индекс, с которого обращаемся к изображениям. */
			var startImage = elStartIndex + core.previewPicturesEl.find('.active').length;

			operationCount = core.loader.first() + core.loader.count() - startImage;
			core.loader.loadNext();

			// Добавляем блоки.
			for ( var i = 0; i < operationCount; i++) {
				var el = $('<img />').appendTo(core.previewPicturesEl);
				fillBlock(el, startImage + i);
			}
		}
	}

	/**
	 * Перетягивает блоки из конца в начало.
	 */
	function fromEndToBegin(moved, maxElCount) {

		var
			/** Количество блоков, которое нужно добавить. */
			operationCount = maxElCount - core.previewPicturesEl.find('img').length,
			/** Общая ширина добавляемых блоков. */
			width = 0,
			elStartIndex = core.previewPicturesEl.find('img:first').data('uIndex');

		while (width < moved) {

			var imageInfo = core.loader.item(elStartIndex - 1);

			if (!imageInfo) {
				core.loader.loadPrev();
				break;
			}

			var el;

			if (!options.screens || 0 < operationCount) {
				// Добавляем новый элемент, если в полосе мало превьюшек.
				el = $('<img />').prependTo(core.previewPicturesEl);
				operationCount--;
			} else {
				// Перемещаем из конца в начало элемент.
				el = core.previewPicturesEl.find('img:last').prependTo(core.previewPicturesEl);
			}

			fillBlock(el, elStartIndex - 1);

			width += imageInfo.preview.width + options.margin;
			elStartIndex--;
		}

		if (!width) return 0;

		core.previewLeft(core.previewLeft() - width);

		return -width;
	}

	/**
	 * Перетягивает блоки из начала в конец.
	 */
	function fromBeginToEnd(moved) {

		var
			width = 0,
			elStartIndex = core.previewPicturesEl.find('img:first').data('uIndex'),
			elCount = core.previewPicturesEl.find('img').length,
			index = elStartIndex;

		while (width < -moved) {

			if (!core.loader.item(elStartIndex + elCount)) {
				core.loader.loadNext();
				break;
			}

			// Перемещаем из начала в конец элемент.
			var el = core.previewPicturesEl.find('img:first').attr('class', '').appendTo(core.previewPicturesEl);

			fillBlock(el, elStartIndex + elCount);
			width += core.loader.item(elStartIndex).preview.width + options.margin;
			elStartIndex++;
		}

		if (!width) return 0;

		core.previewLeft(core.previewLeft() + width);

		return width;
	}

	/**
	 * Заполняет элемент превьюшки данными.
	 *
	 * @param {jQuery} el
	 * @param {Number} index
	 */
	function fillBlock(el, index) {
		var image = core.loader.item(index);

		el
			.removeAttr('src')
			.removeAttr('class')
			.addClass('active i_' + index)
			.data('uIndex', index);
		el[0].src = image.preview.src;
		el[0].height = image.preview.height;
		el[0].width = image.preview.width;
	}

	function animateToImage(imageIndex, onComplete) {

		var
			containerWidth = core.previewContainerEl.width(),
			elStartIndex = core.previewPicturesEl.find('img:first').data('uIndex'),
			elCount = core.previewPicturesEl.find('img').length;

		if (imageIndex >= elStartIndex && imageIndex < elStartIndex + elCount) {
			var offsetLeft = Math.round(core.previewPicturesEl.find('.i_' + imageIndex).offset().left - core.previewContainerEl.offset().left);

			// Если превьюшка находится в видимой области, анимировать не надо.
			if (0 < offsetLeft && offsetLeft + core.loader.item(imageIndex).preview.width < containerWidth) {
				onComplete && onComplete();

				return;
			};
		}

		var
			/** Ширина всех блоков, которые должны быть слева. */
			leftWidth = 0,
			/** Количество блоков, которое должно быть слева. */
			leftCount = 0,
			/** Ширина всех блоков, которые должны быть справа. */
			rightWidth = 0,
			/** Количество блоков, которое должно быть справа. */
			rightCount = 0,

			/** Количество блоков, которое можно убрать слева. */
			leftRemoveCount = 0,
			/** Ширина всех блоков, которое можно убрать слева. */
			leftRemoveWidth = 0,
			/** Количество блоков, которое можно убрать справа. */
			rightRemoveCount = 0,
			/** Ширина всех блоков, которое можно убрать справа. */
			rightRemoveWidth = 0,
			previewsWidth = core.previewPicturesEl.width();

		// Вычисляем количество блоков, которое должно быть слева.
		var count = core.loader.first();

		for ( var i = imageIndex - 1; i >= count; i--) {
			if (leftWidth >= containerWidth / 2 || !core.loader.item(i)) break;

			leftWidth += core.loader.item(i).preview.width + options.margin;
			leftCount++;
		}

		// Вычисляем количество блоков, которое должно быть справа.
		count = core.loader.first() + core.loader.count();

		for ( var i = imageIndex + 1; i < count; i++) {
			if (rightWidth >= containerWidth - leftWidth || !core.loader.item(i)) break;

			rightWidth += core.loader.item(i).preview.width + options.margin;
			rightCount++;
		}

		if (leftWidth + rightWidth < containerWidth) {
			var start = imageIndex - 1 - leftCount;
			count = core.loader.first();

			for ( var i = start; i >= count; i--) {
				if (leftWidth + rightWidth >= containerWidth || !core.loader.item(i)) break;

				leftWidth += core.loader.item(i).preview.width + options.margin;
				leftCount++;
			}
		}

		// Вычисляем количество блоков, которое можно убрать слева.
		while (true) {
			var imageInfo = core.loader.item(elStartIndex + leftRemoveCount);
			if (!imageInfo) break;

			var previewWidth = imageInfo.preview.width + options.margin;
			if (0 <= core.previewLeft() + leftRemoveWidth + previewWidth) break;

			leftRemoveWidth += previewWidth;
			leftRemoveCount++;
		}

		// Вычисляем количество блоков, которое можно убрать справа.
		while (true) {
			var imageInfo = core.loader.item(elStartIndex + elCount - 1 - rightRemoveCount);
			if (!imageInfo) break;

			var previewWidth = imageInfo.preview.width + options.margin;
			if (core.previewLeft() + previewsWidth - rightRemoveWidth - previewWidth <= containerWidth) break;

			rightRemoveWidth += previewWidth;
			rightRemoveCount++;
		}

		if (imageIndex < elStartIndex) {
			// Изображения не в HTML элементах.
			// Переносим блоки справа налево.

			var
				/** Ширина новых блоков. */
				newWidth = 0,
				/** Количество новых блоков слева. */
				movedCount = leftCount + rightCount + 1,
				/** Индекс, с которого начинается обратный отсчет. */
				startIndex = imageIndex + rightCount;

			// Если часть нужных превьюшек уже есть в HTML.
			if (startIndex >= elStartIndex) {
				var t = startIndex - elStartIndex + 1;
				movedCount -= t;
				startIndex -= t;
			}

			for ( var i = startIndex; i >= imageIndex - leftCount; i--) {

				var el;

				if (rightRemoveCount) {
					el = core.previewPicturesEl.find('img:last');
					rightRemoveCount--;
				} else {
					el = $('<img />');
				}

				el.prependTo(core.previewPicturesEl);
				fillBlock(el, i);

				newWidth += core.loader.item(i).preview.width + options.margin;
			}

			core.previewLeft(core.previewLeft() - newWidth);

			core.eventDispatcher.dispatch('previewsUpdated', { moveX: -newWidth });

			core.eventDispatcher.dispatch('previewsMove', {
				toLeft: 0,
				onComplete: function() {
					core.previewPicturesEl.find('.i_' + (imageIndex + rightCount)).nextAll('img').remove();
					onComplete && onComplete();
				}
			});

		} else if (imageIndex >= elStartIndex + elCount) {
			// Изображения не в HTML элементах.
			// Переносим блоки слева направо.

			var
				/** Ширина старых блоков. */
				oldWidth = 0,
				/** Количество новых блоков справа. */
				movedCount = leftCount + rightCount + 1,
				/** Индекс, с которого начинается прямой отсчет. */
				startIndex = imageIndex - leftCount;

			// Если часть нужных превьюшек уже есть в HTML.
			if (startIndex <= elStartIndex + elCount - 1) {
				var t = elStartIndex + elCount - startIndex;
				movedCount -= t;
				startIndex += t;
			}

			for ( var i = startIndex; i <= imageIndex + rightCount; i++) {

				var el;

				if (leftRemoveCount) {
					el = core.previewPicturesEl.find('img:first');
					oldWidth += core.loader.item(el.data('uIndex')).preview.width + options.margin;
					leftRemoveCount--;
				} else {
					el = $('<img />');
				}

				el.appendTo(core.previewPicturesEl);
				fillBlock(el, i);
			}

			core.previewLeft(core.previewLeft() + oldWidth);

			core.eventDispatcher.dispatch('previewsUpdated', { moveX: oldWidth });

			var oldPreviewsWidth = core.previewPicturesEl.width();

			core.eventDispatcher.dispatch('previewsMove', {
				toLeft: containerWidth - oldPreviewsWidth,
				onComplete: function() {

					core.previewPicturesEl.find('.i_' + (imageIndex - leftCount)).prevAll('img').remove();

					var newPreviewsWidth = core.previewPicturesEl.width();

					core.previewLeft(core.previewLeft() + oldPreviewsWidth - newPreviewsWidth);

					core.eventDispatcher.dispatch('previewsUpdated', { moveX: oldPreviewsWidth - newPreviewsWidth });

					onComplete && onComplete();
				}
			});
		} else {
			if (!core.previewPicturesEl.find('.i_' + imageIndex).length) {
				onComplete && onComplete();

				return;
			}

			if (elStartIndex > imageIndex - leftCount) {
				// Переносим налево.

				/** Количество перемещаемых блоков. */
				var movedCount = leftCount > rightRemoveCount ? rightRemoveCount : leftCount;
				/** Ширина новых блоков. */
				var newWidth = 0;

				for ( var i = 0; i < movedCount; i++) {
					var el = core.previewPicturesEl.find('img:last');

					fillBlock(el, elStartIndex - 1);
					el.prependTo(core.previewPicturesEl);

					newWidth += core.loader.item(elStartIndex - 1).preview.width + options.margin;
					elStartIndex--;
				}

				core.previewLeft(core.previewLeft() - newWidth);

				core.eventDispatcher.dispatch('previewsUpdated', { moveX: -newWidth });
			} else if (elStartIndex + elCount - 1 < imageIndex + rightCount) {
				// Переносим направо.

				/** Количество перемещаемых блоков. */
				var movedCount = rightCount > leftRemoveCount ? leftRemoveCount : rightCount;
				/** Ширина старых блоков. */
				var oldWidth = 0;

				for ( var i = 0; i < movedCount; i++) {
					var el = core.previewPicturesEl.find('img:first');

					oldWidth += core.loader.item(elStartIndex).preview.width + options.margin;

					fillBlock(el, elStartIndex + elCount);
					el.appendTo(core.previewPicturesEl);
					elStartIndex++;
				}

				core.previewLeft(core.previewLeft() + oldWidth);

				core.eventDispatcher.dispatch('previewsUpdated', { moveX: oldWidth });
			}

			previewsWidth = core.previewPicturesEl.width();

			var pos = Math.round(containerWidth / 2 - core.previewPicturesEl.find('.i_' + imageIndex).position().left);

			if (0 < pos) {
				pos = 0;
			} else if (containerWidth - previewsWidth > pos) {
				pos = containerWidth - previewsWidth;
			}

			core.eventDispatcher.dispatch('previewsMove', {
				toLeft: pos,
				onComplete: function() {
					onComplete && onComplete();
				}
			});
		}
	}
};


/**
 * Переключает изображения.
 *
 * @param {uGallery.core} core Ядро галереи.
 * @return {uGallery.pictures}
 */
uGallery.pictures = function(core) {

	/**
	 * Статусы загрузки изображений по индексам: 1 - загружается, 2 - загрузилось.
	 */
	var loaded = {};

	core.previewContainerEl.click(onGalleryClick);
	core.eventDispatcher.bind('imageSelected', function(evt) {
		setCurrent(evt.data.index);
	});

	/**
	 * Обработчик события клика на полосу превьюшек.
	 * Инициирует событие для смены изображения.
	 */
	function onGalleryClick(evt) {
		if (core.busy() || core.animated()) return;

		var index = $(evt.target).data('uIndex');

		// Тыкнули не на превьюшку.
		if (undefined === index) return;
		// Текущее изображение
		if (core.loader.selectedIndex() == index) return;

		core.eventDispatcher.dispatch('previewClick', {
			current: core.loader.item(index),
			prev: core.loader.item(index - 1),
			next: core.loader.item(index + 1)
		});

		for (var i = index - 2; i <= index + 2; i++) {
			loadImage(i);
		}
	}

	/**
	 * Загружает изображение в кэш браузера.
	 * @param {Number} index Индекс загружаемого изображения.
	 * @return {Boolean} true - загрузка запущена, false - загрузка не запущена (изображение уже загрузилось,
	 *                   грузится или данных об изображении с данным индексом нет).
	 */
	function loadImage(index) {
		if (loaded[index]) return false;

		var imageData = core.loader.item(index);
		if (!imageData) return false;

		var image = new Image();
		$(image).load(function() {
			loaded[index] = 2;
			core.eventDispatcher.dispatch('imageLoaded', { index: index });
		});
		image.src = imageData.image.src;
		loaded[index] = 1;

		return true;
	}

	/**
	 * Возвращает статус загрузки изображения.
	 * @param {Number} index Индекс изображения.
	 * @return {Boolean} true - загружено, false - не загружено.
	 */
	function isLoaded(index) {
		return loaded[index] && 2 == loaded[index];
	}

	/**
	 * Меняет текущее изображение.
	 * @param {Number} Индекс нового текущего изображения.
	 * @return {Boolean} false - изображение уже текущее.
	 */
	function setCurrent(index) {
		if (index == core.loader.selectedIndex()) return false;

		core.loader.selectedIndex(index);
		core.previewContentEl.find('.selected').removeClass('selected');
		core.previewContentEl.find('.i_' + index).addClass('selected');

		for (var i = index - 2; i <= index + 2; i++) {
			loadImage(i);
		}

		return true;
	}

	return {
		loadImage: loadImage,
		isLoaded: isLoaded,
		setCurrent: setCurrent
	};
};


/**
 * Объект для перемещения ленты превьюшек.
 * @param {uGallery.core} core
 * @param {Object} options Параметры:
 *   <ul>
 *     <li>{Number} dndInertiaTime = 1 -время движения объектов по инерции при анимации с затуханием, в секундах.</li>
 *   </ul>
 * @return {uGallery.previewMover}
 */
uGallery.previewMover = function(core, options) {

	var defOptions = {
		dndInertiaTime: 1
	};

	options = $.extend({}, defOptions, options ? options : {});

	/** Шаг анимации в jTweener. */
	var stepDuration = 1 / 30;

	core.eventDispatcher.bind('previewsMove', function(evt) {
		evt.data.speed
			? animateAfterDnd(evt.data.speed, evt.data.onComplete ? evt.data.onComplete : null)
			: animateTo(evt.data.toLeft, evt.data.onComplete ? evt.data.onComplete : null);
	});

	function animateTo(finishLeft, onComplete) {

		var startLeft = core.previewLeft();
		core.animated(true);

		$t(core.previewContentEl, {
			time: 0.3,
			transition: 'easeOutQuad',
			moveX: function(value) {
				core.previewLeft(startLeft + Math.round((finishLeft - startLeft) * value));
			},
			onComplete: function() {
				core.animated(false);
				onComplete && onComplete();
				core.eventDispatcher.dispatch('previewsUpdate');
			}
		}).tween();
	}

	/**
	 * Анимирует движение объектов галереи по инерции после перетаскивания в зависимости от ускорения,
	 * которое придал им юзер.
	 *
	 * @param {Number} speed Начальная скорость перемещения объектов галереи, пикселей в секунду. Знак числа
	 *                       означает направление перемещения.
	 */
	function animateAfterDnd(speed, onComplete) {

		var
			elStartIndex = core.previewPicturesEl.find('img:first').data('uIndex'),
			elCount = core.previewPicturesEl.find('img').length,
			previewsWidth = core.previewPicturesEl.width(),
			containerWidth = core.previewContainerEl.width(),
			margin = (0 < speed && (core.loader.first() < elStartIndex || !core.loader.isLeftBorder())) || (0 > speed && (core.loader.first() + core.loader.count() - 1 > elStartIndex + elCount || !core.loader.isRightBorder())) ? core.preloaderWidth : 0,
			/** Максимальное расстояние, на которое могут переместиться объекты галереи. */
			maxDistance = 0 < speed ? Math.abs(core.previewLeft()) + margin : Math.abs(containerWidth - previewsWidth - core.previewLeft()) + margin,
			/**
			 * Расстояние, на которое переместятся объекты галереи, если анимация будет происходить по
			 * закону квадратичного затухания.
			 */
			distance = Math.abs(speed * options.dndInertiaTime / (stepDuration / options.dndInertiaTime - 2)),
			/** Начальная точка анимации. */
			startLeft = core.previewLeft(),
			/** Конечная точка анимации. */
			finishLeft;

		core.animated(true);

		/**
		 * Объекты будут перемещаться по закону квадратичного затухания, потому что юзер не дотянул до
		 * края объектов галереи.
		 */
		if (distance < maxDistance) {
			finishLeft = 0 < speed ? core.previewLeft() + distance : core.previewLeft() - distance;

			$t(core.previewContentEl, {
				time: options.dndInertiaTime,
				transition: 'easeOutQuad',
				moveX: function(value) {
					core.previewLeft(startLeft + Math.round((finishLeft - startLeft) * value));
				},
				onComplete: function() {
					core.animated(false);
					onComplete && onComplete();
					core.eventDispatcher.dispatch('previewsUpdate');
				}
			}).tween();

			return;
		}

		/**
		 * Юзер слишком сильно крутанул объекты галереи. Он упрется в край. Поэтому меняем закон
		 * анимации на возвратный, как в Айфоне. Чтобы инерция была правдоподобной, нужно в зависимости
		 * от начальной скорости определить время анимации. Для подсчета времени понадобится решить
		 * уравнение третьей степени. Оно возвращает три корня, но у нас ситуация, когда два корня из
		 * трех являются комплексными. Поэтому берем только первый - действительный - корень. Исходим из
		 * того, что минимальный шаг анимации - <code>stepDuration</code>. За это время
		 * преодолевается расстояние <code>speed * stepDuration</code>. Корень уравнения равен
		 * <code>stepDuration / time - 1</code>.
		 */

		var
			/**
			 * Коэффициент для возвратной анимации - как сильно оттянутся блоки от конечной точки.
			 * Определяем его в зависимости от начальной скорости анимации.
			 */
			s = 2 < Math.abs(speed) / 3000 ? 2 : Math.abs(speed) / 3000, // 1.70158 - это по умолчанию у
			// Роберта Пиннера (Robert
			// Penner):
			// http://www.robertpenner.com/easing/
			/** Время анимации в секундах. */
			time = stepDuration
					/ (uGallery.cubic(s / (s + 1), 0, (1 - Math.abs(speed) * stepDuration / maxDistance) / (s + 1)).x[0] + 1),
			/** Закон, по которому происходит возвратная анимация. */
			transition = function(t, b, c, d) {
				return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b;
			};

		finishLeft = 0 < speed ? core.previewLeft() + maxDistance : core.previewLeft() - maxDistance;

		$t(core.previewContentEl, {
			time: time,
			transition: transition,
			moveX: function(value) {
				core.previewLeft(startLeft + Math.round((finishLeft - startLeft) * value));
			},
			onComplete: function() {
				core.animated(false);
				core.eventDispatcher.dispatch('previewsUpdate');
			}
		}).tween();
	}
};


/**
 * Анимация ленты превьюшек по нажатию стрелок.
 * @param {uGallery.core} core Ядро галереи.
 * @param {Object} options - Параметры:
 *   <ul>
 *     <li>{Number} [animateStep = 50] Шаг анимации при нажатии на стрелки.</li>
 *   </ul>
 * @return {uGallery.previewArrowScrolling}
 */
uGallery.previewArrowScrolling = function(core, options) {

	var defOptions = {
		animateStep: 50
	};

	options = $.extend({}, defOptions, options);

	var
		prevEl = core.previewEl.find('.prev'),
		nextEl = core.previewEl.find('.next'),
		timeoutId;

	prevEl.mousedown(function() { startAnimate(true); });
	nextEl.mousedown(function() { startAnimate(false);});

	core.eventDispatcher.bind('previewsUpdated', updateState);

	function startAnimate(isPrev) {
		if (core.busy()) return;
		if (isPrev && 0 <= core.previewLeft()) return;
		if (!isPrev && core.previewLeft() + core.previewPicturesEl.width() <= core.previewContainerEl.width()) return;

		jTweener.removeTween(core.previewContentEl);

		var onMouseUp = function() {
			clearTimeout(timeoutId);
		};

		isPrev ? prevEl.one('mouseup', onMouseUp) : nextEl.one('mouseup', onMouseUp);
		animate(isPrev);
	}

	function animate(isPrev) {
		core.previewLeft(isPrev ? core.previewLeft() + options.animateStep : core.previewLeft() - options.animateStep);

		timeoutId = setTimeout(function() {
			animate(isPrev);
		}, Math.round(1000 / 15));

		core.eventDispatcher.dispatch('previewsUpdate');
		updateState();
	}

	function updateState() {
		var previewsWidth = core.previewPicturesEl.width();
		var containerWidth = core.previewContainerEl.width();

		0 <= core.previewLeft()
			? prevEl.addClass('disabled')
			: prevEl.removeClass('disabled');
		core.previewLeft() + previewsWidth <= containerWidth
			? nextEl.addClass('disabled')
			: nextEl.removeClass('disabled');
	}

	return {
		updateState: updateState
	};
};


/**
 * Перетаскивание превьюшек.
 *
 * @param {uGallery.core} core Ядро галереи.
 * @param {Object} options Параметры:
 *   <ul>
 *     <li>{Number} [dndDuration = 100] - время для анализа скорости перемещения объектов в момент, когда юзер «кинул» мышку, в мс.</li>
 *   </ul>
 * @return {uGallery.previewDnd}
 */
uGallery.previewDnd = function(core, options) {

	options = $.extend({}, {
		dndDuration: 100
	}, options ? options : {});

	var
		isDndMove = false,
		/** Массив объектов с данными о промежуточных точках объектов при перемещении их юзером. */
		dndDots = [],
		/** Положение курсора мыши при перемещении юзером. */
		dndLeft;

	core.previewContentEl.mousedown(startDnd);

	/**
	 * Стартует перетаскивание объектов галереи.
	 *
	 * @param {Event} evt
	 */
	function startDnd(evt) {
		evt.preventDefault();
		evt.stopPropagation();

		if (core.busy()) return;
		if (core.previewPicturesEl.width() < core.previewContainerEl.width()) return;

		// Стопорим анимацию в данный момент.
		jTweener.removeTween(core.previewContentEl);
		core.animated(false);

		core.eventDispatcher.dispatch('previewsUpdate');
		dndLeft = parseInt(evt.pageX);

		isDndMove = false;

		$(document).mousemove(dnd).mouseup(stopDnd);
	}

	/**
	 * Перетаскивает объекты галереи при перемещении мышки.
	 *
	 * @param {Event} evt
	 */
	function dnd(evt) {
		evt.preventDefault();
		evt.stopPropagation();

		isDndMove = true;

		var newLeftPos = core.previewLeft();

		newLeftPos += parseInt(evt.pageX) - dndLeft;
		dndLeft = parseInt(evt.pageX);

		dndDots.push( {
			x: newLeftPos,
			time: uGallery.getTime()
		});
		core.previewLeft(newLeftPos);
	}

	/**
	 * Заканчивает перетаскивание объектов галереи.
	 *
	 * @param {Event} evt
	 */
	function stopDnd(evt) {
		evt.preventDefault();
		evt.stopPropagation();

		$(document).unbind('mousemove', dnd).unbind('mouseup', stopDnd);

		if (!isDndMove) return;
		if (core.busy()) return;

		var previewsWidth = core.previewPicturesEl.width();
		var containerWidth = core.previewContainerEl.width();

		if (previewsWidth < containerWidth) return;

		if (0 < core.previewLeft() || core.previewLeft() < containerWidth - previewsWidth) {
			core.eventDispatcher.dispatch('previewsMove', {
				toLeft: core.previewLeft() < containerWidth - previewsWidth ? containerWidth - previewsWidth : 0
			});

			return;
		}

		var
			/** Текущее время */
			now = uGallery.getTime(),
			/** Расстояние, на которое переместились объекты галереи перед "броском" мыши юзером. */
			distance = 0,
			/** Время, с которого пойдет отсчет перемещения. */
			fromTime,

			counter = 3;

		for (var i = dndDots.length - 1; i >= 0; i--) {
			if (dndDots[i].time + options.dndDuration < now && 0 >= counter) break;

			distance = core.previewLeft() - dndDots[i].x;
			fromTime = dndDots[i].time;
			counter--;
		}

		dndDots = [];

		/**
		 * Скорость перемещения объектов галереи перед "броском" мыши юзером. Если ноль, значит, юзер
		 * жестко перемещал объекты галереи, и анимация перемещения по инерции не требуется.
		 */
		var speed = distance ? Math.round(distance / (now - fromTime) * 1000 / 1.5) : 0;

		/**
		 * Если юзер "бросил" мышку, значит, объекты должны перемещаться еще какое-то время по инерции.
		 */
		speed
			? core.eventDispatcher.dispatch('previewsMove', { speed: speed })
			: core.eventDispatcher.dispatch('previewsUpdate');
	}

	function onMouseUp(evt) {
		evt.preventDefault();
		evt.stopPropagation();

		core.previewContentEl.unbind('mouseup', onMouseUp);
	}
};


/**
 * Управляет бегунком текущего изображения и стрелочками направления текущего изображения.
 *
 * @param {uGallery.core} core Ядро галереи.
 * @return {uGallery.previewRunner}
 */
uGallery.previewRunner = function(core) {
	var
		runnerEl = core.previewContentEl.find('.selected_runner'),
		leftEl = core.previewEl.find('.selected_left'),
		rightEl = core.previewEl.find('.selected_right');

	leftEl.click(changeSelected);
	rightEl.click(changeSelected);

	core.eventDispatcher.bind('imageSelected', function(evt) {
		moveToImage(evt.data.index);
	});

	core.eventDispatcher.bind('previewsUpdated', function(evt) {
		setPos(runnerEl.position().left - evt.data.moveX);
	});

	/**
	 * Перемещение бегунка к изображению.
	 * @param {Number} index Индекс изображения.
	 * @return {Boolean} false - изображение не найдено.
	 */
	function moveToImage(index) {
		var imageInfo = core.previewPicturesEl.find('.i_' + index);
		if (!imageInfo.length) return false;

		setPos(Math.round(imageInfo.position().left + core.loader.item(index).preview.width / 2));

		return true;
	}

	function setPos(left) {
		runnerEl.css('left', left);

		var pos = runnerEl.offset().left - core.previewContainerEl.offset().left;

		0 > pos ? leftEl.removeClass('hidden') : leftEl.addClass('hidden');
		pos > core.previewContainerEl.width() ? rightEl.removeClass('hidden') : rightEl.addClass('hidden');
	}

	/**
	 * Инициирует выбор уже выбранного изображения.
	 * @return {Boolean} false - инициирование не прошло.
	 */
	function changeSelected() {
		if (core.busy()) return false;

		core.eventDispatcher.dispatch('imageSelect', {
			index: core.loader.selectedIndex(),
			imageInfo: core.loader.selected()
		});

		return true;
	}

	return {
		changeSelected: changeSelected,
		moveToImage: moveToImage,
		setPos: setPos
	};
};


/**
 * Управление прелоадерами (левый и правый) в полосе превьюшек.
 * @param {uGallery.core} core Ядро галереи.
 * @return {uGallery.previewPreloader}
 */
uGallery.previewPreloader = function(core) {
	var preloaderEls = core.previewContentEl.find('.preloader');

	core.eventDispatcher.bind('previewsUpdated', updateStates);

	/**
	 * Обновляет состояние прелоадеров.
	 */
	function updateStates() {
		var
			elStartIndex = core.previewPicturesEl.find('img:first').data('uIndex'),
			elCount = core.previewPicturesEl.find('img').length,
			value;

		// Видимость левого прелоадера.
		value = core.loader.isLeftBorder && elStartIndex == core.loader.first() ? 'hidden' : 'visible';
		preloaderEls.eq(0).css('visibility', value);
		// Видимость правого прелоадера.
		value = core.loader.isRightBorder && elStartIndex + elCount == core.loader.first() + core.loader.count() ? 'hidden' : 'visible';
		preloaderEls.eq(1).css('visibility', value);
	}

	return {
		updateStates: updateStates
	};
};


uGallery.history = function(core, options) {

	options = $.extend({}, {
		anchorPrefix: 'image_',
		firstId: null
	}, options);

	var
		curId = null,
		busyTimeoutId,
		checkTimeoutId;

	check();

	core.eventDispatcher.bind('imageSelected', function(evt) {
		change(evt.data.index);
	});
	core.eventDispatcher.bind('pause', function() {
		clearTimeout(checkTimeoutId);
	});
	core.eventDispatcher.bind('resume', check);

	function check() {
		var list = window.location.href.split('#');
		var newId = options.firstId;

		if (list[1] && options.anchorPrefix == list[1].substr(0, options.anchorPrefix.length)) {
			var newIdStr = list[1].substr(options.anchorPrefix.length);
			newId = parseInt(newIdStr, 10);

			if (!(newIdStr == newId && 0 < newId)) {
				newId = options.firstId;
			}
		}

		if (null === curId) {
			curId = newId;
		} else if (curId != newId) {
			var index = core.loader.indexById(newId);
			false === index || dispatchEventClick(index);
			curId = newId;
		}

		checkTimeoutId = setTimeout(check, 300);
	}

	function change(index) {
		clearTimeout(busyTimeoutId);

		var newId = core.loader.item(index).id;

		if (newId != curId) {
			curId = newId;
			window.location.href = window.location.href.split('#')[0] + '#' + options.anchorPrefix + curId;
		}
	}

	function dispatchEventClick(index) {
		clearTimeout(busyTimeoutId);

		if (core.busy() || core.animated()) {
			busyTimeoutId = setTimeout(function() {
				dispatchEventClick(index);
			}, 200);
		} else {
			core.eventDispatcher.dispatch('previewClick', {
				current: core.loader.item(index),
				prev: core.loader.item(index - 1),
				next: core.loader.item(index + 1)
			});
		}
	}

	return {
		getId: function() { return curId; }
	};
};


/**
 * Возвращает XML из строки.
 * @param {String} text XML в строке.
 * @return {Element} XML в объекте.
 *
 * @example
 * $.ajax({
 *   dataType: 'text', // Обязательно, если хочешь получить XML
 *   success: function(data) {
 *     var xmlData = uGallery.getXml(data);
 *     xmlData = $('result', xmlData);
 *   }
 *   // Другие параметры
 * });
 */
uGallery.getXml = function(text) {
	var xmlData = null;

	try {
		if (window.ActiveXObject) { // IE
			xmlData = new ActiveXObject('Microsoft.XMLDOM');
			xmlData.async = false;
			xmlData.loadXML(text);
		} else if (window.DOMParser) { // Все остальные
			var xmlData = (new DOMParser()).parseFromString(text, 'text/xml');
		}

		if (!xmlData || !xmlData.documentElement || 'parsererror' == xmlData.documentElement.nodeName
			|| xmlData.getElementsByTagName('parsererror').length) {
			return false;
		}
	} catch (error) {
		return false;
	}

	return xmlData;
};


/**
 * Загружает изображение в память браузера.
 * @param {String} src Адрес файла изображения.
 * @param {Function} [onLoad] Функция, выполняемая после загрузки изображения.
 */
uGallery.loadImage = function(src, onLoad) {
	var image = new Image();
	image.onload = function() {
		onLoad && onLoad()
	};
	image.src = src;
};

/**
 * Возвращает корни кубического уравнения.
 *
 * Решает уравнение вида x^3 + a*x^2 + b*x + c = 0 методом Виета-Кардано. Возвращает
 * действительные части корней.
 *
 * @param {Number} a Коэффициент при аргументе в квадрате.
 * @param {Number} b Коэффициент при аргументе.
 * @param {Number} c Свободный коэффициент.
 * @return {Object} type: 1 - 1 действительный + 2 комплексных, 2 - 1 действительный + мнимая
 *                  часть комплексных корней, если 0 (т.е. 2 действительных корня), 3 - 3 действительных
 *                  корня. x: массив из трех элементов с действительными корнями уравнения.
 */
uGallery.cubic = function(a, b, c) {
	var q = (a * a - 3 * b) / 9, r = (a * (2 * a * a - 9 * b) + 27 * c) / 54, q3 = q * q * q;

	// Все корни действительные.
	if (r * r < q3) {
		var t = Math.acos(r / Math.sqrt(q3));

		a /= 3;
		q = -2 * Math.sqrt(q);

		return {
			type: 3,
			x: [ q * Math.cos(t / 3) - a, q * Math.cos((t + 2 * Math.PI) / 3) - a, q * Math.cos((t - 2 * Math.PI) / 3) - a ]
		};
	}

	r = Math.abs(r);

	var aa = -Math.pow(r + Math.sqrt(r * r - q3), 1 / 3);
	var bb = 0 == aa ? 0 : q / aa;

	a /= 3;
	q = aa + bb;
	r = Math.abs(aa - bb);

	var x = [ q - a, -0.5 * q - a, (Math.sqrt(3) * 0.5) * r ];

	return {
		type: 0 == x[2] ? 2 : 1,
		x: x
	};
}

/**
 * Возвращает текущее время с точностью до милисекунд.
 * @return {Number}
 */
uGallery.getTime = function() {
	return new Date().getTime();
}


/**
 * Диспетчер любых событий.
 * @author Matthew Foster, Vlad Yakovlev (red.scorpix@gmail.com)
 * @version 1.0.1
 * @date 2009-09-01
 */
uGallery.eventDispatcher = (function() {

	function EventDispatcher() {
		var listenerChain = {};
		var onlyOnceChain = {};

		/**
		 * Добавляет слушателя события.
		 * @param {String|Array} type Название события или событий через пробел.
		 * @param {Function} listener Слушатель.
		 * @param {Boolean} onlyOnce Подписаться на событие только один раз.
		 */
		function bind(type, listener, onlyOnce) {
			if (!listener instanceof Function) {
				throw new Error("Listener isn't a function");
			}

			var chain = onlyOnce ? onlyOnceChain : listenerChain;

			type = 'string' == typeof(type) ? type.split(' ') : type;

			for (var i = 0; i < type.length; i++) {
				if(!chain[type[i]]) {
					chain[type[i]] = [listener];
				} else {
					chain[type[i]].push(listener);
				}
			}
		}

		/**
		 * Проверяет, есть ли у такого события слушатели.
		 * @param {String} type Название события.
		 * @return {Boolean}
		 */
		function hasBinds(type) {
			return ('undefined' != typeof listenerChain[type] || 'undefined' != typeof onlyOnceChain[type]);
		}

		/**
		 * Удаляет слушателя события.
		 * @param {String} type Название события.
		 * @param {Function} listener Слушатель, который нужно удалить.
		 */
		function unbind(type, listener) {
			if (!hasBinds(type)) return false;

			var chains = [listenerChain, onlyOnceChain];

			for (var i = 0; i < chains.length; i++) {
				/** @type {Array} */
				var lst = chains[i][type];

				for(var j = 0; j < lst.length; j++) {
					lst[j] == listener && lst.splice(j, 1);
				}
			}

			return true;
		}

		/**
		 * Инициирует событие.
		 * @param {String} type Название события.
		 * @param {Object} [args] Дополнительные данные, которые нужно передать слушателю.
		 * @return {Boolean}
		 */
		function dispatch(type, args) {

			if (!hasBinds(type)) return false;

			var
				chains = [listenerChain, onlyOnceChain],
				evt = new CustomEvent(type, this, args);

			for (var j = 0; j < chains.length; j++) {
				/** @type {Array} */
				var lst = chains[j][type];

				if (lst) {
					for(var i = 0, il = lst.length; i < il; i++) {
						lst[i](evt);
					}
				}
			}

			if (onlyOnceChain[type]) {
				delete onlyOnceChain[type];
			}

			return true;
		}

		return {
			/**
			 * Добавляет слушателя события.
			 * @param {String|Array} type Название события или событий через пробел.
			 * @param {Function} listener Слушатель.
			 * @param {Boolean} onlyOnce Подписаться на событие только один раз.
			 */
			bind: bind,

			/**
			 * Проверяет, есть ли у такого события слушатели.
			 * @param {String} type Название события.
			 * @return {Boolean}
			 */
			hasBinds: hasBinds,

			/**
			 * Удаляет слушателя события.
			 * @param {String} type Название события.
			 * @param {Function} listener Слушатель, который нужно удалить.
			 */
			unbind: unbind,

			/**
			 * Инициирует событие.
			 * @param {String} type Название события.
			 * @param {Object} [args] Дополнительные данные, которые нужно передать слушателю.
			 * @return {Boolean}
			 */
			dispatch: dispatch
		};
	}

	/**
	 * Основа события.
	 * @param {String} type Тип события.
	 * @param {Object} target Объект, которые инициировал событие.
	 * @param {Object} [data] Дополнительные данные.
	 */
	function CustomEvent(type, target, data) {
		this.type = type;
		this.target = target;

		if (data) {
			this.data = data;
		}
	}

	return function() {
		return new EventDispatcher();
	}
})();
