define([
	"jquery",
	"underscore",
	"storage",
	"crel",
	"xregexp",
	"stacktrace",
	"FileSaver"
], function($, _, storage, crel, XRegExp, printStackTrace, saveAs) {

	var utils = {};

	utils.msie = (function() {
		/**
		 * IE 11 changed the format of the UserAgent string.
		 * See http://msdn.microsoft.com/en-us/library/ms537503.aspx
		 */
		var msie = parseInt((/msie (\d+)/.exec(navigator.userAgent.toLowerCase()) || [])[1], 10);
		if (isNaN(msie)) {
			msie = parseInt((/trident\/.*; rv:(\d+)/.exec(navigator.userAgent.toLowerCase()) || [])[1], 10);
		}
		return msie;
	})();

	utils.urlResolve = (function() {
		var urlParsingNode = document.createElement("a");
		return function urlResolve(url) {
			var href = url;

			if (utils.msie) {
				// Normalize before parse.  Refer Implementation Notes on why this is
				// done in two steps on IE.
				urlParsingNode.setAttribute("href", href);
				href = urlParsingNode.href;
			}

			urlParsingNode.setAttribute('href', href);

			// urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils
			return {
				href: urlParsingNode.href,
				protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '',
				host: urlParsingNode.host,
				search: urlParsingNode.search ? urlParsingNode.search.replace(/^\?/, '') : '',
				hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '',
				hostname: urlParsingNode.hostname,
				port: urlParsingNode.port,
				pathname: (urlParsingNode.pathname.charAt(0) === '/') ?
					urlParsingNode.pathname : '/' + urlParsingNode.pathname
			};
		};
	})();

	// Faster than setTimeout (see http://dbaron.org/log/20100309-faster-timeouts)
	utils.defer = (function() {
		var timeouts = [];
		var messageName = "deferMsg";
		window.addEventListener("message", function(evt) {
			if(evt.source == window && evt.data == messageName) {
				evt.stopPropagation();
				if(timeouts.length > 0) {
					timeouts.shift()();
				}
			}
		}, true);
		return function(fn) {
			timeouts.push(fn);
			window.postMessage(messageName, "*");
		};
	})();

	// Implements underscore debounce using our defer function
	utils.debounce = function(func, context) {
		var isExpected = false;

		function later() {
			isExpected = false;
			func.call(context);
		}

		return function() {
			if(isExpected === true) {
				return;
			}
			isExpected = true;
			utils.defer(later);
		};
	};

	// Generates a 24 chars length random string (should be enough to prevent collisions)
	utils.randomString = (function() {
		var max = Math.pow(36, 6);

		function s6() {
			// Linear [0-9a-z]{6} random string
			return ('000000' + (Math.random() * max | 0).toString(36)).slice(-6);
		}

		return function() {
			return [
				s6(),
				s6(),
				s6(),
				s6()
			].join('');
		};
	})();

	// Return a parameter from the URL
	utils.getURLParameter = function(name) {
		// Parameter can be either a search parameter (&name=...) or a hash fragment parameter (#!name=...)
		var regex = new RegExp("(?:\\?|\\#\\!|&)" + name + "=(.+?)(?:&|\\#|$)");
		try {
			return decodeURIComponent(regex.exec(location.search + location.hash)[1]);
		}
		catch(e) {
			return undefined;
		}
	};

	// Transform a selector into a jQuery object
	function jqElt(element) {
		if(_.isString(element) || !element.val) {
			return $(element);
		}
		return element;
	}

	// For input control
	function inputError(element, event) {
		if(event !== undefined) {
			element.stop(true, true).addClass("error").delay(1000).queue(function() {
				$(this).removeClass("error");
				$(this).dequeue();
			});
			event.stopPropagation();
		}
	}

	// Return input value
	utils.getInputValue = function(element) {
		element = jqElt(element);
		return element.val();
	};

	// Set input value
	utils.setInputValue = function(element, value) {
		element = jqElt(element);
		element.val(value);
	};

	// Return input text value
	utils.getInputTextValue = function(element, event, validationRegex) {
		element = jqElt(element);
		var value = element.val();
		if(value === undefined) {
			inputError(element, event);
			return undefined;
		}
		// trim
		value = utils.trim(value);
		if((value.length === 0) || (validationRegex !== undefined && !value.match(validationRegex))) {
			inputError(element, event);
			return undefined;
		}
		return value;
	};

	// Return input number value
	function getInputNumValue(isFloat, element, event, min, max) {
		element = jqElt(element);
		var value = utils.getInputTextValue(element, event);
		if(value === undefined) {
			return undefined;
		}
		value = isFloat ? parseFloat(value) : parseInt(value, 10);
		if(isNaN(value) || (min !== undefined && value < min) || (max !== undefined && value > max)) {
			inputError(element, event);
			return undefined;
		}
		return value;
	}

	// Return input integer value
	utils.getInputIntValue = _.partial(getInputNumValue, false);

	// Return input float value
	utils.getInputFloatValue = _.partial(getInputNumValue, true);

	// Return input value and check that it's a valid RegExp
	utils.getInputRegExpValue = function(element, event) {
		element = jqElt(element);
		var value = utils.getInputTextValue(element, event);
		if(value === undefined) {
			return undefined;
		}
		try {
			new RegExp(value);
		}
		catch(e) {
			inputError(element, event);
			return undefined;
		}
		return value;
	};

	// Return input value and check that it's a valid JavaScript object
	utils.getInputJsValue = function(element, event) {
		element = jqElt(element);
		var value = utils.getInputTextValue(element, event);
		if(value === undefined) {
			return undefined;
		}
		try {
			/*jshint evil:true */
			eval("var test=" + value);
			/*jshint evil:false */
		}
		catch(e) {
			inputError(element, event);
			return undefined;
		}
		return value;
	};

	// Return checkbox boolean value
	utils.getInputChecked = function(element) {
		element = jqElt(element);
		return element.prop("checked");
	};

	// Set checkbox state
	utils.setInputChecked = function(element, checked) {
		element = jqElt(element);
		element.prop("checked", checked).change();
	};

	// Get radio button value
	utils.getInputRadio = function(name) {
		return $("input:radio[name=" + name + "]:checked").prop("value");
	};

	// Set radio button value
	utils.setInputRadio = function(name, value) {
		$("input:radio[name=" + name + "][value=" + value + "]").prop("checked", true).change();
	};

	// Reset input control in all modals
	utils.resetModalInputs = function() {
		$(".modal input[type=text]:not([disabled]), .modal input[type=password], .modal textarea").val("");
		$(".modal input[type=checkbox]").prop("checked", false).change();
	};

	// Basic trim function
	utils.trim = function(str) {
		return $.trim(str);
	};

	// Slug function
	var nonWordChars = XRegExp('[^\\p{L}\\p{N}-]', 'g');
	utils.slugify = function(text) {
		return text.toLowerCase().replace(/\s/g, '-') // Replace spaces with -
			.replace(nonWordChars, '') // Remove all non-word chars
			.replace(/\-\-+/g, '-') // Replace multiple - with single -
			.replace(/^-+/, '') // Trim - from start of text
			.replace(/-+$/, ''); // Trim - from end of text
	};

	// Check an URL
	utils.checkUrl = function(url, addSlash) {
		if(!url) {
			return url;
		}
		if(url.indexOf("http") !== 0) {
			url = "http://" + url;
		}
		if(addSlash && url.indexOf("/", url.length - 1) === -1) {
			url += "/";
		}
		return url;
	};

	// Create the modal element and add to the body
	utils.addModal = function(id, content) {
		var modal = crel('div', {
			class: 'modal ' + id
		});
		modal.innerHTML = content;
		document.body.appendChild(modal);
	};

	// Create a backdrop and add to the body
	utils.createBackdrop = function(parent) {
		var result = crel('div', {
			'class': 'modal-backdrop fade'
		});
		parent = parent || document.body;
		parent.appendChild(result);
		result.offsetWidth; // force reflow
		result.className = result.className + ' in';
		result.removeBackdrop = function() {
			result.className = 'modal-backdrop fade';
			setTimeout(function() {
				result.parentNode.removeChild(result);
			}, 150);
		};
		return result;
	};

	var openedTooltip;
	utils.createTooltip = function(selector, content) {
		_.each(document.querySelectorAll(selector), function(tooltipElt) {
			var $tooltipElt = $(tooltipElt);
			$tooltipElt.tooltip({
				html: true,
				container: $tooltipElt.parents('.modal-content'),
				placement: 'right',
				trigger: 'manual',
				title: content
			}).click(function() {
				var elt = this;
				if(openedTooltip && openedTooltip[0] === elt) {
					return;
				}
				utils.defer(function() {
					openedTooltip = $(elt).tooltip('show');
				});
			});
		});
	};

	// Create an centered popup window
	utils.popupWindow = function(url, title, width, height) {
		var left = (screen.width / 2) - (width / 2);
		var top = (screen.height / 2) - (height / 2);
		return window.open(url, title, [
			'toolbar=no, ',
			'location=no, ',
			'directories=no, ',
			'status=no, ',
			'menubar=no, ',
			'scrollbars=no, ',
			'resizable=no, ',
			'copyhistory=no, ',
			'width=' + width + ', ',
			'height=' + height + ', ',
			'top=' + top + ', ',
			'left=' + left
		].join(""));
	};

	// Shows a dialog to force the user to click a button before opening oauth popup
	var redirectCallbackConfirm;
	var redirectCallbackCancel;
	utils.redirectConfirm = function(message, callbackConfirm, callbackCancel) {
		redirectCallbackConfirm = callbackConfirm;
		redirectCallbackCancel = callbackCancel;
		$('.modal-redirect-confirm .redirect-msg').html(message);
		$('.modal-redirect-confirm').modal("show");
	};

	utils.init = function() {
		$('.action-redirect-confirm').click(function() {
			redirectCallbackCancel = undefined;
			redirectCallbackConfirm();
		});
		$('.modal-redirect-confirm').on('hidden.bs.modal', function() {
			_.defer(function() {
				redirectCallbackCancel && redirectCallbackCancel();
			});
		});
		$(document).on('click', function() {
			// Close opened tooltip if any
			openedTooltip && openedTooltip.tooltip('hide');
			openedTooltip = undefined;
		});
		$(document).on('click', '.tooltip', function(evt) {
			// Avoid tooltip to close when clicking inside
			evt.stopPropagation();
		});
	};

	var entityMap = {
		"&": "&amp;",
		"<": "&lt;",
		//">": "&gt;",
		'"': '&quot;',
		"'": '&#39;',
		"/": '&#x2F;',
		"\u00a0": ' '
	};

	// Escape HTML entities
	utils.escape = function(str) {
		return String(str).replace(/[&<"'\/\u00a0]/g, function(s) {
			return entityMap[s];
		});
	};

	// Export data on disk
	utils.saveAs = function(content, filename) {
		if(saveAs !== undefined && !/constructor/i.test(window.HTMLElement) /* safari does not support saveAs */) {
			if(_.isString(content)) {
				content = new Blob([
					content
				], {
					type: "text/plain;charset=utf-8"
				});
			}
			saveAs(content, filename);
		}
		else {
			if(_.isString(content)) {
				var uriContent = "data:application/octet-stream;base64," + utils.encodeBase64(content);
				window.open(uriContent, 'file');
			}
			else {
				var reader = new FileReader();
				reader.onload = function(event) {
					utils.redirectConfirm('You are opening a PDF document.', function() {
						var uriContent = 'data:application/pdf;' + event.target.result.substring(event.target.result.indexOf('base64'));
						window.open(uriContent, 'file');
					});
				};
				reader.readAsDataURL(content); // Convert the blob to base64
			}
		}
	};

	// Time shared by others modules
	utils.updateCurrentTime = function() {
		utils.currentTime = Date.now();
	};
	utils.updateCurrentTime();

	// Serialize sync/publish attributes and store it in the storage
	utils.storeAttributes = function(attributes) {
		var storeIndex = attributes.syncIndex || attributes.publishIndex;
		// Don't store sync/publish index
		var storedAttributes = _.omit(attributes, "syncIndex", "publishIndex", "provider");
		// Store providerId instead of provider
		storedAttributes.provider = attributes.provider.providerId;
		storage[storeIndex] = JSON.stringify(storedAttributes);
	};

	// Retrieve/parse an index array from storage
	utils.retrieveIndexArray = function(storeIndex) {
		try {
			return _.compact(storage[storeIndex].split(";"));
		}
		catch(e) {
			storage[storeIndex] = ";";
			return [];
		}
	};

	// Append an index to an array in storage
	utils.appendIndexToArray = function(storeIndex, index) {
		storage[storeIndex] += index + ";";
	};

	// Remove an index from an array in storage
	utils.removeIndexFromArray = function(storeIndex, index) {
		storage[storeIndex] = storage[storeIndex].replace(";" + index + ";", ";");
	};

	// Retrieve/parse an object from storage. Returns undefined if error.
	utils.retrieveIgnoreError = function(storeIndex) {
		try {
			return JSON.parse(storage[storeIndex]);
		}
		catch(e) {
			return undefined;
		}
	};

	var eventList = [];
	utils.logValue = function(value) {
		eventList.unshift(value);
		if(eventList.length > 5) {
			eventList.pop();
		}
	};
	utils.logStackTrace = function() {
		eventList.unshift(printStackTrace());
		if(eventList.length > 5) {
			eventList.pop();
		}
	};
	utils.formatEventList = function() {
		var result = [];
		_.each(eventList, function(event) {
			result.push("\n");
			if(_.isString(event)) {
				result.push(event);
			}
			else if(_.isArray(event)) {
				result.push(event[5] || "");
				result.push(event[6] || "");
			}
		});
		return result.join("");
	};

	// Base64 conversion
	utils.encodeBase64 = function(str) {
		if(str.length === 0) {
			return "";
		}

		// UTF-8 to byte array
		var bytes = [], offset = 0, length, char;

		str = encodeURI(str);
		length = str.length;

		while(offset < length) {
			char = str[offset];
			offset += 1;

			if('%' !== char) {
				bytes.push(char.charCodeAt(0));
			}
			else {
				char = str[offset] + str[offset + 1];
				bytes.push(parseInt(char, 16));
				offset += 2;
			}
		}

		// byte array to base64
		var padchar = '=';
		var alpha = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';

		var i, b10;
		var x = [];

		var imax = bytes.length - bytes.length % 3;

		for(i = 0; i < imax; i += 3) {
			b10 = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
			x.push(alpha.charAt(b10 >> 18));
			x.push(alpha.charAt((b10 >> 12) & 0x3F));
			x.push(alpha.charAt((b10 >> 6) & 0x3f));
			x.push(alpha.charAt(b10 & 0x3f));
		}
		switch(bytes.length - imax) {
			case 1:
				b10 = bytes[i] << 16;
				x.push(alpha.charAt(b10 >> 18) + alpha.charAt((b10 >> 12) & 0x3F) + padchar + padchar);
				break;
			case 2:
				b10 = (bytes[i] << 16) | (bytes[i + 1] << 8);
				x.push(alpha.charAt(b10 >> 18) + alpha.charAt((b10 >> 12) & 0x3F) + alpha.charAt((b10 >> 6) & 0x3f) + padchar);
				break;
		}
		return x.join('');
	};

	// CRC32 algorithm
	var mHash = [
		0,
		1996959894,
		3993919788,
		2567524794,
		124634137,
		1886057615,
		3915621685,
		2657392035,
		249268274,
		2044508324,
		3772115230,
		2547177864,
		162941995,
		2125561021,
		3887607047,
		2428444049,
		498536548,
		1789927666,
		4089016648,
		2227061214,
		450548861,
		1843258603,
		4107580753,
		2211677639,
		325883990,
		1684777152,
		4251122042,
		2321926636,
		335633487,
		1661365465,
		4195302755,
		2366115317,
		997073096,
		1281953886,
		3579855332,
		2724688242,
		1006888145,
		1258607687,
		3524101629,
		2768942443,
		901097722,
		1119000684,
		3686517206,
		2898065728,
		853044451,
		1172266101,
		3705015759,
		2882616665,
		651767980,
		1373503546,
		3369554304,
		3218104598,
		565507253,
		1454621731,
		3485111705,
		3099436303,
		671266974,
		1594198024,
		3322730930,
		2970347812,
		795835527,
		1483230225,
		3244367275,
		3060149565,
		1994146192,
		31158534,
		2563907772,
		4023717930,
		1907459465,
		112637215,
		2680153253,
		3904427059,
		2013776290,
		251722036,
		2517215374,
		3775830040,
		2137656763,
		141376813,
		2439277719,
		3865271297,
		1802195444,
		476864866,
		2238001368,
		4066508878,
		1812370925,
		453092731,
		2181625025,
		4111451223,
		1706088902,
		314042704,
		2344532202,
		4240017532,
		1658658271,
		366619977,
		2362670323,
		4224994405,
		1303535960,
		984961486,
		2747007092,
		3569037538,
		1256170817,
		1037604311,
		2765210733,
		3554079995,
		1131014506,
		879679996,
		2909243462,
		3663771856,
		1141124467,
		855842277,
		2852801631,
		3708648649,
		1342533948,
		654459306,
		3188396048,
		3373015174,
		1466479909,
		544179635,
		3110523913,
		3462522015,
		1591671054,
		702138776,
		2966460450,
		3352799412,
		1504918807,
		783551873,
		3082640443,
		3233442989,
		3988292384,
		2596254646,
		62317068,
		1957810842,
		3939845945,
		2647816111,
		81470997,
		1943803523,
		3814918930,
		2489596804,
		225274430,
		2053790376,
		3826175755,
		2466906013,
		167816743,
		2097651377,
		4027552580,
		2265490386,
		503444072,
		1762050814,
		4150417245,
		2154129355,
		426522225,
		1852507879,
		4275313526,
		2312317920,
		282753626,
		1742555852,
		4189708143,
		2394877945,
		397917763,
		1622183637,
		3604390888,
		2714866558,
		953729732,
		1340076626,
		3518719985,
		2797360999,
		1068828381,
		1219638859,
		3624741850,
		2936675148,
		906185462,
		1090812512,
		3747672003,
		2825379669,
		829329135,
		1181335161,
		3412177804,
		3160834842,
		628085408,
		1382605366,
		3423369109,
		3138078467,
		570562233,
		1426400815,
		3317316542,
		2998733608,
		733239954,
		1555261956,
		3268935591,
		3050360625,
		752459403,
		1541320221,
		2607071920,
		3965973030,
		1969922972,
		40735498,
		2617837225,
		3943577151,
		1913087877,
		83908371,
		2512341634,
		3803740692,
		2075208622,
		213261112,
		2463272603,
		3855990285,
		2094854071,
		198958881,
		2262029012,
		4057260610,
		1759359992,
		534414190,
		2176718541,
		4139329115,
		1873836001,
		414664567,
		2282248934,
		4279200368,
		1711684554,
		285281116,
		2405801727,
		4167216745,
		1634467795,
		376229701,
		2685067896,
		3608007406,
		1308918612,
		956543938,
		2808555105,
		3495958263,
		1231636301,
		1047427035,
		2932959818,
		3654703836,
		1088359270,
		936918000,
		2847714899,
		3736837829,
		1202900863,
		817233897,
		3183342108,
		3401237130,
		1404277552,
		615818150,
		3134207493,
		3453421203,
		1423857449,
		601450431,
		3009837614,
		3294710456,
		1567103746,
		711928724,
		3020668471,
		3272380065,
		1510334235,
		755167117
	];
	utils.crc32 = function(str) {
		var n = 0, crc = -1;
		str.split('').forEach(function(char) {
			n = (crc ^ char.charCodeAt(0)) & 0xFF;
			crc = (crc >>> 8) ^ mHash[n];
		});
		crc = crc ^ (-1);
		if(crc < 0) {
			crc = 0xFFFFFFFF + crc + 1;
		}
		return crc.toString(16);
	};

	window.perfTest = function(cb) {
		var startTime = Date.now();
		for(var i = 0; i < 10000; i++) {
			cb();
		}
		console.log('Run 10,000 times in ' + (Date.now() - startTime) + 'ms');
	};

	return utils;
});
