import {
    AttributeAction,
    type AttributeSelector,
    isTraversal as isTraversalBase,
    SelectorType,
    type Traversal,
} from "css-what";
import type { InternalSelector } from "../types.js";

/**
 * Check whether a selector token performs traversal.
 * @param token Selector token(s) to compile.
 */
export function isTraversal(token: InternalSelector): token is Traversal {
    return token.type === "_flexibleDescendant" || isTraversalBase(token);
}

/**
 * Sort the parts of the passed selector, as there is potential for
 * optimization (some types of selectors are faster than others).
 * @param array Selector to sort
 */
export function sortRules(array: InternalSelector[]): void {
    const ratings = array.map(getQuality);
    for (let index = 1; index < array.length; index++) {
        const procNew = ratings[index];

        if (procNew < 0) {
            continue;
        }

        // Use insertion sort to move the token to the correct position.
        for (
            let currentIndex = index;
            currentIndex > 0 && procNew < ratings[currentIndex - 1];
            currentIndex--
        ) {
            const token = array[currentIndex];
            array[currentIndex] = array[currentIndex - 1];
            array[currentIndex - 1] = token;
            ratings[currentIndex] = ratings[currentIndex - 1];
            ratings[currentIndex - 1] = procNew;
        }
    }
}

function getAttributeQuality(token: AttributeSelector): number {
    switch (token.action) {
        case AttributeAction.Exists: {
            return 10;
        }
        case AttributeAction.Equals: {
            // Prefer ID selectors (eg. #ID)
            return token.name === "id" ? 9 : 8;
        }
        case AttributeAction.Not: {
            return 7;
        }
        case AttributeAction.Start: {
            return 6;
        }
        case AttributeAction.End: {
            return 6;
        }
        case AttributeAction.Any: {
            return 5;
        }
        case AttributeAction.Hyphen: {
            return 4;
        }
        case AttributeAction.Element: {
            return 3;
        }
    }
}

/**
 * Determine the quality of the passed token. The higher the number, the
 * faster the token is to execute.
 * @param token Token to get the quality of.
 * @returns The token's quality.
 */
export function getQuality(token: InternalSelector): number {
    switch (token.type) {
        case SelectorType.Universal: {
            return 50;
        }
        case SelectorType.Tag: {
            return 30;
        }
        case SelectorType.Attribute: {
            return Math.floor(
                getAttributeQuality(token) /
                    // `ignoreCase` adds some overhead, half the result if applicable.
                    (token.ignoreCase ? 2 : 1),
            );
        }
        case SelectorType.Pseudo: {
            return token.data
                ? token.name === "has" ||
                  token.name === "contains" ||
                  token.name === "icontains"
                    ? // Expensive in any case — run as late as possible.
                      0
                    : Array.isArray(token.data)
                      ? // Eg. `:is`, `:not`
                        Math.max(
                            // If we have traversals, try to avoid executing this selector
                            0,
                            Math.min(
                                ...token.data.map((d) =>
                                    Math.min(...d.map(getQuality)),
                                ),
                            ),
                        )
                      : 2
                : 3;
        }
        default: {
            return -1;
        }
    }
}

/**
 * Check whether a token or nested token includes `:scope`.
 * @param t Selector token under inspection.
 */
export function includesScopePseudo(t: InternalSelector): boolean {
    return (
        t.type === SelectorType.Pseudo &&
        (t.name === "scope" ||
            (Array.isArray(t.data) &&
                t.data.some((data) => data.some(includesScopePseudo))))
    );
}
