function Slideshow(banner, images, options) {
	var banner = $(banner);
	var fader;
	var options = $merge({
		imagesDir: "",
		duration: 5000,
		userDuration: 1000,
		wait: 5000,
		width: banner.getStyle("width"),
		height: banner.getStyle("height"),
		backgroundPosition: "top"
	}, options);
	// Current image is the index of the image that is fully loaded and moved to
	// the background div.
	var nxt = 1;
    var listeners = new Array();
	
	function mod(a,b) { var n = a%b; return n < 0 ? n+b : n; }
	function currentImage() { if(images) return mod(nxt-1,images.length); }
	function nextImage() { if(images) return mod(nxt,images.length); }
	function advanceImage() { nxt += 1; }

    function onImageChange(e){
        for(i=0; i<listeners.length; i++){
            listeners[i](e);
        }
    }

	
	// User manually changes to another slide.
	function changeNext(n) {
		nxt = n;
		
		// Don't interrupt in-progress animations, but switch immediately if we can.
		if (!animating) {
			resetFader();
		} else {
			// Hurry along the current fade.
			fx.stop();
			fx.setOptions({duration: springDuration});
			fx.start(1);
		}
		
		// User interation always force a single move even if the animation is paused.
		forceSingle = true;
		eventHandler();
	}
	
	var waitDuration = options.wait;
	var fadeDuration = options.duration;
	var userFadeDuration = options.userDuration;
	var springDuration = 500; // Spring a partially completed fade to completion.
	
	var imageDir = options.imagesDir;
	var images = images;
	var cachedImages = [true];
	var failedInARow = 0;
	
	function imageToCSS(n) {
		return "url(" + imageDir + images[n] + ")";
	}
	
	function loadNextImage() {
		var img = new Image;
		
		// Store the current imageCache and number in a closure. If they change
		// during the time it takes to load, the next cache won't get the incorrect
		// values written to it.
		img.onload = (function(cache, n) {
			return function() {
				failedInARow = 0;
				fader.setStyle("background-image", imageToCSS(n));
				cache[n] = true;
				eventHandler();
			}
		})(cachedImages, nextImage());
		
		img.onerror = function() {
			// Can't load the image, just try the next.
			if (failedInARow > 10) {
				// Don't keep looking forever.
				return;
			}
			failedInARow++;
			advanceImage();
			resetFader();
		}
		
		// Start load process.
		if(images) {
			img.src = imageDir + images[nextImage()];
		}
	}
	
	function updateCurrentImage() {
		var img = $E('img', banner);
		img.src = fader.getStyle("background-image").match(/url\((.+)\)/)[1]
	}
	
	function resetFader() {
		fader.setStyle("opacity", 0);
		loadNextImage();
	}
	
	var fx;
	
	(function createFader() {
		fader = new Element("div", {
			styles: {
				position: "absolute",
				zIndex:2,
				top: 0,
				left: 0,
				width: options.width,
				height: options.height,
				backgroundRepeat: "no-repeat",
				backgroundPosition: options.backgroundPosition
			}
		});
		
		fx = fader.effect("opacity", {
			duration: fadeDuration,
			onComplete: function() {
				animating = false;
				lastTime = new Date;
				updateCurrentImage();
				resetFader();
				eventHandler();
			}
		});
		
		resetFader();
		fader.injectInside(banner);
	})();
	
	// Actually mid-fade, which is never stopped.
	var animating = false;
	// Don't start any new fades automatically, but user interaction can still
	// force a single fade.
	var paused = false;
	var forceSingle = false;
	var lastTime = new Date;
	
	
	function eventHandler() {
		if (animating) {
			return;
		}
		
		if (paused && !forceSingle) {
			return;
		}
		
		if (!forceSingle && ((new Date) - lastTime) < waitDuration) {
			// Come back when the time is right.
			eventHandler.delay(waitDuration - ((new Date) - lastTime));
			return;
		}
		
		if (!cachedImages[nextImage()]) {
			// Not cached yet, the event handler will be called when the cache is
			// complete.
			return;
		}
		
		if (forceSingle) {
			forceSingle = false;
			fx.setOptions({duration: userFadeDuration});
		} else {
            e = { imageId: nxt };
            onImageChange(e);
			fx.setOptions({duration: fadeDuration});
		}
		
		fx.start(0,1);
		animating = true;
		advanceImage();
	}
	eventHandler();
	
	return {
		loadImageSequence: function(seq) {
			images = seq;
			changeNext(0);
			cachedImages = [];
		},
		
		pause: function() {
			paused = true;
		},
		
		resume: function() {
			paused = false;
			eventHandler();
		},
		
		moveTo: function(n) {
			changeNext(n);
		},
		
		moveToNext: function() {
			changeNext(currentImage()+1);
		},
		
		moveToPrevious: function() {
			changeNext(currentImage()-1);
		},

        addListener: function(fn){
            listeners[listeners.length] = fn;
        }

	};
}

window.addEvent("domready", function() {
	if (!Azexis.banners) {
		return;
	}
	
	var link = $E("#main .banner a");

	var slideshow = Slideshow($("main-image"),
		Azexis.banners,
		{
			imagesDir: Azexis.baseUrl + "/content/assets/",
			backgroundPosition: "right"
		}
	);
});
