import { Character } from "../../types/data";
import { Constant, Selector, type SelectorInput } from "../../types/layout";

const excludeClassNames = Constant.ExcludeClassNames.split(Constant.Comma);
let selectorMap: { [selector: string]: number[] } = {};

export function reset(): void {
    selectorMap = {};
}

export function get(input: SelectorInput, type: Selector): string {
    const a = input.attributes;
    let prefix = input.prefix ? input.prefix[type] : null;
    const suffix = type === Selector.Alpha ? `${Constant.Tilde}${input.position - 1}` : `:nth-of-type(${input.position})`;
    switch (input.tag) {
        case "STYLE":
        case "TITLE":
        case "LINK":
        case "META":
        case Constant.TextTag:
        case Constant.DocumentTag:
            return Constant.Empty;
        case "HTML":
            return Constant.HTML;
        default: {
            if (prefix === null) {
                return Constant.Empty;
            }
            prefix = `${prefix}${Constant.Separator}`;
            input.tag = input.tag.indexOf(Constant.SvgPrefix) === 0 ? input.tag.substr(Constant.SvgPrefix.length) : input.tag;
            let selector = `${prefix}${input.tag}${suffix}`;
            const id = Constant.Id in a && a[Constant.Id].length > 0 ? a[Constant.Id] : null;
            const classes =
                input.tag !== Constant.BodyTag && Constant.Class in a && a[Constant.Class].length > 0
                    ? a[Constant.Class]
                          .trim()
                          .split(/\s+/)
                          .filter((c) => filter(c))
                          .join(Constant.Period)
                    : null;
            if (classes && classes.length > 0) {
                if (type === Selector.Alpha) {
                    // In Alpha mode, update selector to use class names, with relative positioning within the parent id container.
                    // If the node has valid class name(s) then drop relative positioning within the parent path to keep things simple.
                    const key = `${getDomPath(prefix)}${input.tag}${Constant.Dot}${classes}`;
                    if (!(key in selectorMap)) {
                        selectorMap[key] = [];
                    }
                    if (selectorMap[key].indexOf(input.id) < 0) {
                        selectorMap[key].push(input.id);
                    }
                    selector = `${key}${Constant.Tilde}${selectorMap[key].indexOf(input.id)}`;
                } else {
                    // In Beta mode, we continue to look at query selectors in context of the full page
                    selector = `${prefix}${input.tag}.${classes}${suffix}`;
                }
            }
            // Update selector to use "id" field when available. There are two exceptions:
            // (1) if "id" appears to be an auto generated string token, e.g. guid or a random id containing digits
            // (2) if "id" appears inside a shadow DOM, in which case we continue to prefix up to shadow DOM to prevent conflicts
            selector = id && filter(id) ? `${getDomPrefix(prefix)}${Constant.Hash}${id}` : selector;
            return selector;
        }
    }
}

function getDomPrefix(prefix: string): string {
    const shadowDomStart = prefix.lastIndexOf(Constant.ShadowDomTag);
    const iframeDomStart = prefix.lastIndexOf(`${Constant.IFramePrefix}${Constant.HTML}`);
    const domStart = Math.max(shadowDomStart, iframeDomStart);

    if (domStart < 0) {
        return Constant.Empty;
    }

    return prefix.substring(0, prefix.indexOf(Constant.Separator, domStart) + 1);
}

function getDomPath(input: string): string {
    const parts = input.split(Constant.Separator);
    for (let i = 0; i < parts.length; i++) {
        const tIndex = parts[i].indexOf(Constant.Tilde);
        const dIndex = parts[i].indexOf(Constant.Dot);
        parts[i] = parts[i].substring(0, dIndex > 0 ? dIndex : tIndex > 0 ? tIndex : parts[i].length);
    }
    return parts.join(Constant.Separator);
}

// Check if the given input string has digits or excluded class names
function filter(value: string): boolean {
    if (!value) {
        return false;
    } // Do not process empty strings
    if (excludeClassNames.some((x) => value.toLowerCase().indexOf(x) >= 0)) {
        return false;
    }
    for (let i = 0; i < value.length; i++) {
        const c = value.charCodeAt(i);
        if (c >= Character.Zero && c <= Character.Nine) {
            return false;
        }
    }
    return true;
}
