/* eslint-disable no-console */

// This is a type-only import that gives access to ServiceWorker types.
// For this to work with Webpack, an import alias for 'serviceworker' may
// also be present in webpack.config.js.
import 'serviceworker';

// From https://github.com/gzuidhof/coi-serviceworker. This script enables the necessary
// headers on GitHub pages to allow the use of SQLite. It has been modified and refactored
// to add support for using the app while offline.
//
// eslint-disable-next-line multiline-comment-style -- Preserve license
/* !
 * @license
 *
 * Based on coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT
 *
 * MIT License
 *
 * Copyright (c) 2021 Guido Zuidhof
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

let coepCredentialless = false;
if (typeof window === 'undefined') {
	self.addEventListener('install', () => self.skipWaiting());
	self.addEventListener('activate', (event: ExtendableEvent) => event.waitUntil(self.clients.claim()));

	const serviceWorkerPath = new URL(self.location.href ?? '').pathname;
	const mainPageBasePath = serviceWorkerPath.replace(/\/[^/]+$/, '/');
	const waitingForClientPath = `${mainPageBasePath}just-one-client.html`;
	const mainPagePaths = [mainPageBasePath, `${mainPageBasePath}index.html`];

	const isJoplinWebClientPage = (client: WindowClient) => {
		const clientUrl = new URL(client.url);
		return mainPagePaths.includes(clientUrl.pathname);
	};

	self.addEventListener('message', async (ev) => {
		if (!ev.data) {
			return;
		} else if (ev.data.type === 'deregister') {
			await self.registration.unregister();

			const clients = await self.clients.matchAll();
			for (const client of clients) {
				if (client instanceof WindowClient) {
					void client.navigate(client.url);
				}
			}
		} else if (ev.data.type === 'coepCredentialless') {
			coepCredentialless = ev.data.value;
		} else if (ev.data.type === 'closeAllJoplinWebTabs') {
			for (const client of await self.clients.matchAll()) {
				if (client instanceof WindowClient && isJoplinWebClientPage(client)) {
					void client.navigate(`${mainPageBasePath}closed.html`);
				}
			}
		}
	});

	self.addEventListener('fetch', (event: FetchEvent) => {
		const originalRequest = event.request;
		const needsExtraHeaders = originalRequest.cache !== 'only-if-cached' || originalRequest.mode === 'same-origin';

		const request = (coepCredentialless && originalRequest.mode === 'no-cors' && !needsExtraHeaders)
			? new Request(originalRequest, {
				credentials: 'omit',
			})
			: originalRequest;

		// Joplin modification: Redirect users to prevent multiple clients from being open at the same time.
		const handleRedirects = async (event: FetchEvent) => {
			const targetUrl = new URL(request.url);

			const redirectable = [...mainPagePaths, waitingForClientPath].includes(targetUrl.pathname) && self.location.origin === targetUrl.origin;
			if (redirectable) {
				const allClients = await self.clients.matchAll({ includeUncontrolled: true });

				let hasLockingClient = false;
				for (const client of allClients) {
					if (!(client instanceof WindowClient)) continue;

					const clientUrl = new URL(client.url);
					if (mainPagePaths.includes(clientUrl.pathname) && event.resultingClientId !== client.id && !client.focused) {
						hasLockingClient = true;
					}
				}

				let redirectUrl = null;
				if (targetUrl.pathname === waitingForClientPath && !hasLockingClient) {
					redirectUrl = `${self.location.origin}${mainPageBasePath}`;
				} else if (targetUrl.pathname !== waitingForClientPath && hasLockingClient) {
					redirectUrl = `${self.location.origin}${waitingForClientPath}`;
				}

				if (redirectUrl) {
					return new Response(`Redirecting to ${redirectUrl}...`, { status: 302, headers: new Headers({ 'Location': redirectUrl }) }); // 302 = Found
				}
			}
			return null;
		};

		const withExtraResponseHeaders = (response: Response) => {
			if (response.status !== 0 && needsExtraHeaders) {
				const newHeaders = new Headers(response.headers);
				newHeaders.set('Cross-Origin-Embedder-Policy',
					coepCredentialless ? 'credentialless' : 'require-corp',
				);
				if (!coepCredentialless) {
					newHeaders.set('Cross-Origin-Resource-Policy', 'cross-origin');
				}
				newHeaders.set('Cross-Origin-Opener-Policy', 'same-origin');

				response = new Response(response.body, {
					status: response.status,
					statusText: response.statusText,
					headers: newHeaders,
				});
			}
			return response;
		};

		const cacheResponse = (cache: Cache, response: Response, requestUrl: URL) => {
			try {
				if (
					request.method === 'GET' &&
					response.ok &&
					requestUrl.origin === self.location.origin &&
					(
						requestUrl.pathname.match(/\.(js|css|wasm|json|ttf|html|png)$/) ||
						// Also cache HTML responses (e.g. for index.html, when requested with a directory
						// URL).
						(response.headers?.get('Content-Type') ?? '').startsWith('text/html')
					)
				) {
					console.log('Service worker: cached', event.request.url);
					void cache.put(request, response.clone());
				}
			} catch (error) {
				console.warn('Failed to save ', event.request?.url, 'to the cache. Error: ', error);
			}
		};

		event.respondWith((async () => {
			const redirectResponse = await handleRedirects(event);
			if (redirectResponse) {
				return redirectResponse;
			}

			const requestUrl = new URL(event.request.url);
			const cache = await caches.open('v1');
			try {
				const response = withExtraResponseHeaders(await fetch(request));

				// Joplin modification: Store the response in the cache to support offline mode
				cacheResponse(cache, response, requestUrl);

				if (requestUrl.origin === self.location.origin && !response.ok) {
					console.warn('Response to request for a main page path', requestUrl, 'was not OK. Responding from the cache.');

					const cachedResponse = await cache.match(request);
					if (cachedResponse) {
						return cachedResponse;
					}
				}

				return response;
			} catch (error) {
				console.error('ERROR requesting', event.request.url, ':', error);
				// Joplin modification: Restore from the cache to support offline mode.
				return cache.match(request);
			}
		})());
	});

} else {
	void (async () => {
		const reloadedBySelf = window.sessionStorage.getItem('coiReloadedBySelf');
		window.sessionStorage.removeItem('coiReloadedBySelf');
		const coepDegrading = reloadedBySelf === 'coepDegrade';

		// You can customize the behavior of this script through a global `coi` variable.
		const coi = {
			shouldRegister: () => !reloadedBySelf,
			shouldDeregister: () => false,
			coepCredentialless: () => true,
			coepDegrade: () => true,
			doReload: () => window.location.reload(),
			quiet: false,
		};

		const n = navigator;
		const controlling = n.serviceWorker && n.serviceWorker.controller;

		// Record the failure if the page is served by serviceWorker.
		if (controlling && !window.crossOriginIsolated) {
			window.sessionStorage.setItem('coiCoepHasFailed', 'true');
		}
		const coepHasFailed = window.sessionStorage.getItem('coiCoepHasFailed');

		if (controlling) {
			// Reload only on the first failure.
			const reloadToDegrade = coi.coepDegrade() && !(
				coepDegrading || window.crossOriginIsolated
			);
			n.serviceWorker.controller.postMessage({
				type: 'coepCredentialless',
				value: (reloadToDegrade || coepHasFailed && coi.coepDegrade())
					? false
					: coi.coepCredentialless(),
			});
			if (reloadToDegrade) {
				!coi.quiet && console.log('Reloading page to degrade COEP.');
				window.sessionStorage.setItem('coiReloadedBySelf', 'coepDegrade');
				coi.doReload();
			}

			if (coi.shouldDeregister()) {
				n.serviceWorker.controller.postMessage({ type: 'deregister' });
			}
		}

		// If we're already coi: do nothing. Perhaps it's due to this script doing its job, or COOP/COEP are
		// already set from the origin server. Also if the browser has no notion of crossOriginIsolated, just give up here.
		//
		// Joplin modification: Always register the service worker.
		// if (window.crossOriginIsolated !== false || !coi.shouldRegister()) return;

		if (!window.isSecureContext) {
			!coi.quiet && console.log('COOP/COEP Service Worker not registered, a secure context is required.');
			return;
		}

		// In some environments (e.g. Firefox private mode) this won't be available
		if (!n.serviceWorker) {
			!coi.quiet && console.error('COOP/COEP Service Worker not registered, perhaps due to private mode.');
			return;
		}

		const registration = await n.serviceWorker.register(window.document.currentScript.getAttribute('src'));
		!coi.quiet && console.log('COOP/COEP Service Worker registered', registration.scope);

		registration.addEventListener('updatefound', () => {
			!coi.quiet && console.log('Reloading page to make use of updated COOP/COEP Service Worker.');
			window.sessionStorage.setItem('coiReloadedBySelf', 'updatefound');
			coi.doReload();
		});

		// If the registration is active, but it's not controlling the page
		if (registration.active && !n.serviceWorker.controller) {
			!coi.quiet && console.log('Reloading page to make use of COOP/COEP Service Worker.');
			window.sessionStorage.setItem('coiReloadedBySelf', 'notControlling');
			coi.doReload();
		}
	})();
}
