/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
(function(){
	"use strict";
	
	let scope;
	if ((typeof exports) !== "undefined"){
		scope = exports;
	}
	else {
		scope = require.register("./extension", {});
	}
	
	const browserAvailable = typeof browser !== "undefined";
	const logging = require("./logging");
	
	scope.inBackgroundScript = !!(
		browserAvailable &&
		browser.extension.getBackgroundPage &&
		browser.extension.getBackgroundPage() === window
	);
	
	scope.getTranslation = browserAvailable? function getTranslation(id){
		return browser.i18n.getMessage(id);
	}: function(id){
		return id;
	};
	
	scope.parseTranslation = function parseTranslation(message, parameters = {}){
		const container = document.createDocumentFragment();
		
		message.split(/(\{[^}]+\})/).forEach(function(part){
			if (part.startsWith("{") && part.endsWith("}")){
				part = part.substring(1, part.length - 1);
				const args = part.split(":");
				switch (args[0]){
					case "image": {
						const image = document.createElement("img");
						image.className = "noticeImage";
						image.src = args.slice(1).join(":");
						container.appendChild(image);
						break;
					}
					case "link": {
						const link = document.createElement("a");
						link.target = "_blank";
						link.textContent = args[1];
						link.href = args.slice(2).join(":");
						container.appendChild(link);
						break;
					}
					default:
						if (parameters[args[0]]){
							const parameter = parameters[args[0]];
							if ((typeof parameter) === "function"){
								container.appendChild(parameter(args.slice(1).join(":")));
							}
							else {
								container.appendChild(document.createTextNode(parameter));
							}
						}
						else {
							container.appendChild(document.createTextNode(part));
						}
				}
			}
			else {
				container.appendChild(document.createTextNode(part));
			}
		});
		return container;
	};
	
	scope.extensionID = browserAvailable? browser.extension.getURL(""): "extensionID";
	
	scope.inIncognitoContext = browserAvailable? browser.extension.inIncognitoContext: false;
	
	scope.message = {
		on: browserAvailable? function(callback){
			return browser.runtime.onMessage.addListener(callback);
		}: function(){
			return false;
		},
		send: browserAvailable? function(data){
			return browser.runtime.sendMessage(data);
		}: function(){
			return false;
		}
	};
	Object.seal(scope.message);
	
	scope.getWrapped = function getWrapped(obj){
		return obj && (obj.wrappedJSObject || obj);
	};
	
	scope.exportFunctionWithName = function exportFunctionWithName(func, context, name){
		const targetObject = scope.getWrapped(context).Object.create(null);
		const exportedTry = exportFunction(func, targetObject, {allowCrossOriginArguments: true, defineAs: name});
		if (exportedTry.name === name){
			return exportedTry;
		}
		else {
			if (func.name === name){
				logging.message(
					"FireFox bug: Need to change name in exportFunction from",
					exportedTry.name,
					"(originally correct) to",
					name
				);
			}
			else {
				logging.error("Wrong name specified for", name, new Error());
			}
			const wrappedContext = scope.getWrapped(context);
			const options = {
				allowCrossOriginArguments: true,
				defineAs: name
			};
			const oldDescriptor = Object.getOwnPropertyDescriptor(wrappedContext, name);
			if (oldDescriptor && !oldDescriptor.configurable){
				logging.error(
					"Unable to export function with the correct name", name,
					"instead we have to use", exportedTry.name
				);
				return exportedTry;
			}
			const exported = exportFunction(func, context, options);
			if (oldDescriptor){
				Object.defineProperty(wrappedContext, name, oldDescriptor);
			}
			else {
				delete wrappedContext[name];
			}
			return exported;
		}
	};
	
	const proxies = new Map();
	const changedToStrings = new WeakMap();
	scope.createProxyFunction = function createProxyFunction(window, original, replacement){
		if (!changedToStrings.get(window)){
			changedToStrings.set(window, true);
			const functionPrototype = scope.getWrapped(window).Function.prototype;
			const toString = functionPrototype.toString;
			scope.changeProperty(window, "toString", {
				object: functionPrototype,
				name: "toString",
				type: "value",
				changed: scope.createProxyFunction(
					window,
					toString,
					function(){
						return proxies.get(this) || toString.call(this);
					}
				)
			});
		}
		const handler = scope.getWrapped(window).Object.create(null);
		handler.apply = scope.exportFunctionWithName(function(_target, thisArgs, args){
			try {
				return args.length?
					replacement.call(thisArgs, ...args):
					replacement.call(thisArgs);
			}
			catch (error){
				return original.apply(thisArgs, args);
			}
		}, window, "");
		const proxy = new window.Proxy(original, handler);
		proxies.set(proxy, original.toString());
		return scope.getWrapped(proxy);
	};
	
	const changedPropertiesByWindow = new WeakMap();
	scope.changeProperty = function(window, group, {object, name, type, changed}){
		let changedProperties = changedPropertiesByWindow.get(scope.getWrapped(window));
		if (!changedProperties){
			changedProperties = [];
			changedPropertiesByWindow.set(scope.getWrapped(window), changedProperties);
		}
		const descriptor = Object.getOwnPropertyDescriptor(object, name);
		const original = descriptor[type];
		descriptor[type] = changed;
		Object.defineProperty(object, name, descriptor);
		changedProperties.push({group, object, name, type, original});
	};
	scope.revertProperties = function(window, group){
		window = scope.getWrapped(window);
		let changedProperties = changedPropertiesByWindow.get(window);
		if (!changedProperties){
			return;
		}
		if (group){
			const remainingProperties = changedProperties.filter(function(changedProperty){
				return changedProperty.group !== group;
			});
			changedPropertiesByWindow.set(window, remainingProperties);
			changedProperties = changedProperties.filter(function(changedProperty){
				return changedProperty.group === group;
			});
		}
		else {
			changedPropertiesByWindow.delete(window);
		}
		
		for (let i = changedProperties.length - 1; i >= 0; i -= 1){
			const {object, name, type, original} = changedProperties[i];
			logging.verbose("reverting", name, "on", object);
			const descriptor = Object.getOwnPropertyDescriptor(object, name);
			descriptor[type] = original;
			Object.defineProperty(object, name, descriptor);
		}
	};
	
	scope.waitSync = function waitSync(reason = "for no reason"){
		logging.message(`Starting synchronous request ${reason}.`);
		try {
			let xhr = new XMLHttpRequest();
			xhr.open("GET", "https://[::]", false);
			xhr.send();
			xhr = null;
		}
		catch (error){
			logging.verbose("Error in XHR:", error);
		}
	};
	
	Object.seal(scope);
}());