// NOTICE: This file is generated by Rollup. To modify it,
// please instead edit the ESM counterpart and rebuild with Rollup (npm run build).
'use strict';

const parser = require('postcss-selector-parser');
const typeGuards = require('../../utils/typeGuards.cjs');
const validateTypes = require('../../utils/validateTypes.cjs');
const hasBlock = require('../../utils/hasBlock.cjs');
const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule.cjs');
const optionsMatches = require('../../utils/optionsMatches.cjs');
const report = require('../../utils/report.cjs');
const ruleMessages = require('../../utils/ruleMessages.cjs');
const validateOptions = require('../../utils/validateOptions.cjs');

const ruleName = 'max-nesting-depth';

const messages = ruleMessages(ruleName, {
	expected: (depth) => `Expected nesting depth to be no more than ${depth}`,
});

const meta = {
	url: 'https://stylelint.io/user-guide/rules/max-nesting-depth',
};

/** @type {import('stylelint').CoreRules[ruleName]} */
const rule = (primary, secondaryOptions) => {
	/**
	 * @param {import('postcss').Node} node
	 */
	const isIgnoreAtRule = (node) =>
		typeGuards.isAtRule(node) && optionsMatches(secondaryOptions, 'ignoreAtRules', node.name);

	/**
	 * @param {import('postcss').Node} node
	 */
	const isIgnoreRule = (node) => {
		return typeGuards.isRule(node) && optionsMatches(secondaryOptions, 'ignoreRules', node.selector);
	};

	return (root, result) => {
		const validOptions = validateOptions(
			result,
			ruleName,
			{
				actual: primary,
				possible: [validateTypes.isNumber],
			},
			{
				optional: true,
				actual: secondaryOptions,
				possible: {
					ignore: ['blockless-at-rules', 'pseudo-classes'],
					ignoreAtRules: [validateTypes.isString, validateTypes.isRegExp],
					ignoreRules: [validateTypes.isString, validateTypes.isRegExp],
					ignorePseudoClasses: [validateTypes.isString, validateTypes.isRegExp],
				},
			},
		);

		if (!validOptions) return;

		root.walkRules(checkStatement);
		root.walkAtRules(checkStatement);

		/**
		 * @param {import('postcss').Rule | import('postcss').AtRule} statement
		 */
		function checkStatement(statement) {
			if (isIgnoreAtRule(statement)) {
				return;
			}

			if (isIgnoreRule(statement)) {
				return;
			}

			if (!hasBlock(statement)) {
				return;
			}

			if (typeGuards.isRule(statement) && !isStandardSyntaxRule(statement)) {
				return;
			}

			const depth = nestingDepth(statement, 0);

			if (depth > primary) {
				report({
					ruleName,
					result,
					node: statement,
					message: messages.expected,
					messageArgs: [primary],
				});
			}
		}
	};

	/**
	 * @param {import('postcss').Node} node
	 * @param {number} level
	 * @returns {number}
	 */
	function nestingDepth(node, level) {
		const parent = node.parent;

		if (!parent) {
			return 0;
		}

		if (isIgnoreAtRule(parent)) {
			return 0;
		}

		// The nesting depth level's computation has finished
		// when this function, recursively called, receives
		// a node that is not nested -- a direct child of the
		// root node
		if (typeGuards.isRoot(parent) || (typeGuards.isAtRule(parent) && parent.parent && typeGuards.isRoot(parent.parent))) {
			return level;
		}

		/**
		 * @param {string} selector
		 */
		function containsPseudoClassesOnly(selector) {
			const normalized = parser().processSync(selector, { lossless: false });
			const selectors = normalized.split(',');

			const filteredSelectors = selectors.filter(
				(sel) => !optionsMatches(secondaryOptions, 'ignoreRules', sel),
			);

			return filteredSelectors.every((sel) => extractPseudoRule(sel));
		}

		/**
		 * @param {string[]} selectors
		 * @returns {boolean}
		 */
		function containsIgnoredPseudoClassesOrRulesOnly(selectors) {
			if (
				!(
					secondaryOptions &&
					(secondaryOptions.ignorePseudoClasses || secondaryOptions.ignoreRules)
				)
			)
				return false;

			return selectors.every((selector) => {
				if (
					secondaryOptions.ignoreRules &&
					optionsMatches(secondaryOptions, 'ignoreRules', selector)
				)
					return true;

				if (!secondaryOptions.ignorePseudoClasses) return false;

				const pseudoRule = extractPseudoRule(selector);

				if (!pseudoRule) return false;

				return optionsMatches(secondaryOptions, 'ignorePseudoClasses', pseudoRule);
			});
		}

		if (
			(optionsMatches(secondaryOptions, 'ignore', 'blockless-at-rules') &&
				typeGuards.isAtRule(node) &&
				node.every((child) => !typeGuards.isDeclaration(child))) ||
			(optionsMatches(secondaryOptions, 'ignore', 'pseudo-classes') &&
				typeGuards.isRule(node) &&
				containsPseudoClassesOnly(node.selector)) ||
			(typeGuards.isRule(node) && containsIgnoredPseudoClassesOrRulesOnly(node.selectors))
		) {
			return nestingDepth(parent, level);
		}

		// Unless any of the conditions above apply, we want to
		// add 1 to the nesting depth level and then check the parent,
		// continuing to add and move up the hierarchy
		// until we hit the root node
		return nestingDepth(parent, level + 1);
	}
};

/**
 * @param {string} selector
 * @returns {string | undefined}
 */
function extractPseudoRule(selector) {
	return selector.startsWith('&:') && selector[2] !== ':' ? selector.slice(2) : undefined;
}

rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;

module.exports = rule;
