/**
 * file description
 *
 * @author: Thibaut De Muynck (ab6x.net)
 * @created: 20110412
 * @modified: 20110714 18:44
 *
 * @package: saxe 2011
 *
 * @required: doTimeout (http://benalman.com/projects/jquery-dotimeout-plugin)
 */

// global puzzle area object
function Puzzle(id, settings) {
	// manage optional settings
	if (typeof(settings) == 'undefined') { settings = {} }

	// settings
	this.settings = {
		startup: false,
		speed: 500,
		refresh: 80,
		fade: 500,
		back_class: 'puzzle_back',
		dummy_class: 'puzzle_dummy',
		// the targeted puzzlePiece object is passed to these functions as their first argument
		pre_shrink_fn: function(piece) {
			piece.container.unbind('click');
			piece.container.toggleClass('pz_shrunk pz_expanded');
		},
		pre_expand_fn: $.noop,
		post_shrink_fn: $.noop,
		post_expand_fn: function(piece) {
			piece.container.click({'piece':piece},piece.block.puzzle.settings.onclick_fn);
			piece.container.toggleClass('pz_shrunk pz_expanded');
		},
		// the targeted puzzlePiece is available to the function as 'arguments[0].data.piece', while 'this' points to the piece's container
		onclick_fn: $.noop,
		// the initial setup callback function where arguments[0] refers to the Puzzle object
		callback_fn: $.noop
	};

	$.extend(this.settings, settings);

	// puzzle properties
	this.container = null;
	this.blocks = [];
	this.width = 0;
	this.height = 0;
	this.first_run = true;

	// instantiation method
	this.init = function(id) {
		// set properties
		this.container = $(id);
		this.width = this.container.width() - this.getXtraWidth(this.container);
		this.height = this.container.height() - this.getXtraHeight(this.container);
		this.container.css({
			'position': 'relative',
			'z-index': -9998
		});
		// collect pieces
		var puzzle_elements = $(id+' > div');
		// append divs if necessary to have a multiple of 3
		var modulo = 3 - (puzzle_elements.length % 3);
		while (modulo > 0 && modulo != 3) {
			var new_div = document.createElement('div');
			$(new_div).addClass(this.dummy_class);
			this.container.append(new_div);
			puzzle_elements.push(new_div);
			modulo--;
		}
		// instantiate puzzle blocks and their pieces
		var puzzle = this;
		var block = new PuzzleBlock(this);
		puzzle_elements.each(function(pi, piece) {
			if (block.countPieces() == 3) {
				block = new PuzzleBlock(puzzle);
			}
			var new_piece = new PuzzlePiece(piece, block);
			block.addPiece(new_piece);
			if (block.countPieces() == 3) {
				puzzle.addBlock(block);
			}
		});
		// apply blocks extra sizes
		var tot_x_w = 0;
		var max_x_h = 0;
		// set block's extra sizes
		$.each(this.blocks, function(bi, block) {
			var b_x_w = 0;
			var b_x_h = 0;
			$.each(block.pieces, function(pi, piece) {
				var el = $(piece.container);
				var x_w = puzzle.getXtraWidth(el);
				var x_h = puzzle.getXtraHeight(el);
				b_x_w = (x_w > b_x_w) ? x_w : b_x_w;
				b_x_h = (x_h > b_x_h) ? x_h : b_x_h;
			});
			block.setXtraSize(b_x_w, b_x_h);
			tot_x_w += b_x_w;
			max_x_h = (b_x_h > max_x_h) ? b_x_h : max_x_h;
		});
		// apply blocks and pieces sizes and positions
		var column_width = (this.width - (1.5 * tot_x_w)) / (this.blocks.length + 1);
		$.each(this.blocks, function(bi, block) {
			var piece_height = (puzzle.height - max_x_h) / 3;
			var tot_height = 0;
			$.each(block.pieces, function(pi, piece) {
				var el = $(piece.container);
				var x_h = puzzle.getXtraHeight(el);
				piece.setSize(column_width, puzzle.getXtraWidth(el), piece_height, x_h, tot_height);
				tot_height += piece_height + x_h;
			});
			block.setSize(column_width, tot_height);
		});
		// expand startup piece
		if (this.settings.startup) {
			var piece_id = this.settings.startup % 3;
			var block_id = parseInt(this.settings.startup / 3) + 1;
			if (piece_id == 0) {
				piece_id = 3;
				block_id--;
			}
			this.blocks[block_id - 1].pieces[piece_id - 1].container.trigger('mouseenter');
		}
		else if (this.first_run) {
			// execute initial setup callback function
			this.first_run = false;
			this.settings.callback_fn(this);
		}
	};

	// private methods
	this.addBlock = function(block) {
		this.blocks.push(block);
		this.container.append(block.container);
	};

	this.getXtraWidth = function(el) {
		return parseInt(el.css('margin-left')) + parseInt(el.css('margin-right')) + parseInt(el.css('border-left-width')) + parseInt(el.css('border-right-width')) +  parseInt(el.css('padding-left')) + parseInt(el.css('padding-right'));
	};

	this.getXtraLeftWidth = function(el) {
		return parseInt(el.css('margin-left')) + parseInt(el.css('border-left-width')) +  parseInt(el.css('padding-left'));
	};

	this.getXtraRightWidth = function(el) {
		return parseInt(el.css('margin-right')) + parseInt(el.css('border-right-width')) + parseInt(el.css('padding-right'));
	};

	this.getXtraHeight = function(el) {
		return parseInt(el.css('margin-top')) + parseInt(el.css('margin-bottom')) + parseInt(el.css('border-top-width')) + parseInt(el.css('border-bottom-width')) +  parseInt(el.css('padding-top')) + parseInt(el.css('padding-bottom'));
	};

	this.getXtraTopHeight = function(el) {
		return parseInt(el.css('margin-top')) + parseInt(el.css('border-top-width')) +  parseInt(el.css('padding-top'));
	};

	this.getXtraBottomHeight = function(el) {
		return parseInt(el.css('margin-bottom')) + parseInt(el.css('border-bottom-width')) + parseInt(el.css('padding-bottom'));
	};

	this.startupTrigger = function(piece) {
		piece.settings.startup.expand();
	};

	// private methods
	this.init(id);
}

// block (3 pieces) puzzle object
function PuzzleBlock(puzzle) {
	this.container = $('<div/>');
	this.puzzle = null;
	this.pieces = [];
	this.width = 0;
	this.x_width = 0;
	this.width = 0;
	this.x_width = 0;
//	this.speed = 500;

	// instantiation method
	this.init = function(puzzle) {
		this.puzzle = puzzle;
		puzzle.container.append(this.container);
		this.container.css('position', 'relative');
		this.container.css('float', 'left');
		this.container.addClass('pz_thin');
		this.speed = this.puzzle.settings.speed;
	};

	// public methods
	this.addPiece = function(piece) {
		this.pieces.push(piece);
		this.container.append(piece.container);
	};

	this.countPieces = function() {
		return this.pieces.length;
	};

	this.setSize = function(width, height) {
		this.width = width;
		this.height = height;
		this.container.css({
			'width': width + this.x_width,
			'height': height + this.x_height
		});
	};

	this.setXtraSize = function(x_width, x_height) {
		this.x_width = x_width;
		this.x_height = x_height;
	};

	this.setPiecesPositions = function() {
		$.each(this.pieces, function(pi, piece) {
			piece.block_position.h = (piece.container.position().left == 0) ? 'left' : 'right';
			if (piece.container.position().top == 0) { piece.block_position.v = 'top'; }
			else if (piece.container.position().top + piece.container.height() + (piece.x_height * 2) >= piece.block.container.height() - piece.block.x_height) { piece.block_position.v = 'bottom'; }
			else { piece.block_position.v = 'middle'; }
		});
	};

	this.widen = function() {
		var container = this.container;
		// check wide state
		if (container.hasClass('pz_wide')) {
			return false;
		}
		container.animate(
			{
				width: (this.width + this.x_width) * 2
			},
			this.speed,
			function() {
				// animation completed
				container.toggleClass('pz_wide pz_thin');
			}
		);
	};

	this.contract = function() {
		var container = this.container;
		// check thin state
		if (container.hasClass('pz_thin')) {
			return false;
		}
		// shrink / organize inner pieces
		$.each(this.pieces, function(pi, piece) {
			if (piece.container.hasClass('pz_expanded')) {
				piece.shrink();
			}
		});
		// reduce block width
		container.animate(
			{
				width: this.width + this.x_width
			},
			this.speed,
			function() {
				// contract animation completed
				container.toggleClass('pz_wide pz_thin');
			}
		);
	};

	// private methods
	this.init(puzzle);
}

// puzzle piece unit object
function PuzzlePiece(el, block) {
	this.container = $(el);
	this.block = block;
	this.width = 0;
	this.x_width = 0;
	this.height = 0;
	this.x_height = 0;
	this.h_anchor = 'left';
	this.h_position = 0;
	this.v_anchor = 'top';
	this.v_position = 0;
	this.block_position = new Object();
//	this.speed = 500;
//	this.refresh = 90;
//	this.fade = 500;

	// instantiation method
	this.init = function() {
		// set state class
		this.container.addClass('pz_shrunk');
		// set dimensions
		this.container.css({
			'position': 'absolute',
			'overflow': 'hidden'
		});
		// settings
		this.speed = this.block.speed;
		this.refresh = this.block.puzzle.settings.refresh;
		this.fade = this.block.puzzle.settings.fade;
		// set mouse hovering event
		var pz_piece = this;
		this.container.mouseenter(function() {
			pz_piece.expand();
		});
		// set (resizable) background image
		this.container.find('img.'+this.block.puzzle.settings.back_class).each(function(i, image) {
			var parent = $(image).parent();
			var img_src = $(image).attr('src');
			$(image).remove();
			var img_container = $("<div />")
				.addClass(pz_piece.block.puzzle.settings.back_class)
				.css({
					'display': 'none',
					'position': 'absolute',
					'left': 0,
					'top': 0,
					'overflow': 'hidden',
					'z-index': -9997
			});
			parent.prepend(img_container);
			var img = $('<img/>')
				.attr('src',img_src + '?random=' + (new Date()).getTime())  //  ?random parameter to fix IE bug triggering img's load event on cache imgs
				.bind('load', function() {
					$(this).css({
						'width': parent.css('width'),
						'height': parent.css('height')
					});
					img_container.fadeIn(pz_piece.fade);
				});
			img_container.append(img);
		});
	};

	// public methods
	this.setSize = function(width, x_width, height, x_height, top_position) {
		this.width = width;
		this.x_width = x_width;
		this.height = height;
		this.x_height = x_height;
		this.v_position = top_position;
		this.container.css({
			'width': width,
			'height': height,
			'top': top_position
		});
	};

	this.expand = function() {
		// check mouse is not over an already expanded piece
		// and make sure no other animation is running in the puzzle
		var animated = $(this.block.puzzle.container+':animated');
		// when startup expansion, wait for all animations to complete before expansion
		if (this.block.puzzle.settings.startup) {
			while (animated.length > 0) {
				animated = $(this.block.puzzle.container+':animated');
			}
			this.block.puzzle.settings.startup = false;
		}
		if (this.container.hasClass('pz_expanded') || animated.length > 0) {
			return false;
		}
		var pz_piece = this;
		// determinate block's pieces positions
		this.block.setPiecesPositions();

		// execute pieces movements
		var p1 = null;
		var p2 = null;

		// hovered piece's block is contracted
		if (this.block.container.hasClass('pz_thin')) {
			$.each(this.block.puzzle.blocks, function(bi, block) {
				// contract other expanded block
				if (pz_piece.block != block && block.container.hasClass('pz_wide')) {
					block.contract();
				}
			});
			// widen current block
			this.block.widen();
			// move / resize block's pieces
			switch (this.block_position.v) {
				case 'top':
					p1 = this.block.pieces[this.locatePiece('left','middle')];
					p2 = this.block.pieces[this.locatePiece('left','bottom')];
					p1.moveBottom(this.speed);
					p2.moveRight(this.speed);
					this.resize('expand');
					break;
				case 'middle':
					p1 = this.block.pieces[this.locatePiece('left','top')];
					p1.container.css('z-index',9999);
					p1.moveRight(this.speed, { top: (p1.height + p1.x_height) * 2 });
					this.resize('expand', { top: 0 });
					break;
				case 'bottom':
					p1 = this.block.pieces[this.locatePiece('left','top')];
					p2 = this.block.pieces[this.locatePiece('left','middle')];
					p1.moveRight(this.speed);
					p2.moveTop(this.speed);
					this.resize('expand');
					break;
			}
		}

		// hovered piece's block is widened
		if (this.block.container.hasClass('pz_wide')) {
			// identify remaining pieces
			var expanded = null;
			$.each(this.block.pieces, function(pi, piece) {
				if (piece.container.hasClass('pz_expanded')) {
					expanded = piece;
				}
				else if (piece != pz_piece) {
					p1 = piece;
				}
			});
			// move / resize block's pieces
			if (this.block_position.h == 'left' && this.block_position.v == 'top') {
				p1.moveBottom(this.speed);
				expanded.resize('shrink',{ top: (expanded.height + expanded.x_height) * 2 });
				this.resize('expand');
			}
			if (this.block_position.h == 'left' && this.block_position.v == 'bottom') {
				p1.moveTop(this.speed);
				expanded.resize('shrink');
				this.resize('expand',{ bottom: 0 + this.x_height });
			}
			if (this.block_position.h == 'right' && this.block_position.v == 'top') {
				p1.moveBottom(this.speed);
				expanded.resize('shrink', { left: (expanded.width + expanded.x_width), top: (expanded.height + expanded.x_height) * 2 });
				this.resize('expand', { right: 0 });
			}
			if (this.block_position.h == 'right' && this.block_position.v == 'bottom') {
				p1.moveTop(this.speed);
				expanded.resize('shrink', { left: (expanded.width + expanded.x_width) });
				this.resize('expand', { right: 0 });
			}
		}
	};

	this.shrink = function() {
		// determinate pieces positions
		this.block.setPiecesPositions();
		var p1 = null;
		var p2 = null;
		// shrinking top piece
		if (this.block_position.v == 'top') {
			p1 = this.block.pieces[this.locatePiece('left','bottom')];
			p2 = this.block.pieces[this.locatePiece('right','bottom')];
			p1.moveMiddle(this.speed);
			p2.moveLeft(this.speed);
			this.resize('shrink');
		}
		// shrinking bottom piece
		if (this.block_position.v == 'bottom') {
			p1 = this.block.pieces[this.locatePiece('left','top')];
			p2 = this.block.pieces[this.locatePiece('right','top')];
			p1.moveMiddle(this.speed);
			p2.moveLeft(this.speed);
			this.resize('shrink', { top: (this.height + this.x_height) * 2 });
		}
	};

	// private methods
	this.locatePiece = function(h_pos, v_pos) {
		var target = null;
		$.each(this.block.pieces, function(pi, piece) {
			if (piece.block_position.h == h_pos && piece.block_position.v == v_pos) {
				target = pi;
			}
		});
		return target;
	};

	this.resize = function(mode, extranim) {
		// manage optional extranim  and self parameters
		if (typeof(extranim) == 'undefined') { extranim = false; }
		// determinate resize directions
		var rsize_dir = new Object();
		rsize_dir.h = (this.container.position().left == 0) ? 'right' : 'left';
		rsize_dir.v = (this.container.position().top > (this.block.height / 2) ) ? 'up' : 'down';
		var prev_anchor = null;
		// horizontal
		if (rsize_dir.h == 'left') {
			prev_anchor = this.h_anchor;
			this.h_anchor = 'right';
			this.h_position = 0;
			this.container.css(prev_anchor,'');
			this.container.css(this.h_anchor, this.h_position);
		}
		// vertical
		if (rsize_dir.v == 'up') {
			prev_anchor = this.v_anchor;
			this.v_anchor = 'bottom';
			this.v_position = this.block.height - (this.container.position().top + this.height);
			this.container.css(prev_anchor,'');
			this.container.css(this.v_anchor, this.v_position);
		}

		// execute piece's resizing (with optional extra animation)
		var anim_params = new Object();
		if (mode == 'expand') {
			anim_params = {
				width: (this.width * 2) + this.x_width,
				height: (this.height * 2) + this.x_height
			};
			var pre_fn = this.block.puzzle.settings.pre_expand_fn;
			var post_fn = this.block.puzzle.settings.post_expand_fn;
		}
		if (mode == 'shrink') {
			anim_params = {
				width: this.width,
				height: this.height
			};
			var pre_fn = this.block.puzzle.settings.pre_shrink_fn;
			var post_fn = this.block.puzzle.settings.post_shrink_fn;
		}
		if (extranim) {
			$.extend(anim_params, extranim);
		}
		var pz_piece = this;
		// execute pre-animate function
		pre_fn(pz_piece);
		this.container.animate(
			anim_params,
			this.speed,
			function() {
				// expand/shrink animation completed
				// reset anchors
				if (pz_piece.h_anchor != 'left') {
					prev_anchor = pz_piece.h_anchor;
					pz_piece.h_anchor = 'left';
					pz_piece.h_position = pz_piece.container.position().left;
					pz_piece.container.css(prev_anchor, '');
					pz_piece.container.css('left', pz_piece.h_position);
				}
				if (pz_piece.v_anchor != 'top') {
					prev_anchor = pz_piece.v_anchor;
					pz_piece.v_anchor = 'top';
					pz_piece.v_position = pz_piece.container.position().top;
					pz_piece.container.css(prev_anchor, '');
					pz_piece.container.css('top', pz_piece.v_position);
				}
				// reset z-indexes
				$.each(pz_piece.block.pieces, function(pi, piece) {
					piece.container.css('z-index', '');
				});
				// execute post-animate function
				post_fn(pz_piece);
				// execute initial callback function
				if (pz_piece.block.puzzle.first_run) {
					pz_piece.block.puzzle.first_run = false;
					pz_piece.block.puzzle.settings.callback_fn(pz_piece.block.puzzle);
				}
			}
		);
		// execute content resize routine (while animation runs)
		$.doTimeout(this.refresh, pz_piece.contentResize, [pz_piece, pz_piece.container.css('width'), pz_piece.container.css('height')]);
	};

	this.contentResize = function(args) {
		var piece = args[0];
		var w = args[1];
		var h = args[2];
		var p_w = piece.container.css('width');
		var p_h = piece.container.css('height');
		var back_img = piece.container.find('.'+piece.block.puzzle.settings.back_class+' img:first');
		// relaunch content resize while animation runs
		if (p_w/w != 1 || p_h/h != 1) {
			back_img.css({
				width: p_w,
				height: p_h
			});
			$.doTimeout(piece.refresh, piece.contentResize, [piece, p_w, p_h]);
		}
		else {
			// on the last iteration make sure the image has the container size
			// the refresh setting must be tweaked to fit the browser's JS engine capabilities
			//console.log('container w:'+piece.container.css('width')+' h:'+piece.container.css('height'));
			//console.log('image w:'+back_img.css('width')+' h:'+back_img.css('height'));
		}
	};

	this.moveLeft = function(speed, extranim) {
		// manage optional extranim parameter
		if (typeof(extranim) == 'undefined') { extranim = false; }
		var anim_params = new Object();
		// prepare animate parameters
		anim_params = {
			left: 0
		};
		if (extranim) {
			$.extend(anim_params, extranim);
		}
		this.container
			.animate(
				anim_params,
				speed
			);
	};

	this.moveRight = function(speed, extranim) {
		// manage optional extranim parameter
		if (typeof(extranim) == 'undefined') { extranim = false; }
		var anim_params = new Object();
		// prepare animate parameters
		anim_params = {
			left: this.width + this.x_width
		};
		if (extranim) {
			$.extend(anim_params, extranim);
		}
		this.container
			.animate(
				anim_params,
				speed
			);
	};

	this.moveTop = function(speed, extranim) {
		// manage optional extranim parameter
		if (typeof(extranim) == 'undefined') { extranim = false; }
		var anim_params = new Object();
		// prepare animate parameters
		anim_params = {
			top: 0
		};
		if (extranim) {
			$.extend(anim_params, extranim);
		}
		this.container
			.animate(
				anim_params,
				speed
			);
	};

	this.moveMiddle = function(speed, extranim) {
		// manage optional extranim parameter
		if (typeof(extranim) == 'undefined') { extranim = false; }
		var anim_params = new Object();
		// prepare animate parameters
		anim_params = {
			top: this.height + this.x_height
		};
		if (extranim) {
			$.extend(anim_params, extranim);
		}
		this.container
			.animate(
				anim_params,
				speed
			);
	};

	this.moveBottom = function(speed, extranim) {
		// manage optional extranim parameter
		if (typeof(extranim) == 'undefined') { extranim = false; }
		var anim_params = new Object();
		// prepare animate parameters
		anim_params = {
			top: (this.height + this.x_height) * 2
		};
		if (extranim) {
			$.extend(anim_params, extranim);
		}
		this.container
			.animate(
				anim_params,
				speed
			);
	};

	this.init();
}
