(function(globalObject) {
	// TODO: Not sure if that will work once packaged in Electron
	const sandboxProxy = require('../../vendor/lib/@joplin/lib/services/plugins/sandboxProxy.js');
	const ipcRenderer = require('electron').ipcRenderer;

	const ipcRendererSend = (message, args) => {
		try {
			return ipcRenderer.send(message, args);
		} catch (error) {
			console.error('Could not send IPC message:', message, ': ', args, error);
			throw error;
		}
	};

	const urlParams = new URLSearchParams(window.location.search);
	const pluginId = urlParams.get('pluginId');
	const libraryData = JSON.parse(decodeURIComponent(urlParams.get('libraryData')));

	let eventId_ = 1;
	const eventHandlers_ = {};

	function mapEventHandlersToIds(argName, arg) {
		if (Array.isArray(arg)) {
			for (let i = 0; i < arg.length; i++) {
				arg[i] = mapEventHandlersToIds(`${i}`, arg[i]);
			}
			return arg;
		} else if (typeof arg === 'function') {
			const id = `___plugin_event_${argName}_${eventId_}`;
			eventId_++;
			eventHandlers_[id] = arg;
			return id;
		} else if (arg === null) {
			return null;
		} else if (arg === undefined) {
			return undefined;
		} else if (typeof arg === 'object') {
			for (const n in arg) {
				arg[n] = mapEventHandlersToIds(n, arg[n]);
			}
		}

		return arg;
	}

	const callbackPromises = {};
	let callbackIndex = 1;

	const target = (path, args) => {
		if (path === 'require' || path === 'plugins.require') { // plugins.require is deprecated
			const modulePath = args && args.length ? args[0] : null;
			if (!modulePath) throw new Error('No module path specified on `require` call');

			if (modulePath === 'sqlite3') {
				return require('sqlite3');
			}

			if (modulePath === 'fs-extra') {
				return require('fs-extra');
			}

			// 7zip-bin is required by one of the default plugins (simple-backup)
			if (modulePath === '7zip-bin') {
				return { path7za: libraryData.pathTo7za };
			}

			throw new Error(`Module not found: ${modulePath}`);
		}

		const callbackId = `cb_${pluginId}_${Date.now()}_${callbackIndex++}`;
		const promise = new Promise((resolve, reject) => {
			callbackPromises[callbackId] = { resolve, reject };
		});

		ipcRendererSend('pluginMessage', {
			target: 'mainWindow',
			pluginId: pluginId,
			callbackId: callbackId,
			path: path,
			args: mapEventHandlersToIds(null, args),
		});

		return promise;
	};

	ipcRenderer.on('pluginMessage', async (_event, message) => {
		if (message.eventId) {
			const eventHandler = eventHandlers_[message.eventId];

			if (!eventHandler) {
				console.error('Got an event ID but no matching event handler: ', message);
				return;
			}

			let result = null;
			let error = null;
			try {
				result = await eventHandler(...message.args);
			} catch (e) {
				error = e;
			}

			if (message.callbackId) {
				ipcRendererSend('pluginMessage', {
					target: 'mainWindow',
					pluginId: pluginId,
					mainWindowCallbackId: message.callbackId,
					result: result,
					error: error,
				});
			}
			return;
		}

		if (message.pluginCallbackId) {
			const callbackId = message.pluginCallbackId;
			const promise = callbackPromises[callbackId];
			if (!promise) {
				console.error('Got a callback without matching promise: ', message);
				return;
			}

			delete callbackPromises[callbackId];

			if (message.error) {
				promise.reject(message.error);
			} else {
				promise.resolve(message.result);
			}
			return;
		}

		console.warn('Unhandled plugin message:', message);
	});

	// Track whether the plugin has called joplin.plugins.register().
	// If the plugin script throws before register() is called, the 'started'
	// event will never fire, which blocks other plugins from working.
	// See: https://github.com/laurent22/joplin/issues/12793
	let registerCalled = false;

	const originalTarget = target;
	const wrappedTarget = (path, args) => {
		if (path === 'plugins.register') {
			registerCalled = true;
		}
		return originalTarget(path, args);
	};

	// Catch unhandled errors from the plugin script. If the plugin throws
	// before calling register(), notify the host so it can unblock startup.
	window.onerror = (message) => {
		if (!registerCalled) {
			console.error(`Plugin "${pluginId}" threw an error before registering:`, message);
			ipcRendererSend('pluginMessage', {
				target: 'mainWindow',
				pluginId: pluginId,
				path: '__pluginFailedToStart__',
				args: [String(message)],
			});
		}
	};

	const pluginScriptPath = urlParams.get('pluginScript');
	const script = document.createElement('script');
	script.src = pluginScriptPath;
	document.head.appendChild(script);

	globalObject.joplin = sandboxProxy(wrappedTarget);
})(window);
