carousel = function() {
	

	/* ------------------
	| PREFERENCES
	------------------ */

	var pathTo_configFile = 'config.txt';
	var pathTo_imgsDir = 'slides';
	var spaceBetweenSlides = 10;
	var startSlideOutDuration = .8; //seconds
	var transitionDuration = .6; //seconds
	var pauseDuration = 4; //seconds

	
	/* ------------------
	| STYLING
	------------------ */
	
	css = {
		wrapper: {
			position: 'relative',
			margin: '40px auto 0 auto',
			overflow: 'hidden',
			background: "url('bg.jpg')",
			borderRadius: 10,
			MozBorderRadius: 10,
			webkitBorderRadius: 10,
			padding: !ie ? '10px !important' : 10
		},
		carousel: {
			overflow: 'hidden',
			padding: !ie ? '0 !important' : 0,
			position: 'absolute'
		},
		slides: {
			width: 75,
			height: 280,
			position: 'absolute',
			top: 10,
			backgroundRepeat: 'no-repeat',
			listStyle: 'none',
			display: 'block',
			overflow: 'hidden',
			borderRadius: !ie ? 10 : null,
			MozBorderRadius: !ie ? 10 : null,
			webkitBorderRadius: !ie ? 10 : null,
			opacity: !ie ? .4 : null,
			filter: 'alpha(opacity=40)'
		},
		slide_on: {
			width: 240,
			height: 300,
			top: 0,
			opacity: !ie ? 1 : null,
			filter: 'alpha(opacity=100)',
			boxShadow: !ie ? '0 6px 22px #222' : null
		},
		captionHolders: {
			width: '100%',
			background: "#fff",
			height: 100,
			position: 'absolute',
			bottom: 0,
			borderRadius: !ie ? 10 : null,
			MozBorderRadius: !ie ? 10 : null,
			webkitBorderRadius: !ie ? 10 : null,
			opacity: !ie ? .75 : null,
			filter: "alpha(opacity=75)"
		},
		captionHeadings: {
			textAlign: 'center',
			margin: '0 0 10px 0',
			fontSize: 20,
			padding: '5px 4px',
			background: '#ccc',
			color: '#000',
			fontFamily: 'Trebuchet MS'
		},
		captionText: {
			textAlign: 'center',
			color: '#333',
			margin: '-5px 0 3 0',
			fontSize: 13,
			padding: '2px 8px 4px 8px',
			fontWeight: 'bold',
			fontFamily: 'Trebuchet MS',
			lineHeight: '16px'
		},
		captionLinks: {
			color: '#a9e100',
			textDecoration: 'none'
		},
		pauseIcon: {
			background: '#26a1ce',
			padding: 8,
			color: '#fff',
			fontSize: 14,
			fontWeight: 'bold',
			fontFamily: 'Impact',
			border: 'solid 1px #ccc'
		}
	}
	

	/* ------------------
	| PREP
	------------------ */
	
	var ac = arguments.callee;
	var slidesData;
	var interruptAutoInt = false;
	var ie = navigator.appVersion.indexOf('MSIE') != -1;
	
	
	/* ------------------
	| SLIDES DATA - load JSON-formatted slides data from config file
	------------------ */
	
	var slidesData = false;
	$.ajax({
		url: pathTo_configFile,
		dataType: 'json',
		async: false,
		success: function(response) { slidesData = response; },
	});
	

	/* ------------------
	| INITIAL CHECKS - stop right here if anything wrong
	------------------ */
	
	var problem = false;
	
	//can't find holder element
	if ($('#carouselHolder').length == 0) problem = "could not find an element with ID 'carouselHolder' to house carousel in.";
	
	//no or bad imgs dir set
	if (!pathTo_imgsDir || typeof pathTo_imgsDir != 'string')
		problem = "no or invalid images directory set in @pathTo_imgsDir variable";
		
	//no or bad path to config file
	if (!pathTo_configFile || typeof pathTo_configFile != 'string')
		problem = "no or invalid path to config file set in @pathTo_configFile variable";
	
	//couldn't load config data
	if (!slidesData) problem = "could not load slides config file, or data within it has parse errors. Check path specified in @pathTo_configFile variable is correct and, if so, check formatting in file";
	
	//necessary CSS missing
	if (!css || typeof css != 'object' || !css.carousel || typeof css.carousel != 'object' || css.carousel.overflow != 'hidden' || !css.slides || typeof css.slides != 'object' || !css.slides.width || !css.slides.height || !css.slide_on || typeof css.slide_on != 'object' || !css.slide_on.width || !css.slide_on.height)
		problem = "corrupted CSS config or necessary styles missing. Please restore original";
			
	//insufficient slides
	if (slidesData.length < 3)
		problem = "must have at least 3 slides";
		
	//feedback
	if (problem) {
		alert("CAROUSEL ERROR\n\n"+problem);
		return;
	}


	/* ------------------
	| MASTER WRAPPER - if IE, allow for box model difference (i.e. padding taken from overeall dimensions, not added to it)
	------------------ */
	
	var wrapper;
	$(wrapper = document.createElement('div'))
		.appendTo('#carouselHolder')
		.css({
			width: ((slidesData.length - 1) * (css.slides.width + spaceBetweenSlides)) + css.slide_on.width,
			height: css.slide_on.height
		});
	if (ie && css.wrapper.padding)
		$(wrapper).css({
			height: $(wrapper).height() + (css.wrapper.padding * 2),
			width: $(wrapper).width() + (css.wrapper.padding * 2)
		});
	$(wrapper).css(css.wrapper);
	
	//pause icon
	var pauseIcon;
	(pauseIcon = $(document.createElement('div')))
		.appendTo($(wrapper))
		.text('||')
		.css($.extend(css.pauseIcon, {display: 'none', position: 'absolute', zIndex: 100, top: 20, right: 20}));

	
	/* ------------------
	| BUILD CAROUSEL
	------------------ */
	
	var carousel_node;
	(carousel_node = $(document.createElement('ul')))
		.appendTo($(wrapper))
		.css($.extend(css.carousel, {width: $(wrapper).width(), height: '100%', margin: 0}));
	
	
	/* ------------------
	| BUILD SLIDES - for each, add its background image (as stipulated in slidesData) and position it accordingly. Give slide 1 .on state
	| Don't position them here - that's handled by centerOnSlide(). Also add child elements for each (i.e. caption holder & caption)
	------------------ */
	
	for(var u=0; u<slidesData.length; u++) {
		
		var thisSlideIsOn = u + 1 == Math.ceil(slidesData.length / 2);
		
		var li;
		(li = $(document.createElement('li')))
			.appendTo(carousel_node)
			.css($.extend(css.slides, {background: "url('"+pathTo_imgsDir+'/'+slidesData[u].bgImgURL+"')"}))
			.attr('id', 'carouselSlide_'+u);

		if (thisSlideIsOn) { li.css('width', css.slide_on.width); li.addClass('on'); }
		li.css('cursor', 'pointer');
		
		var captionHolder;
		(captionHolder = $(document.createElement('div')))
			.appendTo(li)
			.css(css.captionHolders)
			.hide()
			
		var heading;
		(heading = $(document.createElement('h3')))
			.appendTo(captionHolder)
			.text(slidesData[u].headline)
			.css(css.captionHeadings);
			
		var text;
		(text = $(document.createElement('p')))
			.appendTo(captionHolder)
			.html(slidesData[u].caption)
			.css(css.captionText)
			.find('a').css(css.captionLinks);
	}
	
	slides = carousel_node.children('li');
	
	
	/* ------------------
	| POSITION SLIDES - called both at load and each time a new slide is turned on. @indexOfSlideToTurnOn is the index of the slide to appear
	| on. Passed when forcing a new slide to be on - not passed on load (middle slide is on by default)
	| 'on' slide is centered, which will mean some slides being pushed off the carousel's left or right boundary. These slides are picked up
	| out of the DOM and reinserted at the other end where space just became vacant
	------------------ */
	
	var positionSlides = function(indexOfSlideToTurnOn) {
		
		//prep
		
		var left;
		var on = slides.filter('.on');
		var indexOfOn = on.prevAll('li').length;
		var wasOn, wasOnIndex, bumped;
		var carouselMidPoint = (carousel_node.width() / 2) - (css.slide_on.width / 2);
		
		//if any time other than load, turn off currently-on slide and turn on slide represented by @indexOfSlideToTurnOn
		
		if (indexOfSlideToTurnOn || indexOfSlideToTurnOn === 0) {
			wasOn = on;
			wasOnIndex = indexOfOn;
			wasOn.removeClass('on');
			on = slides.eq(indexOfSlideToTurnOn);
			on.addClass('on');
			
			//log indexes of bumped slides
			bumped = indexOfSlideToTurnOn - Math.floor(slidesData.length/2); //slides bumped beyond boundary
			var slidesBumped = [];
			if (bumped > 0)
				for(var b=0; b<bumped; b++) slidesBumped.push(b);
			else
				for(var b=0; b<Math.abs(bumped); b++) slidesBumped.push(slidesData.length-1-b);
		}
		
		//prep cont.
		var indexOfOn = on.prevAll('li').length;
		
		//iterate over slides, work out new left pos and do anim on each. For new 'on' slide, enlarge (for prev on slide, contract)

		slides.each(function() {
			
			var thisSlideIndex = $(this).prevAll('li').length;
			var thisSlideIsOn = indexOfOn == thisSlideIndex;
			
			if (!indexOfSlideToTurnOn && indexOfSlideToTurnOn !== 0) {
				left = thisSlideIndex * (css.slides.width + spaceBetweenSlides);
				if (indexOfOn < thisSlideIndex) left += css.slide_on.width - css.slides.width;
			} else {
				if (thisSlideIndex == indexOfSlideToTurnOn) {
					left = carouselMidPoint;
				} else {
					if (thisSlideIndex > indexOfSlideToTurnOn) {
						left = (Math.floor(slidesData.length / 2) * (css.slides.width + spaceBetweenSlides)) + css.slide_on.width + spaceBetweenSlides;
						left += ((thisSlideIndex - indexOfSlideToTurnOn)-1) * (css.slides.width + spaceBetweenSlides);
					} else
						left = carouselMidPoint - ((indexOfSlideToTurnOn - thisSlideIndex) * (css.slides.width + spaceBetweenSlides));
				}
			}
			
			var animCallback;
			var duration = indexOfSlideToTurnOn || indexOfSlideToTurnOn == '0' ? transitionDuration * 1000 : startSlideOutDuration * 1000;
			var animDataObj = {left: left};
			
			//this slide was on, is no longer
			if (wasOnIndex != undefined && wasOnIndex == thisSlideIndex) {
				$.extend(animDataObj, css.slides);
				$(this).children('div').hide();
			}
			
			//this slide is being turned on
			if (wasOn != undefined && thisSlideIndex == indexOfSlideToTurnOn || (!indexOfSlideToTurnOn && indexOfSlideToTurnOn !== 0 && Math.floor(slides.length / 2) == thisSlideIndex))
				$.extend(animDataObj, css.slide_on, {opacity: 1, filter: 'alpha(opacity=100)'});
			
			//do anim on slides (unless browser is shit, ie IE, in which case static change)
			if (!ie)
				$(this).animate(animDataObj, duration, null, function() {
					if (thisSlideIsOn) showCaption();
				});
			else {
				$(this).css(animDataObj);
				if (thisSlideIsOn) showCaption();
			}
			
		});
		
		//for slides bumped beyond the carousel's left or right boundary, as it slides out into invisibility, clone it and, simultaneously,
		//slide in the clone at the end, giving wrap effect. After anim, remove clone and replace with actual slide that was bumped by
		//removing it from the DOM and reinserting it (this maintains the node order - crucial to anim)
		//cloneData = multi-dim array for each clone: [0] = clone node, [1] = target left, [2] = reference to slide that was cloned, [3] = reinsertion
		//func. Storing all this means that, after each() has run, we can animate all clones to their places and remove the slides they are clones of.


		if (bumped != undefined) {
			var cloneData = [];
			var counter = 0;
			slides.each(function() {
				var thisSlideIndex = $(this).prevAll().length;
				if ($.inArray(thisSlideIndex, slidesBumped) != -1) {
					
					var thiss = $(this);
					var arrayPos = $.inArray(thisSlideIndex, slidesBumped);
					var arrayPos_inverted = (slidesBumped.length - 1) - arrayPos;
					
					//bumped off left or right edge?
					var boundary = thisSlideIndex < indexOfSlideToTurnOn ? 'left' : 'right';
					
					//clone bumped slide, insert it into DOM and position it in preparation for its appearance. DOM insertion place depends on
					//which other slides bumped - must maintain DOM order for future shuffling to work!
					var clone = $(this).clone();
					
					if (boundary == 'right')
						var insertionFunc = function() { arrayPos === 0 && slidesBumped.length > 1 ? carousel_node.children('li').eq(arrayPos).after(clone) : carousel_node.prepend(clone); }
					else
						var insertionFunc = function() { carousel_node.append(clone); }
						
					clone.css('left', boundary == 'right' ? -css.slides.width : carousel_node.width());
					
					//work out target left for clone
					if (boundary == 'right')
						var targetLeft = counter * (css.slides.width + spaceBetweenSlides);
					else
						var targetLeft = carousel_node.width() - (css.slides.width * (arrayPos_inverted + 1)) - (arrayPos_inverted * spaceBetweenSlides);

					//log this clone's data
					cloneData.push([clone, targetLeft, $(this), insertionFunc]);
					counter++;
					
				}
			});
			
			//visually bring in clones now each() has done
			$.each(cloneData, function(key, val) {
				val[3]();
				val[0].animate({left: val[1]}, transitionDuration * 1000, null, function() {
					val[2].remove();
					slides = carousel_node.children('li');
				});
			});
			
		}
		
	}


	/* ------------------
	| SET UP AUTO INT - on time out so we know starting slide-out has time to run before we set this up
	------------------ */

	setTimeout(function() {
		autoInt = setInterval(function() {
			if (interruptAutoInt || slides.filter(':animated').length != 0) return;
			slides.eq(Math.ceil(slidesData.length/2)).click();
		}, pauseDuration * 1000);
	}, (pauseDuration + startSlideOutDuration) * 1000);
	
	
	/* ------------------
	| SHOW CAPTION of currently-on slide
	------------------ */
	
	var showCaption = function() {
		var onSlide = slides.filter('.on');
		onSlide.children('div').fadeIn('slow');
	}
	
	
	/* ------------------
	| CENTER starting .on slide
	------------------ */	
	
	positionSlides();
	
	
	/* ------------------
	| CLICK - when a not-on slide is clicked, turn it on. If clicked again while on, go to slide's associated URL
	------------------ */
	
	carousel_node.delegate('li:not(.on)', 'click', function() {
		positionSlides($(this).prevAll('li').length);
	});
	carousel_node.delegate('li.on', 'click', function() { location.href = slidesData[$(this).attr('id').match(/\d+$/)].linkURL; });
	
	
	/* ------------------
	| PAUSE ICON - show/hide pause icon on mouseenter/leave on the carousel. Mouse enter interrupts the auto rotate - resumes on mouseleave.
	------------------ */
	
	$(wrapper).bind('mouseenter mousedown', function() {
		interruptAutoInt = true;
		if (!pauseIcon.is(':animated')){
			pauseIcon.fadeIn() }
		else {
			pauseIcon.stop();
			pauseIcon.css({opacity: 1, filter: "alpha(opacity=100)"});
		}
	});
	$(wrapper).mouseleave(function() {
		pauseIcon.fadeOut();
		setTimeout(function() {
			interruptAutoInt = false;
		}, pauseDuration * 1000);
	});
	
	
};
google.setOnLoadCallback(carousel);
