const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
const PrefBranch = "avg.userPreferences.DNT.";

// Blocking Configuration States
const STATE_DEFAULT = 0;
const STATE_BLOCKALL = 1;
const STATE_ALLOWALL = 2;
const STATE_CUSTOM = 3;

Cu.import("resource://gre/modules/XPCOMUtils.jsm");
//class constructor

function AVGContentPolicy() {
	this.wrappedJSObject = this;

	Cu.import("resource://avg/Preferences.js");
	Cu.import("resource://avg/avg-dnt-adapter.js");
	Cu.import("resource://avg/avgJsm.js");
	this.onTrackerFound(adapter_avgdnt.StoreTrackers);

	this.LoadConfiguration();
}

AVGContentPolicy.prototype = {
	QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPolicy, Ci.nsISupports, Ci.nsIFactory, Ci.nsIObserver, Ci.nsIChannelEventSink]),

	classDescription: "AVG DNT Content Policy Component",
	classID: Components.ID("{62dcaf40-58f4-11e1-b86c-0800200c9a66}"),
	contractID: "@mozilla.org/content-policy;1",
	_xpcom_categories: [{
		category: "content-policy",
		entry: "@avg.com/content-policy;1",
		value: "@mozilla.org/content-policy;1",
		service: "true"
	}, {
		category: "net-channel-event-sinks",
		entry: "@avg.com/content-policy;1",
		value: "@mozilla.org/content-policy;1",
		service: "true"
	}],

	__noSuchMethod__: function() {
		log('no such method: ' + arguments[0])
	},

	shouldLoad: function(aContentType, aContentLocation, aRequestOrigin, aContext, aMimeTypeGuess, aExtra) {
		var result = Ci.nsIContentPolicy.ACCEPT;
		if (typeof this.dbO == "undefined") {
			return result;
		}
		var tabId = this.tabId(aContext);
		var dbData = this.dbO['items'];
		var url = aContentLocation.spec;
		for (var i = 0; i < dbData.length; i++) {
			var re = new RegExp(dbData[i].pattern);
			if (re.test(url.toLowerCase())) {
				var tracker = dbData[i];
				this.onTrackerFound(tabId, tracker.aid);
				if (this.TrackerBlocked(tracker.aid)) {
					// add minimal delegate script to the DOM required by tracker
					if (this.dbO.delegates) {
						this.injectDelegateCode(aContext, tracker.name);
					}
					return Ci.nsIContentPolicy.REJECT_REQUEST;
				}
			}
		}

		// we should check for TYPE_SUBDOCUMENT as well if we want frames.
		if ((Ci.nsIContentPolicy.TYPE_SUBDOCUMENT == aContentType) && /^http/.test(aContentLocation.scheme)) {
			// do stuff here, possibly change result.
		}

		return result;
	},

	shouldProcess: function(aContentType, aContentLocation, aRequestOrigin, aContext, aMimeType, aExtra) {
		let result = Ci.nsIContentPolicy.ACCEPT;

		return result;
	},

	LoadConfiguration: function(pdbO) {
		// instantiate tracker database
		this.dbO = pdbO || Dat.load();
		// initialize config properties
		if (typeof this.dbO == "undefined") {
			this.defaultBlocks = [];
			this.defaultAllows = [];
		} else {
			this.defaultBlocks = getBlocksFromDat(this.dbO["items"], 'block');
			this.defaultAllows = getBlocksFromDat(this.dbO["items"], 'allow');
		}

		//check for disable flag
		if (adapter_avgdnt.DNTGetState()) {
			Preferences.set(PrefBranch + "Disabled", true);
			this.disableDNT(true);
			LogAddonMsgs_avg.CreateDumpLogString("DNT Disabled: true");
			return;
		}else{
			if(!adapter_avgdnt.DNTIsDisablePropertyExists() && adapter_avgdnt.STARTUP_DISABLED_DNT){
				Preferences.set(PrefBranch + "Disabled", true);
				this.disableDNT(true);
				LogAddonMsgs_avg.CreateDumpLogString("DNT Default (Server) Disabled: true");
				return;
			}
		}
		if (this.defaultBlocks.length == 0) {
			return;
		}
		//DB loaded succefully
		adapter_avgdnt.DNTDBState = true;
		//import old settings
		this.importOldSettings();
		//Get User Block/allowed ID's
		this.state = Preferences.get(PrefBranch + 'State', 0);
		this.blockedIDs = Preferences.get(PrefBranch + 'BlockedIDs', this.defaultBlocks.join()).split(',');
		this.allowedIDs = Preferences.get(PrefBranch + 'AllowedIDs', this.defaultAllows.join()).split(',');
		this.addDNTHeader = Preferences.get(PrefBranch + 'AddDNTHeader', '-1');
		//first time load
		if (this.addDNTHeader == "-1") {
			Preferences.set(PrefBranch + 'AddDNTHeader', "1");
			Preferences.set('privacy.donottrackheader.enabled', true);
		}
		// add preference observers
		var _self = this;
		Preferences.observe(PrefBranch + 'State', function(prefValue) {
			_self.state = prefValue
		});
		Preferences.observe(PrefBranch + 'BlockedIDs', function(prefValue) {
			_self.blockedIDs = prefValue.split(',')
		});
		Preferences.observe(PrefBranch + 'AllowedIDs', function(prefValue) {
			_self.allowedIDs = prefValue.split(',')
		});
		Preferences.observe(PrefBranch + 'AddDNTHeader', function(prefValue) {
			_self.addDNTHeader = prefValue;
			Preferences.set('privacy.donottrackheader.enabled', prefValue == '1' ? true : false);
		});
		LogAddonMsgs_avg.CreateDumpLogString("DNT blocks Loaded: BlockIDs: " + this.blockedIDs.join() + "\nallowedIDs: " + this.allowedIDs.join());
	},

	disableDNT: function(state) {
		var categoryManager = Cc["@mozilla.org/categorymanager;1"].getService(Components.interfaces.nsICategoryManager);

		if (state) {
			categoryManager.deleteCategoryEntry("content-policy", "@avg.com/content-policy;1", false);            
			categoryManager.deleteCategoryEntry("net-channel-event-sinks", "@avg.com/content-policy;1", false);
		} else {
			categoryManager.addCategoryEntry("content-policy", "@avg.com/content-policy;1", "@mozilla.org/content-policy;1", false, true);
			categoryManager.addCategoryEntry("net-channel-event-sinks", "@avg.com/content-policy;1", "@mozilla.org/content-policy;1", false, true);
			this.LoadConfiguration();
		}
	},

	asyncOnChannelRedirect: function(oldChannel, newChannel, flags, callback) {
		this.onChannelRedirect(oldChannel, newChannel, flags);
		callback.onRedirectVerifyCallback(Cr.NS_OK);
	},

	onChannelRedirect: function(oldChannel, newChannel, flags) {
		return;
	},

	tabId: function(aContext) {
		var browserWindow = null;
		var isDOMNode = aContext instanceof Ci.nsIDOMNode ? aContext.nodeType : 0;
		if (isDOMNode) {
			var wm = Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator);
			browserWindow = wm.getMostRecentWindow("navigator:browser");
			var doc = isDOMNode == Ci.nsIDOMNode.DOCUMENT_NODE ? aContext : aContext.ownerDocument;
			var win = doc.defaultView;

			var currBrowser = null;
			try {
				if ("location" in doc && /^http/.test(doc.location.protocol)) {
					currBrowser = browserWindow.gBrowser.getBrowserForDocument(win.top.document);
					return currBrowser;
				} else {
					return null;
				}
			} catch (ex) {
				return null;
			}
		}
		return null;
	},

	injectDelegateCode: function(aContext, name) {
		try {
			if (!aContext || !name || !this.dbO.delegates[name]) {
				return;
			}

			var doc = aContext.ownerDocument || aContext;
			if (!doc || !doc.documentElement) {
				return;
			}

			var sNode = doc.getElementById('avgdntdelegate');
			if (sNode != null) {
				return;
			}

			var code = this.dbO.delegates[name];
			var scriptElem;

			scriptElem = doc.createElement("script");
			scriptElem.setAttribute('id', "avgdntdelegate");
			scriptElem.setAttribute('type', "text/javascript");

			scriptElem.appendChild(doc.createTextNode(code));
			doc.documentElement.insertBefore(scriptElem, doc.documentElement.firstChild);
		} catch (e) {}
	},

	onTrackerFound: function(callbackFunc) {
		if (typeof(callbackFunc) === 'function') {
			this.onTrackerFound = callbackFunc;
			return true;
		}

		return false;
	},

	TrackerBlocked: function(trackerId) {
		switch (parseInt(this.state)) {
		case STATE_BLOCKALL:
			return true;
		case STATE_ALLOWALL:
			return false;
		case STATE_CUSTOM:
			if (this.blockedIDs.indexOf(trackerId) !== -1) return true;
			else if (this.allowedIDs.indexOf(trackerId) !== -1) return false;
		case STATE_DEFAULT:
		default:
			return (this.defaultBlocks.indexOf(trackerId) !== -1);
		}
	},

	importOldSettings: function() {
		var oldBranchName = 'extensions.avgdnt.';
		var oldPrefBranch = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefService).getBranch(oldBranchName);
		var children = oldPrefBranch.getChildList('', {});
		//return immediately if the old branch was removed
		if (children.length === 0) return;

		var state = parseInt(Preferences.get(oldBranchName + 'blockall', STATE_DEFAULT));
		//if old state is not a valid new state then force to default
		if ([STATE_DEFAULT, STATE_BLOCKALL, STATE_ALLOWALL].indexOf(state) === -1) {
			state = STATE_DEFAULT;
		}

		//if state is default then compare the old blocked tracker array to the
		//current defaultBlocks array to infer whether state may be custom.
		if (state === STATE_DEFAULT) {
			try {
				//convert element's type from string to number
				var bTrackers = JSON.parse(Preferences.get(oldBranchName + 'bTrackers', ''));
				//if the old block list differs from the new default list then the state may
				//be custom - not guaranteed to be correct because the dat may be different!
				if (arrayCompare(bTrackers, this.defaultBlocks, true) === false) {
					//get an array of trackerIDs from the dat that are blocked in the old DNT (i.e., blocked)
					var blockedIDs = this.defaultBlocks.concat(this.defaultAllows).filter(function(elem) {
						return bTrackers.indexOf(elem) !== -1
					});
					//if the blockedIDs and defaultBlocks arrays are still not the same the state is custom
					if (arrayCompare(blockedIDs, this.defaultBlocks, true) === false) {
						state = STATE_CUSTOM;
						//get an array of trackerIDs from the dat that are not being blocked (i.e., allowed)
						var allowedIDs = this.defaultBlocks.concat(this.defaultAllows).filter(function(elem) {
							return blockedIDs.indexOf(elem) === -1
						});

						Preferences.set(PrefBranch + 'BlockedIDs', blockedIDs.toString());
						Preferences.set(PrefBranch + 'AllowedIDs', allowedIDs.toString());
					}
				}
			} catch (e) {
				LogAddonMsgs_avg.CreateDumpLogString("\importOldSettings err: " + e + "\n");
			}
		}

		Preferences.set(PrefBranch + 'State', state.toString());
		var DNTHeader = Preferences.get(oldBranchName + 'dnthttpflag', false);
		Preferences.set(PrefBranch + 'AddDNTHeader', DNTHeader == true ? "1" : "0");
		//remove the old preferences
		oldPrefBranch.deleteBranch('');
	}
};

var components = [AVGContentPolicy];
var postRegister = function() {};
var postUnRegister = function() {};
if (XPCOMUtils.generateNSGetFactory) {
	var NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
} else {
	var NSGetModule = XPCOMUtils.generateNSGetModule(components, postRegister, postUnRegister);
}


// ---  Utilities

function getBlocksFromDat(trackers, action) {
	var defaultBlocks = [];
	if (trackers) {
		trackers.forEach(function(item) {
			if (item.action == action && defaultBlocks.indexOf(item.aid) == -1) {
				defaultBlocks.push(item.aid);
			}
		});
	}

	return defaultBlocks;
}

function arrayCompare(a, b, ignoreCase) {
	try {
		if (a.length != b.length) {
			return false;
		}
		var c = /^1|true$/i.test(ignoreCase) === false ? (/^0|false$/i.test(ignoreCase) === true ? 0 : 0) : 1;
		for (var i = 0; i < a.length; i++) {
			if (a[i].constructor.name === 'Array') {
				if (!arrayCompare(a[i], b[i]), ignoreCase) {
					return false;
				}
			}
			var z = c ? a[i].toString().toLowerCase() : a[i];
			if (b.some(function(elem) {
				var elem = c ? elem.toString().toLowerCase() : elem;
				return z === elem;
			}) === false) {
				return false;
			}

		}
		return true;
	} catch (e) {
		LogAddonMsgs_avg.CreateDumpLogString("DNT Array Compare ERR: " + e);
	}
}

function log(msg) {
	// TODO: reset default debug value to false
	if (!Preferences.get(PrefBranch + 'debug', true)) {
		return;
	}
	var consoleService = Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService);
	consoleService.logStringMessage(msg);
}

Dat = {
	load: function() {
		var updateInterval = 86400000; // (i.e., millisec/24 hours)
		var now = (new Date()).getTime();
		var lastUpdateTime = Number(Preferences.get(PrefBranch + 'lastUpdated', 0));
		var updateIntervalExpired = (now >= lastUpdateTime + updateInterval) ? 1 : 0;
		var dbO;

		// check profileDir - will only be present after first launch
		var localDat = avgFile.read('avg\\dt.dat');
		if (localDat) {
			try {
				dbO = Dat.parse(localDat);
			} catch (e) {}
		}

		//no local file (may be first launch) so check delivered file
/*if (!dbO) {
			var deliveredDat = avgFile.read('chrome://avg/content/dat.js');
			if (deliveredDat) {
				Dat.save(deliveredDat);
				dbO = Dat.parse(deliveredDat);
			}
		}*/

		if (!dbO) {
			// local file not found, check remote update
			Dat.update();
			LogAddonMsgs_avg.CreateDumpLogString("DNT DB is missing, updating DB");
		} else {
			// check for newer dat version - don't download data when remoteVersion is empty
			dump("updateIntervalExpired : " + updateIntervalExpired + " dbo Version: " + dbO.version);
			if (updateIntervalExpired && dbO.version) {
				var now = (new Date()).getTime();
				Preferences.set(PrefBranch + 'lastUpdated', now.toString());
				Dat.isObsolete(dbO.version, function(remoteVersion) {
					var remoteVersion = Number(remoteVersion) || -1;
					if (Number(dbO.version) < remoteVersion) {
						Dat.update();
						LogAddonMsgs_avg.CreateDumpLogString("DNT DB is isObsolete: New version: " + remoteVersion.toString());
					}
				});
			}
		}

		return dbO;
	},

	isObsolete: function(version, callback) {
		// get remote dat version - asynchronously
		var version_url = 'http://dnt.cloud.avg.com/ver.txt';
		avgFile.xhr(version_url, 'GET', true, callback);
	},

	update: function(callback) {
		// set default callback if not provided
		callback = (typeof(callback) == 'function') ? callback : Dat.save;
		// get remote dat - asynchronously
		var update_url = 'http://dnt.cloud.avg.com/dat.js?a=1';
		avgFile.xhr(update_url, 'GET', true, callback);
	},

	save: function(dbO) {
		// write new data to local file
		var dbO_parsed = Dat.parse(dbO);
		try {
			if (dbO_parsed != null) {
				var avgDNTPolicy = Cc["@mozilla.org/content-policy;1"].getService().wrappedJSObject;
				avgDNTPolicy.LoadConfiguration(dbO_parsed);
				avgFile.write('avg\\dt.dat', dbO);
				LogAddonMsgs_avg.CreateDumpLogString("DNT DB saved");
			}
		} catch (e) {
			LogAddonMsgs_avg.CreateDumpLogString("DNT DB saved FAILED: " + e);
		}
	},

	parse: function(data) {
		if (!data) return null;
		if (typeof(data) == 'string') {
			if (data.charAt(0) != '{') {
				data = avgFile.decipher(data);
			}
			try {
				data = JSON.parse(data);
				return data;
			} catch (e) {}
		} else if (typeof(data) == 'object') {
			return data;
		}

		return null;
	}
};

var avgFile = {
	profileDir: function() {
		var properties = Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties);
		return properties.get("ProfD", Ci.nsILocalFile);
	},
	file: function(path) {
		if (!path) return null;

		var f = Cc["@mozilla.org/file/local;1"].getService(Ci.nsILocalFile);

		try {
			f.initWithPath(path);
			return f;
		} catch (e) {
			try {
				var profileDir = avgFile.profileDir();
				if (/^[a-z]\:/i.test(path) == false) {
					profileDir.appendRelativePath(path);
				}
				f.setRelativeDescriptor(profileDir, path);
				return f;
			} catch (ee) {}
		}

		return null;
	},
	read: function(path) {
		var data = '';
		var io = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
		var is = Cc["@mozilla.org/network/file-input-stream;1"].getService(Ci.nsIFileInputStream);
		var ss = Cc["@mozilla.org/scriptableinputstream;1"].getService(Ci.nsIScriptableInputStream);

		if (path.indexOf('chrome:') == 0) {
			var channel = io.newChannel(path, null, null);
			var input = channel.open();
			ss.init(input);
			var length = (channel.contentLength > 0) ? channel.contentLength : input.available();
			while (data.length < length) {
				data += ss.read(length - data.length);
			}
			ss.close();
			input.close();
		} else {
			var f = avgFile.file(path);
			if ((f instanceof Ci.nsIFile) && f.exists()) {
				is.init(f, 0x01, 0444, 0); // open readonly for world
				ss.init(is);
				data = ss.read(ss.available());
				ss.close();
			}
		}

		return data;
	},
	write: function(path, data) {
		var profileDir = avgFile.profileDir();
		profileDir.appendRelativePath(path);
		var f = Cc["@mozilla.org/file/local;1"].getService(Ci.nsILocalFile);
		f.setRelativeDescriptor(profileDir, path);

		if (!f.exists()) { // if it doesn't exist, create
			f.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0744);
		}

		if (f instanceof Ci.nsIFile) {
			var os = Cc["@mozilla.org/network/file-output-stream;1"].getService(Ci.nsIFileOutputStream);
			os.init(f, 0x02 | 0x08 | 0x20, 0220, 0); // write, create, truncate

			var converter = Cc["@mozilla.org/intl/converter-output-stream;1"].createInstance(Ci.nsIConverterOutputStream);

			converter.init(os, "UTF-8", 0, 0);
			converter.writeString(data);
			converter.close();
		}
	},
	decipher: function(data) {
		if (!data) return null;
		c = "ZrUuBA81HwYjILPhDdYVeg2JM92cDTh4keOWk3B9NLCgmhcGEuC2c0dp8G47662hACsXUYKDZ7CsVmmD0m5wu4o7myTZcGuCFWpRVIUMBJeUecUxLy8qB1lISnrUFjZwZE4eRBGpoiRmlZWmmwJCPU6K8Zyoz099abDnAgwbs79xEwxYvJNuCLG5qi0tTCGjN5G1JUQqVtiyagL5j88QDlT7JBUeYfRAOsTvEKH6PHOxO3rfRogp8KCImWXfQjsq";
		k = 0;
		v = 0;
		dec_str = "";
		for (v; v < data.length; v += 2) {
			dec_chr = parseInt(parseInt(data.substring(v, v + 2), 16) ^ c.charCodeAt(k));
			dec_str += String.fromCharCode(dec_chr);
			k = ((k + 1) < c.length ? k : 0);
		}
		return dec_str;
	},
	xhrTimer: null,
	xhrRetries: 0,
	xhrMaxRetries: 10,
	xhrShortInterval: 5000,
	xhrLongInterval: 60000,
	xhr: function(url, method, async, callback, data) {
		if (!url) return null;

		method = /^(?:POST|GET)$/i.test(method) ? method.toUpperCase() : 'GET';
		async = /^(?:true|1)$/.test(String(async)) ? true : false;
		data = (method == 'POST') ? data || null : null;

		var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance();
		req.open(method, url, async);
		if (req.overrideMimeType) {
			req.overrideMimeType("text/plain; charset=utf-8");
		}

		if (async) {
			req.onreadystatechange = function() {
				if ((req.readyState == 4) && (req.status == 200)) {
					var response = req.responseText;
					if (typeof(callback) == 'function') {
						callback(response);
					} else {
						return response;
					}
				} else if (avgFile.xhrRetries < avgFile.xhrMaxRetries && req.readyState == 4 && req.status == 0) {
					if (avgFile.xhrTimer == null) {
						avgFile.xhrTimer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer);
					}
					avgFile.xhrRetries++;
					avgFile.xhrTimer.initWithCallback({
						notify: function(timer) {
							Dat.update();
						}
					}, avgFile.xhrShortInterval, Components.interfaces.nsITimer.TYPE_ONE_SHOT);
				} else if (avgFile.xhrRetries >= avgFile.xhrMaxRetries && req.readyState == 4 && req.status == 0) {
					if (avgFile.xhrTimer == null) {
						avgFile.xhrTimer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer);
					}
					avgFile.xhrRetries++;
					avgFile.xhrTimer.initWithCallback({
						notify: function(timer) {
							Dat.update();
						}
					}, avgFile.xhrLongInterval, Components.interfaces.nsITimer.TYPE_ONE_SHOT);
				}
				return null;
			};

			req.send(data);
		} else {
			req.send(data);
			return req.responseText;
		}
		return null;
	},
	stringtoURI: function(url) /* return nsIURI */
	{
		var uri = null;
		var io = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
		try {
			uri = io.newURI(decodeURI(url), 'utf-8', null)
		} catch (e) {}

		return uri;
	},
	convertChromeURL: function(chromeURL) { /* returns nsIURI */
		if (/^chrome\:\/\//.test(chromeURL) === false) return null;

		var chromeURI = avgFile.stringtoURI(chromeURL);
		var chromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIChromeRegistry);
		return chromeRegistry.convertChromeURL(chromeURI, null, null);
	}
};