/// <reference lib="webworker" />

import '../patch_window.js';
import { sleep } from '$lib/utils.js';
import { rollup } from '@rollup/browser';
import { DEV } from 'esm-env';
import * as resolve from 'resolve.exports';
import commonjs from './plugins/commonjs.js';
import glsl from './plugins/glsl.js';
import json from './plugins/json.js';
import replace from './plugins/replace.js';
import loop_protect from './plugins/loop-protect.js';

/** @type {string} */
var pkg_name;

/** @type {string} */
let packages_url;

/** @type {string} */
let svelte_url;

/** @type {number} */
let current_id;

/** @type {(arg?: never) => void} */
let fulfil_ready;
const ready = new Promise((f) => {
	fulfil_ready = f;
});

/**
 * @type {{
 *   compile: typeof import('svelte/compiler').compile;
 *   compileModule: typeof import('svelte/compiler').compileModule;
 *   VERSION: string;
 * }}
 */
let svelte;

self.addEventListener(
	'message',
	/** @param {MessageEvent<import('../workers.js').BundleMessageData>} event */ async (event) => {
		switch (event.data.type) {
			case 'init': {
				({ packages_url, svelte_url } = event.data);

				const { version } = await fetch(`${svelte_url}/package.json`).then((r) => r.json());
				console.log(`Using Svelte compiler version ${version}`);

				const compiler = await fetch(`${svelte_url}/compiler/index.js`).then((r) => r.text());
				(0, eval)(compiler + '\n//# sourceURL=compiler/index.js@' + version);

				svelte = globalThis.svelte;

				fulfil_ready();
				break;
			}

			case 'bundle': {
				await ready;
				const { uid, files } = event.data;

				if (files.length === 0) return;

				current_id = uid;

				setTimeout(async () => {
					if (current_id !== uid) return;

					const result = await bundle({ uid, files });

					if (JSON.stringify(result.error) === JSON.stringify(ABORT)) return;
					if (result && uid === current_id) postMessage(result);
				});

				break;
			}
		}
	}
);

/** @type {Record<'client' | 'server', Map<string, { code: string, result: ReturnType<typeof svelte.compile> }>>} */
let cached = {
	client: new Map(),
	server: new Map()
};

const ABORT = { aborted: true };

/** @type {Map<string, Promise<{ url: string; body: string; }>>} */
const FETCH_CACHE = new Map();

/**
 * @param {string} url
 * @param {number} uid
 */
async function fetch_if_uncached(url, uid) {
	if (FETCH_CACHE.has(url)) {
		return FETCH_CACHE.get(url);
	}

	// TODO: investigate whether this is necessary
	await sleep(50);
	if (uid !== current_id) throw ABORT;

	const promise = fetch(url)
		.then(async (r) => {
			if (!r.ok) throw new Error(await r.text());

			return {
				url: r.url,
				body: await r.text()
			};
		})
		.catch((err) => {
			FETCH_CACHE.delete(url);
			throw err;
		});

	FETCH_CACHE.set(url, promise);
	return promise;
}

/**
 * @param {string} url
 * @param {number} uid
 */
async function follow_redirects(url, uid) {
	const res = await fetch_if_uncached(url, uid);
	return res?.url;
}

/**
 *
 * @param {number} major
 * @param {number} minor
 * @param {number} patch
 * @returns {number}
 */
function compare_to_version(major, minor, patch) {
	const v = svelte.VERSION.match(/^(\d+)\.(\d+)\.(\d+)/);

	// @ts-ignore
	return +v[1] - major || +v[2] - minor || +v[3] - patch;
}

function is_v4() {
	return compare_to_version(4, 0, 0) >= 0;
}

function is_v5() {
	return compare_to_version(5, 0, 0) >= 0;
}

function is_legacy_package_structure() {
	return compare_to_version(3, 4, 4) <= 0;
}

function has_loopGuardTimeout_feature() {
	return compare_to_version(3, 14, 0) >= 0;
}

/**
 *
 * @param {Record<string, unknown>} pkg
 * @param {string} subpath
 * @param {number} uid
 * @param {string} pkg_url_base
 */
async function resolve_from_pkg(pkg, subpath, uid, pkg_url_base) {
	// match legacy Rollup logic — pkg.svelte takes priority over pkg.exports
	if (typeof pkg.svelte === 'string' && subpath === '.') {
		return pkg.svelte;
	}

	// modern
	if (pkg.exports) {
		try {
			const [resolved] =
				resolve.exports(pkg, subpath, {
					browser: true,
					conditions: ['svelte', 'development']
				}) ?? [];

			return resolved;
		} catch {
			throw `no matched export path was found in "${pkg_name}/package.json"`;
		}
	}

	// legacy
	if (subpath === '.') {
		let resolved_id = resolve.legacy(pkg, {
			fields: ['browser', 'module', 'main']
		});

		if (typeof resolved_id === 'object' && !Array.isArray(resolved_id)) {
			const subpath = resolved_id['.'];
			if (subpath === false) return 'data:text/javascript,export {}';

			resolved_id =
				subpath ??
				resolve.legacy(pkg, {
					fields: ['module', 'main']
				});
		}

		if (!resolved_id) {
			// last ditch — try to match index.js/index.mjs
			for (const index_file of ['index.mjs', 'index.js']) {
				try {
					const indexUrl = new URL(index_file, `${pkg_url_base}/`).href;
					return (await follow_redirects(indexUrl, uid)) ?? '';
				} catch {
					// maybe the next option will be successful
				}
			}

			throw `could not find entry point in "${pkg_name}/package.json"`;
		}

		return resolved_id;
	}

	if (typeof pkg.browser === 'object') {
		// this will either return `pkg.browser[subpath]` or `subpath`
		return resolve.legacy(pkg, {
			browser: subpath
		});
	}

	return subpath;
}

/**
 * @param {number} uid
 * @param {'client' | 'server'} mode
 * @param {typeof cached['client']} cache
 * @param {Map<string, import('$lib/types.js').File>} local_files_lookup
 */
async function get_bundle(uid, mode, cache, local_files_lookup) {
	let bundle;

	/** A set of package names (without subpaths) to include in pkg.devDependencies when downloading an app */
	/** @type {Set<string>} */
	const imports = new Set();

	/** @type {import('$lib/types.js').Warning[]} */
	const warnings = [];

	/** @type {{ message: string }[]} */
	const all_warnings = [];

	/** @type {typeof cache} */
	const new_cache = new Map();

	/** @type {import('@rollup/browser').Plugin} */
	const repl_plugin = {
		name: 'svelte-repl',
		async resolveId(importee, importer) {
			if (uid !== current_id) throw ABORT;

			if (importee === 'esm-env') return importee;

			const v5 = is_v5();
			const v4 = !v5 && is_v4();

			if (!v5) {
				// importing from Svelte
				if (importee === `svelte`)
					return v4 ? `${svelte_url}/src/runtime/index.js` : `${svelte_url}/index.mjs`;

				if (importee.startsWith(`svelte/`)) {
					const sub_path = importee.slice(7);
					if (v4) {
						return `${svelte_url}/src/runtime/${sub_path}/index.js`;
					}

					return is_legacy_package_structure()
						? `${svelte_url}/${sub_path}.mjs`
						: `${svelte_url}/${sub_path}/index.mjs`;
				}
			}

			// importing from another file in REPL
			if (local_files_lookup.has(importee) && (!importer || local_files_lookup.has(importer)))
				return importee;
			if (local_files_lookup.has(importee + '.js')) return importee + '.js';
			if (local_files_lookup.has(importee + '.json')) return importee + '.json';

			// remove trailing slash
			if (importee.endsWith('/')) importee = importee.slice(0, -1);

			// importing from a URL
			if (/^https?:/.test(importee)) return importee;

			if (importee.startsWith('.')) {
				if (importer && local_files_lookup.has(importer)) {
					// relative import in a REPL file
					// should've matched above otherwise importee doesn't exist
					console.error(`Cannot find file "${importee}" imported by "${importer}" in the REPL`);
					return;
				} else {
					// relative import in an external file
					const url = new URL(importee, importer).href;
					self.postMessage({ type: 'status', uid, message: `resolving ${url}` });

					return await follow_redirects(url, uid);
				}
			} else {
				// fetch from unpkg
				self.postMessage({ type: 'status', uid, message: `resolving ${importee}` });

				const match = /^((?:@[^/]+\/)?[^/]+)(\/.+)?$/.exec(importee);
				if (!match) {
					return console.error(`Invalid import "${importee}"`);
				}

				const pkg_name = match[1];
				const subpath = `.${match[2] ?? ''}`;

				// if this was imported by one of our files, add it to the `imports` set
				if (importer && local_files_lookup.has(importer)) {
					imports.add(pkg_name);
				}

				const fetch_package_info = async () => {
					try {
						const pkg_url = await follow_redirects(
							`${pkg_name === 'svelte' ? '' : packages_url}/${pkg_name}/package.json`,
							uid
						);

						if (!pkg_url) throw new Error();

						const pkg_json = (await fetch_if_uncached(pkg_url, uid))?.body;
						const pkg = JSON.parse(pkg_json ?? '""');

						const pkg_url_base = pkg_url.replace(/\/package\.json$/, '');

						return {
							pkg,
							pkg_url_base
						};
					} catch (_e) {
						throw new Error(`Error fetching "${pkg_name}" from unpkg. Does the package exist?`);
					}
				};

				const { pkg, pkg_url_base } = await fetch_package_info();

				try {
					const resolved_id = await resolve_from_pkg(pkg, subpath, uid, pkg_url_base);
					return new URL(resolved_id + '', `${pkg_url_base}/`).href;
				} catch (reason) {
					throw new Error(`Cannot import "${importee}": ${reason}.`);
				}
			}
		},
		async load(resolved) {
			if (uid !== current_id) throw ABORT;

			if (resolved === 'esm-env') {
				return `export const BROWSER = true; export const DEV = true`;
			}

			const cached_file = local_files_lookup.get(resolved);
			if (cached_file) return cached_file.source;

			if (!FETCH_CACHE.has(resolved)) {
				self.postMessage({ type: 'status', uid, message: `fetching ${resolved}` });
			}

			const res = await fetch_if_uncached(resolved, uid);
			return res?.body;
		},
		transform(code, id) {
			if (uid !== current_id) throw ABORT;

			self.postMessage({ type: 'status', uid, message: `bundling ${id}` });

			if (!/\.(svelte|js)$/.test(id)) return null;

			const name = id.split('/').pop()?.split('.')[0];

			const cached_id = cache.get(id);
			let result;

			if (cached_id && cached_id.code === code) {
				result = cached_id.result;
			} else if (id.endsWith('.svelte')) {
				result = svelte.compile(code, {
					filename: name + '.svelte',
					generate: 'client',
					dev: true
				});

				if (result.css) {
					result.js.code +=
						'\n\n' +
						`
					const $$__style = document.createElement('style');
					$$__style.textContent = ${JSON.stringify(result.css.code)};
					document.head.append($$__style);
				`.replace(/\t/g, '');
				}
			} else if (id.endsWith('.svelte.js')) {
				result = svelte.compileModule(code, {
					filename: name + '.js',
					generate: 'client',
					dev: true
				});
				if (!result) {
					return null;
				}
			} else {
				return null;
			}

			new_cache.set(id, { code, result });

			// @ts-expect-error
			(result.warnings || result.stats?.warnings)?.forEach((warning) => {
				// This is required, otherwise postMessage won't work
				// @ts-ignore
				delete warning.toString;
				// TODO remove stats post-launch
				// @ts-ignore
				warnings.push(warning);
			});

			/** @type {import('@rollup/browser').TransformResult} */
			const transform_result = {
				code: result.js.code,
				map: result.js.map
			};

			return transform_result;
		}
	};

	try {
		bundle = await rollup({
			input: './__entry.js',
			plugins: [
				repl_plugin,
				commonjs,
				json,
				glsl,
				loop_protect,
				replace({
					'process.env.NODE_ENV': JSON.stringify('production')
				})
			],
			inlineDynamicImports: true,
			onwarn(warning) {
				all_warnings.push({
					message: warning.message
				});
			}
		});

		return {
			bundle,
			imports: Array.from(imports),
			cache: new_cache,
			error: null,
			warnings,
			all_warnings
		};
	} catch (error) {
		return { error, imports: null, bundle: null, cache: new_cache, warnings, all_warnings };
	}
}

/**
 * @param {{ uid: number; files: import('$lib/types.js').File[] }} param0
 * @returns
 */
async function bundle({ uid, files }) {
	if (!DEV) {
		console.clear();
		console.log(`running Svelte compiler version %c${svelte.VERSION}`, 'font-weight: bold');
	}

	/** @type {Map<string, import('$lib/types').File>} */
	const lookup = new Map();

	lookup.set('./__entry.js', {
		name: '__entry',
		source: `
			export { mount, unmount, untrack } from 'svelte';
			export {default as App} from './App.svelte';
		`,
		type: 'js',
		modified: false
	});

	files.forEach((file) => {
		const path = `./${file.name}.${file.type}`;
		lookup.set(path, file);
	});

	/** @type {Awaited<ReturnType<typeof get_bundle>>} */
	let client = await get_bundle(uid, 'client', cached.client, lookup);
	let error;

	try {
		if (client.error) {
			throw client.error;
		}

		cached.client = client.cache;

		const client_result = (
			await client.bundle?.generate({
				format: 'iife',
				exports: 'named'
				// sourcemap: 'inline'
			})
		)?.output[0];

		const server = false // TODO how can we do SSR?
			? await get_bundle(uid, 'server', cached.server, lookup)
			: null;

		if (server) {
			cached.server = server.cache;
			if (server.error) {
				throw server.error;
			}
		}

		const server_result = server
			? (
					await server.bundle?.generate({
						format: 'iife',
						name: 'SvelteComponent',
						exports: 'named'
						// sourcemap: 'inline'
					})
				)?.output?.[0]
			: null;

		return {
			uid,
			client: client_result,
			server: server_result,
			imports: client.imports,
			warnings: client.warnings,
			error: null
		};
	} catch (err) {
		console.error(err);

		/** @type {Error} */
		// @ts-ignore
		const e = error || err;

		// @ts-ignore
		delete e.toString;

		return {
			uid,
			client: null,
			server: null,
			imports: null,
			warnings: client.warnings,
			error: Object.assign({}, e, {
				message: e.message,
				stack: e.stack
			})
		};
	}
}
