import { ExtractSource, Syntax, Type } from "@clarity-types/core";
import { Event, Setting, ExtractData } from "@clarity-types/data";
import encode from "./encode";
import * as internal from "@src/diagnostic/internal";
import { Code, Constant, Severity } from "@clarity-types/data";
import { hashText } from "@src/clarity";
import hash from "@src/core/hash";

export let data: ExtractData = {};
export let keys: Set<number> = new Set();

let variables : { [key: number]: { [key: number]: Syntax[] }} = {};
let selectors : { [key: number]: { [key: number]: string }} = {};
let hashes : { [key: number]: { [key: number]: string }} = {};
let validation : { [key: number]: string } = {};

export function start(): void {
    reset();
}

// Input string is of the following form:
// EXTRACT 101|element { "1": ".class1", "2": "~window.a.b", "3": "!abc"}
// if element is present on the page it will set up event 101 to grab the contents of the class1 selector into component 1,
// the javascript evaluated contents of window.a.b into component 2,
// and the contents of Clarity's hash abc into component 3
export function trigger(input: string): void { 
    try {
        var parts = input && input.length > 0 ? input.split(/ (.*)/) : [Constant.Empty];
        var keyparts = parts[0].split(/\|(.*)/);
        var key = parseInt(keyparts[0]);
        var element = keyparts.length > 1 ?  keyparts[1] : Constant.Empty;
        var values = parts.length > 1 ? JSON.parse(parts[1]) : {};
        variables[key] = {};
        selectors[key] = {};
        hashes[key] = {};
        validation[key] = element;
        for (var v in values) {
            // values is a set of strings for proper JSON parsing, but it's more efficient 
            // to interact with them as numbers
            let id = parseInt(v);
            let value = values[v] as string;
            let source = ExtractSource.Text;
            if (value.startsWith(Constant.Tilde)) {
                source = ExtractSource.Javascript
            } else if (value.startsWith(Constant.Bang)) {
                source = ExtractSource.Hash
            }
            switch (source) {
                case ExtractSource.Javascript:
                    let variable = value.slice(1);
                    variables[key][id] = parse(variable);
                    break;
                case ExtractSource.Text:
                    selectors[key][id] = value;
                    break;
                case ExtractSource.Hash:
                    let hash = value.slice(1);
                    hashes[key][id] = hash;
                    break;
            }
        }
    }
    catch(e) {
        internal.log(Code.Config, Severity.Warning, e ? e.name : null);
    }
}

export function clone(v: Syntax[]): Syntax[] {
    return JSON.parse(JSON.stringify(v));
}

export function compute(): void {
    try {
        for (let v in variables) {
            let key = parseInt(v);
            if (validation[key] == Constant.Empty || document.querySelector(validation[key]))
            {
                let variableData = variables[key];
                for (let v in variableData) {
                    let variableKey = parseInt(v);
                    let value = str(evaluate(clone(variableData[variableKey])));
                    if (value) { 
                        update(key, variableKey, value);
                    }
                }

                let selectorData = selectors[key];
                for (let s in selectorData) {
                    let shouldMask = false;
                    let selectorKey = parseInt(s);
                    let selector = selectorData[selectorKey];
                    if (selector.startsWith(Constant.At)){
                        shouldMask = true;
                        selector = selector.slice(1);
                    }
                    let nodes = document.querySelectorAll(selector) as NodeListOf<HTMLElement>;
                    if (nodes) {
                        let text = Array.from(nodes).map(e => {
                            if (e.tagName === "IMG") {
                                let img = e as HTMLImageElement;
                                return img.src || img.currentSrc || Constant.Empty;
                            }
                            return e.textContent;
                        }).join(Constant.Seperator);
                        update(key, selectorKey, (shouldMask ? hash(text).trim() : text).slice(0, Setting.ExtractLimit));
                    }
                }

                let hashData = hashes[key];
                for (let h in hashData) {
                    let hashKey = parseInt(h);
                    let content = hashText(hashData[hashKey]).trim().slice(0, Setting.ExtractLimit);
                    update(key, hashKey, content);
                }
            }
        }

        if (keys.size > 0) {
            encode(Event.Extract);
        }
    }
    catch (e) { internal.log(Code.Selector, Severity.Warning, e ? e.name : null); }
}

export function reset(): void {
    keys.clear();
}

export function update(key: number, subkey: number, value: string): void {
    var update = false;
    if (!(key in data)) {
        data[key] = {};
        update = true;
    }
    
    if (!isEmpty(hashes[key]) 
        && (!(subkey in data[key]) || data[key][subkey] != value))
    {
        update = true;
    }

    data[key][subkey] = value;
    if (update) {
        keys.add(key);
    }

    return;
}

export function stop(): void {
   reset();
}

function parse(variable: string): Syntax[] {
    let syntax: Syntax[] = [];
    let parts = variable.split(Constant.Dot);
    while (parts.length > 0) {
        let part = parts.shift();
        let arrayStart = part.indexOf(Constant.ArrayStart);
        let conditionStart = part.indexOf(Constant.ConditionStart);
        let conditionEnd = part.indexOf(Constant.ConditionEnd);
        syntax.push({
            name : arrayStart > 0 ? part.slice(0, arrayStart) : (conditionStart > 0 ? part.slice(0, conditionStart) : part),
            type : arrayStart > 0 ? Type.Array : (conditionStart > 0 ? Type.Object : Type.Simple),
            condition : conditionStart > 0 ? part.slice(conditionStart + 1, conditionEnd) : null
        });
    }

    return syntax;
}

// The function below takes in a variable name in following format: "a.b.c" and safely evaluates its value in javascript context
// For instance, for a.b.c, it will first check window["a"]. If it exists, it will recursively look at: window["a"]["b"] and finally,
// return the value for window["a"]["b"]["c"].
function evaluate(variable: Syntax[], base: Object = window): any {
    if (variable.length == 0) { return base; }
    let part = variable.shift();
    let output;
    if (base && base[part.name]) {
        let obj = base[part.name];
        if (part.type !== Type.Array && match(obj, part.condition)) {
            output = evaluate(variable, obj);
        }
        else if (Array.isArray(obj)) {
            let filtered = [];
            for (var value of obj) {
                if (match(value, part.condition)) {
                    let op = evaluate(variable, value)
                    if (op) { filtered.push(op); }
                }
            }
            output = filtered;
        }
        
        return output;
    }

    return null;
}

function str(input: string): string {
    // Automatically trim string to max of Setting.ExtractLimit to avoid fetching long strings
    return input ? JSON.stringify(input).slice(0, Setting.ExtractLimit) : input;
}

function match(base: Object, condition: string): boolean {
    if (condition) {
        let prop = condition.split(":");
        return prop.length > 1 ? base[prop[0]] == prop[1] : base[prop[0]]
    }

    return true;
}

function isEmpty(obj: Object): boolean {
    return Object.keys(obj).length == 0;
}
