---
export interface Props {
	production?: boolean;
	expanded?: boolean;
}
const TAG = 'vtbot-linter'; // see also start of script
const { production = false, expanded = false } = Astro.props;
const active = import.meta.env.DEV || production;
---

{active && <meta name={TAG} content={expanded ? 'expanded' : 'collapsed'} />}

<script>
	import {
		TRANSITION_AFTER_SWAP,
		TRANSITION_BEFORE_SWAP,
		supportsViewTransitions,
		type TransitionBeforeSwapEvent,
	} from 'astro:transitions/client';
	import { astroContextIds, elementsWithStyleProperty, ILLEGAL_TRANSITION_NAMES } from './css';
	import { deriveCSSSelector } from './derive-css-selector';

	const TAG = 'vtbot-linter'; // see also frontmatter

	const tag = () => document.querySelector(`meta[name="${TAG}"]`);
	const enabled = () => !!tag();
	const expanded = () => tag()?.getAttribute('content') === 'expanded';
	let group: typeof console.group;

	const ERROR =
		'background-color: #880000; color: #ffffff; padding: 2px 4px; border-radius: 4px; font-weight: bold;';
	const WARNING =
		'background-color: #887700; color: #ffffff; padding: 2px 4px; border-radius: 4px;';
	const IMPORTANT = 'background-color:#eee; color: #333; font-weight: bold; padding: 2px 4px;';
	const INFO = 'color:#888;';

	function flagNested(event: TransitionBeforeSwapEvent, property: string, origin?: 'old' | 'new') {
		const nameMap = new Map<string, Element>();

		const here = `in ${origin} DOM (${event[origin === 'old' ? 'from' : 'to'].pathname})`;

		if (!origin) {
			flagNested(event, property, 'old');
			flagNested(event, property, 'new');
			return;
		}
		const selector = `[${property}]`;
		const elements = (origin === 'old' ? document : event.newDocument).querySelectorAll(selector);

		elements.forEach((element) => {
			const name = element.getAttribute(property);
			if (!name) {
				group(`%c[vtbot-linter] no value given for ${selector} ${here}`, ERROR);
				console.log(`%o at %c${deriveCSSSelector(element)}`, element, IMPORTANT);
				console.groupEnd();
			} else {
				if (name.match(/astro-........-[0-9]+/)) {
					group(
						`%c[vtbot-linter] looks like an implicit name: ${property}=${name} ${here}`,
						WARNING
					);
					console.log(`%o at %c${deriveCSSSelector(element)}`, element, IMPORTANT);
					console.groupEnd();
				}
				const earlier = nameMap.get(name);
				if (earlier) {
					group(`%c[vtbot-linter] duplicate ${property}=${name} ${here}`, ERROR);
					console.log(
						`%o at %c${deriveCSSSelector(element)}%c and earlier %o at %c${deriveCSSSelector(
							earlier
						)}`,
						element,
						IMPORTANT,
						'',
						earlier,
						IMPORTANT
					);
					console.groupEnd();
				} else {
					nameMap.set(name, element);
				}
			}

			const violation = element.parentElement?.closest(selector);

			if (violation) {
				group(
					`%c[vtbot-linter] nested HTML elements with ${selector} in ${origin} DOM (${
						event[origin === 'old' ? 'from' : 'to'].pathname
					})`,
					ERROR
				);
				console.log(
					`%o at %c${deriveCSSSelector(element)}%c is nested inside %o at %c${deriveCSSSelector(
						violation
					)}`,
					element,
					IMPORTANT,
					'',
					violation,
					IMPORTANT
				);
				console.groupEnd();
			}
		});
	}

	const emptyElementSet = new Set<Element>();

	function flagNonUniqueOrMissingTransitionNames(
		event: TransitionBeforeSwapEvent,
		origin: 'old' | 'new'
	) {
		const here = `in ${origin} DOM (${event[origin === 'old' ? 'from' : 'to'].pathname})`;

		const namedElements = elementsWithStyleProperty(document, 'view-transition-name');
		const warned = new Set<string>();
		const ignore = new Set(
			(
				(document.querySelector('meta[name=vtbot-linter-ignore]') as HTMLMetaElement)?.content ?? ''
			).split(' ')
		);

		[...namedElements.keys()].forEach((name) => {
			if (name !== '') {
				const elements = namedElements.get(name)!;
				if (elements.size === 0 && !ignore.has(name)) {
					warned.add(name);
					group(
						`%c[vtbot-linter] no HTMLElement with view transition name "${name}" exists ${here}.`,
						WARNING
					);
					console.log(
						`This means either that a transition name has been defined but not used, e.g. by setting transition:name on an Astro component instead of an HTML element; or the HTML element that used the transition name has been moved to another DOM in the meantime.`
					);
					console.groupEnd();
				}
				if (elements.size > 1 && name !== 'none' && !ignore.has(name)) {
					warned.add(name);
					group(`%c[vtbot-linter] view transition name "${name}" is not unique ${here}`, ERROR);
					[...elements!.values()]?.forEach((element) => {
						console.log(`%o at %c${deriveCSSSelector(element)}`, element, IMPORTANT);
					});
					console.groupEnd();
				}
			}
		});

		// Most illegal view-transition-names were mapped to '' by the browser.
		// There are some additional keywords to checks for
		const errors = new Set(
			['', 'initial', 'unset', 'inherit'].flatMap((n) => {
				const elements = [...(namedElements.get(n)?.values() ?? emptyElementSet)];
				elements.forEach((element) => {
					n && !warned.has(n) && element.setAttribute(ILLEGAL_TRANSITION_NAMES, n);
				});
				return elements;
			})
		);

		// prune errors, delete what we had warned before
		[...errors.values()].forEach((element) => {
			if (element instanceof HTMLStyleElement) {
				if (element.getAttribute(ILLEGAL_TRANSITION_NAMES)) {
					const filtered = element
						.getAttribute(ILLEGAL_TRANSITION_NAMES)!
						.split(', ')
						.filter((n) => !warned.has(n))
						.join(', ');
					if (filtered) element.setAttribute(ILLEGAL_TRANSITION_NAMES, filtered);
					else {
						element.removeAttribute(ILLEGAL_TRANSITION_NAMES);
						errors.delete(element);
					}
				}
			} else {
				if (
					warned.has(
						element.attributes['style' as any]?.nodeValue?.match(
							/view-transition-name:\s*([^;]*)/
						)?.[1] ?? ''
					)
				) {
					errors.delete(element);
				}
			}
		});

		if (supportsViewTransitions && errors.size > 0) {
			group(`%c[vtbot-linter] Illegal view-transition-name(s) ${here}`, ERROR);
			console.log(
				'%cMaybe it starts with a number, or is a reserved word, or it contains illegal characters?',
				WARNING
			);
			[...errors.values()].forEach((element) => {
				const illegalNames =
					element.getAttribute(ILLEGAL_TRANSITION_NAMES) ??
					element.attributes['style' as any]?.nodeValue?.match(
						/view-transition-name:\s*([^;]+)/
					)?.[1] ??
					'<unknown>';
				element.removeAttribute(ILLEGAL_TRANSITION_NAMES);
				console.log(
					`%c${illegalNames}%c in %o at %c${deriveCSSSelector(element)}`,
					ERROR,
					'',
					element,
					IMPORTANT
				);
			});
			console.groupEnd();
		}
	}

	function flagMissingScopedStyles(event: TransitionBeforeSwapEvent, origin: 'old' | 'new') {
		const here = `in ${origin} DOM (${event[origin === 'old' ? 'from' : 'to'].pathname})`;
		const { inStyleSheets, inElements } = astroContextIds();
		const notDefined = new Set([...inElements].filter((id) => !inStyleSheets.has(id)));
		const notUsed = new Set([...inStyleSheets].filter((id) => !inElements.has(id)));
		notDefined.forEach((id) => {
			group(`%c[vtbot-linter] scoped style id "${id}" is used but not defined ${here}. `, WARNING);
			[...document.querySelectorAll(`[data-astro-cid-${id}], [class*="astro-${id}"]`)]
				.filter((e, _, arr) => !arr.includes(e.parentElement!))
				.forEach((e) => console.log(e));
			console.log(
				'%c[vtbot-linter] The style sheet might got optimized away or the HTML element might have lost its style sheet, e.g. when being copied from another DOM.',
				IMPORTANT
			);
			console.groupEnd();
		});
	}

	function flagSuspiciousScriptTypes(event: TransitionBeforeSwapEvent) {
		const suss = [...event.newDocument.querySelectorAll('script[type]')].filter(
			(s) =>
				s instanceof HTMLScriptElement &&
				s.type &&
				s.type !== 'module' &&
				s.type !== 'text/javascript' &&
				s.dataset.vtbotLinter !== 'ignore'
		) as HTMLScriptElement[];

		if (!suss.length) return;

		group(`%c[vtbot-linter] suspicious script types in ${event.to.pathname}`, WARNING);
		suss.forEach((s) => console.log(s));
		console.groupEnd();
	}

	let savedEvent: TransitionBeforeSwapEvent;

	const beforeSwap = (event) => {
		savedEvent = event;
		group = expanded() ? console.group : console.groupCollapsed;
		if (enabled()) {
			flagNested(event, 'data-astro-transition-persist');
			flagNested(event, 'data-vtbot-replace');
			if (supportsViewTransitions) {
				flagNonUniqueOrMissingTransitionNames(event, 'old');
			} else {
				console.log(
					'%c[vtbot-linter] check for unique view transition names not supported in this browser',
					INFO
				);
			}
			flagMissingScopedStyles(event, 'old');
			flagSuspiciousScriptTypes(event);
		}
	};

	const afterSwap = () => {
		if (enabled()) {
			flagNonUniqueOrMissingTransitionNames(savedEvent, 'new');
			flagMissingScopedStyles(savedEvent, 'new');
		}
	};

	document.addEventListener(TRANSITION_BEFORE_SWAP, beforeSwap);
	document.addEventListener(TRANSITION_AFTER_SWAP, afterSwap);
</script>
