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

const excludeClassNames = ExcludeClassNamesList;
let selectorMap: { [selector: string]: number[] } = {};

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

export function get(input: SelectorInput, type: Selector): string {
    let a = input.attributes;
    let prefix = input.prefix ? input.prefix[type] : null;
    let 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;
            let id = Constant.Id in a && a[Constant.Id].length > 0 ? a[Constant.Id] : null;
            let 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.
                    let key = getDomPath(prefix) + input.tag + Constant.Dot + classes;
                    if (!(key in selectorMap)) { selectorMap[key] = []; }
                    if (!selectorMap[key].includes(input.id)) { 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 {
    let parts = input.split(Constant.Separator);
    for (let i = 0; i < parts.length; i++) {
        let tIndex = parts[i].indexOf(Constant.Tilde);
        let 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().includes(x))) { return false; }
    for (let i = 0; i < value.length; i++) {
        let c = value.charCodeAt(i);
        if (c >= Character.Zero && c <= Character.Nine) { return false };
    }
    return true;
}
