(function($) {
	$.fn.onLoadedImage = function(callback) {
		return this.each(function() {
			if (this.tagName.toLowerCase() != "img") {
				return;
			}
			var image = this;
			$this = $(this);
			if (!image.complete || (typeof image.naturalWidth != "undefined"
					&& image.naturalWidth == 0)) {
				$this.load(function() {
					callback(image);
					});
			} else {
				callback(image);
			}
		});
	};

	$.fn.iigallery = function(options) {
		if (!self.classInitialized) {
			$(window).hashchange(function() {
				hashChanged(location.hash)
			});
			// hack for jquery.hashchange plugin
			$(document).ready(function() {});
			// galleries array
			self.galleries = new Array();
			self.classInitialized = true;
		}
		// build main options before element iteration
		var opts = $.extend({}, $.fn.iigallery.defaults, options);
		// iterate and reformat each matched element
		return this.each(function() {
			$this = $(this);
			// build element specific options
			var localOpts = $.meta
				? $.extend({}, opts, $this.data())
				: opts;
			new IIGallery($this, localOpts);
		});
	};

	$.fn.iigallery.defaultImageLinkFunc = function($link, index) {
		return $link.attr("href");
	}

	// plugin defaults
	$.fn.iigallery.defaults = {
		// image link generator function
		imageLinkFunc: $.fn.iigallery.defaultImageLinkFunc,
		// number of thumbnails to show at once
		thumbCount: 6,
		// number of images to preload
		preloadCount: 6,
		// image description source selector ($container.find(descriptionSourceSelector).html)
		descriptionSourceSelector: "p",
		// texts and translations
		prevPageTitle: "previous page",
		prevPageHtml: "&lt;&lt;",
		nextPageTitle: "next page",
		nextPageHtml: "&gt;&gt;",
		pageInfoHtml: "page %p from %n",
		nextImageHtml: undefined,
		prevImageHtml: undefined,
		// image border div size
		imageBorderSize: 0,
		// callbacks
		preImageChange: undefined,
		postImageChange: undefined,
		preGalleryInit: undefined,
		postGalleryInit: undefined,
		// selectors
		thumbnailsSelector: "ul.thumbnails",
		navigationSelector: ".navigation",
		paginationSelector: ".pagination",
		imageSelector: ".image",
		imageBorderSelector: ".image-border",
		captionSelector: ".caption",
		descriptionSelector: ".description"
	};

	// private invisible methods

	// hash changed event handler
	function hashChanged(hash) {
		if (hash == self.lastHash) {
			return;
		}
		self.lastHash = hash;
		if (hash == "") {
			for (var i in self.galleries) {
				self.galleries[i].setImageIndex(0);
			}
		} else {
			var imageId = parseHash(hash);
			if (self.galleries[imageId.galleryIndex] != undefined) {
				self.galleries[galleryIndex].setImageIndex(imageId.imageIndex);
			}
		}
	}

	// hash parsing method
	function parseHash(hash) {
		if (hash.match("^#[0-9]+-[0-9]+$")) {
			var imageId = hash.substr(1).split(/-/);
			galleryIndex = parseInt(imageId[0]);
			imageIndex = parseInt(imageId[1]);
			return { galleryIndex: galleryIndex, imageIndex: imageIndex };
		}
		return { galleryIndex: -1, imageIndex: -1 };
	}

	// gallery's main object
	IIGallery = function($container, options) {
		if (options.preGalleryInit) {
			options.preGalleryInit();
		}
		this.galleryIndex = self.galleries.length;
		self.galleries.push(this);
		this.$container = $container.addClass("iig");
		this.options = options;
		this.currentIndex = 0;
		this.images = new Array();
		this.setupInterface();
		if (options.postGalleryInit) {
			options.postGalleryInit(this);
		}
	};

	$.extend(IIGallery.prototype, {
		// gallery interface initialization
		setupInterface: function() {
			var iigallery = this;

			// setup thumbnails list
			this.$thumbnails = this.$container.find(this.options.thumbnailsSelector)
				.addClass("iig-thumbnails");
			this.thumbnailList = this.$thumbnails.find("li");
			this.thumbnailCount = this.thumbnailList.length;
			this.thumbnailWidth = $(this.thumbnailList[0]).width();
			this.thumbnailHeight = $(this.thumbnailList[0]).height();

			var images = this.images;

			var index = 0;
			this.thumbnailList.each(function() {
				$this = $(this);
				var $link = $this.find("a");
				var imageLink = iigallery.options.imageLinkFunc(this, $link, index);
				var title = (iigallery.options.titleSelector == undefined)
					? $link.attr("title")
					: $link.find(iigallery.options.titleSelector).html();
				images.push({
					url: imageLink,
					title: title,
					description: $this.find(iigallery.options.descriptionSourceSelector).html()
				});
				var imageId = iigallery.galleryIndex + "-" + index++;
				$link.attr("href", "#" + imageId);
				// center thumbnail
				$this.find("img").onLoadedImage(function(image) {
					iigallery.center(image,
							iigallery.thumbnailWidth,
							iigallery.thumbnailHeight);
				});
			});
		
			// setup page navigation
			this.$pagination = this.$container.find(this.options.paginationSelector)
				.addClass("iig-pagination")
				.html('<a href="" title="'
					+ this.options.prevPageTitle
					+ '" class="prev-page" >'
					+ this.options.prevPageHtml + '</a>'

					+ '<span class="pageinfo"></span>'

					+ '<a href="" title="'
					+ this.options.nextPageTitle
					+ '" class="next-page" >'
					+ this.options.nextPageHtml + '</a>');

			// find navigation
			this.$navigation = this.$container.find(this.options.navigationSelector).eq(0)
				.addClass("iig-navigation");

			// find image and information holders
			this.$imageHolder = this.$container.find(this.options.imageSelector).eq(0)
				.css({ position: "relative" })
				.addClass("iig-image");
			this.$imageBorder = this.$container.find(this.options.imageBorderSelector).eq(0)
				.addClass("iig-image-border");
			this.$captionHolder = this.$container.find(this.options.captionSelector).eq(0)
				.addClass("iig-caption");
			this.$descHolder = this.$container.find(this.options.descriptionSelector).eq(0)
				.addClass("iig-description");

			// holders for next image link
			this.$imageWrapper = $('<div class="iig-image-wrapper" />')
				.css({ position: "relative", margin: "auto" })
				.appendTo(this.$imageBorder);
			var linkWidth = (this.options.nextImageHtml != undefined &&
					this.options.prevImageHtml != undefined)
				? 50 : 100;
			if (this.options.nextImageHtml != undefined) {
				this._createHoverLink("next", linkWidth);
			}
			if (this.options.prevImageHtml != undefined) {
				this._createHoverLink("prev", linkWidth);
			}

			// displays image
			this.initImage();
		},

		// creates next/previous image hover link
		_createHoverLink: function(mode, linkWidth) {
			if (mode == "prev") {
				var html = this.options.prevImageHtml;
				var positionKey = "left";
			} else {
				var html = this.options.nextImageHtml;
				var positionKey = "right";
			}
			var $hoverLink = $('<span>' + html + '</span>')
				.css({
					position: "absolute",
					top: "30%",
					display: "none"
					})
				.css(positionKey, 0);

			this.$nextPhotoInfo = $('<a class="iig-image-navigation '
					+ mode + '" href="">&nbsp;</a>')
				.css({
					position: "absolute",
					top: 0,
					display: "block",
					width: linkWidth + "%",
					height: "100%",
					backgroundImage: "url(data:image/gif;base64,AAAA)"
					})
				.css(positionKey, 0)
				.hover(function() {
						$hoverLink.show();
					},
					function() {
						$hoverLink.hide();
					})
				.append($hoverLink)
				.appendTo(this.$imageWrapper);
		},

		// setups pagination (shows/hides links when needed, changes
		// hrefs etc.)
		setNavigationLinks: function() {
			var page = Math.floor(this.currentIndex / this.options.thumbCount);
			var pages = Math.ceil(this.thumbnailCount / this.options.thumbCount);

			// next page links
			var nextPage = page + 1;
			var nextImageIndex = nextPage * this.options.thumbCount;
			var $nextLink = this.$container.find("a.next-page");
			if (nextImageIndex < this.thumbnailCount) {
				$nextLink.attr("href", "#" + this.galleryIndex
						+ "-" + nextImageIndex);
				$nextLink.css({ visibility: "visible" });
			} else {
				$nextLink.attr("href", "#");
				$nextLink.css({ visibility: "hidden" });
			}

			// prev page links
			var prevPage = page - 1;
			var prevImageIndex = prevPage * this.options.thumbCount;
			var $prevLink = this.$container.find("a.prev-page");
			if (prevImageIndex >= 0) {
				$prevLink.attr("href", "#" + this.galleryIndex
						+ "-" + prevImageIndex);
				$prevLink.css({ visibility: "visible" });
			} else {
				$prevLink.attr("href", "#");
				$prevLink.css({ visibility: "hidden" });
			}

			// page number
			if (pages == 1) {
				this.$container.find(".pageinfo").hide();
			} else {
				this.$container.find(".pageinfo")
					.html(this.options.pageInfoHtml
							.replace(/%p/, (page + 1))
							.replace(/%n/, pages));
			}

			// next image links
			nextImageIndex = this.currentIndex + 1;
			if (nextImageIndex >= this.thumbnailCount) {
				this.$container.find("a.next").hide();
			} else {
				this.$container.find("a.next")
					.attr("href", "#" + this.galleryIndex + "-"
							+ nextImageIndex)
					.show();
			}

			// prev image links
			prevImageIndex = this.currentIndex - 1;
			if (prevImageIndex < 0) {
				this.$container.find("a.prev").hide();
			} else {
				this.$container.find("a.prev")
					.attr("href", "#" + this.galleryIndex + "-"
							+ prevImageIndex)
					.show();
			}
		},

		// initializes image - displays either image specified by
		// location.hash or first image in gallery
		initImage: function() {
			var imageId = parseHash(location.hash);
			if (imageId.galleryIndex == -1) {
				// no gallery and image in location.hash
				this.setImageIndex(0);
			} else if (imageId.galleryIndex == this.galleryIndex) {
				// this gallery and its image in location.hash
				this.setImageIndex(imageId.imageIndex);
				if ($.scrollTo) {
					$.scrollTo(this.$container);
				}
			} else {
				// another gallery and its image in location.hash
				this.setImageIndex(0, false);
			}
		},

		// centers element in container using margins
		center: function(element, parentWidth, parentHeight) {
			if (element.tagName.toLowerCase() != "img") {
				if (!element.complete) {
					return;
				}
				width = $(element).width();
				height = $(element).height();
			} else {
				width = element.width;
				height = element.height;
			}
			if (width == 0 || height == 0) {
				return;
			}
			var marginTop = Math.floor((parentHeight - height) / 2);
			var marginLeft = Math.floor((parentWidth - width) / 2);
			$(element).css({ marginTop: marginTop, marginLeft: marginLeft });
		},

		// preloads next/previous image and continues with preloading
		// up to limit
		preloadImage: function(imageIndex, delta) {
			var iigallery = this;

			if (delta == 0) {
				delta = 1;
			}

			if (imageIndex == this.thumbnailCount) {
				imageIndex = 0;
			} else if (imageIndex < 0) {
				imageIndex = this.thumbnailCount - 1;
			}

			var preloaded = 0;
			while (this.images[imageIndex].image != undefined) {
				preloaded++;
				if (preloaded == this.thumbnailCount) {
					// all images loaded
					return;
				}
				imageIndex += delta;
				if (imageIndex == this.thumbnailCount) {
					imageIndex = 0;
				} else if (imageIndex == -1) {
					imageIndex = this.thumbnailCount - 1;
				}
			}

			var distance = 0;
			if (delta > 0) {
				distance = imageIndex - this.currentIndex;
			} else if (delta < 0) {
				distance = this.currentIndex - imageIndex;
			}
			if (distance < 0) {
				distance += this.thumbnailCount + 1;
			}
			if (distance > this.options.preloadCount) {
				// preload limit reached
				return;
			}

			this.images[imageIndex].image = new Image();
			this.images[imageIndex].image.onload = function() {
				iigallery.preloadImage(imageIndex + delta, delta);
			};
			this.images[imageIndex].image.src = this.images[imageIndex].url;
		},

		// sets index of current image and displays it
		setImageIndex: function(imageIndex, useCallbacks) {
			if (useCallbacks == undefined) {
				useCallbacks = true;
			}

			var delta = 1;
			if (imageIndex > 0 && (imageIndex - this.currentIndex == -1
					|| imageIndex >= this.thumbnailCount - 1)) {
				delta = -1;
			}

			var iigallery = this;
			if (imageIndex >= this.thumbnailCount) {
				imageIndex = this.thumbnailCount - 1;
			} else if (imageIndex < 0) {
				imageIndex = 0;
			}
			this.currentIndex = imageIndex;

			var pageStartIndex = Math.floor(imageIndex / this.options.thumbCount)
				* this.options.thumbCount;
			var index = 0;
			this.thumbnailList.each(function() {
				$this = $(this);
				if (index < pageStartIndex
						|| index >= pageStartIndex
						+ iigallery.options.thumbCount) {
					$this.css({ display: "none" });
				} else {
					$this.css({ display: "list-item" });
				}
				if (index == imageIndex) {
					$this.addClass("active");
				} else {
					$this.removeClass("active");
				}
				index++;
			});

			if (this.images[imageIndex].image == undefined) {
				this.images[imageIndex].image = new Image();
				this.images[imageIndex].image.onload = function() {
					iigallery.showCurrentImage(useCallbacks);
				};
				this.images[imageIndex].image.src = this.images[imageIndex].url;
			} else {
				this.showCurrentImage(useCallbacks);
			}
			if (this.options.preloadCount > 0) {
				this.preloadImage(this.currentIndex + delta, delta);
			}
		},

		// shows current image using user defined callbacks
		showCurrentImage: function(useCallbacks) {
			if (useCallbacks && this.options.preImageChange != undefined) {
				var iigallery = this;
				this.options.preImageChange(this, function() {
					iigallery._showCurrentImage(useCallbacks);
				});
			} else {
				this._showCurrentImage(useCallbacks);
			}
		},

		// shows current image - internal implementation
		_showCurrentImage: function(useCallbacks) {
			var imageWidth = this.images[this.currentIndex].image.width;
			var imageHeight = this.images[this.currentIndex].image.height;
			this.$imageBorder
				.width(imageWidth + this.options.imageBorderSize * 2)
				.height(imageHeight + this.options.imageBorderSize * 2);
			this.$imageWrapper.width(imageWidth)
				.height(imageHeight);
			var $img = this.$imageWrapper.find("img");
			if ($img.length == 0) {
				this.$imageWrapper.prepend(this.images[this.currentIndex].image);
			} else {
				$img.replaceWith(this.images[this.currentIndex].image);
			}
			this.$captionHolder.empty().append(this.images[this.currentIndex].title);
			this.$descHolder.empty().append(this.images[this.currentIndex].description);

			if (useCallbacks && this.options.postImageChange != undefined) {
				this.options.postImageChange(this,
						this.images[this.currentIndex]);
			}

			this.setNavigationLinks();
		}
	});
})(jQuery);
