jQuery(function ($) {
	// initialize filter menu
	var
		$filterMenu = $("#select"),
		$fieldsets = $("fieldset", $filterMenu),
		$productsContainer = $(".products-container"),
		$productsList = $("ul.products", $productsContainer),
		$products = $("> li", $productsList),
		$variants = $("ul.variants > li", $products),
		repositionProductList,
		selector,
		parameter,
		products,
		product,
		variants,
		variant;

	$("input[type=radio]", $filterMenu).attr("checked", true);
	$("input[type=checkbox]", $filterMenu).attr("checked", false);

	if(!$filterMenu.length && !$products.length) return;


	// add utility function filter to jQuery namespace
	(function () {
		function arrayFilter(test, array) {
			if ("function" === typeof array.filter) {
				return array.filter(test);
			}

			var
				result = [],
				i, l, el;

			for (i = 0, l = array.length; i < l; i += 1) {
				el = array[i];
				if (test(el)) {
					result.push(el);
				}
			}

			return result;
		}

		$.extend({
			"arrayFilter": arrayFilter
		});
	}());

	// add utility function diff to jQuery namespace
	(function () {
		function arrayDiff(a, b) {
			return $.arrayFilter(function (el) { return $.inArray(el, b) == -1; }, a);
		}

		$.extend({
			"diff": arrayDiff
		});
	}());


	// define selector class
	(function () {
		function controllerHandleProductsUpdated(e, source) {
			this.model.disableEmptyOptions(source);
		}

		function controllerInit($view) {
			this.model = Object.create(selector.model);
			this.view = Object.create(selector.view);

			this.view.init($view);
			this.model.init(this.view.getElementsParameters());
		}



		function modelDisableEmptyOptions(products) {
			var
				parameters = this.getParameters(),
				i, l;

			for (i = 0, l = parameters.length; i < l; i += 1) {
				parameters[i].disableEmptyOptions(products);
			}
		}

		function modelGetParameters() {
			return this.parameters;
		}

		function modelInit($viewsParameters) {
			var
				parameters = [],
				parameterController;

			$viewsParameters.each(function (i, viewParameter) {
				parameterController = Object.create(parameter.controller);
				parameterController.init($(viewParameter));
				parameters.push(parameterController);
			});
			this.setParameters(parameters);
		}

		function modelSetParameters(parameters) {
			this.parameters = parameters;
		}



		function viewGetElementsParameters() {
			return this.$elementsParameters;
		}

		function viewInit($baseElement) {
			var $element = $("#select", $baseElement);
			this.setBaseElement($baseElement)
			this.setElement($element);
			this.setElementsParameters($("fieldset", $element));
		}

		function viewSetBaseElement($baseElement) {
			this.$baseElement = $baseElement;
		}

		function viewSetElement($element) {
			this.$element = $element;
		}

		function viewSetElementsParameters($elementsParameters) {
			this.$elementsParameters = $elementsParameters;
		}



		selector = {};
		selector.controller = {
			handleProductsUpdated: controllerHandleProductsUpdated,
			init: controllerInit
		};
		selector.model = {
			disableEmptyOptions: modelDisableEmptyOptions,
			getParameters: modelGetParameters,
			init: modelInit,
			setParameters: modelSetParameters
		};
		selector.view = {
			getElementsParameters: viewGetElementsParameters,
			init: viewInit,
			setBaseElement: viewSetBaseElement,
			setElement: viewSetElement,
			setElementsParameters: viewSetElementsParameters
		};
	}());



	// define parameter class
	(function () {
		function controllerDisableEmptyOptions(products) {
			this.view.disableEmptyOptions(this.getId(), this.getOptionIds(), products);
		}

		function controllerGetConstraints() {
			return this.model.getConstraints();
		}

		function controllerGetId() {
			return this.model.getId();
		}

		function controllerGetOptionIds() {
			return this.model.getOptionIds();
		}

		function controllerHandleClickAll(e) {
			var
				changes = {
					"added": [],
					"removed": this.model.getConstraints()
				};

			this.view.deselectAllOptions();
			this.model.clearConstraints();

			this.triggerConstraintsChanged(changes);
		}

		function controllerHandleClickOption(e) {
			var
				after = this.view.getConstraints(),
				before = this.model.getConstraints(),
				changes = {
					"added": $.diff(after, before),
					"removed": $.diff(before, after)
				};

			this.model.setConstraints(after);
			if (this.model.isConstrained()) {
				this.view.deselectOptionAll();
			} else {
				this.view.selectOptionAll();
			}
			this.triggerConstraintsChanged(changes);
		}

		function controllerInit($view) {
			this.model = Object.create(parameter.model);
			this.view = Object.create(parameter.view);

			this.view.init($view);
			this.model.init(this.view.getParameterId(), this.view.getOptionIds());

			this.view.addClickListenerAll($.proxy(this.handleClickAll, this));
			this.view.addClickListenerOptions($.proxy(this.handleClickOption, this));
		}

		function controllerTriggerConstraintsChanged(changes) {
			this.view.triggerConstraintsChanged(this, changes);
		}



		function modelClearConstraints() {
			this.setConstraints([]);
		}

		function modelGetConstraints() {
			return this.constraints;
		}

		function modelGetId() {
			return this.id;
		}

		function modelGetOptionIds() {
			return this.optionIds;
		}

		function modelInit(id, optionIds) {
			this.setId(id);
			this.setOptionIds(optionIds);
			this.clearConstraints();
		}

		function modelIsConstrained() {
			return !!this.getConstraints().length;
		}

		function modelSetConstraints(constraints) {
			this.constraints = constraints;
		}

		function modelSetId(id) {
			this.id = id;
		}

		function modelSetOptionIds(optionIds) {
			this.optionIds = optionIds;
		}



		function viewAddClickListenerAll(listener) {
			this.getElementAll().bind("click", listener);
		}

		function viewAddClickListenerOptions(listener) {
			this.getElementsOptions().bind("click", listener);
		}

		function viewDeselectAllOptions() {
			this.getElementsOptions().attr("checked", false);
		}

		function viewDeselectOptionAll() {
			this.getElementAll().attr("checked", false);
		}

		function viewDisableEmptyOptions(parameterId, optionIds, products) {
			var optionId, i, l;

			for (i = 0, l = optionIds.length; i < l; i += 1) {
				optionId = optionIds[i];
				if (products.hasPassedWithOption(parameterId, optionId)) {
					this.enableOption($("#option_" + parameterId + "_" + optionId));
				} else {
					this.disableOption($("#option_" + parameterId + "_" + optionId));
				}
			}
		}

		function viewDisableInvalidOptions() {
			var
				result = [],
				snuff = "option_" + this.getParameterId() + "_",
				$optionElement,
				disableOption = this.disableOption;

			this.getElementsOptions().each(function(i, optionElement) {
				$optionElement = $(optionElement);
				if (-1 === parseInt($optionElement.attr("id").replace(snuff, ""), 10)) {
					disableOption($optionElement);
				}
			});

			return result;
		}

		function viewDisableOption($option) {
			$option
				.addClass("disabled")
				.attr("disabled", true)
				.find("+ label")
					.addClass("disabled");
		}

		function viewEnableOption($option) {
			$option
				.removeClass("disabled")
				.attr("disabled", false)
				.find("+ label")
					.removeClass("disabled");
		}

		function viewGetConstraints() {
			var
				result = [],
				snuff = "option_" + this.getParameterId() + "_";

			this.getElementsOptions().each(function(i, optionElement) {
				var $optionElement = $(optionElement);

				if ($optionElement.attr("checked")) {
					result.push(parseInt($optionElement.attr("id").replace(snuff, ""), 10));
				}
			});

			return result;
		}

		function viewGetElement() {
			return this.$element;
		}

		function viewGetElementAll() {
			return this.$elementAll;
		}

		function viewGetElementsOptions() {
			return this.$elementsOptions;
		}

		function viewGetOptionIds() {
			var
				result = [],
				snuff = "option_" + this.getParameterId() + "_",
				id;

			this.getElementsOptions().each(function(i, optionElement) {
				id = parseInt($(optionElement).attr("id").replace(snuff, ""), 10);
				if (-1 !== id) {
					result.push(id);
				}
			});

			return result;
		}

		function viewGetParameterId() {
			return parseInt(this.getElementAll().attr("id").replace("parameter_", ""), 10);
		}

		function viewInit($view) {
			this.setElement($view);
			this.setElementAll($(":radio", $view));
			this.setElementsOptions($(":checkbox", $view));
			this.disableInvalidOptions();
		}

		function viewSelectOptionAll() {
			this.getElementAll().attr("checked", true);
		}

		function viewSetElement($element) {
			this.$element = $element;
		}

		function viewSetElementAll($elementAll) {
			this.$elementAll = $elementAll;
		}

		function viewSetElementsOptions($elementsOptions) {
			this.$elementsOptions = $elementsOptions;
		}

		function viewTriggerConstraintsChanged(source, changes) {
			this.getElement().trigger("constraints-changed", [source, changes]);
		}



		parameter = {};
		parameter.controller = {
			disableEmptyOptions: controllerDisableEmptyOptions,
			getConstraints: controllerGetConstraints,
			getId: controllerGetId,
			getOptionIds: controllerGetOptionIds,
			handleClickAll: controllerHandleClickAll,
			handleClickOption: controllerHandleClickOption,
			init: controllerInit,
			triggerConstraintsChanged: controllerTriggerConstraintsChanged
		};
		parameter.model = {
			clearConstraints: modelClearConstraints,
			getConstraints: modelGetConstraints,
			getId: modelGetId,
			getOptionIds: modelGetOptionIds,
			init: modelInit,
			isConstrained: modelIsConstrained,
			setConstraints: modelSetConstraints,
			setId: modelSetId,
			setOptionIds: modelSetOptionIds
		};
		parameter.view = {
			addClickListenerAll: viewAddClickListenerAll,
			addClickListenerOptions: viewAddClickListenerOptions,
			deselectAllOptions: viewDeselectAllOptions,
			deselectOptionAll: viewDeselectOptionAll,
			disableEmptyOptions: viewDisableEmptyOptions,
			disableInvalidOptions: viewDisableInvalidOptions,
			disableOption: viewDisableOption,
			enableOption: viewEnableOption,
			getConstraints: viewGetConstraints,
			getElement: viewGetElement,
			getElementAll: viewGetElementAll,
			getElementsOptions: viewGetElementsOptions,
			getOptionIds: viewGetOptionIds,
			getParameterId: viewGetParameterId,
			init: viewInit,
			selectOptionAll: viewSelectOptionAll,
			setElement: viewSetElement,
			setElementAll: viewSetElementAll,
			setElementsOptions: viewSetElementsOptions,
			triggerConstraintsChanged: viewTriggerConstraintsChanged
		};
	}());


	// define products class
	(function () {
		function controllerHandleConstraintsChanged(e, source, changes) {
			var
				added = changes.added,
				removed = changes.removed,
				parameterId = source.getId(),
				model = this.model;

			if (!!removed.length) {
				model.unapplyConstraints(parameterId, removed);
			}

			if (!!added.length) {
				model.applyConstraints(parameterId, added);
			}

			model.updateProductsView();
			this.view.update(model.countPassedVariants());

			this.triggerProductsUpdated();
		}

		function controllerHandleDetailsClicked(e, sourceProduct) {
			sourceProduct.embellishProductLinkWithPassedVariantsQuery();
		}

		function controllerHasPassedWithOption(parameterId, optionId) {
			return this.model.hasPassedWithOption(parameterId, optionId);
		}

		function controllerInit($baseElement) {
			this.model = Object.create(products.model);
			this.view = Object.create(products.view);

			this.view.init($baseElement);
			this.model.init(this.view.getElementsProducts());
		}

		function controllerTriggerProductsUpdated() {
			this.view.triggerProductsUpdated(this);
		}



		function modelApplyConstraints(parameterId, optionIds) {
			var
				products = this.getProducts(),
				i, l;

			for (i = 0, l = products.length; i < l; i += 1) {
				products[i].applyConstraints(parameterId, optionIds);
			}
		}

		function modelCountPassedVariants() {
			var
				products = this.getProducts(),
				result = 0,
				i, l;

			for (i = 0, l = products.length; i < l; i += 1) {
				result += products[i].countPassedVariants();
			}

			return result;
		}

		function modelGetProducts() {
			return this.products;
		}

		function modelHasPassedWithOption(parameterId, optionId) {
			var
				products = this.getProducts(),
				i, l;

			for (i = 0, l = products.length; i < l; i += 1) {
				if (products[i].hasPassedWithOption(parameterId, optionId)) {
					return true;
				}
			}

			return false;
		}

		function modelInit($viewsProducts) {
			var
				products = [],
				productController;

			$viewsProducts.each(function (i, viewProduct) {
				productController = Object.create(product.controller);
				productController.init($(viewProduct));
				products.push(productController);
			});

			this.setProducts(products);
		}

		function modelSetProducts(products) {
			this.products = products;
		}

		function modelUnapplyConstraints(parameterId, optionIds) {
			var
				products = this.getProducts(),
				i, l;

			for (i = 0, l = products.length; i < l; i += 1) {
				products[i].unapplyConstraints(parameterId, optionIds);
			}
		}

		function modelUpdateProductsView() {
			var
				products = this.getProducts(),
				i, l;

			for (i = 0, l = products.length; i < l; i += 1) {
				products[i].updateView();
			}
		}



		function viewGetElement() {
			return this.$element;
		}

		function viewGetElementsProducts() {
			return this.$elementsProducts;
		}

		function viewInit($baseElement) {
			var $element = $("ul.products", $baseElement);
			this.setBaseElement($baseElement);
			this.setElement($element);
			this.setElementsProducts($("> li", $element));

			this.$passedOverallVariantsElement = $("#passed-overall-variants");
		}

		function viewSetBaseElement($baseElement) {
			this.$baseElement = $baseElement;
		}

		function viewSetElement($element) {
			this.$element = $element;
		}

		function viewSetElementsProducts($elementsProducts) {
			this.$elementsProducts = $elementsProducts;
		}

		function viewTriggerDetailsClicked(source) {
			this.getElement.trigger("details-clicked", [source]);
		}

		function viewTriggerProductsUpdated(source) {
			this.getElement().trigger("products-updated", [source]);
		}

		function viewUpdate(countPassedVariants) {
			this.$passedOverallVariantsElement.text(countPassedVariants);
		}



		products = {};
		products.controller = {
			handleConstraintsChanged: controllerHandleConstraintsChanged,
			handleDetailsClicked: controllerHandleDetailsClicked,
			hasPassedWithOption: controllerHasPassedWithOption,
			init: controllerInit,
			triggerProductsUpdated: controllerTriggerProductsUpdated
		};
		products.model = {
			applyConstraints: modelApplyConstraints,
			countPassedVariants: modelCountPassedVariants,
			getProducts: modelGetProducts,
			hasPassedWithOption: modelHasPassedWithOption,
			init: modelInit,
			setProducts: modelSetProducts,
			unapplyConstraints: modelUnapplyConstraints,
			updateProductsView: modelUpdateProductsView
		};
		products.view = {
			getElement: viewGetElement,
			getElementsProducts: viewGetElementsProducts,
			init: viewInit,
			setBaseElement: viewSetBaseElement,
			setElement: viewSetElement,
			setElementsProducts: viewSetElementsProducts,
			triggerProductsUpdated: viewTriggerProductsUpdated,
			update: viewUpdate
		};
	}());



	// define product class
	(function () {
		function controllerApplyConstraints(parameterId, optionIds) {
			this.model.applyConstraints(parameterId, optionIds);
		}

		function controllerCountPassedVariants() {
			return this.model.countPassedVariants();
		}

		function controllerDelegateDetailsClicked() {
			var $element = this.getViewElement();

			function dispatchDetailClicked(e) {
				$element.trigger("details-clicked", [this]);
			}

			this.view.addDispatcherDetailsClicked($.proxy(dispatchDetailClicked, this));
		}

		function controllerEmbellishProductLinkWithPassedVariantsQuery() {
			this.view.embellishProductLink(this.model.getPassedVariantsQuery());
		}

		function controllerGetPassedVariantsQuery() {
			return this.model.getPassedVariantsQuery();
		}

		function controllerGetViewElement() {
			return this.view.getElement();
		}

		function controllerHasPassedWithOption(parameterId, optionId) {
			return this.model.hasPassedWithOption(parameterId, optionId);
		}

		function controllerInit($element) {
			this.model = Object.create(product.model);
			this.view = Object.create(product.view);

			this.view.init($element);
			this.model.init(this.view.getParameters(), this.view.getElementVariants());

			this.delegateDetailsClicked();
		}

		function controllerUnapplyConstraints(parameterId, optionIds) {
			this.model.unapplyConstraints(parameterId, optionIds);
		}

		function controllerUpdateView() {
			var count = this.countPassedVariants();
			this.view.update(count);
		}



		function modelAddFailedConstraint(constraint) {
			this.getFailedConstraints().push(constraint);
		}

		function modelAddPassedConstraint(constraint) {
			this.getPassedConstraints().push(constraint);
		}

		function modelApplyConstraints(parameterId, optionIds) {
			var
				constraint = {
					"parameterId": parameterId
				},
				optionId,
				i, l,
				variants = this.getVariants();

			for (i = 0, l = optionIds.length; i < l; i += 1) {
				optionId = optionIds[i];
				constraint.optionId = optionId;
				if (this.hasOption(parameterId, optionId)) {
					this.addPassedConstraint(constraint);
				} else {
					this.addFailedConstraint(constraint);
					variants.applyConstraint(constraint);
				}
			}
		}

		function modelCountPassedVariants() {
			return this.getVariants().countPassedVariants();
		}

		function modelGetFailedConstraints() {
			return this.failedConstraints;
		}

		function modelGetParameters() {
			return this.parameters;
		}

		function modelGetPassedVariants() {
			return this.getVariants().getPassedVariants();
		}

		function modelGetPassedVariantsQuery() {
			var
				result = [],
				variants = this.getPassedVariants(),
				i, l;

			if (variants.length === this.getVariants().count()) {
				return "";
			}

			for (i = 0, l = variants.length; i < l; i += 1) {
				result.push("v[]=" + variants[i].getId());
			}
			result = result.join("&");

			return result;
		}

		function modelGetPassedConstraints() {
			return this.passedConstraints;
		}

		function modelGetVariants() {
			return this.variants;
		}

		function modelHasFailedConstraints() {
			return !!this.getFailedConstraints().length;
		}

		function modelHasOption(parameterId, optionId) {
			var parameters = this.getParameters();
			return !!parameters[parameterId] && -1 !== $.inArray(optionId, parameters[parameterId]);
		}

		function modelHasPassedConstraints() {
			return !!this.getPassedConstraints().length;
		}

		function modelHasPassedVariantWithOption(parameterId, optionId) {
			return this.getVariants().hasPassedWithOption(parameterId, optionId);
		}

		function modelHasPassedWithOption(parameterId, optionId) {
			return (this.hasOption(parameterId, optionId) && !!this.countPassedVariants()) || this.hasPassedVariantWithOption(parameterId, optionId);
		}

		function modelHelperRemoveConstraint(constraint, array) {
			var
				parameterId = constraint.parameterId,
				optionId = constraint.optionId,
				cmp,
				removeIndex = -1,
				i, l;

			for (i = 0, l = array.length; i < l; i += 1) {
				cmp = array[i];
				if (parameterId === cmp.parameterId && optionId === cmp.optionId) {
					removeIndex = i;
					break;
				}
			}

			if (-1 === removeIndex) {
				return;
			}

			array.splice(removeIndex, 1);
		}

		function modelInit(parameters, $elementVariants) {
			var variantsController = Object.create(variants.controller);
			variantsController.init($elementVariants);

			this.setParameters(parameters);
			this.setFailedConstraints([]);
			this.setPassedConstraints([]);
			this.setVariants(variantsController);
		}

		function modelRemoveFailedConstraint(constraint) {
			modelHelperRemoveConstraint(constraint, this.getFailedConstraints());
		}

		function modelRemovePassedConstraint(constraint) {
			modelHelperRemoveConstraint(constraint, this.getPassedConstraints());
		}

		function modelSetFailedConstraints(constraints) {
			this.failedConstraints = constraints;
		}

		function modelSetParameters(parameters) {
			this.parameters = parameters;
		}

		function modelSetPassedConstraints(constraints) {
			this.passedConstraints = constraints;
		}

		function modelSetVariants(variants) {
			this.variants = variants;
		}

		function modelUnapplyConstraints(parameterId, optionIds) {
			var
				constraint = {
					"parameterId": parameterId
				},
				optionId,
				i, l,
				variants = this.getVariants();

			for (i = 0, l = optionIds.length; i < l; i += 1) {
				optionId = optionIds[i];
				constraint.optionId = optionId;
				this.removeFailedConstraint(constraint);
				this.removePassedConstraint(constraint);
				variants.unapplyConstraint(constraint);
			}
		}



		function viewAddDispatcherDetailsClicked(dispatcher) {
			this.getElement().delegate("a.detail-form", "click", dispatcher);
		}

		function viewEmbellishProductLink(query) {
			if (!query) {
				return;
			}
			this.getElementProductLink().attr("href", function (i, href) { return href.replace(/\?.*/, "") + "?" + query; });
		}

		function viewGetElement() {
			return this.$element;
		}

		function viewGetElementCount() {
			return this.$elementCount;
		}

		function viewGetElementProductLink() {
			return this.$elementProductLink;
		}

		function viewGetElementVariants() {
			return this.$elementVariants;
		}

		function viewGetParameters() {
			var
				parameters = {},
				classes = this.getElement().attr("class").split(" "),
				optionRegExp = /option_([0-9]+)_([0-9]+)/,
				c, i, l,
				parameterId,
				optionId;
			optionRegExp.compile(optionRegExp);

			for (var i = 0, l = classes.length; i < l; i += 1) {
				if (!(c = classes[i])) {
					continue;
				}

				if (!(matches = optionRegExp.exec(c))) {
					continue
				};

				parameterId = matches[1];
				if ("undefined" === typeof parameters[parameterId]) {
					parameters[parameterId] = [parseInt(matches[2], 10)];
				} else {
					parameters[parameterId].push(parseInt(matches[2], 10));
				}
			}

			return parameters;
		}

		function viewInit($element) {
			var $passedVariants = $(".passed-variants", $element);
			this.setElement($element);
			this.setElementCount($passedVariants);
			this.setElementProductLink($(".detail-form", $element));
			this.setElementVariants($passedVariants);
		}

		function viewSetElement($element) {
			this.$element = $element;
		}

		function viewSetElementCount($elementCount) {
			this.$elementCount = $elementCount;
		}

		function viewSetElementProductLink($elementProductLink) {
			this.$elementProductLink = $elementProductLink;
		}

		function viewSetElementVariants($elementVariants) {
			this.$elementVariants = $elementVariants;
		}

		function viewUpdate(count) {
			this.getElementCount().text(count);
			if (!!count) {
				this.getElement().show();
			} else {
				this.getElement().hide();
			}
		}



		product = {};
		product.controller = {
			applyConstraints: controllerApplyConstraints,
			countPassedVariants: controllerCountPassedVariants,
			delegateDetailsClicked: controllerDelegateDetailsClicked,
			embellishProductLinkWithPassedVariantsQuery: controllerEmbellishProductLinkWithPassedVariantsQuery,
			getPassedVariantsQuery: controllerGetPassedVariantsQuery,
			getViewElement: controllerGetViewElement,
			hasPassedWithOption: controllerHasPassedWithOption,
			init: controllerInit,
			unapplyConstraints: controllerUnapplyConstraints,
			updateView: controllerUpdateView
		};
		product.model = {
			addFailedConstraint: modelAddFailedConstraint,
			addPassedConstraint: modelAddPassedConstraint,
			applyConstraints: modelApplyConstraints,
			countPassedVariants: modelCountPassedVariants,
			getFailedConstraints: modelGetFailedConstraints,
			getParameters: modelGetParameters,
			getPassedConstraints: modelGetPassedConstraints,
			getPassedVariants: modelGetPassedVariants,
			getPassedVariantsQuery: modelGetPassedVariantsQuery,
			getVariants: modelGetVariants,
			hasOption: modelHasOption,
			hasPassedVariantWithOption: modelHasPassedVariantWithOption,
			hasPassedWithOption: modelHasPassedWithOption,
			init: modelInit,
			removeFailedConstraint: modelRemoveFailedConstraint,
			removePassedConstraint: modelRemovePassedConstraint,
			setFailedConstraints: modelSetFailedConstraints,
			setParameters: modelSetParameters,
			setPassedConstraints: modelSetPassedConstraints,
			setVariants: modelSetVariants,
			unapplyConstraints: modelUnapplyConstraints
		};
		product.view = {
			addDispatcherDetailsClicked: viewAddDispatcherDetailsClicked,
			embellishProductLink: viewEmbellishProductLink,
			getElement: viewGetElement,
			getElementCount: viewGetElementCount,
			getElementProductLink: viewGetElementProductLink,
			getElementVariants: viewGetElementVariants,
			getParameters: viewGetParameters,
			init: viewInit,
			setElement: viewSetElement,
			setElementCount: viewSetElementCount,
			setElementProductLink: viewSetElementProductLink,
			setElementVariants: viewSetElementVariants,
			update: viewUpdate
		};
	}());



	// define variants class
	(function () {
		function controllerApplyConstraint(constraint) {
			this.applyConstraints(constraint.parameterId, [constraint.optionId]);
		}

		function controllerApplyConstraints(parameterId, optionIds) {
			this.model.applyConstraints(parameterId, optionIds);
		}

		function controllerCount() {
			return this.model.count();
		}

		function controllerCountPassedVariants() {
			return this.model.countPassedVariants();
		}

		function controllerGetPassedVariants() {
			return this.model.getPassedVariants();
		}

		function controllerHasPassedWithOption(parameterId, optionId) {
			return this.model.hasPassedWithOption(parameterId, optionId);
		}

		function controllerInit($element) {
			this.model = Object.create(variants.model);
			this.view = Object.create(variants.view);

			this.view.init($element);
			this.model.init(this.view.getElementVariants());
		}

		function controllerUnapplyConstraint(constraint) {
			this.unapplyConstraints(constraint.parameterId, [constraint.optionId]);
		}

		function controllerUnapplyConstraints(parameterId, optionIds) {
			this.model.unapplyConstraints(parameterId, optionIds);
		}



		function modelApplyConstraints(parameterId, optionIds) {
			var
				variants = this.getVariants(),
				i, l;

			for (i = 0, l = variants.length; i < l; i += 1) {
				variants[i].applyConstraints(parameterId, optionIds);
			}
		}

		function modelCount() {
			return this.getVariants().length;
		}

		function modelCountPassedVariants() {
			var
				count = 0,
				variants = this.getVariants(),
				i, l;

			for (i = 0, l = variants.length; i < l; i += 1) {
				if (!variants[i].hasFailedConstraints()) {
					count += 1;
				}
			}

			return count;
		}

		function modelGetPassedVariants() {
			var
				result = [],
				variant,
				variants = this.getVariants(),
				i, l;

			for (i = 0, l = variants.length; i < l; i += 1) {
				variant = variants[i];
				if (!variant.hasFailedConstraints()) {
					result.push(variant);
				}
			}

			return result;
		}

		function modelGetVariants() {
			return this.variants;
		}

		function modelHasPassedWithOption(parameterId, optionId) {
			var
				variant,
				variants = this.getVariants(),
				i, l;

			for (i = 0, l = variants.length; i < l; i += 1) {
				variant = variants[i];
				if (!variant.hasFailedConstraints() && variant.hasOption(parameterId, optionId)) {
					return true;
				}
			}

			return false;
		}

		function modelInit($viewVariants) {
			var
				variantController,
				variants = [];

			function createVariant(i, rawVariant) {
				variantController = Object.create(variant.controller);
				variantController.init(rawVariant);
				variants.push(variantController);
			}

			$.each($viewVariants.metadata().variants, createVariant);

			this.setVariants(variants);
		}

		function modelSetVariants(variants) {
			this.variants = variants;
		}

		function modelUnapplyConstraints(parameterId, optionIds) {
			var
				variants = this.getVariants(),
				i, l;

			for (i = 0, l = variants.length; i < l; i += 1) {
				variants[i].unapplyConstraints(parameterId, optionIds);
			}
		}



		function viewGetElement() {
			return this.$element;
		}

		function viewGetElementVariants() {
			return this.getElement();
		}

		function viewInit($element) {
			this.setElement($element);
		}

		function viewSetElement($element) {
			this.$element = $element;
		}



		variants = {};
		variants.controller = {
			applyConstraint: controllerApplyConstraint,
			applyConstraints: controllerApplyConstraints,
			count: controllerCount,
			countPassedVariants: controllerCountPassedVariants,
			getPassedVariants: controllerGetPassedVariants,
			hasPassedWithOption: controllerHasPassedWithOption,
			init: controllerInit,
			unapplyConstraint: controllerUnapplyConstraint,
			unapplyConstraints: controllerUnapplyConstraints
		};
		variants.model = {
			applyConstraints: modelApplyConstraints,
			count: modelCount,
			countPassedVariants: modelCountPassedVariants,
			getPassedVariants: modelGetPassedVariants,
			getVariants: modelGetVariants,
			hasPassedWithOption: modelHasPassedWithOption,
			init: modelInit,
			setVariants: modelSetVariants,
			unapplyConstraints: modelUnapplyConstraints
		};
		variants.view = {
			getElement: viewGetElement,
			getElementVariants: viewGetElementVariants,
			init: viewInit,
			setElement: viewSetElement
		};
	}());



	// define variant class
	(function () {
		function controllerApplyConstraints(parameterId, optionIds) {
			this.model.applyConstraints(parameterId, optionIds);
		}

		function controllerGetId() {
			return this.model.getId();
		}

		function controllerHasFailedConstraints() {
			return this.model.hasFailedConstraints();
		}

		function controllerHasOption(parameterId, optionId) {
			return this.model.hasOption(parameterId, optionId);
		}

		function controllerInit(rawVariant) {
			this.model = Object.create(variant.model);

			this.model.init(rawVariant);
		}

		function controllerUnapplyConstraints(parameterId, optionIds) {
			this.model.unapplyConstraints(parameterId, optionIds);
		}



		function modelAddFailedConstraint(constraint) {
			this.getFailedConstraints().push(constraint);
		}

		function modelAddPassedConstraint(constraint) {
			this.getPassedConstraints().push(constraint);
		}

		function modelApplyConstraints(parameterId, optionIds) {
			var
				constraint = {
					"parameterId": parameterId
				},
				optionId,
				i, l;

			for (i = 0, l = optionIds.length; i < l; i += 1) {
				optionId = optionIds[i];
				constraint.optionId = optionId;
				if (this.hasOption(parameterId, optionId)) {
					this.addPassedConstraint(constraint);
				} else {
					this.addFailedConstraint(constraint);
				}
			}
		}

		function modelGetFailedConstraints() {
			return this.failedConstraints;
		}

		function modelGetId() {
			return this.id;
		}

		function modelGetParameters() {
			return this.parameters;
		}

		function modelGetPassedConstraints() {
			return this.passedConstraints;
		}

		function modelHasFailedConstraints() {
			return !!this.getFailedConstraints().length;
		}

		function modelHasOption(parameterId, optionId) {
			var parameters = this.getParameters();
			return !!parameters[parameterId] && -1 !== $.inArray(optionId, parameters[parameterId]);
		}

		function modelHasPassedConstraints() {
			return !!this.getPassedConstraints().length;
		}

		function modelHelperRemoveConstraint(constraint, array) {
			var
				parameterId = constraint.parameterId,
				optionId = constraint.optionId,
				cmp,
				removeIndex = -1,
				i, l;

			for (i = 0, l = array.length; i < l; i += 1) {
				cmp = array[i];
				if (parameterId === cmp.parameterId && optionId === cmp.optionId) {
					removeIndex = i;
					break;
				}
			}

			if (-1 === removeIndex) {
				return;
			}

			array.splice(removeIndex, 1);
		}

		function modelInit(raw) {
			var parameters = {};

			function addOption(i, option) {
				var
					split = option.split("_"),
					parameterId = parseInt(split[0], 10);

				if ("undefined" === typeof parameters[parameterId]) {
					parameters[parameterId] = [parseInt(split[1], 10)];
				} else {
					parameters[parameterId].push(parseInt(split[1], 10));
				}
			}

			this.setId(raw.id);
			$.each(raw.parameters, addOption);

			this.setParameters(parameters);
			this.setFailedConstraints([]);
			this.setPassedConstraints([]);
		}

		function modelRemoveFailedConstraint(constraint) {
			modelHelperRemoveConstraint(constraint, this.getFailedConstraints());
		}

		function modelRemovePassedConstraint(constraint) {
			modelHelperRemoveConstraint(constraint, this.getPassedConstraints());
		}

		function modelSetFailedConstraints(constraints) {
			this.failedConstraints = constraints;
		}

		function modelSetId(id) {
			this.id = id;
		}

		function modelSetParameters(parameters) {
			this.parameters = parameters;
		}

		function modelSetPassedConstraints(constraints) {
			this.passedConstraints = constraints;
		}

		function modelSetVariants(variants) {
			this.variants = variants;
		}

		function modelUnapplyConstraints(parameterId, optionIds) {
			var
				constraint = {
					"parameterId": parameterId
				},
				optionId,
				i, l;

			for (i = 0, l = optionIds.length; i < l; i += 1) {
				optionId = optionIds[i];
				constraint.optionId = optionId;
				this.removeFailedConstraint(constraint);
				this.removePassedConstraint(constraint);
			}
		}



		variant = {};
		variant.controller = {
			applyConstraints: controllerApplyConstraints,
			getId: controllerGetId,
			hasFailedConstraints: controllerHasFailedConstraints,
			hasOption: controllerHasOption,
			init: controllerInit,
			unapplyConstraints: controllerUnapplyConstraints
		};
		variant.model = {
			addFailedConstraint: modelAddFailedConstraint,
			addPassedConstraint: modelAddPassedConstraint,
			applyConstraints: modelApplyConstraints,
			getFailedConstraints: modelGetFailedConstraints,
			getId: modelGetId,
			getParameters: modelGetParameters,
			getPassedConstraints: modelGetPassedConstraints,
			hasFailedConstraints: modelHasFailedConstraints,
			hasOption: modelHasOption,
			init: modelInit,
			removeFailedConstraint: modelRemoveFailedConstraint,
			removePassedConstraint: modelRemovePassedConstraint,
			setFailedConstraints: modelSetFailedConstraints,
			setId: modelSetId,
			setParameters: modelSetParameters,
			setPassedConstraints: modelSetPassedConstraints,
			unapplyConstraints: modelUnapplyConstraints
		};
	}());



	// handle selector behavior
	(function () {
		var
			selectorController = Object.create(selector.controller),
			productsController = Object.create(products.controller);
		selectorController.init($("#body"));
		productsController.init($("#body"));

		$("#body")
			.bind("constraints-changed", $.proxy(productsController.handleConstraintsChanged, productsController))
			.bind("products-updated", $.proxy(selectorController.handleProductsUpdated, selectorController))
			.bind("details-clicked", $.proxy(productsController.handleDetailsClicked, productsController));

		productsController.triggerProductsUpdated();
	}());


	// handle product list paging
	(function () {
		var
			itemHeight = $products.outerHeight(true),
			maxVisibleCount = Math.round($productsContainer.height() / itemHeight),
			currentPosition = 0;

		repositionProductList =
			function () {
				var
					animationProperties = {
						"margin-top": 0
					},
					animationOptions = {
						"duration": "fast"
					};

				currentPosition = 0;
				$productsList.animate(animationProperties, animationOptions);
			};

		function clickHandlerNextPage(e) {
				var
					animationProperties,
					animationOptions = {
						duration: "normal"
					},
					$visibleProducts = $products.filter(":visible");

				if(currentPosition+1 >= $visibleProducts.length) return false;

				currentPosition = Math.min($visibleProducts.length-1, currentPosition + maxVisibleCount);
				animationProperties = {
					"margin-top": -currentPosition * itemHeight
				};

				$productsList.animate(animationProperties, animationOptions);
				return false;
			}

		function clickHandlerNext(e) {
			var
				animationProperties,
				animationOptions = {
					duration: "normal"
				},
				$visibleProducts = $products.filter(":visible");

			if(currentPosition+1 >= $visibleProducts.length) return false;

			animationProperties = {
				"margin-top": -(++currentPosition)*itemHeight
			};

			$productsList.animate(animationProperties, animationOptions);
			return false;
		}

		function clickHandlerPrevious(e) {
			var
				animationProperties,
				animationOptions = {
					duration: "normal"
				};

			if(currentPosition <= 0) return false;

			animationProperties = {
				"margin-top": -(--currentPosition) * itemHeight
			};

			$productsList.animate(animationProperties, animationOptions);
			return false;
		}

		function clickHandlerPreviousPage(e) {
			var
				animationProperties,
				animationOptions = {
					duration: "normal"
				};

			if(currentPosition <= 0) return false;

			currentPosition = Math.max(0, currentPosition - maxVisibleCount);
			animationProperties = {
				"margin-top": -currentPosition * itemHeight
			};

			$productsList.animate(animationProperties, animationOptions);
			return false;
		}

		$("#body .navigation .next-page").bind("click", clickHandlerNextPage);
		$("#body .navigation .next").bind("click", clickHandlerNext);
		$("#body .navigation .previous").bind("click", clickHandlerPrevious);
		$("#body .navigation .previous-page").bind("click", clickHandlerPreviousPage);

		$("#body").bind("products-updated", repositionProductList);

	}());
});
