'use strict';

const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule');
const isStandardSyntaxSelector = require('../../utils/isStandardSyntaxSelector');
const parseSelector = require('../../utils/parseSelector');
const report = require('../../utils/report');
const ruleMessages = require('../../utils/ruleMessages');
const validateOptions = require('../../utils/validateOptions');
const {
	isPseudoClass,
	isAttribute,
	isClassName,
	isUniversal,
	isIdentifier,
	isTag,
} = require('postcss-selector-parser');
const { assert } = require('../../utils/validateTypes');

const ruleName = 'selector-not-notation';
const messages = ruleMessages(ruleName, {
	expected: (type) => `Expected ${type} :not() pseudo-class notation`,
});
const meta = {
	url: 'https://stylelint.io/user-guide/rules/selector-not-notation',
	fixable: true,
};

/** @typedef {import('postcss-selector-parser').Node} Node */
/** @typedef {import('postcss-selector-parser').Selector} Selector */
/** @typedef {import('postcss-selector-parser').Pseudo} Pseudo */

/**
 * @param {Node} node
 * @returns {boolean}
 */
const isSimpleSelector = (node) =>
	isPseudoClass(node) ||
	isAttribute(node) ||
	isClassName(node) ||
	isUniversal(node) ||
	isIdentifier(node) ||
	isTag(node);

/**
 * @param {Node} node
 * @returns {node is Pseudo}
 */
const isNot = (node) =>
	isPseudoClass(node) && node.value !== undefined && node.value.toLowerCase() === ':not';

/**
 * @param {Selector[]} list
 * @returns {boolean}
 */
const isSimple = (list) => {
	if (list.length > 1) return false;

	assert(list[0], 'list is never empty');
	const [first, second] = list[0].nodes;

	if (!first) return true;

	if (second) return false;

	return isSimpleSelector(first) && !isNot(first);
};

/** @type {import('stylelint').Rule} */
const rule = (primary, _, context) => {
	return (root, result) => {
		const validOptions = validateOptions(result, ruleName, {
			actual: primary,
			possible: ['simple', 'complex'],
		});

		if (!validOptions) return;

		root.walkRules((ruleNode) => {
			if (!isStandardSyntaxRule(ruleNode)) return;

			const selector = ruleNode.selector;

			if (!selector.includes(':not(')) return;

			if (!isStandardSyntaxSelector(selector)) return;

			const fixedSelector = parseSelector(selector, result, ruleNode, (container) => {
				container.walkPseudos((pseudo) => {
					if (!isNot(pseudo)) return;

					if (primary === 'complex') {
						const prev = pseudo.prev();
						const hasConsecutiveNot = prev && isNot(prev);

						if (!hasConsecutiveNot) return;

						if (context.fix) return fixComplex(prev);
					} else {
						const selectors = pseudo.nodes;

						if (isSimple(selectors)) return;

						const mustFix =
							context.fix &&
							selectors.length > 1 &&
							selectors[1] &&
							(selectors[1].nodes.length === 0 ||
								selectors.every(({ nodes }) => nodes.length === 1));

						if (mustFix) return fixSimple(pseudo);
					}

					assert(pseudo.source && pseudo.source.end);

					report({
						message: messages.expected,
						messageArgs: [primary],
						node: ruleNode,
						index: pseudo.sourceIndex,
						endIndex: pseudo.source.end.column,
						result,
						ruleName,
					});
				});
			});

			if (context.fix && fixedSelector) {
				ruleNode.selector = fixedSelector;
			}
		});
	};
};

/**
 * @param {Pseudo} not
 * @returns {Node}
 */
const getLastConsecutiveNot = ({ parent, sourceIndex }) => {
	assert(parent);

	const nodes = parent.nodes;
	const index = nodes.findIndex((node) => node.sourceIndex >= sourceIndex && !isNot(node));
	const node = index === -1 ? nodes[nodes.length - 1] : nodes[index - 1];

	assert(node);

	return node;
};

/**
 * @param {Pseudo} not
 */
function fixSimple(not) {
	const simpleSelectors = not.nodes
		.filter(({ nodes }) => nodes[0] && isSimpleSelector(nodes[0]))
		.map((s) => {
			assert(s.nodes[0]);
			s.nodes[0].rawSpaceBefore = '';
			s.nodes[0].rawSpaceAfter = '';

			return s;
		});
	const firstSelector = simpleSelectors.shift();

	assert(firstSelector);
	assert(not.parent);

	not.empty();
	not.nodes.push(firstSelector);

	for (const s of simpleSelectors) {
		const last = getLastConsecutiveNot(not);

		not.parent.insertAfter(last, last.clone({ nodes: [s] }));
	}
}

/**
 * @param {Pseudo} previousNot
 */
function fixComplex(previousNot) {
	const indentAndTrimRight = (/** @type {Selector[]} */ selectors) => {
		for (const s of selectors) {
			assert(s.nodes[0]);
			s.nodes[0].rawSpaceBefore = ' ';
			s.nodes[0].rawSpaceAfter = '';
		}
	};
	const [head, ...tail] = previousNot.nodes;
	let node = previousNot.next();

	if (head == null || head.nodes.length === 0) return;

	assert(head.nodes[0]);
	head.nodes[0].rawSpaceBefore = '';
	head.nodes[0].rawSpaceAfter = '';
	indentAndTrimRight(tail);

	while (isNot(node)) {
		const selectors = node.nodes;
		const prev = node;

		indentAndTrimRight(selectors);
		previousNot.nodes = previousNot.nodes.concat(selectors);
		node = node.next();
		prev.remove();
	}
}

rule.ruleName = ruleName;
rule.messages = messages;
rule.meta = meta;
module.exports = rule;
