/**
 * @fileoverview Rule to limit and disallow CSS selectors.
 * @author Tanuj Kanti
 */
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
 * @import { CSSRuleDefinition } from "../types.js"
 * @typedef {"maxSelectors" | "disallowedSelectors"} SelectorComplexityMessageIds
 * @typedef {[{
 *     maxIds?: number,
 *     maxClasses?: number,
 *     maxTypes?: number,
 *     maxAttributes?: number,
 *     maxPseudoClasses?: number,
 *     maxUniversals?: number,
 *     maxCompounds?: number,
 *     maxCombinators?: number,
 *     disallowCombinators?: string[],
 *     disallowPseudoClasses?: string[],
 *     disallowPseudoElements?: string[],
 *     disallowAttributes?: string[],
 *     disallowAttributeMatchers?: string[],
 * }]} SelectorComplexityOptions
 * @typedef {CSSRuleDefinition<{ RuleOptions: SelectorComplexityOptions, MessageIds: SelectorComplexityMessageIds }> } SelectorComplexityRuleDefinition
 */
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
/**
 * Get the location of a selector of a given name.
 * @param {Array<Object>} allSelector All CSS selector nodes.
 * @param {string} disallowedSelector The name of the disallowed selector.
 * @returns {Object} The location of the disallowed selector.
 */
function getDisallowedSelectorsLocation(allSelector, disallowedSelector) {
    return allSelector.find(selector => selector.name === disallowedSelector)
        .loc;
}
/**
 * An error for exceeding the maximum allowed selectors of a specific type.
 * @param {Object} context The ESLint rule context object.
 * @param {Object} selectorLoc The location of the selector.
 * @param {number} maxValue The max number of selectors that are allowed.
 * @param {string} selectorType The type of CSS selector.
 * @returns {void}
 */
function exceedLimitError(context, selectorLoc, maxValue, selectorType) {
    context.report({
        loc: selectorLoc,
        messageId: "maxSelectors",
        data: {
            selector: selectorType,
            limit: maxValue,
        },
    });
}
/**
 * Gives an array of CSS selectors of a specific type.
 * @param {Array<Object>} selectors All CSS selectors nodes.
 * @param {string} selectorType The type of CSS selector to filter out.
 * @returns {Array<Object>} Filtered selectors.
 */
function getSelectors(selectors, selectorType) {
    return selectors.filter(selector => selector.type === selectorType);
}
/**
 * Get the names of all CSS selectors.
 * @param {Array<Object>} selectors All CSS selector nodes.
 * @returns {Array<string>} Array of selector names.
 */
function getSelectorNames(selectors) {
    return selectors.map(selector => selector.name);
}
/**
 * Get the location of the attribute matcher or operator in a given attribute selector.
 * @param {Array<Object>} selectors All CSS selector nodes.
 * @param {number} index The index of the attribute selector in the selectors array.
 * @returns {{ startLoc: Object, endLoc: Object }} The start and end locations of the operator.
 */
function getOperatorLocation(selectors, index) {
    const selector = selectors[index];
    let startLoc;
    let endLoc;
    if (selector.name.type === "Identifier") {
        startLoc = selector.name.loc.end;
    }
    if (selector.value) {
        endLoc = selector.value.loc.start;
    }
    return { startLoc, endLoc };
}
/**
 * Get the location of a given disallowed combinator.
 * @param {Array<Object>} selectors All CSS selector nodes.
 * @param {Array<Object>} combinatorNodes All combinator nodes.
 * @param {string} combinator Name of combinator.
 * @param {number} index The index of the given combinator.
 * @returns {Object} The location of the disallowed combinator.
 */
function getDisallowedCombinatorsLocation(selectors, combinatorNodes, combinator, index) {
    let location;
    if (combinator === " ") {
        const selectorsArr = [];
        let selectorsGroup = [];
        selectors.forEach(selector => {
            if (selector.type === "Combinator") {
                selectorsArr.push(selectorsGroup);
                selectorsGroup = [];
            }
            else {
                selectorsGroup.push(selector);
            }
        });
        if (selectorsGroup.length > 0) {
            selectorsArr.push(selectorsGroup);
        }
        location = {
            start: selectorsArr[index].at(-1).loc.end,
            end: selectorsArr[index + 1][0].loc.start,
        };
    }
    else {
        const currentCombinatorNode = combinatorNodes[index];
        location = currentCombinatorNode.loc;
    }
    return location;
}
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
export default /** @satisfies {SelectorComplexityRuleDefinition} */ ({
    meta: {
        type: "problem",
        docs: {
            description: "Disallow and limit CSS selectors",
            recommended: false,
            url: "https://github.com/eslint/css/blob/main/docs/rules/selector-complexity.md",
        },
        schema: [
            {
                type: "object",
                properties: {
                    maxIds: {
                        type: "integer",
                        minimum: 0,
                    },
                    maxClasses: {
                        type: "integer",
                        minimum: 0,
                    },
                    maxTypes: {
                        type: "integer",
                        minimum: 0,
                    },
                    maxAttributes: {
                        type: "integer",
                        minimum: 0,
                    },
                    maxPseudoClasses: {
                        type: "integer",
                        minimum: 0,
                    },
                    maxUniversals: {
                        type: "integer",
                        minimum: 0,
                    },
                    maxCompounds: {
                        type: "integer",
                        minimum: 0,
                    },
                    maxCombinators: {
                        type: "integer",
                        minimum: 0,
                    },
                    disallowCombinators: {
                        type: "array",
                        items: {
                            type: "string",
                        },
                        uniqueItems: true,
                    },
                    disallowPseudoClasses: {
                        type: "array",
                        items: {
                            type: "string",
                        },
                        uniqueItems: true,
                    },
                    disallowPseudoElements: {
                        type: "array",
                        items: {
                            type: "string",
                        },
                        uniqueItems: true,
                    },
                    disallowAttributes: {
                        type: "array",
                        items: {
                            type: "string",
                        },
                        uniqueItems: true,
                    },
                    disallowAttributeMatchers: {
                        type: "array",
                        items: {
                            type: "string",
                        },
                        uniqueItems: true,
                    },
                },
                additionalProperties: false,
            },
        ],
        defaultOptions: [
            {
                maxIds: Infinity,
                maxClasses: Infinity,
                maxTypes: Infinity,
                maxAttributes: Infinity,
                maxPseudoClasses: Infinity,
                maxUniversals: Infinity,
                maxCompounds: Infinity,
                maxCombinators: Infinity,
                disallowCombinators: [],
                disallowPseudoClasses: [],
                disallowPseudoElements: [],
                disallowAttributes: [],
                disallowAttributeMatchers: [],
            },
        ],
        messages: {
            maxSelectors: "Exceeded maximum {{selector}} selector. Only {{limit}} allowed.",
            disallowedSelectors: "'{{selectorName}}' {{selector}} is not allowed.",
        },
    },
    create(context) {
        const [{ maxIds, maxClasses, maxTypes, maxAttributes, maxPseudoClasses, maxUniversals, maxCompounds, maxCombinators, disallowCombinators, disallowPseudoClasses, disallowPseudoElements, disallowAttributes, disallowAttributeMatchers, },] = context.options;
        return {
            Selector(node) {
                const selectors = node.children;
                const selectorLoc = node.loc;
                const idSelectors = getSelectors(selectors, "IdSelector");
                const classSelectors = getSelectors(selectors, "ClassSelector");
                const typeSelectors = selectors.filter(child => child.type === "TypeSelector" && child.name !== "*");
                const attributeSelectors = getSelectors(selectors, "AttributeSelector");
                const pseudoClassSelectors = getSelectors(selectors, "PseudoClassSelector");
                const universalSelectors = selectors.filter(child => child.type === "TypeSelector" && child.name === "*");
                const combinatorNodes = getSelectors(selectors, "Combinator");
                const combinators = getSelectorNames(combinatorNodes);
                const pseudoClassSelectorsNames = getSelectorNames(pseudoClassSelectors);
                const pseudoElementSelectors = getSelectors(selectors, "PseudoElementSelector");
                const pseudoElementNames = getSelectorNames(pseudoElementSelectors);
                const attributeNames = attributeSelectors.map(s => s.name.name);
                const attributeMatchers = attributeSelectors
                    .map(child => child.matcher)
                    .filter(Boolean);
                if (idSelectors.length > maxIds) {
                    exceedLimitError(context, selectorLoc, maxIds, "id");
                }
                if (classSelectors.length > maxClasses) {
                    exceedLimitError(context, selectorLoc, maxClasses, "class");
                }
                if (typeSelectors.length > maxTypes) {
                    exceedLimitError(context, selectorLoc, maxTypes, "type");
                }
                if (attributeSelectors.length > maxAttributes) {
                    exceedLimitError(context, selectorLoc, maxAttributes, "attribute");
                }
                if (pseudoClassSelectors.length > maxPseudoClasses) {
                    exceedLimitError(context, selectorLoc, maxPseudoClasses, "pseudo-class");
                }
                if (universalSelectors.length > maxUniversals) {
                    exceedLimitError(context, selectorLoc, maxUniversals, "universal");
                }
                if (combinatorNodes.length > maxCombinators) {
                    exceedLimitError(context, selectorLoc, maxCombinators, "combinator");
                }
                if (combinatorNodes.length + 1 > maxCompounds) {
                    exceedLimitError(context, selectorLoc, maxCompounds, "compound");
                }
                if (disallowPseudoClasses.length > 0) {
                    let disallowedPseudoClassLocation;
                    for (const pseudoClassName of pseudoClassSelectorsNames) {
                        if (disallowPseudoClasses.includes(pseudoClassName)) {
                            disallowedPseudoClassLocation =
                                getDisallowedSelectorsLocation(pseudoClassSelectors, pseudoClassName);
                            context.report({
                                loc: disallowedPseudoClassLocation,
                                messageId: "disallowedSelectors",
                                data: {
                                    selectorName: pseudoClassName,
                                    selector: "pseudo-class",
                                },
                            });
                        }
                    }
                }
                if (disallowCombinators.length > 0) {
                    let disallowedCombinatorLocation;
                    for (const [index, combinator] of combinators.entries()) {
                        if (disallowCombinators.includes(combinator)) {
                            disallowedCombinatorLocation =
                                getDisallowedCombinatorsLocation(selectors, combinatorNodes, combinator, index);
                            context.report({
                                loc: disallowedCombinatorLocation,
                                messageId: "disallowedSelectors",
                                data: {
                                    selectorName: combinator,
                                    selector: "combinator",
                                },
                            });
                        }
                    }
                }
                if (disallowPseudoElements.length > 0) {
                    let disallowPseudoElementsLocation;
                    for (const pseudoElement of pseudoElementNames) {
                        if (disallowPseudoElements.includes(pseudoElement)) {
                            disallowPseudoElementsLocation =
                                getDisallowedSelectorsLocation(pseudoElementSelectors, pseudoElement);
                            context.report({
                                loc: disallowPseudoElementsLocation,
                                messageId: "disallowedSelectors",
                                data: {
                                    selectorName: pseudoElement,
                                    selector: "pseudo-element",
                                },
                            });
                        }
                    }
                }
                if (disallowAttributes.length > 0) {
                    let disallowAttributesLocation;
                    for (const attributeName of attributeNames) {
                        if (disallowAttributes.includes(attributeName)) {
                            disallowAttributesLocation =
                                attributeSelectors.find(selector => selector.name.name === attributeName).name.loc;
                            context.report({
                                loc: disallowAttributesLocation,
                                messageId: "disallowedSelectors",
                                data: {
                                    selectorName: attributeName,
                                    selector: "attribute",
                                },
                            });
                        }
                    }
                }
                if (disallowAttributeMatchers.length > 0) {
                    for (const [index, attributeMatcher,] of attributeMatchers.entries()) {
                        if (disallowAttributeMatchers.includes(attributeMatcher)) {
                            const { startLoc, endLoc } = getOperatorLocation(attributeSelectors.filter(s => s.matcher), index);
                            context.report({
                                loc: {
                                    start: startLoc,
                                    end: endLoc,
                                },
                                messageId: "disallowedSelectors",
                                data: {
                                    selectorName: attributeMatcher,
                                    selector: "attribute-matcher",
                                },
                            });
                        }
                    }
                }
            },
        };
    },
});
