/**
 * @fileoverview Rule to enforce the use of fallback fonts and a generic font last.
 * @author Tanuj Kanti
 */
//-----------------------------------------------------------------------------
// Type Definitions
//-----------------------------------------------------------------------------
/**
 * @import { CSSRuleDefinition } from "../types.js"
 * @typedef {"useFallbackFonts" | "useGenericFont"} FontFamilyFallbacksMessageIds
 * @typedef {CSSRuleDefinition<{ RuleOptions: [], MessageIds: FontFamilyFallbacksMessageIds }>} FontFamilyFallbacksRuleDefinition
 */
//-----------------------------------------------------------------------------
// Helpers
//-----------------------------------------------------------------------------
const genericFonts = new Set([
    "serif",
    "sans-serif",
    "monospace",
    "cursive",
    "fantasy",
    "system-ui",
    "ui-serif",
    "ui-sans-serif",
    "ui-monospace",
    "ui-rounded",
    "emoji",
    "math",
    "fangsong",
]);
/**
 * Check if the value is a CSS-wide keyword.
 * @param {string} value The value to check.
 * @param {Set<string>} cssWideKeywords The CSS-wide keywords to check against.
 * @returns {boolean} True if the value is a CSS-wide keyword, false otherwise.
 */
function isCSSWideKeyword(value, cssWideKeywords) {
    return cssWideKeywords.has(value.trim().toLowerCase());
}
/**
 * Check if the node is an identifier with a CSS-wide keyword.
 * @param {Object} node The node to check.
 * @param {Set<string>} cssWideKeywords The CSS-wide keywords to check against.
 * @returns {boolean} True if the node is a CSS-wide keyword identifier, false otherwise.
 */
function isCSSWideKeywordIdentifier(node, cssWideKeywords) {
    return (node.type === "Identifier" &&
        isCSSWideKeyword(node.name, cssWideKeywords));
}
/**
 * Check if the node is a CSS variable function.
 * @param {Object} node The node to check.
 * @returns {boolean} True if the node is a variable function, false otherwise.
 */
function isVarFunction(node) {
    return node.type === "Function" && node.name === "var";
}
/**
 * Report an error if the font property values do not have fallbacks or a generic font.
 * @param {string} fontPropertyValues The font property values to check.
 * @param {Object} context The ESLint context object.
 * @param {Object} node The CSS node being checked.
 * @param {Set<string>} cssWideKeywords The CSS-wide keywords to check against.
 * @returns {void}
 * @private
 */
function reportFontWithoutFallbacksInFontProperty(fontPropertyValues, context, node, cssWideKeywords) {
    if (isCSSWideKeyword(fontPropertyValues, cssWideKeywords)) {
        return;
    }
    const valueList = fontPropertyValues.split(",").map(v => v.trim());
    if (valueList.length === 1) {
        const containsGenericFont = Array.from(genericFonts).some(font => valueList[0].includes(font));
        if (!containsGenericFont) {
            context.report({
                loc: node.loc,
                messageId: "useFallbackFonts",
            });
        }
    }
    else {
        if (!genericFonts.has(valueList.at(-1))) {
            context.report({
                loc: node.loc,
                messageId: "useGenericFont",
            });
        }
    }
}
//-----------------------------------------------------------------------------
// Rule Definition
//-----------------------------------------------------------------------------
export default /** @satisfies {FontFamilyFallbacksRuleDefinition} */ ({
    meta: {
        type: "suggestion",
        docs: {
            description: "Enforce use of fallback fonts and a generic font last",
            recommended: true,
            url: "https://github.com/eslint/css/blob/main/docs/rules/font-family-fallbacks.md",
        },
        messages: {
            useFallbackFonts: "Use fallback fonts and a generic font last.",
            useGenericFont: "Use a generic font last.",
        },
    },
    create(context) {
        const sourceCode = context.sourceCode;
        const cssWideKeywords = new Set(sourceCode.lexer.cssWideKeywords.map(keyword => keyword.toLowerCase()));
        const variableMap = new Map();
        return {
            "Rule > Block > Declaration"(node) {
                if (node.property.startsWith("--")) {
                    const variableName = node.property;
                    const variableValue = node.value.type === "Raw" && node.value.value;
                    variableMap.set(variableName, variableValue);
                }
            },
            "Rule > Block > Declaration[property='font-family'] > Value"(node) {
                const valueArr = node.children;
                if (valueArr.length === 1) {
                    if (isCSSWideKeywordIdentifier(valueArr[0], cssWideKeywords)) {
                        return;
                    }
                    if (valueArr[0].type === "Function" &&
                        valueArr[0].name === "var") {
                        const variableName = valueArr[0].children[0].type === "Identifier" &&
                            valueArr[0].children[0].name;
                        const variableValue = variableMap.get(variableName);
                        if (!variableValue) {
                            return;
                        }
                        if (isCSSWideKeyword(variableValue, cssWideKeywords)) {
                            return;
                        }
                        const variableList = variableValue
                            .split(",")
                            .map(v => v.trim());
                        if (variableList.length === 1 &&
                            !genericFonts.has(variableList[0])) {
                            context.report({
                                loc: node.loc,
                                messageId: "useFallbackFonts",
                            });
                        }
                        else if (!genericFonts.has(variableList.at(-1))) {
                            context.report({
                                loc: node.loc,
                                messageId: "useGenericFont",
                            });
                        }
                    }
                    else {
                        if (valueArr[0].type === "Identifier" &&
                            genericFonts.has(valueArr[0].name)) {
                            return;
                        }
                        context.report({
                            loc: node.loc,
                            messageId: "useFallbackFonts",
                        });
                    }
                }
                else {
                    const isUsingVariable = valueArr.some(child => isVarFunction(child));
                    if (isUsingVariable) {
                        const fontsList = [];
                        const lastNode = valueArr.at(-1);
                        if (lastNode.type === "Function" &&
                            lastNode.name === "var") {
                            const variableName = lastNode.children[0].type === "Identifier" &&
                                lastNode.children[0].name;
                            const lastVariable = variableMap.get(variableName);
                            if (!lastVariable) {
                                return;
                            }
                        }
                        valueArr.forEach(child => {
                            if (child.type === "String") {
                                fontsList.push(child.value);
                            }
                            if (child.type === "Identifier") {
                                fontsList.push(child.name);
                            }
                            if (child.type === "Function" &&
                                child.name === "var") {
                                const variableName = child.children[0].type === "Identifier" &&
                                    child.children[0].name;
                                const variableValue = variableMap.get(variableName);
                                if (variableValue) {
                                    const variableList = variableValue
                                        .split(",")
                                        .map(v => v.trim());
                                    fontsList.push(...variableList);
                                }
                            }
                        });
                        if (fontsList.length > 0 &&
                            !genericFonts.has(fontsList.at(-1))) {
                            context.report({
                                loc: node.loc,
                                messageId: "useGenericFont",
                            });
                        }
                    }
                    else {
                        const lastFont = valueArr.at(-1);
                        if (!(lastFont.type === "Identifier" &&
                            genericFonts.has(lastFont.name))) {
                            context.report({
                                loc: node.loc,
                                messageId: "useGenericFont",
                            });
                        }
                    }
                }
            },
            "Rule > Block > Declaration[property='font'] > Value"(node) {
                const valueArr = node.children;
                if (valueArr.length === 1) {
                    const firstValue = valueArr[0];
                    // If it font is set to system font, we don't need to check for fallbacks
                    if (firstValue.type === "Identifier") {
                        return;
                    }
                    // If the value is a variable function, we need to check the variable value
                    if (firstValue.type === "Function" &&
                        firstValue.name === "var") {
                        // Check if the function is a variable
                        const variableName = firstValue.children[0].type === "Identifier" &&
                            firstValue.children[0].name;
                        const variableValue = variableMap.get(variableName);
                        if (!variableValue) {
                            return;
                        }
                        reportFontWithoutFallbacksInFontProperty(variableValue, context, node, cssWideKeywords);
                    }
                }
                else {
                    const isUsingVariable = valueArr.some(child => isVarFunction(child));
                    if (isUsingVariable) {
                        const beforOperator = [];
                        const afterOperator = [];
                        const operator = valueArr.find(child => child.type === "Operator" &&
                            child.value === ",");
                        const operatorOffset = operator && operator.loc.end.offset;
                        if (operatorOffset) {
                            valueArr.forEach(child => {
                                if (child.loc.end.offset < operatorOffset) {
                                    beforOperator.push(sourceCode.getText(child).trim());
                                }
                                else if (child.loc.end.offset > operatorOffset) {
                                    afterOperator.push(sourceCode.getText(child).trim());
                                }
                            });
                            if (afterOperator.length !== 0) {
                                const usingVar = afterOperator.some(value => value.startsWith("var"));
                                if (!usingVar) {
                                    if (!genericFonts.has(afterOperator.at(-1))) {
                                        context.report({
                                            loc: node.loc,
                                            messageId: "useGenericFont",
                                        });
                                    }
                                }
                                else {
                                    if (afterOperator.at(-1).startsWith("var")) {
                                        const lastNode = valueArr.at(-1);
                                        const isFunctionVar = lastNode.type === "Function" &&
                                            lastNode.name === "var";
                                        const variableName = isFunctionVar &&
                                            lastNode.children[0].type ===
                                                "Identifier" &&
                                            lastNode.children[0].name;
                                        const variableValue = variableMap.get(variableName);
                                        if (!variableValue) {
                                            return;
                                        }
                                        const variableList = variableValue
                                            .split(",")
                                            .map(v => v.trim());
                                        if (variableList.length > 0 &&
                                            !genericFonts.has(variableList.at(-1))) {
                                            context.report({
                                                loc: node.loc,
                                                messageId: "useGenericFont",
                                            });
                                        }
                                    }
                                    else {
                                        if (!genericFonts.has(afterOperator.at(-1))) {
                                            context.report({
                                                loc: node.loc,
                                                messageId: "useGenericFont",
                                            });
                                        }
                                    }
                                }
                            }
                        }
                        else {
                            if (sourceCode
                                .getText(valueArr.at(-1))
                                .trim()
                                .startsWith("var")) {
                                const lastNode = valueArr.at(-1);
                                const isFunctionVar = lastNode.type === "Function" &&
                                    lastNode.name === "var";
                                const variableName = isFunctionVar &&
                                    lastNode.children[0].type ===
                                        "Identifier" &&
                                    lastNode.children[0].name;
                                const variableValue = variableMap.get(variableName);
                                if (!variableValue) {
                                    return;
                                }
                                reportFontWithoutFallbacksInFontProperty(variableValue, context, node, cssWideKeywords);
                            }
                            else {
                                if (!genericFonts.has(sourceCode
                                    .getText(valueArr.at(-1))
                                    .trim())) {
                                    context.report({
                                        loc: node.loc,
                                        messageId: "useFallbackFonts",
                                    });
                                }
                            }
                        }
                    }
                    else {
                        const fontPropertyValues = sourceCode.getText(node);
                        if (fontPropertyValues) {
                            reportFontWithoutFallbacksInFontProperty(fontPropertyValues, context, node, cssWideKeywords);
                        }
                    }
                }
            },
        };
    },
});
