var squishy = function($){
	var pub,
		debug,
		config,
		css,
		anim,
		el,
		acceptingClicks;
	
	debug = false;
	
	acceptingClicks = true;
	
	anim = $.extend({}, squishyConfig.anim || {}, {
		
	});
	
	config = $.extend({}, squishyConfig.config || {}, {
		squishyStyles: [
			'width',
			'height',
			'opacity',
			'top',
			'left'
		],
		openClass: 'open',
		shutClass: 'shut',
		activeClass: 'active',
		inactiveClass: 'inactive'
	});
	
	css = $.extend({}, squishyConfig.css || {}, {
		
	});
	
	el = {
		grid: '#grid',
		squishwrap: '#squishwrap',
		sides: '#left, #right',
		columns: '.col',
		clickable: '.clickable',
		clicker: '.clicker',
		info: '.info',
		squishy: '.squishy',
		pieces: '.backdrop, .info',
		plain: '.plain',
		corners: '.corners',
		nav: config['topOffsetElement']
	};
	
	
	var totalImages = 0,
		loadedImages = 0;
	
	/**
	 * Checks to make sure that all the images have been loaded before
	 * initializing squishy.
	 * 
	 * @public
	 */
	function start() {
		var items = $(el.squishwrap).find('img.backdrop');
		totalImages = items.length;
		items.each(function(elt) {
			$(window).load(function() {
				loadedImages++;
//				console.log("LOADED... "+$(this).attr('alt')+" -> "+loadedImages);
				if(loadedImages >= totalImages) {
					init();
				}
			});
		});
	}
	
	
	/**
	 * Initialize squishy
	 * 
	 * @public
	 */
	function init() { debug && console.log('===== init()');
		oldieCheck();
		setDimensions();
		embedStyles();
		bindHandlers();
	}
	
	
	
	/**
	 * Bind event handlers
	 * 
	 * @private
	 */
	function bindHandlers() { debug && console.log('----- bindHandlers()');
		$(el.clicker).bind({
			click: clickRouter
		});
		
		$(window).bind({
			resize: setDimensions
		});
		
		// For some reason in IE the click doesn't penetrate all the way
		// to the link element, so we have to capture it and manually
		// trigger the link.
		if ($.browser.msie) {
			$(el.clickable).click(function(){
				$(this).find(el.clicker).trigger('click');
			});
		}
	}
	
	
	
	/**
	 * Add corner elements to all squishy elements.
	 * 
	 * @private
	 */
	function oldieCheck() { debug && console.log('----- cornerUp()');
		if ($.browser.msie && $.browser.version < 9) {
			$('body').addClass(config['oldieClass']);
		}
	}
	
	
	
	/**
	 * Set all squishy styles directly on the element.
	 * 
	 * @private
	 */
	function embedStyles() { debug && console.log('----- embedStyles()');
		var $squishy = $(el.squishy).find(el.pieces).andSelf(),
			styles = config['squishyStyles'];
			
		$squishy.each(function(){
			var $this = $(this);
			
			for(var i = 0, len = styles.length; i < len; i++) {
				// Make sure only explicit numeric declarations are set.
				var value = parseFloat($this.css(styles[i]));
				if (!isNaN(value)) {
				 	$this.css(styles[i], value);
				}
			}
		});
		
		$(el.info).css({ zIndex: '', opacity: 0, visibility: 'hidden' });
	}
	
	
	
	/**
	 * Route click events to the appropriate function.
	 * 
	 * @private
	 * 
	 * @param {Object} ev jQuery Event Object
	 * 
	 * @returns false to prevent the browser from attempting to navigate the link.
	 */
	function clickRouter(ev) { debug && console.log('----- clickRouter()');
		
		if (acceptingClicks) {
			var $target = $(ev.target).parent(),
				targetCol = $target.parent().attr('id'),
				$active = $('.' + config['activeClass']),
				activeCol = $active.parent().attr('id');
				
			if ($active.length == 0) {
				// Nothing is open, so we can just open the target.
				open($target);
				
			} else {
				if ($target.is('.' + config['activeClass'])) {
					// The target is the active element, so just shut it.
					shut($target);
					
				} else if ($target.parent().is('.' + config['openClass']) && targetCol == activeCol) {
					// The target is in the same column as the active element, so we can just open the target.
					open($target);
					
				} else if (config['oto'] == 'sequential'){
					var deferred = $.Deferred();
					
					deferred.then(function(){
						open($target);
					});
					
					shut($active, deferred);
					
				} else if (config['oto'] == 'direct') {
					executionCleanup();
					open($target);
				}
			}
		}
		
		return false;
	}
	
	
	
	/**
	 * Create an object that represents the first "frame" of the pending animation for the specified target element.
	 * 
	 * @param {String|Object} target String or jQuery object selector for column to be opened.
	 * @param {Object} start CSS object or origin values for the element.
	 * @param {Object} finish CSS object of destination values for the element.
	 * 
	 * @returns {Object} An object representing the first "frame" of an animation for the target.
	 */
	// function getFirstFrame(target, styles, base, dir) {
	function getFirstFrame(target, start, finish) {
		var $t = $(target),
			target = $t.attr('id'),
			duration = anim[$t.attr('data-action')] || 500,
			totalFrames = Math.ceil(duration / 1000 * config['fps']),
			frame = {};
		
		for (var style in start) {
			var s = parseFloat(start[style]),
				f = parseFloat(finish[style]);
			
			if (f !== undefined && !isNaN(f) && !isNaN(s)) {
				frame[style] = {
					value: s,
					fd: (f - s) / totalFrames,
					b: s,
					c: f - s
				}
			}
		}
		
		return frame;
	}
	
	
	
	/**
	 * Iterate over all elements in the collection and push them onto the queue array
	 * to create an array of "first frame" elements. Because the queue array is numeric, execution
	 * order per group, but not necessarily per element.
	 * 
	 * @private
	 * 
	 * @param {Array} queue Array to use as queue
	 * @param {Array} collections Array of jQuery collections
	 * @param {String|Object} target Selector
	 */
	function enqueue(queue, collections, target) { debug && console.log('----- enqueue()');
		var	$target = $('[data-group=target]'),
			duration = anim[$target.attr('data-action')] || 500,
			totalFrames = Math.ceil(duration / 1000 * config['fps']),
			targetAction = $target.attr('data-action'),
			targetColid = $target.parent().attr('id'),
			targetId = $target.attr('id'),
			targetKey = targetId + targetAction;
			
		for (var group in collections) {
			// Step through each group in the collection.
			collections[group].each(function(){
				var $this = $(this),
					id = $this.attr('id'),
					pid = $this.parent().attr('id'),
					colid = ($this.is(el.columns)) ? $this.attr('id') : $this.parents(el.columns).attr('id') ,
					group = $this.attr('data-group'),
					action = $this.attr('data-action'),
					base = {},
					frame = {},
					origin = {},
					destination = {};
				
				// Add any global overrides to the base.
				for (var type in css['global']) {
					if ($this.hasClass(type) && css['global'][type][group][targetAction]) {
						$.extend(base, css['global'][type][group][targetAction]);
					}
				}
				
				// Determine which origin and destination CSS to use
				// Pieces have a slightly different structure and need to be handled sepeartely.
				if (!id && group.indexOf('Pieces') != -1) { // growAffectedPieces|shrinkAffectedPieces|targetPieces
					var itemKey = 'img';
					
					if ($this.hasClass('info')) { itemKey = 'info'; }

					if (css[pid] 
						&& css[pid]['pieces']
						&& css[pid]['pieces'][itemKey]) {
						
						if (css[pid]['pieces'][itemKey][targetKey]) {
							destination = css[pid]['pieces'][itemKey][targetKey];
						} else {
							destination = css[pid]['pieces'][itemKey][action]
						}
					}
					
				} else if (css[id] && css[id][targetKey]) {
					destination = css[id][targetKey];
					
				} else if (css[id] && css[id][action]) {
					destination = css[id][action];
				}
				
				// Combine the base with the destination.
				destination = $.extend(base, destination);
				
				if (!$.isEmptyObject(destination)) {
					// Create an origin object based on the current settings of the element.
					for (var style in destination) {
						var	value = parseFloat($this.css(style));
						
						if (value !== undefined && value !='auto' && !isNaN(value)) {
							origin[style] = value;
						}
					}
					
					// Request the frame CSS.
					frame = getFirstFrame($this, origin, destination);
					
					if (!$.isEmptyObject(frame)) {
						queue.push({
							target: $this,
							css: frame
						});
					}
				}
			});
		}
	}
	
	
	
	/**
	 * Trigger the beginning of an element opening sequence. The sequence is uninteruptable, and
	 * when completed, the target element will be in it's "open" state with all other elements either
	 * in their "shut" state, or in thier "targetopen" state.
	 * 
	 * @public
	 * 
	 * @param {String|Object} target String or jQuery object selector for column to be opened.
	 */
	function open(target) { debug && console.log('----- open()');
		var $target = $(target),
			$targetPieces = $(target).find(el.pieces),
			$growing = $(target).parents(el.columns),
			$growAffected = $growing.find(el.squishy).not($target),
			$growAffectedPieces = $growAffected.find(el.pieces),
			$shrinking = $(el.columns).not($growing),
			$shrinkAffected = $shrinking.find(el.squishy),
			$shrinkAffectedPieces = $shrinkAffected.find(el.pieces),
			queue = [];
		
		$target.attr({
			'data-group': 'target',
			'data-action': 'open'
		});
		 
		$targetPieces.attr({
			'data-group': 'targetPieces',
			'data-action': 'open'
		});
		
		$growing.attr({
			'data-group': 'growing',
			'data-action': 'open'
		});
		
		$growAffected.attr({
			'data-group': 'growAffected',
			'data-action': 'open'
		});
		
		$growAffectedPieces.attr({
			'data-group': 'growAffectedPieces',
			'data-action': 'open'
		});
		
		$shrinking.attr({
			'data-group': 'shrinking',
			'data-action': 'shut'
		});
		
		$shrinkAffected.attr({
			'data-group': 'shrinkAffected',
			'data-action': 'shut'
		});
		
		$shrinkAffectedPieces.attr({
			'data-group': 'shrinkAffectedPieces',
			'data-action': 'shut'
		});
		
		// For opening procedures we need to bring the info element forward.
		// $target.find(el.info).prependTo($target).css({ zIndex: 9999 });
		$target.find(el.info).css({ zIndex: 9999, opacity: 0, visibility: '' });
		
		// Don't react to clicks while we're openeing a panel.
		acceptingClicks = false;
		
		// Populate queue. For opening the items are queued in
		// the following order for space management.
		// 1. $shirnkAffectedPieces
		// 2. $shrinkAffected
		// 3. $shrinking
		// 4. $growing
		// 5. $growAffected
		// 6. $growAffectedPieces
		// 7. $target
		// 8. $targetPieces.
		
		enqueue(queue, [
			$shrinkAffectedPieces,
			$shrinkAffected,
			$shrinking,
			$growing,
			$growAffected,
			$growAffectedPieces,
			$target,
			$targetPieces
		]);
		
		executeQueue(queue);
							
		return false;
	}
	
	
	
	/**
	 * Trigger the beginning of an element shut sequence. The sequence is uninteruptable, and when completed,
	 * all elements will be in their "shut" state.
	 * 
	 * @public
	 * 
	 * @param {String|Object} target String or jQuery object selector for column to be opened.
	 * @param {Ojbect} [deferred] jQuery Deferred Object. If present, will be passed to clanup function.
	 */
	function shut(target, deferred) { debug && console.log('----- shut()');
		var $target = $(target),
			$targetPieces = $(target).find(el.pieces),
			$shrinking = $(target).parents(el.columns),
			$shrinkAffected = $shrinking.find(el.squishy).not($target),
			$shrinkAffectedPieces = $shrinkAffected.find(el.pieces),
			$growing = $(el.columns).not($shrinking),
			$growAffected = $growing.find(el.squishy),
			$growAffectedPieces = $growAffected.find(el.pieces),
			queue = [];
		
		$target.attr({
			'data-group': 'target',
			'data-action': 'shut'
		});
		
		$targetPieces.attr({
			'data-group': 'targetPieces',
			'data-action': 'shut'
		});
		
		$shrinking.attr({
			'data-group': 'shrinking',
			'data-action': 'shut'
		});
		
		$shrinkAffected.attr({
			'data-group': 'shrinkAffected',
			'data-action': 'shut'
		});
		
		$shrinkAffectedPieces.attr({
			'data-group': 'shrinkAffectedPieces',
			'data-action': 'shut'
		});
		
		$growing.attr({
			'data-group': 'growing',
			'data-action': 'shut'
		});
		
		$growAffected.attr({
			'data-group': 'growAffected',
			'data-action': 'shut'
		});
		
		$growAffectedPieces.attr({
			'data-group': 'growAffectedPieces',
			'data-action': 'shut'
		});
		
		// Don't react to clicks while we're openeing a panel.
		acceptingClicks = false;
		
		// Populate queue. For opening the items are queued in
		// the following order for space management.
		// 1. $targetPieces
		// 2. $target
		// 3. $shirnkAffectedPieces
		// 4. $shrinkAffected
		// 5. $shrinking
		// 6. $growing
		// 7. $growAffected
		// 8. $growAffectedPieces
		
		enqueue(queue, [
			$targetPieces,
			$target,
			$shrinkAffectedPieces,
			$shrinkAffected,
			$shrinking,
			$growing,
			$growAffected,
			$growAffectedPieces
		]);
		
		executeQueue(queue, deferred);
		
		return false;
	}
	
	
	
	/**
	 * Execute the queue to transform the elements from one state to another.
	 * 
	 * @private
	 * 
	 * @param {Array} queue An array of frame objects.
	 * @param {jQuery.Deferred} [deferred] Deferred object that will be passed on to the cleanup function if present.
	 */
	function executeQueue(queue, deferred) { debug && console.log('----- executeQueue()');
		var $target = $('[data-group=target]'),
			duration = anim[$target.attr('data-action')] || 500,
			executeFrame, 
			executeInterval,
			currentFrame = 1,
			totalFrames = Math.ceil(duration / 1000 * config['fps']);
		
		executeFrame = function() {
			if (currentFrame <= totalFrames) {
				for (var i = 0, len = queue.length; i < len; i++) {
					var q = queue[i],
						css = {};
					
					// Increment each css value by frame delta units, and
					// update the target's css values.
					for (var rule in q['css']) {
						if (q['css'][rule]['fd'] != 0) {
							q['css'][rule]['value'] = easing(config.easing, currentFrame, q['css'][rule]['b'], q['css'][rule]['c'], totalFrames);
							css[rule] = q['css'][rule]['value'];
						}
					}
					
					// Update the element with the new width.
					q['target'].css(css);
					if (q['target'].is('.squishy')) {
						q['target'].find('> a').css({
							width: css['width'],
							height: css['height']
						});
					}
				}
				
			} else {
				clearInterval(executeInterval);
				
				// Make sure everything got to it's final position.
				$.each(queue, function(idx){
					var q = queue[idx],
						css = {};
					
					for (var rule in q['css']) {
						css[rule] = q['css'][rule]['b'] + q['css'][rule]['c']
					}
					q['target'].css(css);
					if (q['target'].is('.squishy')) {
						q['target'].find('> a').css({
							width: css['width'],
							height: css['height']
						});
					}
				});
				
				executionCleanup(deferred);
			}
			currentFrame += 1;
		}
		
		queue && queue.length && (executeInterval = setInterval(executeFrame, duration / totalFrames));
	}
	
	
	
	/**
	 * Called once the animation portion of a transition has finished to set squishy
	 * back to a ready state. If a Deferred object is passed in, it will have it's 
	 * resolve method called.
	 * 
	 * @private
	 * 
	 * @param {jQuery.Deferred} deferred jQuery Deferred object that will have it's resolve() method called.
	 */
	function executionCleanup(deferred) { debug && console.log('----- executionCleanup()');
		var $target = $('[data-group=target]'),
			action = $target.attr('data-action');
		
		if (action == 'open') {
			// Make sure only the open column has the open class
			$('.' + config['openClass']).removeClass(config['openClass']);
			$target.parent().addClass(config['openClass']);

			// Maks sure only the active element has the active class
			$('.' + config['activeClass']).removeClass(config['activeClass']);
			$target.addClass(config['activeClass']);
			
		} else {
			// Remove the open class from all columns
			$(el.columns).removeClass('open');
			
			// Remove the active class from the active element.
			$('.' + config['activeClass']).removeClass(config['activeClass']);
		}
		
		// Remove the transitional data elements.
		$('[data-group]')
			.removeAttr('data-group')
			.removeAttr('data-action');
			
		$(el.info).not('.' + config['activeClass'] + ' > ' + el.info).css({ visibility: 'hidden' });
		
		// Trigger any deferred actions.
		if (deferred !== undefined) {
			deferred.resolve();
		}
		
		acceptingClicks = true;
	}
	
	
	
	/**
	 * Hard set calculated widths on certain elements to allow for centering and use of percentage widths later.
	 * 
	 * @private
	 */
	function setDimensions() { debug && console.log('----- setWidths()');
		var totalWidth = $(window).width(),
			gridWidth = $(el['grid']).width(),
			totalHeight = $(window).height(),
			navHeight = $(el['nav']).height(),
			$sides = $(el['sides']),
			$squishwrap = $(el['squishwrap']);
			
		$sides.css({
			width: (totalWidth - gridWidth) / 2
		});
		
		$squishwrap.css({
			width: totalWidth,
			height: totalHeight - navHeight
		});
	}
	
	
	/**
	 * Return a value with easing applied for the specified easing type and paramaters.
	 * 
	 * @public
	 * 
	 * @param {String} [which] Which easing to use, defaults to linear.
	 * @param {Number} t Current animation time. Can be any unit, but unit must match @d.
	 * @param {Number} b Begining value of animated property.
	 * @param {Number} c Total change in value of animated property.
	 * @param {Number} d Total duration of animation. Can be any unit, but unit must match @t.
	 */
	function easing(which, t, b, c, d) {
		var easers = {
			linear: function (t, b, c, d) {
				return c * t / d + b;
			},
			
			easeInQuad: function (t, b, c, d) {
				return c*(t/=d)*t + b;
			},
			
			easeOutQuad: function (t, b, c, d) {
				return -c *(t/=d)*(t-2) + b;
			},
			
			easeInOutQuad: function (t, b, c, d) {
				if ((t/=d/2) < 1) return c/2*t*t + b;
				return -c/2 * ((--t)*(t-2) - 1) + b;
			},
			
			easeInOut: this.easeInOutQuad,
			
			easeInCubic: function (t, b, c, d) {
				return c*(t/=d)*t*t + b;
			},
			
			easeOutCubic: function (t, b, c, d) {
				return c*((t=t/d-1)*t*t + 1) + b;
			},
			
			easeInOutCubic: function (t, b, c, d) {
				if ((t/=d/2) < 1) return c/2*t*t*t + b;
				return c/2*((t-=2)*t*t + 2) + b;
			},
			
			easeInQuart: function (t, b, c, d) {
				return c*(t/=d)*t*t*t + b;
			},
			
			easeOutQuart: function (t, b, c, d) {
				return -c * ((t=t/d-1)*t*t*t - 1) + b;
			},
			
			easeInOutQuart: function (t, b, c, d) {
				if ((t/=d/2) < 1) return c/2*t*t*t*t + b;
				return -c/2 * ((t-=2)*t*t*t - 2) + b;
			},
			
			easeInQuint: function (t, b, c, d) {
				return c*(t/=d)*t*t*t*t + b;
			},
			easeOutQuint: function (t, b, c, d) {
				return c*((t=t/d-1)*t*t*t*t + 1) + b;
			},
			
			easeInOutQuint: function (t, b, c, d) {
				if ((t/=d/2) < 1) return c/2*t*t*t*t*t + b;
				return c/2*((t-=2)*t*t*t*t + 2) + b;
			},
			
			easeInSine: function (t, b, c, d) {
				return -c * Math.cos(t/d * (Math.PI/2)) + c + b;
			},
			
			easeOutSine: function (t, b, c, d) {
				return c * Math.sin(t/d * (Math.PI/2)) + b;
			},
			
			easeInOutSine: function (t, b, c, d) {
				return -c/2 * (Math.cos(Math.PI*t/d) - 1) + b;
			},
			
			easeInExpo: function (t, b, c, d) {
				return (t==0) ? b : c * Math.pow(2, 10 * (t/d - 1)) + b;
			},
			
			easeOutExpo: function (t, b, c, d) {
				return (t==d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b;
			},
			
			easeInOutExpo: function (t, b, c, d) {
				if (t==0) return b;
				if (t==d) return b+c;
				if ((t/=d/2) < 1) return c/2 * Math.pow(2, 10 * (t - 1)) + b;
				return c/2 * (-Math.pow(2, -10 * --t) + 2) + b;
			},
			
			easeInCirc: function (t, b, c, d) {
				return -c * (Math.sqrt(1 - (t/=d)*t) - 1) + b;
			},
			
			easeOutCirc: function (t, b, c, d) {
				return c * Math.sqrt(1 - (t=t/d-1)*t) + b;
			},
			
			easeInOutCirc: function (t, b, c, d) {
				if ((t/=d/2) < 1) return -c/2 * (Math.sqrt(1 - t*t) - 1) + b;
				return c/2 * (Math.sqrt(1 - (t-=2)*t) + 1) + b;
			},
			
			easeInElastic: function (t, b, c, d) {
				var s=1.70158;var p=0;var a=c;
				if (t==0) return b;  if ((t/=d)==1) return b+c;  if (!p) p=d*.3;
				if (a < Math.abs(c)) { a=c; var s=p/4; }
				else var s = p/(2*Math.PI) * Math.asin (c/a);
				return -(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b;
			},
			
			easeOutElastic: function (t, b, c, d) {
				var s=1.70158;var p=0;var a=c;
				if (t==0) return b;  if ((t/=d)==1) return b+c;  if (!p) p=d*.3;
				if (a < Math.abs(c)) { a=c; var s=p/4; }
				else var s = p/(2*Math.PI) * Math.asin (c/a);
				return a*Math.pow(2,-10*t) * Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b;
			},
			
			easeInOutElastic: function (t, b, c, d) {
				var s=1.70158;var p=0;var a=c;
				if (t==0) return b;  if ((t/=d/2)==2) return b+c;  if (!p) p=d*(.3*1.5);
				if (a < Math.abs(c)) { a=c; var s=p/4; }
				else var s = p/(2*Math.PI) * Math.asin (c/a);
				if (t < 1) return -.5*(a*Math.pow(2,10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )) + b;
				return a*Math.pow(2,-10*(t-=1)) * Math.sin( (t*d-s)*(2*Math.PI)/p )*.5 + c + b;
			},
			
			easeInBack: function (t, b, c, d, s) {
				if (s == undefined) s = 1.70158;
				return c*(t/=d)*t*((s+1)*t - s) + b;
			},
			
			easeOutBack: function (t, b, c, d, s) {
				if (s == undefined) s = 1.70158;
				return c*((t=t/d-1)*t*((s+1)*t + s) + 1) + b;
			},
			
			easeInOutBack: function (t, b, c, d, s) {
				if (s == undefined) s = 1.70158; 
				if ((t/=d/2) < 1) return c/2*(t*t*(((s*=(1.525))+1)*t - s)) + b;
				return c/2*((t-=2)*t*(((s*=(1.525))+1)*t + s) + 2) + b;
			}
		} // END easers
		
		which = which || 'linear';
		
		return easers[which](t, b, c, d);
	}
	
	
	
	function setEasing(easing) { config.easing = easing; }
	
	function setOpenDuration(duration) { anim.open = duration; }
	
	function setShutDuration(duration) { anim.shut = duration; }
	
	
	
	// Publish public methods by returning them.
	pub = {
		start: start,
		init: init,
		open: open,
		shut: shut,
		easing: easing,
		setEasing: setEasing,
		setOpenDuration: setOpenDuration,
		setShutDuration: setShutDuration
	};
	return pub;
	
}(jQuery);

$(document).ready(function(){
	squishy.start();
});


