/*
    Created: 09 May 2011
    Author: willem odendaal (willem@newmedialabs.co.za)
    Description: 
        The easySquashedGridLayout jquery plugin lays out all children in a container in a better way than standard HTML floats. It
        makes better use of whitespace. It animates the shuffling of panels (by default, but animation can be turned off). It can be called
        repeatedly if necessary to refresh the layout (i.e. when the size of the container changes).

        It requires that :
            1. The container have "position" == "relative".
            2. Children of the container have explicit width set.

        Usage:
            $("#containerName").easySquashedGridLayout( { animate : true, margin: 5 } );

		Important: 
			If the children of the container contain Images, then you need to wait until all the images have finished loading
			before calling easySquashedGridLayout. For example, jquery window.load gets fired only after everything (including 
			images) have been loaded:

			$(window).load(function () {
				$("#applications-container").easySquashedGridLayout();
			});


        Note: that CSS Margins on the child elements are *ignored* as the margin option/setting above is used instead.


*/

(function ($) {
	$.fn.easySquashedGridLayout = function (options) {

		var settings = {
			animate: true,
			margin: 5 //This margin is simpler to work with than the CSS margin.
		};

		if (options) {
			$.extend(settings, options);
		}

		var layoutViewModel = getLayoutViewModel(this);
		translateLayoutViewModel(layoutViewModel);
		applyLayoutViewModel(layoutViewModel, this);


		/* Classes */
		function Elm(w, h, $element) {

			this.element = $element;
			this.width = w;
			this.height = h;
			this.x = -1;
			this.y = -1;

			this.outerWidth = function () {
				return this.width + (settings.margin * 2);
			};

			this.outerHeight = function () {
				return this.height + (settings.margin * 2);
			};

			this.topPlusMargin = function () {
				return this.y + settings.margin;
			};

			this.leftPlusMargin = function () {
				return this.x + settings.margin;
			};
		}


		function Container(w, elmts) {
			return { width: w, height: 0, elements: elmts };
		}


		function Point(xx, yy) {
			return { x: xx, y: yy };
		}


		/* Functions */
		function getLayoutViewModel($container) {
			//Returns view model that represents the items in the container (but only 
			//	width and height, not position. Position will be set in the 'translate' method)
			var children = $container.children();
			var elements = [];
			children.each(function () {
				var w = $(this).outerWidth();
				var h = $(this).outerHeight();
				var display = $(this).css('display');

				if (w > 0 && h > 0 && display == 'block') {
					var elm = new Elm(w, h, $(this));
					elements.push(elm);
				}
			});

			var vm = new Container($container.width(), elements);
			return vm;
		}


		function translateLayoutViewModel(layoutViewModel) {
			//Calculate X and Y for the viewModel elements.
			var containerWidth = layoutViewModel.width;
			var translatedElements = [];

			$(layoutViewModel.elements).each(function (i, elm) {
				var $translatedElements = $(translatedElements);
				var potentialTopLeftPoints = getPotentialTopLeftPoints($translatedElements, containerWidth);
				var point = getTopMostLeftMostPointForElement(potentialTopLeftPoints, $translatedElements, containerWidth, elm);

				elm.x = point.x;
				elm.y = point.y;
				translatedElements.push(elm);
			});

			layoutViewModel.height = getMaxYPos(layoutViewModel.elements);
		}


		function getPotentialTopLeftPoints($translatedElements, containerWidth) {
			//Scan the Top-Right, Bottom-Right, Bottom-Left corners of each element and hit test.
			//	Return valid points that don't clip other elements or the container border.
			var potentialTopLeftPoints = [];

			$translatedElements.each(function (i, elm) {
				var bottom = elm.y + elm.outerHeight();
				var right = elm.x + elm.outerWidth();
				var topRight = new Point(right + 1, elm.y);
				var bottomRight = new Point(right + 1, bottom + 1);
				var bottomLeft = new Point(elm.x, bottom + 1);

				if (!hitTest(topRight, $translatedElements, containerWidth))
					potentialTopLeftPoints.push(topRight);

				if (!hitTest(bottomLeft, $translatedElements, containerWidth))
					potentialTopLeftPoints.push(bottomLeft);

				if (!hitTest(bottomRight, $translatedElements, containerWidth))
					potentialTopLeftPoints.push(bottomRight);

			});

			//Sort points so Y is most important, then X.
			potentialTopLeftPoints = potentialTopLeftPoints.sort(sortPoint);

			return potentialTopLeftPoints;
		}


		function sortPoint(pointA, pointB) {
			return (pointA.y * 3 + pointA.x) - (pointB.y * 3 + pointB.x);
		}


		function getTopMostLeftMostPointForElement(potentialTopLeftPoints, $translatedElements, containerWidth, element) {
			//See if element can be placed in each possible potentialTopLeftPoints by hit testing all corners of the element.
			//	Return the first matching point. (topLeft does not have to be tested as it's already valid at this point)
			var validLocations = [];

			var i = 0;
			for (i = 0; i < potentialTopLeftPoints.length; i++) {
				var point = potentialTopLeftPoints[i];

				var bottom = point.y + element.outerHeight();
				var right = point.x + element.outerWidth();

				var topRight = new Point(right, point.y);
				var bottomRight = new Point(right, bottom);
				var bottomLeft = new Point(point.x, bottom);

				if (!hitTest(topRight, $translatedElements, containerWidth)
				&& !hitTest(bottomRight, $translatedElements, containerWidth)
				&& !hitTest(bottomLeft, $translatedElements, containerWidth)) {
					validLocations.push(point);
				}

			}

			var topMostPoint = getTopMostPoint(validLocations);

			if (!topMostPoint) {
				//No valid points from collection, add to the bottom (underneath everything else).
				var maxY = getMaxYPos($translatedElements);
				return new Point(0, maxY + settings.margin);
			}
			else {
				return topMostPoint;
			}

		}


		function getTopMostPoint(points) {
			if (!points)
				return null;

			if (points.length == 0)
				return null;

			if (points.length == 1)
				return points[0];

			var topMost = points[0];

			for (var i = 0; i < points.length; i++) {
				var p = points[i];
				if (p.y < topMost.y)
					topMost = p;
			}

			return topMost;
		}


		function hitTest(point, $translatedElements, containerWidth) {
			//Returns true if the point(x,y) is within any of the bounds of the translatedElements, or if it
			//	is >= the containerWidth. ContainerHeight is ignored because the container is allowed to
			//	extend its height automatically.
			if (point.x >= containerWidth) {
				return true;
			}

			for (var i = 0; i < $translatedElements.length; i++) {
				var elm = $translatedElements[i];

				if ((point.x >= elm.x && point.x <= elm.x + elm.outerWidth())
				&& (point.y >= elm.y && point.y <= elm.y + elm.outerHeight())) {
					return true;
				}

			}

			return false;
		}


		function ensureAllElementsAreAbsolutelyPositioned($elements) {
			//Even if elements are positioned relatively, switch to absolute but maintain the same
			//	position (that's why we need to iterate backwards).
			var reversedElements = layoutViewModel.elements.reverse();

			$(reversedElements).each(function (i, elm) {
				var $elmX = elm.element.position().left + (settings.margin * 2);
				var $elmY = elm.element.position().top  + (settings.margin * 2);
				elm.element.css('left', $elmX);
				elm.element.css('top', $elmY);
				elm.element.css('position', 'absolute');
			});
		}

		function applyLayoutViewModel(layoutViewModel, $container) {
			//Given the viewModel, apply the changes to the actual elements in
			//	the container (possibly using animation, so more than just a numeric
			//	change).
			$container.css('padding', '0px');
			$container.height(layoutViewModel.height + (settings.margin * 2));
			$container.children().css('margin', '0px');
			ensureAllElementsAreAbsolutelyPositioned($(layoutViewModel.elements));

			var animationDuration = 500;
			if (!settings.animate)
				animationDuration = 0;

			$(layoutViewModel.elements).each(function (i, elm) {
				elm.element.animate(
				{
					top: elm.topPlusMargin(),
					left: elm.leftPlusMargin()
				}, animationDuration);
			});

		}


		function getMaxYPos(elementsArray) {
			var maxY = 0;

			$(elementsArray).each(function (i, elm) {
				if (elm.y > -1 && elm.x > -1) {
					if (elm.y + elm.height > maxY)
						maxY = elm.y + elm.height;
				}
			});

			return maxY;
		}



	};
})(jQuery);
