// #mark Update queue

function Queue () {

	var self = this;

	this.reset = function () {
		this.pendingItems = [];
		this.activeItems = [];
		this.froms = [];
		this.active = [];
		this.delay = 500;
	}
	
	this.reset();

	this.add = function (delivery) {
		if (delivery.passRequired && !delivery.oneByOne) {
			var checkValue = delivery.extra;
			var previousValue = this.froms[delivery.from];
			if (previousValue) {
				if (previousValue != checkValue) delivery.oneByOne = true;
			} else {
				this.froms[delivery.from] = checkValue;
			}
		}
		this.pendingItems.push(delivery);
	}

	this.update = function () {
		if (this.pendingItems.length) {

			var addBack = [];

			do {
				var delivery = this.pendingItems.shift();
				var oneByOne = delivery.oneByOne;
				if (oneByOne && this.active[delivery.from]) {
					addBack.push(delivery);
				} else {
					this.activeItems.push(delivery);
					this.active[delivery.from] = true;
					delivery.updateStatus();
				}
			} while (this.pendingItems.length);

			while (addBack.length) {
				var delivery = addBack.shift();
				this.pendingItems.push(delivery);
			}

		} else if (dashboardWidget) {
			setTimeout(finishUpdate,500);
		}
	}

	this.completed = function (delivery) {
		var index = this.activeItems.indexOf(delivery);
		if (index >= 0) this.activeItems.splice(index,1);
		this.active[delivery.from] = false;
		if (!this.busy()) this.reset();
	}

	this.busy = function () {
		if (this.pendingItems.length > 0) return true;
		if (this.activeItems.length > 0) return true;
		return false;
	}

}

// #mark Deliveries and updating

function Delivery (settings) {

	var self = this;
	this.req = [];

	// Functions
	this.setUpdatableFunctions = setUpdatableFunctions;
	this.setUpdatableFunctions();

	this.setDelivery = setDelivery;
	this.loadItemCache = loadItemCache;
	this.loadPage = loadPage;
	if (typeof loadCookiePage == 'function') {
		this.loadCookiePage = loadCookiePage;
	} else {
		this.loadCookiePage = loadPage;
	}
	this.loadSection = loadSection;
	this.startRequests = startRequests;
	this.resetRequests = resetRequests;
	this.connectFailed = connectFailed;
	this.notFound = notFound;
	this.notSupported = notSupported;
	this.shouldShowError = shouldShowError;
	this.updateComplete = updateComplete;
	this.updateItems = updateItems;
	this.showMessage = showMessage;
	this.saveItems = saveItems;

	if (dashboardWidget) {
		this.loading = false;
		this.spinAngle = 0;
		this.startLoad = startLoad;
		this.stopLoad = stopLoad;
		this.addButtons = addButtons;
		this.editParent = editParent;
		this.closeParent = closeParent;
	}
	this.setDelivery(settings);

}

function setUpdatableFunctions () {
	this.updateStatus = updateStatus;
	this.startUpdate = startUpdate;
	this.cleanLocation = cleanLocation;
}

function setDelivery (settings) {

	this.reqCount = 0;
	this.reqComplete = 0;

	//  Success values:
	// -1: Couldn't connect: no internet or keychain locked
	//  0: Couldn't connect: site down
	//  1: Not found, no password, needs sign in, or no cookies
	//  2: Found

	this.success = 2;
	this.lastUpdate = false;
	this.lastError = false;
	this.nextUpdate = 0;
	this.backgroundUpdate = 0;
	this.interval = 0;
	this.items = [];
	this.previousItems = false;
	this.count = 0;

	this.fromEdited = false;
	this.skipExtra = false;
	this.tries = 0;
	this.tap = false;

	// Initialization
	this.id = settings.uuid;
	this.hash = (settings.hash) ? settings.hash : '';
	this.name = (settings.name) ? settings.name : '';
	this.from = (settings.from) ? settings.from : 'www.amazon.com';
	this.extra = (settings.extra) ? settings.extra : '';
	this.no = (settings.no) ? settings.no : '';
	this.date = (settings.date) ? new Date(settings.date.toString().replace(/-/g,'/')) : false;
	this.modified = (settings.modified) ? parseFloat(settings.modified) : 0;
	this.displayed = (settings.displayed) ? settings.displayed : 0;

	if (dashboardWidget) this.lastUpdated = (settings.lastupdated) ? parseFloat(settings.lastupdated) : 0;

	this.usedItemCache = false;
	this.clearItemCache = false;
	var cacheJSON = settings.itemCacheJSON;
	// Make sure the cache includes the number, in case the delivery was edited
	if (cacheJSON && cacheJSON.indexOf(this.no) !== -1) {
		this.itemCache = JSON.parse(cacheJSON);
	}

	this.password = '';
	this.keychainLocked = false;

	setService(this,false,false);
	this.fromName = getFromName(this.from,false,false);
	this.fromTrans = getFromName(this.from,true,false);

}

function setServiceByNumber (no,allowed) {
	var result = false;
	var guess = guessFrom(no,false);
	if (guess.no) {
		var froms = (guess.best) ? guess.best : guess.more;
		var from = froms[0];
		if (allowed.indexOf(from) != -1) result = setService(false,from,no);
	}
	return result;
}

function setServiceByString (string,substring,no) {
	var result = false;
	var from = serviceFromString(string,substring);
	if (from) result = setService(false,from,no);
	return result;	
}

function updateStatus () {
	if (dashboardWidget) this.startLoad();
	this.resetRequests(true);
	this.success = 2;
	this.showMessage(translateFormat('Contacting %@…',this.fromTrans));
	if (this.items.length > 0) this.previousItems = this.items;
	this.startUpdate(this,false);
}

function loadItemCache (method,site) {

	this.items = [];

	for (var x in this.itemCache) {

		var cache = this.itemCache[x];
		var item = resetItem();
		item.title = cache.itemName;

		item.status = '';
		item.details = {};
		this.items[x] = item;

		if (cache.url) {
			var args = setArgs(site,x);
			this.loadCookiePage(cache.url,method,false,args);
		}

	}

	this.count = this.items.length;
	this.usedItemCache = true;

}

function loadPage (url,method,postData,args) {

	var reqNo = this.reqCount;
	this.reqCount++;

	var request = new XMLHttpRequest();
	request.args = args;
	request.getTracker = getTracker;
	request.onload = method;
	request.url = url;
	request.postData = postData;

	request.onreadystatechange = function(){
		if (this.timer && this.readyState >= 3) clearTimeout(this.timer);
		if (this.readyState == 4) {
			if (this.status == 0 && !this.responseText) {
				if (!request.delivery) {
					if (syncFail) syncFail(args);
				} else {
					var seconds = secondsSince(this.startTime);
					logMessage(1,'Timed out after '+seconds+' seconds');
					logRequest(1,this);
					this.abort(); // Stops onload from getting called in 10.5
					var connectionDown = (seconds < 1 || seconds > 280);
					this.delivery.connectFailed(connectionDown);
				}
			}
		}
	}

	var service = args.service;
	request.noCache = (this.noCache || args.noCache || (service && service.noCache));
	if (service && service.postType) request.postType = service.postType;

	request.startRequest = function(){
		this.startTime = new Date();
		var postData = this.postData;
		var referer = this.args.referer;
		if (postData) {
			var postType = (this.postType) ? this.postType : 'application/x-www-form-urlencoded';
			this.open('POST',url);
			if (referer) this.setRequestHeader('Referer',referer);
			this.setRequestHeader('Cache-Control','no-cache');
			this.setRequestHeader('Content-type',postType);
			this.setRequestHeader('Content-length',postData.length);
			this.setRequestHeader('Connection','close');
			this.send(postData);
			this.postData = null;
		} else {
			this.open('GET',url);
			if (referer) this.setRequestHeader('Referer',referer);
			if (this.noCache) {
				this.setRequestHeader('Cache-Control','no-cache'); // max-age=60
				this.setRequestHeader('Date',(new Date()).toUTCString());
			}
			this.send();
		}
	}

	this.req[reqNo] = request;

	if (this.sync) {
		var timeout = (sync.updated > 1) ? 15000 : 30000;
		request.timer = setTimeout(function(){
			syncFail(args);
		},timeout);
		request.startRequest();
	} else {
		request.delivery = this;
		this.startRequests();
	}

}

function setArgs (site,childNo) {
	var args = {};
	args.site = site;
	args.childNo = (childNo === false) ? 0 : childNo;
	args.isChild = (childNo === false) ? false : true;
	return args;
}

function DummyRequest (delivery,responseText,args) {
	this.delivery = delivery;
	this.args = args;
	this.responseText = responseText;
	this.status = 200;
	this.readyState = 4;
	this.abort = function(){};
	this.getTracker = getTracker;
	this.getTracker();
}

function loadSection (section,args) {
	var reqNo = this.reqCount;
	this.reqCount++;
	this.req[reqNo] = new DummyRequest(this,section,args);
}

function startRequests () {

	var nextRequest = null;
	var busy = false;

	for (i in this.req) {
		var request = this.req[i];
		if (request) {
			var state = request.readyState;
			if (!state) {
				if (!nextRequest) nextRequest = request;
			} else if (state < 4) {
				busy = true;
			}
		}
	}

	var started = false;
	if (!busy && nextRequest) {
		nextRequest.startRequest();
		started = true;
	}

	return started;

}

function resetRequests (cancelAll) {
	if (cancelAll) {
		for (i in this.req) {
			var request = this.req[i];
			if (request) {
				if (typeof request.abort == 'function') request.abort();
				if (request.timer) clearTimeout(request.timer);
			}
		}
	}
	this.reqCount = 0;
	this.reqComplete = 0;
}

function connectFailed (connectionDown) {
	if (!this.count) this.count = -1;
	this.notFound(false,connectionDown);
	this.reqComplete++;
	this.updateItems();
}

function notFound (message,connectionDown,showImmediately) {

	if (dashboardWidget) this.stopLoad();
	if (this.count == 0) {

		if (this.passRequired && !passwordForDelivery(this)) {

			var item = errorItem(this);
			item.shipText = translate('Password required');
			if (dashboardWidget) {
				item.deliverText = '<a href="javascript:editPassword(\''+this.id+'\');">'+
					translate('Click to enter your password.')+'</a>';
				item.clickable = true;
			} else {
				item.deliverText = translate('Please enter your password.');
			}
			this.tap = 'password';
			this.success = (this.keychainLocked) ? -1 : 1;
			this.items = [item];
			this.count = -1;
			this.displayed = 0;

		} else if (manualSignIn[this.from+'-'+this.extra]) {

			var item = errorItem(this);
			item.shipText = translate('Sign in required');
			if (dashboardWidget) {
				item.deliverText = '<a href="javascript:openSafariSite(\''+this.url+'\',\''+this.from+'\',\''+this.extra+'\');">'+
					translate('Click to sign in.')+'</a>';
				item.clickable = true;
			} else {
				item.deliverText = translate('Please sign in.');
			}
			this.tap = 'signin';
			this.success = 1;
			this.items = [item];
			this.count = -1;
			this.displayed = 0;

		} else if (dashboardWidget && DeliveryStatus && !DeliveryStatus.cookiesEnabled()) {

			var item = errorItem(this);
			item.shipText = translate('Cookies are required');
			item.deliverText = '<a href="javascript:openSite(\'https://junecloud.com/sync/help/delivery-status-widget/cookies.html\');">'+
				translate('Click here for help.')+'</a>';
			item.clickable = true;
			this.success = 1;
			this.items = [item];
			this.count = -1;
			this.displayed = 0;

		} else if (showImmediately && message) {

			var item = errorItem(this);
			item.shipText = message;
			item.deliverText = translate('Please check your settings.');
			this.success = 2;
			this.items = [item];
			this.count = -1;
			this.displayed = 0;		

		} else if (!dashboardWidget || this.shouldShowError()) { 

			var item = errorItem(this);
			item.shipText = (message) ? message : translate('Not found');
			item.deliverText = translate('Please check your settings.');
			this.success = 1;
			this.items = [item];
			this.count = -1;
			this.displayed = 0;

		} else {

			// Ideally, we should stop setting the count in Services.js
			// Just pass the type of error to this function, so it can set it!
			logMessage(1,'Ignoring not found error for '+this.from+'. Last success: '+this.lastUpdate);
			this.items = this.previousItems;
			this.count = this.items.length;
			this.success = 0;
			if (!this.lastError) this.lastError = new Date();

		}

	} else if (this.count == -1) {

		var item = errorItem(this);
		item.shipText = translate('Couldn’t connect');
		item.deliverText = translate('Please try again later.');

		this.items = [item];
		this.displayed = 0;
		this.success = (connectionDown) ? -1 : 0;

	} else this.success = (connectionDown) ? -1 : 0;

	if (dashboardWidget) checkVersion(3,true);

}

function notSupported () {

	this.count = 1;
	this.items = [];
	var item = errorItem(this);

	if (this.url) {
		item.icon = 'transit';
		item.shipText = translate('This shipment can only be viewed online.');
		if (dashboardWidget) {
			item.deliverText = translate('Click to view this delivery.');
		} else {
			item.deliverText = (typeof usesClicks !== 'undefined' && usesClicks) ? 
				translate('Click “view details online” to show it.') : translate('Tap “view details online” to show it.');
		}
		this.success = 2;
	} else {
		item.icon = 'error';
		item.shipText = translate('This service is not supported.');
		item.deliverText = translate('Please check your settings.');
		this.success = 1;
	}
	item.shipBold = true;
	item.deliverBold = false;

	this.items[0] = item;
	this.displayed = 0;

}

function shouldShowError () {
	if (!this.lastUpdate) return true;
	if (!this.previousItems) return true;
	if (this.previousItems[0] && this.previousItems[0].icon == 'error') return true;
	if (!this.lastError) return false; // First error
	if ((this.lastError.getTime()/1000) + 18000 < getUnixTime()) return true; // 5 hours
	return false;
}

function updateOther (delivery) {

	var item = resetItem();
	if (delivery.name) {
		item.title = htmlEntities(delivery.name);
	} else if (delivery.no) {
		item.title = htmlEntities(delivery.no);
	} else if (delivery.extra) {
		var domain = delivery.extra.replace(/^[a-z]+:\/\//i,'');
		domain = domain.replace(/\/.*/i,'');
		item.title = domain;
	}

	if (delivery.url) {
		item.shipText = translate('This shipment can only be viewed online.');
		item.shipBold = false;
	} else {
		item.shipText = translate('Ship date unknown');
		item.shipBold = false;
	}

	if (delivery.date) {
		var daysApart = daysUntilDate(delivery.date.getTime());
		var convertedDate = convertDate(delivery.date);
		item.icon = (daysApart < 0) ? 'checkmark' : '';
		item.date = convertedDate;
		item.deliverText = translate('Delivered by %@');
		item.deliverBold = false;
		item.deliverDate = convertedDate;
	} else {
		item.icon = 'transit';
		item.date = '';
		item.deliverText = translate('Delivery date unknown');
		item.deliverBold = false;
		item.deliverDate = '';
	}
	delivery.items = [];
	delivery.items[0] = item;
	delivery.count = 1;

}

function updateComplete () {
	return (this.reqComplete >= this.reqCount) ? true : false;
}

function updateItems () {

	if (this.updateComplete() == false) {
		this.startRequests();
		return;
	}

	if (this.clearItemCache && this.usedItemCache) {
		this.itemCache = null;
		this.usedItemCache = false;
		this.clearItemCache = false;
		this.updateStatus();
		return;
	}

	this.req = [];
	this.previousItems = false;

	var newInterval = 99999999;
	switch (this.success) {
		case -1: newInterval = 0; break;
		case 0: newInterval = 60; break;
		case 1: newInterval = (this.tap == 'password' || this.tap == 'signin') ? 1440 : 60; break;
		default: break;
	}

	if (this.from != 'other' && this.count > 0) {

		for (var x in this.items) {

			var item = this.items[x];
			var hasDeliveryDays = false;

			var shipInput = item.shipSaved;
			var deliverInput = item.deliverSaved;
			var isComplete = (item.status.match(/^complete$/i)) ? true : false;
			var statusDate = false;

			if (shipInput || !item.shipText) {

				var isShipped = (isComplete || item.status.match(/^shipped$/i)) ? true : false;

				item.shipText = translate('Ship date unknown');
				item.shipBold = false;
				var shipDate = statusDate = dateFromString(shipInput);

				if (isComplete) {

					item.shipText = translate('Delivered!');
					item.shipBold = true;

				} else if (shipInput) {

					if (shipDate) {

						if (isShipped) {
							item.shipText = translate('Shipped!');
							item.shipExtra = translate('Shipped %@');
							item.shipBold = true;
						} else item.shipText = translate('Shipped by %@');
						if (this.countToShip) item.daysNo = daysUntilDate(shipDate);

					} else {
						item.shipText = shipInput;
						item.shipBold = true;
					}

				} else if (isShipped) {

					item.shipText = translate('Shipped!');
					item.shipBold = true;

				}

				if (item.details && item.details.status) {
					var thisStatus = stripHTML(item.details.status);
					var thisLocation = stripHTML(item.details.location);
					if (thisLocation || (thisStatus && !thisStatus.match(/^delivered$/i))) {

						if (shipDate && item.shipText) item.shipExtra = item.shipText;
						item.shipText = thisStatus;
						if (thisLocation && item.shipText.indexOf(thisLocation) == -1) item.shipText += ': '+thisLocation;

						var detailDate = dateFromString(item.details.date);
						if (detailDate) {
							item.shipExtra = translate('Status updated %@');
							statusDate = detailDate;
						}
						item.shipBold = true;

					}
				} else if (item.status && !isComplete && !isShipped) {
					if (shipDate && item.shipText) item.shipExtra = item.shipText;
					item.shipText = item.status;
					item.shipBold = true;
				}

				item.shipDate = convertDate(shipDate);
				if (this.countToShip) item.date = item.shipDate;

			}

			if (deliverInput || !item.deliverText) {

				var deliverDaysNo = false;
				item.deliverText = translate('Delivery date unknown');
				item.deliverBold = false;
				var deliverDate = dateFromString(deliverInput);

				if (isComplete) {

					item.deliverText = translate('Delivered!');
					item.deliverBold = true;
					if (deliverDate) {
						if (item.shipText == item.deliverText) {
							item.deliverText = translate('Delivered %@');
							item.deliverBold = false;
						} else item.deliverExtra = translate('Delivered %@');
					}
					deliverDaysNo = daysUntilDate(deliverDate);

				} else if (deliverInput) {

					if (deliverDate) {
						item.deliverText = translate('Delivered by %@');
						deliverDaysNo = daysUntilDate(deliverDate);
					} else {
						item.deliverText = deliverInput;
					}

				}

				if (deliverDaysNo !== false) {
					item.daysNo = deliverDaysNo;
					hasDeliveryDays = true;
				}

				item.deliverDate = convertDate(deliverDate);
				if (item.deliverDate) item.date = item.deliverDate;

			}

			if (isComplete) {
				item.icon = 'checkmark';
			} else if (item.daysNo > 0 || (item.daysNo === 0 && hasDeliveryDays)) {
				item.icon = '';
			} else if (!item.icon) {
				item.icon = 'transit';
			}

			var minutes = 40;
			if (this.neverUpdate) {
				minutes = 99999999;
			} else if (item.icon == 'checkmark') {
				var daysModified = (this.modified) ? daysUntilDate(this.modified*1000) : 0;
				minutes = (daysModified <= -7) ? 99999999 : 1440;
			} else if (item.icon == 'error') {
				if (this.tap == 'password' || this.tap == 'signin') {
					minutes = 99999999;
				} else {
					var daysModified = (this.modified) ? daysUntilDate(this.modified*1000) : 0;
					if (daysModified <= -30) {
						minutes = 99999999;
					} else if (daysModified <= -10) {
						minutes = 1440;
					} else if (daysModified <= -3) {
						minutes = 180;
					} else {
						minutes = 60;
					}
				}
			} else if (item.daysNo === false) {
				var daysModified = 0;
				if (statusDate) {
					daysModified = daysUntilDate(statusDate);
				} else if (this.modified) {
					daysModified = daysUntilDate(this.modified*1000);
				}
				if (daysModified <= -30) {
					minutes = 1440;
				} else if (daysModified <= -15) {
					minutes = 360;
				} else if (daysModified <= -5) {
					minutes = 60;
				}
			} else if (item.daysNo <= -1) {
				var daysApart = (hasDeliveryDays) ? item.daysNo : -1;
				if (daysApart <= -30) {
					minutes = 1440;
				} else if (daysApart <= -15) {
					minutes = 720;
				} else if (daysApart <= -5) {
					minutes = 180;
				}
			} else if (item.daysNo <= 3) {
				minutes = 20;
			} else if (item.daysNo <= 10) {
				minutes = 60;
			} else if (item.daysNo <= 30) {
				minutes = 360;
			} else if (item.daysNo > 30) {
				minutes = 1440;
			}
			if (minutes < newInterval) newInterval = minutes;
			if (statusDate) item.statusDate = convertDate(statusDate);

			this.items[x] = item;

		}

	}

	if (this.success > 0) {
		this.lastUpdate = new Date();
		this.lastError = false;
	}
	this.resetRequests(false);
	this.interval = newInterval;

	if (dashboardWidget) {
		var rightNow = getUnixTime();
		this.nextUpdate = rightNow + (newInterval * 60);
		var backgroundInterval = newInterval;
		if (backgroundInterval < 1440) {
			backgroundInterval = backgroundInterval * 3;
			if (backgroundInterval >= 720) backgroundInterval = Math.max(newInterval,720);
			if (backgroundInterval < 30) backgroundInterval = 30;
		}
		this.backgroundUpdate = rightNow + (backgroundInterval * 60);
		if (this.success > 0) {
			hadSuccess = true;
			if (this.success == 2) setHistoryTitle(this.id);
		}
		if (this.useItemCache) {
			this.itemCache = cacheFromItems(this.items);
		}
		displayItem(this.id,this.displayed,false);
		checkForChanges(this);
	}

	updateQueue.completed(this);
	updateQueue.update();
	this.saveItems();

}

function resetItem () {

	var item = {};
	item.title = translate('Unknown Item');
	item.status = '';
	item.statusDate = false;
	item.date = '';
	item.icon = '';
	item.daysNo = false;

	item.details = {};
	item.details.date = '';
	item.details.location = '';
	item.details.mapLocation = '';
	item.details.status = '';

	item.shipSaved = '';
	item.shipText = ''; 
	item.shipBold = false;
	item.shipExtra = '';
	item.shipDate = 0;

	item.deliverSaved = '';
	item.deliverText = '';
	item.deliverBold = false;
	item.deliverExtra = '';
	item.deliverDate = 0;
	item.clickable = false;

	return item;

}

function errorItem (delivery) {
	var item = resetItem();
	item.icon = 'error';
	if (delivery.name) {
		item.title = htmlEntities(delivery.name);
	} else if (delivery.no) {
		item.title = htmlEntities(delivery.no);
	} else if (delivery.domain) {
		var title = delivery.domain.replace(/^(www|m)\./i,'');
		item.title = title;
	}
	item.shipBold = true;
	item.deliverBold = false;
	return item;
}

// #mark Various utilities

function hasOSFeatureLevel (requiredLevel) {
	return (osFeatureLevel >= requiredLevel);
}

function translate (key) {
	try {
		var ret = translation[key];
		if (ret === undefined || ret === '') ret = key;
		return ret;
	} catch (ex) { }
	return key;
}

function translateFormat (key,value1,value2,value3) {
	key = translate(key);
	if (value1) key = key.replace('%@',value1);
	if (value2) key = key.replace('%@',value2);
	if (value3) key = key.replace('%@',value3);
	return key;
}

function preferredLanguage () {
	var userLang = translate('xx');
	for (var i = 0; i < arguments.length; i++) {
		if (arguments[i] == userLang) return userLang;
	}
	return arguments[0];
}

function getUnixTime () {
	var currentTime = new Date();
	return currentTime.getTime()/1000;
}

function secondsSince(date) {
	return ((new Date()).getTime() - date.getTime()) / 1000
}

function passwordForDelivery (delivery) {
	var password = delivery.password;
	if (!password && (typeof passwordForAccountOnServer === 'function')) {
		var result = passwordForAccountOnServer(delivery.extra,delivery.from);
		if (result == null) {
			if (!dashboardWidget) delivery.keychainLocked = true;
		} else {
			delivery.password = result;
			password = result;
		}
	}
	return password;
}

function extraInformationForService (service, type) {
	var info;
	if (typeof extraInformation !== 'undefined') {
		var serviceInfo = extraInformation[service];
		if (serviceInfo) info = serviceInfo[type];
	}
	if (!info && typeof requestExtraInformation === 'function') {
		info = requestExtraInformation(service.from,type);
		if (!extraInformation[service]) extraInformation[service] = [];
		extraInformation[service][type] = info;
	}
	return info;
}

function overlappingMatches (regExText, regExPattern) {
	var regEx = new RegExp(regExPattern,'gim');
	var matches = [];
	var match;
	while (match = regEx.exec(regExText)) {
		matches.push(match[0]);
		regEx.lastIndex = match.index+1;
	}
	return (matches.length > 0) ? matches : null;
}

String.prototype.hashCode = function(){
	var hash = 0, i, l, charCode;
	if (this.length == 0) return hash;
	for (i = 0, l = this.length; i < l; i++) {
		charCode  = this.charCodeAt(i);
		hash  = ((hash<<5)-hash)+charCode;
		hash |= 0; // Convert to 32bit integer
	}
	return hash;
};

// #mark Strings and display text

function getFromName (from,translateName,dropDown) {

	switch(from) {

		case 'www.adobe.com':
			var name = 'Adobe';
			break;
		case 'www.apple.com':
		case 'www.apple.com/zip':
			var name = 'Apple';
			break;
		case 'www.apple.com/japan':
		case 'www.apple.com/japanzip':
			var name = 'Apple';
			if (dropDown) name += ' – '+translate('Japan');
			break;
		case 'www.apple.com/hk':
			var name = 'Apple';
			if (dropDown) name += ' – '+translate('Hong Kong');
			break;
		case 'www.aramex.com':
			var name = 'Aramex';
			if (dropDown) name += ' – '+translate('Shop and Ship');
			break;
		case 'auspost.com.au':
			var name = 'Australia Post';
			break;
		case 'em.canadapost.ca':
			var name = (translateName) ? translate('Canada Post') : 'Canada Post';
			break;
		case 'www.chronopost.fr':
			var name = 'Chronopost';
			if (dropDown) name += ' – '+translate('France');
			break;
		case 'www.dhl.com':
		case 'track.dhl-usa.com':
			var name = 'DHL';
			if (dropDown) name += ' – Express';
			break;
		case 'www.dhlglobalmail.com':
			var name = 'DHL';
			if (dropDown) name += ' – '+translate('US')+' GlobalMail';
			break;
		case 'nolp.dhl.de':
		case 'nolp.dhl.de/int':
			var name = 'DHL';
			if (dropDown) name += ' – '+translate('Germany');
			break;
		case 'www.dpd.com':
			var name = 'DPD';
			if (dropDown) name += ' – '+translate('Germany');
			break;
		case 'www.dpd.co.uk':
			var name = 'DPD';
			if (dropDown) name += ' – '+translate('UK');
			break;
		case 'www.fedex.com':
		case 'spportal.fedex.com':
			var name = 'FedEx';
			break;
		case 'www.gls-germany.com':
			var name = 'GLS';
			break;
		case 'checkout.google.com':
			var name = 'Google Wallet';
			break;
		case 'www.hlg.de':
			var name = 'Hermes Logistik Gruppe';
			if (dropDown) name += ' – '+translate('Germany');
			break;
		case 'www.hongkongpost.com':
			var name = 'Hongkong Post';
			break;
		case 'www.japanpost.jp/ems':
		case 'www.japanpost.jp/m10':
		case 'www.japanpost.jp/reg':
		case 'www.japanpost.jp/yupack':
			var name = (translateName) ? translate('Japan Post') : 'Japan Post';
			break;
		case 'www.laposte.fr':
			var name = 'La Poste';
			if (dropDown) name += ' – '+translate('France');
			break;
		case 'www.lasership.com':
			var name = 'LaserShip';
			break;
		case 'www.ontrac.com':
			var name = 'OnTrac';
			break;
		case 'www.parcelforce.com':
			var name = 'Parcelforce';
			break;
		case 'www.post.at':
			var name = (translateName)?'Post':'Post.at';
			if (dropDown) name += ' – '+translate('Austria');
			break;
		case 'www.postdanmark.dk':
			var name = 'PostNord';
			if (dropDown) name += ' – '+translate('Denmark');
			break;
		case 'www.poste.it':
		case 'www.poste.it/posta1':
		case 'www.poste.it/pacco1':
		case 'www.poste.it/maxi':
		case 'www.poste.it/pacco':
		case 'www.poste.it/ems':
			var name = (translateName) ? 'Poste Italiane' : 'Poste.it';
			break;
		case 'www.posten.no':
			var name = (translateName) ? 'Posten' : 'Posten.no';
			if (dropDown) name += ' – '+translate('Norway');
			break;
		case 'www.posten.se': // www.postnord.se
			var name = 'PostNord';
			if (dropDown) name += ' – '+translate('Sweden');
			break;
		case 'www.postnl.nl':
		case 'www.tntpost.nl':
			var name = 'PostNL';
			break;
		case 'www.purolator.com':
			var name = 'Purolator';
			break;
		case 'www.royalmail.com':
			var name = 'Royal Mail';
			break;
		case 'www.sagawa-exp.co.jp':
			var name = (translateName) ? translate('Sagawa Express') : 'Sagawa';
			break;
		case 'www.post.ch':
			var name = (translateName) ? translate('Swiss Post') : 'Swiss Post';
			break;
		case 'www.tnt.com':
			var name = 'TNT';
			if (dropDown) name += ' – '+translate('by consignment number');
			break;
		case 'www.tnt.com/ref':
			var name = 'TNT';
			if (dropDown) name += ' – '+translate('by reference number');
			break;
		case 'www.tntexpress.com.au':
			var name = 'TNT Express';
			if (dropDown) name += ' – '+translate('Australia');
			break;
		case 'www.ups.com':
			var name = 'UPS';
			break;
		case 'www.ups-mi.net':
			var name = 'UPS Mail Innovations';
			break;
		case 'www.usps.com':
			var name = 'U.S. Postal Service';
			break;
		case 'www.kuronekoyamato.co.jp':
			var name = (translateName) ? translate('Yamato Transport') : 'Yamato Transport';
			break;
		case 'www.yodel.co.uk':
			var name = 'Yodel';
			break;
		case 'www.amazon.com':
		case 'www.amazon.ca':
		case 'www.amazon.co.uk':
		case 'www.amazon.co.jp':
		case 'www.amazon.de':
		case 'www.amazon.at':
		case 'www.amazon.es':
		case 'www.amazon.fr':
		case 'www.amazon.in':
		case 'www.amazon.it':
			var name = 'A'+from.substring(5);
			break;
		case 'www.amazon.com/url':
			var name = 'Amazon';
			if (dropDown) name += ' – '+translate('by tracking link');
			break;
		case 'other':
			var name = (translateName) ? translate('Other') : 'Other';
			break;
		default:
			var name = from;
			break;

	}
	return name;

}

function deliveryTitle ( no, name, label, count ) {
	var title = '';
	if (name) {
		if (count > 1 && label) {
			title = htmlEntities(name)+': '+label;
		} else title = htmlEntities(name);
	} else if (label) {
		title = label;
	} else title = htmlEntities(no);
	return title;
}

function stripWhiteSpace (string) {
	// \s should match [\f\n\r\t\v\u00A0\u2028\u2029] but \u2028 is needed for iOS 4
	return (typeof string == 'string') ? string.replace(/[\s\n\u2028]+/g,' ') : '';
}

function trim (string) {
	if (typeof string == 'string') {
		// Remove non-breaking space characters (&#160; or \xAO)
		// while (string.indexOf(' ') > -1) string = string.replace(' ',' ');
		string = string.replace(/^(\s|&nbsp;)+|(\s|&nbsp;)+$/ig,'');
	}
	return string;
}

function cleanSpaces (string) {
	return (typeof string == 'string') ? string.replace(/(\s|&nbsp;)+/g,' ') : '';
}

function clearEmptyString (string) {
	return (typeof string == 'string') ? string.replace(/^ ?-+ ?$/,'') : '';
}

function stripHTML (string) {
	if (typeof string == 'string') {

		// Deal with some really ugly HTML from UPS Mail Innovations
		string = string.replace(/<a [^>]*onmouseover="([^"]*)"[^>]*>/g,' ');

		// Remove script, noscript, and style tags
		string = string.replace(/<(noscript|script|style) ?[^>]*>(.*?)<\/\1>/ig,' ');

		// Remove HTML tags
		string = string.replace(/<[^>]*>/g,' ');

		// Remove non-breaking space characters (&#160; or \xAO) and reduce mutiple spaces
		// while (string.indexOf(' ') > -1) string = string.replace(' ',' ');
		string = cleanSpaces(string);

		// Convert some HTML entities
		string = string.replace(/&quot;/g,'"');

	}
	return trim(string);
}

function htmlEntities (string) {
	if (typeof string == 'string') {
		string = string.replace(/\&/g,'&#38;');
		string = string.replace(/"/g,'&quot;');
		string = string.replace(/</g,'&#60;');
		string = string.replace(/>/g,'&#62;');
	}
	return string;
}

function decodeDecimalEntities (string) {
	return string.replace(/&#(\d+);/g, 
		function (whole,parens) {
			if (parens >= -32768 && parens <= 65535) {
				return String.fromCharCode(parens);
			} else return whole;
		});
}

function removeHtmlEntities (string) {
	if (typeof string == 'string') {
		var entities = {
			'lt':'<',
			'gt':'>',
			'amp':'&',
			'apos':'\'',
			'quot':'"',
			'lsquo':'‘',
			'rsquo':'’',
			'ldquo':'“',
			'rdquo':'”',
			'nbsp':' ',
			'ndash':'–',
			'mdash':'—',

			'auml':'ä',
			'euml':'ë',
			'iuml':'ï',
			'ouml':'ö',
			'uuml':'ü',
			'Auml':'Ä',
			'Euml':'Ë',
			'Iuml':'Ï',
			'Ouml':'Ö',
			'Uuml':'Ü',

			'szlig':'ß',

			'aacute':'á',
			'eacute':'é',
			'iacute':'í',
			'oacute':'ó',
			'uacute':'ú',
			'Aacute':'Á',
			'Eacute':'É',
			'Iacute':'Í',
			'Oacute':'Ó',
			'Uacute':'Ú',

			'agrave':'à',
			'egrave':'è',
			'igrave':'ì',
			'ograve':'ò',
			'ugrave':'ù',
			'Agrave':'À',
			'Egrave':'È',
			'Igrave':'Ì',
			'Ograve':'Ò',
			'Ugrave':'Ù',

			'acirc':'â',
			'ecirc':'ê',
			'icirc':'î',
			'ocirc':'ô',
			'ucirc':'û',
			'Acirc':'Â',
			'Ecirc':'Ê',
			'Icirc':'Î',
			'Ocirc':'Ô',
			'Ucirc':'Û',

			'agrave':'à',
			'egrave':'è',
			'igrave':'ì',
			'ograve':'ò',
			'ugrave':'ù',
			'Agrave':'À',
			'Egrave':'È',
			'Igrave':'Ì',
			'Ograve':'Ò',
			'Ugrave':'Ù',

			'ntilde':'ñ',
			'atilde':'ã',
			'otilde':'õ',
			'Ntilde':'Ñ',
			'Atilde':'Ã',
			'Otilde':'Õ',

			'aring':'å',
			'Aring':'Å',

			'oslash':'ø',
			'Oslash':'Ø',
		
			'copy':'©',
			'trade':'™',
			'reg':'®',
			'bull':'•',
			'deg':'°',

			'quest':'?',
			'iquest':'¿',

			'frac14':'¼',
			'frac12':'½',
			'frac34':'¾'
		};
		return string.replace(/&(#(?:x[0-9a-f]+|\d+)|[a-z]+[0-9]*);?/gi, function(whole,parens) {
			if (parens[0] === "#") {
				var charCode = parens[1].toLowerCase() === 'x' ? parseInt(parens.substr(2),16) : parseInt(parens.substr(1),10);
				return String.fromCharCode(charCode);
			} else return entities.hasOwnProperty(parens) ? entities[parens] : whole;
		});
	}
	return string;
}

function doublePlusEncode (string) {
	return encodeURIComponent(string).replace(/%2B/g,'%252B');
}

// #mark JSON parsing

function parseJSONInHTML (html, pattern) {
	if (!html) return null;
	var result = html.match(pattern);
	if (!result) return null;
	return parseJSON(result[1]);
}

function parseJSON (json) {
	var object = null;
	try {
		object = JSON.parse(json);
	} catch (err) {
		logMessage(1,err);
	}
	return object;
}

// #mark Date parsing

function dateFromString (string) {
	var date = false;
	if (string) {
		date = new Date(string);
		if (new String(date.getDate()) == 'NaN') date = false;
	}
	return date;
}

function cleanGermanDate (string) {
	string = string.replace(/\./g,' ');
	string = string.replace(/\b(Januar|Jänner|Jan|Jän)\s/i,'January ');
	string = string.replace(/\b(Februar|Feb)\s/i,'February ');
	string = string.replace(/\bMärz\s/i,'March ');
	string = string.replace(/\bApr\s/i,'April ');
	string = string.replace(/\bMai\s/i,'May ');
	string = string.replace(/\bJuni\s/i,'June ');
	string = string.replace(/\bJuli\s/i,'July ');
	string = string.replace(/\bAug\s/i,'August ');
	string = string.replace(/\bSept\s/i,'September ');
	string = string.replace(/\b(Oktober|Okt)\s/i,'October ');
	string = string.replace(/\b(Dezember|Dez)\s/i,'December ');
	string = string.replace(/\s+,/g,',');
	string = string.replace(/\s+/g,' ');
	return string;
}

function cleanSpanishDate (string) {
	string = string.replace(/\b(enero|ene)\s/i,'January ');
	string = string.replace(/\b(febrero|feb)\.?\s/i,'February ');
	string = string.replace(/\b(marzo|mar)\s/i,'March ');
	string = string.replace(/\b(abril|abr)\.?\s/i,'April ');
	string = string.replace(/\b(mayo|may)\s/i,'May ');
	string = string.replace(/\b(junio|jun)\.?\s/i,'June ');
	string = string.replace(/\b(julio|jul)\.?\s/i,'July ');
	string = string.replace(/\b(agosto|ago)\s/i,'August ');
	string = string.replace(/\b(septiembre|sept|set)\.?\s/i,'September ');
	string = string.replace(/\b(octubre|oct)\.?\s/i,'October ');
	string = string.replace(/\b(noviembre|nov)\.?\s/i,'November ');
	string = string.replace(/\b(diciembre|dic)\.?\s/i,'December ');
	return string;
}

function cleanFrenchDate (string) {
	string = string.replace(/\./g,' ');
	string = string.replace(/^\s?le /i,'');
	string = string.replace(/\b(janvier|janv\.?)\b/i,'January ');
	string = string.replace(/\b(février|févr\.?)\b/i,'February ');
	string = string.replace(/\bmars\b/i,'March ');
	string = string.replace(/\b(avril|avr\.?)\b/i,'April ');
	string = string.replace(/\bmai\b/i,'May ');
	string = string.replace(/\bjuin\b/i,'June ');
	string = string.replace(/\b(juillet|juil\.?)\b/i,'July ');
	string = string.replace(/\baoût\b/i,'August ');
	string = string.replace(/\b(septembre|sept\.?)\b/i,'September ');
	string = string.replace(/\b(octobre|oct\.?)\b/i,'October ');
	string = string.replace(/\b(novembre|nov\.?)\b/i,'November ');
	string = string.replace(/\b(décembre|déc\.?)\b/i,'December ');
	string = string.replace(/\s+,/g,',');
	string = string.replace(/\s+/g,' ');
	return string;
}

function cleanItalianDate (string) {
	string = string.replace(/\/(gen|genn)\//i,'/01/');
	string = string.replace(/\/(feb|febbr)\//i,'/02/');
	string = string.replace(/\/(mar)\//i,'/03/');
	string = string.replace(/\/(apr)\//i,'/04/');
	string = string.replace(/\/(mag|magg)\//i,'/05/');
	string = string.replace(/\/(giu|giugno)\//i,'/06/');
	string = string.replace(/\/(lug|luglio)\//i,'/07/');
	string = string.replace(/\/(ago|ag)\//i,'/08/');
	string = string.replace(/\/(set|sett)\//i,'/09/');
	string = string.replace(/\/(ott)\//i,'/10/');
	string = string.replace(/\/(nov)\//i,'/11/');
	string = string.replace(/\/(dic)\//i,'/12/');
	string = string.replace(/\b(gen|genn)\.?\b/i,'January ');
	string = string.replace(/\b(feb|febbr)\.?\b/i,'February ');
	string = string.replace(/\b(mar)\.?\b/i,'March ');
	string = string.replace(/\b(apr)\.?\b/i,'April ');
	string = string.replace(/\b(mag|magg)\.?\b/i,'May ');
	string = string.replace(/\b(giu|giugno)\.?\b/i,'June ');
	string = string.replace(/\b(lug|luglio)\.?\b/i,'July ');
	string = string.replace(/\b(ago|ag)\.?\b/i,'August ');
	string = string.replace(/\b(set|sett)\.?\b/i,'September ');
	string = string.replace(/\b(ott)\.?\b/i,'October ');
	string = string.replace(/\b(nov)\.?\b/i,'November ');
	string = string.replace(/\b(dic)\.?\b/i,'December ');
	string = string.replace(/\s+/g,' ');
	return string;
}

function cleanDutchDate (string) {
	string = string.replace(/[a-z]+ ([0-9]+ [a-z]+ [0-9]{4})/i,'$1');
	string = string.replace(/\b(jan)\s/i,'January ');
	string = string.replace(/\b(feb)\.?\s/i,'February ');
	string = string.replace(/\b(maart)\s/i,'March ');
	string = string.replace(/\b(apr)\.?\s/i,'April ');
	string = string.replace(/\b(mei)\s/i,'May ');
	string = string.replace(/\b(juni)\.?\s/i,'June ');
	string = string.replace(/\b(juli)\.?\s/i,'July ');
	string = string.replace(/\b(aug)\s/i,'August ');
	string = string.replace(/\b(sept)\.?\s/i,'September ');
	string = string.replace(/\b(oct|okt)\.?\s/i,'October ');
	string = string.replace(/\b(nov)\.?\s/i,'November ');
	string = string.replace(/\b(dec)\.?\s/i,'December ');
	return string;
}

function cleanDanishDate (string) {
	string = string.replace('.',' ');
	string = string.replace(/\bjanuar\s/i,'January ');
	string = string.replace(/\bfebruar\s/i,'February ');
	string = string.replace(/\bmarts\s/i,'March ');
	string = string.replace(/\bmaj\s/i,'May ');
	string = string.replace(/\bjuni\s/i,'June ');
	string = string.replace(/\bjuli\s/i,'July ');
	string = string.replace(/\boktober\s/i,'October ');
	return string;
}

function getSwedishMonth (string) {
	switch (string) {
		case 'januari':		case 'jan':	return '1';
		case 'februari':	case 'feb':	return '2';
		case 'mars':		case 'mar':	return '3';
		case 'april':		case 'apr':	return '4';
		case 'maj':						return '5';
		case 'juni':		case 'jun':	return '6';
		case 'juli':		case 'jul':	return '7';
		case 'augusti':		case 'aug':	return '8';
		case 'september':	case 'sep':	return '9';
		case 'oktober':		case 'okt':	return '10';
		case 'november':	case 'mov':	return '11';
		case 'december':	case 'dec':	return '12';
	}
}

function daysUntilDate (fromDate) {
	if (dashboardWidget && DeliveryStatus) {
		return DeliveryStatus.daysUntilDate(fromDate/1000);
	} else {
		// Note: this is approximate
		var date = new Date();
		date.setTime(fromDate);
		date.setHours(0);
		date.setMinutes(0);
		date.setSeconds(0);
		return Math.ceil((date-new Date())/86400000);
	}
}

function monthsUntilDate (fromDate) {
	if (dashboardWidget && DeliveryStatus) return DeliveryStatus.monthsUntilDate(fromDate/1000);
}

// #mark Logging

function logRequest (level,request) {
	if (typeof logLevel === 'undefined') return;
	if (logLevel >= level && request) {
		var message = '';
		if (request.url) message += request.url+'\n';
		message += 'Status: '+request.status+'\n';
		message += 'Ready State: '+request.readyState+'\n';
		if (request.getAllResponseHeaders) message += request.getAllResponseHeaders()+'\n';
		if (message) message += '\n';
		alert(message+request.responseText);
	}
}

function logMessage (level,message) {
	if (typeof logLevel === 'undefined') return;
	if (logLevel >= level) alert(message);
}
