//  Copyright (c) 2010-2011, Ioannis (Yiannis) Chatzikonstantinou, All rights reserved.
//  http://www.yconst.com
//  http://www.volatileprototypes.com
// 
//  Redistribution and use in source and binary forms, with or without modification, 
//  are permitted provided that the following conditions are met:
//  	- Redistributions of source code must retain the above copyright 
//  notice, this list of conditions and the following disclaimer.
//  	- Redistributions in binary form must reproduce the above copyright 
//  notice, this list of conditions and the following disclaimer in the documentation 
//  and/or other materials provided with the distribution.
//  
//  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
//  EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
//  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
//  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
//  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
//  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
//  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
//  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
//  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY 
//  OF SUCH DAMAGE.

(function( $ ){
	
	//
	// Entry Point
	// _________________________________________________________
	
	$.fn.freetile = function( method ) {
		// Method calling logic
		if ( typeof FreeTile[ method ] === 'function' ) {
		  return FreeTile[ method ].apply( this, Array.prototype.slice.call( arguments, 1 ));
		} else if ( typeof method === 'object' || ! method ) {
		  return FreeTile.init.apply( this, arguments );
		} else {
		  $.error( 'Method ' +  method + ' does not exist on jQuery.FreeTile' );
		}
		
		return this;
	};
	
	FreeTile = {
		
		//
		// "Public" Methods
		// _________________________________________________________
		
		// Method 1.
		// Smart and generic method that chooses between
		// initialize or re-layout or append.
		init : function(options) {
			var _container = this;
			
			// Setup Options
			var _o = FreeTile.setupOptions(_container, options);
			
			// Setup container bindings for resize and custom events
			if (!_o.tiled) FreeTile.setupContainerBindings(_container, _o);
			
			// If there is content to append and container has been already
			// tiled, continue in append mode.
			if (_o.tiled && !$.isEmptyObject(_o.contentToAppend)) {
				_container.append(_o.contentToAppend);
				_o.contentToAppend.imagesLoaded(function() {
					FreeTile.positionAll(_container, _o);
				});
			// Otherwise continue by first just positioning the elements
			// and then doing a re-layout if any images are not yet loaded.
			} else {
				_container.imagesLoaded_timeout(function() {
					FreeTile.positionAll(_container, _o);
				});
			}
			
			return _container;
		},
		
		// Method 2.
		// Similar to method 1 but only does something if there is
		// content to append.
		append : function(options) {
			var _container = this;
			
			// Setup Options
			var _o = FreeTile.setupOptions(_container, options);
			
			// If there is content to append and container has been already
			// tiled, continue in append mode.
			if (_o.tiled && !$.isEmptyObject(_o.contentToAppend)) {
				_container.append(_o.contentToAppend);
				_o.contentToAppend.imagesLoaded(function() {
					FreeTile.positionAll(_container, _o);
				});
			}
			
			return _container;
		},
		
		// Method 3.
		// Layout all elements just once. Single shot. Nothing else
		// is done.
		layout : function(options) {
			var _container = this;
			
			// Setup Options
			var _o = FreeTile.setupOptions(_container, options);
			
			// Position Elements
			FreeTile.positionAll(_container, _o);
			
			return _container;
		},
		
		//
		// Sub Methods
		// _________________________________________________________
		
		// Setup Options Object
		setupOptions : function(_container, _options) {

			// Get the data object from the container. If it doesn't exist it probably means
			// it's the first time FreeTile is called..
			var _cdata = _container.data('freetiledata');

			// Generate the options object.
			var _newoptions = $.extend(true,
				{},
				this.defaults,
				_cdata,
				this.reset,
				_options
			);
			
			// At this point we have a nice options object which is a special blend
			// of user-defined options, stored preferences and defaults. Let's save it.
			_container.data('freetiledata', _newoptions);

			// Temporary variable to denote whether the container has already been
			// processed before.
			_newoptions.tiled = (_cdata != undefined);
			
			// The real 'animate' property is dependent, apart from user preference,
			// on whether this is the first time that FreeTile is being called (should
			// be false) and whether we are appending content (should be false too).
			// !! animate and _animate are different variables!
			// _animate is an internal variable that indicates whether animation is
			// POSSIBLE & REQUESTED !!
			_newoptions._animate = _newoptions.animate && _newoptions.tiled && $.isEmptyObject(_newoptions.contentToAppend);
			
			this.reset.callbk = _newoptions.persistentCallback && _newoptions.callbk ? _newoptions.callbk : function() {};

			return _newoptions;
		},
		
		// Setup bindings to resize and custom events.
		setupContainerBindings : function(_container, _o) {
			// Bind to window resize.
			if (_o.containerResize) {
				var win = $(window),
					curWidth = win.width(),
					curHeight = win.height();
				win.resize(function() {
					clearTimeout(_container.data("freeTileTimeout"));
					_container.data("freeTileTimeout", setTimeout(function() {
						var newWidth = win.width(),
						newHeight = win.height();
						//Call function only if the window *actually* changes size!
						if (newWidth != curWidth || newHeight != curHeight) {
							curWidth = newWidth,
							curHeight = newHeight;
							_container.freetile('layout');
						}
					}, 400) );
				});
			}
			// Bind to custom events.
			if (_o.customEvents) {
				_container.bind(_o.customEvents, function() {
					clearTimeout(_container.data("freeTileTimeout"));
					_container.data("freeTileTimeout", setTimeout(function() { _container.freetile('layout')}, 400) );
				});
			}
			return _container;
		},
		
		// Position a single element
		positionOne : function(_container, _this, _o, _v) { // Container, object, options, variables
			
			// Find out how many columns the element spans.
			var _span = Math.max(1, Math.min(_v._columns, Math.ceil( _this.outerWidth(true) / _v._colWidth )));
			
			// Index possible element positions.			/	inspired by jQuery.Masonry
			var _range = _v._columns - _span + 1;
			var _tempTop = [];
			
			// We only need as many top positions as the range
			// of possible left positions. So, _range instead of _columns.
			for (i=0; i<_range; i++) {
				_values = _v._currentTop.slice(i,i + _span);
				_tempTop.push(Math.max.apply(Math, _values));
			}
				
			// Score-based packing.
			score = 10E+8;
			for (i = _range;i-->0;) {
				s = _tempTop[i] + i * _v._colWidth * _o.cornerFactor;
				if (s <= score) {
					_idx = i;
					score = s;
				}
			}
			_top = _tempTop[_idx];
			/**
				// If snapHeight is defined, 'snap' to the next multiple of snapHeight
				// only for elements belonging to the snapClass selector.
				_top = _o.snapHeight === undefined || !_this.hasClass(_o.snapClass) ?
				Math.min.apply(Math, _tempTop) :
				Math.floor(Math.min.apply(Math, _tempTop)/_o.snapHeight + 0.9999)*_o.snapHeight;
				// The column closest to the left <=minY value.
				for (i = _v._columns;i-->0;) {if (_tempTop[i] <= _top) {_idx = i}}
				_v._left = _idx * _v._colWidth;
			}*/
			
			// Position the element.
			// The following was a cause for "Too Much Recursion(Firefox)/Rangeerror(Safari).
			// Apparently a jQuery 1.3.2 bug, workaround found at
			// http://bugs.jquery.com/ticket/3583 :
			// "options.old (set in $.speed for queueing purpose) seems to store
			// too much recursive call and freeze the browser when called more
			// than x times"
			var pos = {
					  left: _idx * _v._colWidth + _v._xPad + 'px',
					  top: _top + _v._yPad + 'px'
					  }
			if (_o._animate && !_this.hasClass('noanim')) {
				setTimeout(function() {
					_this.animate(pos, $.extend( true, {}, _o.animationOptions));
				}, _v.currentDelay);
				
				// Set new element delay value
				_v.currentDelay += _o.elementDelay;
			
			} else {
				_this.css(pos);
				if (--(_v._i) <= 0) _o.callbk();
			}
				
			// Increase values in the currentTop array depending on the
			// span of the current element(_span)
			_newH = _top + _this.outerHeight(true);
			for (i=_span;i-->0;) {_v._currentTop[_idx+i] = _newH}
			
		},
		
		// Main layout algorithm
		positionAll : function(_container, _o) {
			
			// Initialize variables object.
			var _v = {};
			
			//Get elements
			var _elements = !$.isEmptyObject(_o.contentToAppend) ? _o.contentToAppend :
								_o.elementClass === undefined ? _container.children() :
									_container.children(_o.elementClass);
			
			if (!_elements.length) return(false);
			
			// Set elements position to absolute. http://bit.ly/hpo7Nv
			_elements.css({position: 'absolute'});
			
			// Get the number of elements.
			_v._l = _elements.length;
			
			// Store the container's visibility properties.
			var _disp = _container.css('display');
			var _vis = _container.css('visibility');
			
			// Temporarily show the container...
			_container.css({ display: 'block', width: '', visibility: 'hidden' });
			
			// Calculate column width and number of columns
			_v._colWidth = _o.columnWidth === undefined ? Math.max(_elements.outerWidth(true),2) : Math.max(_o.columnWidth,2);
			_v._columns = Math.floor(_container.width() / _v._colWidth);
			
			// Get saved positions of elements if they exist and if we are appending
			// new content.
			_savedTop = _container.data("freetiletop");
			_v._currentTop = !$.isEmptyObject(_o.contentToAppend) && _savedTop ? _savedTop : [];
			
			// Compensate if _currentTop array is shorter than the number of columns.
			for (i = _v._columns-_v._currentTop.length;i-->0;) _v._currentTop.push(0);
		
			// Calculate container padding for correct element positioning
			_v._xPad = parseInt(_container.css("padding-left"), 10);
			_v._yPad = parseInt(_container.css("padding-top"), 10);
			
			// Initialize some variable in the variables holder
			_v._left = 0;
			_v._i = _elements.length;
			_v.currentDelay = 0;
			
			// Set Callback. (will be cleared on next run)
			_o.animationOptions.complete = function() { if (--(_v._i) <= 0) _o.callbk(); }
			
			// Position each element.
			_elements.each(function() {
				FreeTile.positionOne(_container, $(this), _o, _v);
			});
			
			// Define container-specific CSS and force width if forceWidth is true,
			// taking into account containerWidthStep if present, to specify a different
			// width-stepping than the width of the elements.
			// Also restore original position information.
			var _ccss = {display: _disp, visibility: _vis};
			
			// If container position is static make it relative to properly position elements.
			if (_container.css('position') == 'static') _ccss.position = 'relative';
			
			// If forceWidth is true, apply new width to the container.
			if (_o.forceWidth) _ccss.width = _o.containerWidthStep ?
			_o.containerWidthStep * (parseInt(_container.width() / _o.containerWidthStep)) :
			_v._columns * _v._colWidth;
			
			// Save current heights
			_container.data("freetiletop", _v._currentTop);
			
			// Apply initial CSS properties.
			_container.css(_ccss);
			
			// Re-use _ccss to apply height.
			_ccss = {height: Math.max.apply(Math, _v._currentTop)};
			
			// Apply or animate.
			if (_o._animate) _container.animate(_ccss, $.extend( true, {}, _o.animationOptions)); else _container.css(_ccss);
			
			// Mark elements as tiled.
			_elements.addClass("tiled");
			
			return _container;
		},
		
		// Defaults
		defaults : {
			elementClass : undefined,
			columnWidth : undefined,
			snapHeight : undefined,
			snapClass : undefined,
			animate : false,
			elementDelay : 0,
			containerResize : true,
			customEvents : undefined,
			persistentCallback : false,
			forceWidth : false,
			containerWidthStep : undefined,
			cornerFactor: 0.03
		},
		
		// Overriding options.
		reset : {
			animationOptions : { complete: function() {} },
			callbk : function() {},
			contentToAppend : {}
		}
	}
	
	//
	// Helper Methods
	// _________________________________________________________
	
	// $('img.photo',this).imagesLoaded(myFunction)
	// execute a callback when all images have loaded.
	// needed because .load() doesn't work on cached images
	
	// Modified with a two-pass approach to changing image
	// src. First, the proxy imagedata is set, which leads
	// to the first callback being triggered, which resets
	// imagedata to the original src, which fires the final,
	// user defined callback.
	
	// modified by yiannis chatzikonstantinou.
	
	// original:
	// mit license. paul irish. 2010.
	// webkit fix from Oren Solomianik. thx!
	
	// callback function is passed the last image to load
	//   as an argument, and the collection as `this`
  
	$.fn.imagesLoaded = function(callback){
		var elems = this.find('img'),
			elems_src = [],
			len = elems.length;
			
		if ( !elems.length ) {
			callback.call( this );
			return this;
		}
		
		elems.one('load error', function(){
			if ( --len == 0 ) {
				// Rinse and repeat.
				len = elems.length;
				elems.one('load error', function() {
					if ( --len == 0 ) {
						callback.call( elems, this );
					}
				}).each(function() {
					this.src = elems_src.shift();
				});
			}
		}).each(function(){
			elems_src.push( this.src );
			// webkit hack from http://groups.google.com/group/jquery-dev/browse_thread/thread/eee6ab7b2da50e1f
			// data uri bypasses webkit log warning (thx doug jones)
			this.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";
		});
		return this;
	};
	
	// Original version follows. It behaves better during page load and so it is used there.
	// Browser crappiness at it's fullest magnitude.
	
	$.fn.imagesLoaded_timeout = function(callback){
		var elems = this.find('img'),
			_this = this,
			timeOut = 50;
		
		if ( !elems.length ) {
			callback.call( this );
			return this;
		}
		
		this.data("imageloadtimeout", setTimeout(function() {callback.call(elems, _this) }, timeOut));
		
		elems.bind('load',function(){
			clearTimeout(_this.data("imageloadtimeout"));
			_this.data("imageloadtimeout", setTimeout(function() {callback.call(elems, _this) }, timeOut));
		});
	  
		return this;
	};

})( jQuery );
