jehan / Dominos Pizza Voucher Codes

// ==UserScript==
// @name        Dominos Pizza Voucher Codes
// @description Finds voucher codes for Dominos Pizza Australia & NZ
// @match     	*://*.dominos.com.au/*
// @match     	*://dominos.com.au/*
// @match     	*://*.dominos.co.nz/*
// @match     	*://dominos.co.nz/*
// @match     	*://*.dominospizza.co.nz/*
// @match     	*://dominospizza.co.nz/*
// @version     9.3
// @require		http://code.jquery.com/jquery-latest.min.js
// @require		https://raw.githubusercontent.com/flokii/dominos/master/sha1.js
// @require		http://www-cs-students.stanford.edu/~tjw/jsbn/jsbn.js
// @require		http://www-cs-students.stanford.edu/~tjw/jsbn/jsbn2.js
// @require		http://www-cs-students.stanford.edu/~tjw/jsbn/prng4.js
// @require		http://www-cs-students.stanford.edu/~tjw/jsbn/rng.js
// @require		http://www-cs-students.stanford.edu/~tjw/jsbn/rsa.js
// @require		http://www-cs-students.stanford.edu/~tjw/jsbn/base64.js
// @resource	turtle https://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==

setLogging();
ctrl = tryGen = false;
nThreads = 3;
max_retry = 25;
max_ajax_callback_retry = 2;
ajax_timeout = 10000;
maxCPM = GM_getValue('maxCPM', 30);
country = window.location.hostname.split('.').pop();


var lastCountry = GM_getValue('country', null);
if(lastCountry && (lastCountry != country))
	clearCache();
GM_setValue('country', country);

if(country == 'au'){
	fileListURL = "goo.gl/gd3hTA"; //"goo.gl/lHScDV";
	sendCodesURLs = ['G3uTCz','HlTPWA','LzGvkp','kIIRcO','l1wYpD','OiG75K','R1pwZL','ZlQ8hT','K9RzN2','YZCMdS','JoDR2h','oplNof','rrJZrS','QXbUoa','w5TaHJ','TzqVxT','UWncZJ','N0EykP','Aqf4L6','8su4Cp'];
	storeListRef = 'goo.gl/tf457k';
}
else {
	fileListURL = 'goo.gl/SsY3E6';
	sendCodesURLs = ['4mru6g', 'tiHGQm', 'ekxW0X', 'wf80OK', 'qpX6zi', 'MPA7Oy', 'gr34RZ', 'cTwIkH', '9phyAf', 'XXdukM'];
	storeListRef = 'https://goo.gl/3zaWBc';
}

window.dealList = null;
window.priceOrder = {};
invalidDeals = [];
updInterval = 1800000;
consentVersion = 2;

sentCodes = [];
dealListCallbacks = {};
window.ajaxReq = ajaxReq, window.updateBasket = updateBasket, window.testCode = testCode, window.getSessionVars = getSessionVars, window.chkCode = chkCode, window.showStatus = showStatus; // GM weirdness // window.getBasket = getBasket, 
setupGMMenu();
googl = {};
window.b64Map = "-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; // override value in base64.js Need to set b64map=b64Map in verifySig due to script loading issues
b64RegExp = new RegExp('[' + window.b64Map + ']+', 'g');
window.nilCallbacks = { complete: nil, error: nil };
debug = false; // debug only
window.page = window.location.pathname.split('/').pop();

if(debug){
	clearCache(); 
	setLogging(true); //debug
	GM_setValue('consent_code_sharing', consentVersion);
}

if(!('fill' in Array.prototype)) // fuck you Chrome
	Array.prototype.fill = function(val){
		for(var i = 0; i < this.length; i++)
			this[i] = val;
		return this;
	};

(function(){
	updateCodeData(
	{
		complete: function(codes){
			Log("updateCodeData() callback()");
			Log("updateCodeData() callback() codes = ", codes);

			createCodeList(codes);
			return true;
		},
		error: function(){
			Log("Error in updateCodeData()");
			return null;
		}
	});
	setTimeout(arguments.callee, updInterval);
})();

Log("promise_ 1");
promise_storeUrlList = new Promise_(getStoreUrlList);

if(page.indexOf('OrderTime')==0)
	writeSessionVars(true); // new session

promise_sessionVars = new Promise_(getSessionVars);
promise_dealList = new Promise_(getDealList);
promise_codeList = new Promise_(createCodeList);

Log("promise_ 2");
if(!validPage(page))
	return;

promise_SPCodes = new Promise_(getSPCodes, false);
promiseAll([promise_storeUrlList, promise_sessionVars], getSPCodes);
promiseAll([promise_sessionVars, promise_codeList, promise_dealList], initialiseCodes);
promiseAll([promise_codeList, promise_SPCodes], applySPCodes);


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_form .input-group label { height: 3em!important; }");
GM_addStyle("#voucher_select option:checked, #voucher_select option:hover { box-shadow: 0 0 10px 100px #9C9D9F inset; }");
GM_addStyle("#voucher_select option { display: block!important; text-align: left;}");
GM_addStyle("#voucher_select { background: url('http://i.imgur.com/eH8Ci9N.png') no-repeat center center; background-size: 100% auto; }");
GM_addStyle("#voucher_select {-webkit-appearance: none; -moz-appearance: none; text-indent: 1px; text-overflow: ''; }");
GM_addStyle("#voucher_select { background-color: transparent!important; width: 2em!important; margin-right: 0.3em; direction: rtl; }");
GM_addStyle("#voucher_select option { direction: ltr; }");
//GM_addStyle("#voucher_form > img { float: right; display: none; } #voucher_form > label { height:1.1em!important; display: inline-block!important; width: auto; max-width: 100%;}");
GM_addStyle(".input-group > .row > * { height:2em!important;}");
GM_addStyle(".input-group > .row * { display: inline-block!important; float:none!important; clear:both!important; vertical-align:middle!important;}");
GM_addStyle(".input-group > .row > .col-4 { width: auto!important; margin-left: -1em; }"); //padding-left: 1em; }");
GM_addStyle(".input-group > .row > .col-4 * { border: none!important; }");
GM_addStyle(".input-group > .row > div * { padding-top:0!important; padding-bottom:0!important; height: 100%; }");

GM_addStyle(".input-group > .row > .col-8 { width: auto!important; margin-right: 0.75em!important; }");
GM_addStyle("#apply_voucher { width: 4em!important; }");//margin-top: -1px!important;}");
GM_addStyle("#voucher_code { width: 7em!important; }");
//GM_addStyle("#loading-indicator { width: auto!important; visibility: visible!important; }");
//GM_addStyle("#loading-indicator.loaded { visibility: hidden!important; }"); //visible;display: none!important;}");

GM_addStyle("#loading-indicator { display: none!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_select { direction: ltr!important; }");


window.onkeydown = window.onkeyup = function(e){
	if(e.keyCode!=17)
		return;
	window.ctrl = e.ctrlKey;
}

document.addEventListener("DOMContentLoaded", function(){
	Log("Event: DOMContentLoaded");
	$("a[href$='Menu.Payment']").each(
		function(){			
			this.onclick = (function(href){			
				return function(e){
					e.stopPropagation();
					e.preventDefault();
					sendCodeBatch(function(){
						window.location.href = href;
					});
				}
			})(this.href);
			this.href = '';
		}
	);
	initBasketObserver();
	$status = $('#voucher_form .input-group label');
	scrollStatus("Tip: Most Traditional Pizza coupons can be used for Chef's Best or Mogul Pizzas");

	$("<select id='voucher_select' title='Find vouchers'/>").insertBefore('#apply_voucher').change(function(e){
		if(this.value == -1)
			tryGenericCodes();
		else
			tryCodes(this.value);
		resetChoice();
	}).each(function(){});
});

function tryGenericCodes(){
	var i = 0;
	getGenericDescIds();
	tryGen = true;
	(function(){
		Log("tryGenericCodes() i = " + i + " desc_id = " + genericDescIds[i]);
		while(i < genericDescIds.length)
			if((genericDescIds[i] in window.priceOrder) && priceOrder[genericDescIds[i]])
				return tryCodes(genericDescIds[i], arguments.callee), i++;
			else
				i++;
		return tryGen = false, showStatus("All deals checked"), updateBasket(), true;
	})();
}

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 runUnsafeCode(c){
	Log("runUnsafeCode():\n" + 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, a){
			if(typeof a != 'undefined')
				t += "\n" + JSON.stringify(a);
			console.log(t);
			//GM_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 cloneObj(obj){
	var n = rndStr();
	var cl = unsafeWindow[n] = obj;
	delete unsafeWindow[n];
	return cl;
    var oldState = history.state;
    history.replaceState(obj, null);
    var clonedObj = history.state;
    history.replaceState(oldState, null);
    return clonedObj;
}

function createCodeList(codes){
	Log("createCodeList()");
	createCodeList_();
	return true;
	
	function createCodeList_(){
		if(typeof codes == 'undefined'){
			var codeList = GM_getValueJSON('codeList', null);
			if(codeList)
				createCodeList.resolve(codeList);
			return true;
		}

		Log("createCodeList()");
		var codeList = { delivery: {}, pickup: {}};
		for(var desc_id in codes){
			if(!codes.hasOwnProperty(desc_id))
				continue;
			if(desc_id == 'deal_list')
				continue;
			if(1 in codes[desc_id]){
				codeList.delivery[desc_id] = codes[desc_id][1];
				//codeList.delivery[desc_id].order = sortIntKeys(codeList.delivery[desc_id]);
				codeList.pickup[desc_id] = cloneObj(codes[desc_id][1]);
			}
			if(0 in codes[desc_id]){
				if(desc_id in codeList.pickup)
					codeList.pickup[desc_id] = concat(codeList.pickup[desc_id], codes[desc_id][0], 'prices');
				else
					codeList.pickup[desc_id] = codes[desc_id][0];		
			}
			//if(desc_id in codeList.pickup)
				//codeList.pickup[desc_id].order = sortIntKeys(codeList.pickup[desc_id]);
		}
		GM_setValueJSON('codeList', codeList);
		return createCodeList.resolve(codeList);
	}
}

function initialiseCodes(arr){
	Log("initialiseCodes() arr:", arr);
	var sessionVars = arr[0], codeList = arr[1], dealList = arr[2];
	Log("initialiseCodes()");
	if(!('newCodeList' in initialiseCodes))
		initialiseCodes.newCodeList = 0;
	if((!('lock' in initialiseCodes)) || !initialiseCodes.lock)
		initialiseCodes.lock = true;
	else
		return initialiseCodes.newCodeList++, false;
	Log("initialiseCodes() 2");
	var startTime = new Date().getTime();

	//Log("initialiseCodes() 3 codesList = ", codeList);
	window.codes = sessionVars.orderDetails.delivery ? codeList.delivery : codeList.pickup;
	createMenu(sessionVars, codeList, dealList);
	initialiseCodes.lock = false;
	if(initialiseCodes.newCodeList)
		return initialiseCodes.newCodeList--, arguments.callee();
}

function createMenu(sessionVar, codeList, dealList){
	Log('createMenu()');
	Log('createMenu() validDescIds:', sessionVars.validDescIds);
	Log('createMenu() invalidDescIds:', sessionVars.invalidDescIds);
	if(!window.dealList)
		return false;
	Log('createMenu() 3');
	var voucherSelect = document.getElementById('voucher_select');
	if(!voucherSelect){
		mkObserver(function(node){
			if(node.id != 'voucher_select')
				return true;
			return createMenu(), null;
		}, document);
		return false;
	}

	if(!('extraOptions' in createMenu)){
		createMenu.extraOptions = true;
		nullOption = document.createElement('option');
		nullOption.value = '';
		voucherSelect.appendChild(nullOption);
		nullOption.style.display = 'none';
		resetChoice();
		
		var o = document.createElement('option');
		o.value = '-1';
		o.innerHTML = "Try All Generic Vouchers";
		voucherSelect.appendChild(o);
	}
	
	//Log('createMenu() 3aa window.codes: ', window.codes);
	//Log("dealList:", window.dealList);
	Log("dealList.order.length:" + window.dealList.order.length);
	for(var i = 0, lastOption = null; i < window.dealList.order.length; i++){
		var desc_id = window.dealList.order[i], deal = window.dealList.deals[desc_id];
		Log('createMenu() 3a i: ' + i);
		Log('createMenu() 3b desc_id:' + desc_id + ' deal:' + deal);
		var option = voucherSelect.querySelector('option[value="' + desc_id + '"]');
		if(!(desc_id in window.codes) || !count(window.codes[desc_id])){
			if(option){
				Log('createMenu() 3c desc_id:' + desc_id + ' deal:' + deal);
				option.parentNode.removeChild(option);
			}
			continue;
		}
		
		if(!option){
			Log('createMenu() 3d: desc_id:' + desc_id + ' deal:' + deal);
			option = document.createElement('option');
			option.value = desc_id;
			//voucherSelect.appendChild(option);
			voucherSelect.insertBefore(option, lastOption && lastOption.nextElementSibling);
			lastOption = option;
		}
		else
			Log('createMenu() 3dd:' + option.value);
		Log('3e window.codes[desc_id]:', window.codes[desc_id]);
		
		if(sessionVars.invalidDescIds.indexOf(desc_id) >= 0)
			option.classList.add('noValidCodes');
		else
			if(sessionVars.validDescIds.indexOf(desc_id) >= 0)
				option.classList.add('validCode');

		window.priceOrder[desc_id] = sortIntKeys(window.codes[desc_id]);
		
		var text;
		var lPrice = window.priceOrder[desc_id][0], hPrice = window.priceOrder[desc_id][window.priceOrder[desc_id].length - 1];
		var dl = false;
		if(deal.indexOf('$')>=0){
			dl = '$';
			lPrice = '$' + money(lPrice);
			hPrice = '$' + money(hPrice);
		}
		else
			if(deal.indexOf('%')>=0){
				dl = '%';
				lPrice += '%';
				hPrice += '%';
				window.priceOrder[desc_id].reverse();
			}		
		if(dl){
			var m = lPrice + (lPrice == hPrice ? '' : ('-' + hPrice) );
			text = deal.replace(dl, m);
		}
		else
			text = deal;

		if(country=='nz')
			text = text.replace('Chips', 'Chups');
		if(option.innerHTML != text)
			option.innerHTML = text;
		Log('createMenu() 3f');
	}
	
	scrapePageCodes();
}

function getGenericDescIds(){
	genericDescIds = [];
	var deal;
	for(var desc_id in window.dealList.deals){
		deal = dealList.deals[desc_id];
		Log("Deal: " + deal);
		if((deal.indexOf('Free')!= -1) || ((deal.length > 3) && (deal.indexOf('Off') == deal.length - 3)) || ( (deal.indexOf('Off') >= 0) && (deal.indexOf('xclud') >= 0) )){
			Log("Deal gen: " + deal);
			genericDescIds.push(desc_id);
		}
	}
	Log("genericDescIds: ", genericDescIds);
}

function concat(obj1, obj2, dataTypes){
	Log('concat()');
	var opArray = [1, 1, 1, 0]; // 0 for array, 1 for object
	var dTypes = ['descs', 'dels', 'prices', 'codes'];
	var i = dTypes.indexOf(dataTypes);
	if(i==-1)
		return Log("Error: Invalid dataTypes passed to concat() arg: " + dataTypes);
			
	
	var c = concatOp(obj1, obj2, opArray.slice(i));
	
	return c;
}

function concatOp(obj1, obj2, opArray){
	//Log("concatOp() obj1:", obj1);
	//Log("concatOp() obj2:", obj2);
	if(opArray[0] == 0)
		return obj1.concat(obj2); // codes, no shuffle for sake of SPCodes
	for(var key in obj2){
		if(!obj2.hasOwnProperty(key))
			continue;
		//Log("concatOp() key = " + key);
		try { //debug
			key in obj1;
		} //debug
		catch(e){//debug
			Log("Error typeof obj1 = " + typeof obj1 + " object:", obj1); //debug
			Log("Error typeof obj2 = " + typeof obj2 + " object:", obj2); //debug
			
		}//debug
		obj1[key] = (key in obj1) ? concatOp(obj1[key], obj2[key], opArray.slice(1)) : obj2[key];
	}
	return obj1;
}

function concatObj(obj1, obj2){
	Log('concatObj()');
	for(var key in obj2){
		Log('concatObj() key = ' + key);
		if(!obj2.hasOwnProperty(key))
			continue;
		if(key in obj1){
			if(('length' in obj1[key]) && ('length' in obj2[key]))
				obj1[key] = concatArray(obj1[key], obj2[key]);
			else
				obj1[key] = arguments.callee(obj1[key], obj2[key]);
		} else
			obj1[key] = obj2[key];
	}
	return obj1;
}

function concatArray(arr1, arr2){
	Log("concatArray()");
	for(var i in arr2)
		if(arr2.hasOwnProperty(i) && (arr1.indexOf(arr2[i])<0))
			arr1.push(arr2[i]);
	return arr1;
}

function concatAssocArray(arr1, arr2){
	Log("concatArray()");
	for(var i in arr2)
		if(!(i in arr1))
			arr1[i] = arr2[i];
	return arr1;
}

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 money(m){
	if(typeof m == 'undefined')
		return '';
	var s = m.toString();
	s = s.substr(0, s.length - 2) + '.' + s.substr(-2);
	//Log("money() " + m + " => " + s);
	return s;
}

function scrollStatus(text){
	$status.html("<marquee behavior='scroll' direction='left'>" + text + "</marquee>");
}

function showStatus(sText){
	$status.html(sText);
}

function initRKey(k){
	if(!('parent' in k))
		k = window;
	for(var i in k){
		try {
			if(k[i].toString().match(/\$.*h.*l\(s/)){
				var keyL = i.length + 4, padL = 6, padT = 2;
				return k[i] = (function(rHnd){
					return function(sd){
						return rHnd(sd.replace(/\d{4,}/, Math.floor(Math.random() * Math.exp(keyL) * padL / (padL + padT))));
					}
				})(k[i]);
			}
		}
		catch(e){
			// unassigned
		}
	}
	return -1;
}

function setSpinner(b){
	if(typeof b == 'undefined')
		var b = true;
	document.getElementById('loading-indicator').className = b ? '' : 'loaded';
}

function getSpinner(){
	return document.getElementById('loading-indicator').className.indexOf('loaded') > -1;
}

function shuffle(o){
	if(o.length > 0)
		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(desc_id, pos){
	Log("nextCode() desc_id = " + desc_id + " pos = " + JSON.stringify(pos));
	if(!('codeIndex' in pos)){
		pos.priceIndex = 0;
		pos.codeIndex = 0;
	}
	if(pos.codeIndex >= window.codes[desc_id][window.priceOrder[desc_id][pos.priceIndex]].length){
		pos.codeIndex = 0;
		pos.priceIndex++;
	}
	if(pos.priceIndex >= window.priceOrder[desc_id].length)
		return null;
	return window.codes[desc_id][window.priceOrder[desc_id][pos.priceIndex]][pos.codeIndex++];
}

function getUnixTime(){
	return Math.floor(Date.now() / 1000);
}

function chkSPC(code, desc_id, price, del){
	Log("chkSPC()");
	if(spCodes.indexOf(code) >= 0)
		return false;
	
	if(window.codes[desc_id][price].length < 10)
		return false;
	Log('chkSPC() 1 new spcCode: ' + code + ' batchsz = ' + window.codes[desc_id][price].length);
	spCodes.push(code);
	GM_setValueJSON('spCodes_' + sessionVars.orderDetails.store, spCodes);
	Log("chkSPC() 2 codes:", window.codes[desc_id][price]);
	window.codes[desc_id][price].unshift(code); // will never get to the second copy
	Log("chkSPC() 3 codes:", window.codes[desc_id][price]);
	encodeOp(code, 'spc');
	return true;
}

function tryCodes(desc_id, callback){
	var codesTried = 0, startTime = getUnixTime(); 
	if(typeof callback == 'undefined')
		callback = function(){ return true };
	Log("tryCodes() deal = " + window.dealList.deals[desc_id]);
	showStatus('Searching for vouchers...');
	var pos = {}, option = document.querySelector("option[value=\"" + desc_id + "\"]"), chks = [], accepted = false;
	Ctrl = ctrl || tryGen;
	(function tryCodes_(){
		Log("tryCodes_()");
		var code = nextCode(desc_id, pos);
		if(code === null){
			if(accepted == false)
				invalidDeals.push(desc_id);
			if(chks.length==0){
				option.classList.add('noValidCodes');
				option.title = 'No valid codes';
				sessionVars.invalidDescIds.push(desc_id);
				sessionVars.invalidDescIds = unique(sessionVars.invalidDescIds);
				writeSessionVars();
			}
			if(Ctrl){
				showStatus("All codes checked");
				if(!tryGen)
					updateBasket(); // triggers sendCodeBatch();
				return callback();
			}			
			return showStatus("No valid codes found");
		}
		testCode(code, function(codeStatus){
			Log('codeStatus: ' + codeStatus);
			switch(codeStatus){
				case 'validCode':
					option.classList.add('validCode');
					sessionVars.validDescIds.push(desc_id)
					sessionVars.validDescIds = unique(sessionVars.validDescIds);
					writeSessionVars();
					if(!Ctrl)
						updateBasket();
					if(tryGen || !Ctrl){
						var status, deal = window.dealList.deals[desc_id], price = window.priceOrder[desc_id][pos.priceIndex];
						
						if(price > 0){
							if(deal.indexOf('%') >= 0)
								status = 'Voucher loaded. Value: ' + price + '% Off';
							else
								status = 'Voucher loaded. Value: $' + money(price);
						}
						else
							status = 'Voucher loaded';
						showStatus(status);
						chkSPC(code, desc_id, price, sessionVars.orderDetails.delivery);
						return callback(), true;
					}
					chks.push(code);

				case 'deliveryOnly': // as merged
					chkSPC(code, desc_id, price, true);
				case 'wrongTime': // fallthrough
					accepted = true;
			}

			Log("tryCodes_() codesTried: " + codesTried);
			if(++codesTried >= maxCPM){
				var currTime = getUnixTime(), timeElapsed = currTime - startTime;
				var timeToWait = 60 - timeElapsed;
				if(timeToWait > 0){
					window.setTimeout(function(){
						showStatus("Waiting for " + timeToWait + " seconds...");
						timeToWait -= 1;
						if(timeToWait > 0)
							setTimeout(arguments.callee, 1000);
					}, 1000);
					codesTried = 0;
					startTime+= 60;
					return window.setTimeout(tryCodes_, timeToWait * 1000), true;
				}
				else {
					codesTried = Math.round(codesTried * (timeElapsed % 60) / timeElapsed); // estimate how many were tried from the start of this minute;
					startTime += timeElapsed - (timeElapsed % 60);
				}
			}
			return tryCodes_();
		});
	})();
}

function chkCodes(codes, callback){
	if(codes.length == 0)
		return callback();
	var callbacks = {
		pre: function(data, callback){
			callback(0);
		},
		each: function(data, callback){
			callback(0);
		},
		complete:function(){
			Log('chkCodes() callbacks:complete');
			callback();
		}
	};
	var fns = new Array(codes.length);
	for(var i = 0; i < codes.length; i++){
		fns[i] = (function(code){
			return function(callback){
				return chkCode(code), callback();
			}
		})(codes[i]);
	}
	return queueCalls(fns, callbacks);
}

function GM_getValueJSON(name, def){
	var v = typeof def == 'string' ? def : JSON.stringify(def);
	Log("GM_getValueJSON(" + name + ')');
	var val = GM_getValue(name, v);
	if(val === v)
		return def;
	try {
		var json = JSON.parse(val);
	}
	catch(e){
		Log("JSON parse error in GM_getValueJSON(" + name + ") text = " + val);
		return null;
	}
	return json;
}

function loadCodes(){
	Log("loadCodes()");
	var codes = GM_getValueJSON('codes', false);
	if(codes === false){
		Log("loadCodes() Error parsing stored codes. Clearing cache");
		var consent = GM_getValue('consent_code_sharing'), logging = GM_getValue('logging');
		clearCache();
		GM_setValue('consent_code_sharing', consent);
		GM_setValue('logging', logging);
		return false;
	}
	return codes;
}

function mkRefUrl(url){
	return "https://www.googleapis.com/urlshortener/v1/url?key=AIzaSyCApOR49mifYDzj8juYocoKTTapQ1R6U-4&shortUrl=http://" + url + "&projection=ANALYTICS_TOP_STRINGS";
}

function splitStrByRIndex(str, indexes){
	var l = str.length, b, arr = {}, key, n;
	for(var i = 0; i < indexes.length; i+=2){
		if(indexes[i + 1] == -1)
			indexes[i + 1] = l;
		arr[indexes[i]] = str.substr(l - indexes[i + 1], indexes[i + 1]);
		l -= indexes[i + 1];
	}
	Log('splitStrByRIndex:', arr);
	return arr;
}

function getLatestRef(url, callback, interval){
	Log("getLatestRef() url = " + url);
	//GM_deleteValue('getLatestRef_LastChecked_goo.gl/tf457k'); GM_deleteValue('getLatestRef_Timestamp_goo.gl/tf457k'); //debug only
	var lastChecked = GM_getValue('getLatestRef_LastChecked_' + url, null);
	if(lastChecked && (Date.now() - lastChecked < interval))
		return false;
	var minTimestamp = GM_getValue('getLatestRef_Timestamp_' + url, b64map[0]);
	return ajaxReqJSON(mkRefUrl(url), parseMsg), true; 
	
	function parseReferer(ref){
		//Log("ref.id = " + ref.id);
		return splitStrByRIndex(ref.id, ['domain', 3, 'timestamp', 5, 'signature', getRSA().b64Length, 'msg', -1]); // Why the fuck does the JS spec not require for..in to be in list order?
	}
	
	function parseMsg(data){
		Log("getLatestRef() parseMsg()");
		var refs = [], refObjs = [];
		for(var timespan in data.analytics)
			for(var r in data.analytics[timespan].referrers){
				var rf = data.analytics[timespan].referrers[r];
				if(rf.id.length)
					refs.push(rf);
			}		
		refs.sort();
		var lastRef = null;		
		for(var i = 0; i < refs.length; i++){
			if(refs[i] == lastRef)
				continue;
			lastRef = refs[i];
			if(lastRef.id.length < 94)
				continue;
			var refObj = parseReferer(lastRef);
			if(refObj === null){
				Log("Error: Could not parse ref", lastRef);
				continue;			
			}
			//Log('refObj:', refObj);
			if(refObj.timestamp <= minTimestamp)
				continue;
			refObjs.push(refObj);
		}
		
		GM_setValue('getLatestRef_LastChecked_' + url, Date.now());
		if(refObjs.length == 0){
			Log("getLatestRef() parseMsg() No refs found!");
			return callback(null);
		}

		refObjs.sort(function(a,b){
			if(a.timestamp == b.timestamp)
				return 0;
			return a.timestamp > b.timestamp ? 1 : -1; //newest last
		});
		GM_setValue('getLatestRef_Timestamp_' + url, refObjs[refObjs.length - 1].timestamp);
		do {
			var refObj = refObjs.pop();
			if(!refObj){
				Log("Error: All signatures invalid, bailing");
				return null;
			}
			Log("Timestamp: " + getDateString(refObj.timestamp));
		} while(!verifySigRefObj(refObj) && Log("Error: Invalid signature"));		
		return callback(refObj);
	}
}

function updateCodeData(callbacks){
	Log("updateCodeData() 1");
	if(typeof callbacks == 'undefined')
		var callbacks = nilCallbacks;
	else
		callbacks = concatAssocArray(callbacks, nilCallbacks);
	Log("updateCodeData() 2");
	return getLatestRef(fileListURL, parseFilenames, 900000);
	Log("updateCodeData() 3");

	function parseFilenames(refObj){
		if(refObj == null){
			Log("No new filesList found");
			return false;
		}
		
		Log("parseFilenames() 2");		
		var fileListNew = refObj.msg.split('.');		
		Log("parseFileList() 3");

		var fileListCurr_ = GM_getValue('fileList', '');
		Log("fileListCurr_ = " + fileListCurr_);
		if(refObj.msg == fileListCurr_){
			Log("No new files");
			return true; // no new files
		}
		Log("parseFilenames() 3");
		var fileListCurr = fileListCurr_.split('.');
		var codes;
		if(fileListNew.length && fileListCurr.length && isSubset(fileListNew, fileListCurr)&&(codes = loadCodes()))
			;
		else
			fileListCurr = [], codes = {};
		Log("parseFileList() 3a fileListCurr: ", fileListCurr);
		Log("parseFileList() 3b fileListNew: ", fileListNew);
		var newFiles = complement(fileListNew, fileListCurr);
		for(var i = 0; i < newFiles.length; i++)
			newFiles[i]  = 'https://paste.ee/r/' + newFiles[i];
		Log("parseFileList() 4 newFiles: ", newFiles);
		return getCodeFiles(newFiles, codes, 
		{
			complete: function(codes){
				Log("parseFilenames() 1 getDataFile() complete()");
				return callbacks.complete(codes);
			},
			error: function(){
				return Log("updateCodeData() getCodeFiles() Error"), callbacks.error();
			}
		});
	}
}

function priceWalk(codes, callback, walkDel){
	if(typeof walkDel == 'undefined')
		var walkDel = true;
	var f;
	for(var desc_id in codes)
		if(codes.hasOwnProperty(desc_id))
			if(walkDel){
				for(var del in codes[desc_id])
					if(codes[desc_id].hasOwnProperty(del))
						if(f = walkPrices(codes[desc_id][del]))
							return f;
			}
			else
				if(f = walkPrices(codes[desc_id]))
					return f;
	return false;
	
	function walkPrices(prices){
		for(var price in prices){
			if(!prices.hasOwnProperty(price))
				continue;
			var c = callback(prices[price]); // true: stop searching, null: continue, false:delete row and continue
			if(c === true)
				return { desc_id: desc_id, del: del, price: price };
			if(c === null)
				continue;
			if(c === false){
				Log("priceWalk() deleting desc_id:" + desc_id + " del:" + del + " price:" + price);
				delete prices[price];
				continue;
			}
			prices[price] = c;
		}
		return false;
	}
}

function delGMValues(prefix, keepUrl){
	if(typeof keepUrl == 'undefined')
		keepUrl = false;
	var entries = cloneInto(GM_listValues(), window);
	for(var i = 0; i < entries.length; i++){
		if(entries[i].indexOf(prefix)!=0)
			continue;
		if(keepUrl && (entries[i] == prefix + keepUrl))
			continue;
		Log("Deleting entry: " + entries[i]);
		GM_deleteValue(entries[i]);
	}
}

function queueCalls(fns, callbacks){
	if(fns.length===0)
		return true;
	
	var fnArray = [];
	var procIndex = 0;

	for(var reqIndex = 0, min = Math.min(nThreads, fns.length), i = 0; i < min; i++)
		nextFn();		
	return true;
	
	function procFns(){
		Log("procFns() procIndex = " + procIndex + " reqIndex = " + reqIndex);
		if(procIndex >= fns.length)
			return callbacks.complete();
		Log("procFns() 2");
		if(typeof fnArray[procIndex] != 'undefined')
			if(!callbacks.each(fnArray[procIndex], 
				{
					complete: function(){
						Log("procFns() each:callback() procIndex = " + procIndex);
						procIndex++;
						return procFns();
					},
					error: function(){
						Log("Error in procFns() each()");
							return callbacks.error();
					}
				}
			))
				return callbacks.error();
		return true;
	}

	function nextFn(){
		var reqIndex_ = reqIndex++;
		if(reqIndex_ >= fns.length)
			return Log('queueCalls() No more functions');

		fns[reqIndex_]( // call user function
			{
			complete:
				function(data){ // supply data to user.pre
					if(!callbacks.pre(data,
						{
							complete: function(dataPreProc){ // return processed data from user.pre
								Log("nextFn() reqIndex_ = " + reqIndex_ + " fns.length = " + fns.length);
								Log("nextFn() typeof fnArray = " + (typeof fnArray) + ' fnArray = ', fnArray);
								fnArray[reqIndex_] = dataPreProc; // store it away for .each
								nextFn();
								return reqIndex_ == procIndex ? procFns() : true;
							},
							error: function(){
								Log("Error in pre complete() callback()");
								return callbacks.error(), null;
							}
						},
						reqIndex_
					))
						return Log("Error returned from pre() fn:" + callbacks.pre.toString()), callbacks.error(), null;
				},
			error: 
				function(){
					Log("nextFn() Error returned from function callback");
					return callbacks.error(), null;
				}
			}
		);		
	}
}

function getCodeFiles(files, codes, callbacks){
	Log("getCodeFiles() 1 files: ", files);
	var fileIndex = 0;
	var dealListUrls = [], dealListLoading = false;
	
	function sweepDealListUrls(i){
		for(var j = files.length - 1; j >= 0; j--){
			Log('sweep[' + i + ']: ' + dealListUrls[j]);
			if(!(j in dealListUrls))
				return false;
			if(dealListUrls[j]!==false){
				dealListLoading = true;
				return getDealList(dealListUrls[j]);
			}
		}		
	}

	var fileCallbacks = {
		pre:  function preProcCodes(text, callbacks, i){
			var codes = expandCodeFile(text);
			if(!codes){
				Log("Error: expandCodeFile() returned null. Text:\n" + text);
				return null;
			}
			Log("getCodeFiles() preProcCodes() codes = ", codes);
			Log("getCodeFiles() preProcCodes() callbacks = ", callbacks);
			dealListUrls[i] = ('deal_list' in codes) ? codes.deal_list : false;
			sweepDealListUrls(i);
			return callbacks.complete(codes);
			//return getDealList(codes, callbacks);
		},
		each: function(newCodes, callbacks){
			Log("getCodeFiles() each() newCodes = ", newCodes);
			var fName = files[fileIndex++];
			if(!(codes = addCodeData(codes, newCodes)))
				return callbacks.error(), null;
			GM_setValueJSON('codes', codes);
			GM_setValueJSON('fileList', files.slice(0, fileIndex));
			Log('Deleting ' + 'datacache_' + fName);
			GM_deleteValue('datacache_' + fName);
			Log("getCodeFiles() each() codes = ", codes);
			return callbacks.complete();
		},
		complete: function(){
			if(!dealListLoading)
				getDealList();
			delGMValues('datacache_');
			Log("getCodeFiles() complete() codes = ", codes);
			return callbacks.complete(codes);
		},
		error: function(){
			Log("Error in getCodeFiles() callback");
			return callbacks.error();
		}
	};
	return getDataFiles(files, fileCallbacks);
}

function getDataFiles(files, fileCallbacks){
	Log("getDataFiles() 2");
	var fns = new Array(files.length);
	for(var i = 0; i < files.length; i++){
		fns[i] = (function(file){
			return function(callbacks){
				Log("Calling getDataFile() file: " + file + " callbacks: ", callbacks);
				return getDataFile(file, callbacks);
			}
		})(files[i]);
	}
	Log("getDataFiles() 3");
	return queueCalls(fns, fileCallbacks);
}

function GM_setValueJSON(name, value){
	Log("GM_setValueJSON(" + name + ")");
	var json = JSON.stringify(value);
	return GM_setValue(name, json);
}

function getDataFile(fName, callbacks){
	Log("getDataFile() 1 fName = " + fName);
	var data, fCacheName = 'datacache_' + fName;

	Log('getDataFile() 2 fName = ' + fName);
	data = GM_getValue(fCacheName, false);
	if(data === null)
		return callbacks.error();
	Log('getDataFile() 3 callbacks = ', callbacks);
	return data ? callbacks.complete(data) :
		ajaxReq(fName, 
			{ 
				complete: function(text){
					Log('getDataFile() ajax callbacks.complete()');
					GM_setValue(fCacheName, text);
					return callbacks.complete(text); //codes = expandCodeFile(text)) ? callbacks.complete(codes) : callbacks.error();
				},
				error: callbacks.error			
			}
		);
}


function expandCodeFile(text){
	//Log("expandCodeFile() 1 text: " + text);
	try {
		var codes = JSON.parse(text);
	}
	catch(e){
		Log("JSON parse error in expandCodeFile()");
		return null;
	}
	var codesProc = {};
	if(0 in codes){
		Log("expandCodeFile() codes[0]:\n" + codes[0]);
		var json = codes[0].replace(b64RegExp, function(m){
			return '"' + fromBase64(m) + '"';
		});
		//Log("expandCodeFile() json:\n" +  json);
		try {
			codesProc.add = JSON.parse(json);
		}
		catch(e){
			Log("Error parsing json. Text:\n" + json);		
		}
		for(var i = 0; i < codesProc.add.length; i++)
			if(typeof codesProc.add[i] != 'object'){
				var n = parseInt(codesProc.add[i]);
				//codesProc.add.splice.apply(this, i, 1, new Array(n).fill([]));
				
				Array.prototype.splice.apply(codesProc.add, [i, 1].concat(new Array(n).fill([])));
				//Log("expandCodeFile() Added " + n + " filler arrays for desc_id " + i + " codesProc:", codesProc.add);
				i += n - 1;
			}
	}
	if(1 in codes)
		codesProc.deal_list = codes[1].replace('\/', '/');
	if(2 in codes){
		codesProc.remove = JSON.parse(codes[2].replace(b64RegExp, function(m){
			return '"' + fromBase64(m) + '"';
		}));
		
		codesProc.remove[0] = parseInt(codesProc.remove[0]);
		for(var i = 1; i < codesProc.remove.length; i++){
			codesProc.remove[i] = parseInt(codesProc.remove[i]) + codesProc.remove[i-1];
			//Log('codesProc.remove[i]', codesProc.remove[i]);
		}		
	}
	Log("expandCodeFile() 1 codesProc: " + codesProc);

	priceWalk(codesProc.add, function(codes){
		if(typeof codes != 'object'){
			//Log("expandCodeFile() priceWalk() code inflated:", codes);
			return [parseInt(codes)];
		}
		for(var i = 0; i < codes.length; i++)
			codes[i] = parseInt(codes[i]);

		for(var i = 1; i < codes.length; i++)
			codes[i] += codes[i-1];
		//Log("expandCodeFile() codes = ", codes);
		return codes;
	});

	//Log("expandCodeFile() 3 codes = ", codesProc);
	return codesProc;
}

function getDealList(dealListUrl){
	Log("getDealList()");
	if(!('dealListUrls' in arguments.callee))
		arguments.callee.dealListUrls = [];
	if(typeof dealListUrl != 'undefined'){
		arguments.callee.dealListUrls.push( dealListUrl );
		Log("getDealList() dealListUrl = " + dealListUrl);
	}

	if(('lock' in arguments.callee) && arguments.callee.lock){
		Log("Already loading dealList");		
		return false;
	}
	arguments.callee.lock = true;
	Log("getDealList() 1 dealListUrl = " + dealListUrl);
	
	if(!window.dealList){
		var dealList = GM_getValueJSON('dealList', null);
		if(dealList){			
			Log("getDealList() Loading dealList from cache");
			window.dealList = dealList;
			arguments.callee.resolve(dealList); // no return, can resolve multiple times
		}
	}
	
	if(getDealList.dealListUrls.length == 0)
		return arguments.callee.lock =  false;

	var dealListUrl = getDealList.dealListUrls.pop();
	getDealList.dealListUrls = []; // only want the latest
	var dealListUrl_ = GM_getValue('dealListUrl', null);
	if(dealListUrl == dealListUrl_)
		return getDealList.lock = false; // already resolved above

	Log("getDealList() 2"); 
	// new dealList available
	ajaxReq(dealListUrl, function(data){
		Log("getDealList() ajax callback");
		window.dealList = procDealList(data);
		GM_setValue('dealListUrl', dealListUrl);
		GM_setValueJSON('dealList', window.dealList);
		getDealList.resolve(window.dealList);
		getDealList.lock = false;
		return getDealList.dealListUrls.length && getDealList(getDealList.dealListUrls.pop());
	});
	return true;

	function procDealList(resp){
		Log("procDealList() 1 resp = " + resp);
		var dealList = { order: [], deals: [] };
		//resp = resp.replace(/:([^,[\]{}"! ]+)/gi, function(m, p1){
		var phraseList = null;
		resp = resp.replace(/,*phrase_list:([^,}]+)/, function(m, p1){
			phraseList = p1;
			return '';
		});
		Log("procDealList() 2 resp = " + resp);
		resp = resp.replace(/([{,]*)([^:{]+):([^,}]+)/gi, function(m, p1, p2, p3){
			var n = parseInt(fromBase64(p3));
			dealList.order.push(n);
			Log("procDealList() 3 " + m + ': ' + p1 + ' ' + p2 + ' ' + p3 + " => " + n);
			dealList.deals[n] = p2;
			return p1 + '"' + p2 + '":"' +  n + '"';
		});
		Log("dealList:", dealList);
		return dealList;
	}
}

function stripSpaces(str){
	if(typeof str == 'object'){
		var ss = [], s;
		while(s = str.pop())
			ss.push(stripSpaces(s));
		return ss.reverse();
	}
	while(str.indexOf('  ')>=0)
		str = str.split('  ').join(' ');
	return str;
}

function addCodeData(codes, newCodes){
	Log("addCodeData()");
	Log("addCodeData() codes = ", codes);
	Log("addCodeData() newCodes = ", newCodes);
	
	if('deal_list' in newCodes)
		codes.deal_list = newCodes.deal_list;
	if('remove' in newCodes){
		priceWalk(codes, function(cds){
			if(newCodes.remove.length == 0)
				return cds;
			var c = complement(cds, newCodes.remove);
			if(c.length != cds.length){
				var removed = complement(cds, c);
				Log("addCodeData() Codes removed: " + JSON.stringify(removed));
				newCodes.remove = complement(newCodes.remove, removed);
			}
			return c.length > 0 ? c : false;
		});
	}
	if('add' in newCodes) // remove comes before add so codes with altered descriptions can be changed with diffs
		codes = concat(codes, newCodes.add, 'descs'); //concatObj(codes, newCodes.add);
	GM_setValueJSON('codes', codes);
	Log("addCodeData() codes 2 = ", codes);
	return codes
}

function unique(arr){
	//Log("unique() input arr:", arr);
	if(arr.length == 0)
		return arr;
	var u = [];
	for(var i in arr){
		//Log("unique() arr[i] = " + 	arr[i]);
		if(!arr.hasOwnProperty(i))
			continue;
		//Log("unique() 3");
		if(u.indexOf(arr[i])<0)
			u.push(arr[i]);
	}
	//Log("unique() output arr:", u);
	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 indexOf(obj, val){
	for(var i in obj)
		if(obj[i]==val)
			return i;
	return -1;
}

function complementRecurs(supObj, subObj){ // remove items in supObj which are also in subObj
	if('length' in subObj)
		for(var i in subObj){
			if(!subObj.hasOwnProperty(i))
				continue;
			var j = indexOf(supObj, subObj[i]);
			if((typeof subObj[i] != 'object') && (j!=-1)){
				Log("Deleting item: " + supObj[j] + " i: " + i + " j: " + j);
				delete supObj[j];
			}
			else {
				if((j<0) && (i in supObj))
					j = i;
				arguments.callee(supObj[j], subObj[i]);
			}
		}
	else
		for(var i in subObj){
			if(!subObj.hasOwnProperty(i))
				continue;
			if(!(i in supObj))
				continue;
			if(typeof subObj[i] != 'object')
				delete supObj[i];
			else
				arguments.callee(supObj[i], subObj[i]);
		}
}

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

function intersect(array1, array2){
	return array1.filter(function(n){
		return array2.indexOf(n) != -1;
	});
}

function getIntKeys(obj){
	var keys=[];
	for(var key in obj)
		if(obj.hasOwnProperty(key) && !isNaN(parseInt(key)))
			keys.push(key);
	return keys;
}

function sortIntKeys(arr){
	var keys = getIntKeys(arr);
	Log("keys 1 = ", keys);
	keys = keys.length == 1 ? keys : keys.sort(function(a, b){
		return parseInt(a) > parseInt(b) ? 1 : -1;
	});
	Log("keys 2 = ", keys);
	return keys;
}

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){
	vc %= 1000000;
	if(vc > 99999)
		return vc;
	return ("0000" + vc.toString()).substr(-5);
}

function testCode(code, callback){
	Log('testCode() code = ' + code);
	ajaxReq(window.location.origin + "/eStore/en/Basket/ApplyVoucher?voucherCode=" + fmtVCode(code), 
		function(resp){
			var codeStatus = parseResponse(resp, code);
			return callback(codeStatus);
		},
	null);
}

function updateBasket(){
	Log('updateBasket() 1');
	return runUnsafeFunction(function(){
		require(['common/basket'], function(basket){ basket.updateBasket(); });
	});
}

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 getRSA(){
	if(typeof rsa != 'undefined')
		return rsa;
	var n = "58A21762CE28535FB52EF65493B397D30B40E0C216DA6105155C72CC4076726D0CF102CE22FB973A695A37A5F52E9E14CE3A0BE48969FA3BBC11F643AD90DA01CD3AFA8F6CE9BC4BC069663767BC6DC1399091B33AE0698DC497E29FA22B8C0389F614865A52489E9E6B994B4AF8762C0D465CFA7D34AADDAFF15B54B787741D";
	var exp = '10001';
	rsa = new RSAKey();
	initRKey(rsa);
	rsa.setPublic(n, exp);
	rsa.b64Length = Math.ceil(rsa.n.bitLength() / 6);
	return rsa;
}

function verifySigRefObj(refObj){
	return verifySig(refObj.signature, refObj.msg + refObj.timestamp);
}

function verifySig(sigB64, msg){
	b64map = window.b64Map;
	Log('b4map = ' + b64map);
	var genHash = sha1(msg);
	Log("msg: " + msg + ' genhash: ' + sha1(msg));
	var sigHex = b64tohex(sigB64);
	Log('sigHex:' + sigHex);
	var sigBigInt = parseBigInt(sigHex, 16);	
	var receivedHash = getRSA().doPublic(sigBigInt).toRadix(16).substr(-40);
	Log("rec Hash:" + receivedHash);
	Log("gen Hash:" + genHash);
	var verified = genHash == receivedHash;
	if(verified)
		Log('RSA signature Verified');
	else
		Log('RSA signature invalid');
	return verified;
}


function fromBase64(text){
	var b = 1, t=0;
	for(var i = text.length - 1; i>=0; i--){
		t += window.b64Map.indexOf(text[i]) * b;
		b *= 64;
	}
	//Log('fromBase64 text:' + text + ' int:' + t);
	return t;
}

function replaceB64Groups(text){
	if(typeof b64RegExp == 'undefined')
		b64RegExp = new RegExp('[' + b64map + ']+');
	return text.replace(b64Regexp, fromBase64);
}

function ajaxReqJSON(url, callback){
	Log("ajaxReqJSON() url = " + url);
	ajaxReqOpts({url : url}, 
	{
		complete: function(resp){
			Log("ajaxReqJSON() ajaxReqOpts() url: " + url + " return:" + resp.responseText);
			try {
				var json = JSON.parse(resp.responseText);
				Log("ajaxReqJSON() Successfully parsed JSON");
			}
			catch(e){
				Log("ajaxReqJSON() Could not decode raw ajax data for url " + url + ", reformatting...", e);
				return false;
			}
			Log("ajaxReqJSON() outside try/catch");
			callback(json, resp.responseText);
			return true;
		},
		error: nil
	})
}

function ajaxReqBin(url, callback){
	Log("ajaxReqBin() url = " + url);
	var reqObj = {
		url: url,
		binary: true,
		overrideMimeType: 'text/plain; charset=x-user-defined'
	};

	var t = 0;

	ajaxReqOpts(reqObj, function(resp){
		Log("ajaxReqBin() ajaxReqOpts() ajax return, resp = " + JSON.stringify(resp));
		var text = '';
		for(var i = 0; i < resp.responseText.length; i++)
			text += String.fromCharCode(resp.responseText.charCodeAt(i) & 0xFF);
		if(callback(text))
			return true;
		if(t++ < max_ajax_callback_retry)
			return false;
		return true;
	});
}

function deCompactJSON(text, keys, vals){
	Log("deCompactJSON() 1 ");
	Log("deCompactJSON() 1 text = " + text);
	if(typeof keys == 'undefined')
		var keys = false;
	if(typeof vals == 'undefined')
		var vals = false;
	text = text.split("\n").join('');
	text = text.split('"').join('');
	text = text.replace(/([^:,{}[\]]+)/g, '"$1"');
	Log("deCompactJSON() 2 text = " + text);
	//Log("deCompactJSON() 2 text = " + text);
	while(text.indexOf(',,')>=0)
		text = text.split(',,').join(',[],');
	
	if(keys || vals){
		var regex = '[^"[\\]{}:,]+';
		if(keys && vals)
			;
		else
			if(keys)
				regex += '(?=":)'
			else
				if(vals)
					regex = '(?<=:")' + regex;
		//Log("deCompactJSON() 3 regex = " + regex);
		var rx = new RegExp(regex, 'g');
		text = text.replace(rx, fromBase64);
	}

	//Log("deCompactJSON() 4 text = " + text);
	try {
		var json = JSON.parse(text);
	}
	catch(e){
		Log("Error in deCompactJSON() text: " + text);
		return null;
	}
	Log("deCompactJSON() Successfully decoded JSON string");
	return json;
}

function ajaxReq(url, callback, data){
	Log("ajaxReq() url = " + url);
	var retry = 0;
	if(typeof callback == 'function')
		callback = { complete: callback, error: nil };
	function fail_(res){
		fail(res, "Error in ajaxReq() url = " + url);
		return callback.error();
	}

	var reqObj = {
		url: url
	};

	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" };
		}
	}
	ajaxReqOpts(reqObj, 
	{
		complete: function(resp){
			return callback.complete(resp.responseText);
		},
		error: function(){
			Log("Error in return from ajaxReqOpts() to ajaxReq()");		
		}
	});
}

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 mkObserver(filterFn, contextNode){
	var observer = new MutationObserver(
		function(mutations){
			for(var i = 0; i < mutations.length; i++){
				//Log('mutation:', mutations[i]);
				for(var j = 0; j < mutations[i].addedNodes.length; j++){
					//Log("mutation type: " + mutations[i].type);
					if(mutations[i].type != 'childList')
						continue;
					if(filterFn(mutations[i].addedNodes[j])===null)
						return observer.disconnect(), null;
				}
			}
		}
	);
	observer.observe(contextNode, { attributes: false, childList: true, subtree: true, characterData: false });
	return observer;
}

function initBasketObserver(){
	// create an observer instance
	var basket_rows = document.getElementById('basket_rows');
	if(!basket_rows){
		var observer = mkObserver(function(node){
			if(node.id == 'basket_rows'){
				basket_rows = node;
				return observeVouchers(), null;
			}
			return true;
		}, document);
	}
	else 
		observeVouchers();
		
	function observeVouchers(){
		return mkObserver(function(node){
			Log('observeVouchers() callback()');
			if(!('className' in node))
				return true;
			Log('observeVouchers() callback() className: ' + node.className);
			if(node.className.indexOf('voucher-container')>=0){
				chkBasketVoucher(node);
				Log('observeVouchers() callback() 3');
			}
			else
				if(node.className.indexOf('total-container')>=0){
					Log('observeVouchers() callback() 4');
					//sendCodeBatch();
				}
			return true;
		}, basket_rows);
	}
}


function scrapePageCodes(){
	if(typeof pageScraped != 'undefined')
		return;
	pageScraped = true;
	Log("scrapePageCodes()");
	var scrapedCodes = [];
	for(var i in unsafeWindow){
		if(!unsafeWindow.hasOwnProperty(i))
			continue;
		if(i.indexOf('target-image')!==0)
			continue;
		var code = parseInt(i.split('-').pop());
		Log("scrapePageCodes() Code found on page: " + code);
		if(getCodeData(code))
			continue;
		scrapedCodes.push(code);
	}
	
	if(scrapedCodes.length === 0)
		return false;
	var lastScraped = GM_getValueJSON('scrapedCodes', []);
	if(isSubset(lastScraped, scrapedCodes))
		return;
	GM_setValueJSON('scrapedCodes', scrapedCodes);
	var newCodes = complement(scrapedCodes, lastScraped); // newCodes = newCodes.concat(complement(scrapedCodes, lastScraped));
	Log("newCodes = ", newCodes);
	for(var i = 0; i < newCodes.length; i++)
		encodeOp(newCodes[i], 'new');
	return true; //sendCodeBatch();
}

RSA_KEY_SZ = 1024;
MAX_MSG_LENGTH = RSA_KEY_SZ - 10 - 2 - 70; // 10 bit padding_sz, 2 bit MSB boundary, 70 bits entropy
CODE_OPS = ['expired', 'new', 'wrong', 'spc'];

function testEnc(){ // debug only
	Log('onload');
	if(typeof unsafeWindow.ecommerceData == 'undefined')
		return setTimeout(arguments.callee, 250);
	var b = new BigInteger('1010101011111111');
	//sendCodeBatch(b);
	Log('b64:' + bigInt2Base64(b));
}

function encodeOp(n, op){
	Log("encodeOp()");
	var bits; //deal 9 bits, code 19bits
	if(op == 'dealNone'){
		bits = 9 + 1;
		n = (n << 1); // bit0 = 0
	}
	else {
		bits = 19 + 3;
		n = (n << 3) | (CODE_OPS.indexOf(op) << 1) | 1; // bit0 = 1
	}

	var refQ = GM_getValueJSON('refQ', []), sentRefs = GM_getValueJSON('sentRefs', []);
	if(hasRef(n, refQ) || hasRef(n, sentRefs))
		return Log("Already existing for n = " + n + " op = " + op), true;
	
	if(getTotalBitLength(refQ) + bits > MAX_MSG_LENGTH) //bigInt.bitLength()
		sendCodeBatch();

	refQ.push({n : n, bits: bits});
	GM_setValueJSON('refQ', refQ);
	Log('refQ:', refQ);	
}

function hasRef(n, a){
	for(var i = 0; i < a.length; i++)
		if(a[i].n == n)
			return true;
	return false;
}

function complementRefQ(sup, sub){ // O(n log n)
	Log("complementRefQ()");
	Log("complementRefQ() sup.length = " + sup.length + " sub.length = " + sub.length);
	var ns = {};
	for(let ref of sup)
		ns[ref.n] = ref;
	var union = sup.concat(sub);
	union.sort(function(a,b){
		if(a.n==b.n){
			delete ns[a.n];
			return 0;
		}
		return a.n > b.n ? 1 : -1;			
	});
	var c = [];
	for(let n in ns)
		if(ns.hasOwnProperty(n))
			c.push(ns[n]);
	Log("complementRefQ() c.length = " + c.length);
	return c;
}

function mask(n, bits){
	var mask = (1 << bits) - 1;
	return n & mask;
}

function getTotalBitLength(a){
	for(var t = 0, i = 0; i < a.length; i++)
		t += a[i].bits;
	return t;
}

function appendToBigInt(n, bits, bigInt){
	var bi;
	if((typeof bigInt == 'undefined') || !bigInt || (!bigInt.bitLength())){
		bi = nbv(1 << (bits - 1));
		bi.clearBit(bits - 1);
	}
	else {
		Log("appendToBigInt() 1 bitLength = " + bigInt.bitLength());
		bi = bigInt.shiftLeft(bits);
	}
	bi[0] |= n; // terrible...
	Log("appendToBigInt() 2 bitLength = " + bi.bitLength());
	return bi;
}

function sendCodeBatch(callback){
	Log("sendCodeBatch() 1");
	if(typeof callback == 'undefined')
		callback = nil;
	var refQ = GM_getValueJSON('refQ', []);
	if(!refQ.length)
		return callback(), false;
	Log("sendCodeBatch() 1c");
	if(!('scb' in window))
		window.scb = 1;
	else 
		if(window.scb)
			return window.scb++, false;

	Log("sendCodeBatch() 2");

	//var refQ_ = [];
	var sentRefs = GM_getValueJSON('sentRefs', []);
	refQ = shuffle(refQ); // guaranteed unique by encodeOp()
	/*for(var i = 0; i < refQ.length; i++){ // don't need to check length because encodeOp() batches it
		if(sentRefs.indexOf(refQ[i].n) >= 0)
			continue;
		refQ_.push(refQ[i]);
		sentRefs.push(refQ[i].n);
		Log("sendCodeBatch() 2b Added n = " + refQ[i].n + " to refQ_");
	}
	//refQ_ = [...new Set(sentRefs)];
	Log("sendCodeBatch() 3a refQ_.length = " + refQ_.length);
	refQ = shuffle(refQ_); // entropy++	*/
	Log("sendCodeBatch() 3b refQ.length = " + refQ.length);
	
	var bigInt = null;
	for(var i = 0; i < refQ.length; i++){
		bigInt = appendToBigInt(refQ[i].n, refQ[i].bits, bigInt);
		Log("Prog bigInt:" + bigInt2Hex(bigInt));
	}
	
	Log("sendCodeBatch() 4");

	var n = sessionVars.orderDetails.delivery ? 1 : 0;
	n = (n << 9) | (sessionVars.orderDetails.store % 1000);

	bigInt = appendToBigInt(n, 10, bigInt);
	bigInt = encryptBigInt(bigInt);
	getTime(
		function(time){
			bigInt = appendToBigInt(time - 1000000000, 5 * 6, bigInt);
			Log("sendCodeBatch() ref   hex:\n" + bigInt2Hex(bigInt));
			ref = bigInt2Base64(bigInt) + '.me';
			ajaxSendRef(getSendCodesURL(), ref,
				{
					complete: function(resp){
						GM_setValueJSON('sentRefs', sentRefs.concat(refQ));
						var refQNew = complementRefQ(GM_getValueJSON('refQ', []), sentRefs);
						GM_setValueJSON('refQ', refQNew);
						Log("sendCodeBatch() ajax callback");
						return callback();
					},
					error: function(){
						Log("Error in sendCodeBatch() ajax call");
					}
				}
			);			
			Log("sendCodeBatch()   ref b64:\n" + ref);
		}
	);
	return --window.scb ? arguments.callee(callback) : true;
}

function encryptBigInt(bigInt){
	var rsa = getRSA();
	//Log("encryptBigInt() bigInt.bitLength()  1 = " + bigInt.bitLength());
	bigInt = padBigInt(bigInt)
	bigInt = bigInt.modPowInt(rsa.e, rsa.n);
	//Log("encryptBigInt() bigInt.bitLength()  3 = " + bigInt.bitLength());
	//Log("encrypted hex:" + bigInt2Hex(bigInt));
	return bigInt;
}

function padBigInt(bigInt){ // because pkcs is stupid
	if(!('rng' in window))
		window.rng = new SecureRandom();
	Log("padBigInt() bigInt.bitLength()  1 = " + bigInt.bitLength());

	bigInt = appendToBigInt(bigInt.bitLength(), 10, bigInt); // bigInt.bitLength() changes within this function
	
	Log("padBigInt() bigInt.bitLength()  2 = " + bigInt.bitLength());
	
	var MSBBoundaryBits = 2;
	var msgLength = bigInt.bitLength();
	var padBits = RSA_KEY_SZ - msgLength - MSBBoundaryBits;
	var nBytes = Math.ceil(padBits / 8);
	var rBits = padBits % 8;
	Log('padBits = ' + padBits + ' rBits = ' + rBits + ' nBytes = ' + nBytes);
	var ba = new Array(nBytes);
	rng_get_bytes(ba);
	var padding = new BigInteger(ba, null);
	Log("padBigInt() padding.bitLength()  1 = " + padding.bitLength());
	
	if(rBits)
		padding = padding.shiftRight(8 - rBits);
	Log("padBigInt() padding.bitLength()  2 = " + padding.bitLength());
	padding = padding.shiftLeft(RSA_KEY_SZ - padBits - MSBBoundaryBits);
	Log("padBigInt() padding.bitLength()  3 = " + padding.bitLength());
	bigInt = bigInt.add(padding);
	Log("padBigInt() bigInt.bitLength()  5 = " + bigInt.bitLength());
	return bigInt;
}

function bigInt2Hex(BigInt){
	var bigInt = BigInt.shiftRight(0);
	Log("bitLength: " + bigInt.bitLength());
	var hexArr = [];
	while(bigInt.bitLength()){
		hexArr.push(byte2Hex(255 & bigInt[0]));
		bigInt = bigInt.shiftRight(8);
	}
	return hexArr.reverse().join('');
}

function bigInt2Base64(bigInt){
	Log('bigInt2Base64()');
	Log('bitLength 1:' + bigInt.bitLength());
	var b64Arr = [];
	for(var bitLength = bigInt.bitLength(); bitLength > 0; bitLength -= 6){
		b64Arr.push(window.b64Map[bigInt[0] & 63]);
		bigInt = bigInt.shiftRight(6);
	}
	return b64Arr.reverse().join('');
}

function strReverse(str){
	return str.split('').reverse().join('');
}

function int2Base64(n, padTo){
	var b64Str = '';
	while(n){
		b64Str += window.b64Map[(n & 63)]; // faster to build in reverse
		n >>= 6;
	}
	for(var pad = padTo - b64Str.length; pad > 0; pad--)
		b64Str += window.b64Map[0];
	return strReverse(b64Str.substr(0, padTo));
}

function getDesc(code){
	ajaxReq(window.location.origin + '/eStore/en/Voucher?voucherCode=' + code, function(data){
		var div = document.createElement('div');
		div.innerHTML = data;
		var desc = div.querySelector('#product-name-label').textContent;
		var price = div.querySelector('#product-description-label').textContent;
		var p = price.indexOf('$');
		price = p < 0 ? 0 : price.substring(p, price.indexOf(' ', p + 1));
		Log('getDesc() code: ' + code + ' price: ' + price + ' desc: ' + desc);
		return chkCode(code, price, desc), true;
	});
}

function chkBasketVoucher(voucher){
	Log("chkBasketVoucher() 1");
	if(typeof checkedBasketCodes == 'undefined')
		window.checkedBasketCodes = [];
	var code = parseInt(voucher.className.split('at-basket-voucher-')[1]);
	var desc = voucher.getElementsByClassName('at-description')[0].childNodes[1].textContent.trim();
	var price = voucher.getElementsByClassName('at-voucher-price')[0].textContent;
	if(price.length == 0){
		price = voucher.getElementsByClassName('at-product-price');
		if(price.length)
			price = price[0].textContent;
		else {
			if((desc.indexOf('%') < 0) && (desc.indexOf('Off') < 0))
				return getDesc(code);
			else
				price = 0;
		}
	}
	
	return chkCode(code, price, desc);
}

function chkCode(code, price, desc){
	Log("chkCode() 1");
	if(desc.split('/').length > 1) // weird multi deals
		return true;
	Log("chkCode() 3");
	var data = getCodeData(code);
	data.desc = window.dealList.deals[data.desc_id];
	Log("chkCode() 4");
	if(price){ //compare price
		price = Math.round(parseFloat(price.substr(1)) * 100);
		if(price != data.price){
			Log("Stored price incorrect: " + data.price + " Retrieved price: " + price + " Sending code: " + code);
			encodeOp(code, 'wrong');
		}
	}
	Log("Stored Description: " + data.desc);
	Log("Retrieved Description: " + desc);
	if(!matchWords(desc, data.desc)){
		Log("Sending wrong code: " + code);
		encodeOp(code, 'wrong');
	}
	return true;
}

function stripDesc(text){ // text must be lower case
	var remove = ['from', 'for', /del\S*/, /pick\S*/, /\$[0-9\.]+/, /[0-9\.]+%/, 'belgian', 'range', 'any', 'large', 'with', ' free', 'whole', 'order', 'menu', 'price', 'chipotle', 'spicy', 'drink', 'oven', 'cut'];
	var replace = { ' pack': 'pk', e: '&eacute;', chocolate: /choc\b/, creme: /cr\S+me\b/ };
	text = text.toLowerCase();
	remove.forEach(function(r){
		text = text.replace(r, ' ');
	});
	for(var n in replace)
		text = text.replace(replace[n], n);
	
	text = text.split(';')[0];
	text = stripSpaces(text).trim();
	return text;
}

function matchWords(text1, text2){
	Log('matchWords() text1: ' + text1 + ' text2: ' + text2);
	text1 = stripDesc(text1);
	text2 = stripDesc(text2);
	var words1 = splitText(text1), words2 = splitText(text2);
	if(words1.every(matchWord, words2) && words2.every(matchWord, words1)){
		Log("Description matches, all keywords found");
		return true;
	}
	Log("Error: not all keywords found");
	return false;

	function matchWord(word){
		Log("matchWords() Testing word: " + word);
		if(this.indexOf(word) == -1){
			Log("matchWords() Word not matched: " + word);
			return false;
		}
		return true;
	}

	function splitText(text){
		return text.replace(/s\b/g, '').match(/\b[\w.]+\b/g);
	}
}

function getSendCodesURL(){
	var i = Math.floor(Math.random() * sendCodesURLs.length);
	return "https://goo.gl/" + sendCodesURLs[i];
}

function getTime(callback){
	Log('getTime()');
	var now = new Date().getTime() / 1000;
	if(now < GM_getValue('timeLastSet', 0) + 3600)
		return callback(now - GM_getValue('timeDiff', 0));
	ajaxReqAll('http://www.asio.gov.au', '', 'HEAD', 
		{
			complete: function(resp){
				Log('getTime() response');
				callback(new Date().getTime() / 1000 - GM_getValue('timeDiff', 0));
			},
			error: function(){
				Log("Error in getTime() callback");
			}
		}
	);
}

function setTimeDiff(resp){
	var st = resp.responseHeaders.indexOf('Date: ') + 6;
    var en = resp.responseHeaders.indexOf("\n", st);
	
	var server = new Date(resp.responseHeaders.substring(st, en)).getTime();
	var pc = new Date().getTime();

	var timeDiff = (pc - server) / 1000;
	GM_setValue('timeDiff', timeDiff);
	GM_setValue('timeLastSet', pc);	
}

apiKey = "key=AIzaSyCApOR49mifYDzj8juYocoKTTapQ1R6U-4&";


function ajaxReqAll(url, ref, method, callbacks){
	Log('ajaxReqAll() url: ' + url + ' ref: ' + ref);
	var opts = {
		method: method,
		url: url,
		referer: ref,
	};
	ajaxReqOpts(opts, callbacks);
}

function ajaxSendRef(url, ref, callbacks){
	Log('ajaxSendRef() url: ' + url + ' ref: ' + ref);
	var opts = {
		method: 'HEAD',
		url: url,
		referer: ref,
		onerror: callbacks.complete
	};
	ajaxReqOpts(opts, callbacks);
}

function ajaxReqOpts(opts, callbacks){
	Log("ajaxReqOpts() opts:", opts);
	Log("ajaxReqOpts() callback:", callbacks);
	var retry = 0, retry_callback = 0;

	var reqObj = {
		method: 'GET',
		onload: function(resp){
			Log('ajaxReqOpts return');
			setTimeDiff(resp);
			if(typeof callbacks == 'undefined')
				return null;
			//Log("ajaxReqOpts() resp:", resp);
			Log("ajaxReqOpts() responseText:" + resp.responseText);
			if(callbacks.complete(resp) !== false)
				return true;
			if(retry_callback++ < max_ajax_callback_retry){
				Log("ajaxReqOpts() callbacks failed, retrying " + retry_callback + '/' + max_ajax_callback_retry);
				retry = 0;
				ajax_wrapper();
			}
			else {
				Log("Maximum errors exceeded for ajaxReqOpts()");
				return callbacks.error(), null;				
			}
		},
		timeout: ajax_timeout,
		ontimeout: fail_,
		onerror: fail_
	};

	for(var i in opts)
		reqObj[i] = opts[i];

	if('referer' in reqObj){
		if(!('headers' in reqObj))
			reqObj.headers = {};
		reqObj.headers.Referer = reqObj.referer;
		delete reqObj.referer;
	}

	function ajax_wrapper(){
		Log("ajax_wrapper() retry = " + retry);
		if(retry++ >= max_retry)
			return;
		Log("ajax_wrapper() calling GM_xmlhttpRequest()");
		var ret = GM_xmlhttpRequest(reqObj);
		Log("ret:" + JSON.stringify(ret));
		Log("ajax_wrapper() 2 opts = " + JSON.stringify(reqObj));
	};

	ajax_wrapper();

	function fail_(res){
		fail(res, "Error in ajaxReqOpts() url = " + reqObj.url);
		ajax_wrapper();
	}
}

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){
	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(!getCodeData(code))
				encodeOp(code, 'new');
				//newCodes.push(code);
			break;
		case 'expired':
			encodeOp(code, 'expired');
			//expiredCodes.push(code);
			break;
		case 'validCode':
			Log('Valid code found:' + code);
		case 'wrongTime':
			if(!getCodeData(code))
				encodeOp(code, 'new');
				//newCodes.push(code);
			break;
	}
	return r;
}

function getCodeData(code){
	return priceWalk(codes, function(codeRow){
		//Log('codeRow:', codeRow);
		return codeRow.indexOf(code) == -1 ? null : true;
	}, false);
}

function getSessionVars(){
	Log("getSessionVars()");
	sessionVars = ldSessionVars();
	if('orderDetails' in sessionVars){
		Log("getSessionVars() Loading cached copy");
		return getSessionVars.resolve(sessionVars);
	}
	Log("getSessionVars() 2");
	
	(function getOrderDetails(){
		Log("getSessionVars() getOrderDetails()");
		if(!('ecommerceData' in unsafeWindow)){
			Log("getSessionVars() making observer");
			mkObserver(function(node){
				if((!('tagName' in node)) || (node.tagName.toUpperCase() != 'SCRIPT'))
					return true;
				if(node.textContent.indexOf('ecommerceData')<0)
					return true;
				Log("getSessionVars() found script");
				scrapeSessionVars(node.textContent);
				return null;
			}, document);
		}
		else
			readSessionVars();
	})();
	return true;
	
	function scrapeSessionVars(text){
		Log("getSessionVars() scrapeSessionVars()");
		var da = text.indexOf('"deliveryAddress":');
		var st = text.indexOf('"store":', Math.max(da, 0)) + 10;
		return readSessionVars({
			store: parseInt(text.substr(st, 5)),
			delivery: da > 0
		});
	}

	function readSessionVars(orderDetails){	
		sessionVars.orderDetails = typeof orderDetails != 'undefined' ?
			orderDetails :
			{
				store: unsafeWindow.ecommerceData[0].additionalFields.store,
				delivery: 'deliveryAddress' in unsafeWindow.ecommerceData[0].additionalFields
			};
		writeSessionVars();
		return getSessionVars.resolve(sessionVars);
	}
}

function setupGMMenu(){
	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'
	);
		GM_registerMenuCommand( "Codes/Minute", function(){
			GM_addStyle("#cpm { padding: 0.6em; border-radius: 0.5em; background-color: #efefef; color: black; position: fixed; width:12em; top: 10em; right: 5em; z-index: 5000!important; } #cpm input { border-width: 0!important; font-size: inherit!important; color: gray!important; width: 2em; z-index: inherit!important; position: relative!important; border-radius:0.3em;} #cpm div { margin-top: 0.4em; color: gray; font-size: 0.8em;}");
			GM_addStyle("#cpm.hdden { visibility: hidden; opacity: 0; transition: visibility 0s 0.4s, opacity 0.4s linear;}");
			$("<div id='cpm'>Codes per Minute: <input type='text'><br><div>Setting this too high may result in a temporary IP block</div></div>").appendTo(document.body).keypress(
				function(e){
					if(e.keyCode != 13)
						return true;
					var maxCPM_ = parseInt($("#cpm input").val());
					if(isNaN(maxCPM_) || !maxCPM_)
						return alert("Please enter a valid number");
					maxCPM = maxCPM_;
					GM_setValue('maxCPM', maxCPM);
					var $cpm = $("#cpm").addClass('hdden');
					setTimeout(function(){ $cpm.remove(); }, 800);
				}
			).children().val(maxCPM);
		}, 'C'
	);
}

function getDateString(unixTimestampB64){
	var timestamp = new Date((fromBase64(unixTimestampB64) + 1000000000) * 1000);
	return timestamp.toString();
}

function countCodes(codes){
	Log("countCodes() 1");
	var n = 0;
	priceWalk(codes, function(row){ n+= row.length; Log("countCodes() walk"); return null; }, false);	
	return n;
}

function validPage(page){
	var v = ['Product', 'Voucher'];
	for(var i = 0; i < v.length; i++)
		if(page.indexOf(v[i])==0)
			return true;
	return false;
}

function nil(){
	return true;
}

function writeSessionVars(flush){
	Log("writeSessionVars()");
	if((typeof flush != 'undefined') && flush)
		sessionVars = { invalidDescIds: [], validDescIds: [], newSPCodes: { pickup: {}, delivery: {} } };
	Log("writeSessionVars() sessionVars:", sessionVars);
	return GM_setValueJSON('sessionVars', sessionVars), sessionVars;
}

function ldSessionVars(){
	Log("ldSessionVars()");
	var sessionVars = GM_getValueJSON('sessionVars', null);
	sessionVars = sessionVars ? parseSessionVars(sessionVars) : writeSessionVars(true);
	Log("ldSessionVars() sessionVars:", sessionVars);
	return sessionVars;

	function parseSessionVars(sessionVars){		
		for(var i in sessionVars){
			if(!sessionVars.hasOwnProperty(i))
				continue;
			for(var j in sessionVars[i]){
				if(!sessionVars[i].hasOwnProperty(j))
					continue;
				sessionVars[i][j] = parseInt(sessionVars[i][j]);
			}
		}
		return sessionVars;
	}
}

function getShortUrl(url, callback){
	return ajaxReqOpts(
	{
		method: "POST",
		url: "https://www.googleapis.com/urlshortener/v1/url?key=AIzaSyCApOR49mifYDzj8juYocoKTTapQ1R6U-4",
		headers: {
			"Content-Type": "application/json",
			//"Accept": "text/xml"            // If not specified, browser defaults will be used.
		},
		data: JSON.stringify({longUrl:url})//{\"longUrl\": \"url\"}"
	}, 
	{
		complete: function(response){
			var a;
			try {
				a = JSON.parse(response.responseText.split("\n").join('').split("\r").join(''));
			}
			catch(e){
				Log("JSON parse error in getSPCStoreRefUrl() complete()");
				return null;
			}
			return callback(a.id);
		},
		error: function(){
			Log("Error retrieving shortUrl");
			return null;
		}	
	}
	);
}

function Promise_(fn, run){
	Log('Promise_() promise() constructor');
	this.callbacks = [];
	this.data = null;
	Log('Promise_() promise() constructor 2');
	this.resolve = (function(th){
		return function(data){
			Log('this', th);
			th.data = data;
			Log('Promise_() promise() callbacks:', th.callbacks);
			for(let callback of th.callbacks)
				callback();
		};
	})(this);
	fn.resolve = this.resolve;
	Log('Promise_() promise() constructor 3');
	if((typeof run == 'undefined') || run)
		fn();
	Log('Promise_() promise() constructor 4');
}

function logPromises(text, promises){
	var p = [];
	for(let promise of promises)
		p.push(promiseDump(promise));
	Log(text, p);
}

function promiseDump(promise){
	var t, l = {};
	for(let i in promise){
		t = typeof promise[i];
		if((t == 'string') || (t == 'number'))
			l[i] = promise[i];
	}
	return l;
}

function promiseAll(promises, fnThen){
	logPromises("promiseAll() promises:", promises);
	for(let promise of promises)
		promise.callbacks.push(chkComplete);
	return chkComplete(), true;

	function chkComplete(){
		logPromises("promiseAll() chkComplete() promises:", promises);
		var dataArr = [];
		for(let promise of promises){
			if(!promise.data)
				return false;
			dataArr.push(promise.data);
		}
		return fnThen(dataArr);
	}
}

function getSPCodes(arr){
	var storeUrlList = arr[0], sessionVars = arr[1];
	Log("getSPCodes()");
	Log("getSPCodes() arr:", arr);
	var spCodes = GM_getValueJSON('spCodes_' + sessionVars.orderDetails.store, null);
	if(spCodes)
		getSPCodes.resolve(window.spCodes = spCodes, true); // no returning as there might be newer ones
	
	var storeRefUrl = getSPCStoreRefUrl(sessionVars.orderDetails.store, storeUrlList);
	if(storeRefUrl === false)
		return getStoreUrlList(true); 
	Log("getSPCodes() getSPCStoreRefUrl() callback store = " +  sessionVars.orderDetails.store + " storeRefUrl = " + storeRefUrl);
	var spCodes = [];
	getLatestRef(storeRefUrl, function(reqObj){
		Log("getSPCodes() getSPCStoreRefUrl() getLatestRef() callback reqObj", reqObj);
		if(!reqObj)
			return null;
		for(var last = 0, i = 0; i < reqObj.msg.length; i+=3){
			last += fromBase64(reqObj.msg.substr(i, 3));
			spCodes.push(last);
		}
		GM_setValueJSON('spCodes_' + sessionVars.orderDetails.store, spCodes);
		Log("getSPCodes() getSPCStoreRefUrl() getLatestRef() callback spCodes = " + spCodes.join(','));
		return getSPCodes.resolve(window.spCodes = spCodes);
	}, 90000);
	return true;
}

function getSPCStoreRefUrl(store, storeUrlList){
	Log("getSPCStoreRefUrl()");
	st = int2Base64(store - 98000, 2);
	for(var i = 0; i < storeUrlList.length; i+=8){
		if(storeUrlList.substr(i, 2)==st){
			var path = storeUrlList.substr(i + 2, 6);
			Log('parseSPCRefUrl() store = ' + store + ' path = ' + path);
			return 'goo.gl/' + path;
		}
	}
	return false;
}

function applySPCodes(arr){
	Log("applySPCodes()");
	if(!('lock' in applySPCodes))
		applySPCodes.lock = true;
	else
		if(applySPCodes.lock)
			return false;
	applySPCodes.lock = true;
	Log("applySPCodes() 2");
	var codeList = arr[0], spCodes = arr[1];
	Log("applySPCodes() codes:", spCodes);
	priceWalk(codeList, function(codeRow){
		if(spCodes.length == 0)
			return true;
		var cs = intersect(spCodes, codeRow);
		if(cs.length == 0)
			return null;
		//spCodes = complement(spCodes, codeRow); // disabled as code may appear more than once
		return cs.concat(complement(codeRow, cs)); // put them at the start of the array
	});
	//GM_setValueJSON('codeList', codeList); // disabled, as may use multiple stores.
	createCodeList.resolve(codeList);
	applySPCodes.lock = false;
}

function getStoreUrlList(flush){
	var data;
	if((typeof flush == 'undefined') || !flush){
		data = GM_getValue('StoreUrlRefList', null);
		if(data)
			return getStoreUrlList.resolve(data);
	}

	return getLatestRef(storeListRef, function(reqObj){
		Log("getStoreUrlList() getLatestRef() callback reqObj", reqObj);
		if(!reqObj)
			return null;
		var listUrl = 'https://paste.ee/r/' + reqObj.msg;
		Log("getStoreUrlList() getLatestRef() callback listUrl = " + listUrl);
		ajaxReq(listUrl, function(data){
			GM_setValue('StoreUrlRefList', data);
			Log("getStoreUrlList() getLatestRef() callback ajax callback data:" + data);
			return getStoreUrlList.resolve(data);
		});
	});
}