/** @typedef {Record<string, {prelude?: string | undefined, descriptors?: Record<string, string> | undefined}>} AtRuleDefinitions */
/** @typedef {Record<string, string>} PropertyDefinitions */
/** @typedef {Record<string, string>} TypeDefinitions */
/** @typedef {Array<string>} CSSWideKeywords */
/** @typedef {{atrules?: AtRuleDefinitions | undefined, properties?: PropertyDefinitions | undefined, types?: TypeDefinitions | undefined, cssWideKeywords?: CSSWideKeywords | undefined}} SyntaxDefinition */

/**
 * Merge multiple separate syntax definitions.
 * Individual definitions will override each other unless the second definition starts with a pipe symbol `|`.
 *
 * @param {...(SyntaxDefinition|undefined)} sources
 * @returns {SyntaxDefinition}
 */
export default function mergeSyntaxDefinitions(...sources) {
	/** @type {SyntaxDefinition|undefined} */
	let target = sources[0] ?? {};

	for (let i = 1; i < sources.length; i++) {
		const source = sources[i];

		if (!source) continue;

		target = mergeTwoSyntaxDefinitions(target, source);
	}

	return target;
}

/**
 * Merge two separate syntax definitions.
 * Individual definitions will override each other unless the second definition starts with a pipe symbol `|`.
 *
 * @param {SyntaxDefinition} source1
 * @param {SyntaxDefinition} source2
 * @returns {SyntaxDefinition}
 */
function mergeTwoSyntaxDefinitions(source1, source2) {
	/** @type {SyntaxDefinition} */
	const target = {};

	target.atrules = mergeAtRuleDefinitions(source1.atrules, source2.atrules);
	target.properties = mergePropertyOrTypeDefinitions(source1.properties, source2.properties);
	target.types = mergePropertyOrTypeDefinitions(source1.types, source2.types);
	target.cssWideKeywords = mergeKeywords(source1.cssWideKeywords, source2.cssWideKeywords);

	return target;
}

/**
 * @param {AtRuleDefinitions | undefined} source1
 * @param {AtRuleDefinitions | undefined} source2
 * @returns {AtRuleDefinitions | undefined}
 */
function mergeAtRuleDefinitions(source1, source2) {
	if (typeof source1 === 'undefined' || typeof source2 === 'undefined')
		return source1 ?? source2 ?? {};

	/** @type {AtRuleDefinitions} */
	const target = structuredClone(source1);

	for (const [atRuleName, atRule] of Object.entries(source2)) {
		const targetAtRule = target[atRuleName] ?? {};

		targetAtRule.prelude = mergePrelude(targetAtRule.prelude, atRule.prelude);
		targetAtRule.descriptors = mergePropertyOrTypeDefinitions(
			targetAtRule.descriptors,
			atRule.descriptors,
		);

		target[atRuleName] = targetAtRule;
	}

	return target;
}

/**
 * @param {Record<string, string> | undefined} source1
 * @param {Record<string, string> | undefined} source2
 * @returns {Record<string, string> | undefined}
 */
function mergePropertyOrTypeDefinitions(source1, source2) {
	if (typeof source1 === 'undefined' || typeof source2 === 'undefined')
		return source1 ?? source2 ?? {};

	/** @type {Record<string, string>} */
	const target = {
		...source1,
	};

	for (const [name, syntax] of Object.entries(source2)) {
		target[name] = mergeSyntax(target[name], syntax);
	}

	return target;
}

/**
 * @param {Array<string> | undefined} source1
 * @param {Array<string> | undefined} source2
 * @returns {Array<string> | undefined}
 */
function mergeKeywords(source1, source2) {
	if (typeof source1 === 'undefined' || typeof source2 === 'undefined')
		return source1 ?? source2 ?? [];

	return Array.from(new Set([...source1, ...source2]));
}

/**
 * @param {string | undefined} source1
 * @param {string} source2
 * @returns {string}
 */
function mergeSyntax(source1, source2) {
	if (typeof source1 === 'undefined') return source2;

	const trimmedSource2 = source2.trimStart();

	if (!trimmedSource2.startsWith('|')) return source2;

	return `${source1} ${trimmedSource2}`;
}

/**
 * @param {string | undefined} source1
 * @param {string | undefined} source2
 * @returns {string | undefined}
 */
function mergePrelude(source1, source2) {
	if (typeof source1 === 'undefined' || typeof source2 === 'undefined') return source1 ?? source2;

	const trimmedSource2 = source2.trimStart();

	if (!trimmedSource2.startsWith('|')) return source2;

	return `${source1} ${trimmedSource2}`;
}
