function GalleryUI(gallery, albumSelector, albumDescription, thumbnailsArea, image, previousButton, pauseButton, nextButton) {
	this.gallery = gallery; // stores the gallery instance for data retrieval
	this.albumSelector = albumSelector;
	this.albumDescription = albumDescription;
	this.thumbnailsArea = thumbnailsArea;
	this.image = image;
	// set the image container to a constant size, so the zoom effect can work.
	this.image.parent().width(this.gallery.width).height(this.gallery.height);
	
	this.previousButton = previousButton;
	this.pauseButton = pauseButton;
	this.nextButton = nextButton;
	this.paused = true;
	this.currentTimeout = null;
	
	var self = this; // alias this for use within the event handlers below, which change "this".
	
	this.transitions = {
		"Fade": {
			"out": {opacity: 0},
			"in": {opacity: 1}
		},
		"Zoom": {// the image must be set to position: relative within its container div in order for this to work.
			"out": {left: "50%", top: "50%", width: 0, height: 0},
			"in": {left: "0%", top: "0%", width: this.gallery.width, height: this.gallery.height}
		}
	};
	
	// populates the albums selector
	$.each(gallery.getAlbumTitles(), function(index, title) {
		self.albumSelector.append("<option value='" + index + "'>" + title + "</option>");
	});
	
	this.thumbnailsArea.delegate("img", "click", function(e) {
		self.stopSlideshow();
		self.selectSlide($(this).attr("id"), true);
	}).delegate("img", "mouseover mouseout", function(e) {
		var slide = gallery.getCurrentAlbum().getSlide($(this).attr("id"));
		if (e.type == "mouseover") {
			$(this).width(slide.thumbWidth).height(slide.thumbHeight);
		}
		else {
			$(this).width(24).height(24);
		}
	});
	
	this.albumSelector.change(function() {
		self.selectAlbum($(this).val());
	});
	
	this.previousButton.click(function() {
		self.stopSlideshow();
		self.selectSlide(null, false);
	});
	
	this.pauseButton.click(function() {
		self.paused = !self.paused;
		if (self.paused) {
			self.stopSlideshow();
		}
		else {
			self.startSlideshow(null); // pass null to resume from the current slide position.
		}
	});
	
	this.nextButton.click(function() {
		self.stopSlideshow();
		self.selectSlide(null, true);
	});
	
	// starts the slideshow from a specific slideid or one could pass null to simply proceed to the next slide in sequence.
	this.startSlideshow = function(slideid) {
		this.paused = false;
		this.pauseButton.text("Pause");
		this.selectSlide(slideid, true);
	}
	
	this.stopSlideshow = function() {
		this.paused = true;
		this.pauseButton.text("Play");
		this.image.clearQueue("slideshow");
		if (this.currentTimeout != null) {
			clearTimeout(this.currentTimeout);
		}
	};
	
	this.selectAlbum = function(albumid) {
		this.stopSlideshow();
		var album = this.gallery.loadAlbum(albumid);
		this.albumDescription.text(album.description);
		if (this.gallery.showThumbnails) {
			var $albumdiv = this.thumbnailsArea.children("div#" + albumid);
			var slide = null;
			if ($albumdiv.length == 0) {// the album has not yet been loaded into the thumbnails area.
				var numThumbnails = album.getSlideCount();
				var thumbnails = new Array(numThumbnails);
				for (var i = 0; i < numThumbnails; ++i) {
					slide = album.getSlide(i);
					thumbnails[i] = "<div><img id='" + i + "' src='" + slide.src + "' alt='" + slide.alt + "' style='width:24px;height:24px;' /></div>";
				}
				this.thumbnailsArea.append("<div id='" + albumid + "'>" + thumbnails.join('') + "</div>");
			}
			$albumdiv.show();
			this.thumbnailsArea.children("div:not(#" + albumid + ")").hide();	
		}
		this.selectSlide(0, true); // loads the first slide into the main image area.
		if (this.gallery.autoStart) {
			// starts the slideshow after the normal delay, because a new image has already been loaded.
			this.currentTimeout = setTimeout(function() {self.startSlideshow(null); }, album.transDuration);
		}
	}
	
	this.selectSlide = function(slideid, ascendingOrder) {
		var album = this.gallery.getCurrentAlbum();
		var slide = album.selectSlide(slideid, ascendingOrder);
		var current_transition = this.transitions[album.transition];
		this.image.queue("slideshow");
		this.image.animate(current_transition["out"], "slow", "swing", function() {
			var $albumdiv = self.thumbnailsArea.children("div#" + gallery.selectedAlbum);
			$albumdiv.find("img").removeClass("selected");
			$albumdiv.find("img#" + album.selectedSlide).addClass("selected");
			self.image.attr("src", slide.src).attr("alt", slide.alt);
			self.image.animate(current_transition["in"], "slow", "swing", function() {
				if (!self.paused) {
					self.currentTimeout = setTimeout(function() {self.selectSlide(null, ascendingOrder); }, album.transDuration);
				}
			});
		});
	};
	
	this.selectAlbum(0); // initializes the first album of the slideshow
}

function Gallery(xmldata) {
	this.xmlroot = $(xmldata).find("AlbumBook");
	this.title = this.xmlroot.attr("title");
	this.description = this.xmlroot.attr("description");
	this.width = Number(this.xmlroot.attr("width"));
	this.height = Number(this.xmlroot.attr("height"));
	this.showThumbnails = this.xmlroot.attr("showThumbnails");
	this.autoStart = this.xmlroot.attr("autoStart");
	var $albums = this.xmlroot.children("Album");
	this.albums = new Array($albums.length);
	this.maxAlbum = $albums.length - 1;
	this.selectedAlbum = 0;
		
	// lazily loads the album data, so the user does not have to wait for all of the album data to be loaded
	// before the slideshow starts.
	this.loadAlbum = function(albumid) {
		this.selectedAlbum = albumid;
		var album = this.albums[albumid];
		if (album == null || $.isEmptyObject(album)) {
			var $album = this.xmlroot.children("Album").eq(albumid);
			var $slides = $album.children("Slide");
			album = new Album($album.attr("title"), $album.attr("description"), $album.attr("path"), $album.attr("transType"), Number($album.attr("transTime")), $slides.length);
			$slides.each(function(s_index) {
				album.slides[s_index] = new Slide(album.imagePath + "/" + $(this).attr("src"), 
					$(this).attr("caption"), {width: Number($(this).attr("width")), height: Number($(this).attr("height"))},
					{width: Number($(this).attr("thumbWidth")), height: Number($(this).attr("thumbHeight"))});
			});			
			this.albums[albumid] = album;
		}
		return album;
	};
	
	this.getCurrentAlbum = function() {
		return this.albums[this.selectedAlbum];
	};
	
	this.getAlbum = function(albumid) {
		return this.albums[albumid];
	};
	
	this.getAlbumCount = function() {
		return this.maxAlbum + 1;	
	};
	
	/* returns all album titles in order.
	 * The reason I loop over the xml elements is because not all albums will be loaded by the time this is called. */
	this.getAlbumTitles = function() {
		var titles = new Array(this.maxAlbum + 1);
		this.xmlroot.children("Album").each(function(index) {
			titles[index] = $(this).attr("title");
		});
		return titles;
	};
	
	this.loadAlbum(0); // loads the initial album into the albums array, so the slideshow can start immediately.
}


function Album(title, description, imagePath, transition, transDuration, numSlides) {
	this.selectedSlide = 0;
	// use numSlides to optimise memory usage by telling the slides array how slide objects will be stored.
	this.maxSlide = numSlides - 1;
	this.slides = new Array(numSlides);
	
	this.title = title;
	this.description = description;
	this.imagePath = imagePath;
	this.transition = transition;
	this.transDuration = transDuration * 1000; // converts the transDuration from seconds to milliseconds for the timeout.
	
	this.selectSlide = function(slideId, ascendingOrder) {
		if (slideId == null) {
			if (ascendingOrder) {
				if (this.selectedSlide == this.maxSlide) {
					this.selectedSlide = 0;
				}
				else {
					++this.selectedSlide;
				}
			} else {
				if (this.selectedSlide == 0) {
					this.selectedSlide = this.maxSlide;
				}
				else {
					--this.selectedSlide;
				}
			}
		}
		else {
			this.selectedSlide = slideId;
		}
		return this.slides[this.selectedSlide];
	};
	
	this.getCurrentSlide = function() {
		return this.slides[this.selectedSlide];
	};
	
	this.getSlide = function(slideid) {
		return this.slides[slideid];
	};
	
	this.getSlideCount = function() {
		return this.maxSlide + 1;
	}
}

function Slide(source, description, regularDimensions, thumbnailDimensions) {
	this.src = source;
	this.alt = description;
	this.width = regularDimensions.width;
	this.height = regularDimensions.height;
	this.thumbWidth = thumbnailDimensions.width;
	this.thumbHeight = thumbnailDimensions.height;
}
