import MagicString from 'magic-string';
import * as svelte from 'svelte/compiler';
import { escape_for_interpolation } from './escape.js';

/** @typedef {ReturnType<typeof import('svelte/compiler').parseCss>['children']} StyleSheetChildren */

/** @typedef {{ property: string; value: string; start: number; end: number; type: 'Declaration' }} Declaration */

const parse = svelte.parseCss
	? svelte.parseCss
	: /** @param {string} css */
		(css) => {
			return /** @type {{ css: { children: StyleSheetChildren } }} */ (
				svelte.parse(`<style>${css}</style>`)
			).css;
		};

const SKIP_PARSING_REGEX = /url\(/i;

/** Capture a single url(...) so we can process them one at a time */
const URL_FUNCTION_REGEX = /url\(\s*.*?\)/gi;

/** Captures the value inside a CSS url(...) */
const URL_PARAMETER_REGEX = /url\(\s*(['"]?)(.*?)\1\s*\)/i;

/** Splits the URL if there's a query string or hash fragment */
const HASH_OR_QUERY_REGEX = /[#?]/;

/**
 * Assets handled by Vite that are referenced in the stylesheet always start
 * with this prefix because Vite emits them into the same directory as the CSS file
 */
const VITE_ASSET_PREFIX = './';

const AST_OFFSET = '<style>'.length;

/**
 * We need to fix the asset URLs in the CSS before we inline them into a document
 * because they are now relative to the document instead of the CSS file.
 * @param {{
 * 	css: string;
 * 	vite_assets: Set<string>;
 * 	static_assets: Set<string>;
 * 	paths_assets: string;
 * 	base: string;
 * 	static_asset_prefix: string;
 * }} opts
 * @returns {string}
 */
export function fix_css_urls({
	css,
	vite_assets,
	static_assets,
	paths_assets,
	base,
	static_asset_prefix
}) {
	// skip parsing if there are no url(...) occurrences
	if (!SKIP_PARSING_REGEX.test(css)) {
		return css;
	}

	css = escape_for_interpolation(css);

	// safe guard in case of trailing slashes (but this should never happen)
	if (paths_assets.endsWith('/')) {
		paths_assets = paths_assets.slice(0, -1);
	}

	if (base.endsWith('/')) {
		base = base.slice(0, -1);
	}

	const s = new MagicString(css);

	const parsed = parse(css);

	for (const child of parsed.children) {
		find_declarations(child, (declaration) => {
			if (!SKIP_PARSING_REGEX.test) return;

			const cleaned = tippex_comments_and_strings(declaration.value);

			/** @type {string} */
			let new_value = declaration.value;

			/** @type {RegExpExecArray | null} */
			let url_function_found;
			URL_FUNCTION_REGEX.lastIndex = 0;
			while ((url_function_found = URL_FUNCTION_REGEX.exec(cleaned))) {
				const [url_function] = url_function_found;

				// After finding a legitimate url(...), we want to operate on the original
				// that may have a string inside it
				const original_url_function = declaration.value.slice(
					url_function_found.index,
					url_function_found.index + url_function.length
				);

				const url_parameter_found = URL_PARAMETER_REGEX.exec(original_url_function);
				if (!url_parameter_found) continue;

				const [, , url] = url_parameter_found;
				const [url_without_hash_or_query] = url.split(HASH_OR_QUERY_REGEX);

				/** @type {string | undefined} */
				let new_prefix;

				// Check if it's an asset processed by Vite...
				let current_prefix = url_without_hash_or_query.slice(0, VITE_ASSET_PREFIX.length);
				let filename = url_without_hash_or_query.slice(VITE_ASSET_PREFIX.length);
				const decoded = decodeURIComponent(filename);

				if (current_prefix === VITE_ASSET_PREFIX && vite_assets.has(decoded)) {
					new_prefix = paths_assets;
				} else {
					// ...or if it's from the static directory
					current_prefix = url_without_hash_or_query.slice(0, static_asset_prefix.length);
					filename = url_without_hash_or_query.slice(static_asset_prefix.length);
					const decoded = decodeURIComponent(filename);

					if (current_prefix === static_asset_prefix && static_assets.has(decoded)) {
						new_prefix = base;
					}
				}

				if (!new_prefix) continue;

				new_value = new_value.replace(`${current_prefix}${filename}`, `${new_prefix}/${filename}`);
			}

			if (declaration.value === new_value) return;

			if (!svelte.parseCss) {
				declaration.start = declaration.start - AST_OFFSET;
				declaration.end = declaration.end - AST_OFFSET;
			}

			s.update(declaration.start, declaration.end, `${declaration.property}: ${new_value}`);
		});
	}

	return s.toString();
}

/**
 * @param {StyleSheetChildren[0]} rule
 * @param {(declaration: Declaration) => void} callback
 */
function find_declarations(rule, callback) {
	// Vite already inlines relative @import rules, so we don't need to handle them here
	if (!rule.block) return;

	for (const child of rule.block.children) {
		if (child.type !== 'Declaration') {
			find_declarations(child, callback);
			continue;
		}
		callback(child);
	}
}

/**
 * Replaces comment and string contents with whitespace.
 * @param {string} value
 * @returns {string}
 */
export function tippex_comments_and_strings(value) {
	let new_value = '';
	let escaped = false;
	let in_comment = false;

	/** @type {null | '"' | "'"} */
	let quote_mark = null;

	let i = 0;
	while (i < value.length) {
		const char = value[i];

		if (in_comment) {
			if (char === '*' && value[i + 1] === '/') {
				in_comment = false;
				new_value += char;
			} else {
				new_value += ' ';
			}
		} else if (!quote_mark && char === '/' && value[i + 1] === '*') {
			in_comment = true;
			new_value += '/*';
			i++; // skip the '*' since we already added it
		} else if (escaped) {
			new_value += ' ';
			escaped = false;
		} else if (quote_mark && char === '\\') {
			escaped = true;
			new_value += ' ';
		} else if (char === quote_mark) {
			quote_mark = null;
			new_value += char;
		} else if (quote_mark) {
			new_value += ' ';
		} else if (quote_mark === null && (char === '"' || char === "'")) {
			quote_mark = char;
			new_value += char;
		} else {
			new_value += char;
		}

		i++;
	}

	return new_value;
}
