/**
 * automated best-fit images
 *
 * requires:
 * - jquery.js
 * - window.js
 * - object.js
 */

jQuery(function ($) {
	(function () {
		var
			bestfit = window.bestfit = {},
			$window = $(window);





		function controllerDispatchBestFit() {
			return this.view.getSrcAttributes()
				.done($.proxy(this.model.updateSrcAttributes, this.model))
				.done($.proxy(this.dispatchSwitchImageCurrent, this));
		}

		function controllerDispatchRealignment(index, image) {
			this.view.adjustElementHeight();
			this.view.alignImage(index);
			return image.deferred;
		}

		function controllerDispatchSwitchImageCurrent() {
			this.model.getImageCurrent()
				.done($.proxy(this.view.switchImage, this.view));
		}

		function controllerHandleDelayedResize() {
			this.dispatchBestFit();
		}

		function controllerHandleResize() {
			this.model.getImageCurrent()
				.done($.proxy(this.dispatchRealignment, this));
		}

		function controllerInit($viewElement, $sheet) {
			if ("undefined" === typeof this.model) {
				throw TypeError("this.model is not defined! Subclasses of bestfit must create their model property before calling super.init.");
			}
			if ("undefined" === typeof this.view) {
				throw TypeError("this.view is not defined! Subclasses of bestfit must create their view property before calling super.init.");
			}

			this.view.init($viewElement, $sheet);
			// if we have no image, we don't do anything here
			if (1 > this.view.countImages()) {
				return;
			}

			this.model.init();
			this.model.setSrc(0, this.view.getSrc(0));
			this.model.getImage(0)
				.done($.proxy(this.dispatchRealignment, this))
				.done($.proxy(this.dispatchBestFit, this));

			$window.bind("resize", $.proxy(this.handleResize, this));
			$window.bind("delayed-resize", $.proxy(this.handleDelayedResize, this));
		}





		function modelGetImage(index) {
			var
				image = this.images[index],
				imageObj;

			if ("undefined" === typeof image.deferred) {
				image.deferred = $.Deferred();
				image.obj = {
					"src": image.src
				};
				imageObj = new Image();
				imageObj.onload = function () {
					image.obj.height = imageObj.height;
					image.obj.width = imageObj.width;
					image.deferred.resolve(index, image);
					imageObj = null;
				};
				imageObj.src = image.src;
			}

			return image.deferred;
		}

		function modelGetImageCurrent() {
			return this.getImage(this.current);
		}

		function modelGetImageNext() {
			return this.getImage(this.getIndexNext());
		}

		function modelGetIndexNext() {
			return (this.current + 1) % this.images.length;
		}

		function modelGetIndexPrevious() {
			return (this.current + this.images.length - 1) % this.images.length;
		}

		function modelGetSourceAttributes(dimensions) {
			var attributes = this.sourceAttributes[dimensions.width + "x" + dimensions.height];
			if ("undefined" === typeof attributes) {
				this.sourceAttributes[dimensions.width + "x" + dimensions.height] = attributes = {
					deferred: $.Deferred()
				};
				if ("function" !== typeof this.pathnameSourceAttributes) {
					throw TypeError("this.pathnameSourceAttributes is not a function! Subclasses of bestfit must implement a method pathnameSourceAttributes.");
				}
				$.getJSON(this.pathnameSourceAttributes(dimensions))
					.done(function (json) {
						attributes.obj = json;
						attributes.deferred.resolve(attributes);
					});
			}
			return attributes.deferred;
		}

		function modelInit() {
			this.current = 0;
			this.images = [];
			this.sourceAttributes = {};
		}

		function modelNext() {
			this.current = this.getIndexNext();
		}

		function modelSetSrc(index, src) {
			var image = this.images[index];

			if ("undefined" !== typeof image && src === image.src) {
				return;
			}

			if ("undefined" !== typeof image && "undefined" !== typeof image.obj) {
				delete image.obj.onload;
			}

			this.images[index] = {
				"src": src
			};
		}

		function modelUpdateSrcAttributes(sourceAttributes) {
			var
				i, l,
				attributes = sourceAttributes.obj;

			for (i = 0, l = attributes.length; i < l; i += 1) {
				this.setSrc(i, attributes[i]);
			}
		}





		function viewAdjustElementHeight() {
			// the constant 204 here is the sum of the heights of header/footer elements
			// TODO: externalise this constant into an option
			this.$element.height(this.$sheet.height() - 204);
		}

		function viewAlignImage(index) {
			var
				$imgElement = this.$imgElements.eq(index),
				containerDimensions = this.dimensions(),
				ratios = {
					"height": containerDimensions.height / $imgElement.height(),
					"width": containerDimensions.width / $imgElement.width()
				};

			$imgElement
				.removeClass("stretch-height stretch-width");
			if (ratios.height > ratios.width) {
				$imgElement.addClass("stretch-height");
				this.resetVerticalAlignment(index);
				this.centerImageHorizontally(index);
			} else {
				$imgElement.addClass("stretch-width");
				this.resetHorizontalAlignment(index);
				this.centerImageVertically(index);
			}
		}

		function viewCenterImageHorizontally(index) {
			var
				containerDimensions = this.dimensions(),
				$image = this.$imgElements.eq(index);
			$image.css("left", (containerDimensions.width - $image.width()) / 2);
		}

		function viewCenterImageVertically(index) {
			var
				containerDimensions = this.dimensions(),
				$image = this.$imgElements.eq(index);
			$image.css("top", (containerDimensions.height - $image.height()) / 2);
		}

		function viewCountImages() {
			return this.$imgElements.length;
		}

		function viewDimensions() {
			return {
				"height": this.$element.height(),
				"width": this.$element.width()
			};
		}

		function viewGetSrc(index) {
			return this.$imgElements.eq(index).attr("src");
		}

		function viewGetSrcAttributes() {
			var result = {
				deferred: $.Deferred(),
				obj: []
			};

			this.$imgElements.each(function (i, imgElement) {
				result.obj.push($(imgElement).attr("src"));
			});

			return result.deferred.resolve(result);
		}

		function viewInit($element, $sheet) {
			this.$element = $element.css("min-height", "0");
			this.$imgElements = $("img.stretch", $element);
			this.$sheet = $sheet;

			this.adjustElementHeight();
		}

		function viewResetHorizontalAlignment(index) {
			this.$imgElements.eq(index).css("left", "0");
		}

		function viewResetVerticalAlignment(index) {
			this.$imgElements.eq(index).css("top", "0");
		}

		function viewSwitchImage(index, image) {
			this.$imgElements.eq(index).attr("src", image.src);
		}





		bestfit.controller = {
			dispatchBestFit: controllerDispatchBestFit,
			dispatchSwitchImageCurrent: controllerDispatchSwitchImageCurrent,
			dispatchRealignment: controllerDispatchRealignment,
			handleDelayedResize: controllerHandleDelayedResize,
			handleResize: controllerHandleResize,
			init: controllerInit
		};

		bestfit.model = {
			getImage: modelGetImage,
			getImageCurrent: modelGetImageCurrent,
			getImageNext: modelGetImageNext,
			getIndexNext: modelGetIndexNext,
			getIndexPrevious: modelGetIndexPrevious,
			getSrcAttributes: modelGetSourceAttributes,
			init: modelInit,
			next: modelNext,
			setSrc: modelSetSrc,
			updateSrcAttributes: modelUpdateSrcAttributes
		};

		bestfit.view = {
			adjustElementHeight: viewAdjustElementHeight,
			alignImage: viewAlignImage,
			centerImageHorizontally: viewCenterImageHorizontally,
			centerImageVertically: viewCenterImageVertically,
			countImages: viewCountImages,
			dimensions: viewDimensions,
			getSrc: viewGetSrc,
			getSrcAttributes: viewGetSrcAttributes,
			init: viewInit,
			resetHorizontalAlignment: viewResetHorizontalAlignment,
			resetVerticalAlignment: viewResetVerticalAlignment,
			switchImage: viewSwitchImage
		};
	} ());
});
