(function($) {

	//this function sets up the plugin for all passed elements
	$.fn.startPuzzle = function(settings) {
		for (var i = 0; i < this.length; i++) {
			startPuzzle(this[i], settings);
		}
	}
	
	//these are the default global settings - if you fail to specify one, these will fill the hole
	//see the docs for elaborate explainations
	var defaultsettings = {
		randomorder		: false,
		width			: 600,
		height 			: 220,
		margin			: 1,
		startatimage	: false,
		fadetime 		: 500,
		delaytime 		: 100,
		movetime 		: 100,
		shufflemoves 	: 1000,
		showimageonbg	: true,
		showtextonwin	: 'Nice!',
		loopimages		: true,
		puzzlesettings	: new Object()
	};
	
	/*
	This function goes over all images and fills up their missing properties
	properties	: an object that holds the settings per 
	amount		: the amount of slides - if one is not specified in the settings, it's still given the defaults
	*/
	function addDefaulImageProperties(properties, amount) {
		for (var i = 0; i < amount; i++) {
			if (!properties[i]) {
				properties[i] = new Object();
			}
			if (!properties[i]['cols']) 		properties[i]['cols'] = 4;
			if (!properties[i]['rows'])			properties[i]['rows'] = 4;
			if (!properties[i]['bgopacity'])	properties[i]['bgopacity'] = 0.3;
		}
	}
	
	/*
	This function is basically the core of the plugin - it creates and maintains the game
	The HTML slidingpuzzle object is passed, as well as an object with settings
	*/
	function startPuzzle(obj, settingsparam) {
		var puzzleObj = $(obj);
		
		//the default settings are used to fill any gaps
		var settings = $.extend({},defaultsettings,settingsparam);
		addDefaulImageProperties(settings['puzzlesettings'], puzzleObj.children().length);
		puzzleObj.css({width : settings['width'], height : settings['height']});
		
		//If it's always the same amount, there might be a visible pattern with only limited fragments (< 6)
		settings['shufflemoves'] += parseInt(Math.random() * 2) - 1
		//the sources of the images are stored for later access before we clear the object's content
		var puzzleArray = new Array();
		puzzleObj.children('img').each( function(index, element) {
			puzzleArray.push(element.src);
		});
		puzzleObj.html("");
		
		var emptyFrag = new Object();
		var listenersDisabled = false;
		var sequence = new Array();
		
		//set up an image sequence and define the first image, either random or ordered
		for (var i = 0; i < puzzleArray.length; i++)
			sequence.push(i);
		if (settings['randomorder']) {
			shuffle(sequence);
		}
		var sequenceIndex = 0;
		var currentIndex;
		//set up the index so we can keep track of what image we're showing
		if (settings['startatimage'] == false) {
			currentIndex = sequence[sequenceIndex];
		}
		else {
			currentIndex = settings['startatimage'] % sequence.length;
			for (var i = 0; i < sequence.length; i++) {
				if (currentIndex == sequence[i]) {
					sequenceIndex = i;
				}
			}
		}
		//and start off the first image
		nextImage();
		
		//This function creates different fragments and creates an opague background image and shows them
		//When it's done, it kicks off the shuffleImage and updateClickListeners functions
		function nextImage() {
			var waittime = 0;
			var s = settings['puzzlesettings'][currentIndex];
			//For every fragment..
			for (var i = 0; i < s['cols']; i++) {
				for (var j = 0; j < s['rows'] && !(i == s['cols']-1 && j == s['rows']-1); j++) {
					waittime += settings['delaytime'];
					fragment = $("<div class='fragment frag"+i+"_"+j+"'></div>");
					//the fragment is positioned and given the full image as background, but then panned
					fragment.css({ 'background-image' 	: 'url('+puzzleArray[currentIndex]+')',
								   left 				: (settings['margin'] + i * settings['width'] / s['cols'])+'px',
								   top  				: (settings['margin'] + j * settings['height'] / s['rows'])+'px',
								   'background-position': -1 * (settings['margin'] + i * settings['width'] / s['cols'])+'px '+ -1 * (settings['margin'] + j * settings['height'] / s['rows'])+'px'
								  })
							.width(settings['width'] / s['cols'] - 2*settings['margin'])
							.height(settings['height'] / s['rows'] - 2*settings['margin']);
					fragment.hide();
					puzzleObj.append(fragment);
					fragment.delay(waittime).fadeIn(settings['fadetime']);
				}
			}
			//the image on the background is added here
			if (settings['showimageonbg']) {
				fullPuzzle = $("<div class='bgpuzzle'></div>");
				fullPuzzle.css({'background-image' 	: 'url('+puzzleArray[currentIndex]+')'})
						  .width(settings['width'])
						  .height(settings['height']);
				fullPuzzle.hide();
				puzzleObj.append(fullPuzzle)
				//the wait time still remains from the previous fragment, so we can just increase it once more
				fullPuzzle.delay(waittime + settings['delaytime']).fadeTo(settings['fadetime'], s['bgopacity']);
			}
			//the fragments can be shuffled now - the function returns the coordinates of the empty field
			emptyFrag = shuffleImage();
			//and the listeners are updated so that the proper fragments respond to clicks
			listenersDisabled = false;
			updateClickListeners(emptyFrag);
		}
		
		//This function shuffles the fragments around
		//The coordinates of the empty space are returned
		function shuffleImage() {
			var s = settings['puzzlesettings'][currentIndex];
			var e = {x: s['cols']-1, y: s['rows']-1}
			var dirs;
			//this part is looped as long as we end up shuffling to the winning situation
			//believe me, it happens - nonstop, when your image is sufficiently small ;)
			do {
				for (var i = 0; i < settings['shufflemoves']; i++) {
					dirs = new Array();
					//see which of the surrounding fragments actually exist
					if (e['x'] - 1 >= 0) 		dirs.push({x : e['x'] - 1, y : e['y']});
					if (e['x'] + 1 < s['cols']) dirs.push({x : e['x'] + 1, y : e['y']});
					if (e['y'] - 1 >= 0) 		dirs.push({x : e['x'], 	   y : e['y'] - 1});
					if (e['y'] + 1 < s['rows']) dirs.push({x : e['x'],     y : e['y'] + 1});
					var r = parseInt(Math.random() * dirs.length);
					//and then re-assign the class of the selected fragment, essentially moving it
					var fragment = puzzleObj.children('.frag'+dirs[r]['x']+"_"+dirs[r]['y']).last();
					fragment.attr('class','fragment frag'+e['x']+"_"+e['y']);
					e = dirs[r]
				}
				//now we need to place all fragments on their correct graphical position
				for (var i = 0; i < s['cols']; i++) {
					for (var j = 0; j < s['rows']; j++) {
						//select the fragment, and if it's not the empty square..
						var fragment = puzzleObj.children('.frag'+i+"_"+j).last();
						if (typeof fragment !== 'undefined') {
							//position it propertly
							fragment.css({ 'left'	: (settings['margin'] + i * settings['width'] / s['cols'])+'px',
										   'top'	: (settings['margin'] + j * settings['height'] / s['rows'])+'px'
							});
						}
					}
				}
			} while(puzzlecomplete());
			return e;
		}
		
		//This function applies the click-listener to all the fragments surrounding the empty field
		function updateClickListeners() {
			var e = emptyFrag;
			var s = settings['puzzlesettings'][currentIndex];
			//Remove any listeners that are still present
			puzzleObj.find('.fragment').unbind('click')
						  .css({'cursor':'default'});
			var frags = new Array();
			//for all the bordering fragments that actually exist
			if (e['x'] - 1 >= 0) 		frags.push({x : e['x'] - 1, y : e['y']});
			if (e['x'] + 1 < s['cols']) frags.push({x : e['x'] + 1, y : e['y']});
			if (e['y'] - 1 >= 0) 		frags.push({x : e['x'],     y : e['y'] - 1});
			if (e['y'] + 1 < s['rows']) frags.push({x : e['x'],     y : e['y'] + 1});
			$.each(frags, function(index, coords) {
				//attach a listener and style them so they have a little hand symbol
				var frag = puzzleObj.children('.frag'+coords['x']+"_"+coords['y']).last();
				frag.click(fragmentListener)
					.css({'cursor':'pointer'});
			});
		}
		
		//This function is triggered when a fragment is clicked
		function fragmentListener() {
			var s = settings['puzzlesettings'][currentIndex];
			if (!listenersDisabled) {
				listenersDisabled = true;
				//Distill its coordinates from the class specification of the fragment that was clicked
				var point = this.className.split(" ").last().substr(4).split("_");
				//move it to the empty space
				this.className = "fragment frag"+emptyFrag['x']+"_"+emptyFrag['y'];
				$(this).animate({left : (settings['margin'] + emptyFrag['x'] * settings['width'] / s['cols'])+'px',
							  	 top  : (settings['margin'] + emptyFrag['y'] * settings['height'] / s['rows'])+'px'
								 },
								 settings['movetime'],
								 function() {
								 	//when it has been moved, (temporarily) remove the listeners and check if it was the last one
								 	listenersDisabled = false;
								 	updateClickListeners();
								 	if (puzzlecomplete()) {
								 		win();
								 	}
								 }
								);
				//and of course administer the new empty spot
				emptyFrag = {x : parseInt(point[0]), y :  parseInt(point[1])}
			}
		}
		
		//This is called when the puzzle has been completed
		//It displays the background image on full opacity and possibly a congratulatory dialogue
		function win() {
			//first we need to disable any user influence
			listenersDisabled = true;
			puzzleObj.find('.fragment').unbind('click')
									   .css({'cursor':'default'});
			//if the image wasn't shown on the background yet, create it now
			fullPuzzle = puzzleObj.children('.bgpuzzle').last();
			if (typeof fullPuzzle == 'undefined') {
				fullPuzzle = $("<div class='bgpuzzle'></div>");
				fullPuzzle.css({'background-image' 	: 'url('+puzzleArray[currentIndex]+')'})
						  .width(settings['width'])
						  .height(settings['height']);
				fullPuzzle.hide();
				puzzleObj.append(fullPuzzle)
			}
			//and we fade it in
			fullPuzzle.fadeTo(settings['fadetime'], 1, function() {
				//if we need to set up text to show
				if (settings['showtextonwin'] != false) {
					//create a div, center it and fill it with the text
					var hoverText = $("<div class='hovertext'><span>"+settings['showtextonwin']+"</span><div class='blackbg'></div></div>");
					hoverText.hide();
					puzzleObj.append(hoverText);
					hoverText.css({ left : (settings['width'] / 2 - hoverText.width() / 2) + 'px',
									top  : (settings['height'] / 2 - hoverText.height() / 2) + 'px'
								  });
					//the transperant black background is added
					hoverText.children('.blackbg').width(hoverText.width())
												  .height(hoverText.height());
					//text text is shown, and then hidden again
					hoverText.fadeIn(settings['fadetime'], function() { 
						hoverText.delay(500).fadeOut(settings['fadetime'], function() { hoverText.remove(); });
					});
				}
				//the next part of the chain is triggered with a variable time
				setTimeout(cleanupimage, (settings['showtextonwin'] != false) ? settings['fadetime'] * 2 + 500 : 500);
			});
		}
		
		//This is the final function in the chain of functions that run this game
		//The current image is cleaned up, and depending on the settings the next one might be loaded
		function cleanupimage() {
			//all fragments are removed
			puzzleObj.children('.fragment').remove();
			//and the next image's index is calculated
			sequenceIndex++;
			if (settings['loopimages']) {
				sequenceIndex %= sequence.length
			}
			//if there's an image with this index
			if (sequence.length > sequenceIndex) {
				currentIndex = sequence[sequenceIndex];
				//the full image is faded and we kick off the next image
				puzzleObj.children('.bgpuzzle').last().fadeOut(settings['fadetime'], function() {
					puzzleObj.children('.bgpuzzle').last().remove();
					nextImage();
				});
			}
		}
		
		//this function checks if all fragments are in place
		//and a boolean is returned regarding the result
		function puzzlecomplete() {
			var s = settings['puzzlesettings'][currentIndex];
			var childs = puzzleObj.children(".fragment");
			for (var i = 0; i < s['cols']; i++) {
				for (var j = 0; j < s['rows'] && !(i == s['cols']-1 && j == s['rows']-1); j++) {
					//the coordinates are distilled from the fragment and compared to one matching its index
					var point = childs.get(i * s['rows'] + j).className.split(" ").last().substr(4).split("_");
					if (! (point[0] == i && point[1] == j))
						return false;
				}
			}
			return true;
		}
		
	}
	
	//General purpose function to shuffle an array
	function shuffle(arr) {
		for(var j, x, i = arr.length; i; j = parseInt(Math.random() * i), x = arr[--i], arr[i] = arr[j], arr[j] = x);
		return arr;
	}
	
	//a small prototype to grab the last element of an array
	Array.prototype.last = function() {
		return this[this.length-1];
	}
	
})(jQuery);
