/** \file
 * \brief Operations and data structures connected to protection levels
 *
 *  \author Copyright (C) 2019-2021  Libor Polcak
 *  \author Copyright (C) 2019  Martin Timko
 *  \author Copyright (C) 2021  Matus Svancar
 *	\author Copyright (C) 2022  Marek Salon
 *  \author Copyright (C) 2022  Martin Bednar
 *  \author Copyright (C) 2023  Martin Zmitko
 *
 *  \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/>.
//

/**
 * Wrapping groups
 *
 * Used to control the built-in levels and GUI (e.g. level tweaks).
 */
var wrapping_groups = {
	empty_level: { /// Automatically populated
		level_text: "",
		level_id: "",
		level_description: "",
	},
	group_map: {}, ///Automatically populated
	wrapper_map: {}, ///Automatically populated
	group_names: [], ///Automatically populated
	get_wrappers: function(level) {
		wrappers = [];
		for (group of wrapping_groups.groups) {
			if ((level[group.id] !== undefined) && level[group.id] !== 0) {
				let arg_values = group.params[level[group.id] - 1].config;
				group.wrappers.forEach((w) => wrappers.push([w, ...arg_values]));
			}
		}
		return wrappers;
	},
	groups: [
		{
			name: "time_precision",
			label: browser.i18n.getMessage("jssgroupTimePrecision"),
			description: browser.i18n.getMessage("jssgroupTimePrecision"),
			description2: [browser.i18n.getMessage("jssgroupTimePrecisionDescription2", browser.i18n.getMessage("jssgroupPhysicalLocationGeolocation"))],
			params: [
				{
					short: browser.i18n.getMessage("jssgroupPoor"),
					description: browser.i18n.getMessage("jssgroupTimePoorDescription"),
					config: [10, false],
				},
				{
					short: browser.i18n.getMessage("jssgroupLow"),
					description: browser.i18n.getMessage("jssgroupTimeLowDescription"),
					config: [100, false],
				},
				{
					short: browser.i18n.getMessage("jssgroupHigh"),
					description: browser.i18n.getMessage("jssgroupTimeHighDescription"),
					config: [1000, true],
				},
			],
			wrappers: [
				// HRT
				"Performance.prototype.now",
				// PT2
				"PerformanceEntry.prototype",
				// ECMA
				"window.Date",
				// TEMPORAL
				"Temporal.Now.instant",
				// DOM
				"Event.prototype.timeStamp",
				// GP
				"Gamepad.prototype.timestamp",
				// VR
				"VRFrameData.prototype.timestamp",
        // SENSOR
        "Sensor.prototype.timestamp",
			],
		},
		{
			name: "htmlcanvaselement",
			label: browser.i18n.getMessage("jssgroupLocallyRenderedImages"),
			description: browser.i18n.getMessage("jssgroupLocallyRenderedImagesDescription"),
			description2: [
				browser.i18n.getMessage("jssgroupCanvasLocallyRenderedImagesDescription2"),
				browser.i18n.getMessage("jssgroupCanvasLocallyRenderedImagesDescription3")
			],
			params: [
				{
					short: browser.i18n.getMessage("jssgroupLittleLies"),
					description: browser.i18n.getMessage("jssgroupLocallyRenderedImagesLittleLiesDescription"),
					config: [0],
				},
				{
					short: browser.i18n.getMessage("jssgroupStrict"),
					description: browser.i18n.getMessage("jssgroupLocallyRenderedImagesStrictDescription"),
					config: [1],
				},
			],
			wrappers: [
				// H-C
				"CanvasRenderingContext2D.prototype.getImageData",
				"HTMLCanvasElement.prototype.toBlob",
				"HTMLCanvasElement.prototype.toDataURL",
				"OffscreenCanvas.prototype.convertToBlob",
				"CanvasRenderingContext2D.prototype.isPointInStroke",
				"CanvasRenderingContext2D.prototype.isPointInPath",
				"WebGLRenderingContext.prototype.readPixels",
				"WebGL2RenderingContext.prototype.readPixels",
			],
		},
		{
			name: "audiobuffer",
			label: browser.i18n.getMessage("jssgroupLocallyGeneratedAudio"),
			description: browser.i18n.getMessage("jssgroupLocallyGeneratedAudioDescription"),
			description2: [
				browser.i18n.getMessage("jssgroupLocallyGeneratedAudioDescription2"),
			],
			params: [
				{
					short: browser.i18n.getMessage("jssgroupLittleLies"),
					description: browser.i18n.getMessage("jssgroupLocallyGeneratedAudioLittleLiesDescription"),
					config: [0],
				},
				{
					short: browser.i18n.getMessage("jssgroupStrict"),
					description: browser.i18n.getMessage("jssgroupLocallyGeneratedAudioStrictDescription"),
					config: [1],
				},
			],
			wrappers: [
				// AUDIO
				"AudioBuffer.prototype.getChannelData",
				"AudioBuffer.prototype.copyFromChannel",
				"AnalyserNode.prototype.getByteTimeDomainData",
				"AnalyserNode.prototype.getFloatTimeDomainData",
				"AnalyserNode.prototype.getByteFrequencyData",
				"AnalyserNode.prototype.getFloatFrequencyData"
			],
		},
		{
			name: "webgl",
			label: browser.i18n.getMessage("jssgroupGraphicCardInformation"),
			description: browser.i18n.getMessage("jssgroupGraphicCardInformationDescription"),
			description2: [
				browser.i18n.getMessage("jssgroupGraphicCardInformationDescription2"),
				browser.i18n.getMessage("jssgroupGraphicCardInformationDescription3"),
		],
			params: [
				{
					short: browser.i18n.getMessage("jssgroupLittleLies"),
					description: browser.i18n.getMessage("jssgroupGraphicCardInformationLittleLiesDescription"),
					config: [0],
				},
				{
					short: browser.i18n.getMessage("jssgroupStrict"),
					description: browser.i18n.getMessage("jssgroupGraphicCardInformationStrictDescription"),
					config: [1],
				},
			],
			wrappers: [
				// WEBGL
				"WebGLRenderingContext.prototype.getParameter",
				"WebGL2RenderingContext.prototype.getParameter",
				"WebGLRenderingContext.prototype.getFramebufferAttachmentParameter",
				"WebGL2RenderingContext.prototype.getFramebufferAttachmentParameter",
				"WebGLRenderingContext.prototype.getActiveAttrib",
				"WebGL2RenderingContext.prototype.getActiveAttrib",
				"WebGLRenderingContext.prototype.getActiveUniform",
				"WebGL2RenderingContext.prototype.getActiveUniform",
				"WebGLRenderingContext.prototype.getAttribLocation",
				"WebGL2RenderingContext.prototype.getAttribLocation",
				"WebGLRenderingContext.prototype.getBufferParameter",
				"WebGL2RenderingContext.prototype.getBufferParameter",
				"WebGLRenderingContext.prototype.getProgramParameter",
				"WebGL2RenderingContext.prototype.getProgramParameter",
				"WebGLRenderingContext.prototype.getRenderbufferParameter",
				"WebGL2RenderingContext.prototype.getRenderbufferParameter",
				"WebGLRenderingContext.prototype.getShaderParameter",
				"WebGL2RenderingContext.prototype.getShaderParameter",
				"WebGLRenderingContext.prototype.getShaderPrecisionFormat",
				"WebGL2RenderingContext.prototype.getShaderPrecisionFormat",
				"WebGLRenderingContext.prototype.getTexParameter",
				"WebGL2RenderingContext.prototype.getTexParameter",
				"WebGLRenderingContext.prototype.getUniformLocation",
				"WebGL2RenderingContext.prototype.getUniformLocation",
				"WebGLRenderingContext.prototype.getVertexAttribOffset",
				"WebGL2RenderingContext.prototype.getVertexAttribOffset",
				"WebGLRenderingContext.prototype.getSupportedExtensions",
				"WebGL2RenderingContext.prototype.getSupportedExtensions",
				"WebGLRenderingContext.prototype.getExtension",
				"WebGL2RenderingContext.prototype.getExtension",
			],
		},
		{
			name: "plugins",
			label: browser.i18n.getMessage("jssgroupInstalledBrowserPlugins"),
			description: browser.i18n.getMessage("jssgroupInstalledBrowserPluginsDescription"),
			description2: [browser.i18n.getMessage("jssgroupInstalledBrowserPluginsDescription2")],
			params: [
				{
					short: browser.i18n.getMessage("jssgroupLittleLies"),
					description: browser.i18n.getMessage("jssgroupInstalledBrowserPluginsLittleLiesDescription"),
					config: [0],
				},
				{
					short: browser.i18n.getMessage("jssgroupFake"),
					description: browser.i18n.getMessage("jssgroupInstalledBrowserPluginsFakeDescription"),
					config: [1],
				},
				{
					short: browser.i18n.getMessage("jssgroupStrict"),
					description: browser.i18n.getMessage("jssgroupInstalledBrowserPluginsStrictDescription"),
					config: [2],
				},
			],
			wrappers: [
				// NP
				"Navigator.prototype.plugins", // also modifies "Navigator.prototype.mimeTypes",
			],
		},
		{
			name: "enumerateDevices",
			label: browser.i18n.getMessage("jssgroupConnectedCamerasAndMicrophones"),
			description: browser.i18n.getMessage("jssgroupConnectedCamerasAndMicrophonesDescription"),
			description2: [
				browser.i18n.getMessage("jssgroupConnectedCamerasAndMicrophonesDescription2"),
		],
			params: [
				{
					short: browser.i18n.getMessage("jssgroupLittleLies"),
					description: browser.i18n.getMessage("jssgroupConnectedCamerasAndMicrophonesLittleLiesDescription"),
					config: [0],
				},
				{
					short: browser.i18n.getMessage("jssgroupAddFake"),
					description: browser.i18n.getMessage("jssgroupConnectedCamerasAndMicrophonesAddFakeDescription"),
					config: [1],
				},
				{
					short: browser.i18n.getMessage("jssgroupStrict"),
					description: browser.i18n.getMessage("jssgroupConnectedCamerasAndMicrophonesStrictDescription"),
					config: [2],
				},
			],
			wrappers: [
				// MCS
				"MediaDevices.prototype.enumerateDevices",
			],
		},
		{
			name: "hardware",
			label: browser.i18n.getMessage("jssgroupHardware"),
			description: browser.i18n.getMessage("jssgroupHardwareDescription"),
			description2: [
				browser.i18n.getMessage("jssgroupHardwareDescription2"),
			],
			params: [
				{
					short: browser.i18n.getMessage("jssgroupLow"),
					description: browser.i18n.getMessage("jssgroupHardwareLowDescription"),
					config: [0],
				},
				{
					short: browser.i18n.getMessage("jssgroupMedium"),
					description: browser.i18n.getMessage("jssgroupHardwareMediumDescription"),
					config: [1],
				},
				{
					short: browser.i18n.getMessage("jssgroupHigh"),
					description: browser.i18n.getMessage("jssgroupHardwareHighDescription"),
					config: [2],
				},
			],
			wrappers: [
				// HTML-LS
				"Navigator.prototype.hardwareConcurrency",
				// DM
				"Navigator.prototype.deviceMemory",
			],
		},
		{
			name: "net",
			label: browser.i18n.getMessage("jssgroupNetworkConditions"),
			description: browser.i18n.getMessage("jssgroupNetworkConditionsDescription"),
			description2: [],
			params: [
				{
					short: browser.i18n.getMessage("jssgroupRemove"),
					description: browser.i18n.getMessage("jssgroupNetworkConditionsRemoveDescription"),
					config: [0],
				},
			],
			wrappers: [
				// NET
				"Navigator.prototype.connection",
				"window.NetworkInformation",
			],
		},
		{
			name: "xhr",
			label: browser.i18n.getMessage("jssgroupXMLHttpRequestRequests"),
			description: browser.i18n.getMessage("jssgroupXMLHttpRequestRequestsDescription"),
			description2: [browser.i18n.getMessage("jssgroupXMLHttpRequestRequestsDescription2")],
			params: [
				{
					short: browser.i18n.getMessage("jssgroupAsk"),
					description: browser.i18n.getMessage("jssgroupXMLHttpRequestRequestsAskDescription"),
					config: [false, true],
				},
				{
					short: browser.i18n.getMessage("jssgroupBlock"),
					description: browser.i18n.getMessage("jssgroupXMLHttpRequestRequestsBlockDescription"),
					config: [true, false],
				},
			],
			wrappers: [
				// AJAX
				"XMLHttpRequest.prototype.open",
				"XMLHttpRequest.prototype.send",
			],
		},
		{
			name: "arrays",
			label: browser.i18n.getMessage("jssgroupArrays"),
			description: browser.i18n.getMessage("jssgroupArraysDescription"),
			description2: [],
			params: [
				{
					short: browser.i18n.getMessage("jssgroupArraysShift"),
					description: browser.i18n.getMessage("jssgroupArraysShiftDescription"),
					config: [false],
				},
				{
					short: browser.i18n.getMessage("jssgroupArraysRandomize"),
					description: browser.i18n.getMessage("jssgroupArraysRandomizeDescription"),
					config: [true],
				},
			],
			wrappers: [
				"window.DataView",
				"window.Uint8Array",
				"window.Int8Array",
				"window.Uint8ClampedArray",
				"window.Int16Array",
				"window.Uint16Array",
				"window.Int32Array",
				"window.Uint32Array",
				"window.Float32Array",
				"window.Float64Array",
				'window.BigInt64Array',
				'window.BigUint64Array',
			],
		},
		{
			name: "shared_array",
			label: browser.i18n.getMessage("jssgroupSharedArraysBuffer"),
			description: browser.i18n.getMessage("jssgroupSharedArraysBufferDescription"),
			description2: [],
			params: [
				{
					short: browser.i18n.getMessage("jssgroupMedium"),
					description: browser.i18n.getMessage("jssgroupSharedArraysBufferMediumDescription"),
					config: [false],
				},
				{
					short: browser.i18n.getMessage("jssgroupStrict"),
					description: browser.i18n.getMessage("jssgroupSharedArraysBufferStrictDescription"),
					config: [true],
				},
			],
			wrappers: [
				// SHARED
				"window.SharedArrayBuffer"
			],
		},
		{
			name: "webworker",
			label: browser.i18n.getMessage("jssgroupWebWorker"),
			description: browser.i18n.getMessage("jssgroupWebWorkerDescription"),
			description2: [browser.i18n.getMessage("jssgroupWebWorkerDescription2")],
			params: [
				{
					short: browser.i18n.getMessage("jssgroupLow"),
					description: browser.i18n.getMessage("jssgroupWebWorkerLowDescription"),
					config: [false, false],
				},
				{
					short: browser.i18n.getMessage("jssgroupStrict"),
					description: browser.i18n.getMessage("jssgroupWebWorkerStrictDescription"),
					config: [true, false],
				},
				{
					short: browser.i18n.getMessage("jssgroupRemove"),
					description: browser.i18n.getMessage("jssgroupWebWorkerRemoveDescription"),
					config: [false, true],
				},
			],
			wrappers: [
				"window.Worker",
			],
		},
		{
			name: "geolocation",
			label: browser.i18n.getMessage("jssgroupPhysicalLocationGeolocation"),
			description: browser.i18n.getMessage("jssgroupPhysicalLocationGeolocationDescription"),
			description2: [browser.i18n.getMessage("jssgroupPhysicalLocationGeolocationDescription2")],
			params: [
				{
					short: browser.i18n.getMessage("jssgroupGeolocationTimestampOnly"),
					description: browser.i18n.getMessage("jssgroupGeolocationTimestampOnlyDescription", browser.i18n.getMessage("jssgroupTimePrecision")),
					config: [-1],
				},
				{
					short: browser.i18n.getMessage("jssgroupGeolocationVillage"),
					description: browser.i18n.getMessage("jssgroupGeolocationVillageDescription"),
					config: [2],
				},
				{
					short: browser.i18n.getMessage("jssgroupGeolocationTown"),
					description: browser.i18n.getMessage("jssgroupGeolocationTownDescription"),
					config: [3],
				},
				{
					short: browser.i18n.getMessage("jssgroupGeolocationRegion"),
					description: browser.i18n.getMessage("jssgroupGeolocationRegionDescription"),
					config: [4],
				},
				{
					short: browser.i18n.getMessage("jssgroupGeolocationLongDistance"),
					description: browser.i18n.getMessage("jssgroupGeolocationLongDistanceDescription"),
					config: [5],
				},
				{
					short: browser.i18n.getMessage("jssgroupRemove"),
					description: browser.i18n.getMessage("jssgroupGeolocationRemoveDescription"),
					config: [0],
				},
			],
			wrappers: [
				// GPS
				"Navigator.prototype.geolocation",
				"window.Geolocation",
				"window.GeolocationCoordinates",
				"window.GeolocationPosition",
				"window.GeolocationPositionError",
				"Geolocation.prototype.getCurrentPosition",
				"Geolocation.prototype.watchPosition",
				"Geolocation.prototype.clearWatch"
			],
		},
    {
			name: "physical_environment",
			label: browser.i18n.getMessage("jssgroupPhysicalEnvironmentSensors"),
			description: browser.i18n.getMessage("jssgroupPhysicalEnvironmentSensorsDescription"),
			description2: [browser.i18n.getMessage("jssgroupPhysicalEnvironmentSensorsDescription2")],
			params: [
				{
					short: browser.i18n.getMessage("jssgroupHigh"),
					description: browser.i18n.getMessage("jssgroupPhysicalEnvironmentSensorsHighDescription"),
					config: [true],
				},
			],
			wrappers: [
        // GENERIC SENSOR API Sensors

				// Magnetometer
				"Magnetometer.prototype.x",
        "Magnetometer.prototype.y",
        "Magnetometer.prototype.z",

        // Accelerometer, LinearAccelerationSensor, and GravitySensor
        "Accelerometer.prototype.x",
        "Accelerometer.prototype.y",
        "Accelerometer.prototype.z",

        // Gyroscope
        "Gyroscope.prototype.x",
        "Gyroscope.prototype.y",
        "Gyroscope.prototype.z",

        // AbsoluteOrientationSensor and RelativeOrientationSensor
        "OrientationSensor.prototype.quaternion",

        // AmbientLightSensor
        "AmbientLightSensor.prototype.illuminance"
			],
		},
		{
			name: "useridle",
			label: browser.i18n.getMessage("jssgroupUserIdleDetection"),
			description: browser.i18n.getMessage("jssgroupUserIdleDetectionDescription"),
			description2: [browser.i18n.getMessage("jssgroupUserIdleDetectionDescription2")],
			params: [
				{
					short: browser.i18n.getMessage("jssgroupConfuse"),
					description: browser.i18n.getMessage("jssgroupConfuseDescription"),
					config: [0],
				},
				{
					short: browser.i18n.getMessage("jssgroupBlock"),
					description: browser.i18n.getMessage("jssgroupBlockDescription"),
					config: [1],
				},
				{
					short: browser.i18n.getMessage("jssgroupRemove"),
					description: browser.i18n.getMessage("jssgroupRemoveDescription"),
					config: [2],
				},
			],
			wrappers: [
				// COOP-SCHEDULING
				"window.IdleDetector",
				"IdleDetector.requestPermission",
				"IdleDetector.prototype.screenState",
				"IdleDetector.prototype.userState",
			],
		},
		{
			name: "coopschedule",
			label: browser.i18n.getMessage("jssgroupCoopschedule"),
			description: browser.i18n.getMessage("jssgroupCoopscheduleDescription"),
			description2: [browser.i18n.getMessage("jssgroupCoopscheduleDescription2")],
			params: [
				{
					short: browser.i18n.getMessage("jssgroupConfuse"),
					description: browser.i18n.getMessage("jssgroupCoopscheduleConfuseDescription"),
					config: [],
				},
			],
			wrappers: [
				// COOP-SCHEDULING
				"IdleDeadline.prototype.didTimeout",
				"IdleDeadline.prototype.timeRemaining",
			],
		},
		{
			name: "gamepads",
			label: browser.i18n.getMessage("jssgroupGamepads"),
			description: browser.i18n.getMessage("jssgroupGamepadsDescription"),
			description2: [],
			params: [
				{
					short: browser.i18n.getMessage("jssgroupStrict"),
					description: browser.i18n.getMessage("jssgroupGamepadsStrictDescription"),
					config: [true],
				},
			],
			wrappers: [
				// GAMEPAD
				"Navigator.prototype.getGamepads",
			],
		},
		{
			name: "vr",
			label: browser.i18n.getMessage("jssgroupVirtualAndAugmentedRealityDevices"),
			description: browser.i18n.getMessage("jssgroupVirtualAndAugmentedRealityDevicesDescription"),
			description2: [browser.i18n.getMessage("jssgroupVirtualAndAugmentedRealityDevicesDescription2")],
			params: [
				{
					short: browser.i18n.getMessage("jssgroupStrict"),
					description: browser.i18n.getMessage("jssgroupVirtualAndAugmentedRealityDevicesStrictDescription"),
					config: [],
				},
			],
			wrappers: [
				// VR
				"Navigator.prototype.activeVRDisplays",
				// XR
				"Navigator.prototype.xr",
			],
		},
		{
			name: "playback",
			label: browser.i18n.getMessage("jssgroupMultimediaPlayback"),
			description: browser.i18n.getMessage("jssgroupMultimediaPlaybackDescription"),
			description2: [browser.i18n.getMessage("jssgroupMultimediaPlaybackDescription2")],
			params: [
				{
					short: browser.i18n.getMessage("jssgroupLittleLies"),
					description: browser.i18n.getMessage("jssgroupMultimediaPlaybackLittleLiesDescription"),
					config: [0],
				},
				{
					short: browser.i18n.getMessage("jssgroupStrict"),
					description: browser.i18n.getMessage("jssgroupMultimediaPlaybackStrictDescription"),
					config: [1],
				},
				{
					short: browser.i18n.getMessage("jssgroupBlock"),
					description: browser.i18n.getMessage("jssgroupMultimediaPlaybackBlockDescription"),
					config: [2],
				},
			],
			wrappers: [
				// EME
				"Navigator.prototype.requestMediaKeySystemAccess",
				// MEDIA-CAPABILITIES
				"MediaCapabilities.prototype.encodingInfo",
				"MediaCapabilities.prototype.decodingInfo",
				// HTML5
				"HTMLMediaElement.prototype.canPlayType",
			],
		},
		{
			name: "analytics",
			label: browser.i18n.getMessage("jssgroupUnreliableTransfersToServerBeacons"),
			description: browser.i18n.getMessage("jssgroupUnreliableTransfersToServerBeaconsDescription"),
			description2: [browser.i18n.getMessage("jssgroupUnreliableTransfersToServerBeaconsDescription2"), browser.i18n.getMessage("jssgroupUnreliableTransfersToServerBeaconsDescription3")],
			params: [
				{
					short: browser.i18n.getMessage("jssgroupBlock"),
					description: browser.i18n.getMessage("jssgroupUnreliableTransfersToServerBeaconsBlockDescription"),
					config: [],
				},
			],
			wrappers: [
				// BEACON
				"Navigator.prototype.sendBeacon",
			],
		},
		{
			name: "battery",
			label: browser.i18n.getMessage("jssgroupHardwareBattery"),
			description: browser.i18n.getMessage("jssgroupHardwareBatteryDescription"),
			description2: [],
			params: [
				{
					short: browser.i18n.getMessage("jssgroupRemove"),
					description: browser.i18n.getMessage("jssgroupHardwareBatteryRemoveDescription"),
					config: [],
				},
			],
			wrappers: [
				// BATTERY
				"Navigator.prototype.getBattery",
				"window.BatteryManager",
			],
		},
		{
			name: "windowname",
			label: browser.i18n.getMessage("jssgroupPersistentIdentifierOfTheBrowserTab"),
			description: browser.i18n.getMessage("jssgroupPersistentIdentifierOfTheBrowserTabDescription"),
			description2: [browser.i18n.getMessage("jssgroupPersistentIdentifierOfTheBrowserTabDescription2")],
			params: [
				{
					short: browser.i18n.getMessage("jssgroupStrict"),
					description: browser.i18n.getMessage("jssgroupPersistentIdentifierOfTheBrowserTabStrictDescription"),
					config: [],
				},
			],
			wrappers: [
				// WINDOW-NAME
				"window.name",
			],
		},
		{
			name: "nfc",
			label: browser.i18n.getMessage("jssgroupNFC"),
			description: browser.i18n.getMessage("jssgroupNFCDescription"),
			description2: [browser.i18n.getMessage("jssgroupNFCDescription2")],
			params: [
				{
					short: browser.i18n.getMessage("jssgroupRemove"),
					description: browser.i18n.getMessage("jssgroupNFCRemoveDescription"),
					config: [],
				},
			],
			wrappers: [
				// BATTERY
				"window.NDEFMessage",
				"window.NDEFReader",
				"window.NDEFRecord",
			],
		},
		{
			name: "wasm",
			label: browser.i18n.getMessage("jssgroupWASM"),
			description: browser.i18n.getMessage("jssgroupWASMDescription"),
			description2: [
				browser.i18n.getMessage("jssgroupWASMDescription2", [browser.i18n.getMessage("jssgroupLocallyRenderedImages"), browser.i18n.getMessage("jssgroupLocallyGeneratedAudio"), browser.i18n.getMessage("jssgroupLittleLies")]),
				browser.i18n.getMessage("jssgroupWASMDescription3"),
			],
			params: [
				{
					short: browser.i18n.getMessage("jssgroupWASMDisabled"),
					description: browser.i18n.getMessage("jssgroupWASMDisabledDescription"),
					config: [0],
				},
				{
					short: browser.i18n.getMessage("jssgroupWASMPassive"),
					description: browser.i18n.getMessage("jssgroupWASMPassiveDescription"),
					config: [1],
				},
				{
					short: browser.i18n.getMessage("jssgroupWASMActive"),
					description: browser.i18n.getMessage("jssgroupWASMActiveDescription"),
					description2: [
						"jssgroupWASMActiveDescription2","jssgroupWASMActiveDescription3","jssgroupWASMActiveDescription4"
					],
					config: [2],
				},
			],
			wrappers: [], // Special case with no wrappers, this group is for modifying htmlcanvaselement, webgl and audiobuffer farbling behaviour
		},
	],
}
var modify_wrapping_groups = modify_wrapping_groups || (() => null); // Give other scripts the possibility to modify the wrapping_groups objects
modify_wrapping_groups();

/**
 * Check if the given API is supported by the browser
 * @param String to the object which presence to check.
 */
function is_api_undefined(api) {
	if (!self.window) return false; // mv3 service worker?
	let s = api.split(".");
	let last = self;
	for (p of s) {
		try {
			if (last[p] === undefined) {
				return true;
			}
		}
		catch (e) {
			// We may have encountered something like
			// TypeError: 'get startTime' called on an object that does not implement interface PerformanceEntry.
			break;
		}
		last = last[p];
	}
	return false;
}

/**
 * Returns true if all given API wrappers are unsuported.
 */
function are_all_api_unsupported(wrappers) {
	for (wrapper of wrappers) {
		if (!is_api_undefined(wrapper)) {
			return false;
		}
	}
	return true;
}

/// Automatically populate infered metadata in wrapping_groups.
wrapping_groups.groups.forEach(function (group) {
	group.id = group.name;
	if (!are_all_api_unsupported(group.wrappers) || group.wrappers.length === 0) {
		wrapping_groups.group_names.push(group.name);
		wrapping_groups.empty_level[group.id] = 0;
	}
	wrapping_groups.group_map[group.id] = group
	for (wrapper_name of group.wrappers) {
		wrapping_groups.wrapper_map[wrapper_name] = group.name;
	}
});

// *****************************************************************************
// levels of protection

// Level aliases
const L0 = "0";
const L1 = "1";
const L2 = "2";
const L3 = "3";
const L_EXPERIMENTAL = "Experiment"; // Use a long ID so that it is not in conflict with pre0.7 user-defined levels

/// Built-in levels
var level_0 = {
	"builtin": true,
	"level_id": L0,
	"level_text": browser.i18n.getMessage("JSSL0Name"),
	"level_description": browser.i18n.getMessage("JSSL0Description"),
};

var level_1 = {
	"builtin": true,
	"level_id": L1,
	"level_text": browser.i18n.getMessage("JSSL1Name"),
	"level_description": browser.i18n.getMessage("JSSL1Description"),
	"time_precision": 3,
	"net": 1,
	"webworker": 3,
	"geolocation": 3,
  "physical_environment": 1,
	"useridle": 1,
	"coopschedule": 1,
	"gamepads": 1,
	"vr": 1,
	"analytics": 1,
	"battery": 1,
	"nfc": 1,
};

var level_2 = {
	"builtin": true,
	"level_id": L2,
	"level_text": browser.i18n.getMessage("JSSL2Name"),
	"level_description": browser.i18n.getMessage("JSSL2Description"),
	"time_precision": 3,
	"htmlcanvaselement": 1,
	"audiobuffer": 1,
	"webgl": 1,
	"plugins": 2,
	"enumerateDevices": 2,
	"hardware": 1,
	"net": 1,
	"webworker": 3,
	"geolocation": 3,
  "physical_environment": 1,
	"useridle": 2,
	"coopschedule": 1,
	"gamepads": 1,
	"vr": 1,
	"analytics": 1,
	"battery": 1,
	"windowname": 1,
	"nfc": 1,
	"wasm": 1,
};

var level_3 = {
	"builtin": true,
	"level_id": L3,
	"level_text": browser.i18n.getMessage("JSSL3Name"),
	"level_description": browser.i18n.getMessage("JSSL3Description"),
	"time_precision": 3,
	"htmlcanvaselement": 2,
	"audiobuffer": 2,
	"webgl": 2,
	"plugins": 3,
	"enumerateDevices": 3,
	"hardware": 3,
	"net": 1,
	"webworker": 3,
	"geolocation": 6,
  "physical_environment": 1,
	"useridle": 3,
	"coopschedule": 1,
	"gamepads": 1,
	"vr": 1,
	"playback": 2,
	"analytics": 1,
	"battery": 1,
	"windowname": 1,
	"nfc": 1,
};

var level_experimental = {
	"builtin": true,
	"level_id": L_EXPERIMENTAL,
	"level_text": browser.i18n.getMessage("JSSLexperimentalName"),
	"level_description": browser.i18n.getMessage("JSSLexperimentalDescription"),
	"time_precision": 3,
	"htmlcanvaselement": 2,
	"audiobuffer": 2,
	"webgl": 2,
	"plugins": 3,
	"enumerateDevices": 3,
	"hardware": 3,
	"net": 1,
	"xhr": 1,
	"arrays": 2,
	"shared_array": 2,
	"webworker": 2,
	"geolocation": 6,
  "physical_environment": 1,
	"useridle": 3,
	"coopschedule": 1,
	"gamepads": 1,
	"vr": 1,
	"playback": 3,
	"analytics": 1,
	"battery": 1,
	"windowname": 1,
	"nfc": 1,
};

var modify_builtin_levels = modify_builtin_levels || (() => null); // Give other scripts the possibility to modify builtin levels
modify_builtin_levels();

var levels = {};
var default_level = {};
var domains = {};
function init_levels() {
	levels = {
		[level_0.level_id]: level_0,
		[level_1.level_id]: level_1,
		[level_2.level_id]: level_2,
		[level_3.level_id]: level_3,
		[level_experimental.level_id]: level_experimental
	};
	default_level = Object.create(levels[L2]);
	default_level.level_text = "Default";
	domains = {};
}
init_levels();

let levels_initialised = false; // Initialized in updateLevels()
let fp_levels_initialised = false; // Initialized in fp_levels.js/loadFpdConfig()
let levels_updated_callbacks = [];
var tweak_domains = tweak_domains || {};
async function updateLevels(res) {
	init_levels();
	custom_levels = res["custom_levels"] || {};
	for (let key in custom_levels) {
		levels[key] = custom_levels[key];
	}
	for (let key in levels) {
		levels[key].wrappers = wrapping_groups.get_wrappers(levels[key]);
	}
	var new_default_level = res["__default__"];
	if (new_default_level === undefined || new_default_level === null || !(new_default_level in levels)) {
		default_level = Object.assign({}, levels[L2]);
		setDefaultLevel(L2);
	}
	else {
		default_level = Object.assign({}, levels[new_default_level]);
	}
	default_level.is_default = true;
	var redefined_domains = res["domains"] || {};
	for (let [d, settings] of Object.entries(tweak_domains)) {
		if ((default_level.level_id in settings.level_id) && !(d in redefined_domains)) {
			redefined_domains[d] = {level_id: default_level.level_id, tweaks: settings.tweaks};
		}
	}
	for (let [d, {level_id, tweaks, restore, restore_tweaks}] of Object.entries(redefined_domains)) {
		let level = levels[level_id];
		if (level === undefined) {
			domains[d] = default_level;
		}
		else {
			if (tweaks) {
				// this domain has "tweaked" wrapper groups from other levels, let's merge them
				level = Object.assign({}, level, tweaks);
				level.tweaks = tweaks;
				delete level.wrappers; // we will lazy instantiate them on demand in getCurrentLevelJSON()
			}
			if (restore) {
				level.restore = restore;
				if (restore_tweaks) {
					level.restore_tweaks = restore_tweaks;
				}
			}
		}
		domains[d] = level;
	}

	levels_initialised = true;
	if (fp_levels_initialised) { // Wait for both levels_initialised and fp_levels_initialised
		var orig_levels_updated_callbacks = levels_updated_callbacks;
		levels_updated_callbacks = [];
		orig_levels_updated_callbacks.forEach((it) => it());
		await updateUserScripts();
	}
}
browser.storage.sync.get(null).then(updateLevels);

var wrappersPortId = null;
var cachedSiteSettings = null;
async function updateUserScripts() {
	if (browser.tabs.executeScript || !("userScripts" in browser) || self.window) {
		return;
	}
	if (!cachedSiteSettings) {
		({wrappersPortId, cachedSiteSettings} = await
			browser.storage.local.get(["wrappersPortId", "cachedSiteSettings"]));
	}
	siteSettings = JSON.stringify({domains, fpdWhitelist, fpdOn: fpDetectionOn && fpdSettings.detection});
	if (siteSettings == cachedSiteSettings) {
		console.debug("userScripts: site settings not changed, nothing to update.");
		return;
	}
	cachedSiteSettings = siteSettings;
	const confCache = new Map();
	const allGlobs = new Set();
	const entries = Object.entries(domains);
	entries.push(["*", default_level]);
	let count = 0;
	for (const [domain, l] of entries) {
		if (l.tweaks && !l.wrappers) {
			l.wrappers = wrapping_groups.get_wrappers(l);
		}
		const fpdOn = fpDetectionOn && !isFpdWhitelisted(domain);
		const confKey = JSON.stringify([l.wrappers, fpdOn]);
		let conf = confCache.get(confKey);
		const glob = `*.${domain}`;
		if (!conf) {
			const fpdWrappers = fpdOn ? fp_levels.page_wrappers[fpdSettings.detection] : [];
			const injection = fp_assemble_injection(l, fpdWrappers);
			conf = {
				id: `L:${l.level_id}-FPD:${fpdOn}-C:${count++}`,
				injection,
				includeGlobs: [glob],
				excludeGlobs: [],
			};
			confCache.set(confKey, conf);
		} else {
			conf.includeGlobs.push(glob);
		}
		if (domain == '*') { // default level
			conf.excludeGlobs = [... allGlobs].filter(g => !conf.includeGlobs.includes(g));
			conf.includeGlobs = [];
			conf.matches = ["<all_urls>"];
			break;
		}
		allGlobs.add(glob);
		const imply = `.${domain}`;
		for (const otherConf of confCache.values()) {
			if (otherConf === conf) continue;
			const otherImplied = otherConf.includeGlobs.filter(g => g !== glob && g.endsWith(imply));
			for (const impliedGlob of otherImplied) {
				conf.excludeGlobs.push(impliedGlob);
			}
			const otherImplies = otherConf.includeGlobs.filter(g => glob.endsWith(g.replace('*', '')));
			for (const implyingGlob of otherImplies) {
				otherConf.excludeGlobs.push(implyingGlob);
			}
		}
	}

	const usTemplate = {
		runAt: "document_start",
		allFrames: true,
		world: "MAIN",
	};
	const globs2matches = globs => [... new Set(globs)].map(g => `*://${g}/*`);

	const scripts = [];
	for (const conf of confCache.values()) {
		const {portId, code} = patchWindow({
			portId: wrappersPortId,
			code: conf.injection,
		});
		if (wrappersPortId !== portId) {
			console.debug(`Switching wrappersPortId from ${wrappersPortId} to ${portId} on userScripts update.`);
			wrappersPortId = portId;
		}
		browser.storage.local.set({cachedSiteSettings, wrappersPortId});
		const {id, includeGlobs, excludeGlobs} = conf;
		const opts = {
			id,
			matches: conf.matches || globs2matches(includeGlobs),
			excludeMatches: globs2matches(excludeGlobs),
		};
		const debugCode = `console.info("Injecting wrapper for " + document.URL + ": "  + JSON.stringify(${JSON.stringify(opts)}) + ", ${portId}.");`;
		opts.js =  [{code: debugCode}, {code}];
		scripts.push(
			Object.assign(opts, usTemplate)
		);
	}

	await browser.userScripts.unregister();
	console.debug("Registering wrapping userScripts", confCache, scripts, domains);
	if (scripts.length) {
		await browser.userScripts.register(scripts);
	}
}

function changedLevels(changed, area) {
	if (area !== "sync") return;
	browser.storage.sync.get(null).then(updateLevels);
}
browser.storage.onChanged.addListener(changedLevels);

function setDefaultLevel(level) {
	browser.storage.sync.set({__default__: level});
}

function saveDomainLevels() {
	tobesaved = {};
	for (k in domains) {
		let {level_id, tweaks, restore, restore_tweaks} = domains[k];
		if (k[k.length - 1] === ".") {
			k = k.substring(0, k.length-1);
		}
		// Tweaks not contain the new value defined by the user. However, that might
		// be the value of the level. Remove such tweaks as they to not change the
		// level of protection
		if (tweaks) {
			for (let [group, param] of Object.entries(tweaks)) {
				if (param === (levels[level_id][group] || 0)) {
					delete tweaks[group]; // remove redundant entries
				}
			}
			if (Object.keys(tweaks).length === 0) {
				tweaks = undefined;
			}
			// Do not save built-in tweaks as they are automatically added in updateLevels
			if (k in tweak_domains) {
				// Skip further check if the user has a different default_level
				if (default_level.level_id in tweak_domains[k].level_id) {
					tdb_tweaks = Object.entries(tweak_domains[k].tweaks);
					current_tweaks = Object.entries(tweaks || {});
					if (tdb_tweaks.length === current_tweaks.length) {
						var equal = true;
						for ([name, val] of tdb_tweaks) {
							if (tweaks[name] !== val) {
								equal = false;
								break;
							}
						}
						if (equal) {
							// This entry should not be saved
							continue;
						}
					}
				}
			}
		}
		tobesaved[k] = tweaks ? {level_id, tweaks} : {level_id};
		if (restore) {
			tobesaved[k].restore = restore;
			if (restore_tweaks) {
				tobesaved[k].restore_tweaks = restore_tweaks;
			}
		}
	}
	browser.storage.sync.set({domains: tobesaved});
}

function getCurrentLevelJSON(url) {
	var subDomains = extractSubDomains(getEffectiveDomain(url));
	for (let domain of subDomains.reverse()) {
		if (domain in domains) {
			let l = domains[domain];
			if (l.tweaks && !l.wrappers) {
				l.wrappers = wrapping_groups.get_wrappers(l);
			}
			return l;
		}
	}
	return default_level;
}

function getTweaksForLevel(level_id, tweaks_obj) {
	tweaks_obj = tweaks_obj || {}; // Make sure that tweaks_obj is an object
	let working = Object.assign({}, wrapping_groups.empty_level, levels[level_id], tweaks_obj);
	Object.keys(working).forEach(function(key) {
		if (!wrapping_groups.group_names.includes(key)) {
			delete working[key];
		}
	});
	return working;
}
