denise / Dominos Pizza Voucher Codes

// ==UserScript==
// @name        Dominos Pizza Voucher Codes
// @description Finds voucher codes for dominos.com.au
// @match     	*://*.dominos.com.au/*
// @match     	*://dominos.com.au/*
// @version     5.3
// @require		http://code.jquery.com/jquery-latest.min.js
// @resource	turtle http://i.imgur.com/eH8Ci9N.png
// @copyright   jehan
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_deleteValue
// @grant       GM_listValues
// @grant       GM_addStyle
// @grant       GM_getResourceURL
// @grant       GM_xmlhttpRequest
// @grant       GM_registerMenuCommand
// @grant       GM_log
// @run-at		document-start
// ==/UserScript==

nThreads = 3;
max_retry = 25;
ajax_timeout = 10000;
check_interval = 50;
fileListURL = "http://goo.gl/Mrp9fr";
sendCodesURLs = ['http://goo.gl/7GQdVE', 'http://goo.gl/8JqQYP', 'http://goo.gl/xT9W1J', 'http://goo.gl/cP4DqO', 'http://goo.gl/g3VPhU', 'http://goo.gl/RUhyIJ', 'http://goo.gl/oCS5pe', 'http://goo.gl/0lWoLY', 'http://goo.gl/P6WqEK', 'http://goo.gl/VoeDZ3'];
updInterval = 1800000;
expiredCodes = [];
newCodes = [];
wrongCodes = [];
wrongDelivery = [];
sentCodes = [];

window.ajaxReq = ajaxReq, window.getBasket = getBasket, window.updateBasket = updateBasket, window.testCode = testCode, window.getOrderDetails = getOrderDetails, window.chkCode = chkCode; // GM weirdness
setLogging();
(function(){
	updateCodeData(function(codes){
		Log("updateCodeData() callback");
		if(typeof initCodes != 'undefined')
			initialiseCodes(codes);
	});
	setTimeout(arguments.callee, updInterval);
})();

if(window.location.pathname.split('/').pop()!='ProductMenu')
	return;

var consentVersion = 2;
if(GM_getValue('consent_code_sharing', 0) != consentVersion)
	alert("Privacy Statement:\n'Dominos Pizza Voucher Codes' userscript notifies the script author of new, incorrect and expired codes.\nThis process is completely anonymous.\nThe only information shared is the code, the store and whether it was a delivery or pickup.\nNo other personal details are shared, and it's impossible to even find out your IP address.\nIf you do not wish to participate, please uninstall this script immediately.");
GM_setValue('consent_code_sharing', consentVersion);

GM_addStyle("#voucher-button-container option { direction: ltr!important; }");
GM_addStyle("#voucher_select option:checked, #voucher_select option:hover { box-shadow: 0 0 10px 100px #9C9D9F inset; }");
GM_addStyle("#voucher_form > img { float: right; display: none; } #voucher_form > label { height:1.1em!important; display: inline-block!important; }");
GM_addStyle("#voucher-button-container { position: relative; } #voucher-button-container > img { position: absolute; height: 1.6em; z-index: -1; left: 54.3%; top: 0.25em; } #apply_voucher { float: right; }");
GM_addStyle("#voucher_code { width: 50%!important; } #voucher-button-container > * { display: inline!important; }");
GM_addStyle("#voucher-button-container select { color:#58595B; width: 13%; height: 1.6em; display: inline-block; direction: rtl; opacity: 0; }");

GM_addStyle(".noValidCodes { text-decoration: line-through!important; color: #999999; }");

GM_addStyle(".validCode { color: #227722; }");

if(window.navigator.userAgent.indexOf('WebKit')>0)
	GM_addStyle("#voucher-button-container select { direction: ltr!important; }");

GM_registerMenuCommand("Clear Cache", clearCache, 'C');

GM_registerMenuCommand("List Cache", function(){
	Log("Cache: [" + cloneInto(GM_listValues(), window).join(', ') + ']');	
	}, 'A'
);

GM_registerMenuCommand("Dump Cache", function(){
	var text = "Dump Cache:\n";
	var entries = cloneInto(GM_listValues(), window);
	for(var i = 0; i < entries.length; i++)
		text += entries[i] + ": " + GM_getValue(entries[i], '') + "\n";
	Log(text);
}, 'D');


GM_registerMenuCommand( "Toggle Logging", function(){
	var l = GM_getValue('logging', false) === 'true';
	setLogging(!l);
	alert('Logging is now ' + (l ? 'disabled' : 'enabled'));
	}, 'L'
);

document.addEventListener("DOMContentLoaded", function(){
	Log("Event: DOMContentLoaded");
	$("<img src='" + GM_getResourceURL('turtle') + "'>").error(function(){
		this.src = "http://i.imgur.com/eH8Ci9N.png";
	}).appendTo("#voucher-button-container");
	$("<select id='voucher_select' title='Find vouchers'/>").insertBefore('#apply_voucher').change(function(e){
		tryCodes(this.value);
		resetChoice();
	}).each(function(){
		Log("loadevent initialiseCodes()");
		initialiseCodes(loadCodes());	
	});

	$status = $('#voucher_form label');
	$spinner = $("<img src='/eStore/Resources/Images/ajax-loader.gif'>").insertAfter($status);
	
	scrollStatus("Tip: Most Traditional Pizza coupons can be used for Chef's Best or Mogul Pizzas");

	initUserCodes();
	initUnsafe();
});

function rndStr(){
	var s = '';
	for(var i = 0, l = Math.random()*5+5; i<l; i++)
		s += String.fromCharCode(Math.floor(Math.random()*26) + Math.round(Math.random()) * 32 + 65);
	return s;
}

function jQueryBorg(ajaxLog){
	window[ajaxLog] = [];
	require(["jquery", "basketUrls"], function ($, b) {
		var ajax_ = $.ajax;
		$.ajax = function(arr, a){
			if(arr.url.indexOf(b.VoucherApply)>=0){
				var oldSuccess = arr.success;
				var vc = 'voucherCode=';
				var code = parseInt(arr.url.substr(arr.url.indexOf(vc) + vc.length));
				arr.success = function(resp){
					window[ajaxLog].push({ code : code, resp: resp });
					return oldSuccess(resp);
				};
			};
			return ajax_(arr, a);
		};
	});
}

function initUnsafe(){ // unsafeWindow working unreliably as of FF 31.0
	runUnsafeFunction(function(basketName){
		require(['common/basket'], function(basket){ window[basketName] = basket; });
	}, [basketName = rndStr()]);
	runUnsafeFunction(jQueryBorg, [ajaxLog = rndStr()]);
	Log("ajaxLog = " + ajaxLog);
}

function runUnsafeCode(c){
	var id = rndStr();
	$("<script id='" + id + "'>(function(){var unsafeWindow = window; " + c + "; var s = document.getElementById('" + id + "'); s.parentNode.removeChild(s); })();</script>").appendTo(document.body);
}

function runUnsafeFunction(fn, args){
	if(typeof args == 'undefined')
		var args = [];
	else
		args = args.map(function(val){
			return typeof val == 'string' ? '"' + val.split('"').join('\"') + '"' : val;
		});
	var c = '(' + fn.toString() + ')(' + args.join(',') + ');';
	runUnsafeCode(c);
}

function setLogging(l){
	if(typeof l == 'undefined')
		var l = GM_getValue('logging', false) === 'true';
	GM_setValue('logging',  l ? 'true' : 'false');
	Log = l ? function(t){ console.log(t); return true;} : function(){}; // Need to wrap console.log so Chrome doesn't have a fit
}

function clearCache(){
	Log("clearCache()");
	var entries = cloneInto(GM_listValues(), window);
	for(var i = 0; i < entries.length; i++){
		Log("Deleting entry: " + entries[i]);
		GM_deleteValue(entries[i]);
	}
}

function resetChoice(){
	nullOption.selected = 'selected';
}

function initialiseCodes(codes){
	Log("initialiseCodes()");
	if(typeof delivery === 'undefined')
		return getOrderDetails(function(){
			initialiseCodes(codes);
		});
	if(codes === null)
		return initCodes = false;
	if((typeof initCodes === 'undefined') || (initCodes === false))
		initCodes = true;
	else
		return updCodes = codes;
		
	if('metadata' in codes)
		delete codes.metadata;

	if(delivery === false)
		mergeDupes(codes);
	window.codes = codes;
	window.deals = sortKeys(codes);
	window.prices = {};
	var voucherSelect = document.getElementById('voucher_select');
	var maxLen = getMaxLen();
	
	if(typeof nullOption == 'undefined'){
		window.nullOption = document.createElement('option');
		nullOption.value = '';
		voucherSelect.appendChild(nullOption);
		nullOption.style.display = 'none';
		resetChoice();
	}
	
	deals = deals.map(function(deal){
		return deal.replace(/\s{2,}/g, ' ');
	});
	
	for(var i = 0, lastOption = null; i < deals.length; i++){
		var deal = deals[i];
		Log("initialiseCodes() deal = " + deal);
		prices[deal] = sortIntKeys(codes[deal]);
		var lPrice = money(prices[deal][0]), hPrice = money(prices[deal][prices[deal].length-1]);
		var m = '$' + lPrice + (lPrice == hPrice ? '' : ('-$' + hPrice) );
		var text = insertBreaks(deal.replace('$', m), maxLen);
		var option = document.querySelector('option[value="' + deal + '"]');
		if(!option){
			option = document.createElement('option');
			option.value = deal;
			voucherSelect.insertBefore(option, lastOption && lastOption.nextElementSibling);
			lastOption = option;
		}
		if(option.innerHTML != text)
			option.innerHTML = text;
	}
	
	for(var i in deals){
		var deal = deals[i];
		for(var j in prices[deal])
			window.codes[deal][prices[deal][j]] = shuffle(window.codes[deal][prices[deal][j]]);
	}
	initCodes = false;
	if(typeof updCodes != 'undefined'){
		var nc = updCodes;
		delete updCodes;
		initialiseCodes(nc);
	}
	Log("initialiseCodes() exit");
}

function concatObj(obj1, obj2){
	for(var key in obj2){
		if(!obj2.hasOwnProperty(key))
			continue;
		if(key in obj1){
			if('length' in obj2[key])
				obj1[key] = obj1[key].concat(obj2[key]);
			else
				arguments.callee(obj1[key], obj2[key]);
		} else
			obj1[key] = obj2[key];
		delete obj2[key];
	}
}

function count(obj){
	var n = 0;
	if('length' in obj)
		return obj.length;
	for(var key in obj)
		if(obj.hasOwnProperty(key))
			n += count(obj[key]);
	return n;
}

function mergeDupes(codes){
	var deals = getKeys(codes), nDeals = deals.length, nCodes = count(codes);
	while(deal = deals.pop()){	
		var newDeal = deal.replace(/\s*Pick\S+/gi, '');
		if(deal == newDeal)
			continue;
		Log("mergeDupes() deal = " + deal + " newDeal = " + newDeal);
		if(newDeal in codes)
			concatObj(codes[newDeal], codes[deal]);
		else
			codes[newDeal] = codes[deal];
		delete codes[deal];
	}
	Log("mergeDupes() " + (nDeals - getKeys(codes).length) + " deals combined");
	Log("mergeDupes() Codes Start: " + nCodes + " Codes End: " + count(codes));
}

window.onresize = function(){
	var options = document.querySelectorAll('#voucher_select option');
	for(var i in options)
		options[i].innerHTML = insertBreaks(options[i].innerHTML, getMaxLen());
};

function getMaxLen(){
	var fs = parseFloat(window.getComputedStyle(document.getElementById('voucher_select')).fontSize);
	return Math.floor(2 * window.innerWidth / fs);
}

function insertBreaks(text, maxLen){
	if(typeof text == 'undefined')
		return '';
	text = text.split('<br>').join('');
	if(text.length <= maxLen)
		return text;
	var l = text.lastIndexOf(',', maxLen);
	if(l == -1)
		l = text.lastIndexOf(' ', maxLen);
	if(l == -1)
		return Log("insertBreaks() Error: Could not split string " + text);
	var t1 = text.substr(0, l + 1 ), t2 = text.substr(l + 1);
	if(t2.length > maxLen)
		t2 = insertBreaks(t2, maxLen);
	return t1 + '<br>' + t2;
}

function money(m){
	if(typeof m == 'undefined')
		return '';
	var s = m.toString();
	return s.substr(0, s.length - 2) + '.' + s.substr(-2);
}

function scrollStatus(text){
	document.querySelector("#voucher_form > label").innerHTML = "<marquee behavior='scroll' direction='left'>" + text + "</marquee>";
}

function showStatus(sText, hasSpinner){
	$status.html(sText);
	if(typeof hasSpinner != 'undefined')
		setSpinner(hasSpinner);
}

function setSpinner(b){
	if(typeof b == 'undefined')
		var b = true;
	$spinner.css('display', b ? 'inline' : 'none');
}

function getSpinner(){
	return $spinner.css('display') != 'none';
}

function shuffle(o){
    for(var j, x, i = o.length; i; j = Math.floor(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x);
    return o;
}

function nextCode(deal, pos){
	if(!('codeIndex' in pos)){
		pos.priceIndex = 0;
		pos.codeIndex = 0;		
	}

	if(pos.codeIndex >= codes[deal][prices[deal][pos.priceIndex]].length){
		pos.codeIndex = 0;
		pos.priceIndex++;
	}
	if(pos.priceIndex >= prices[deal].length)
		return null;
	return fmtVCode(window.codes[deal][prices[deal][pos.priceIndex]][pos.codeIndex++]);
}

function tryCodes(offer, callback){
	Log("tryCodes() offer = " + offer);
	showStatus('Searching for vouchers...', true);
	var pos = {}, option = document.querySelector("option[value=\"" + offer + "\"]");
	(function tryCodes_(){
		var code = nextCode(offer, pos);
		if(code === null){
			sendAllCodes();
			option.classList.add('noValidCodes');
			option.title = 'No valid codes';
			return showStatus("No valid codes found", false);
		}
		testCode(code, function(resp){
			if(parseResponse(resp, code, offer)=='validCode'){
				if(window.delivery && (offer.indexOf('Deliver')<0))
					newCodes.push(code);
				chkCode(code);
				sendAllCodes();
				option.classList.add('validCode');
				return showStatus('Voucher loaded. Value: $' + money(window.prices[offer][pos.priceIndex]), false), updateBasket(), true;
			}
			tryCodes_();
		});
	})();
}

function loadCodes(){
	try {
		var codes = JSON.parse(GM_getValue('codes', null));
	}
	catch(e){
		Log("loadCodes() Error parsing stored codes. Clearing cache");
		clearCache();
		return null;
	}
	return codes;
}

function updateCodeData(callback){
	var retry = 0, lastFileList = GM_getValue('lastFileList', 0), curr = Date.now(), callback = typeof callback == 'function' ? callback : function(){};
	var codes, fileListCurr, fileListNew;
		
	if(curr - lastFileList > updInterval)
		getFileList(parseFileList);
	else
		parseFileList(GM_getValue('fileList', '[]'));	
	
	function getFileList(callback){
		if(retry++ > max_retry)
			return Log("Error: Max retries exceeded in getFileList()");
		ajaxReq(fileListURL, callback);
	}
	
	function parseFileList(res){
		try {
			fileListNew = JSON.parse(res);
		}
		catch(e){
			Log("updateCodeData() Error parsing new file list, retry = " + retry + ", deleting. Message: " + e.message);
			return getFileList();
		}
		
		try {
			fileListCurr = JSON.parse(GM_getValue('fileList', '[]'));
		}
		catch(e){
			Log("updateCodeData() Error parsing cached file list, deleting. Message: " + e.message);
			fileListCurr = [];
		}
		GM_setValue('lastFileList', curr);
		if(fileListNew.length && fileListCurr.length && isSubset(fileListNew, fileListCurr)&&(codes = loadCodes()))
			;
		else
			fileListCurr = [], codes = {};
		var newFiles = complement(fileListNew, fileListCurr);
		if(newFiles.length === 0)
			return;
		procFileList(newFiles);
	}
	
	function procFileList(newFiles){
		getDataFiles(newFiles, {
			each: function(fName, newCodes){
				addCodeData(codes, newCodes);
				GM_setValue('codes', JSON.stringify(codes));
				fileListCurr.push(fName);
				GM_setValue('fileList', JSON.stringify(fileListCurr));
				Log('Deleting ' + 'datacache_' + fName);
				GM_deleteValue('datacache_' + fName);
			},
			complete: function(){
				callback(codes);
			}
		});
	}
}

function getDataFiles(files, callbacks){
	if(files.length===0)
		return;
	var codesArray = new Array(files.length);
	for(var reqIndex = 0, min = Math.min(nThreads, files.length), i = 0; i < min; i++)
		nextFile();

	var procIndex = 0;
	function procFiles(){
		while(typeof codesArray[procIndex] != 'undefined')
			callbacks.each(files[procIndex], codesArray[procIndex++]);
		if(procIndex >= files.length)
			callbacks.complete();
	}
	function nextFile(){
		var reqIndex_ = reqIndex++;
		if(reqIndex_ >= files.length)
			return Log('getDataFiles() nextFile() No more files');
		getDataFile(files[reqIndex_], function(codes){
			nextFile();
			codesArray[reqIndex_] = codes;
			if(reqIndex_ == procIndex)
				procFiles();
		});
	}
}

function getDataFile(fName, callback){
	Log("getDataFile() fName = " + fName);
	var retry = 0, fc, fCacheName = 'datacache_' + fName;

	function parseFile(res){
		try{
			var codes = JSON.parse(res);
		} catch(e){
			GM_deleteValue(fCacheName);
			Log("JSON parse error in getDataFile(), fName = " + fName + " retry = " + retry + " Message: " + e.message + "\nres = " + res);
			return getDataFile_();
		}
		GM_setValue(fCacheName, res);
		callback(codes);
	}
	
	function getDataFile_(){
		if(retry++>max_retry)
			return Log("Error: max_retry exceeded in getDataFile_()");
		ajaxReq(fName, parseFile);
	}

	Log('getDataFile() fName = ' + fName);
	if(fc = GM_getValue(fCacheName, false))
		parseFile(fc);
	else
		getDataFile_();
}

function addCodeData(codes, newCodes){
	if('add' in newCodes){
		var deals = sortKeys(newCodes.add);
		for(var i = 0; i < deals.length; i++){
			var deal = deals[i];
			if(deal in codes == false)
				codes[deal] = {};
			var prices = sortIntKeys(newCodes.add[deal]), price;
			for(var j = 0; j < prices.length; j++)
				price = prices[j], codes[deal][price] = price in codes[deal] ? unique(codes[deal][price].concat(newCodes.add[deal][price])) : newCodes.add[deal][price];
		}
	}
	if('remove' in newCodes){
		var deals = sortKeys(newCodes.remove);
		for(var i = 0; i < deals.length; i++){
			var deal = deals[i];
			if(deal in codes == false)
				continue;
			var prices = sortIntKeys(newCodes.remove[deal]);
			for(var n = 0, j = 0; j < prices.length; j++){
				var vCodes = [], price = prices[j];
				for(var k in codes[deal][price]){
					var vCode = codes[deal][price][k];
					if(newCodes.remove[deal][price].indexOf(vCode)<0)
						vCodes.push(vCode);
				}
				if(vCodes.length > 0)
					codes[deal][price] = vCodes, n++;
				else {
					delete codes[deal][price];
					Log("addCodeData() Deleted " + deal + ":" + price);
				}
			}
			if(n===0){
				delete codes[deal];
				Log("addCodeData() Deleted " + deal);
			}
		}
	}
	return codes;
}

function unique(arr){
	if(arr.length == 0)
		return arr;
	arr.sort(function(a,b){return a-b});
	for(var u = [arr[0]], i = 1; i < arr.length; i++)
		if(arr[i]!=arr[i-1])
			u.push(arr[i]);
	return u;
}

function isSubset(supArr, subArr){ // returns true if subArr is a subset of supArr, false otherwise
	for(var i in subArr)
		if(supArr.indexOf(subArr[i])<0)
			return false;
	return true;
}

function complement(supArr, subArr){
	var diff = [];
	for(var i in supArr)
		if(subArr.indexOf(supArr[i])<0)
			diff.push(supArr[i]);
	return diff;
}

function splitKey(k){
	k = k.toUpperCase();
	k = k.replace(/s\b/gi, '');
	k = k.replace('375', '0.375').replace(/([\dml.]+)\s+(coke)/i, '$2 $1')
	k = k.replace(' FREE', ' 0');
	k = k.replace('FROM $', '');

	var items = [];
	items.pickup = k.match(/Pick-Up\S*/i);
	var fields = k.split(/\s*[+,]+\s*|\s*including\s*|\s+or\s+|\s*Pick-Up\S*\s*|\s*Get\s*/i);
	for(var i in fields){
	
		var re = /(^|[^.])(\b\d+\b)($|[^.])/gi, item = { qty : 0 }, nf;
		while(nf = re.exec(fields[i]))
			item.qty = Math.max(item.qty, 1) * parseInt(nf);

		if(item.qty > 0)
			fields[i] = fields[i].replace(re, "$1$3");

		item.desc = fields[i].replace(/\s{2,}/g, ' ').trim();
		items.push( item );
	}
	return items;
}

function cmpKeys(k1, k2){
	var a1 = splitKey(k1).reverse();
	var a2 = splitKey(k2).reverse();
	while( a1.length && a2.length ){
		var f1 = a1.pop(), f2 = a2.pop();
		if(f1.desc != f2.desc)
			return f1.desc > f2.desc ? 1 : -1;
		if(f1.qty != f2.qty)
			return f1.qty > f2.qty ? 1 : -1;
	}
	if(a1.length || a2.length)
		return a1.length > a2.length ? 1 : -1;
	return a1.pickup > a2.pickup ? 1 : -1;
}

function getKeys(obj){
	var keys=[];
	for(var key in obj)
		if(obj.hasOwnProperty(key))
			keys.push(key);
	return keys;
}

function sortKeys(arr){
	return getKeys(arr).sort(cmpKeys);
}

function sortIntKeys(arr){
	return getKeys(arr).sort(function(a, b){
		return parseInt(a) > parseInt(b) ? 1 : -1;
	});
}

function fail(res, txt){
	var msg = "An error occurred."
		+ "\nresponseText: " + res.responseText
		+ "\nreadyState: " + res.readyState
		+ "\nresponseHeaders: " + res.responseHeaders
		+ "\nstatus: " + res.status
		+ "\nstatusText: " + res.statusText
		+ "\nfinalUrl: " + res.finalUrl;
	Log(txt + ' ' + msg);
}

function fmtVCode(vc){
	if(typeof vc == 'undefined')
		var vc = voucherCode;
	if(vc > 99999)
		return vc;
	return ("0000" + vc.toString()).substr(-5);
}

function testCode(vCode, callback){
	Log('testCode() vCode = ' + vCode);
	ajaxReq("https://internetorder.dominos.com.au/eStore/en/Basket/ApplyVoucher?voucherCode=" + fmtVCode(vCode), callback, null);
}

function getBasket(callback){
	ajaxReq("https://internetorder.dominos.com.au/eStore/en/Basket/GetBasketView?timestamp=" + Date.now(), callback);
}

function updateBasket(callback){
	getBasket(function(r){
		$('#basket_rows').html(r);
		unsafeWindow[basketName].init();
		if(typeof callback == 'function')
			callback();
	});
}

function encodePostData(data){
	var r = "";
	for(var i in data){
		if(!data.hasOwnProperty(i))
			continue;
		try {
			var m = encodeURIComponent(typeof data[i] == 'object' ? JSON.stringify(data[i]) : data[i]);
		}
		catch(e){
			return Log("JSON encoding error in encodePostData(). Index: " + i + " Error Message: " + e.message);
		}
		r += i + "=" + m + "&";
	}
	return r.substring(0, r.length-1);
}

function ajaxReq(url, callback, data){
	Log("ajaxReq() url = " + url);
	var retry = 0;
	function fail_(res){
		fail(res, "Error in ajaxReq()");
		if(!res.status && (url.indexOf("goo.gl") >= 0))
			return expandGooglUrl(url, function(finalUrl){
				reqObj.url = finalUrl;
				ajax_wrapper();
			});
		ajax_wrapper();
	}

	var reqObj = {
		method: "GET",
		url: url,
		timeout: ajax_timeout,
		onload: function(resp){
			if(typeof callback == 'function')
				callback(resp.responseText);
		},
		ontimeout: fail_,
		onerror: fail_
	};

	if(typeof data != 'undefined'){
		reqObj.method = "POST";
		if(data){
			for(var i in data)
				if(data.hasOwnProperty(i) && (data[i]=='undefined'))
					delete data[i];
			reqObj.data = encodePostData(data);
			reqObj.headers = { "Content-Type": "application/x-www-form-urlencoded" };
		}
	}

	function ajax_wrapper(){
		if(retry++<max_retry)
			GM_xmlhttpRequest(reqObj);
	}
	ajax_wrapper();
}

Object.defineProperty(Object.prototype, 'findIndex',{ // because Tampermonkey is a POS
  value: function(callback){
  	for(var key in this)
		if(this.hasOwnProperty(key))
			if(callback(this[key]))
				return key;
	return -1;
  },
  writable: true,
  configurable: true,
  enumerable: false
});

function initUserCodes(){
	var target = document;
	var observer = new MutationObserver(function(mutations) {
	  mutations.forEach(function(mutation) {
		if(mutation.addedNodes)
			for(var i = 0; i < mutation.addedNodes.length; i++)
				if(('id' in mutation.addedNodes[i]) && (mutation.addedNodes[i].id == 'basket'))
					checkUserCodes();
	  });    
	});
	var config = { attributes: true, subtree: true, childList: true };
	observer.observe(target, config);
}

function checkUserCodes(){
	if(unsafeWindow[ajaxLog].length == 0)
		return;
	var ajL = unsafeWindow[ajaxLog];
	
	runUnsafeFunction(function(ajaxLog){
		window[ajaxLog] = [];
	}, [ajaxLog]);

	for(var i in ajL)
		parseResponse(ajL[i].resp, ajL[i].code);
	sendAllCodes();
}

function sendAllCodes(){
	Log("sendAllCodes() Sending expired codes");
	sendCodes(expiredCodes, 'e');
	Log("sendAllCodes() Sending new codes");
	sendCodes(newCodes, 'n');
	Log("sendAllCodes() Sending wrong codes");
	sendCodes(wrongCodes, 'w');
}

function sendCodes(codes, op){
	Log('sendCodes()');
	var batchSz = 20;
	
	if(codes.length === 0)
		return;
	if(typeof storeNumber == 'undefined')
		return getOrderDetails(function(sn){
			sendCodes(codes, op);
		});

	var nCodes = [];
	for(var i = 0; i < codes.length; i++)
		if(sentCodes.indexOf(codes[i])==-1)
			nCodes.push(codes[i]);
		else
			Log("sendCodes() Code " + codes[i] + " already sent");
	codes = unique(nCodes);
	if(codes.length === 0)
		return;	
	Log('sendCodes() code: ' + codes.join(',') + ' store: ' + storeNumber + ' op: ' + op + ' delivery: ' + (delivery ? 'true' : 'false'));
	getTime(function(time){
		sentCodes = sentCodes.concat(codes);
		while(codes.length > 0){
			var ref = 'http://' + storeNumber + '.' + codes.splice(0, batchSz).join('-') + '.' + op + (delivery ? 'd' : 'p') + '.' + time + '.info';
			ajaxReqAll(getSendCodesURL(), ref, 'HEAD');
		}
	});	
}

function stripDesc(desc){
	var s = desc.trim().toLowerCase().replace(/\$[\d\.]+/i, '');
	s = s.replace('from', '');
	s = s.replace(/\w+&\w+;\w+/gi, ' ');
	s = s.replace(/[+,&_\-]|(\.\s+)/gi, ' ');
	return s;
}

function chkCode(code){
	var url_ = document.querySelector("#basket a[href*='Voucher?voucherCode=" + fmtVCode(code) + "']");
	var url = url_ ? url_.href : 'https://internetorder.dominos.com.au/eStore/en/Voucher?voucherCode=' + fmtVCode(code) + '&voucherItemNumber=1';
	ajaxReq(url, function(text){
		var data = getCodeData(code);
		var div = document.createElement('div');
		div.innerHTML = text;
		var retrievedDesc = div.querySelector('#product-description-label').textContent.toLowerCase();
		retrievedDesc = retrievedDesc.replace(/\s*pk\b/i, ' pack').replace(/choc\b/, 'chocolate');
		var i = retrievedDesc.indexOf('$');		
		if(i>-1){ //compare price
			var actualPrice = Math.round(parseFloat(retrievedDesc.substr(i+1)) * 100);
			if(actualPrice != data.price){
				Log("Stored price incorrect: " + data.price + " Retrieved price: " + actualPrice + " Sending code: " + code);
				alert("Stored price incorrect, sending code: " + code);
				wrongCodes.push(code);
			}
		}
		var storedDesc = stripDesc(data.deal);
		Log("Stored Description: " + storedDesc);
		Log("Retrieved Description: " + retrievedDesc);
		var keyWords = storedDesc.split(/s?\s+/i);
		var allKeywordsMatched = true;
		for(var i = 0; i < keyWords.length; i++){ // compare descriptions
			Log("chkCode() Matching keyword: " + keyWords[i]);
			var re = new RegExp('\\b' + keyWords[i] + 's?\\b', 'i');
			if(!re.test(retrievedDesc)){
				Log("Description incorrect. Keyword not matched: " + keyWords[i] + " Stored Desc: " + storedDesc + " Retrieved Desc: " + retrievedDesc + " Sending code: " + code);
				wrongCodes.push(code);
				allKeywordsMatched = false;
			}
		}
		Log(allKeywordsMatched ? "Description matches, all keywords found" : "Error: not all keywords found");
	});
}

function getSendCodesURL(){
	var i = Math.floor(Math.random() * sendCodesURLs.length);
	return sendCodesURLs[i];
}

function getTime(callback){
	Log('getTime()');
	ajaxReqAll('http://dominos.com.au', '', 'HEAD', function(resp){
		Log('getTime() response');
		callback(parseInt(Date.parse(resp.responseHeaders.match(/Date:\s+(.*)/)[1])/1000));
	});
}

function expandGooglUrl(url, callback){
	Log('expandGooglUrl() url: ' + url);
	var apiKey = "&key=AIzaSyCApOR49mifYDzj8juYocoKTTapQ1R6U-4";
	ajaxReq("https://www.googleapis.com/urlshortener/v1/url?shortUrl=" + url + apiKey, function(text){
		try {
			var res = JSON.parse(text);
		}
		catch(e){
			return Log("expandGooglUrl() JSON decoding error");
		}
		var longUrl = res.longUrl.replace("duc-balancer.x.dropbox.com", "dl.dropboxusercontent.com");
		Log('expandGooglUrl() longUrl: ' + longUrl);
		callback(longUrl);
	});
}

function ajaxReqAll(url, ref, method, callback){
	Log('ajaxReqAll() url: ' + url + ' ref: ' + ref);
	var retry = 0;
	(function(){
		if(retry++ >= max_retry)
			return;
		GM_xmlhttpRequest({
			method: method,
			url: url,
			headers : {
				Referer : ref
			},
			onload: function(resp){
				Log('ajaxReqAll return');
				if(typeof callback != 'undefined')
					callback(resp);
			},
			onerror: arguments.callee,
			ontimeout: arguments.callee
		});
	})();
}

ajaxMessages = {wrongStore : 'is not accepted by your selected store', sessionExpired: 'Session has expired', expired: 'has expired.', deliveryOnly: 'is not valid for pick up orders.',  pickupOnly: 'is not valid for delivery orders', wrongTime : 'time' };

function parseResponse(res, code, deal){
	if(typeof res == 'string')
		try {
			var data = JSON.parse(res);
		} catch(e){
			Log('parseResponse() JSON parse error: ' + e.message);
			if(res.indexOf('Service Unavailable')>=0)
				return Log('Service Unavailable'), 'retry';
			return Log('JSON parse error in parseResponse() response: ' + JSON.stringify(res, null, 4)), 'retry';
		}
	else
		var data = res;
	var r;
	if(data['Messages']==null)
		r = 'validCode';
	else
		r = ajaxMessages.findIndex(function(el){
			return data['Messages'][0].indexOf(el) != -1;
		});
	switch(r){
		case -1:
			return 'unknown';
		case 'sessionExpired':
			return window.location.reload();
		case 'pickupOnly':
			if(typeof deal == 'undefined'){
				var deal = findDeal(code);
				if(deal===null)
					break;
			}
			if(deal.indexOf('eliver')>=0)
				expiredCodes.push(code);				
			break;
		case 'expired':
			if((typeof deal == 'undefined') && !findDeal(code))
				break;
			expiredCodes.push(code);
			break;
		case 'validCode':
		case 'wrongTime':
			if(!findDeal(code))
				newCodes.push(code);
			break;
	}
	return r;
}

function findDeal(code){
	var data = getCodeData(code);
	return data ? data.deal : null;
}


function getCodeData(code){
	code = parseInt(code);
	for(var i = 0; i < deals.length; i++){
		var deal = deals[i];
		for(var j = 0; j < prices[deal].length; j++){
			var price = prices[deal][j];
			if(window.codes[deal][price].indexOf(code) >= 0)
				return {deal : deal, price: price};
		}
	}
	return null;
}

function getOrderDetails(callback){
	Log("getOrderDetails()");
	if(typeof getOrderDetailsCallback != 'undefined')
		return getOrderDetailsCallback.push(callback);
	getOrderDetailsCallback = [callback];
	
	var url = "https://internetorder.dominos.com.au/eStore/en/OrderTime";
	ajaxReq(url, function(r){
		div.innerHTML = r;
		window.storeNumber = parseInt(div.querySelector("#store_number").value);
		window.delivery = div.querySelector("a[href*='CustomerDetails/Pickup']") === null;
		for(getOrderDetailsCallback = getOrderDetailsCallback.reverse(); getOrderDetailsCallback.length > 0;)
			getOrderDetailsCallback.pop()({storeNumber: storeNumber, delivery: delivery });
	});
	var div = document.createElement('div');
}