///////////////////////////////////////////////////////////////////////////////
//	Core
///////////////////////////////////////////////////////////////////////////////
/**
 * The Moen Global object.
 * @title  Moen Global
 * @module Moen
 */

/**
 * The Moen global namespace object.
 * @namespace
 * @class Moen
 * @static
 */
var Moen = {};

/**
 * The bootstrap method is meant to load and initialize page components.
 * This method exists simply for organizational purposes.
 *
 * The bootstrap method can only be executed once.
 *
 * @method bootstrap
 * @static
 */
Moen.bootstrap = (function () {
	/*
	 * For browsers without the console object
	 */
	if (typeof window.console === 'undefined') {
		window['console'] = {
			assert: function () {},
			count: function () {},
			debug: function () {},
			dir: function () {},
			dirxml: function () {},
			error: function () {},
			group: function () {},
			groupCollapsed: function () {},
			groupEnd: function () {},
			info: function () {},
			log: function () {},
			profile: function () {},
			profileEnd: function () {},
			time: function () {},
			timeEnd: function () {},
			trace: function () {},
			warn: function () {}
		};
	}

	return function () {
		throw new Error('Bootstrap has already executed.');
	};
}());

///////////////////////////////////////////////////////////////////////////////
//	Environment
///////////////////////////////////////////////////////////////////////////////
/**
 * The Moen Environment
 * @module env
 * @title Environment
 */

/**
 * @namespace Moen
 * @title Moen Environment
 * @class env
 * @static
 */
Moen.env = (function () {
	var env = {};

	/**
	 * Contains the cached result of executing <a href="#method_getLocation">Moen.env.getLocation();</a>.
	 * @namespace Moen.env
	 * @property section
	*/
	env.section = null;

	/**
	 * Retrieves information about where the page is considered to be
	 * "located". This allows other modules such as the
	 * <code>AbstractAction</code> to execute context-based functions.
	 * @namespace Moen.env
	 * @method getLocation
	 * @return {Object} Returns an object containing a <code>section</code> and
	 * <code>pages</code> <code>Array</code>. The <code>section</code>
	 * <code>String</code> is simply the current ID assigned to the
	 * <code>&lt;body&gt;</code> element. The <code>pages</code>
	 * <code>Array</code> contains all of the classes applied to the
	 * <code>&lt;body&gt;</code> element.
	*/
	env.getLocation = function () {
		if (this.section !== null) {
			return this.section;
		} else {
			this.section = {
				section: document.body.id,
				pages: document.body.className.replace(/\s{1,}/g, '|').split('|')
			};

			return this.section;
		}
	};

	/**
	 * Retrieves the value of the named query parameter.<br>
	 * For example, if the URL of the current page was:
	 * <b>http://moen.com/where-to-buy?location=all</b><br>
	 * <code>Moen.env.getQueryParameter('location')</code> would return
	 * <code>"all"</code>.
	 * @namespace Moen.env
	 * @method getQueryParameter
	 * @return {String} Returns the query parameter value as a
	 * <code>String</code>. If not found, returns
	 * <code>undefined</code>.
	*/
	env.getQueryParameter = function (key) {
		// Potentially may need to refactor this to cache query params
		var queryString = window.location.href.replace(/(.*)\/((?:[^?])*\/)?([^.\/?]+)(\?+(.*))?/g, '$5').split('&');
		for (var i = 0, j = queryString.length; i < j; i = i + 1) {
			var param = queryString[i].split('=');
			if (param[0] === key) {
				return param[1];
			}
		}
		return undefined;
	};

	return env;
}());

///////////////////////////////////////////////////////////////////////////////
//	Language
///////////////////////////////////////////////////////////////////////////////
/**
 * The Language Components
 * @module lang
 * @title Language Components
 */

/**
 * The lang class
 * @namespace Moen
 * @class lang
 * @static
 */
Moen.lang = {
	/**
	 * TODO: Add description
	 * @namespace Moen.lang
	 * @method round
	 */
	round: function (number, precision) {
		var rounded = String(Math.round(number * Math.pow(10, precision)) / Math.pow(10, precision));

		rounded = rounded.split('.');

		if (rounded.length === 1) {
			rounded[1] = '';
		}

		for (var i = 0, j = precision - rounded[1].length; i < j; i = i + 1) {
			rounded[1] += '0';
		}

		rounded = rounded.join('.');

		return rounded;
	},

	/**
	 * TODO: Add description
	 * @author Douglas Crockford
	 * @namespace Moen.lang
	 * @method typeOf
	 * @modifiedBy Ben Truyman
	 */
	typeOf: function (value) {
		var s = typeof value;
		if (s === 'object') {
			if (value) {
				if (typeof value.length === 'number' && !(value.propertyIsEnumerable('length')) && typeof value.splice === 'function') {
					s = 'array';
				}
			} else {
				s = 'null';
			}
		}
		return s;
	}

};

/**
 * TODO: Add description
 * @namespace Moen.lang
 * @class Enumerable
 */
Moen.lang.Enumerable = function (data) {
	this.data = data;

	this.findByAttribute = function () {
		var results = [],
			key,
			value;

		if (typeof arguments[0] === 'object') {
			var refinements = arguments[0];
			results = data;
			for (key in refinements) {
				if (refinements[key] !== null) {
					if (typeof refinements[key].min !== 'undefined' && typeof refinements[key].max !== 'undefined') {
						results = new Moen.lang.Enumerable(results).findByRange(key, refinements[key].min, refinements[key].max).data;
					} else {
						results = new Moen.lang.Enumerable(results).findByAttribute(key, refinements[key]).data;
					}
				}
			}
		} else {
			key = arguments[0];
			value = arguments[1];
			for (var i = 0; i < data.length; i++) {
				if (data[i][key] && data[i][key] == value) {
					results.push(data[i]);
				} else if (Moen.lang.typeOf(data[i][key]) === 'array' && $.inArray(value, data[i][key]) !== -1) {
					results.push(data[i]);
				}
			}
		}

		return new Moen.lang.Enumerable(results);
	};
	this.findByRange = function (key, min, max) {
		var results = [];
		for (var i = 0, j = data.length; i < j; i++) {
			if (data[i][key] && data[i][key] >= min && data[i][key] <= max) {
				results.push(data[i]);
			}
		}
		return new Moen.lang.Enumerable(results);
	};
};

/**
 * TODO: Add description
 * @namespace Moen.lang
 * @class HTMLTemplate
 */

Moen.lang.HTMLTemplate = function () {
	var HTMLTemplate = {},
		argumentsString = '',
		applications = {},
		fragment,
		template;

	for (var i = 0, j = arguments.length; i < j; i = i + 1) {
		argumentsString += arguments[i];
	}

	HTMLTemplate.initialize = function (str) {
		template = fragment = str;
	};

	HTMLTemplate.getTemplate = function () {
		return template;
	};

	HTMLTemplate.setTemplate = function (str) {
		template = str;
	};

	HTMLTemplate.getFragment = function () {
		return fragment;
	};

	HTMLTemplate.apply = function (params) {
		var regexp;

		fragment = template;

		for (var param in params) {
			applications[param] = params[param];
		}

		for (var application in applications) {
			regexp = new RegExp('%' + application + '%', 'g');
			fragment = fragment.replace(regexp, applications[application]);
		}

		return fragment;
	};

	HTMLTemplate.reset = function () {
		fragment = template;
	};

	HTMLTemplate.initialize(argumentsString);

	return HTMLTemplate;
};

///////////////////////////////////////////////////////////////////////////////
//	Actions
///////////////////////////////////////////////////////////////////////////////
/**
 * The Actions Component<br>
 * @module action
 * @title Action Components
 */
Moen.action = {};

/**
 * Actions are a way of organizing and executing functional components on a
 * page. Actions will only execute if they are in the right page context as
 * specified at upon construction. If no arguments are passed during
 * construction, an action's initialize method will be invoked on every page.
 * @namespace Moen.action
 * @class AbstractAction
 * @constructor
 * @param {Array|String|null} sections (optional) The sections that must match for the action's initialize method to be invoked.
 * 								Sections in this case happen to be possible IDs on the body element.
 * @param {Array|String} pages (optional) The pages that must match for the action's initialize method to be invoked.
 * 								Pages in this case happen to be possible classes on the body element.
 */

/**
 * Invoked automatically if the sections and pages passed in during contruction match.
 * @method initialize
 */
Moen.action.AbstractAction = function (sections, pages) {
	var AbstractAction = {},
		applicableSections = [],
		applicablePages = [];

	function execute () {
		$(function () {
			if (AbstractAction.initialize) {
				AbstractAction.initialize();
			}
		});
	}

	switch (Moen.lang.typeOf(sections)) {
	case 'string':
		applicableSections.push(sections);
		break;
	case 'array':
		applicableSections = sections;
		break;
	default:
		break;
	}

	switch (Moen.lang.typeOf(pages)) {
	case 'string':
		applicablePages.push(pages);
		break;
	case 'array':
		applicablePages = pages;
		break;
	default:
		break;
	}

	if (sections === null || typeof sections === 'undefined' || $.inArray(Moen.env.getLocation()['section'], applicableSections) !== -1) {
		if (applicablePages.length !== 0) {
			for (var i = 0, j = applicablePages.length; i < j; i = i + 1) {
				if ($.inArray(applicablePages[i], Moen.env.getLocation()['pages']) !== -1) {
					execute();
					break;
				}
			}
		} else {
			execute();
		}
	}

	return AbstractAction;
};

/**
 * TODO: Add description
 * @namespace Moen.action
 * @class AccountForm
 * @static
 */
Moen.action.AccountForm = (function () {
	var AccountForm = Moen.action.AbstractAction(null, ['registration','edit_account']);

		AccountForm.initialize = function () {
			this.container = $('#user').get(0);

			this.setupCommunications();
			this.setupCountries();
		};

		AccountForm.setupCommunications = function () {
			this.communications = $('#communications').get(0);
			this.survey = $('#survey').get(0);
			this.recentProjects = $('#opt-recentprojects').get(0);
			this.timeframe = $('#opt-timeframe').get(0);
			this.upcomingProjects = $('input.upcomingprojects[@type=checkbox]').get();

			$(this.upcomingProjects).click(Moen.util.Event.createDelegate(this, this.handleUpcomingProjectsChange));

			if (this.communications && this.survey) {
				$('#communications-yes, #communications-no, #surveyCheck ').click(Moen.util.Event.createDelegate(this, this.handleCommunicationsChange));
				this.checkCommunications();
			}

			if (this.recentProjects && this.timeframe) {
				this.checkUpcomingProjects();
			}
		};

		AccountForm.setupCountries = function () {
			var countryContainer = $('#country', this.container).get(0),
				stateContainer = $('#state', this.container).get(0),
				stateGroups = {},
				stateSelectionCache = {};

			// Gather state groups
			$('optgroup', stateContainer).each(function () {
				stateGroups[this.className] = this.innerHTML;
			});

			// Setup handler for country change
			$(countryContainer).change(function (e) {
				setStates(this.value);
			});

			// Setup handler for state/province change
			$(stateContainer).change(function (e) {
				stateSelectionCache[countryContainer.value] = this.value;
			});

			setStates(countryContainer.value);

			function setStates (countryCode) {
				if (stateGroups[countryCode]) {
					// We use jQuery's html() method to normalize some IE issues
					$(stateContainer).html('<option value=""></option>' + stateGroups[countryCode]);
				}
				if(stateSelectionCache[countryCode]) {
					stateContainer.value = stateSelectionCache[countryCode];
				}
			}

			// Set initial country as US if it has no value
			if (countryContainer.value === '') {
				$(countryContainer).val('US').change();
			}

		};

		AccountForm.showSurvey = function () {
			$(this.survey).slideDown();
		};
		AccountForm.hideSurvey = function () {
			$(this.survey).hide();
		};

		AccountForm.handleCommunicationsChange = function (e) {
			this.checkCommunications();
		};

		AccountForm.handleUpcomingProjectsChange = function (e) {
			var input = e.currentTarget;

			if ($(input).val().toLowerCase() === 'na' && input.checked) {
				this.disallowProjects();
			} else if ($('input.upcomingprojects:checked').length === 0) {
				$(this.recentProjects).hide();
				$(this.timeframe).hide();
			} else {
				this.allowProjects();
			}
		};

		AccountForm.checkCommunications = function () {
			if ($('#communications-yes').attr('checked') === true) {
				this.showSurvey();
			} else {
				this.hideSurvey();
			}
                        if($('#surveyCheck').attr('checked')  === true ) {
                            this.showSurvey();
			} else {
				this.hideSurvey();
			}

		};

		AccountForm.checkUpcomingProjects = function () {
			var hasNoProjects = false;

			$(this.recentProjects).hide();
			$(this.timeframe).hide();

			$(this.upcomingProjects).each(function () {
				if ($(this).val().toLowerCase() === 'na' && this.checked) {
					hasNoProjects = true;
				} else if (this.checked) {
					$(AccountForm.timeframe).show();
				}
			});

			if (hasNoProjects) {
				this.disallowProjects();
			}
		};

		AccountForm.allowProjects = function () {
			$(this.upcomingProjects).each(function () {
				if($(this).val().toLowerCase() === 'na') {
					this.checked = false;
				}
			});
			$(this.recentProjects).hide();
			$(this.timeframe).show();
		};
		AccountForm.disallowProjects = function () {
			$(this.upcomingProjects).each(function () {
				if($(this).val().toLowerCase() !== 'na') {
					this.checked = false;
				}
			});
			$(this.timeframe).hide();
			$(this.recentProjects).show();
		};

	return AccountForm;

}());

/**
 * TODO: Add description
 * @namespace Moen.action
 * @class Common
 * @static
 */

Moen.action.Common = (function () {
	var Common = Moen.action.AbstractAction();

	Common.loginModal = null;

	Common.initialize = function () {
		// Change cursor to "busy" state while loading AJAX content
		$.ajaxSetup({
			beforeSend: function () {
				$('body').addClass('busy');
			},
			complete: function () {
				$('body').removeClass('busy');
			}
		 });

		$.ajaxSetup({ traditional: true });

		// Site Search Form Validator
		$('.site_search form').each(function () {
			var validator = Moen.util.FormValidator(this, {
				search_terms: {
					required: true
				}
			});

			validator.handleFailure = function (results) {
				alert('Please enter a search query.');
				return false;
			};
		});

		// External links
		$('a[rel="external"], a[rel="document"]').live('click', function (event) {
			event.preventDefault();
			window.open($(this).attr('href'));
			return false;
		});

		// Print links
		$('a[rel="print"]').live('click', function (event) {
			event.preventDefault();
			window.print();
		});

		// Disabled Tab
		$('.ui-state-disabled a').click(function (event) {
			event.preventDefault();
		});

		// Modal Window
		(function () {
			var modalWindow = new Moen.ui.Overlay({
				cacheContent: false,
				destroyOnHide: true
			});

			$('a[rel="modal"]').live('click', function (event) {
				event.preventDefault();
				var content;

				if (String(this.href).match(/.*(\.jpg$|\.gif$|\.jpeg$|\.png$|AssetRoutingServlet*)/i)) {
					content = 'http://fr.moen.ca/enfr/img-embed.modal?title=true&src=' + encodeURIComponent(this.href);
				} else {
					var url = Moen.util.String.explodeURL(this.href);
					url.extension = 'modal';
					url.query.title = 'true'; // Includes the default title bar with close button
					url = Moen.util.String.implodeURL(url);
					content = url;
				}

				modalWindow.overlayClass = $(this).attr('data-modal-type') || $(this).attr('class');
				modalWindow.overlayClass += ' ui-overlay';

				$.get(content, function (html) {
					modalWindow.contentBox = $(html).appendTo('body').get(0);

					$('a.close', modalWindow.contentBox).click(function (event) {
						modalWindow.hide();
						event.preventDefault();
					});

					modalWindow.show();
				}, 'html');
			});
		}());

		// Forms
		$('form').each(function () {
			Moen.util.FormHelper.setupAutoTabber(this);
			if (!Modernizr.input.placeholder) {
				Moen.util.FormHelper.setupDefaultValueHandler(this);
			}
			Moen.util.FormHelper.setupInputStyler(this);
		});

		// Region Selector
		$('#region-lang a').click(function (event) {

		});

		// Tips
		$('.tip').each(function () {
			Moen.ui.Tooltip(this, $('span', this).html());
		});
		if ($('#view-cart').hasClass('disabled')) {
			$('#view-cart').each(function () {
				Moen.ui.Tooltip($('div', this), $('span', this).text());
			});
		}
	};

	Common.postInitialize = function () {

		if (window.postInitializers) {
			for (var i = 0; i < postInitializers.length; i++) {
				console.log(postInitializers[i]);
				postInitializers[i].call();
			}
		}

		$(function () {
			/**
			 * Link "Auto-activator"
			 * To use, append "#activate=INSERT_HREF" onto the URL of a page.
			 * This script will attempt to find the first link with an 'href'
			 * attribute equal to INSERT_HREF, and simulate a click on it.
			 */
			var hash = unescape(window.location.hash),
				activator = '#activate=';

			if (hash.match(activator)) {
				var link = $('a[href~=' + hash.replace(activator, '') + ']').get(0);
				$(link).click();
			}
		});
	};

	return Common;
}());

/**
 * TODO: Add description
 * @namespace Moen.action
 * @class FaucetSelector
 * @static
 */
Moen.action.FaucetSelector = (function () {
	var FaucetSelector = Moen.action.AbstractAction();

	FaucetSelector.initialize = function () {
		var faucetSelectorLinks = $('.faucet_selector-link').get(),
			initialized = false;

		if (faucetSelectorLinks.length > 0) {
			initialized = true;
			Moen.ui.FaucetSelector.initialize();
			$(faucetSelectorLinks).click(function (event) {
				event.preventDefault();
				Moen.ui.FaucetSelector.show();
			});
		}

		if (String(window.location.hash).match('faucet-selector')) {
			if (!initialized) {
				Moen.ui.FaucetSelector.initialize();
				initialized = true;
			}
			Moen.ui.FaucetSelector.show();
		}
	};

	return FaucetSelector;

}());

/**
 * TODO: Add description
 * @namespace Moen.action
 * @class Gallery
 * @static
 */
Moen.action.Gallery = (function () {
	var Gallery = Moen.action.AbstractAction(['gallery', 'search']);

	Gallery.initialize = function () {
		this.containers = {
			gallery: $('#gallery-product').get(0),
			products: $('#products').get(0)
		};

		if (this.containers.gallery) {
			this.setupCompareCheckboxes();
			this.setupFinishTooltips();
			this.setupBrowseBy();
		}
	};

	Gallery.setupCompareCheckboxes = function () {
		var maxChecked = 4,
			amountChecked = 0,
			checkboxes = $('input[name="products"]', this.containers.products).get();

		$('form', this.containers.gallery).bind('submit', function (event) {
			if (amountChecked < 2 || amountChecked > 4) {
				alert('Veuillez choisir de 2 \340 4 produits \340 comparer.');
				event.preventDefault();
			}
		});

		$(checkboxes).each(function () {
			if ($(this).attr('checked')) {
				amountChecked++;
			}
			$(this).bind(($.browser.msie ? 'click' : 'change'), function () {
				if ($(this).attr('checked')) {
					amountChecked++;
					if (amountChecked === maxChecked) {
						$(checkboxes).not(':checked').attr('disabled', 'disabled');
					}
				} else {
					amountChecked--;
					if (amountChecked < maxChecked) {
						$(checkboxes).attr('disabled', '');
					}
				}
			});
		});

		// Uncheck all checkboxes if for some reason the user has loaded the page with more than the maxChecked checked
		if (amountChecked > maxChecked) {
			$(checkboxes).attr('checked', '');
			amountChecked = 0;
		} else if (amountChecked === maxChecked) {
			$(checkboxes).not(':checked').attr('disabled', 'disabled');
		}

		$('div.compare-input').each(function (index, item) {
			var tip = $(item),
				Tooltip = new Moen.ui.Tooltip($('input:first', item), $('span.tip span', tip).html(), {'delay': 250});
		});

	};

	Gallery.setupFinishTooltips = function () {
		$('.finish', this.containers.products).each(function (index, item) {
			var $this = $(this),
				Tooltip;
				Tooltip = new Moen.ui.Tooltip($this, $('span', $this).html(), {});
		});
	};

	Gallery.setupBrowseBy = function () {
		var collectionList = $('#collections'),
			collectionItems,
			collapsedHeight,
			expandedHeight,
			currentHeight = 0,
			toggle = $('#browse_catalog-by_category-toggle .toggle');

		collectionItems = $('li', collectionItems);
		collapsedHeight = $('li:first', collectionItems).height();
		expandedHeight = $(collectionList).height();

		currentHeight = collapsedHeight;

		collectionList.css({
			height: collapsedHeight
		});

		toggle.click(function (event) {
			event.preventDefault();

			if (currentHeight === collapsedHeight) {
				collectionList.animate({'height': expandedHeight}, 'slow');
				toggle.text('R\351duire').removeClass('expand').addClass('collapse');
				currentHeight = expandedHeight;
			} else {
				collectionList.animate({'height': collapsedHeight}, 'slow');
				toggle.text('Tout afficher').removeClass('collapse').addClass('expand');
				currentHeight = collapsedHeight;
			}

		});
	};

	return Gallery;
}());

/**
 * TODO: Add description
 * @namespace Moen.action
 * @class Homepage
 * @static
 */
Moen.action.Homepage = (function () {
	var Homepage = Moen.action.AbstractAction('home');

		Homepage.initialize = function () {
			var buttons = $('#postcard-buttons ul li a'),
				slides = $('.slide'),
				Slideshow;

			Slideshow = new Moen.ui.Slideshow($('#postcards'), {'slides': slides});

			Slideshow.bind('slideChanged', function () {
				buttons.eq(Slideshow.getCurrentSlideIndex()).click();
			});

			buttons.each(function (index, item) {
				var button = $(item);
				button.click(function (event) {
					event.preventDefault();
					Slideshow.gotoSlide(slides.eq(index), 250);
					buttons.removeClass('selected');
					button.addClass('selected');
				});
			}).hover(Slideshow.stop, Slideshow.start);

			Slideshow.start();
			buttons.eq(Slideshow.getCurrentSlideIndex()).click();


			$('#btn-prev').click(function(e){
				e.preventDefault();
				Slideshow.prev();

			});

			$('#btn-next').click(function(e){
				e.preventDefault();
				Slideshow.next();
			});

			if ($.browser.msie) {
				$('#postcards').removeClass('ombrag\351');
			}

		};

	return Homepage;

}());

/**
 * TODO: Add description
 * @namespace Moen.action
 * @class Login
 * @static
 */
Moen.action.Login = (function () {
	var Login = Moen.action.AbstractAction('login');

		Login.initialize = function () {
			if (Moen.env.getQueryParameter('role')) {
				$('#register_form input[name=regEmail]').focus();
			}
		};

	return Login;

}());

/**
 * TODO: Add description
 * @namespace Moen.action
 * @class Paginator
 * @static
 */
Moen.action.Paginator = (function () {
	var Paginator = Moen.action.AbstractAction(['gallery', 'search']);

		Paginator.initialize = function () {
			$('.paginator-sort-by').change(function () {
				$(this).parent().submit();
			});
		};

	return Paginator;

}());

/**
 * TODO: Add description
 * @namespace Moen.action
 * @class Survey
 * @static
 */
Moen.action.Survey = (function () {
	var Survey = Moen.action.AbstractAction();

	Survey.initialize = function () {
		var ENTRY = 'survey_entry',
			PAGE = 'survey_page',
			VISITED_TIME = 5 // In minutes
			entryCookie = Moen.util.Cookie.get(ENTRY),
			loc = Moen.env.getLocation();

		if (!entryCookie) {
			Moen.util.Cookie.set(ENTRY, new Date().getTime(), {
				path: '/'
			});
		}

		/*
		 * Check to see if the user has been on the site for 3 minutes or
		 * longer and that visited a specific section
		 */
		if (entryCookie && ((new Date().getTime() - entryCookie) / 1000 / 60) >= VISITED_TIME) {
			s.loadModule('Survey');
			s.Survey.suites = 'moencom2009live';
			Moen.util.Omniture.launchSurvey(Survey.surveyId);
		}
	};

	return Survey;
}());

///////////////////////////////////////////////////////////////////////////////
//	User Interface Components
///////////////////////////////////////////////////////////////////////////////
/**
 * The User Interface Components
 * @module ui
 * @title User Interface Components
 */
Moen.ui = {};

/**
 * TODO: Add description
 * @namespace Moen.ui
 * @class CheckboxManager
 */
Moen.ui.CheckboxManager = function (checkboxes, options) {
	var CheckboxManager = $(this),
	defaultOptions = {
		'limit': 4
	},
	numberChecked = checkboxes.filter(':checked').length,
	isEnabled = true;

	options = $.extend(true, {}, defaultOptions, options);

	function testLimit() {
		if (numberChecked === options.limit) {
			CheckboxManager.disable();
			isEnabled = false;
		} else if (numberChecked === options.limit - 1) {
			if (isEnabled === false) {
				CheckboxManager.enable();
				isEnabled = true;
			}
		}
	}

	CheckboxManager.enable = function () {
		checkboxes.filter('input:disabled').removeAttr('disabled');
		CheckboxManager.trigger('enabled');
	};
	CheckboxManager.disable = function () {
		checkboxes.filter(function () {
			return (this.checked === false);
		}).attr('disabled', 'disabled');
		CheckboxManager.trigger('disabled');
	};

	testLimit();
	checkboxes.click(function (event) {
		if (this.checked) {
			numberChecked = numberChecked + 1;
		} else {
			numberChecked = numberChecked - 1;
		}
		testLimit();
	});

	return CheckboxManager;
};

/**
 * TODO: Add description
 * @namespace Moen.ui
 * @class FaucetSelector
 * @static
 */
Moen.ui.FaucetSelector = (function () {
	// Public
	var FaucetSelector = {
		ui: null
	},
	// Private
	cache = {
		tabByOption: {}
	},
	contentBox = null,
	imageDimensions = [],
	overlay = null,
	maskOpacity = null,
	model = null,
	states = {
		activeCriteria: null,
		activeTab: null,
		built: false,
		busy: false,
		busyMessage: 'Loading',
		criteriaHidden: false,
		initialized: false,
		options: {},
		preferences: {},
		productIndex: {
			last: null,
			current: null
		},
		productInformation: {
			name: null,
			price: null
		},
		products: [],
		roomType: null
	},
	// Static
	ACTIVE_CLASS = 'active',
	BUSY_CLASS = 'busy',
	DISABLED = 'disabled',
	ENDECA = {
		finish: 'Fini',
		finishGroup: 'FS_Finish',
		location: 'Endroit',
		locations: {
			bathroom: 'Robinets de lavabo',
			kitchen: 'Robinets d\047\351vier de cuisine'
		},
		mount: 'Mount_Type',
		numberOfHandles: 'Nombre de poign\351es',
		numberOfHoles: 'Nombre de trous',
		priceRange: 'Prix',
		spoutShape: 'Forme du bec',
		spoutType: 'Type de bec',
		style: 'Style'
	},
	FADE_SPEED = 500,
	NO_PREFERENCE = 'Pas de pr\351f\351rence',
	PATHS = {
		template: 'http://fr.moen.ca/enfr/faucet-selector/layout.ajax',
		models: {
			bathroom: 'http://fr.moen.ca/enfr/faucet-selector/gallery.json?location=bath',
			kitchen: 'http://fr.moen.ca/enfr/faucet-selector/gallery.json?location=kitchen'
		},
		gallery: '/gallery/faucet-selector'
	},
	ROOM_TYPES = ['bathroom', 'kitchen'],
	SLIDE_SPEED = 200;

	FaucetSelector.initialize = function () {
		if (states.initialized) {
			return false;
		}

		// Preload necessary images
		Moen.util.ImagePreloader.load([
			'http://fr.moen.ca/img/moen/FR_155970.jpg',
			'http://fr.moen.ca/img/moen/FR_155971.jpg',
			'http://fr.moen.ca/img/moen/FR_161340.png'
		]);

		// Setup custom event handlers
		$(this).bind('modelReady', Moen.util.Event.createDelegate(this, this.handleModelReady));

		// Build modal window
		overlay = Moen.ui.Overlay({
			transition: ($.browser.msie) ? 'none' : 'fade'
		});

		$(overlay).bind('show.overlay', Moen.util.Event.createDelegate(this, this.build));
	};

	FaucetSelector.build = function () {

		if (states.built === true) {
			return false;
		}

		// Store UI elements
		this.ui = {
			container: $('#faucet_selector').get(0),
			busyMessage: $('#faucet_selector-busy_message').get(0),
			hide: $('#faucet_selector-hide').get(0),
			intro: {
				container: $('#faucet_selector-intro').get(0),
				backgrounds: {
					bathroom: $('#faucet_selector-intro-backgrounds-bathroom').get(0),
					kitchen: $('#faucet_selector-intro-backgrounds-kitchen').get(0)
				},
				navigation: {
					bathroom: $('#faucet_selector-intro-navigation-bathroom').get(0),
					kitchen: $('#faucet_selector-intro-navigation-kitchen').get(0)
				}
			},
			main: {
				container: $('#faucet_selector-main').get(0),
				header: {
					start_over: $('#faucet_selector-main-header-start_over').get(0),
					tabs: $('#faucet_selector-main-header-tabs li').get()
				},
				body: {
					container: $('#faucet_selector-main-body').get(0),
					criteria: {
						container: $('#faucet_selector-main-body-criteria').get(0),
						items: $('#faucet_selector-main-body-criteria > li').get()
					},
					criteria_navigation: {
						container: $('#faucet_selector-main-body-criteria_navigation').get(0),
						get_results: $('#faucet_selector-main-body-criteria_navigation-get_results').get(0),
						next_tab: $('#faucet_selector-main-body-criteria_navigation-next_tab').get(0)
					},
					product_information: {
						container: $('#faucet_selector-main-body-product_information').get(0),
						name: $('#faucet_selector-main-body-product_information-name').get(0),
						price: $('#faucet_selector-main-body-product_information-price').get(0)
					},
					products: {
						container: $('#faucet_selector-main-body-products').get(0),
						items: $('#faucet_selector-main-body-products li').get()
					},
					no_results: $('#faucet_selector-main-body-no_results').get(0),
					slider: {
						container: $('#faucet_selector-main-body-slider').get(0),
						indicator: $('#faucet_selector-main-body-slider-indicator').get(0),
						track: $('#faucet_selector-main-body-slider-track').get(0),
						gallery: {
							container: $('#faucet_selector-main-body-slider-gallery').get(0),
							link: $('#faucet_selector-main-body-slider-gallery-link').get(0),
							results: $('#faucet_selector-main-body-slider-gallery-results').get(0)
						}
					}
				},
				footer: {
					container: $('#faucet_selector-main-footer').get(0)
				}
			}
		};

		// Hide button
		$(this.ui.hide).click(function () {
			FaucetSelector.hide();
		});

		// Get Results button
		$(this.ui.main.body.criteria_navigation.get_results).click(Moen.util.Event.createDelegate(this, this.handleGetResultsClick));

		// Next Tab button
		$(this.ui.main.body.criteria_navigation.next_tab).click(Moen.util.Event.createDelegate(this, this.handleNextTabClick));

		// Start setting everything up
		this.setupIntro();
		this.setupMain();
		this.gotoIntro();

		if (Moen.env.getQueryParameter('fs')) {
			$(this.ui.intro.navigation.bathroom).click();
			this.show();
		}

		states.built = true;
	};

	// Setter-uppers

	FaucetSelector.setupIntro = function () {
		var backgrounds = FaucetSelector.ui.intro.backgrounds;
		$.each(ROOM_TYPES, function (index) {
			var roomType = String(this),
				isIE6 = ($.browser.msie && $.browser.version == '6.0');

			if (index !== 0) {
				if (isIE6) $(backgrounds[roomType]).hide();
				else $(backgrounds[roomType]).hide().fadeTo(0, 0);
			} else {
				$(backgrounds[roomType]).css('opacity', 1);
			}

			$(FaucetSelector.ui.intro.navigation[this])
				.hover(function (e) {
					for (var background in backgrounds) {
						if (background === roomType) {
							if (isIE6) $(backgrounds[background]).show();
							else $(backgrounds[background]).show().stop().fadeTo(FADE_SPEED, 1);
						} else {
							if (isIE6) $(backgrounds[background]).hide();
							else $(backgrounds[background]).stop().fadeTo(FADE_SPEED, 0);
						}
					}
				})
				.click(function (e) {
					FaucetSelector.setRoomType(roomType);
				});
		});
	};
	FaucetSelector.setupMain = function () {
		// Setup handlers for tab clicks
		$(this.ui.main.header.tabs).mousedown(Moen.util.Event.createDelegate(this, this.handleTabClick));
		$('a', this.ui.main.header.tabs).click(Moen.util.Event.dummyEventHandler);

		// Setup handlers for criteria option clicks
		$('.options a', this.ui.main.body.criteria.items)
			.mousedown(Moen.util.Event.createDelegate(this, this.handleOptionClicks))
			.click(Moen.util.Event.dummyEventHandler);

		// Setup handlers for product clicks
		$('a', this.ui.main.body.products.items).each(function (index) {
			var half = Math.floor(FaucetSelector.ui.main.body.products.items.length / 2);

			if (index !== half) {
				$(this).click(function (e) {
					e.preventDefault();
					FaucetSelector.setSliderValue(states.productIndex.current + (index - half));
				});
			} else {
				$(this).click(function (e) {
					e.preventDefault();
					Moen.util.Window.open(this.href);
				});
			}
		});

		// Setup handler for gallery view click
		$(this.ui.main.body.slider.gallery.link).click(function (e) {
			e.preventDefault();
			Moen.util.Window.open(this.href, null, {scrollbars: 'yes'});
		});

		// Start Over button
		$(this.ui.main.header.start_over).click(Moen.util.Event.createDelegate(this, this.handleStartOverClick));

		// Create placeholder variables for all possible options
		$.each(this.ui.main.body.criteria.items, function () {
			$('a[rel]', this).each(function () {
				if (typeof states.options[this.rel] === 'undefined') {
					states.options[this.rel] = null;
				}
			});
		});

		// Setup product slider
		$(this.ui.main.body.slider.track).slider({
			step: 1,
			slide: Moen.util.Event.createDelegate(this, this.handleSliderSlide),
			stop: Moen.util.Event.createDelegate(this, this.handleSliderStop)
		});
	};

	// Loaders

	FaucetSelector.loadModel = function (roomType) {
		console.log('Chargement ' + roomType + ' model.');
		this.setBusyState(true, 'Chargement des produits...');
		$.ajax({
			url: PATHS.models[roomType],
			type: 'POST',
			dataType: 'json',
			error: function (xhr, textStatus, errorThrown) {
				FaucetSelector.setBusyMessage(
					'Une erreur s\047est produite pendant le chargement des produits.<br />' +
					'Veuillez essayer de nouveau plus tard.<br />' +
					'Code d\047erreur&nbsp;: ' + xhr.status
				);
			},
			success: function (data, textStatus) {
				FaucetSelector.setBusyState(false);
				model = data;
				$(FaucetSelector).trigger('modelReady');
				console.log('Loaded ' + roomType + ' model.');
				console.log(model.length + ' Products: ', model);
			}
		});
	};

	// Showers/Hiders

	FaucetSelector.show = function () {
		if (!overlay.contentBox) {
			$.get(PATHS.template, function (html) {
				overlay.contentBox = $(html).hide().appendTo('body').get(0);
				states.initialized = true;
				show();
			});
		} else {
			show();
		}

		function show () {
			overlay.show();
		}
	};
	FaucetSelector.hide = function () {
		overlay.hide();
	};

	FaucetSelector.showCriteria = function () {
		$([this.ui.main.body.criteria.container,
			this.ui.main.body.criteria_navigation.container]).show();

		states.criteriaHidden = false;
	};
	FaucetSelector.hideCriteria = function () {
		$([this.ui.main.body.criteria.container,
			this.ui.main.body.criteria_navigation.container]).hide();

		states.criteriaHidden = true;
	};

	FaucetSelector.showNoResults = function () {
		$(this.ui.main.body.no_results).show();
	};
	FaucetSelector.hideNoResults = function () {
		$(this.ui.main.body.no_results).hide();
	};

	FaucetSelector.showProducts = function () {
		$([	this.ui.main.body.product_information.container,
			this.ui.main.body.products.container,
			this.ui.main.body.slider.container]).show();

		this.updateGalleryLink();

		// Adjust the slider
		$(this.ui.main.body.slider.track).slider('option', 'max', states.products.length - 1);
		this.setSliderValue(Math.floor(states.products.length / 2));
	};
	FaucetSelector.hideProducts = function () {
		$([	this.ui.main.body.product_information.container,
			this.ui.main.body.products.container,
			this.ui.main.body.slider.container]).hide();
	};

	// Flow

	FaucetSelector.gotoIntro = function () {
		$(this.ui.intro.container)
			.css('display', 'block')
			.stop()
			.fadeTo(FADE_SPEED, 1);

		$(this.ui.main.container)
			.css('display', 'block')
			.stop()
			.fadeTo(FADE_SPEED, 0, function () {
				$(this).css('display', 'none');
			});
	};
	FaucetSelector.gotoMain = function () {
		// Reset any previously set options
		this.resetOptions();

		// Activate the first criteria
		this.setActiveTab(this.ui.main.header.tabs[0]);

		// Show the Main container
		$(this.ui.main.container)
			.css('display', 'block')
			.stop()
			.fadeTo(FADE_SPEED, 1);

		// Hide the Intro container
		$(this.ui.intro.container)
			.css('display', 'block')
			.stop()
			.fadeTo(FADE_SPEED, 0, function () {
				$(this).css('display', 'none');
			});

		// Show criteria window
		this.showCriteria();
	};

	// Handlers

	FaucetSelector.handleGetResultsClick = function (e) {
		this.updateProducts();
		this.showProducts();
		this.hideCriteria();
		e.preventDefault();
	};

	FaucetSelector.handleOptionClicks = function (e) {
		var option = e.currentTarget;

		if (!$(option).hasClass(DISABLED)) {
			this.setOption(option.rel, this.getHashValue(option));
		}

		e.preventDefault();
	};

	FaucetSelector.handleModelReady = function (e) {
		this.gotoMain();
	};

	FaucetSelector.handleNextTabClick = function (e) {
		var tabs = this.ui.main.header.tabs,
			currentIndex = this.getTabIndex(this.getActiveTab()),
			newIndex = (currentIndex + 2 > tabs.length) ? 0 : currentIndex + 1;

		console.log('Next tab is:', tabs[newIndex]);

		this.setActiveTab(tabs[newIndex]);

		e.preventDefault();
	};

	FaucetSelector.handleSliderSlide = function (e, ui) {
		this.setSliderValue(ui.value);
		this.slideTo(this.getSliderValue());
	};
	FaucetSelector.handleSliderStop = function (e) {
		// this.updateSliderValue();
		// this.slideTo(this.getSliderValue());
		console.log('Stopped slider on: ' + this.getSliderValue());
	};

	FaucetSelector.handleStartOverClick = function (e) {
		this.gotoIntro();
	};

	FaucetSelector.handleTabClick = function (e) {
		var tab = e.currentTarget;
		this.setActiveTab(tab);
	};

	FaucetSelector.handleTemplateReady = function (e) {
		this.build();
	};

	// Getters & Setters

	FaucetSelector.getActiveCriteria = function () {
		return states.activeCriteria;
	};
	FaucetSelector.setActiveCriteria = function (criteria) {
		// Make sure the criteria window is actually being shown
		this.showCriteria();

		if (states.activeCriteria === criteria) {
			return false;
		}

		if (states.activeCriteria !== null) {
			$(states.activeCriteria).removeClass(ACTIVE_CLASS + 'Criteria');
		}

		$(criteria).addClass(ACTIVE_CLASS + 'Criteria');

		states.activeCriteria = criteria;

		console.log('Set "Active Criteria" to: ', criteria);

		// Attempt to set the appropriate tab if necessary
		this.setActiveTab(this.getTabByCriteria(criteria));

		// Show appropriate content based on roomType
		$('[class$="-specific"]', criteria).hide();
		$('.' + states.roomType + '-specific', criteria).show();

		// Update the available options for this tab
		this.updateAvailableOptions();
	};

	FaucetSelector.getActiveTab = function () {
		return states.activeTab;
	};
	FaucetSelector.setActiveTab = function (tab) {
		// Make sure the criteria window is actually being shown
		this.showCriteria();

		if (states.activeTab === tab && states.criteriaHidden !== true) {
			return false;
		}

		if (states.activeTab !== null) {
			$(states.activeTab).removeClass(ACTIVE_CLASS);
		}

		$(tab).addClass(ACTIVE_CLASS);

		states.activeTab = tab;

		console.log('Set "Active Tab" to: ', tab);

		// Attempt to set the appropriate criteria if necessary
		this.setActiveCriteria(this.getCriteriaByTab(tab));

	};

	FaucetSelector.getBusyMessage = function () {
		return states.busyMessage;
	};
	FaucetSelector.setBusyMessage = function (message) {
		this.ui.busyMessage.innerHTML = message;

		console.log('Set "Busy Message" to: ', message);
	};

	FaucetSelector.getBusyState = function () {
		return states.busy;
	};
	FaucetSelector.setBusyState = function (busy, message) {
		if (busy) {
			$(this.ui.container).addClass(BUSY_CLASS);
			$([this.ui.intro.container, this.ui.main.container]).fadeTo(FADE_SPEED, 0);
		} else {
			$(this.ui.container).removeClass(BUSY_CLASS);
		}

		if (message) {
			this.setBusyMessage(message);
		} else {
			this.setBusyMessage('Loading...');
		}
		console.log('Set "Busy State" to: ', busy);
	};

	FaucetSelector.getOption = function (option) {
		return states.options[option];
	};
	FaucetSelector.setOption = function (option, value, visibilityToggle) {
		var setContainer;

		if (value === NO_PREFERENCE) {
			states.options[option] = null;
		} else if (states.options[option] === value) {
			return;
		} else {
			states.options[option] = value;
		}

		console.log('Current options: ', states.options);

		// Find the option set container
		$('.set', this.ui.main.body.criteria.items).each(function () {
			if ($('.options a[rel="' + option + '"]', this).length > 0) {
				setContainer = this;
			}
		});

		$('.options .' + ACTIVE_CLASS, setContainer).removeClass(ACTIVE_CLASS);
		$('.options a[hash="#' + this.normalizeHashValue(value) + '"]', setContainer).parent().addClass(ACTIVE_CLASS);

		$('.support .' + ACTIVE_CLASS, setContainer).removeClass(ACTIVE_CLASS);
		$('.support .' + Moen.util.String.uglify(value), setContainer).addClass(ACTIVE_CLASS);

		// Yield the browser
		setTimeout(function () {
			FaucetSelector.updatePreferenceDisplay();
		}, 0);

		this.updateAvailableOptions(visibilityToggle || false);
		this.updateProducts();

		console.log('Set option "' + option + '" to: "' + Moen.util.String.uglify(value) + '"');
	};

	FaucetSelector.getProductInformation = function () {
		return states.productInformation;
	};
	FaucetSelector.setProductInformation = function (name, price) {
		states.productInformation = {
			name: name,
			price: price
		};

		this.ui.main.body.product_information.name.innerHTML = name;
		this.ui.main.body.product_information.price.innerHTML = '*$' + Moen.lang.round(price, 2);
	};

	FaucetSelector.getRoomType = function () {
		return states.roomType;
	};
	FaucetSelector.setRoomType = function (roomType) {
		// Track the roomType the user selected
		Moen.util.Omniture.trackData({
			name: 'S\351lecteur de robinet',
			type: 'o',
			data: {
				eVar13: roomType
			}
		});

		$(this.ui.main.container)
			.removeClass(states.roomType)
			.addClass(roomType);

		states.roomType = roomType;

		this.loadModel(roomType);

		console.log('Set "Room Type" to: ', roomType);
	};

	FaucetSelector.getSliderValue = function () {
		return states.productIndex.current;
	};
	FaucetSelector.setSliderValue = function (value) {
		states.productIndex.last = this.getSliderValue();
		states.productIndex.current = value;

		$(this.ui.main.body.slider.track).slider('value', value);
		this.slideTo(value);
	};

	// Helpers

	FaucetSelector.getCriteriaByTab = function (tab) {
		return $($('a', tab).attr('hash')).get(0);
	};

	FaucetSelector.getHashValue = function (anchor) {
		var hashValue = $(anchor).attr('hash').replace(/^#(.*)$/, '$1').replace(/%20/g, ' ');
		return hashValue;
	};
	FaucetSelector.normalizeHashValue = function (hash) {
		if ($.browser.safari) {
			hash = hash.replace(/ /g, '%20');
		}

		return hash;
	};

	FaucetSelector.getTabByCriteria = function (criteria) {
		return $('a[hash=#' + criteria.id + ']', this.ui.main.header.tabs).parent().get(0);
	};
	FaucetSelector.getTabByOption = function (option) {
		if (typeof cache.tabByOption[option] === 'undefined') {
			var id = null,
				criteriaId = null;
			$('.criteria', this.ui.main.body.criteria.container).each(function () {
				criteriaId = this.id;
				$('a', this).each(function () {
					if (this.rel === option) {
						id = criteriaId;
					}
				});
			});
			return cache.tabByOption[option] = $('a[hash=#' + id + ']', this.ui.main.header.tabs).parent().get(0);
		} else {
			return cache.tabByOption[option];
		}
	};
	FaucetSelector.getTabIndex = function (tab) {
		var tabs = this.ui.main.header.tabs;

		for (var i = 0, j = tabs.length; i < j; i = i + 1) {
			if (tab === tabs[i]) {
				return i;
			}
		}

		return null;
	};

	// Miscellaneous

	FaucetSelector.resetOptions = function () {
		for (var option in states.options) {
			this.setOption(option, NO_PREFERENCE, true);
		}

		this.hideNoResults();
		this.hideProducts();
	};

	FaucetSelector.slideTo = function (index, animate) {
		if (index === states.productIndex.last) return;

		if (typeof animate === 'undefined') {
			animate = true;
		}

		var $products = $(this.ui.main.body.products.items),
			middle = Math.floor($products.length / 2),
			direction = (states.productIndex.last < states.productIndex.current) ? 1 : -1;

		// Define image dimensions if they haven't already been defined
		if (imageDimensions.length === 0) {
			for (var i = 0, j = $products.length; i < j; i = i + 1) {
				var $el = $products.eq(i);
				imageDimensions.push({
					top: $el.position().top,
					left: $el.position().left,
					height: $el.height(),
					width: $el.width()
				});
			}
			$products.css('position', 'absolute');
			console.log('Defined image dimensions: ', imageDimensions);
		}

		// Update product information display
		this.setProductInformation(states.products[index].collection, states.products[index].rolledUpPrice);

		// Update slider indicator position and information
		this.updateSliderIndicatorInformation();

		// Update image sources and begin animation
		for (var i = 0, j = $products.length; i < j; i = i + 1) {
			var relativeIndex = index - middle + i;
			if (relativeIndex >= 0 && relativeIndex < states.products.length) {
				$products.eq(i)
					.css('visibility', 'visible')
					.find('a')
						.attr('href', states.products[relativeIndex].url);

				with ({i: i, relativeIndex: relativeIndex}) {
					setTimeout(function () {
						$products.eq(i).find('a').find('img')
							.attr('src', '')
							.attr('src', states.products[relativeIndex].imageUrl);
					}, 1);
				}

				if (typeof imageDimensions[i + direction] !== 'undefined' && animate === true) {
					$products.eq(i).stop().css({
						top: imageDimensions[i + direction].top,
						left: imageDimensions[i + direction].left,
						height: imageDimensions[i + direction].height,
						width: imageDimensions[i + direction].width
					}).animate({
						top: imageDimensions[i].top,
						left: imageDimensions[i].left,
						height: imageDimensions[i].height,
						width: imageDimensions[i].width
					}, SLIDE_SPEED);
				}
			} else {
				$products.eq(i).css('visibility', 'hidden');
			}
		}
	};

	FaucetSelector.updateAvailableOptions = function (visibilityToggle) {
		/*
			Begin checking the tabs options to see if they'll
			return a resultset > 0 based on the current preferences set
		*/
		$('.set a', states.activeCriteria).each(function () {
			if (FaucetSelector.getHashValue(this) !== NO_PREFERENCE) {
				var options = $.extend(true, {}, states.options),
					thisOption = this.rel,
					thisValue = FaucetSelector.getHashValue(this),
					dataset = [];

				options[thisOption] = thisValue;

				dataset = new Moen.lang.Enumerable(model).findByAttribute(options).data;

				if (dataset.length === 0) {
					$(this).addClass(DISABLED);
					if (visibilityToggle) {
						$(this).parent().hide();
					}
				} else {
					$(this).removeClass(DISABLED);
					$(this).parent().show();
				}
			}
		});
	};

	FaucetSelector.updateGalleryLink = function () {
		var href = PATHS.gallery + '?filters=',
			obj = {},
			SEPARATOR = '|',
			ASSIGNMENT_OPERATOR = ':',
			gallery = this.ui.main.body.slider.gallery;

		href += ENDECA.location + ASSIGNMENT_OPERATOR + ENDECA.locations[states.roomType];

		for (var i in states.options) {
			obj[ENDECA[i]] = states.options[i];
		}

		var options = Moen.util.Object.serialize(obj, SEPARATOR, ASSIGNMENT_OPERATOR);
		if (options.length > 0) {
			href += SEPARATOR + options;
		}

		gallery.results.innerHTML = states.products.length;
		gallery.link.href = href;

		console.log($(gallery.container).width() / 2);
		$(gallery.container).show().css('left', ($(this.ui.main.body.slider.container).width() / 2) - ($(gallery.container).width() / 2));
	};

	FaucetSelector.updatePreferenceDisplay = function () {
		for (var option in states.options) {
			var tab = this.getTabByOption(option);
			states.preferences[tab.id] = states.preferences[tab.id] || {};
			if (states.options[option] !== null) {
				states.preferences[tab.id][option] = states.options[option];
			} else {
				states.preferences[tab.id][option] = null;
			}
		}

		for (var tab in states.preferences) {
			var display = [];
			for (var preference in states.preferences[tab]) {
				if (states.preferences[tab][preference] !== null) {
					display.push(states.preferences[tab][preference]);
				}
			}
			if (display.length === 0) {
				$('#' + tab + ' .preference span').get(0).innerHTML = 'Pas de pr\351f\351rence';
			} else {
				display = display.join(' + ');
				$('#' + tab + ' .preference span').get(0).innerHTML = display;
			}
		}
	};

	FaucetSelector.updateProducts = function () {
		// Find products in the model that match the options state
		states.products = new Moen.lang.Enumerable(model).findByAttribute(states.options).data;

		console.log('Matched ' + states.products.length + ' products.');

		// Make sure products were found
		if (states.products.length === 0) {
			console.error('No products to show.');
		}
	};

	FaucetSelector.updateSliderIndicatorInformation = function () {
		var $handle = $('a', this.ui.main.body.slider.track),
			$indicator = $(this.ui.main.body.slider.indicator),
			$galleryLink = $(this.ui.main.body.slider.gallery.container);

		$indicator.css('left', $handle.position().left - (($indicator.width() - $handle.width()) / 2))
			.text(Number(states.productIndex.current + 1) + ' de ' + states.products.length)
			.stop();

		if ($.browser.msie) {
			$galleryLink.hide();
			$indicator.show().animate({dummy: 1}, 1700, function () {
				$(this).hide();
				$galleryLink.show();
			});
		} else {
			$galleryLink.stop().fadeTo((1 - $galleryLink.css('opacity')) * FADE_SPEED, 0);
			$indicator.fadeTo((1 - $indicator.css('opacity')) * FADE_SPEED, 1, function () {
				$(this).fadeTo(1300, 1, function () {
					$(this).fadeTo(FADE_SPEED, 0);
					$galleryLink.show().stop().fadeTo(FADE_SPEED, 1);
				});
			});
		}
	};

	if (Moen.env.getQueryParameter('fs')) {
		$(function () {
			FaucetSelector.initialize();
		});
	}

	return FaucetSelector;
}());

/**
 * TODO: Add description
 * @namespace Moen.ui
 * @class FilterList
 */
Moen.ui.FilterList = function (list, target, options) {
	var FilterList = $(this),
	defaultOptions = {
		'fade': 0
	},
	filters = ['all'],
	a = [],
	ul = $('<ul></ul>'),
	x = 0;

	options = $.extend(true, {}, defaultOptions, options);

	list.each(function (index, item) {
		a = $.merge(a, $(item).attr('class').split(' '));
	});

	filters = $.merge(filters, a.unique());
	function triggerChanged() {
		x = x + 1;
		if (x >= list.length) {
			x = 0;
			FilterList.trigger('changed');
		}
	}

	$.each(filters, function (index, item) {
		var li = $('<li><a href="#' + item + '">' + item + '</a></li>');
		li.click(function (event) {
			event.preventDefault();
			$('li', ul).removeClass('selected');
			$(this).addClass('selected');
			if (item === 'all') {
				list.show();
				x = list.length;
				FilterList.trigger('changed');
			} else {
				$.each(list, function (n, i) {
					var product = $(i);
					if (item === 'all') {
						product.show();
						triggerChanged();
					} else {
						if (product.hasClass(item) === false) {
							product.fadeOut(options.fade, function () {
								triggerChanged();
							});
						} else {
							product.fadeIn(options.fade, function () {
								triggerChanged();
							});
						}
					}
				});
			}
		});

		ul.append(li);

	});

	target.append(ul);
	$('li', ul).filter(':first').click();

	return FilterList;
};

/**
 * TODO: Add description
 * @namespace Moen.ui
 * @class Lights
 */
Moen.ui.Lights = function (options) {
	var Lights = $(this),
		defaultOptions = {
			id: 'lights',
			speed: 500,
			opacity: 0.5,
			clickToClose: true
		},
		mask,
		doc = $(document),
		settings = $.extend(true, {}, defaultOptions, options);

	Lights.on = function () {
		mask.fadeOut(settings.speed, function () {
			mask.hide();
			Lights.trigger('on');
		});
	};

	Lights.off = function () {
		mask.css({
			'opacity': 0,
			'height': doc.height()
		}).show().fadeTo(settings.speed,
			settings.opacity,
			function () {
				Lights.trigger('off');
			});
		};

	Lights.handleClick = function (e) {

	};

	if($('#' + settings.id).length > 0) {
		mask = $('#' + settings.id);
		mask.hide();
	} else {
		mask = $('<div id="' + settings.id + '"></div>');
		mask.hide().appendTo($('body'));
	}
	return Lights;
};

/**
 * TODO: Add description
 * @namespace Moen.ui
 * @class Overlay
 */
Moen.ui.Overlay = function (options) {
	var Overlay = {},
		defaults = {
			closeButtonSelector: null,
			contentBox: null,
			destroyOnHide: false,
			hideOnEscape: true,
			hideOnMaskClick: true,
			maskClass: 'ui-overlay-mask',
			overlayClass: 'ui-overlay',
			transition: 'fade',
			useMask: true
		},
		events = Moen.ui.Overlay.events,
		fadeSpeed = 200,
		isOpen = false,
		settings = {},
		that = Overlay;

	settings = $.extend(true, defaults, options);

	Overlay.contentBox = settings.contentBox;

	// Priviledged
	Overlay.initialize = function () {
		$('body').append(this.contentBox);
		this.maskClass = settings.maskClass;
		this.overlayClass = settings.overlayClass;
	};

	Overlay.destroy = function () {
		$(this.mask).remove();
		$(this.contentBox).remove();

		this.mask = this.contentBox = null;

		$(Overlay).trigger(events.DESTROY);
	};

	Overlay.show = function () {
		if (isOpen || !this.contentBox) {
			return;
		}

		isOpen = true;

		$(that.contentBox).addClass(that.overlayClass);

		// Build mask if necessary
		if (settings.useMask && !that.mask) {
			that.mask = $('<div/>').addClass(that.maskClass).get(0);
			$(that.mask)
				.bind('click', Moen.util.Event.createDelegate(that, handleMaskClick))
				.hide()
				.prependTo($('body'));

			/*
			 * We need to listen for a window resize in IE6 since
			 * it does not recognize "position: fixed" in the CSS
			 */
			if ($.browser.msie && $.browser.version == '6.0') {
				$(that.contentBox).css('margin-top', '0');
				$(window).bind('resize', handleWindowResize);
				resizeMask();
			}
		}

		// Determine what action to take with animation and the mask
		var maskOpacity = 0.7;
		switch (settings.transition) {
		case 'fade':
			if (settings.useMask) {
				// If the transition setting is set to fade and we're using a mask, first fade in the content and immediately after the mask
				$(that.contentBox).fadeTo(0, 0).show().fadeTo(fadeSpeed, 1, function () {
					$(that.mask).fadeTo(0, 0).show().fadeTo(fadeSpeed, maskOpacity);
					checkOverlayPosition();
				});
			} else {
				// If the transition setting is set to fade and we're NOT using a mask, fade in the contentBox
				$(that.contentBox).fadeTo(0, 0).show().fadeTo(fadeSpeed, 1, function () {
					checkOverlayPosition();
				});
			}
			break;
		default:
			// If we're using a mask but no transition, show both the mask
			if (settings.useMask) {
				$(that.mask).fadeTo(0, maskOpacity).show();
			}
			// Regardless, show the contentBox
			$(that.contentBox).show();
			break;
		}

		$('body, html').addClass('modal');

		// Setup the escape key hiding if the setting is set to true
		if (settings.hideOnEscape) {
			$(document).bind('keydown', keydownHandler);
		}

		$(Overlay).trigger(events.SHOW);
	};

	Overlay.hide = function () {

		if (!isOpen) {
			return;
		}

		isOpen = false;

		switch (settings.transition) {
		case 'fade':
			$(this.contentBox).fadeOut(fadeSpeed, Moen.util.Event.createDelegate(this, function () {
				if (settings.useMask) {
					$(this.mask).fadeOut(fadeSpeed, function () {
						finish();
					});
				} else {
					finish();
				}
			}));
			break;
		default:
			$(this.contentBox).hide();
			if (settings.useMask) {
				$(this.mask).hide();
				finish();
			}
			break;
		}

		if ($.browser.msie && $.browser.version == '6.0') {
			$(window).unbind('resize', handleWindowResize);
		}

		function finish () {
			$('body, html').removeClass('modal');

			if (settings.destroyOnHide) {
				Overlay.destroy();
			}

			$(Overlay).trigger(events.HIDE);
		}
	};

	/*
	 * Checks to see if the overlay is taller than the browser's viewport.
	 * If so, we disable the behavior where the modal follows the user while scrolling.
	 */
	function checkOverlayPosition () {
		var overlayContainer = $(Overlay.contentBox);
		if (overlayContainer.height() > ((document.documentElement && document.documentElement.clientHeight) ? document.documentElement.clientHeight : window.innerHeight)) {
			overlayContainer.css({
				marginTop: 0,
				position: 'absolute',
				top: (self.pageYOffset) ? self.pageYOffset : document.documentElement.scrollTop
			});
		}
	}

	function handleMaskClick (event) {
		if (settings.hideOnMaskClick) {
			this.hide();
		}
	}

	function handleWindowResize (event) {
		resizeMask();
	}

	function keydownHandler (event) {
		if (event.keyCode === 27) {
			that.hide();
			$(document).unbind('keydown', keydownHandler);
		}
	}

	function resizeMask () {
		var $doc = $(document);
		$(Overlay.mask)
			.height($doc.height())
			.width($doc.width());
	}

	Overlay.initialize();

	return Overlay;
};
Moen.ui.Overlay.events = {
	DESTROY: 'destroy.overlay',
	HIDE: 'hide.overlay',
	LOAD: 'load.overlay',
	SHOW: 'show.overlay'
};

/**
 * TODO: Add description
 * @namespace Moen.ui
 * @class ProductSlider
 */
Moen.ui.ProductSlider = function (container, options) {
	var ProductSlider = $(this),
	defaultOptions = {
		'speed': 500,
		'increment': 3
	},
	products = $('div.products ul li, container'),
	productList = $('div.products ul', container),
	slider = $('div.ui-slider, container'),
	backward = $('div.controls div.backward a, container'),
	forward = $('div.controls div.forward a, container');

	options = $.extend(true, {}, defaultOptions, options);

	function getProductWidth() {
		return $('li:first', productList).width();
	}

	function setListWidth() {
		var listWidth = 0;
		products.each(function (index, item) {
			if ($(item).css('display') === 'block') {
				listWidth = listWidth + $(item).width();
			}
		});
		productList.css('width', listWidth);
		return listWidth;
	}

	function getMaxSliderValue() {
		var maxSliderValue = setListWidth() - slider.outerWidth();
		if (maxSliderValue <= 0) {
			maxSliderValue = 0;
		}
		return maxSliderValue;
	}

	ProductSlider.reset = function () {
		slider.slider('option', 'max', getMaxSliderValue());
		slider.slider('value', 0);
	};

	container.addClass('js');

	slider.slider({
		orientation: 'horizontal',
		slide: function (event, object) {
			productList.css('left', (object.value) * -1 + 'px');
		},
		change: function (event, object) {
			productList.stop();
			// productList.animate({'left': (object.value) * -1 + 'px'}, options.speed * options.increment);
		}
	});

	ProductSlider.reset();

	backward.click(function (event) {
		event.preventDefault();
		slider.slider('value', slider.slider('value') - getProductWidth() * options.increment);
	});

	forward.click(function (event) {
		event.preventDefault();
		slider.slider('value', slider.slider('value') + getProductWidth() * options.increment);
	});

	return ProductSlider;

};

/**
 * TODO: Add description
 * @namespace Moen.ui
 * @class Slideshow
 */
Moen.ui.Slideshow = function (slideshow, options) {
	var Slideshow = $(this),
		defaultOptions = {
			'speed': 5000,
			'transition': 250,
			'updateHash': false,
			'slides': $('.slide', slideshow)
		},
		interval,
		initialSlide;

	options = $.extend(true, {}, defaultOptions, options);

	function updateHash(hash) {
		if (options.updateHash) {
			window.location.hash = hash;
		}
	}

	function prev() {
		var slideId = Slideshow.getCurrentSlide().attr('id');
		if (options.slides.index(options.slides.filter('#' + slideId)) === 0) {
			return options.slides.filter(function(index) {
				return index === options.slides.length - 1;
			});
		} else {
			return $(options.slides.eq(options.slides.index(options.slides.filter('#' + slideId)) - 1));
		}
	}

	function next() {
		var slideId = Slideshow.getCurrentSlide().attr('id');
		if (options.slides.index(options.slides.filter('#' + slideId)) === options.slides.length - 1) {
			return options.slides.filter(':first-child');
		} else {
			return $(options.slides.eq(options.slides.index(options.slides.filter('#' + slideId)) + 1));
		}
	}

	Slideshow.getCurrentSlide = function () {
		return options.slides.filter('.selected');
	};

	Slideshow.getCurrentSlideIndex = function () {
		var currentSlideIndex = 0;
		options.slides.each(function (index, item) {
			if ($(item).hasClass('selected') === true) {
				currentSlideIndex = index;
			}
		});
		return currentSlideIndex;
	};
	Slideshow.gotoSlide = function (slide, speed) {
		if (slide.hasClass('selected') === false) {
			Slideshow.getCurrentSlide().fadeOut(speed, function () {
				$(this).removeClass('selected');
				slide.addClass('selected').removeClass('next');
				updateHash(slide.attr('id'));
				Slideshow.trigger('slideChanged');
				Slideshow.start();
			});
			slide.addClass('next').fadeIn('slow');
		}
	};

	Slideshow.next = function () {
		Slideshow.gotoSlide(next(), options.transition);
	};

	Slideshow.prev = function () {
		Slideshow.gotoSlide(prev(), options.transition);
	};

	Slideshow.start = function () {
		if (options.speed > -1) {
			if (interval > 0) {
				Slideshow.stop();
			}
			interval = window.setInterval(Slideshow.next, options.speed);
		}
	};

	Slideshow.stop = function () {
		window.clearInterval(interval);
	};

	slideshow.addClass('js');

	slideshow.mousemove(function (event) {
		Slideshow.start();
	});

	if (window.location.hash === '') {
		initialSlide = options.slides.filter(':first');
		updateHash(options.slides.filter(':first').attr('id'));
	} else {
		initialSlide = $(window.location.hash, slideshow);
	}

	initialSlide.addClass('selected');

	return Slideshow;
};

/**
 * TODO: Add description
 * @namespace Moen.ui
 * @class Tooltip
 */
Moen.ui.Tooltip = function (target, content, options) {
	var Tooltip = $(this),
	defaultOptions = {
		delay: 300,
		fade: 250,
		offset: 4,
		position: 'right' // Can be top, right, bottom or left
	},
	iframe,
	hideTimeout,
	showTimeout,
	wrapper = $('#tip-wrapper').get(0),
	visible = false;

	options = $.extend(true, {}, defaultOptions, options);

	// Lazily create tip wrapper (and iframe)
	if ($.browser.msie && $.browser.version == '6.0' || true) {
		iframe = $('#tip-wrapper-iframe').get(0);
		if (typeof iframe === 'undefined') {
			iframe = $('<iframe id="tip-wrapper-iframe" src="about:blank" frameBorder="0" scrolling="no" />').get(0);
			$(iframe).hide().appendTo('body');
		}
	}

	if (typeof wrapper === 'undefined') {
		wrapper = $('<div id="tip-wrapper"><div id="tip-content"></div></div>').get(0);
		$(wrapper).hide().appendTo('body');
	}

	Tooltip.show = function () {
		if (visible === false) {
			showTimeout = window.setTimeout(function () {
				var targetOffset = $(target).offset(),
					leftPosition = 0,
					topPosition = 0,
					padding = (Modernizr.boxshadow) ? 0 : 6;

				$(wrapper).find('#tip-content').html(content);
				wrapper.className = wrapper.className.replace(/align-[\w]+/, '');
				$(wrapper).addClass('align-' + options.position);

				switch (options.position) {
				case 'top':
					topPosition = targetOffset.top - $(wrapper).height() - options.offset + padding;
					leftPosition = horizontalCenter();
					break;
				case 'right':
					topPosition = verticalCenter();
					leftPosition = targetOffset.left + $(target).width() + options.offset + ($(target).hasClass('icon-right') ? 24 : 0) + padding;
					break;
				case 'bottom':
					topPosition = targetOffset.top + $(target).height() + padding;
					leftPosition = horizontalCenter();
					break;
				case 'left':
					topPosition = verticalCenter();
					leftPosition = targetOffset.left - options.offset - $(wrapper).width() + padding;
					break;
				}

				function verticalCenter () {
					return targetOffset.top + padding + ($(target).height() / 2) - ($(wrapper).height() / 2);
				}

				function horizontalCenter () {
					return targetOffset.left + padding + ($(target).width() / 2) - ($(wrapper).width() / 2);
				}

				if (iframe) {
					$(iframe).show();
				}

				$(wrapper).css({
					left: leftPosition,
					top: topPosition
				});

				if (iframe !== null) {
					$(iframe).css({
						left: leftPosition,
						top: topPosition,
						height: $(wrapper).height(),
						width: $(wrapper).width()
					});
				}

				$(wrapper).unbind();
				$(wrapper).hover(function (event) {
					cancelHide();
				}, function (event) {
					Tooltip.hide();
				});

				$(wrapper).fadeIn(options.fade);

				visible = true;
			}, options.delay * 2);
		}
	};

	Tooltip.hide = function () {
		hideTimeout = setTimeout(function () {
			$(wrapper).fadeOut(options.fade, function () {
				if (iframe) {
					$(iframe).hide();
				}
			});
			hideTimeout = null;
			visible = false;
		}, options.delay);
	};

	function cancelHide () {
		window.clearTimeout(hideTimeout);
		hideTimeout = null;
	}

	$(target).hover(function () {
		Tooltip.show();
		cancelHide();
	}, function () {
		if (visible) {
			Tooltip.hide();
		}
		window.clearTimeout(showTimeout);
	});

	return Tooltip;
};

/**
 * TODO: Add description
 * @namespace Moen.ui
 * @class VideoPlayer
 */
Moen.ui.VideoPlayer = (function () {

	var VideoPlayer = function (options) {

		var VideoPlayer = {},
			defaults = {
				container: null,
				autoPlay: false,
				height: 240,
				width: 320,
				flv: null,
				image: 'http://fr.moen.ca/enfr/assets/moencom/plugins/design-videos/Kitche_Trend_Image_sm.jpg',
				autoDispose: false
			},
			settings = $.extend(true, defaults, options),
			videoPlayer = null;

		VideoPlayer = {
			container: settings.container,
			initialize: function () {
				/*
				 * Check to see if another instance with the same container exists and destroy it
				 */
				for (var i = 0; i < _instances.length; i++) {
					if (_instances[i].container === settings.container) {
						_instances[i].destroy();
						_instances.splice(i, 1);
					}
				}

				// Create the SWF instance
				var config = {
					swf: 'http://fr.moen.ca/enfr/assets/moencom/plugins/video-player/video-player.swf',
					height: settings.height,
					width: settings.width,
					flashvars: {
						account: Moen.util.Omniture.account
				  },
					params: {
						allowScriptAccess: 'always',
						allowfullscreen: 'true',
						scale: 'noscale',
						salign: 'lt',
						menu: 'false',
						wmode: 'opaque'
					}
				};

				if ($.browser.msie) {
					config.id = 'swf' + Moen.util.Date.microtime();
				}

				$(settings.container).flash(config);

				$(settings.container).flash(function () {
					var that = this,
						interval = setInterval(function () {
							if (that.initAkamaiPlayer) {
								that.initAkamaiPlayer({
									src: settings.flv,
									height: settings.height,
									width: settings.width,
									fullscreenHeight: settings.height,
									fullscreenWidth: settings.width,
									loadImage: settings.image,
									endEvent: 'affiche',
                                                                        scaleMode: '\351tirer',
									mode: 'recouvrement',
									enableFullscreen: false,
									autostart: settings.autoPlay,
									initAtX: 0,
									initAtY: 0,
									fullscreenTo: 'navigateur',
									showTime: false
								});

								window.clearInterval(interval);

								$(VideoPlayer).trigger('ready');
							}
						}, 50);
				});

				// Add this instance to the static instances array
				_instances.push(VideoPlayer);
			},
			destroy: function () {
				$(this.container).empty();
				for (var i = 0, j = _instances.length; i < j; i = i + 1) {
					if (_instances[i] === this) {
						_instances.splice(i, 1);
					}
				}
			},
			play: function () {
				// pauseAll();
				$(settings.container).flash(function () {
					this.resumeAkamaiPlayer();
					$(VideoPlayer).trigger('played');
				});
			},
			pause: function () {
				$(settings.container).flash(function () {
					this.pauseAkamaiPlayer();
					$(VideoPlayer).trigger('paused');
				});
			}
		};

		// Initialize the video player
		VideoPlayer.initialize();

		return VideoPlayer;
	},
	_instances = [];

	function pauseAll () {
		for (var i = 0; i < _instances.length; i++) {
			_instances[i].pause();
		}
	}

	return VideoPlayer;

}());

///////////////////////////////////////////////////////////////////////////////
//	Utilities
///////////////////////////////////////////////////////////////////////////////
/**
 * A collection of useful utilities.
 * @module util
 * @title Utilities
 */
Moen.util = {};

/**
 * TODO: Add description
 * @namespace Moen.util
 * @class AjaxHelper
 */
Moen.util.AjaxHelper = function () {

	var AjaxHelper = $(this),
	ajaxResponse = '',
	interval = 100;

	AjaxHelper.html = function (url, target, options) {
		var defaultOptions = {
			'requestType': 'get',
			'speed': 250,
			'id': ''
		},
		ajax,
		ajaxResponse = '';

		options = $.extend(true, {}, defaultOptions, options);

		ajax = $.ajax({
			'type': options.requestType,
			'url': url,
			'success': function (response) {
				ajaxResponse = response;
			}
		});

		if(options.speed > 0){
			target.fadeOut(options.speed, function () {
				target.empty();
				target.addClass('ajax-loader');
				target.fadeIn(options.speed, function () {
					var timeout = window.setInterval(function () {
						if(ajaxResponse !== '') {
							window.clearInterval(timeout);
							target.fadeOut(options.speed, function () {
								target.removeClass('ajax-loader');
								target.html(ajaxResponse);
								AjaxHelper.trigger('success', [options.id]);
								target.fadeIn(options.speed);
							});
						}
					}, interval);
				});
			});
		} else {
			var timeout = window.setInterval(function () {
				if(ajaxResponse !== '') {
					window.clearInterval(timeout);
					target.html(ajaxResponse);
					AjaxHelper.trigger('success', [options.id]);
				}
			}, interval);
			target.empty();
		}
	};

	return AjaxHelper;

};

/**
 * The Cookie utility.
 * @namespace Moen.util
 * @class Cookie
 * @static
*/
Moen.util.Cookie = {
	get: function (key) {
		var cookies = document.cookie.split(/;|\|/),
			cookie;

		for (var i = 0, j = cookies.length; i < j; i = i + 1) {
			cookie = cookies[i].split('=');
			if ($.trim(cookie[0]) === key) {
				return cookie[1];
			}
		}

		return null;
	},
	set: function (key, value, options) {
		var cookie,
			options = $.extend(false, {}, options);

		cookie = key + '=' + escape(value) +
			((options.expires) ? ';expires=' + options.expires : '') +
			((options.path) ? ';path=' + options.path : '') +
			((options.domain) ? ';domain=' + options.domain : '') +
			((options.secure === true) ? ';secure' : '');

		document.cookie = cookie;
	}
};

/**
 * Date utilities.
 * @namespace Moen.util
 * @class Date
 * @static
*/
Moen.util.Date = {
	microtime: function () {
		return new Date().getTime();
	}
};

/**
 * The Event utility.
 * @namespace Moen.util
 * @class Event
 * @static
 */
Moen.util.Event = {
	/**
	 * A helper method that creates a function to call the specified function
	 * within the context of the specified object.
	 * @method createDelegate
	 * @param context {Object} The object that will become <code>this</code>
	 * when the specified function is called.
	 * @param func {Function} The function to be called.
	 * @return {Function}
	 */
	createDelegate: function (context, func) {
		return function () {
			// Call function within context of specified object
			func.apply(context, arguments);
		};
	},
	/**
	 * Dummy event handler that prevents default event behavior.
	 * @method dummyEventHandler
	 * @param e {Event}
	 */
	dummyEventHandler: function (e) {
		e.preventDefault();
	}
};

/**
 * TODO: Add description
 * @namespace Moen.util
 * @class EventManager
 */
Moen.util.EventManager = function () {
	var EventManager = $(this),
	currentEvent,
	observers = $();

	EventManager.observe = function (expr) {
		observers = observers.add(expr);
	};

	EventManager.ignore = function (expr) {
		observers = observers.remove(expr);
	};

	EventManager.dispatch = function (event, target, value) {
		currentEvent = event;
		observers.trigger(event, [target, value]);
		EventManager.trigger(event, [target, value]);
	};

	EventManager.getCurrentEvent = function () {
		return currentEvent;
	};

	EventManager.saveState = function () {
		window.location.hash = currentEvent;
	};

	EventManager.loadState = function () {
		EventManager.setState(window.location.hash);
	};

	return EventManager;
};

/**
 * TODO: Add description
 * @namespace Moen.util
 * @class FormHelper
 * @static
 */
Moen.util.FormHelper = {
	setupAutoTabber: function (form) {
		var allowedKeys = /^9$|^16$|^17$|^18$|^27$|^224$/,
			inputs = $('input', form).get();

		function getInputIndex (input) {
			for (var i = 0, j = inputs.length; i < j; i = i + 1) {
				if (inputs[i] === input) {
					return i;
				}
			}
			return false;
		}

		$('input', form).each(function () {
			if (this.maxLength !== -1) {
				$(this).keyup(function (e) {
					// Make sure the length of the field equals the maxlength allowed and the key pressed isn't one of the allowed keys
					if ($(this).val().length === this.maxLength && !String(e.keyCode).match(allowedKeys)) {
						$(this).blur();

						if (this.defaultValue !== this.value) {
							$(this).change();
						}

						// Find the next input
						var i = getInputIndex(this) + 1;
						var type = inputs[i].type;
						while (type == "hidden" && i < inputs.length-1){
						    type = $(inputs[i++]).type;
						}
						$(inputs[i]).focus().select();
					}
				});
			}
		});
	},
	setupDefaultValueHandler: function (form) {
		$('input.replaceable', form).each(function () {
			this.value = $(this).attr('placeholder');
		});

		$('input.replaceable', form).bind('focus', function () {
			if (this.value === $(this).attr('placeholder')) {
				this.value = '';
			}
		});

		$('input.replaceable', form).bind('blur', function () {
			if ($.trim(this.value) === '') {
				this.value = $(this).attr('placeholder');
			}
		});
	},
	setupInputStyler: function (form) {
		$('input', form).bind('focus', function () {
			$(this).addClass('input-text-focus');
		});

		$('input', form).bind('blur', function () {
			$(this).removeClass('input-text-focus');
		});
	}
};

/**
 * The Form Validator utility.
 * @namespace Moen.util
 * @class FormValidator
 * @constructor
 * @param form {HTMLElement} DOM element reference of a form
 * @param elements {Object}
 */
Moen.util.FormValidator = function (form, elements) {
	var FormValidator = {},
	patterns = {
		email: /([\w-\.]+)@((?:[\w]+\.)+)([a-zA-Z]{2,4})/,
		trim: /^[ \t]+|[ \t]+$/,
		zip: (function () {
			switch (Moen.env.countryLocale.toUpperCase()) {
			case 'US':
				return (/[0-9]{5}([[-]|\" \"]{0,1}[0-9]{4}){0,1}/);
				break;
			case 'CA':
				return (/[a-ceghj-npr-tvxy][0-9][a-ceghj-npr-tv-z][\" \"]{0,1}[0-9][a-ceghj-npr-tv-z][0-9]/i);
				break;
			}
		}())
	},
	// Validators
	hasMinLength = function (str, length) {
		if (str.length >= length) {
			return true;
		}
		return false;
	},
	hasMaxLength = function (str, length) {
		if (str.length <= length) {
			return true;
		}
		return false;
	},
	isEmail = function (str) {
		if (str.match(patterns.email) !== null) {
			return true;
		}
		return false;
	},
	isEmpty = function (dom) {
		str = dom.value.replace(patterns.trim, '');
		if (str !== '' !== dom.value) {
			return true;
		}
		return false;
	},
	isZIP = function (str) {
		console.log(patterns.zip, str.toString());
		if (patterns.zip.test(str.toString()) === true) {
			return true;
		}
		return false;
	},
	run = function (validator) {
		var self = validator,
		returns = {
			areValid: true,
			elements: {
				failed: [],
				passed: []
			},
			form: self.form
		};

		for (var i in self.elements) {
			// Create temporary variable for current form element
			var element = $('[name="' + i + '"]', self.form).get(0);

			// Validate current element
			if (validate(element, self.elements[i]) === true) {
				returns.elements.passed.push(element);
			} else {
				returns.elements.failed.push(element);
			}
		}

		// Set entire form validation status (convenience variable)
		returns.areValid = returns.elements.failed.length > 0 ? false : true;

		return returns;
	},
	validate = function (dom, options) {
		// Get elements value
		var value = $(dom).val();

		// Begin checking options
		if (
				(options.email === true && !isEmail(value)) ||
				(typeof options.minLength !== 'undefined' && !hasMinLength(value, options.minLength)) ||
				(typeof options.maxLength !== 'undefined' && !hasMaxLength(value, options.maxLength)) ||
				(options.required === true && !isEmpty(dom)) ||
				(options.zip === true && !isZIP(value))
			) {
			return false;
		}

		return true;
	};

	// Build FormValidator object
	FormValidator = {
		form: form,
		elements: elements,
		run: function () {
			return run(this);
		},
		handleSubmit: function (e) {
			var results = this.run(),
				canSubmit = true;

			if (results.areValid === false) {
				canSubmit = this.handleFailure(results);
			} else {
				canSubmit = this.handleSuccess(results);
			}

			if (!canSubmit) {
				e.preventDefault();
			}
		},
		handleFailure: function (results) {
			return false;
		},
		handleSuccess: function (results) {
			return true;
		}
	};

	// Auto-bind submit handler to form validator object
	$(FormValidator.form).submit(Moen.util.Event.createDelegate(FormValidator, FormValidator.handleSubmit));

	// Return validator object
	return FormValidator;
};

/**
 * TODO: Add description
 * @namespace Moen.util
 * @class ImagePreloader
 */
Moen.util.ImagePreloader = {
	load: function (images) {
		var collection = [];

		if (typeof images === 'string') {
			collection.push(images);
		} else if (Moen.lang.typeOf(images) === 'array') {
			collection = images;
		}

		for (var i = 0, j = collection.length; i < j; i = i + 1) {
			$('<img/>').attr('src', collection[i]);
		}
	}
};

/**
 * Contains a set of utilities used with Number.
 * @namespace Moen.util
 * @class Number
 * @static
 */
Moen.util.Number = {};

/**
 * Contains a set of utilities used with Objects.
 * @namespace Moen.util
 * @class Object
 * @static
 */
Moen.util.Object = {
	getNumberOfProperties: function (object) {
		if (object.__count__ !== undefined) {
			return object.__count__;
		} else {
			var count = 0;
			for (property in object) {
				if (object.hasOwnProperty(property)) {
					count++;
				}
			}
			return count;
		}
	},
	/**
	 * Serializes a one-level <code>Object</code> into a <code>String</code>.
	 * @param {Object} object The <code>Object</code> to be serialized
	 * @param {String} separator Defaults to "," (optional)
	 * @param {String} assignmentOperator Defaults to "=" (optional)
	 * @param {Array} whitelist A list of acceptable keys to be serialized (optional)
	 * @return {Object} Returns an <code>Object</code> serialized based on the
	 * options provided.
	 */
	serialize: function (object, separator, assignmentOperator, whitelist) {
		var serializedObject = '',
			TOKEN = ''; // Chose an obscure token to prevent accidental matching
		separator = separator || ',';
		assignmentOperator = assignmentOperator || '=';

		if (whitelist) {
			whitelist = TOKEN + whitelist.join(TOKEN) + TOKEN;
		}

		for (var property in object) {
			if ('string|number|boolean'.indexOf(typeof object[property]) !== -1
				&& (!whitelist || (whitelist && whitelist.indexOf(TOKEN + property + TOKEN) !== -1))) {
				serializedObject += separator + property + assignmentOperator + object[property];
			}
		}
		return serializedObject.substring(1);
	}
};

/**
 * Contains a set of utilities used with Strings.
 * @namespace Moen.util
 * @class String
 * @static
 */
Moen.util.String = {
	/**
	 * Transforms a messy <code>String</code> into a pretty formatted one.<br>
	 * This is a convenience method for removing all hyphens and then
	 * tranforming the <code>String</code> into titlecase.
	 * See inverse method:
	 * <a href="#method_uglify">Moen.util.String.uglify</a>
	 * @method beautify
	 * @param {String} str The <code>String</code> to be transformed.
	 * @return {String} Returns a pretty formatted <code>String</code>.
	 */
	beautify: function (str) {
		str = str.replace(/-/g, ' ');
		return this.toTitleCase(str);
	},
	/**
	 * See inverse method:
	 * <a href="#method_beautify">Moen.util.String.beautify</a>
	 * @method uglify
	 */
	uglify: function (str) {
		return str
			.replace(' & ', ' et ')
			.replace(/[^a-zA-Z0-9_]/g, '-')
			.replace(/^-|-$/g, '')
			.toLowerCase()
			.replace(/~[^-a-z0-9_]+~/g, '')
			.replace(/[-]+/g, '-')
			.replace(/[_]+/g, '_');
	},
	/**
	 * TODO: Add description
	 * @method explodeURL
	 */
	explodeURL: function (url) {
		var regexp = /^(([^:\/\?#]+):)?(\/\/([^\/\?#]*))?([^\.\?#]*)(\.([^\?#]*))?(\?([^#]*))?(#(.*))?/,
			exploded = regexp.exec(url),
			urlFragments = {
				scheme: exploded[2],
				authority: exploded[4],
				path: exploded[5],
				extension: exploded[7],
				query: {},
				fragment: exploded[11]
			};

		var queryPieces = exploded[9];

		if (queryPieces) {
			var chunk;

			queryPieces = queryPieces.split('&');

			for (var i = 0, j = queryPieces.length; i < j; i = i + 1) {
				chunk = queryPieces[i].split('=');
				urlFragments.query[chunk[0]] = chunk[1];
			}
		}

		return urlFragments;
	},
	/**
	 * TODO: Add description
	 * @method implodeURL
	 */
	implodeURL: function (obj) {
		var url = '';
		if (obj['scheme'] && obj['authority']) {
			url += obj['scheme'] + '://' + obj['authority'];
		}
		url += obj['path'];
		if (obj['extension']) {
			url += '.' + obj['extension'];
		}
		if (Moen.util.Object.getNumberOfProperties(obj['query']) > 0) {
			url += '?' + Moen.util.Object.serialize(obj['query'], '&', '=');
		}
		if (obj['fragment']) {
			url += '#' + obj['fragment'];
		}
		return url;
	},
	/**
	 * Transforms a <code>String</code> into titlecase.
	 * @author David Gouch <http://individed.com>
	 * @method toTitleCase
	 * @param {String} str The <code>String</code> to be transformed.
	 * @return {String} Returns a <code>String</code> transformed into titlecase.
	 */
	toTitleCase: function (str) {
		return str.replace(/([\w&`'‘’"“.@:\/\{\(\[<>_]+-? *)/g, function(match, p1, index, title) {
			if (index > 0 && title.charAt(index - 2) !== ":" &&
				match.search(/^(a(nd?|s|t)?|b(ut|y)|en|for|i[fn]|o[fnr]|t(he|o)|vs?\.?|via)[ \-]/i) > -1)
				return match.toLowerCase();
			if (title.substring(index - 1, index + 1).search(/['"_{(\[]/) > -1)
				return match.charAt(0) + match.charAt(1).toUpperCase() + match.substr(2);
			if (match.substr(1).search(/[A-Z]+|&|[\w]+[._][\w]+/) > -1 ||
				title.substring(index - 1, index + 1).search(/[\])}]/) > -1)
				return match;
			return match.charAt(0).toUpperCase() + match.substr(1);
		});
	}
};

/**
 * Contains a set of utilities used on the <code>window</code> object.
 * @namespace Moen.util
 * @class Window
 */
Moen.util.Window = {
	/**
	 * @method open
	 * @param {String} url
	 * @param {String} name
	 * @param {Object} options A configuration <code>Object</code>.
	 */
	open: function (url, name, options) {
		var defaultOptions = {
			height: window.screen.availHeight - 100,
			width: window.screen.availWidth - 50,
			location: 'yes',
			status: 'yes',
			toolbar: 'yes',
			menubar: 'yes',
			directories: 'yes',
			scrollbars: 'yes'
		};

		name = name || 'window' + Moen.util.Date.microtime();

		options = $.extend(false, options, defaultOptions);

		var settings = Moen.util.Object.serialize(options, ',', '=', ['status', 'toolbar', 'location', 'menubar', 'directories', 'resizable', 'scrollbars', 'height', 'width']);

		window.open(url, name, settings);
	}
};

///////////////////////////////////////////////////////////////////////////////
//	Native object extensions
///////////////////////////////////////////////////////////////////////////////
Array.prototype.indexOf = function (value, options) {
	var i,
	defaultOptions = {
		'start': 0
	};

	options = $.extend(true, {}, defaultOptions, options);

	for (i = options.start; i < this.length; i = i + 1) {
		if (this[i] === value) {
			return i;
		}
	}

	return -1;
};
Array.prototype.unique = function () {
	var i,
	tempArray = [],
	length = this.length;

	for (i = 0; i < length; i = i + 1) {
		if (tempArray.indexOf(this[i]) < 0) {
			tempArray.push(this[i]);
		}
	}

	return tempArray;
};


