import { Privacy } from "@clarity-types/core";
import * as Data from "@clarity-types/data";
import * as Layout from "@clarity-types/layout";
import config from "@src/core/config";

const catchallRegex = /\S/gi;
const maxUrlLength = 2048;
let unicodeRegex = true;
let digitRegex = null;
let letterRegex = null;
let currencyRegex = null;

export function text(value: string, hint: string, privacy: Privacy, mangle: boolean = false, type?: string): string {
    if (value) {
        if (hint == "input" && (type === "checkbox" || type === "radio")) {
            return value;
        }

        switch (privacy) {
            case Privacy.None:
                return value;
            case Privacy.Sensitive:
                switch (hint) {
                    case Layout.Constant.TextTag:
                    case "value":
                    case "placeholder":
                    case "click":
                        return redact(value);
                    case "input":
                    case "change":
                        return mangleToken(value);
                }
                return value;
            case Privacy.Text:
            case Privacy.TextImage:
                switch (hint) {
                    case Layout.Constant.TextTag:
                    case Layout.Constant.DataAttribute:
                        return mangle ? mangleText(value) : mask(value);
                    case "src":
                    case "srcset":
                    case "title":
                    case "alt":
                    case "href":
                    case "xlink:href":
                        if (privacy === Privacy.TextImage) {
                            return value?.startsWith('blob:') ? 'blob:' : Data.Constant.Empty;
                        }
                        return value;
                    case "value":
                    case "click":
                    case "input":
                    case "change":
                        return mangleToken(value);
                    case "placeholder":
                        return mask(value);
                }
                break;
            case Privacy.Exclude:
                switch (hint) {
                    case Layout.Constant.TextTag:
                    case Layout.Constant.DataAttribute:
                        return mangle ? mangleText(value) : mask(value);
                    case "value":
                    case "input":
                    case "click":
                    case "change":
                        return Array(Data.Setting.WordLength).join(Data.Constant.Mask);
                    case "checksum":
                        return Data.Constant.Empty;
                }
                break;
            case Privacy.Snapshot:
                switch (hint) {
                    case Layout.Constant.TextTag:
                    case Layout.Constant.DataAttribute:
                        return scrub(value, Data.Constant.Letter, Data.Constant.Digit);
                    case "value":
                    case "input":
                    case "click":
                    case "change":
                        return Array(Data.Setting.WordLength).join(Data.Constant.Mask);
                    case "checksum":
                    case "src":
                    case "srcset":
                    case "alt":
                    case "title":
                        return Data.Constant.Empty;
                }
                break;
        }
    }
    return value;
}

export function url(input: string, electron: boolean = false, truncate: boolean = false): string {
    let result = input;
    // Replace the URL for Electron apps so we don't send back file:/// URL
    if (electron) {
        result = Data.Constant.HTTPS + Data.Constant.Electron;
    } else {
        let drop = config.drop;
        if (drop && drop.length > 0 && input && input.indexOf("?") > 0) {
            let [path, query] = input.split("?");
            let swap = Data.Constant.Dropped;
            result = path + "?" + query.split("&").map(p => drop.some(x => p.indexOf(x + "=") === 0) ? (p.split("=")[0] + "=" + swap) : p).join("&");
        }
    }

    if (truncate) {
        result = result.substring(0, maxUrlLength);
    }
    return result;
}

function mangleText(value: string): string {
    let trimmed = value.trim();
    if (trimmed.length > 0) {
        let first = trimmed[0];
        let index = value.indexOf(first);
        let prefix = value.substr(0, index);
        let suffix = value.substr(index + trimmed.length);
        return prefix + trimmed.length.toString(36) + suffix;
    }
    return value;
}

function mask(value: string): string {
    return value.replace(catchallRegex, Data.Constant.Mask);
}

export function scrub(value: string, letter: string, digit: string): string {
    regex(); // Initialize regular expressions
    return value ? value.replace(letterRegex, letter).replace(digitRegex, digit) : value;
}

function mangleToken(value: string): string {
    let length = ((Math.floor(value.length / Data.Setting.WordLength) + 1) * Data.Setting.WordLength);
    let output: string = Layout.Constant.Empty;
    for (let i = 0; i < length; i++) {
        output += i > 0 && i % Data.Setting.WordLength === 0 ? Data.Constant.Space : Data.Constant.Mask;
    }
    return output;
}

function regex(): void {
    // Initialize unicode regex, if supported by the browser
    // Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Unicode_Property_Escapes
    if (unicodeRegex && digitRegex === null) {
        try {
            digitRegex = new RegExp("\\p{N}", "gu");
            letterRegex = new RegExp("\\p{L}", "gu");
            currencyRegex = new RegExp("\\p{Sc}", "gu");
        } catch { unicodeRegex = false; }
    }
}

function redact(value: string): string {
    let spaceIndex = -1;
    let gap = 0;
    let hasDigit = false;
    let hasEmail = false;
    let hasWhitespace = false;
    let array = null;

    regex(); // Initialize regular expressions

    for (let i = 0; i < value.length; i++) {
        let c = value.charCodeAt(i);
        hasDigit = hasDigit || (c >= Data.Character.Zero && c <= Data.Character.Nine); // Check for digits in the current word
        hasEmail = hasEmail || c === Data.Character.At; // Check for @ sign anywhere within the current word
        hasWhitespace = c === Data.Character.Tab || c === Data.Character.NewLine || c === Data.Character.Return || c === Data.Character.Blank;

        // Process each word as an individual token to redact any sensitive information
        if (i === 0 || i === value.length - 1 || hasWhitespace) {
            // Performance optimization: Lazy load string -> array conversion only when required
            if (hasDigit || hasEmail) {
                if (array === null) { array = value.split(Data.Constant.Empty); }
                // Work on a token at a time so we don't have to apply regex to a larger string
                let token = value.substring(spaceIndex + 1, hasWhitespace ? i : i + 1);
                // Check if unicode regex is supported, otherwise fallback to calling mask function on this token
                if (unicodeRegex && currencyRegex !== null) {
                    // Do not redact information if the token contains a currency symbol
                    token = token.match(currencyRegex) ? token : scrub(token, Data.Constant.Letter, Data.Constant.Digit);
                } else {
                    token = mask(token);
                }
                // Merge token back into array at the right place
                array.splice(spaceIndex + 1 - gap, token.length, token);
                gap += token.length - 1;
            }
            // Reset digit and email flags after every word boundary, except the beginning of string
            if (hasWhitespace) {
                hasDigit = false;
                hasEmail = false;
                spaceIndex = i;
            }
        }
    }
    return array ? array.join(Data.Constant.Empty) : value;
}
