import { Selector, SelectorType, AttributeAction } from "./types";

const attribValChars = ["\\", '"'];
const pseudoValChars = [...attribValChars, "(", ")"];

const charsToEscapeInAttributeValue = new Set(
    attribValChars.map((c) => c.charCodeAt(0))
);
const charsToEscapeInPseudoValue = new Set(
    pseudoValChars.map((c) => c.charCodeAt(0))
);
const charsToEscapeInName = new Set(
    [
        ...pseudoValChars,
        "~",
        "^",
        "$",
        "*",
        "+",
        "!",
        "|",
        ":",
        "[",
        "]",
        " ",
        ".",
    ].map((c) => c.charCodeAt(0))
);

/**
 * Turns `selector` back into a string.
 *
 * @param selector Selector to stringify.
 */
export function stringify(selector: Selector[][]): string {
    return selector
        .map((token) => token.map(stringifyToken).join(""))
        .join(", ");
}

function stringifyToken(
    token: Selector,
    index: number,
    arr: Selector[]
): string {
    switch (token.type) {
        // Simple types
        case SelectorType.Child:
            return index === 0 ? "> " : " > ";
        case SelectorType.Parent:
            return index === 0 ? "< " : " < ";
        case SelectorType.Sibling:
            return index === 0 ? "~ " : " ~ ";
        case SelectorType.Adjacent:
            return index === 0 ? "+ " : " + ";
        case SelectorType.Descendant:
            return " ";
        case SelectorType.ColumnCombinator:
            return index === 0 ? "|| " : " || ";
        case SelectorType.Universal:
            // Return an empty string if the selector isn't needed.
            return token.namespace === "*" &&
                index + 1 < arr.length &&
                "name" in arr[index + 1]
                ? ""
                : `${getNamespace(token.namespace)}*`;

        case SelectorType.Tag:
            return getNamespacedName(token);

        case SelectorType.PseudoElement:
            return `::${escapeName(token.name, charsToEscapeInName)}${
                token.data === null
                    ? ""
                    : `(${escapeName(token.data, charsToEscapeInPseudoValue)})`
            }`;

        case SelectorType.Pseudo:
            return `:${escapeName(token.name, charsToEscapeInName)}${
                token.data === null
                    ? ""
                    : `(${
                          typeof token.data === "string"
                              ? escapeName(
                                    token.data,
                                    charsToEscapeInPseudoValue
                                )
                              : stringify(token.data)
                      })`
            }`;

        case SelectorType.Attribute: {
            if (
                token.name === "id" &&
                token.action === AttributeAction.Equals &&
                token.ignoreCase === "quirks" &&
                !token.namespace
            ) {
                return `#${escapeName(token.value, charsToEscapeInName)}`;
            }
            if (
                token.name === "class" &&
                token.action === AttributeAction.Element &&
                token.ignoreCase === "quirks" &&
                !token.namespace
            ) {
                return `.${escapeName(token.value, charsToEscapeInName)}`;
            }

            const name = getNamespacedName(token);

            if (token.action === AttributeAction.Exists) {
                return `[${name}]`;
            }

            return `[${name}${getActionValue(token.action)}="${escapeName(
                token.value,
                charsToEscapeInAttributeValue
            )}"${
                token.ignoreCase === null ? "" : token.ignoreCase ? " i" : " s"
            }]`;
        }
    }
}

function getActionValue(action: AttributeAction): string {
    switch (action) {
        case AttributeAction.Equals:
            return "";
        case AttributeAction.Element:
            return "~";
        case AttributeAction.Start:
            return "^";
        case AttributeAction.End:
            return "$";
        case AttributeAction.Any:
            return "*";
        case AttributeAction.Not:
            return "!";
        case AttributeAction.Hyphen:
            return "|";
        case AttributeAction.Exists:
            throw new Error("Shouldn't be here");
    }
}

function getNamespacedName(token: {
    name: string;
    namespace: string | null;
}): string {
    return `${getNamespace(token.namespace)}${escapeName(
        token.name,
        charsToEscapeInName
    )}`;
}

function getNamespace(namespace: string | null): string {
    return namespace !== null
        ? `${
              namespace === "*"
                  ? "*"
                  : escapeName(namespace, charsToEscapeInName)
          }|`
        : "";
}

function escapeName(str: string, charsToEscape: Set<number>): string {
    let lastIdx = 0;
    let ret = "";

    for (let i = 0; i < str.length; i++) {
        if (charsToEscape.has(str.charCodeAt(i))) {
            ret += `${str.slice(lastIdx, i)}\\${str.charAt(i)}`;
            lastIdx = i + 1;
        }
    }

    return ret.length > 0 ? ret + str.slice(lastIdx) : str;
}
