/** \file
 * \brief Code that updates configuration stored by the user after upgrades
 *
 *  \author Copyright (C) 2019  Martin Timko
 *  \author Copyright (C) 2019-2021  Libor Polcak
 *
 *  \license SPDX-License-Identifier: GPL-3.0-or-later
 */
//
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  (at your option) any later version.
//
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with this program.  If not, see <https://www.gnu.org/licenses/>.
//

async function installUpdate() {
	/**
	 * 0.3+ storage
	 *  {
	 *    __default__: 2, // Default protection level
	 *    version: 2.2,     // The version of this storage
	 *    custom_levels: {}, // associative array of custom level (key, its id => object)
	 *      {level_id: short string used for example on the badge
	 *       level_text: Short level description
	 *       level_description: Full level description
	 *       ...
	 *       wrapping_params (key-value pairs), see wrapping_groups for the list of params and
	 *                       supported values
	 *      }
	 *    domains: {}, // associative array of levels associated with specific domains (key, the domain => object)
	 *      {level_id: short string of the level in use
	 *      }
	 *	  nbsWhitelist: {} // associative array of hosts that are removed from http protection control (hostname => boolean)
	 *	  requestShieldOn: {} // Boolean, if it's TRUE or undefined, the http request protection is turned on,  if it's FALSE, the protection si turned off
	 *	  fpDetectionOn: {} // Boolean, if it's TRUE, the fingerprint detection is turned on,  if it's FALSE or undefined, the protection si turned off
	 *
	 *
	 */
		let item = await browser.storage.sync.get(null);
		if (!item.hasOwnProperty("version") || (item.version < 2.1)) {
			browser.storage.sync.clear();
			console.log("All JavaScript Restrictor data cleared! Unfortunately, we do not migrate settings from versions bellow 0.3.");
			item = {
				__default__: 2,
				version: 2.1,
				custom_levels: {},
				domains: {},
			};
		}
		let original_version = item.version;
		make_configuration_compatible_with_update(item);
		if (original_version < 6.7 && item.version >= 6.7) {
			await browser.storage.sync.remove("whitelistedHosts"); // Renamed in 6.2 but not removed, removed in version 6.7
		}
		await browser.storage.sync.set(item);
		// origin of update.js must be recognized (background script vs. options page)
		if (typeof fpdLoadConfiguration === "function") {
			fpdLoadConfiguration();
		}
		else {
			browser.runtime.sendMessage({purpose: "fpd-load-config"});
		}
		if (typeof nbsLoadConfiguration === "function") {
			nbsLoadConfiguration();
		}
		else {
			browser.runtime.sendMessage({purpose: "nbs-load-config"})
		}
}

function make_configuration_compatible_with_update(item) {
		if (item.version == 2.1) {
			// No Geolocation below 2.2
			for (level in item["custom_levels"]) {
				let l = item["custom_levels"][level];
				if (l.time_precision) {
					l.geolocation = true;
					if (l.time_precision_randomize) {
						l.geolocation_locationObfuscationType = 5;
					}
					else if (l.time_precision_precision == 2) {
						l.geolocation_locationObfuscationType = 2;
					}
					else if (l.time_precision_precision == 1) {
						l.geolocation_locationObfuscationType = 3;
					}
					else if (l.time_precision_precision == 0) {
						l.geolocation_locationObfuscationType = 4;
					}
					else {
						l.geolocation_locationObfuscationType = -1;
					}
					// note that the obfuscation type might be redefined below
				}
				if (l.shared_array || l.webworker || l.xhr || l.arrays) {
					l.geolocation = true;
					l.geolocation_locationObfuscationType = 0;
				}
				if (l.geolocation_locationObfuscationType === undefined && l.htmlcanvaselement) {
					l.geolocation = true;
					l.geolocation_locationObfuscationType = 3;
				}
			}
			item.version = 2.2;
		}
		if (item.version == 2.2) {
			// No window.name wrapping below 2.2
			for (level in item["custom_levels"]) {
				let l = item["custom_levels"][level];
				let count = 0;
				count += Number(Boolean(l.time_precision)) +
				         Number(Boolean(l.hardware)) +
				         Number(Boolean(l.battery)) +
				         Number(Boolean(l.geolocation)) +
				         Number(Boolean(l.shared_array)) +
				         Number(Boolean(l.webworker)) +
				         Number(Boolean(l.xhr)) +
				         Number(Boolean(l.arrays)) +
				         Number(Boolean(l.htmlcanvaselement));
				if (count >= 3) {
					l.windowname = true;
				}
			}
			item.version = 2.3;
		}
		if (item.version == 2.3) {
			// No enumerateDevices wrapping below 2.4
			for (level in item["custom_levels"]) {
				let l = item["custom_levels"][level];
				let count = 0;
				count += Number(Boolean(l.time_precision)) +
				         Number(Boolean(l.hardware)) +
				         Number(Boolean(l.battery)) +
				         Number(Boolean(l.geolocation)) +
				         Number(Boolean(l.shared_array)) +
				         Number(Boolean(l.webworker)) +
				         Number(Boolean(l.xhr)) +
				         Number(Boolean(l.arrays)) +
				         100*Number(Boolean(l.htmlcanvaselement));
				if (count >= 102) {
					l.enumerateDevices = true;
				}
			}
			item.version = 2.4;
		}
		if (item.version < 2.6) {
			// No Beacon API (analytics) wrapping below 2.6
			for (level in item["custom_levels"]) {
				let l = item["custom_levels"][level];
				if (l.windowname || l.battery || l.geolocation || l.enumerateDevices || l.time_precision || l.hardware) {
					l.analytics = true;
				}
			}
			item.version = 2.6;
		}
		if (item.version < 2.7) {
			for (level in item["custom_levels"]) {
				let l = item["custom_levels"][level];
				if (l.htmlcanvaselement) {
					l.htmlcanvaselement_method = 1;
					l.audiobuffer = true;
					l.audiobuffer_method = 0;
					l.webgl = true;
					l.webgl_method = 0;
				}
			}
			item.version = 2.7;
		}
		if (item.version < 2.8) {
			for (level in item["custom_levels"]) {
				let l = item["custom_levels"][level];
				if (l.htmlcanvaselement) {
					l.plugins = true;
					if (l.htmlcanvaselement_method == 0) {
						l.plugins_method = 0;
					}
					else {
						l.plugins_method = 2;
					}
				}
			}
			item.version = 2.8;
		}
		if (item.version < 2.9) {
			for (level in item["custom_levels"]) {
				let l = item["custom_levels"][level];
				if (l.analytics) {
					l.gamepads = true;
				}
			}
			item.version = 2.9;
		}
		if (item.version < 3) {
			for (level in item["custom_levels"]) {
				let l = item["custom_levels"][level];
				if (l.gamepads) {
					l.vr = true;
				}
			}
			item.version = 3;
		}
		if (item.version < 4) {
			for (level in item["custom_levels"]) {
				let l = item["custom_levels"][level];
				if (l.hardware || l.battery || l.windowname) {
					l.physical_environment = true;
					l.physical_environment_emulateStationaryDevice = true;
				}
			}
			item.version = 4;
		}
		if (item.version < 5) {
			item.fpDetectionOn = false;
			item.version = 5;
		}
		if (item.version < 6) {
			for (level in item["custom_levels"]) {
				let l = item["custom_levels"][level];
				// time_precision
				if (l.time_precision) {
					if (l.time_precision_randomize || l.time_precision_precision < 1) {
						l.time_precision = 3;
					}
					else if (l.time_precision_precision == 1) {
						l.time_precision = 2;
					}
					else if (l.time_precision_precision > 1) {
						l.time_precision = 1;
					}
					else {
						delete l.time_precision;
					}
				}
				else {
					delete l.time_precision;
				}
				delete l.time_precision_precision;
				delete l.time_precision_randomize;
				// regular farbling
				for (farblable of ["htmlcanvaselement", "audiobuffer", "webgl", "plugins", "enumerateDevices", "hardware"]) {
					if (l[farblable]) {
						if (l[farblable + "_method"] !== undefined) {
							l[farblable] = l[farblable + "_method"] + 1;
						}
						else {
							l[farblable] = 2;
						}
					}
					else {
						delete l[farblable];
					}
					delete l[farblable + "_method"];
				}
				// xhr
				if (l.xhr) {
					if (l.xhr_behaviour_block && !l.xhr_behaviour_ask) {
						l.xhr = 2;
					}
					else {
						l.xhr = 1;
					}
				}
				else {
					delete l.xhr;
				}
				delete l.xhr_behaviour_block;
				delete l.xhr_behaviour_ask;
				// arrays
				if (l.arrays) {
					if (l.arrays_mapping) {
						l.arrays = 2;
					}
					else {
						l.arrays = 1;
					}
				}
				else {
					delete l.arrays;
				}
				delete l.arrays_mapping;
				//sharred_array
				if (l.shared_array) {
					if (l.shared_array_approach_block) {
						l.shared_array = 2;
					}
					else {
						l.shared_array = 1;
					}
				}
				else {
					delete l.shared_array;
				}
				delete l.shared_array_approach_block;
				delete l.shared_array_approach_polyfill;
				// webworker
				if (l.webworker) {
					if (l.webworker_approach_polyfill) {
						l.webworker = 2;
					}
					else {
						l.webworker = 1;
					}
				}
				else {
					delete l.webworker;
				}
				delete l.webworker_approach_polyfill;
				delete l.webworker_approach_slow;
				// geolocation
				if (l.geolocation) {
					if (l.geolocation_locationObfuscationType == 0) {
						l.geolocation = 6;
					}
					else if (l.geolocation_locationObfuscationType == 2) {
						l.geolocation = 2;
					}
					else if (l.geolocation_locationObfuscationType == 3) {
						l.geolocation = 3;
					}
					else if (l.geolocation_locationObfuscationType == 4) {
						l.geolocation = 4;
					}
					else if (l.geolocation_locationObfuscationType == 5) {
						l.geolocation = 5;
					}
					else if (l.geolocation_locationObfuscationType == -1) {
						l.geolocation = 1;
					}
					else {
						l.geolocation = 3;
					}
				}
				else {
					delete l.geolocation;
				}
				delete l.geolocation_locationObfuscationType;
				// others
				for (wrap of ["physical_environment", "gamepads", "vr", "analytics", "battery", "windowname"]) {
					if (l[wrap]) {
						l[wrap] = 1;
					}
					else {
						delete l[wrap];
					}
				}
				delete l.physical_environment_emulateStationaryDevice;
			}
			item.version = 6;
		}
		if (item.version < 6.1) {
			// We no longer ship level 1 as its content is not defined well, the tweaks allow the user
			// to relax the level conditions specifically to each page
			for (domain in item.domains) {
				let level = item.domains[domain];
				// Select the default level if the default level is not 3 (if level 3 is the default, set
				// level 2)
				if (level.level_id == "1") {
					if ([2, 3, "2", "3"].includes(item.__default__)) {
						level.level_id = "2";
					}
					else {
						level.level_id = String(item.__default__);
					}
				}
			}
			item.version = 6.1;
		}
		if (item.version < 6.2) {
			if (item.fpDetectionOn) {
				item.fpdSettings = {
					behavior: 3
				};
			}
			else {
				item.fpdSettings = {
					behavior: 1
				};
			}
			item.fpDetectionOn = true;
			item.nbsWhitelist = item.whitelistedHosts ? item.whitelistedHosts : {};
			delete item.whitelistedHosts;
			item.nbsSettings = {
				notifications: 1
			};
			item.version = 6.2;
		}
		if (item.version < 6.3) {
			if (level_2.windowname === undefined) { // Firefox
				for (level in item["custom_levels"]) {
					let l = item["custom_levels"][level];
					if (!(l.arrays || l.shared_array)) {
						delete l.windowname;
					}
				}
			}
			for (level in item["custom_levels"]) {
				let l = item["custom_levels"][level];
				l.webworker = 2;
			}
			item.version = 6.3;
		}
		if (item.version < 6.4) {
			if (item.nbsSettings) {
				item.nbsSettings.blocking = 1;
			}
			if (item.fpdSettings) {
				item.fpdSettings.detection = 0;
			}
			item.version = 6.4;
		}
		if (item.version < 6.5) {
			for (level in item["custom_levels"]) {
				let l = item["custom_levels"][level];
				if (l.time_precision && l.analytics && l.geolocation) {
					l.coopschedule = 1;
					l.net = 1;
					l.nfc = 1;
					l.useridle = 1;
					if (l.htmlcanvaselement || l.audiobuffer || l.enumerateDevices) {
						l.useridle = 2;
						if (l.htmlcanvaselement === 2 || l.audiobuffer === 2 || l.enumerateDevices === 3) {
							l.useridle = 3;
							l.playback = 2;
						}
					}
				}
			}
			item.version = 6.5;
		}
		if (item.version < 6.6) {
			if (item.fpdSettings) {
				switch (item.fpdSettings.behavior) {
					case 0:
						item.fpdSettings.notifications = 0;
						break;
					case 1:
						item.fpdSettings.behavior = 0;
						item.fpdSettings.notifications = 1;
						break;
					case 2:
						item.fpdSettings.behavior = 1;
						item.fpdSettings.notifications = 1;
						break;
					case 3:
						item.fpdSettings.behavior = 2;
						item.fpdSettings.notifications = 1;
				}
			}
			item.version = 6.6;
		}
		if (item.version < 6.7) {
			item.version = 6.7;
		}
		if (item.version < 6.8) {
			for (level in item["custom_levels"]) {
				let l = item["custom_levels"][level];
				if (l.webworker === 2) {
					let score = 0;
					for (group of ["time_precision", "net", "geolocation", "physical_environment", "useridle", "coopschedule", "gamepads", "vr", "analytics", "nfc"]) {
						if (l[group] !== undefined) {
							score++;
						}
					}
					for (group of ["htmlcanvaselement", "audiobuffer", "webgl", "hardware"]) {
						if (l[group] === 1) {
							score--;
						}
						else if (l[group] > 1) {
							score++;
						}
					}
					for (group of ["enumerateDevices", "plugins"]) {
						if (l[group] === 1 || l[group] === 2) {
							score--;
						}
						else if (l[group] > 2) {
							score++;
						}
					}
					if (score > 8) { // L0: 0, L1: 10, L2: 4, L3: 16
						l.webworker = 3;
					}
				}
			}
			item.version = 6.8;
		}
		if (item.version < 7) {
			for (level in item["custom_levels"]) {
				let l = item["custom_levels"][level];
				if (l.audiobuffer === 1 || l.htmlcanvaselement === 1) {
					l.wasm = 1;
				}
			}
			item.version = 7;
		}
		if (item.version < 7.1) {
			for (domain in item.domains) {
				let normalized_domain = tld.normalize(domain);
				if (normalized_domain !== domain) {
					if (!(normalized_domain in item.domains)) {
						item.domains[normalized_domain] = item.domains[domain];
						delete item.domains[domain];
					}
				}
			}
			item.version = 7.1;
		}
}
browser.runtime.onInstalled.addListener(installUpdate);
// fallback - populate storage with valid data (if onInstalled won't fire)
browser.storage.sync.get(null).then((item) => {
	checkAndSaveConfig(item, false); // level might not be loaded by this time
});

async function checkAndSaveConfig(conf, check_default = true) {
	let checkSettingRange = (module, setting, range, defValue) => {
		if (!(conf[module][setting] in range)) {
			conf[module][setting] = defValue;
		}
	}
	let checkExistAndType = (name, type, defValue) => {
		if (!(name in conf && typeof(conf[name]) === type)) {
			conf[name] = defValue;
		}
	}
	checkExistAndType("version", "number", 2.1);
	checkExistAndType("requestShieldOn", "boolean", true);
	checkExistAndType("fpDetectionOn", "boolean", false);
	checkExistAndType("custom_levels", "object", {});
	if (!("__default__" in conf) || typeof(conf.__default__) !== "string" ||
			(!(conf.__default__ in levels) && check_default)) {
		conf.__default__ = "2";
	}
	checkExistAndType("domains", "object", {});
	checkExistAndType("nbsWhitelist", "object", {});
	checkExistAndType("nbsSettings", "object", {});
	checkSettingRange("nbsSettings", "blocking", [0,1], 1);
	checkSettingRange("nbsSettings", "notifications", [0,1], 1);
	checkExistAndType("fpdWhitelist", "object", {});
	checkExistAndType("fpdSettings", "object", {});
	checkSettingRange("fpdSettings", "behavior", [0,1,2,3], 1);
	checkSettingRange("fpdSettings", "notifications", [0,1], 1);
	checkSettingRange("fpdSettings", "detection", [0,1], 0);
	await browser.storage.sync.set(conf);
	await installUpdate();
}
