carousel = function(config){
	var o = {
   		DELAY_MS: config.delay_ms || 5000,
    	ANIMATE_MS: config.animate_ms || 1000,
		el: config.el,
		index: config.index || 0,
		paused: config.paused || false,
		wrap: !(config.wrap === false),
		controls: config.controls || false, // permalink or step
		controlPosition: config.controlPosition || 'after',
		alwaysShowControls : config.alwaysShowControls || false,
		scrollCt : config.scrollCt || undefined,
		controlTpl : config.controlTpl ? new Template(config.controlTpl) : new Template('{links}'),
		scrollAxis: config.scrollAxis || 'x',
		handleFragments: config.handleFragments || false,
		fragmentDelimiter: config.fragmentDelimiter || '/',
		mode: config.mode || 'id',
		label: config.el.attr('rel') || config.label || 'PAGE:',
		proxyEls: [],
		autosize: config.autosize !== false,
		autosizeEls: config.autosizeEls !== false,
		namePrefix: config.namePrefix || '',
		init: function(){
			if(!this.scrollCt) this.scrollCt = this.el.children('div');
			this.scrollCt.children('div').addClass('pane');
			this.scrollCt.append('<div class="clearer" style="clear: both; float: none;"></div>');
			
			var that = this;
			if(this.scrollAxis == 'x'){
				var ww = 0;
				that.scrollCt.children('.pane').each(function(){
					ww+= jQuery(this).width();
				}); 
				if(this.autosize) this.scrollCt.css({width: ww+'px'});
			}
			if(this.autosizeEls){
				var ew = this.el.width();
				this.scrollCt.children('.pane').css({width: ew+'px'})
			}
			this.addEvents();
			if(this.controls) this.renderControls();
			
			if(!this.paused) this.start();
			if(this.handleFragments) {
				this.initWatchFragment();
			}
		},
		addEvents: function(){
			var that = this;
			this.el.mouseover(function(){ that.mouseOver(true); });
	        this.el.mouseout(function(){ that.mouseOver(false); });
		},
		renderControls: function(){
			if(this.controls == 'overlay'){
				this.controlPosition = "first";
			}
			if(this.controlPosition.match(/(last|after)/)) this.el.append('<div class="carousel-controls"></div>');
			if(this.controlPosition.match(/(first|before)/)) this.el.prepend('<div class="carousel-controls"></div>');
			this.controlsEl = this.el.children('.carousel-controls');
			this.controlsEl.addClass(this.controls);
			
			if(this.controls == 'permalink'){
				this.renderPermalinkControls();
			} else if(this.controls == 'step'){
				this.renderStepControls();
			} else if(this.controls == 'overlay'){
				this.renderOverlayControls();
			} else if(this.controls == 'named'){
				this.renderNamedControls();
			}
		},
		renderControlsImpl: function(cts){
			var links = '';
			for(var i=0; i<cts.length; i++){
				links += this.linkTpl.bind(cts[i]);
			}
			
			this.controlsEl.html(this.controlTpl.bind({links: links}));
		},
		renderNamedControls: function(){
			this.controlTpl = new Template('<div class="secondNav" style="text-align: center"><ul><li>'+this.label+'</li> {links}</ul></div>');
			this.linkTpl = new Template('<li><a href="#{title}">{title}</a><!--[if IE 7]> <![endif]--></li> ');
			
			var controls = [];
			var re = new RegExp('^'+this.namePrefix);
			for(var i=0; i<this.size(); i++) controls.push({index: i, title: this.getEl(i).attr('name').replace(re,'')});
			this.renderControlsImpl(controls);
		},
		renderPermalinkControls: function(){
			this.controlTpl = new Template('<div class="secondNav" style="text-align: right"><ul><li>PAGE: </li>{links}</ul></div>');
			this.linkTpl = new Template('<li><a href="#{index}">{title}</a></li>');
			
			var controls = [];
			for(var i=0; i<this.size(); i++) controls.push({index: i, title: i+1});
			this.renderControlsImpl(controls);
			
			var that = this;
			this.controlsEl.find('a').click(function(){
				var v = jQuery(this).attr('href').replace(/^.*#/,'');
				that.animate(v);
				return false;
			});
		},
		renderStepControls: function(){
			this.controlTpl = new Template('{links}<div style="clear: both; float: none !important;"></div>');
			this.linkTpl = new Template('<a href="#{ref}" rel="{ref}" class="{ref}" title="{title}"><span>{title}</span></a>');
			
			var controls = [{ref: 'next', title: 'Next'},{ref: 'prev', title: 'Previous'}];
			this.renderControlsImpl(controls);
			
			this.toggleStepControls();
			
			var that = this;
			this.controlsEl.children('a').click(function(){
				switch(jQuery(this).attr('rel')){
					case 'next': that.next(); break;
					case 'prev': that.prev(); break;
				}
				return false;
			});
		},
		toggleStepControls: function(){
			this.controlsEl.find('.prev')[!this.wrap && this.index == 0 ? 'addClass' : 'removeClass']('disabled');
			this.controlsEl.find('.next')[!this.wrap && this.index == this.size() -1 ? 'addClass' : 'removeClass']('disabled');	
		},
		highlightCurrent : function(){
			this.controlsEl.find('a.current').removeClass('current');
			this.controlsEl.find('a:eq('+this.index+')').addClass('current');
		},
		renderOverlayControls: function(){
			this.linkTpl = new Template('<a href="#{ref}" rel="{ref}" class="{ref}" title="{title}"><span>{title}</span></a>');
			this.controlTpl = new Template('<a href="#" rel="prev" class="prev" title="Previous"><span><</span></a><a href="#" rel="next" class="next" title="Next"><span>></span></a>');
		
			var controls = [{ref: 'prev', title: 'Previous'},{ref: 'next', title: 'Next'}];
			this.renderControlsImpl(controls);
			
			var that = this;
			this.controlsEl.children('a').click(function(){
				switch(jQuery(this).attr('rel')){
					case 'next': that.next(); break;
					case 'prev': that.prev(); break;
				}
				return false;
			});
			
			this.controlsEl.css({
				width:this.el.width(),
				top: this.scrollAxis == 'x' ? this.el.height()-50 : 0
			});
		},
		mouseOver: function(inside){
			this.mouseover = inside;
			this.el[inside ? 'addClass' : 'removeClass']('over');
			if(this.controls == 'overlay') this.controlsEl[this.alwaysShowControls || this.mouseover ? 'show' : 'hide']();
		},
		size : function(){
			return this.scrollCt.children('.pane').length;
		},
		start: function(){
			var that = this;
			this.interval = window.setInterval(function() {
    			if (!that.paused && !that.mouseover) {
        			that.animate();
    			}
			}, this.DELAY_MS);
        },
		stop: function(){
			window.clearInterval(this.interval);
			this.interval = undefined;
		},
		pause: function(paused){
			this.paused = !(paused === false);
		},
		next: function(){ this.animate(this.index+1); },
		prev: function(){ this.animate(this.index-1); },
		first: function(){ this.animate(0); },
		last: function(){ this.animate(this.size()-1); },
		animate: function(to,cb){
			if(this.animating) return false;
			this.animating = true;
			if(to === undefined){
				this.index = this.index+1;
			} else if(isNaN(to) || (this.mode == 'name' && isNaN(this.namePrefix+to))){
				if(this.mode == 'name') to = this.namePrefix+to;
				var sel = this.mode == 'id' ? '#'+to : '['+this.mode+'='+to+']';
				this.index = jQuery(sel).prevAll().length;
			} else {
				this.index = to;
			}
			
			var proxiedIndex = -1;
			if(this.wrap){
				if(this.index >= this.size()){ this.index = 0; }
				else if(this.index < 0){ this.index = this.size()-1; }
				/* DOES NOT WORK GREAT IN IE
				var first = this.getEl(0);
				var origSize = this.size();
				if(this.index < 0 || this.index >= this.size()){
					var last = this.getEl(this.size()-1);
					this.getEl(this.size()-1).after(this.outerHtml(first,'post_'));
					this.proxyEls.push(this.getEl(this.size()-1));
				}
				if(this.index >= origSize){
					proxiedIndex = 0;
				} else if(this.index < 0){	
					this.scrollCt.css(this.getOffsetDirection(), this.getPos(this.size()-1));
					this.index = this.size()-2;
				}
				*/
			} else {
				this.index = Math.min(Math.max(this.index,0),this.size()-1)
			}
			
			if (this.controls == 'named' || this.controls == 'permalink'){
				this.highlightCurrent();
			}
			
			var props = {};
			var toEl = this.getEl(this.index);
			props[this.getOffsetDirection()] = this.getPos(this.index);
			
			var that = this;
			this.scrollCt.animate(props, {
				duration: this.ANIMATE_MS,
				complete: function(){
					that.animating = false;
					var p = toEl.prev();
					//that.scrollCt.append(p.html());
					//p.remove();
					if(proxiedIndex != -1){
						that.scrollCt.css(that.getOffsetDirection(), that.getPos(proxiedIndex));
						that.index = proxiedIndex;
					}
					if(that.proxyEls.length){
						var el = that.proxyEls.splice(0,1);
						el[0].remove();
					}

					if(that.controls == 'step'){
						that.toggleStepControls();
					}
					
					if(cb && typeof cb == 'function'){
						cb(to);
					}
				}
			});
		},
		outerHtml: function(el, idPrefix){
			var tag = el.get(0).nodeName;
			var attrs = el.get(0).attributes;
			var parts = [tag];
			var tpl = new Template('{name}="{val}"');
			if(!idPrefix) idPrefix = '';
			for(var i=0; i<attrs.length; i++){
				var val = attrs[i].nodeName.match(/^ID$/i) ? idPrefix+attrs[i].nodeValue : attrs[i].nodeValue;
				parts.push(tpl.bind({name: attrs[i].nodeName, val: val}));
			}
			return "<"+parts.join(' ')+">"+el.html()+"</"+tag+">";
		},
		getOffsetDirection: function(){
			return this.scrollAxis == 'x' ? 'left' : 'top';
		},
		getOffset: function(){
			return this.scrollCt.offset()[this.getOffsetDirection()];
		},
		getEl: function(index){
			return this.scrollCt.children('.pane:eq('+index+')');
		},
		getPos: function(index){
			var el = this.getEl(index);
			var pos = el.offset(); 
			var offset = this.getOffset();
			return (pos[this.getOffsetDirection()]-offset) * -1;
		},
		initWatchFragment:function(){
			if(!this.fragmentInterval){
				var that = this;
				var lastFrag = "";
				var getFragment = function(){ return window.location.hash ? window.location.hash.replace(/^#/,'') : ''; };
				var watchFragment = function(){
					var curFrag = getFragment();
					
					if(curFrag != lastFrag){
						var curFragParts = curFrag.split(that.fragmentDelimiter);
						var cb;
						if(curFragParts.length > 1){ 
							cb = function(){
								var secEl = jQuery('#'+curFragParts[1]);
								if(secEl.length) jQuery('html,body').animate({scrollTop: secEl.offset().top}, 1000);
							}
						}
						var lastFragParts = lastFrag.split(that.fragmentDelimiter);
						
						lastFrag = curFrag;
						if(lastFragParts[0] == curFragParts[0]){
							cb();
						} else {
							that.animate(curFragParts[0],cb);
						}
					}
				};
				this.fragmentInterval = window.setInterval(watchFragment, 50);
			}
		}
	};
	return o;
}

var Template = function(str){
	return {
		val : str,
		bind : function(o){
			var str = this.val;
			for(k in o) if(o.hasOwnProperty(k)){
				var re = new RegExp("\\{"+k+"\\}","g");
				str = str.replace(re, o[k]);
			}
			return str;
		}
	};
}
