(function () {
	window.DEBUG = document.cookie.indexOf('wmdebug') > -1;

	window.$ = document.querySelector.bind(document);
	window.$all = document.querySelectorAll.bind(document);
	var alphaNum = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';

	// https://github.com/substack/semver-compare/blob/master/index.js
	function semverCompare(a, b) {
		var pa = a.split('.');
		var pb = b.split('.');
		for (var i = 0; i < 3; i++) {
			var na = Number(pa[i]);
			var nb = Number(pb[i]);
			if (na > nb) { return 1; }
			if (nb > na) { return -1; }
			if (!isNaN(na) && isNaN(nb)) { return 1; }
			if (isNaN(na) && !isNaN(nb)) { return -1; }
		}
		return 0;
	}

	function generateRandomId(len) {
		var length = len || 10;
		var id = '';
		for (var i = length; i--;) {
			id += alphaNum[~~(Math.random() * alphaNum.length)];
		}
		return id;
	}

	function onButtonClick(btn, listener) {
		btn.addEventListener('click', function buttonClickListener(e) {
			listener(e);
			return false;
		});
	}

	function log() {
		if (window.DEBUG) {
			console.log(...arguments);
		}
	}

	// Generate 2 ASTs for the code to be inserted in loops for infinite run protection.
	// The `myVar` variable names would be changed later for every insertion.
	function getLoopProtectorBlocks() {
		var ast1 = esprima.parse('var myvar = Date.now();');
		var ast2 = esprima.parse('while(a){if (Date.now() - a787897 > 1000) { window.top.previewException(new Error("Infinite loop")); break;}}');
		return {
			before: ast1.body[0],
			inside: ast2.body[0].body.body[0]
		}
	}

	/**
	 * Add timed limit on the loops found in the passed AST body
	 * @param {ASTBody}	Body of an AST generated by esprima or any ES compliant AST
	 */
	function addInfiniteLoopProtection(astBody) {
		if (!astBody) { return; }
		if (!Array.isArray(astBody)) {
			addInfiniteLoopProtection(astBody.body);
			return;
		}
		var el, randomVariableName, insertionBLocks;

		for (var i = astBody.length; i--;) {
			el = astBody[i];
			if (el && el.type === 'ForStatement' || el.type === 'WhileStatement' || el.type === 'DoWhileStatement') {
				randomVariableName = '_' + generateRandomId(3);
				insertionBLocks = getLoopProtectorBlocks();
				insertionBLocks.before.declarations[0].id.name = insertionBLocks.inside.test.left.right.name = randomVariableName;
				// Insert time variable assignment
				astBody.splice(i, 0, insertionBLocks.before);
				// If the loop's body is a single statement, then convert it into a block statement
				// so that we can insert our conditional break inside it.
				if (!Array.isArray(el.body)) {
					el.body = {
						body: [ el.body ],
						type: 'BlockStatement'
					};
				}
				// Insert IfStatement
				el.body.body.unshift(insertionBLocks.inside);
			}
			if (el.body) {
				addInfiniteLoopProtection(el.body);
			}
		}
	}

	function getHumanDate(timestamp) {
		var d = new Date(timestamp);
		var retVal = d.getDate() + ' '
			+ [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'][d.getMonth()] + ' '
			+ d.getFullYear();
		return retVal;
	}

	// create a one-time event
	function once(node, type, callback) {
		// create event
		node.addEventListener(type, function(e) {
			// remove event
			e.target.removeEventListener(type, arguments.callee);
			// call handler
			return callback(e);
		});

	}

	window.utils = {
		semverCompare: semverCompare,
		generateRandomId: generateRandomId,
		onButtonClick: onButtonClick,
		addInfiniteLoopProtection: addInfiniteLoopProtection,
		getHumanDate: getHumanDate,
		log: log,
		once: once
	};
})();
