jQuery.fn.flipper = function(o) {
	return this.each(function() {
		new jQuery.flipper(this, o);
		})
	};

jQuery.flipper = function(e, o) {
	var publ = this;
	publ.scope = function() { return priv.scope };
	publ.num = function() { return priv.o.num };
	publ.at = function() { return priv.o.at };
	publ.next = function(n) { return priv.go(publ.at() === null ? 0 : publ.at()+(n||1)) };
	publ.prev = function(n) { return publ.next(-(n||1)) };
	publ.go = function(n) { return priv.go(n) };
	
	var priv = {
		o: {
			at: null,
			num: 0,
			keepLoaded: 8,
			preloadNext: 4,
			preloadPrev: 1,
			animateOutSpeed: 2000,
			animateInSpeed: 2000,
			initHandler: null,
			loadItemHandler: null,
			beforeChangeHandler: null,
			afterChangeHandler: null,
			startLoadingHandler: null,
			stopLoadingHandler: null,
			animateHandler: function(oldItem, newItem, cb) {
				if(oldItem && oldItem.css('display') != 'none') oldItem.fadeOut(priv.o.animateOutSpeed);
				newItem.fadeIn(priv.o.animateInSpeed, cb);
				}
			},
		
		scope: null,
		itemCache: {},
		orderLoaded: [],
		inTransition: false,
		
		init: function(e, o) {
			if(o) jQuery.extend(priv.o, o);
			
			if(!priv.o.loadItemHandler) throw new Error('no loadItemHandler defined');
			
			
			priv.scope = jQuery(e);
			priv.scope.empty();
			
			if(priv.o.at !== null) {
				var goTo = priv.o.at;
				priv.o.at = null;
				priv.go(goTo);
				}

			if(priv.o.initHandler) priv.o.initHandler.apply(publ, []);
			},
		
		go: function(n) {
			if(priv.inTransition) return;
			
			var newN = priv.fixRange(n);
			var oldN = priv.o.at;
			var oldItem = priv.itemCache[oldN];
			
			if(newN == oldN) return;
			
			priv.inTransition = true;

			if(priv.o.beforeChangeHandler) priv.o.beforeChangeHandler.apply(publ, [oldN, newN]);
			priv.getItem(newN, go2);
			
			function go2(newItem) {
				priv.o.animateHandler.apply(publ, [oldItem, newItem, go3]);
				
				function go3() {
					priv.o.at = newN;
					if(priv.o.afterChangeHandler) priv.o.afterChangeHandler.apply(publ, [oldN, newN]);
					priv.inTransition = false;
					}
				}
			},
		
		getItem: function(n, cb) {
			
			for(var i = 0; i < priv.o.preloadPrev; i++) make(priv.fixRange(n-i-1));
			for(var i = 0; i < priv.o.preloadNext; i++) make(priv.fixRange(n+i+1));
			var item = make(n);

			var interval = null;
			var images = item.find('img');
			
			if(images.length && !allLoaded()) {
				if(priv.o.startLoadingHandler) priv.o.startLoadingHandler.apply(publ, []);
				interval = setInterval(waitForLoad, 200);
				}
			else {
				cb(item);
				}

			return;
			
			function allLoaded() {
				for(var i = 0; i < images.length; i++) {
					var im = images[i];
					if(!(im.wasError || im.wasAborted || im.complete || im.wasLoaded)) return false;
					}
				return true;
				}
			
			function waitForLoad() {
				if(allLoaded()) {
					clearInterval(interval);
					if(priv.o.stopLoadingHandler) priv.o.stopLoadingHandler.apply(publ, []);
					cb(item);
					}
				}
				
			function make(n) {
				if(priv.itemCache[n] !== undefined) {
					var a = [];
					for(var i = 0; i < priv.orderLoaded.length; i++)
						if(priv.orderLoaded[i] != n)
							a.push(priv.orderLoaded[i]);
					a.push(n);
					priv.orderLoaded = a;
					return priv.itemCache[n];
					}
				else {
					while(priv.orderLoaded.length >= priv.o.keepLoaded) {
						var killNum = priv.orderLoaded.shift();
						var killItem = priv.itemCache[killNum];
						priv.itemCache[killNum] = undefined;
						killItem.remove();
						}
					
					var newItem = jQuery( priv.o.loadItemHandler.apply(publ, [n]) );
					priv.orderLoaded.push(n);
					priv.itemCache[n] = newItem;
					newItem.css('display', 'none');
					priv.scope.append(newItem);
					if($.browser.safari) $('img', newItem).load(function(){this.complete = true});
			
					return newItem;
					}
				}

			},
		
		fixRange: function(n) {
			while(n<0) { n += priv.o.num };
			n = n % priv.o.num;
			return n;
			}
		
		};
	
	priv.init(e, o);
	};
