akkd-common

Common functions

Dieses Skript sollte nicht direkt installiert werden. Es handelt sich hier um eine Bibliothek für andere Skripte, welche über folgenden Befehl in den Metadaten eines Skriptes eingebunden wird // @require https://update.greasyfork.org/scripts/474549/1263250/akkd-common.js

// ==UserScript==

// #region Info

// @namespace   https://greasyfork.org/en/users/1123632-93akkord
// @exclude     *
// @author      Michael Barros (https://greasyfork.org/en/users/1123632-93akkord)
// @icon        data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuNvyMY98AAA5DSURBVGhDtVr5T1zXGeVviPJDq/bHNq2ZN8MyrGEx4C2JY6tqo+SXRGoVKYqqSlXaqFUjNUlTRXWUWE5iXC/YmNWAjdmXGRgDNruNYciwG4aBYR0wY+x4AWbe1/PdN282PwO21HGO333bfed893zfve85Efx7q6zrpd9UDX32er295bDZ2f6GeVbgdZMT4G1Q2+zwbWfbD6F9SGydvu1s+0GcU3HAh5B2fTgc7QeALLSzGmfbM4GsRuwD+8RWOZaB84y06smW/RUTnx7MMb0kyB860/jT1xrvjO+3LFGWZZmymhcps3kB4C3vL9E+XzsT7UzLImU0LVEGjqVb5gFscVy0cV9a0wKlmucFXjUvUopZ2ed2MpCE/cRwmOYpqWGREoGERmwbcaxxnuJxPN60II7FNywIxKFtNK1Qaq19ZG92608iDtYO5WSBRNa1ZcpgQEQGxDCYNO/vBQTJHZAGpEIckw1HcpMiIBmEkkxLfiSCEBPUQrwpgDiIMjYuUYxpmWLNAO5NLx/PicismZrLsLhE9DMF+TABwN4wolpIw4ikgmTKjgIwCir55xKAyPsEGCGAkVA1MRuRWe9wZzSv+CzyNIRdAC3SwUiFfVJgHy3yDFVAcPS1SAcjOPpxIM8CYnkLEXEQEFcz5Y7IUgXARsLjKnGArbOb6DPYOikgqUWeoWUhLdKM4MirApi8H75RSKix7yxAi6wWhIAw0sF4UQFK9BeUyAcJ4DxIqMYI7K+3uzObXX7iDBH5XZIX3od9tEgzBHGAq08S9lXyuxGgkg8XwO0Y3B9fBQH7ICDDJ2C3CasiDYme6kteLfIMvwAQEsCx5xPA20VBWkWMELAMAWyhBghAFRKR1yC5HRQBin20yDMCApg0C9i9hQICAuQDgIAKhxtVaNK9F+S1CD4LPFmlgdTufL/w3LVfbW8nwCgEzLyoALXmaxNXoQgAOPLPIUAAbVWA6vtQAZwDYgSmnluAv2Q28RJh+wROMgeIP5cAgAUYcSycPCewP4l3OwK7tQ1DjbwgzFuI1CKrBZW4gkDV0RYwqQhI36UAts12FUfF/1uACiEgYwcBXGlEtQmxzdOkVTB5tR1sGy2ywQi3DkOLdDB8AqZ2ELCMycoF8iwCiauuebD/qtlFqajHDEGaI++LfrKPeDLO8Qgkm+boVZBKQvVIwHGxkGNhPtJCBKCQ5y28j+MxAFecuIYlHzAL7yRA+B1E0zFJHb3uoqNtynkeAbFo45nX5KTqmfv0w+ojKpx0UXLDrCiZwXVeRB4l9JOBVbLefUgDdx/RG/UOsfYPnH+WAJDEvTF4L/hL3yIN4l6+/6jJIcizMDGRZWAtxETTEWWOdBomNcUu8/Rep5Mey0SbRPRp7xyi5xTR57qeXDdLI2uPCaepY85FUUUDlFg/I84FC0isX6DjtmXy4rot4LXLt8hYOY3VpM8+PgEKEGGQM6LN5IWIeid9YV0Uz9nEX0eqhshYNYXzeNkRAuocbl7PpGEU0ppgB0EeD4fyCxNruE35tcy4yFDSBxs4QRJvULUzNIyICAFOF0m5PaQrGaSEBuQIHpwMCFJ1c/Q1CPDPAxws7SF98QAZq+/4vK++dYE87BYHESwgxjwHu6CMVjvo84EFkmUvbeFhh8ut4DFEUTXTEIAyml7rcLM1uDSqEH6uddD4+gYJhsD61hal5neQvnQIr34zlIQOhlYf+gXoLnSRruA2RV8aJIwq7aufpfRGPKTGTl/1z4lOPMCB4i4I6KOokn5KqR7DtTOUiWuT6+cFcUY8bJTR6KDfWpy0t2qE/tXH92ME8bAjLOCSjfZcGaPYKyPuiFQI4MkoFTf6yaODd6/N0hMZj8RNMv7y4s/HljGKzMPDy2yUUDFOw34By6Q/30VSwS26MOKkex6Z3F6ZTlqn5JjSH+ir27O4DhEUAjrJUNhLHzQN0+qml9bRwa3V+5RZOYJknaFUiM8bWxV98PXr2I6sPRAC0KQ3Lw+QoXgQAoZJX2LVFpCAhDw94hKPnFt/IPc55mXCEJqmFkm62CmISgU3aWjlR3QrU+fsMkWfa6Fsq528uI5RPmz3Rp+7Lu+52EvHeibFdTwC+ws76L2afnI/AT0Exu5+KB8qaJP1Rf1kuDJEZ8dWxHUcuU3gsdcr9jlQqoCoYivpcG2IABF5QCQhhn3kHicoiFinvMeuDcgIKN19vEnJBbBAQQ9F5naSzXWfA0NdM4t0vGcMyc53EJlGZ7zRZ8xe6WIHRV7spv90T4jrtkDoT9W95EI/TMZ1/0f5SH6rR3+xiyIL+yi1tJ/WNjhTvDSwuCof/G/lVkZ29Vbr9Dx3GyRgkKTwEVAFcPK+Y3HAPmwamf5Y2e19M9dCG2wjJtAE5XndpDvfTkOude6X7m8wITwDD+hyLHhjTpm8ugsdZMB1Ul4PHWMBOIf/aH1DifzKoyf0O5CXzt/AaPaQVHSLPjSNKpbFk/9ce1OWTlso8vQ1+mfrkDiuCtAjz0IF8MTE0cfCK7HBSdlDKFu4YxOkznUOyd/esNH9TcQXHVSMz5H+QjtJOTdo2HVPYeX78UAX2ablyJxWIdKQ30sScKxLGQH/D/c43A8oLbcdOYXrim+LxP779SnRHY/B0bJ20uXewEi30+cdd8RtqgAJ1U4qH0YyswCU0RSUUfY/z5BJSCKb+yFGUYzaU7/5xxuUgI6lnOs0vBwQ8ACjwKI9wD/aRklf2ENRhUquHOtWCPCIPsQIsJWYZOvsKkXhfNQlK2zRTx9fGxfHeb54pwLFIh8lN6+XvujkHFIEHLliJQmFgUdACEiBgGSenCCAZ8K3mh2wC5OXaXzprrdzet7TMb3gsc6tIA0Uj79fP0i6nDayCQEyTbrW5H0ny7Ym7q7jTpnWUF3erkC1KLqJfMEIqAJA4PdFTVv5N0fZjejLS9+hQhlAngW8c7WPNvgp6OWbW1MkIS+kgn46dtMu4sTC3kQZVQQgSEJArd0tpn/YJ75+jk7YlElnCx0dPGP26I5XyhKQ8F29d+0Jz8lEpaNOMpxtoUEIYEkd9gV5z7d19HbhNe/DTXgcKifcP1JKaS9s1OXPAa7j+842yTHf18jWxRXWQI/wnA8tmJiK+siIwjDkWhMjeW/TQ6dQ1U72zdLqI8xH6IAFvHEVFajUBgFjsBIEvFrjcCci+rywiqudxprlEXOkkZV78p7sGvr1KQvtQTL9KtuMMqpMKAsPNsh45poQwKW2bXaJ9KfMpMu20Il2m5dtxOSq7asUndtLxzkHcIAtcCDvOhlON9ORglavSGiInQfBQ1d6hNh3r/TK95BvXDp5xGTctIX5iIsp93u4oh8jgAQun8CkOsgCZhQBsM/euikqGJjyFg9MeP5ad9OrQ4XQ53aRgcscqs77Vb1ykXXSkzcw6dmfd0P+un3YW2S94/mybcgbhZzQc0U53SSf7B7xXMJ1BcDh8pvyH6r75IKBO558XJuc3ylK6x4Ugg9rur3FuKbIOuX5pGVYjobn9+R10eGCZk9J/4Snd8blvYrzH1V0eopwfyHa6aWw1eWxYAEOWMj3RRjrm1isM3TnWlEmQQYPYg9HFcLLXFEuYBI710bS2TYyMGG0BXLacR4Jh4SMhGAujXq+5hy2EC/BGhKukc53wNPoB8mtB6IwexvOtWOL80WwGy8xYCUpF8/F9bqTzbLhdIvvOddxXTdyhf0P+6gCkqodbl4RipUh1iOxdQ4ylo+L5NEjicQMiQQzFN0WFUWX302RIKsTwHyALR/n83ydHlsdCwFRHSch9wHouCJha8BCzoA6rkcp5L6lQvQL6EtwHMlpgL/1l/g4RBbwvXwfAghxhhL4//II6Zjf1Tt8vTsisdoOAYFXPv4Gn9CAlSAWayhTYuXI4AdzpeCaLUgIoHpcwnEug6WDAYCcAYgqsQE/iGOiLyZ9GRNhmQ9oS1hX6QDDZZRFkNNzdbk8inYopCsgLcDRHyd9BQSUQUAS1tQJsI8qIBjGOqdIGAkkWL0gBiIKuA1ygmQo9BzFcDBxkNQzySBwOZTKgwmGgqOtQro6IWBA9PUVkwEB7H8tAQLICz1EcESVIeaIMnFt8gYghDiiKyDIKwKkYAgBiKoGeYEwARx5Ju8XkLyTAMCIty8DD7mPlBZxRjh5cY+IPCMo6oDwMrfZElrEVfjJB4iHCAjPAUbgFS8AI146+MHhAjjpGE8LCCUdTpyhLwN5lERN4oAg7o98KPkQAeo/pG0nIN48T3F1sBMeHC5AFaW2BUT0txOAZBUCQkkHI9j3OwrQJB0E9VuNERUqumyEohFhRihpIMwuKoIjL5J2F4mrZRuByiky4MU+uszmjkjAi3GwAIWo8nUgFPx9RkE8EjsKQ294UQHC99rW2ck2AhCgDwjgEQiNsraAoM98EByDnIjiqvLCAkKJq9jJNgLBAuIr7G7lQxIT931Q2iWi6+ZBCGR4rmCUhdb5ENLCNtsTV8kHl8pQgHiFHQKmIWAaAoYDApgQR3i7j6laMNY78YINYiAvITe0BajkdxP57cj7UAkRVXYIGIWFKibmOPoxjfzNMfDdcdeA9aKR2KJec2V5SsAosLvIP5s8A6SFALQrAVgo5pLVGRF9sTc3FgKYPP/jmSbJnYCciMVkJ10FEV+F4TWNUmmeTV7Ab5tgsloICJCqsV/lwOr0xvmIX/zt2M+ji/sn+csv/6OBMgrPCb6PRcBO+gpE0i9gB/IMLIt3K0ACDLBOFPwfe773zi8/Ov4z8X+svPLBNy8bjtd8GZXbbonJ7+18EURf7OrRn2m16k+1WA3Zptv6E3WD0vfm29KJepv03dPQAZHfY3uywSadbNwWOgGTLTK72Rp9utUifVP571c++OzliIiIiP8B4VfIttF+iOkAAAAASUVORK5CYII=

// #endregion Info

// ==UserLibrary==

// @name        akkd-common
// @description Common functions
// @copyright   2022+, Michael Barros (https://greasyfork.org/en/users/1123632-93akkord)
// @license     CC-BY-NC-SA-4.0; https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode
// @license     GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0.txt
// @version     0.0.20

// ==/UserScript==

// ==/UserLibrary==

// ==OpenUserJS==

// @author      93Akkord

// ==/OpenUserJS==

/// <reference path='C:/Users/mbarros/Documents/DevProjects/Web/Tampermonkey/Palantir/@types/__fullReferencePaths__.js' />

/*

# akkd-common

A collection of commonly used classes and functions.

## Required requires:

- https://code.jquery.com/jquery-3.2.1.min.js
- https://greasyfork.org/scripts/474546-loglevel/code/loglevel.js

*/

// #region Events

// Setup location change events
/**
 *
 * Example usage:
 * ```javascript
 * window.addEventListener('popstate', () => {
 *     window.dispatchEvent(new Event('locationchange'));
 * });
 */
(() => {
    class LocationChangeEvent extends Event {
        constructor(type, prevUrl, newUrl) {
            super(type);

            this.prevUrl = prevUrl;
            this.newUrl = newUrl;
        }
    }

    let prevUrl = document.location.href;
    let oldPushState = history.pushState;

    history.pushState = function pushState() {
        let ret = oldPushState.apply(this, arguments);
        let newUrl = document.location.href;

        window.dispatchEvent(new LocationChangeEvent('pushstate', prevUrl, newUrl));
        window.dispatchEvent(new LocationChangeEvent('locationchange', prevUrl, newUrl));

        prevUrl = newUrl;

        return ret;
    };

    let oldReplaceState = history.replaceState;

    history.replaceState = function replaceState() {
        let ret = oldReplaceState.apply(this, arguments);
        let newUrl = document.location.href;

        window.dispatchEvent(new LocationChangeEvent('replacestate', prevUrl, newUrl));
        window.dispatchEvent(new LocationChangeEvent('locationchange', prevUrl, newUrl));

        prevUrl = newUrl;

        return ret;
    };

    window.addEventListener('popstate', () => {
        let newUrl = document.location.href;

        window.dispatchEvent(new LocationChangeEvent('locationchange', prevUrl, newUrl));

        prevUrl = newUrl;
    });
})();

// #endregion Events

// #region Helper Classes

class Logger {
    /**
     * Creates an instance of Logger.
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {Window} _window
     * @param {string | null} devTag
     * @memberof Logger
     */
    constructor(_window = null, devTag = null) {
        /**
         * @type {Window}
         * @private
         */
        this.window = _window || getWindow();

        /** @type {string | null} */
        this.devTag = devTag;

        /** @type {string[]} */
        this._additionalTags = [];
    }

    /**
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @type {string[]}
     * @public
     * @memberof Logger
     */
    get additionalTags() {
        return this._additionalTags;
    }

    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {string[]} value
     * @memberof Logger
     */
    set additionalTags(value) {
        if (getType(value) != 'array') {
            value = [value];
        }

        this._additionalTags = value;
    }

    /** @type {string} */
    get label() {
        return [].concat([this.devTag], this.additionalTags).join(' ');
    }

    /** @type {(...data: any[]) => void} */
    get log() {
        if (this.devTag) {
            return console.log.bind(console, `${this.label}`);
        } else {
            return console.log.bind(console);
        }
    }

    /** @type {(...data: any[]) => void} */
    get info() {
        if (this.devTag) {
            return console.info.bind(console, `${this.label}`);
        } else {
            return console.info.bind(console);
        }
    }

    /** @type {(...data: any[]) => void} */
    get error() {
        if (this.devTag) {
            return console.error.bind(console, `${this.label}`);
        } else {
            return console.error.bind(console);
        }
    }

    /** @type {(...data: any[]) => void} */
    get debug() {
        if (this.devTag) {
            return console.debug.bind(console, `${this.label}`);
        } else {
            return console.debug.bind(console);
        }
    }

    /** @type {(...data: any[]) => void} */
    get warn() {
        if (this.devTag) {
            return console.warn.bind(console, `${this.label}`);
        } else {
            return console.warn.bind(console);
        }
    }

    /**
     * Maybe use later?
     *
     * @memberof Logger
     */
    _setupFunctions() {
        let self = this;
        let funcs = ['log', 'info', 'error', 'debug', 'warn'];

        for (let i = 0; i < funcs.length; i++) {
            let func = funcs[i];

            self[func] = function () {
                let args = [...arguments];

                if (self.devTag) args.unshift(self.label);

                self.window.console.debug.bind(self.window.console, ...args);
            };
        }
    }
}

class Base64 {
    static keyStr = 'ABCDEFGHIJKLMNOP' + 'QRSTUVWXYZabcdef' + 'ghijklmnopqrstuv' + 'wxyz0123456789+/' + '=';

    /**
     *
     *
     * @static
     * @param {string} input
     * @returns {string}
     * @memberof Base64
     */
    static encode(input) {
        input = escape(input);

        let output = '';

        let chr1;
        let chr2;
        let chr3 = '';

        let enc1;
        let enc2;
        let enc3;
        let enc4 = '';

        let i = 0;

        do {
            chr1 = input.charCodeAt(i++);
            chr2 = input.charCodeAt(i++);
            chr3 = input.charCodeAt(i++);
            enc1 = chr1 >> 2;
            enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
            enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
            enc4 = chr3 & 63;

            if (isNaN(chr2)) {
                enc3 = enc4 = 64;
            } else if (isNaN(chr3)) {
                enc4 = 64;
            }

            output = output + Base64.keyStr.charAt(enc1) + Base64.keyStr.charAt(enc2) + Base64.keyStr.charAt(enc3) + Base64.keyStr.charAt(enc4);
            chr1 = chr2 = chr3 = '';
            enc1 = enc2 = enc3 = enc4 = '';
        } while (i < input.length);

        return output;
    }

    /**
     *
     *
     * @static
     * @param {string} input
     * @returns {string}
     * @memberof Base64
     */
    static decode(input) {
        let output = '';

        let chr1;
        let chr2;
        let chr3 = '';

        let enc1;
        let enc2;
        let enc3;
        let enc4 = '';

        let i = 0;

        let base64test = /[^A-Za-z0-9\+\/\=]/g;

        if (base64test.exec(input)) {
            throw new Error(`There were invalid base64 characters in the input text. Valid base64 characters are: ['A-Z', 'a-z', '0-9,' '+', '/', '=']`);
        }

        input = input.replace(/[^A-Za-z0-9\+\/\=]/g, '');

        do {
            enc1 = Base64.keyStr.indexOf(input.charAt(i++));
            enc2 = Base64.keyStr.indexOf(input.charAt(i++));
            enc3 = Base64.keyStr.indexOf(input.charAt(i++));
            enc4 = Base64.keyStr.indexOf(input.charAt(i++));
            chr1 = (enc1 << 2) | (enc2 >> 4);
            chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
            chr3 = ((enc3 & 3) << 6) | enc4;
            output = output + String.fromCharCode(chr1);

            if (enc3 != 64) output = output + String.fromCharCode(chr2);

            if (enc4 != 64) output = output + String.fromCharCode(chr3);

            chr1 = chr2 = chr3 = '';
            enc1 = enc2 = enc3 = enc4 = '';
        } while (i < input.length);

        return unescape(output);
    }
}

class MultiRegExp {
    constructor(baseRegExp) {
        const { regexp, groupIndexMapper, previousGroupsForGroup } = this._fillGroups(baseRegExp);

        this.regexp = regexp;
        this.groupIndexMapper = groupIndexMapper;
        this.previousGroupsForGroup = previousGroupsForGroup;
    }

    execForAllGroups(str, includeFullMatch) {
        let matches = RegExp.prototype.exec.call(this.regexp, str);

        if (!matches) return matches;

        let firstIndex = matches.index;
        let indexMapper = includeFullMatch
            ? Object.assign(
                  {
                      0: 0,
                  },
                  this.groupIndexMapper
              )
            : this.groupIndexMapper;
        let previousGroups = includeFullMatch
            ? Object.assign(
                  {
                      0: [],
                  },
                  this.previousGroupsForGroup
              )
            : this.previousGroupsForGroup;

        let res = Object.keys(indexMapper).map((group) => {
            let mapped = indexMapper[group];
            let match = matches[mapped];
            let start = firstIndex + previousGroups[group].reduce((sum, i) => sum + (matches[i] ? matches[i].length : 0), 0);
            let end = start + (matches[mapped] ? matches[mapped].length : 0);
            let lineColumnStart = LineColumnFinder(str).fromIndex(start);
            let lineColumnEnd = LineColumnFinder(str).fromIndex(end - 1);

            return {
                match,
                start,
                end,
                startLineNumber: lineColumnStart.line,
                startColumnNumber: lineColumnStart.col,
                endLineNumber: lineColumnEnd.line,
                endColumnNumber: lineColumnEnd.col,
            };
        });

        return res;
    }

    execForGroup(string, group) {
        const matches = RegExp.prototype.exec.call(this.regexp, string);

        if (!matches) return matches;

        const firstIndex = matches.index;

        const mapped = group == 0 ? 0 : this.groupIndexMapper[group];
        const previousGroups = group == 0 ? [] : this.previousGroupsForGroup[group];

        let r = {
            match: matches[mapped],
            start: firstIndex + previousGroups.reduce((sum, i) => sum + (matches[i] ? matches[i].length : 0), 0),
        };

        r.end = r.start + (matches[mapped] ? matches[mapped].length : 0);

        return r;
    }

    /**
     * Adds brackets before and after a part of string
     * @param str string the hole regex string
     * @param start int marks the position where ( should be inserted
     * @param end int marks the position where ) should be inserted
     * @param groupsAdded int defines the offset to the original string because of inserted brackets
     * @return {string}
     */
    _addGroupToRegexString(str, start, end, groupsAdded) {
        start += groupsAdded * 2;
        end += groupsAdded * 2;

        return str.substring(0, start) + '(' + str.substring(start, end + 1) + ')' + str.substring(end + 1);
    }

    /**
     * converts the given regex to a regex where all not captured string are going to be captured
     * it along sides generates a mapper which maps the original group index to the shifted group offset and
     * generates a list of groups indexes (including new generated capturing groups)
     * which have been closed before a given group index (unshifted)
     *
     * Example:
     * regexp: /a(?: )bc(def(ghi)xyz)/g => /(a(?: )bc)((def)(ghi)(xyz))/g
     * groupIndexMapper: {'1': 2, '2', 4}
     * previousGroupsForGroup: {'1': [1], '2': [1, 3]}
     *
     * @param regex RegExp
     * @return {{regexp: RegExp, groupIndexMapper: {}, previousGroupsForGroup: {}}}
     */
    _fillGroups(regex) {
        let regexString;
        let modifier;

        if (regex.source && regex.flags) {
            regexString = regex.source;
            modifier = regex.flags;
        } else {
            regexString = regex.toString();
            modifier = regexString.substring(regexString.lastIndexOf(regexString[0]) + 1); // sometimes order matters ;)
            regexString = regexString.substr(1, regex.toString().lastIndexOf(regexString[0]) - 1);
        }

        // regexp is greedy so it should match (? before ( right?
        // brackets may be not quoted by \
        // closing bracket may look like: ), )+, )+?, ){1,}?, ){1,1111}?
        const tester = /(\\\()|(\\\))|(\(\?)|(\()|(\)(?:\{\d+,?\d*}|[*+?])?\??)/g;

        let modifiedRegex = regexString;

        let lastGroupStartPosition = -1;
        let lastGroupEndPosition = -1;
        let lastNonGroupStartPosition = -1;
        let lastNonGroupEndPosition = -1;
        let groupsAdded = 0;
        let groupCount = 0;
        let matchArr;
        const nonGroupPositions = [];
        const groupPositions = [];
        const groupNumber = [];
        let currentLengthIndexes = [];
        const groupIndexMapper = {};
        const previousGroupsForGroup = {};

        while ((matchArr = tester.exec(regexString)) !== null) {
            if (matchArr[1] || matchArr[2]) {
                // ignore escaped brackets \(, \)
            }

            if (matchArr[3]) {
                // non capturing group (?
                let index = matchArr.index + matchArr[0].length - 1;

                lastNonGroupStartPosition = index;
                nonGroupPositions.push(index);
            } else if (matchArr[4]) {
                // capturing group (
                let index = matchArr.index + matchArr[0].length - 1;
                let lastGroupPosition = Math.max(lastGroupStartPosition, lastGroupEndPosition);

                // if a (? is found add ) before it
                if (lastNonGroupStartPosition > lastGroupPosition) {
                    // check if between ) of capturing group lies a non capturing group
                    if (lastGroupPosition < lastNonGroupEndPosition) {
                        // add groups for x1 and x2 on (?:()x1)x2(?:...
                        if (lastNonGroupEndPosition - 1 - (lastGroupPosition + 1) > 0) {
                            modifiedRegex = this._addGroupToRegexString(modifiedRegex, lastGroupPosition + 1, lastNonGroupEndPosition - 1, groupsAdded);
                            groupsAdded++;
                            lastGroupEndPosition = lastNonGroupEndPosition - 1; // imaginary position as it is not in regex but modifiedRegex
                            currentLengthIndexes.push(groupCount + groupsAdded);
                        }

                        if (lastNonGroupStartPosition - 1 - (lastNonGroupEndPosition + 1) > 0) {
                            modifiedRegex = this._addGroupToRegexString(modifiedRegex, lastNonGroupEndPosition + 1, lastNonGroupStartPosition - 2, groupsAdded);
                            groupsAdded++;
                            lastGroupEndPosition = lastNonGroupStartPosition - 1; // imaginary position as it is not in regex but modifiedRegex
                            currentLengthIndexes.push(groupCount + groupsAdded);
                        }
                    } else {
                        modifiedRegex = this._addGroupToRegexString(modifiedRegex, lastGroupPosition + 1, lastNonGroupStartPosition - 2, groupsAdded);
                        groupsAdded++;
                        lastGroupEndPosition = lastNonGroupStartPosition - 1; // imaginary position as it is not in regex but modifiedRegex
                        currentLengthIndexes.push(groupCount + groupsAdded);
                    }

                    // if necessary also add group between (? and opening bracket
                    if (index > lastNonGroupStartPosition + 2) {
                        modifiedRegex = this._addGroupToRegexString(modifiedRegex, lastNonGroupStartPosition + 2, index - 1, groupsAdded);
                        groupsAdded++;
                        lastGroupEndPosition = index - 1; // imaginary position as it is not in regex but modifiedRegex
                        currentLengthIndexes.push(groupCount + groupsAdded);
                    }
                } else if (lastGroupPosition < index - 1) {
                    modifiedRegex = this._addGroupToRegexString(modifiedRegex, lastGroupPosition + 1, index - 1, groupsAdded);
                    groupsAdded++;
                    lastGroupEndPosition = index - 1; // imaginary position as it is not in regex but modifiedRegex
                    currentLengthIndexes.push(groupCount + groupsAdded);
                }

                groupCount++;
                lastGroupStartPosition = index;
                groupPositions.push(index);
                groupNumber.push(groupCount + groupsAdded);
                groupIndexMapper[groupCount] = groupCount + groupsAdded;
                previousGroupsForGroup[groupCount] = currentLengthIndexes.slice();
            } else if (matchArr[5]) {
                // closing bracket ), )+, )+?, ){1,}?, ){1,1111}?
                let index = matchArr.index + matchArr[0].length - 1;

                if ((groupPositions.length && !nonGroupPositions.length) || groupPositions[groupPositions.length - 1] > nonGroupPositions[nonGroupPositions.length - 1]) {
                    if (lastGroupStartPosition < lastGroupEndPosition && lastGroupEndPosition < index - 1) {
                        modifiedRegex = this._addGroupToRegexString(modifiedRegex, lastGroupEndPosition + 1, index - 1, groupsAdded);
                        groupsAdded++;

                        //lastGroupEndPosition = index - 1; will be set anyway
                        currentLengthIndexes.push(groupCount + groupsAdded);
                    }

                    groupPositions.pop();
                    lastGroupEndPosition = index;

                    let toPush = groupNumber.pop();
                    currentLengthIndexes.push(toPush);
                    currentLengthIndexes = currentLengthIndexes.filter((index) => index <= toPush);
                } else if (nonGroupPositions.length) {
                    nonGroupPositions.pop();
                    lastNonGroupEndPosition = index;
                }
            }
        }

        return {
            regexp: new RegExp(modifiedRegex, modifier),
            groupIndexMapper,
            previousGroupsForGroup,
        };
    }
}

class MoveableElement {
    /**
     * Creates an instance of MoveableElement.
     * @param {HTMLElement} element
     * @param {boolean} requireKeyDown
     * @memberof MoveableElement
     */
    constructor(element, requireKeyDown) {
        this.element = element;
        this.requireKeyDown = requireKeyDown || false;
        this.handleMouseDown = this.handleMouseDown.bind(this);
        this.handleMouseUp = this.handleMouseUp.bind(this);
        this.handleMouseMove = this.handleMouseMove.bind(this);

        this.moving = false;
        this.keyPressed = false;
        this.originalCursor = getStyle(this.element, 'cursor');

        this.setupEvents();
    }

    setupEvents() {
        if (!document.body) {
            setTimeout(() => {
                this.setupEvents();
            }, 250);
        } else {
            document.body.addEventListener('keydown', (ev) => {
                if (ev.which == '17') {
                    this.keyPressed = true;
                }
            });

            document.body.addEventListener('keyup', (ev) => {
                this.keyPressed = false;
            });
        }
    }

    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {MouseEvent} ev
     * @memberof MoveableElement
     */
    handleMouseDown(ev) {
        if (this.keyPressed || !this.requireKeyDown) {
            ev.preventDefault();

            this.element.style.cursor = 'move';

            this.changePointerEvents('none');

            document.body.removeEventListener('mouseup', this.handleMouseUp);
            document.body.addEventListener('mouseup', this.handleMouseUp);

            document.body.removeEventListener('mousemove', this.handleMouseMove);
            document.body.removeEventListener('mouseleave', this.handleMouseUp);

            document.body.addEventListener('mousemove', this.handleMouseMove);
            document.body.addEventListener('mouseleave', this.handleMouseUp);

            try {
                document.querySelectorAll('iframe')[0].style.pointerEvents = 'none';
            } catch (error) {}
        }
    }

    changePointerEvents(value) {
        for (let i = 0; i < this.element.children.length; i++) {
            const child = this.element.children[i];

            child.style.pointerEvents = value;
        }
    }

    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {MouseEvent} ev
     * @memberof MoveableElement
     */
    handleMouseUp(ev) {
        this.moving = false;
        this.element.style.cursor = this.originalCursor;
        this.changePointerEvents('auto');

        document.body.removeEventListener('mouseup', this.handleMouseUp);
        document.body.removeEventListener('mousemove', this.handleMouseMove);
        document.body.removeEventListener('mouseleave', this.handleMouseUp);

        try {
            document.querySelectorAll('iframe')[0].style.pointerEvents = '';
        } catch (error) {}
    }

    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {MouseEvent} ev
     * @memberof MoveableElement
     */
    handleMouseMove(ev) {
        this.moving = true;

        let top = ev.clientY - getStyle(this.element, 'height') / 2;
        let bottom = ev.clientX - getStyle(this.element, 'width') / 2;

        this.element.style.top = `${top}px`;
        this.element.style.left = `${bottom}px`;
    }

    padCoord(coord) {
        return coord.toString().padStart(5, ' ');
    }

    init() {
        this.element.addEventListener('mousedown', this.handleMouseDown);
    }
}

class CurrentLine {
    /**
     * @typedef ILineInfo
     * @prop {string} method
     * @prop {number} line
     * @prop {string} file
     * @prop {string} filename
     */

    /**
     * Returns a single item
     *
     * @param {number} [level] Useful to return levels up on the stack. If not informed, the first (0, zero index) element of the stack will be returned
     * @returns {ILineInfo}
     */
    get(level = 0) {
        const stack = getStack();
        const i = Math.min(level + 1, stack.length - 1);
        const item = stack[i];
        const result = CurrentLine.parse(item);

        return result;
    }

    /**
     * Returns all stack
     *
     * @returns {ILineInfo[]}
     */
    all() {
        const stack = getStack();
        const result = [];

        for (let i = 1; i < stack.length; i++) {
            const item = stack[i];

            result.push(CurrentLine.parse(item));
        }

        return result;
    }

    /**
     *
     *
     * @param {NodeJS.CallSite} item
     * @returns {ILineInfo}
     */
    static parse(item) {
        const result = {
            method: item.getMethodName() || item.getFunctionName(),
            line: item.getLineNumber(),
            file: item.getFileName() || item.getScriptNameOrSourceURL(),
        };

        result.filename = result.file ? result.file.replace(/^.*\/|\\/gm, '').replace(/\.\w+$/gm, '') : null;

        return result;
    }
}

/**
 *
 *
 * @returns {NodeJS.CallSite[]}
 */
function getStack() {
    const orig = Error.prepareStackTrace;

    Error.prepareStackTrace = function (_, stack) {
        return stack;
    };

    const err = new Error();

    Error.captureStackTrace(err, arguments.callee);

    const stack = err.stack;

    Error.prepareStackTrace = orig;

    return stack;
}

class ProgressTimer {
    /**
     * Creates an instance of ProgressTimer.
     * @param {number} total
     * @memberof ProgressTimer
     */
    constructor(total) {
        this.startTime;
        this.total = total;
        this.loaded = 0;
        this.estimatedFinishDt = '';
        this.progressMessage = '';
    }

    /**
     *
     *
     * @memberof ProgressTimer
     */
    start() {
        this.startTime = new Date();
    }

    /**
     *
     *
     * @param {number} loaded
     * @param {string} msg
     * @memberof ProgressTimer
     */
    updateProgress(loaded, msg) {
        this.loaded = loaded;

        this.progress = `${((this.loaded * 100) / this.total).toFixed(2)}%`;
        this.timeRemaining = this._estimatedTimeRemaining(this.startTime, this.loaded, this.total);
        this.downloaded = `${this.loaded}/${this.total}`;
        this.completionTime = `${this._dateToISOLikeButLocal(this.estimatedFinishDt)}`;
        this.totalRuntime = `${this._ms2Timestamp(this.timeTaken)}`;

        this.updateProgressMessage(msg);
        this.printProgress();
    }

    /**
     *
     *
     * @param {string} msg
     * @memberof ProgressTimer
     */
    updateProgressMessage(msg) {
        let msgLines = [];

        msgLines.push(`      completed: ${this.progress}`);
        msgLines.push(`     downloaded: ${this.downloaded}`);
        msgLines.push(`  total runtime: ${this.totalRuntime}`);
        msgLines.push(` time remaining: ${this.timeRemaining}`);
        msgLines.push(`completion time: ${this.completionTime}`);

        if (msg) {
            msgLines.push(msg);
        }

        this.progressMessage = msgLines.join('\n');
    }

    /**
     *
     *
     * @memberof ProgressTimer
     */
    printProgress() {
        console.clear();
        console.debug(this.progressMessage);
    }

    /**
     *
     *
     * @param {Date} startTime
     * @param {number} itemsProcessed
     * @param {number} totalItems
     * @returns {string}
     * @memberof ProgressTimer
     */
    _estimatedTimeRemaining(startTime, itemsProcessed, totalItems) {
        // if (itemsProcessed == 0) {
        //     return '';
        // }

        let currentTime = new Date();
        this.timeTaken = currentTime - startTime;
        this.timeLeft = itemsProcessed == 0 ? this.timeTaken * (totalItems - itemsProcessed) : (this.timeTaken / itemsProcessed) * (totalItems - itemsProcessed);
        this.estimatedFinishDt = new Date(currentTime.getTime() + this.timeLeft);

        return this._ms2Timestamp(this.timeLeft);
    }

    /**
     *
     *
     * @param {number} ms
     * @returns {string}
     * @memberof ProgressTimer
     */
    _ms2Timestamp(ms) {
        // 1- Convert to seconds:
        let seconds = ms / 1000;

        // 2- Extract hours:
        let hours = parseInt(seconds / 3600); // 3,600 seconds in 1 hour
        seconds = seconds % 3600; // seconds remaining after extracting hours

        // 3- Extract minutes:
        let minutes = parseInt(seconds / 60); // 60 seconds in 1 minute

        // 4- Keep only seconds not extracted to minutes:
        seconds = seconds % 60;

        let parts = seconds.toString().split('.');

        seconds = parseInt(parts[0]);
        let milliseconds = parts.length > 1 ? parts[1].substring(0, 3).padEnd(3, 0) : '000';

        hours = hours.toString().padStart(2, '0');
        minutes = minutes.toString().padStart(2, '0');
        seconds = seconds.toString().padStart(2, '0');

        return `${hours}:${minutes}:${seconds}.${milliseconds}`; // hours + ':' + minutes + ':' + seconds;
    }

    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {Date} date
     * @returns {string}
     * @memberof ProgressTimer
     */
    _dateToISOLikeButLocal(date) {
        let offsetMs = date.getTimezoneOffset() * 60 * 1000;
        let msLocal = date.getTime() - offsetMs;
        let dateLocal = new Date(msLocal);
        let iso = dateLocal.toISOString();
        let isoLocal = iso.slice(0, 19);

        return isoLocal.replace(/T/g, ' ');
    }
}

class Benchmark {
    constructor({ logger, printResults } = {}) {
        this.namedPerformances = {};
        this.defaultName = 'default';
        this.logger = logger;
        this.printResults = printResults == undefined ? (this.logger ? true : false) : printResults;
    }

    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {string=} name
     * @memberof Benchmark
     */
    start(name) {
        name = name || this.defaultName;

        this.namedPerformances[name] = {
            startAt: this._hrtime(),
        };
    }

    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {string=} name
     * @memberof Benchmark
     */
    stop(name) {
        name = name || this.defaultName;

        const startAt = this.namedPerformances[name] && this.namedPerformances[name].startAt;

        if (!startAt) throw new Error(`Namespace: ${name} doesnt exist`);

        const diff = this._hrtime(startAt);
        const time = diff[0] * 1e3 + diff[1] * 1e-6;
        const words = this.getWords(diff);
        const preciseWords = this.getPreciseWords(diff);
        const verboseWords = this.getVerboseWords(diff);
        const verboseAbbrWords = this.getVerboseAbbrWords(diff);

        if (this.printResults) {
            let output = name != 'default' ? `[${name}] execution time:` : `execution time:`;

            this.logger(output, time); // words
        }

        return {
            name,
            time,
            words,
            preciseWords,
            verboseWords,
            verboseAbbrWords,
            diff,
        };
    }

    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {T} func
     * @param {{name: string, measure: boolean}=} { name, measure }
     * @returns {T}
     * @memberof Benchmark
     * @template T
     */
    wrapFunc(func, { name, measure = true } = {}) {
        name = this._getFuncName(func, name);

        let self = this;

        wrappedFunc.measure = measure;
        wrappedFunc.benchmark = {
            name,
            results: {
                runs: [],
                avg: null,
                min: null,
                max: null,
                total: null,
                times: null,
                runCount: 0,
            },
            reset: function () {
                this.results.runs = [];
                this.results.avg = null;
                this.results.min = null;
                this.results.max = null;
                this.results.total = null;
                this.results.times = null;
                this.results.runCount = 0;
            },
            printResults: function (logger = console.debug) {
                let output = this.name != 'default' ? `[${this.name}] execution summary:` : `execution summary:`;

                let times = wrappedFunc.benchmark.results.runs.map((run) => {
                    return run.time;
                });

                wrappedFunc.benchmark.results.times = times;
                wrappedFunc.benchmark.results.avg = self._getAvgTime(times);
                wrappedFunc.benchmark.results.total = self._getSumTime(times);
                wrappedFunc.benchmark.results.min = Math.min(...times);
                wrappedFunc.benchmark.results.max = Math.max(...times);

                logger(output, `times: ${this.results.runCount}    total: ${self.getWords(this.results.total)}    min: ${self.getWords(this.results.min)}    max: ${self.getWords(this.results.max)}    avg: ${self.getWords(this.results.avg)}    total: ${self.getWords(this.results.total)}`);
            },
        };

        function wrappedFunc() {
            if (wrappedFunc.measure) {
                self.start(name);
            }

            let res = func(...arguments);

            if (wrappedFunc.measure) {
                wrappedFunc.benchmark.results.runCount++;

                wrappedFunc.benchmark.results.runs.push(self.stop(name));
            }

            return res;
        }

        return this._defineWrappedFuncProperties(wrappedFunc, name);
    }

    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {T} func
     * @param {{name: string, measure: boolean}=} { name, measure }
     * @returns {T}
     * @memberof Benchmark
     * @template T
     */
    wrapAsyncFunc(func, { name, measure = true } = {}) {
        name = this._getFuncName(func, name);

        let self = this;

        wrappedFunc.measure = measure;
        wrappedFunc.benchmark = {
            name,
            results: {
                runs: [],
                avg: null,
                min: null,
                max: null,
                total: null,
                times: null,
                runCount: 0,
            },
            reset: function () {
                this.results.runs = [];
                this.results.avg = null;
                this.results.min = null;
                this.results.max = null;
                this.results.total = null;
                this.results.times = null;
                this.results.runCount = 0;
            },
            printResults: function (logger = console.debug) {
                let output = this.name != 'default' ? `[${this.name}] execution summary:` : `execution summary:`;

                let times = wrappedFunc.benchmark.results.runs.map((run) => {
                    return run.time;
                });

                wrappedFunc.benchmark.results.times = times;
                wrappedFunc.benchmark.results.avg = self._getAvgTime(times);
                wrappedFunc.benchmark.results.total = self._getSumTime(times);
                wrappedFunc.benchmark.results.min = Math.min(...times);
                wrappedFunc.benchmark.results.max = Math.max(...times);

                logger(output, `times: ${this.results.runCount}    total: ${self.getWords(this.results.total)}    min: ${self.getWords(this.results.min)}    max: ${self.getWords(this.results.max)}    avg: ${self.getWords(this.results.avg)}    total: ${self.getWords(this.results.total)}`);
            },
        };

        async function wrappedFunc() {
            if (wrappedFunc.measure) {
                self.start(name);
            }

            let res = await func(...arguments);

            if (wrappedFunc.measure) {
                wrappedFunc.benchmark.results.runCount++;

                wrappedFunc.benchmark.results.runs.push(self.stop(name));
            }

            return res;
        }

        return this._defineWrappedFuncProperties(wrappedFunc, name);
    }

    getWords(diff) {
        let ms = typeof diff === 'number' ? diff : this._hrtime2Ms(diff);
        
        return this._msToHms(ms);

        // return this._prettyHrtime(diff);
    }

    getPreciseWords(diff) {
        return this._prettyHrtime(diff, { precise: true });
    }

    getVerboseWords(diff) {
        return this._prettyHrtime(diff, { verbose: true });
    }

    getVerboseAbbrWords(diff) {
        return this._prettyHrtime(diff, { verbose: true, verboseAbbrv: true, precise: true });
    }

    _msToHms(ms) {        
        // Extract days
        let days = Math.floor(ms / (86400 * 1000));
        ms %= (86400 * 1000);
        
        // Extract hours
        let hours = Math.floor(ms / (3600 * 1000));
        ms %= (3600 * 1000);
        
        // Extract minutes
        let minutes = Math.floor(ms / (60 * 1000));
        ms = ms % (60 * 1000);

        // Extract seconds
        let seconds = Math.floor(ms / 1000);
        ms = ms % 1000;

        let result = [];
        
        if (days > 0) {
            result.push(`${days} day${days === 1 ? '' : 's'}`);
        }
        
        if (hours > 0) {
            result.push(`${hours} hr${hours === 1 ? '' : 's'}`);
        }
        
        if (minutes > 0) {
            result.push(`${minutes} min${minutes === 1 ? '' : 's'}`);
        }
        
        if (seconds > 0) {
            result.push(`${seconds} sec${seconds === 1 ? '' : 's'}`);
        }

        if (ms > 0) {
            result.push(`${ms} ms`);
        }

        return result.join(', ');
    }

    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {number[][]} times
     * @returns {number}
     */
    _getAvgTime(times) {
        return this._getSumTime(times) / times.length;
    }

    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {number[][]} times
     * @returns {number}
     */
    _getSumTime(times) {
        return times.reduce((a, b) => a + b);
    }

    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {number} ms
     * @returns {number[][]}
     * @memberof Benchmark
     */
    _ms2Hrtime(ms) {
        let seconds = Math.round(ms / 1000);
        let nanoSeconds = Math.round(ms * 1000000 - seconds * 1000000 * 1000);

        return [seconds, nanoSeconds];
    }

    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {[number, number]} hrtime
     * @returns {number}
     * @memberof Benchmark
     */
    _hrtime2Ms(hrtime) {
        return hrtime[0] * 1000 + hrtime[1] / 1000000;
    }

    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {T} func
     * @param {string=} name
     * @returns {T}
     * @memberof Benchmark
     * @template T
     */
    _getFuncName(func, name) {
        return name ? name : 'name' in func && func.name.trim() !== '' ? func.name : '[wrapped.func]';
    }

    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {Function} wrappedFunc
     * @param {string} name
     * @returns {Function}
     * @memberof Benchmark
     */
    _defineWrappedFuncProperties(wrappedFunc, name) {
        Object.defineProperty(wrappedFunc, 'name', {
            value: name,
            writable: false,
            configurable: false,
            enumerable: false,
        });

        Object.defineProperty(wrappedFunc, 'toString', {
            value: () => func.toString(),
            writable: false,
            configurable: false,
            enumerable: false,
        });

        return wrappedFunc;
    }

    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {[number, number]=} time
     * @returns {[number, number]}
     * @memberof Benchmark
     */
    _hrtime(time) {
        if (typeof process !== 'undefined') return process.hrtime(time);

        var performance = typeof performance !== 'undefined' ? performance : {};
        let performanceNow = performance.now || performance.mozNow || performance.msNow || performance.oNow || performance.webkitNow || (() => new Date().getTime());

        let clocktime = performanceNow.call(performance) * 1e-3;
        let seconds = Math.floor(clocktime);
        let nanoseconds = Math.floor((clocktime % 1) * 1e9);

        if (time) {
            seconds = seconds - time[0];
            nanoseconds = nanoseconds - time[1];

            if (nanoseconds < 0) {
                seconds--;
                nanoseconds += 1e9;
            }
        }

        return [seconds, nanoseconds];
    }

    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {[number, number]=} time
     * @param {{verbose: boolean; verboseAbbrv: boolean; precise: boolean}} { verbose = false, verboseAbbrv = false, precise = false }
     * @returns {string}
     * @memberof Benchmark
     */
    _prettyHrtime(time, { verbose = false, verboseAbbrv = false, precise = false } = {}) {
        let i, spot, sourceAtStep, valAtStep, decimals, strAtStep, results, totalSeconds;

        let minimalDesc = ['h', 'min', 's', 'ms', 'μs', 'ns'];
        let verboseDesc = !verboseAbbrv ? ['hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond'] : minimalDesc;
        let convert = [60 * 60, 60, 1, 1e6, 1e3, 1];

        if (typeof time === 'number') {
            time = this._ms2Hrtime(time);
        }

        if (!Array.isArray(time) || time.length !== 2) return '';

        if (typeof time[0] !== 'number' || typeof time[1] !== 'number') return '';

        // normalize source array due to changes in node v5.4+
        if (time[1] < 0) {
            totalSeconds = time[0] + time[1] / 1e9;
            time[0] = parseInt(totalSeconds);
            time[1] = parseFloat((totalSeconds % 1).toPrecision(9)) * 1e9;
        }

        results = '';

        for (i = 0; i < 6; i++) {
            // grabbing first or second spot in source array
            spot = i < 3 ? 0 : 1;
            sourceAtStep = time[spot];

            if (i !== 3 && i !== 0) {
                // trim off previous portions
                sourceAtStep = sourceAtStep % convert[i - 1];
            }

            if (i === 2) {
                // get partial seconds from other portion of the array
                sourceAtStep += time[1] / 1e9;
            }

            // val at this unit
            valAtStep = sourceAtStep / convert[i];

            if (valAtStep >= 1) {
                if (verbose) {
                    // deal in whole units, subsequent laps will get the decimal portion
                    valAtStep = Math.floor(valAtStep);
                }

                if (!precise) {
                    // don't fling too many decimals
                    decimals = valAtStep >= 10 ? 0 : 2;
                    strAtStep = valAtStep.toFixed(decimals);
                } else {
                    strAtStep = valAtStep.toString();
                }

                if (strAtStep.indexOf('.') > -1 && strAtStep[strAtStep.length - 1] === '0') {
                    // remove trailing zeros
                    strAtStep = strAtStep.replace(/\.?0+$/, '');
                }

                if (results) {
                    // append space if we have a previous value
                    results += ' ';
                }

                // append the value
                results += strAtStep;

                // append units
                if (verbose) {
                    results += verboseAbbrv ? `${verboseDesc[i]}` : ` ${verboseDesc[i]}`;

                    if (!verboseAbbrv && strAtStep !== '1') {
                        results += 's';
                    }
                } else {
                    results += ` ${minimalDesc[i]}`;
                }

                if (!verbose) {
                    // verbose gets as many groups as necessary, the rest get only one
                    break;
                }
            }
        }

        return results;
    }
}

class ArrayStat {
    /**
     * Creates an instance of ArrayStat.
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {number[]} array
     * @memberof ArrayStat
     */
    constructor(array) {
        this.array = array;
    }

    _getCloned() {
        return this.array.slice(0);
    }

    min() {
        return Math.min.apply(null, this.array);
    }

    max() {
        return Math.max.apply(null, this.array);
    }

    range() {
        return this.max(this.array) - this.min(this.array);
    }

    midrange() {
        return this.range(this.array) / 2;
    }

    sum(array) {
        array = array || this.array;

        let total = 0;

        for (let i = 0, l = array.length; i < l; i++) total += array[i];

        return total;
    }

    mean(array) {
        array = array || this.array;

        return this.sum(array) / array.length;
    }

    median() {
        let array = this._getCloned();

        array.sort(function (a, b) {
            return a - b;
        });

        let mid = array.length / 2;

        return mid % 1 ? array[mid - 0.5] : (array[mid - 1] + array[mid]) / 2;
    }

    modes() {
        if (!this.array.length) return [];

        let modeMap = {};
        let maxCount = 0;
        let modes = [];

        this.array.forEach(function (val) {
            if (!modeMap[val]) modeMap[val] = 1;
            else modeMap[val]++;

            if (modeMap[val] > maxCount) {
                modes = [val];
                maxCount = modeMap[val];
            } else if (modeMap[val] === maxCount) {
                modes.push(val);

                maxCount = modeMap[val];
            }
        });

        return modes;
    }

    letiance() {
        let mean = this.mean();

        return this.mean(
            this._getCloned().map(function (num) {
                return Math.pow(num - mean, 2);
            })
        );
    }

    standardDeviation() {
        return Math.sqrt(this.letiance());
    }

    meanAbsoluteDeviation() {
        let mean = this.mean();

        return this.mean(
            this._getCloned().map(function (num) {
                return Math.abs(num - mean);
            })
        );
    }

    zScores() {
        let mean = this.mean();
        let standardDeviation = this.standardDeviation();

        return this._getCloned().map(function (num) {
            return (num - mean) / standardDeviation;
        });
    }

    withinStd(val, stdev) {
        let low = this.mean() - stdev * this.standardDeviation(); // x.deviation;
        let hi = this.mean() + stdev * this.standardDeviation(); // x.deviation;
        let res = val > low && val < hi;

        console.log(`val: ${val.toString().padEnd(5, ' ')}    mean: ${this.mean()}    stdev: ${this.standardDeviation()}    hi: ${hi}    low: ${low}    res: ${res}`);

        return res;
    }
}

memoizeClass(ArrayStat);

let LineColumnFinder = (function LineColumnFinder() {
    let isArray = Array.isArray;
    let isObject = (val) => val != null && typeof val === 'object' && Array.isArray(val) === false;
    let slice = Array.prototype.slice;

    /**
     * Finder for index and line-column from given string.
     *
     * You can call this without `new` operator as it returns an instance anyway.
     *
     * @class
     * @param {string} str - A string to be parsed.
     * @param {Object|number} [options] - Options.
     *     This can be an index in the string for shorthand of `lineColumn(str, index)`.
     * @param {number} [options.origin=1] - The origin value of line and column.
     */
    function LineColumnFinder(str, options) {
        if (!(this instanceof LineColumnFinder)) {
            if (typeof options === 'number') {
                return new LineColumnFinder(str).fromIndex(options);
            }

            return new LineColumnFinder(str, options);
        }

        this.str = str || '';
        this.lineToIndex = buildLineToIndex(this.str);

        options = options || {};

        this.origin = typeof options.origin === 'undefined' ? 1 : options.origin;
    }

    /**
     * Find line and column from index in the string.
     *
     * @param  {number} index - Index in the string. (0-origin)
     * @return {Object|null}
     *     Found line number and column number in object `{ line: X, col: Y }`.
     *     If the given index is out of range, it returns `null`.
     */
    LineColumnFinder.prototype.fromIndex = function (index) {
        if (index < 0 || index >= this.str.length || isNaN(index)) {
            return null;
        }

        let line = findLowerIndexInRangeArray(index, this.lineToIndex);

        return {
            line: line + this.origin,
            col: index - this.lineToIndex[line] + this.origin,
        };
    };

    /**
     * Find index from line and column in the string.
     *
     * @param  {number|Object|Array} line - Line number in the string.
     *     This can be an Object of `{ line: X, col: Y }`, or
     *     an Array of `[line, col]`.
     * @param  {number} [column] - Column number in the string.
     *     This must be omitted or undefined when Object or Array is given
     *     to the first argument.
     * @return {number}
     *     Found index in the string. (always 0-origin)
     *     If the given line or column is out of range, it returns `-1`.
     */
    LineColumnFinder.prototype.toIndex = function (line, column) {
        if (typeof column === 'undefined') {
            if (isArray(line) && line.length >= 2) {
                return this.toIndex(line[0], line[1]);
            }

            if (isObject(line) && 'line' in line && ('col' in line || 'column' in line)) {
                return this.toIndex(line.line, 'col' in line ? line.col : line.column);
            }

            return -1;
        }

        if (isNaN(line) || isNaN(column)) {
            return -1;
        }

        line -= this.origin;
        column -= this.origin;

        if (line >= 0 && column >= 0 && line < this.lineToIndex.length) {
            let lineIndex = this.lineToIndex[line];
            let nextIndex = line === this.lineToIndex.length - 1 ? this.str.length : this.lineToIndex[line + 1];

            if (column < nextIndex - lineIndex) {
                return lineIndex + column;
            }
        }

        return -1;
    };

    /**
     * Build an array of indexes of each line from a string.
     *
     * @private
     * @param   str {string}  An input string.
     * @return  {number[]}    Built array of indexes. The key is line number.
     */
    function buildLineToIndex(str) {
        let lines = str.split('\n');
        let lineToIndex = new Array(lines.length);
        let index = 0;

        for (let i = 0, l = lines.length; i < l; i++) {
            lineToIndex[i] = index;
            index += lines[i].length + /* "\n".length */ 1;
        }

        return lineToIndex;
    }

    /**
     * Find a lower-bound index of a value in a sorted array of ranges.
     *
     * Assume `arr = [0, 5, 10, 15, 20]` and
     * this returns `1` for `value = 7` (5 <= value < 10),
     * and returns `3` for `value = 18` (15 <= value < 20).
     *
     * @private
     * @param  arr   {number[]} An array of values representing ranges.
     * @param  value {number}   A value to be searched.
     * @return {number} Found index. If not found `-1`.
     */
    function findLowerIndexInRangeArray(value, arr) {
        if (value >= arr[arr.length - 1]) {
            return arr.length - 1;
        }

        let min = 0,
            max = arr.length - 2,
            mid;

        while (min < max) {
            mid = min + ((max - min) >> 1);

            if (value < arr[mid]) {
                max = mid - 1;
            } else if (value >= arr[mid + 1]) {
                min = mid + 1;
            } else {
                // value >= arr[mid] && value < arr[mid + 1]
                min = mid;
                break;
            }
        }

        return min;
    }

    return LineColumnFinder;
})();

class CustomContextMenu {
    /**
     * Example menuItems
     *
     * ```javascript
     * let menuItems = [
     *    {
     *        type: 'item',
     *        label: 'Test1',
     *        onClick: () => {
     *            alert('test1');
     *        },
     *    },
     *    {
     *        type: 'item',
     *        label: 'Test2',
     *        onClick: () => {
     *            console.debug('test2');
     *        },
     *    },
     *    {
     *        type: 'break',
     *    },
     *    {
     *        type: 'item',
     *        label: 'Test3',
     *        onClick: () => {
     *            console.debug('test3');
     *        },
     *    },
     * ];
     *   ```
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {HTMLElement} elemToAttachTo
     * @param {*} menuItems
     * @memberof CustomContextMenu
     */
    constructor(elemToAttachTo, menuItems, onContextMenu) {
        this.elem = elemToAttachTo;
        this.menuItems = menuItems;
        this.menu = null;
        this.onContextMenu = onContextMenu;

        this._createMenu();
        this._setupEvents();

        this.hide = debounce(this.hide.bind(this), 500, true);
    }

    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {number} top
     * @param {number} left
     * @memberof CustomContextMenu
     */
    show(top, left) {
        document.body.appendChild(this.menu);

        this.menu.style.display = 'block';

        this.menu.style.top = `${top}px`;
        this.menu.style.left = `${left}px`;

        this.menu.setAttribute('tabindex', '0');
        this.menu.focus();
    }

    hide() {
        this.menu.style.display = 'none';

        if (document.body.contains(this.menu)) {
            this.menu.remove();
        }
    }

    _setupEvents() {
        this.elem.addEventListener('contextmenu', (ev) => {
            ev.preventDefault();

            if (this.onContextMenu) {
                this.onContextMenu(ev);
            }

            this.show(ev.pageY, ev.pageX);
        });

        document.addEventListener('click', (ev) => {
            if (document.body.contains(this.menu) && !this._isHover(this.menu)) {
                this.hide();
            }
        });

        window.addEventListener('blur', (ev) => {
            this.hide();
        });

        this.menu.addEventListener('blur', (ev) => {
            this.hide();
        });
    }

    _createMenu() {
        this.menu = this._createMenuContainer();

        for (let i = 0; i < this.menuItems.length; i++) {
            let itemConfig = this.menuItems[i];

            switch (itemConfig.type) {
                case 'item':
                    this.menu.appendChild(this._createItem(itemConfig));

                    break;

                case 'break':
                    this.menu.appendChild(this._createBreak());

                    break;

                default:
                    break;
            }
        }

        // document.body.appendChild(this.menu);
    }

    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @returns {HTMLElement}
     * @memberof CustomContextMenu
     */
    _createMenuContainer() {
        let html = `<div class="context" hidden></div>`;

        let elem = this._createElementsFromHTML(html);

        return elem;
    }

    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {*} itemConfig
     * @returns {HTMLElement}
     * @memberof CustomContextMenu
     */
    _createItem(itemConfig) {
        let html = `<div class="context_item">
    <div class="inner_item">
        ${itemConfig.label}
    </div>
</div>`;

        let elem = this._createElementsFromHTML(html);

        if (itemConfig.id) {
            elem.id = itemConfig.id;
        }

        if (itemConfig.onClick) {
            elem.addEventListener('click', (ev) => {
                itemConfig.onClick(ev);

                this.hide();
            });
        }

        return elem;
    }

    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @returns {HTMLElement}
     * @memberof CustomContextMenu
     */
    _createBreak() {
        let html = `<div class="context_hr"></div>`;

        let elem = this._createElementsFromHTML(html);

        return elem;
    }

    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {string} htmlStr
     * @returns {HTMLElement}
     */
    _createElementsFromHTML(htmlStr) {
        let div = document.createElement('div');

        div.innerHTML = htmlStr.trim();

        return div.firstChild;
    }

    _isHover(elem) {
        return elem.parentElement.querySelector(':hover') === elem;
    }
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @class LocalStorageEx
 */
class LocalStorageEx {
    /**
     * Creates an instance of LocalStorageEx.
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @memberof LocalStorageEx
     */
    constructor() {
        this.__storage = localStorage;
    }

    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @readonly
     * @memberof LocalStorageEx
     */
    get UNDEFINED_SAVED_VALUE() {
        return '__** undefined **__';
    }

    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @readonly
     * @memberof LocalStorageEx
     */
    get size() {
        let total = 0;

        for (let x in this.__storage) {
            // Value is multiplied by 2 due to data being stored in `utf-16` format, which requires twice the space.
            let amount = this.__storage[x].length * 2;

            if (!isNaN(amount) && this.__storage.hasOwnProperty(x)) {
                total += amount;
            }
        }

        return total;
    }

    /**
     * Determine if browser supports local storage.
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @returns {boolean}
     * @memberof LocalStorageEx
     */
    isSupported() {
        return typeof Storage !== 'undefined';
    }

    /**
     * Check if key exists in local storage.
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {*} key
     * @returns {boolean}
     * @memberof LocalStorageEx
     */
    has(key) {
        if (typeof key === 'object') {
            key = JSON.stringify(key);
        }

        return this.__storage.hasOwnProperty(key);
    }

    /**
     * Retrieve an object from local storage.
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {*} key
     * @param {*} [defaultValue=null]
     * @returns {*}
     * @memberof LocalStorageEx
     */
    get(key, defaultValue = null) {
        if (typeof key === 'object') {
            key = JSON.stringify(key);
        }

        if (!this.has(key)) {
            return defaultValue;
        }

        let item = this.__storage.getItem(key);

        try {
            if (item === '__** undefined **__') {
                return undefined;
            } else {
                return JSON.parse(item);
            }
        } catch (error) {
            return item;
        }
    }

    /**
     * Save some value to local storage.
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {string} key
     * @param {*} value
     * @returns {void}
     * @memberof LocalStorageEx
     */
    set(key, value) {
        if (typeof key === 'object') {
            key = JSON.stringify(key);
        }

        if (value === undefined) {
            value = this.UNDEFINED_SAVED_VALUE;
        } else if (typeof value === 'object') {
            value = JSON.stringify(value);
        }

        this.__storage.setItem(key, value);
    }

    /**
     * Remove element from local storage.
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {*} key
     * @returns {void}
     * @memberof LocalStorageEx
     */
    remove(key) {
        if (typeof key === 'object') {
            key = JSON.stringify(key);
        }

        this.__storage.removeItem(key);
    }

    toString() {
        return JSON.parse(JSON.stringify(this.__storage));
    }
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @class SessionStorageEx
 * @extends {LocalStorageEx}
 */
class SessionStorageEx extends LocalStorageEx {
    /**
     * Creates an instance of SessionStorageEx.
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @memberof SessionStorageEx
     */
    constructor() {
        super();

        this.__storage = sessionStorage;
    }
}

class IgnoreCaseMap extends Map {
    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {string} key
     * @param {*} value
     * @returns {this}
     * @memberof IgnoreCaseMap
     */
    set(key, value) {
        return super.set(key.toLocaleLowerCase(), value);
    }

    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {string} key
     * @returns {*}
     * @memberof IgnoreCaseMap
     */
    get(key) {
        return super.get(key.toLocaleLowerCase());
    }
}

// #endregion Helper Classes

// #region Helper Functions

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {string} name
 * @param {{logLevel: log.LogLevelDesc, tag: string}} logLevel
 * @return {log.Logger}
 */
function getLogger(name, { logLevel, tag }) {
    prefix.reg(log);

    const colors = {
        TRACE: '220;86;220',
        DEBUG: '86;86;220',
        INFO: '134;134;221',
        WARN: '220;220;86',
        ERROR: '220;86;86',
    };

    /** @type {prefix.LoglevelPluginPrefixOptions} */
    let options = {
        // template: tag ? `[%t] %l [${tag}] %n:` : '[%t] %l %n:',
        levelFormatter: function (level) {
            return level.toUpperCase();
        },
        nameFormatter: function (name) {
            return name || 'root';
        },
        timestampFormatter: function (date) {
            return date.toTimeString().replace(/.*(\d{2}:\d{2}:\d{2}).*/, '$1');
        },
        format: function (level, name, timestamp) {
            let _timestamp = `\x1B[90m[${timestamp}]\x1B[m`;
            let _level = `\x1B[38;2;${colors[level.toUpperCase()]}m${level.toUpperCase()}\x1B[m`;
            let _name = `\x1B[38;2;38;177;38m${tag ? `[${tag}-` : '['}${name}]\x1B[m`;

            let _format = `${_timestamp} ${_level} ${_name}:`;

            return _format;
        },
    };

    const logger = log.getLogger(name);

    prefix.apply(logger, options);

    logger.setLevel(logLevel || 'WARN');

    return logger;
}

function pp(obj, fn) {
    fn = fn || console.log;

    fn(pformat(obj));
}

function pformat(obj, space = 4) {
    return JSON.stringify(obj, null, space);
}

function removeAllButLastStrPattern(string, token) {
    let parts = string.split(token);

    if (parts[1] === undefined) return string;
    else return parts.slice(0, -1).join('') + token + parts.slice(-1);
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {Array.<T> | Array} arr
 * @param {?function(T, T): boolean} callbackObjs
 * @return {T[]}
 * @template T
 */
function dedupeArr(arr, callbackObjs) {
    if (callbackObjs) {
        let tempArr = /** @type {[]} */ (arr).filter((value, index) => {
            return (
                index ===
                arr.findIndex((other) => {
                    return callbackObjs(value, other);
                })
            );
        });

        return tempArr;
    } else {
        return [...new Set(arr)];
    }
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {any} obj
 * @returns {boolean}
 */
function isClass(obj) {
    return typeof obj === 'function' && /^\s*class\s+/.test(obj.toString());
}

/**
 * Checks whether a variable is a class or an instance created with `new`.
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {*} value The variable to check.
 * @returns {boolean} `true` if the variable is a class or an instance created with `new`, `false` otherwise.
 */
function isClassOrInstance(value) {
    // prettier-ignore
    if (typeof value === 'function' &&
        value.prototype &&
        typeof value.prototype.constructor === 'function' &&
        value.prototype.constructor !== Array &&
        value.prototype.constructor !== Object) {
        return true; // It's a class
    } else if (typeof value === 'object' &&
               value.constructor &&
               typeof value.constructor === 'function' &&
               value.constructor.prototype &&
               typeof value.constructor.prototype.constructor === 'function' &&
               value.constructor !== Array &&
               value.constructor !== Object) {
        return true; // It's an instance created with new
    }

    return false;
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {*} value The variable to check.
 * @returns {boolean}
 */
function isFunction(value) {
    try {
        return typeof value == 'function';
    } catch (error) {
        return false;
    }
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {object} obj
 * @param {{ propsToExclude?: string[]; namesOnly: boolean; removeDuplicates: boolean; asObject: boolean }} [{ propsToExclude = [], namesOnly = false, removeDuplicates = true, asObject = false } = {}]
 * @returns
 */
function getObjProps(obj, { propsToExclude = [], namesOnly = false, removeDuplicates = true, asObject = false } = {}) {
    // Default
    let _propsToExclude = [
        //
        '__defineGetter__',
        '__defineSetter__',
        '__lookupSetter__',
        '__lookupGetter__',
        '__proto__',
        '__original__',

        'caller',
        'callee',
        'arguments',

        'toString',
        'valueOf',
        'constructor',
        'hasOwnProperty',
        'isPrototypeOf',
        'propertyIsEnumerable',
        'toLocaleString',
    ];

    _propsToExclude = propsToExclude && Array.isArray(propsToExclude) ? _propsToExclude.concat(propsToExclude) : _propsToExclude;

    let objHierarchy = getObjHierarchy(obj);
    let propNames = getPropNames(objHierarchy);
    let plainObj = {};
    let objKeys = [];

    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {any} obj
     * @returns {Array<any>}
     */
    function getObjHierarchy(obj) {
        let objs = [obj];

        obj = isClassOrInstance(obj) ? obj.prototype || obj.__proto__ : obj;

        do {
            objs.push(obj);
        } while ((obj = Object.getPrototypeOf(obj)));

        return objs;
    }

    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {Array<any>} objHierarchy
     * @returns {string[]}
     */
    function getPropNames(objHierarchy) {
        /** @type {string[]} */
        let propNames = [];

        for (let i = 0; i < objHierarchy.length; i++) {
            const _obj = objHierarchy[i];

            let getPropFuncs = [Object.getOwnPropertyNames, Object.getOwnPropertySymbols];

            getPropFuncs.forEach((func) => {
                let _propNames = func(_obj);

                _propNames.forEach((propName) => {
                    if (!_propsToExclude.includes(propName) && !propNames.includes(propName)) {
                        propNames.push(propName);
                    }
                });
            });
        }

        return propNames;
    }

    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {{ name: string, value: any }[]} props
     * @return {{ name: string, value: any }[]}
     */
    function dedupeProps(props) {
        function findNonNullProp(props, name) {
            let res = props.find((prop) => prop.name == name && prop.value != null);

            if (!res) {
                res = props.find((prop) => prop.name == name);
            }

            return res;
        }

        function propsContains(props, name) {
            return props.some((prop) => prop.name == name);
        }

        let newProps = [];

        for (let i = 0; i < props.length; i++) {
            const prop = props[i];

            let tempProp = findNonNullProp(props, prop.name);

            if (!propsContains(newProps, tempProp.name)) {
                newProps.push(tempProp);
            }
        }

        return newProps;
    }

    function getProps(objHierarchy, doFuncs = false) {
        /** @type {{ name: string, value: any }} */
        let props = [];

        for (let o = 0; o < objHierarchy.length; o++) {
            const _obj = objHierarchy[o];

            for (let p = 0; p < propNames.length; p++) {
                const propName = propNames[p];
                let value;

                try {
                    value = _obj[propName];
                } catch (error) {}

                if (!_propsToExclude.includes(propName)) {
                    if (asObject) {
                        if (!objKeys.includes(propName)) {
                            objKeys.push(propName);

                            plainObj[propName] = value;
                        }
                    } else {
                        props.push({
                            name: propName,
                            value: value,
                        });
                    }
                }
            }
        }

        if (!asObject) {
            if (removeDuplicates) {
                props = dedupeProps(props);
            }

            props = props.filter(function (prop, i, props) {
                let exprs = [
                    //
                    !_propsToExclude.includes(prop.name),
                    // props[i + 1] && prop.name != props[i + 1].name,
                    ...(doFuncs ? [isFunction(prop.value)] : [!isFunction(prop.value)]),
                ];

                return exprs.every(Boolean);
            });
        }

        if (asObject) {
            return plainObj;
        } else {
            return props.sort(function (a, b) {
                let aName = typeof a.name == 'symbol' ? a.name.toString() : a.name;
                let bName = typeof b.name == 'symbol' ? b.name.toString() : b.name;

                if (aName < bName) return -1;
                if (aName > bName) return 1;

                return 0;
            });
        }
    }

    let res;

    if (asObject) {
        getProps(objHierarchy, true);
        getProps(objHierarchy);

        res = plainObj;
    } else {
        res = {
            funcs: getProps(objHierarchy, true),
            props: getProps(objHierarchy),
        };

        if (namesOnly) {
            res.funcs = res.funcs.filter((func) => func.name.toString() != 'Symbol(Symbol.hasInstance)').map((func) => func.name);
            res.props = res.props.filter((prop) => prop.name.toString() != 'Symbol(Symbol.hasInstance)').map((prop) => prop.name);
        }
    }

    objHierarchy = null;

    return res;
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {Window} [_window=window]
 * @param {{ namesOnly: boolean; asObject: boolean }} [{ namesOnly = false, asObject = false } = {}]
 * @returns
 */
function getUserDefinedGlobalProps(_window = null, { namesOnly = false, asObject = false } = {}) {
    _window = _window || getWindow();

    let iframe = document.createElement('iframe');

    iframe.style.display = 'none';

    document.body.appendChild(iframe);

    let plainObj = {};
    let objKeys = [];

    function getProps(obj, doFuncs = false) {
        let props = [];
        let _obj = obj;

        let getPropFuncs = [Object.getOwnPropertyNames, Object.getOwnPropertySymbols];

        getPropFuncs.forEach((func) => {
            let propNames = func(_obj);

            for (let i = 0; i < propNames.length; i++) {
                const propName = propNames[i];
                let value;

                try {
                    value = _obj[propName];
                } catch (error) {}

                if (isNumber(propName) && value?.constructor?.name == 'Window') continue;

                if (!iframe.contentWindow.hasOwnProperty(propName)) {
                    if (asObject) {
                        if (!objKeys.includes(propName)) {
                            objKeys.push(propName);

                            plainObj[propName] = value;
                        }
                    } else {
                        props.push({
                            name: propName,
                            value: value,
                        });
                    }
                }
            }
        });

        if (!asObject) {
            props = props.filter(function (prop, i, props) {
                let propName1 = prop.name;
                let propName2 = props[i + 1] ? props[i + 1].name : undefined;
                let propValue1 = prop.value;
                let propValue2 = props[i + 1] ? props[i + 1].value : undefined;

                let exprs = [
                    //
                    // props[i + 1] && propName1 != propName2,
                    (props[i + 1] && propName1.constructor.name == 'Symbol' && propName2.constructor.name == 'Symbol' && propValue1 != propValue2) || propName1 != propName2,
                    ...(doFuncs ? [isFunction(obj[propName1])] : [!isFunction(obj[propName1])]),
                ];

                return exprs.every(Boolean);
            });
        }

        if (asObject) {
            return plainObj;
        } else {
            return props.sort(function (a, b) {
                let aName = typeof a.name == 'symbol' ? a.name.toString() : a.name;
                let bName = typeof b.name == 'symbol' ? b.name.toString() : b.name;

                if (aName < bName) return -1;
                if (aName > bName) return 1;

                return 0;
            });
        }
    }

    let res;

    if (asObject) {
        getProps(_window, true);
        getProps(_window);

        res = plainObj;
    } else {
        res = {
            funcs: getProps(_window, true),
            props: getProps(_window),
        };

        if (namesOnly) {
            res.funcs = res.funcs.filter((func) => func.name.toString() != 'Symbol(Symbol.hasInstance)').map((func) => func.name);
            res.props = res.props.filter((prop) => prop.name.toString() != 'Symbol(Symbol.hasInstance)').map((prop) => prop.name);
        }
    }

    document.body.removeChild(iframe);

    return res;
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {T} obj
 * @param {T | boolean} thisArg
 * @returns {T}
 * @template T
 */
function storeObjOriginalFuncs(obj, thisArg = true) {
    let props = getObjProps(obj);

    obj.__original__ = {};

    for (let i = 0; i < props.funcs.length; i++) {
        const func = props.funcs[i];

        if (thisArg == true) {
            obj.__original__[func.name] = func.value.bind(obj);
        } else {
            obj.__original__[func.name] = thisArg != false && thisArg != null && thisArg != undefined ? func.value.bind(thisArg) : func.value;
        }
    }

    return obj;
}

function printProps(obj, title) {
    let headerFooterBanner = '*********************************************************';

    console.log(headerFooterBanner);
    console.log(`* ${title || ''}`);
    console.log(headerFooterBanner);

    for (let key in obj) console.log(key + ': ', [obj[key]]);

    console.log(headerFooterBanner);
}

function sortObject(o, desc) {
    let sorted = {};
    let key;
    let a = [];

    for (key in o) {
        if (o.hasOwnProperty(key)) a.push(key);
    }

    if (desc) a.sort(sortDescending);
    else a.sort(sortAscending);

    for (key = 0; key < a.length; key++) sorted[a[key]] = o[a[key]];

    return sorted;
}

function sortAscending(a, b) {
    if (typeof a == 'string') {
        a = a.toLowerCase();
        b = b.toLowerCase();
    }

    if (a < b) return -1;
    else if (a > b) return 1;
    else return 0;
}

function sortDescending(a, b) {
    if (typeof a == 'string') {
        a = a.toLowerCase();
        b = b.toLowerCase();
    }

    if (a > b) return -1;
    else if (a < b) return 1;
    else return 0;
}

function getFileExtension(sFile) {
    return sFile.replace(/^(.*)(\.[^/.]+)$/, '$2');
}

/**
 * Async wait function.
 * Example:
 * (async () => {
 *     await wait(4000).then(() => {
 *         console.log(new Date().toLocaleTimeString());
 *     }).then(() => {
 *         console.log('here');
 *     });
 * })();
 *
 * @param {number} ms - Milliseconds to wait.
 * @param {boolean} [synchronous=false] - Wait synchronously.
 */
async function wait(ms, synchronous = false) {
    let _wait = (ms, synchronous) => {
        if (synchronous) {
            let start = Date.now();
            let now = start;

            while (now - start < ms) now = Date.now();
        } else {
            return new Promise((resolve) => setTimeout(resolve, ms));
        }
    };

    await _wait(ms, synchronous);
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {() => bool} condition
 * @param {{ timeout?: number; callback: () => T; conditionIsAsync: boolean; }} [{ timeout, callback, conditionIsAsync = false } = {}]
 * @returns {T}
 * @template T
 */
async function waitUntil(condition, { timeout, callback, conditionIsAsync = false } = {}) {
    timeout = timeout || -1;
    let maxTime = timeout == -1 ? 20000 : -1;
    let startTime = new Date();

    let timeRanOut = false;

    let done = (() => {
        let deferred = {};

        deferred.promise = new Promise((resolve, reject) => {
            deferred.resolve = resolve;
            deferred.reject = reject;
        });

        return deferred;
    })();

    /** @type {number} */
    let timeoutId;

    if (timeout && timeout > 0) {
        timeoutId = setTimeout(() => {
            timeRanOut = true;

            return done.reject();
        }, timeout);
    }

    let loop = async () => {
        let endTime = new Date();
        let elapsed = endTime - startTime;

        let conditionResult = conditionIsAsync ? await condition() : condition();

        if (conditionResult || timeRanOut || (maxTime != -1 && elapsed > maxTime)) {
            clearTimeout(timeoutId);

            return done.resolve(callback ? await callback() : undefined);
        }

        setTimeout(loop, 0);
    };

    setTimeout(loop, 0);

    return done.promise;
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {any} obj
 * @param {boolean} [getInherited=false]
 * @returns {string}
 */
function getType(obj, getInherited = false) {
    let _typeVar = (function (global) {
        let cache = {};

        return function (obj) {
            let key;

            // null
            if (obj == null) return 'null';

            // window/global
            if (obj == global) return 'global';

            // basic: string, boolean, number, undefined
            if (!['object', 'function'].includes((key = typeof obj))) return key;

            if (obj.constructor != undefined && obj.constructor.name != 'Object' && !getInherited) return obj.constructor.name;

            // cached. date, regexp, error, object, array, math
            // and get XXXX from [object XXXX], and cache it
            return cache[(key = {}.toString.call(obj))] || (cache[key] = key.slice(8, -1));
        };
    })(globalThis);

    return _typeVar(obj);
}

/**
 * Returns a function, that, as long as it continues to be invoked, will not
 * be triggered. The function will be called after it stops being called for
 * N milliseconds. If `immediate` is passed, trigger the function on the
 * leading edge, instead of the trailing.
 *
 * @param {function} func
 * @param {Number} wait
 * @param {Boolean} immediate
 * @returns
 */
function debounce(func, wait, immediate) {
    let timeout;

    return function () {
        let context = this,
            args = arguments;

        let later = function () {
            timeout = null;

            if (!immediate) func.apply(context, args);
        };

        let callNow = immediate && !timeout;

        clearTimeout(timeout);
        timeout = setTimeout(later, wait);

        if (callNow) func.apply(context, args);
    };
}

function equals(x, y) {
    if (x === y) return true;
    // if both x and y are null or undefined and exactly the same

    if (!(x instanceof Object) || !(y instanceof Object)) return false;
    // if they are not strictly equal, they both need to be Objects

    if (x.constructor !== y.constructor) return false;
    // they must have the exact same prototype chain, the closest we can do is
    // test there constructor.

    for (let p in x) {
        if (!x.hasOwnProperty(p)) continue;
        // other properties were tested using x.constructor === y.constructor

        if (!y.hasOwnProperty(p)) return false;
        // allows to compare x[ p ] and y[ p ] when set to undefined

        if (x[p] === y[p]) continue;
        // if they have the same strict value or identity then they are equal

        if (typeof x[p] !== 'object') return false;
        // Numbers, Strings, Functions, Booleans must be strictly equal

        if (!equals(x[p], y[p])) return false;
        // Objects and Arrays must be tested recursively
    }

    for (p in y) {
        if (y.hasOwnProperty(p) && !x.hasOwnProperty(p)) return false;
        // allows x[ p ] to be set to undefined
    }
    return true;
}

function isEncoded(uri) {
    uri = uri || '';

    return uri !== decodeURIComponent(uri);
}

function fullyDecodeURI(uri) {
    while (isEncoded(uri)) uri = decodeURIComponent(uri);

    return uri;
}

/**
 * Get difference in days between two dates.
 *
 * @param {Date} a
 * @param {Date} b
 * @returns
 */
function dateDiffInDays(a, b) {
    let _MS_PER_DAY = 1000 * 60 * 60 * 24;

    // Discard the time and time-zone information.
    let utc1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
    let utc2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());

    return Math.floor((utc1 - utc2) / _MS_PER_DAY);
}

function randomInt(min, max) {
    return Math.floor(Math.random() * (max - min + 1) + min);
}

function randomFloat(min, max) {
    return Math.random() * (max - min + 1) + min;
}

function keySort(keys, desc) {
    return function (a, b) {
        let aVal = null;
        let bVal = null;

        for (let i = 0; i < keys.length; i++) {
            const key = keys[i];

            if (i == 0) {
                aVal = a[key];
                bVal = b[key];
            } else {
                aVal = aVal[key];
                bVal = bVal[key];
            }
        }
        return desc ? ~~(aVal < bVal) : ~~(aVal > bVal);
    };
}

function observe(obj, handler) {
    return new Proxy(obj, {
        get(target, key) {
            return target[key];
        },
        set(target, key, value) {
            target[key] = value;

            if (handler) handler();
        },
    });
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {Console} console
 */
function addSaveToConsole(console) {
    console.save = function (data, filename) {
        if (!data) {
            console.error('Console.save: No data');

            return;
        }

        if (!filename) filename = 'console.json';

        if (typeof data === 'object') data = JSON.stringify(data, undefined, 4);

        let blob = new Blob([data], {
            type: 'text/json',
        });
        let event = document.createEvent('MouseEvents');
        let tempElem = document.createElement('a');

        tempElem.download = filename;
        tempElem.href = window.URL.createObjectURL(blob);
        tempElem.dataset.downloadurl = ['text/json', tempElem.download, tempElem.href].join(':');

        event.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
        tempElem.dispatchEvent(event);
    };
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {Window} _window
 * @param {string} propName
 * @param {{} | [] | any} value
 */
function setupWindowProps(_window, propName, value) {
    if (getType(value) == 'object') {
        if (typeof _window[propName] === 'undefined' || _window[propName] == null) {
            _window[propName] = {};
        }

        let keys = Object.keys(value);

        for (let i = 0; i < keys.length; i++) {
            const key = keys[i];

            if (!(/** @type {{}} */ (_window[propName].hasOwnProperty(key)))) {
                _window[propName][key] = null;
            }

            if (_window[propName][key] == null) {
                _window[propName][key] = value[key];
            }
        }
    } else {
        if (typeof _window[propName] === 'undefined' || _window[propName] == null) {
            _window[propName] = value;
        }
    }
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {{ name: string; value: any; }[]} variables
 */
function exposeGlobalVariables(variables) {
    variables.forEach((variable, index, variables) => {
        try {
            setupWindowProps(getWindow(), variable.name, variable.value);
        } catch (error) {
            logger.error(`Unable to expose variable ${variable.name} into the global scope.`);
        }
    });
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {string} str
 * @returns {string}
 */
function htmlEntitiesDecode(str) {
    return str
        .replace(/&amp;/g, '&')
        .replace(/&lt;/g, '<')
        .replace(/&gt;/g, '>')
        .replace(/&quot;/g, '"');
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {string} metaName
 * @returns {string}
 */
function getMeta(metaName) {
    const metas = document.getElementsByTagName('meta');

    for (let i = 0; i < metas.length; i++) {
        if (metas[i].getAttribute('name') === metaName) {
            return metas[i].getAttribute('content');
        }
    }

    return '';
}

/**
 *
 *
 * @returns {Window & typeof globalThis}
 */
function getWindow() {
    return globalThis.GM_info && GM_info.script.grant.includes('unsafeWindow') ? unsafeWindow : globalThis;
}

/**
 *
 *
 * @param {Window} _window
 */
function getTopWindow(_window = null) {
    _window = _window || getWindow();

    try {
        if (_window.self !== _window.top) {
            _window = getTopWindow(_window.parent);
        }
    } catch (e) {}

    return _window;
}

/**
 * Setup global error handler
 *
 * **Example:**
 * ```javascript
 * setupGlobalErrorHandler({
 *     callback: (error) => console.error('Error:', error),
 *     continuous: true,
 *     prevent_default: true,
 *     tag: '[test-global-error-handler]',
 * });
 * ```
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {{ callback: (error: ErrorEx) => void; continuous?: boolean; prevent_default?: boolean; tag?: string; logFunc?: (...data: any[]) => void; _window: Window; }} [{ callback, continuous = true, prevent_default = false, tag = '[akkd]', logFunc = console.error, _window = window } = {}]
 */
function setupGlobalErrorHandler({ callback, continuous = true, prevent_default = false, tag = null, logFunc = console.error, _window = window } = {}) {
    // respect existing onerror handlers
    let _onerror_original = _window.onerror;

    // install our new error handler
    _window.onerror = function (event, source, lineno, colno, error) {
        if (_onerror_original) {
            _onerror_original(event, source, lineno, colno, error);
        }

        // unset onerror to prevent loops and spamming
        let _onerror = _window.onerror;

        _window.onerror = null;

        // now deal with the error
        let errorObject = new ErrorEx(event, source, lineno, colno, error);
        let errorMessage = createErrorMessage(errorObject);

        if (tag) {
            let rgb = '38;177;38';

            tag = `\x1B[38;2;${rgb}m${tag}\x1B[m`;

            logFunc(tag, errorMessage);
        } else {
            logFunc(errorMessage);
        }

        // run callback if provided
        if (callback) {
            callback(errorObject);
        }

        // re-install this error handler again if continuous mode
        if (continuous) {
            _window.onerror = _onerror;
        }

        // true if normal error propagation should be suppressed
        // (i.e. normally console.error is logged by the browser)
        return prevent_default;
    };

    class ErrorEx {
        /**
         * Creates an instance of ErrorEx.
         * @author Michael Barros <michaelcbarros@gmail.com>
         * @param {string | Event} event
         * @param {string} source
         * @param {number} lineno
         * @param {number} colno
         * @param {Error} error
         * @memberof ErrorEx
         */
        constructor(event, source, lineno, colno, error) {
            this.name = error.name;
            this.message = error && error.message ? error.message : null;
            this.stack = error && error.stack ? error.stack : null;
            this.event = event;
            this.location = document.location.href;
            this.url = source;
            this.lineno = lineno;
            this.colno = colno;
            this.useragent = navigator.userAgent;
            this.fileName = error && error.fileName ? error.fileName : null;
            this.description = error && error.description ? error.description : null;
            this.name = error && error.name ? error.name : null;
            this.error = error;
        }
    }

    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {ErrorEx} error
     * @returns {string}
     */
    function createErrorMessage(error) {
        let name = error && error.name ? error.name : 'Error';
        let message = error && error.message ? error.message : 'Unknown error occured';
        let stack = error && error.stack ? error.stack.split('\n').splice(1).join('\n') : 'Error';

        let errorMessage = `Uncaught Global ${name}: ${message}\n${stack}`;

        return errorMessage;
    }
}

function applyCss(cssFiles) {
    /** @type {{ css: string, node?: HTMLElement }[]} */
    let cssArr = [];

    for (let i = 0; i < cssFiles.length; i++) {
        let cssStr = GM_getResourceText(cssFiles[i]);

        cssArr.push({
            css: cssStr,
        });
    }

    addStyles(cssArr);
}

function applyCss2(cssFiles) {
    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {string} cssStyleStr
     * @returns {HTMLStyleElement}
     */
    function createStyleElementFromCss(cssStyleStr) {
        let style = document.createElement('style');

        style.innerHTML = cssStyleStr.trim();

        return style;
    }

    let ranOnce = false;

    /** @type {HTMLStyleElement[]} */
    getWindow().akkd.styleElements = [];

    function removeStyleElements() {
        for (let i = 0; i < getWindow().akkd.styleElements.length; i++) {
            let styleElement = getWindow().akkd.styleElements[i];

            styleElement.remove();
        }

        getWindow().akkd.styleElements = [];
    }

    function _editStyleSheets() {
        $(document).arrive('style, link', async function () {
            if (this.tagName == 'LINK' && this.href.includes('.css')) {
                removeStyleElements();

                for (let i = 0; i < cssFiles.length; i++) {
                    let cssFile = cssFiles[i];
                    let css = GM_getResourceText(cssFile);

                    let styleElem = createStyleElementFromCss(css);

                    styleElem.id = `akkd-transform-style-${(i + 1).toString().padStart(2, '0')}`;

                    getWindow().akkd.styleElements.push(styleElem);

                    document.body.appendChild(styleElem);
                }
            }
        });

        if (!ranOnce) {
            for (let i = 0; i < cssFiles.length; i++) {
                let cssFile = cssFiles[i];
                let css = GM_getResourceText(cssFile);

                let styleElem = createStyleElementFromCss(css);

                styleElem.id = `akkd-transform-style-${(i + 1).toString().padStart(2, '0')}`;

                getWindow().akkd.styleElements.push(styleElem);

                document.body.appendChild(styleElem);
            }

            ranOnce = true;
        }
    }

    _editStyleSheets();
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {{ css: string, node?: HTMLElement }[]} cssArr
 */
let addStyles = (function () {
    /** @type {string[]} */
    const addedStyleIds = [];

    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {{ css: string, node?: HTMLElement }[]} cssArr
     * @param {{ useGM: boolean }} cssArr { useGM = true } = {}
     */
    function _addStyles(cssArr, { useGM = true } = {}) {
        /**
         *
         *
         * @author Michael Barros <michaelcbarros@gmail.com>
         * @param {string} css
         * @returns {HTMLStyleElement}
         */
        function createStyleElementFromCss(css) {
            let style = document.createElement('style');

            style.innerHTML = css.trim();

            return style;
        }

        function removeStyleElements() {
            for (let i = addedStyleIds.length - 1; i >= 0; i--) {
                /** @type {HTMLStyleElement} */
                let styleElem = document.getElementById(addedStyleIds[i]);

                if (styleElem) {
                    styleElem.remove();

                    addedStyleIds.splice(i, 1);
                }
            }
        }

        function addStyleElements() {
            for (let i = 0; i < cssArr.length; i++) {
                try {
                    const css = cssArr[i].css;
                    const node = cssArr[i].node || document.head;

                    /** @type {HTMLStyleElement} */
                    let elem = useGM ? GM_addStyle(css) : createStyleElementFromCss(css);

                    elem.id = `akkd-custom-style-${(i + 1).toString().padStart(2, '0')}`;

                    node.append(elem);

                    addedStyleIds.push(elem.id);
                } catch (error) {
                    console.error(error);
                }
            }
        }

        removeStyleElements();
        addStyleElements();

        return addedStyleIds;
    }

    return _addStyles;
})();

/**
 * Return uuid of form xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @returns {string}
 */
function uuid4() {
    let uuid = '';
    let ii;

    for (ii = 0; ii < 32; ii += 1) {
        switch (ii) {
            case 8:
            case 20:
                uuid += '-';
                uuid += ((Math.random() * 16) | 0).toString(16);

                break;

            case 12:
                uuid += '-';
                uuid += '4';

                break;

            case 16:
                uuid += '-';
                uuid += ((Math.random() * 4) | 8).toString(16);

                break;

            default:
                uuid += ((Math.random() * 16) | 0).toString(16);
        }
    }

    return uuid;
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {{} | []} obj
 * @param {string | {oldName: string, newName: string}[]} oldName
 * @param {string=} newName
 * @returns
 */
function renameProperty(obj, oldName, newName) {
    function _renameProperty(obj, oldName, newName) {
        let keys = Object.keys(obj);

        for (let i = 0; i < keys.length; i++) {
            let key = keys[i];
            let value = obj[key];

            if (value && typeof value == 'object') {
                obj[key] = _renameProperty(value, oldName, newName);
            }

            if (obj.hasOwnProperty(oldName)) {
                obj[newName] = obj[oldName];

                delete obj[oldName];
            }
        }

        return obj;
    }

    let renames = Array.isArray(oldName) ? oldName : [{ oldName, newName }];

    for (let i = 0; i < renames.length; i++) {
        const rename = renames[i];

        obj = _renameProperty(obj, rename.oldName, rename.newName);
    }

    return obj;
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {Promise<T>} fn
 * @param {{ retries?: number; interval?: number; maxTime?: number; throwError?: boolean; }} { retries = 3, interval = 100, maxTime = null, throwError = false }
 * @returns {Promise<T>}
 * @template T
 */
async function retry(fn, { retries = 3, interval = 100, maxTime = null, throwError = false }) {
    let start = new Date();
    let timeLapsed;

    async function _retry() {
        try {
            return await fn;
        } catch (error) {
            timeLapsed = new Date() - start;

            await wait(interval);

            if (maxTime) {
                if (timeLapsed >= maxTime) {
                    if (throwError) {
                        throw error;
                    } else {
                        return null;
                    }
                }
            } else {
                --retries;

                if (retries === 0) {
                    if (throwError) {
                        throw error;
                    } else {
                        return null;
                    }
                }
            }

            return await _retry();
        }
    }

    return await _retry();
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {string} htmlStr
 * @returns {NodeListOf<ChildNode>}
 */
function createElementsFromHTML(htmlStr) {
    let div = document.createElement('div');

    div.innerHTML = htmlStr.trim();

    return div.childNodes;
}

/**
 * Checks if a variable is a number.
 *
 * @param {*} variable - The variable to check.
 * @returns {boolean} - `true` if the variable is a number or a number represented as a string, `false` otherwise.
 */
function isNumber(variable) {
    return (typeof variable == 'string' || typeof variable == 'number') && !isNaN(variable - 0) && variable !== '';
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {string | number} val
 * @returns
 */
function parseNumberSafe(val) {
    if (isNumber(val)) {
        val = parseFloat(val);
    }

    return val;
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {number} num
 * @returns {boolean}
 */
function isInt(num) {
    return Number(num) === num && num % 1 === 0;
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {number} num
 * @returns {boolean}
 */
function isFloat(num) {
    return Number(num) === num && num % 1 !== 0;
}

/*
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {HTMLElement} elem
 * @param {string} prop
 * @param {Window=} _window
 * @returns {string | number | null}
 */
/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {HTMLElement} elem
 * @param {string} prop
 * @param {Window} [_window=getWindow()]
 * @returns {string | number | null}
 */
function getStyle(elem, prop, _window = null) {
    _window = _window || getWindow();

    let value = parseNumberSafe(
        window
            .getComputedStyle(elem, null)
            .getPropertyValue(prop)
            .replace(/^(\d+)px$/, '$1')
    );

    return value;
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {HTMLElement} beforeElem
 * @param {HTMLElement=} afterElem
 */
function attachHorizontalResizer(beforeElem, afterElem) {
    let resizer = document.createElement('span');

    resizer.className = 'akkd-horz-resizer';

    beforeElem.after(resizer);

    afterElem = afterElem ? afterElem : resizer.nextElementSibling;

    // resizer.addEventListener('mousedown', init, false);

    // /**
    //  *
    //  *
    //  * @author Michael Barros <michaelcbarros@gmail.com>
    //  * @param {MouseEvent} ev
    //  */
    // function init(ev) {
    //     getWindow().addEventListener('mousemove', resize, false);
    //     getWindow().addEventListener('mouseup', stopResize, false);
    // }

    // /**
    //  *
    //  *
    //  * @author Michael Barros <michaelcbarros@gmail.com>
    //  * @param {MouseEvent} ev
    //  */
    // function resize(ev) {
    //     beforeElem.style.height = `${ev.clientY - beforeElem.offsetTop}px`;
    // }

    // /**
    //  *
    //  *
    //  * @author Michael Barros <michaelcbarros@gmail.com>
    //  * @param {MouseEvent} ev
    //  */
    // function stopResize(ev) {
    //     getWindow().removeEventListener('mousemove', resize, false);
    //     getWindow().removeEventListener('mouseup', stopResize, false);
    // }

    let prevX = -1;
    let prevY = -1;
    let dir = null;

    $(resizer).on('mousedown', function (e) {
        prevX = e.clientX;
        prevY = e.clientY;
        dir = 'n'; // $(this).attr('id');

        $(document).on('mousemove', resize);
        $(document).on('mouseup', stopResize);
    });

    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {JQuery.MouseMoveEvent<Document, undefined, Document, Document>} ev
     */
    function resize(ev) {
        if (prevX == -1) return;

        let boxX = $(afterElem).position().left;
        let boxY = $(afterElem).position().top;
        let boxW = $(afterElem).width();
        let boxH = $(afterElem).height();

        let dx = ev.clientX - prevX;
        let dy = ev.clientY - prevY;

        switch (dir) {
            case 'n':
                // north
                boxY += dy;
                boxH -= dy;

                break;

            case 's':
                // south
                boxH += dy;

                break;

            case 'w':
                // west
                boxX += dx;
                boxW -= dx;

                break;

            case 'e':
                // east
                boxW += dx;

                break;

            default:
                break;
        }

        $(afterElem).css({
            // top: boxY + 'px',
            // left: boxX + 'px',
            // width: boxW + 'px',
            height: boxH + 'px',
        });

        let lines = [
            //
            // ['newHeight', newHeight],
            ['clientY', ev.clientY],
            ['beforeElem.top', roundNumber($(beforeElem).position().top)],
            ['beforeElem.height', $(beforeElem).height()],
            '',
            ['afterElem.top', roundNumber($(afterElem).position().top)],
            ['afterElem.height', $(afterElem).height()],
        ];

        // writeDebugMsg(lines);
        console.debug([`y: ${ev.clientY}`, `b.top: ${roundNumber($(beforeElem).position().top)}`, `b.height: ${$(beforeElem).height()}`, `a.top: ${roundNumber($(afterElem).position().top)}`, `a.height: ${$(afterElem).height()}`].join('    '));

        function writeDebugMsg(lines) {
            let outputLines = ['*'.repeat(60)];

            let tags = lines.map((line) => (Array.isArray(line) ? line[0] : line));

            lines.forEach((line) => {
                if (Array.isArray(line)) {
                    // need to require lpad-align
                    // outputLines.push(`${lpadAlign(line[0], tags)}: ${line[1]}`);
                } else {
                    outputLines.push(line);
                }
            });

            outputLines.push('*'.repeat(60));

            console.debug(outputLines.join('\n'));
        }

        function roundNumber(num, places = 2) {
            return parseFloat(parseFloat(num.toString()).toFixed(places));
            // Math.round(num * 100) / 100
        }

        prevX = ev.clientX;
        prevY = ev.clientY;
    }

    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {JQuery.MouseMoveEvent<Document, undefined, Document, Document>} ev
     */
    function stopResize(ev) {
        prevX = -1;
        prevY = -1;

        $(document).off('mousemove', resize);
        $(document).off('mouseup', stopResize);
    }
}

function traceMethodCalls(obj) {
    /** @type {ProxyHandler} */
    let handler = {
        get(target, propKey, receiver) {
            if (propKey == 'isProxy') return true;

            const prop = target[propKey];

            if (typeof prop == 'undefined') return;

            if (typeof prop === 'object' && target[propKey] !== null) {
                if (!prop.isProxy) {
                    target[propKey] = new Proxy(prop, handler);

                    return target[propKey];
                } else {
                    return target[propKey];
                }
            }

            if (typeof target[propKey] == 'function') {
                const origMethod = target[propKey];

                return function (...args) {
                    let result = origMethod.apply(this, args);

                    console.log(propKey + JSON.stringify(args) + ' -> ' + JSON.stringify(result));

                    return result;
                };
            } else {
                return target[propKey];
            }
        },
    };

    return new Proxy(obj, handler);
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {HTMLElement} elem
 * @param {number} [topOffset=0]
 * @returns {boolean}
 */
function isVisible(elem, topOffset = 0) {
    /**
     * Checks if a DOM element is visible. Takes into
     * consideration its parents and overflow.
     *
     * @param {HTMLElement} el      the DOM element to check if is visible
     *
     * These params are optional that are sent in recursively,
     * you typically won't use these:
     *
     * @param {number} top       Top corner position number
     * @param {number} right     Right corner position number
     * @param {number} bottom    Bottom corner position number
     * @param {number} left      Left corner position number
     * @param {number} width     Element width number
     * @param {number} height    Element height number
     * @returns {boolean}
     */
    function _isVisible(el, top, right, bottom, left, width, height) {
        let parent = el.parentNode;
        let VISIBLE_PADDING = 2;

        if (!_elementInDocument(el)) {
            return false;
        }

        // Return true for document node
        if (9 === parent.nodeType) {
            return true;
        }

        // Return false if our element is invisible
        if ('0' === _getStyle(el, 'opacity') || 'none' === _getStyle(el, 'display') || 'hidden' === _getStyle(el, 'visibility')) {
            return false;
        }

        if ('undefined' === typeof top || 'undefined' === typeof right || 'undefined' === typeof bottom || 'undefined' === typeof left || 'undefined' === typeof width || 'undefined' === typeof height) {
            top = el.offsetTop + topOffset;
            left = el.offsetLeft;
            bottom = top + el.offsetHeight;
            right = left + el.offsetWidth;
            width = el.offsetWidth;
            height = el.offsetHeight;
        }

        // If we have a parent, let's continue:
        if (parent) {
            // Check if the parent can hide its children.
            if ('hidden' === _getStyle(parent, 'overflow') || 'scroll' === _getStyle(parent, 'overflow')) {
                // Only check if the offset is different for the parent
                if (
                    // If the target element is to the right of the parent elm
                    left + VISIBLE_PADDING > parent.offsetWidth + parent.scrollLeft ||
                    // If the target element is to the left of the parent elm
                    left + width - VISIBLE_PADDING < parent.scrollLeft ||
                    // If the target element is under the parent elm
                    top + VISIBLE_PADDING > parent.offsetHeight + parent.scrollTop ||
                    // If the target element is above the parent elm
                    top + height - VISIBLE_PADDING < parent.scrollTop
                ) {
                    // Our target element is out of bounds:
                    return false;
                }
            }
            // Add the offset parent's left/top coords to our element's offset:
            if (el.offsetParent === parent) {
                left += parent.offsetLeft;
                top += parent.offsetTop;
            }
            // Let's recursively check upwards:
            return _isVisible(parent, top, right, bottom, left, width, height);
        }

        return true;
    }

    // Cross browser method to get style properties:
    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {HTMLElement} el
     * @param {string} property
     * @returns
     */
    function _getStyle(el, property) {
        let value;

        if (window.getComputedStyle) {
            value = document.defaultView.getComputedStyle(el, null)[property];
        }

        if (el.currentStyle) {
            value = el.currentStyle[property];
        }

        return value;
    }

    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {HTMLElement} element
     * @returns {boolean}
     */
    function _elementInDocument(element) {
        while ((element = element.parentNode)) {
            if (element == document) {
                return true;
            }
        }

        return false;
    }

    return _isVisible(elem);
}

/**
 * @summary
 * High-order function that memoizes a function, by creating a scope
 * to store the result of each function call, returning the cached
 * result when the same inputs is given.
 *
 * @description
 * Memoization is an optimization technique used primarily to speed up
 * functions by storing the results of expensive function calls, and returning
 * the cached result when the same inputs occur again.
 *
 * Each time a memoized function is called, its parameters are used as keys to index the cache.
 * If the index (key) is present, then it can be returned, without executing the entire function.
 * If the index is not cached, then all the body of the function is executed, and the result is
 * added to the cache.
 *
 * @see https://www.sitepoint.com/implementing-memoization-in-javascript/
 *
 * @export
 * @param {Function} func: function to memoize
 * @returns {Function}
 */
function memoize(func) {
    const cache = {};

    function memoized(...args) {
        const key = JSON.stringify(args);

        if (key in cache) return cache[key];

        if (globalThis instanceof this.constructor) {
            return (cache[key] = func.apply(null, args));
        } else {
            return (cache[key] = func.apply(this, args));
        }
    }

    memoized.toString = () => func.toString();

    return memoized;
}

function memoizeClass(clazz, options = { toIgnore: [] }) {
    let funcs = getObjProps(clazz, { namesOnly: true }).funcs;

    for (let i = 0; i < funcs.length; i++) {
        let funcName = funcs[i];

        if (options.toIgnore.includes(funcName)) continue;

        let func = Object.getOwnPropertyDescriptor(clazz.prototype, funcName);

        let memFunc = memoize(func.value);

        Object.defineProperty(clazz.prototype, funcName, {
            get: function () {
                return memFunc;
            },
        });
    }

    let props = getObjProps(clazz, { namesOnly: true }).props;

    for (let i = 0; i < props.length; i++) {
        let propName = props[i];

        if (options.toIgnore.includes(propName)) continue;

        let prop = Object.getOwnPropertyDescriptor(clazz.prototype, propName);
        let cacheKey = `_${propName}-cache_`;

        Object.defineProperty(clazz.prototype, propName, {
            get: function () {
                return (this[cacheKey] = this[cacheKey] || prop.get.call(this));
            },
        });
    }
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {any[]} objects
 * @returns {object}
 */
function merge(...objects) {
    const isObject = (obj) => Object.prototype.toString.call(obj) == '[object Object]' && obj.constructor && obj.constructor.name == 'Object';

    let _merge = (_target, _source, _isMergingArrays) => {
        if (!isObject(_target) || !isObject(_source)) {
            return _source;
        }

        Object.keys(_source).forEach((key) => {
            const targetValue = _target[key];
            const sourceValue = _source[key];

            if (Array.isArray(targetValue) && Array.isArray(sourceValue)) {
                if (_isMergingArrays) {
                    _target[key] = targetValue.map((x, i) => (sourceValue.length <= i ? x : _merge(x, sourceValue[i], _isMergingArrays)));

                    if (sourceValue.length > targetValue.length) {
                        _target[key] = _target[key].concat(sourceValue.slice(targetValue.length));
                    }
                } else {
                    _target[key] = targetValue.concat(sourceValue);
                }
            } else if (isObject(targetValue) && isObject(sourceValue)) {
                _target[key] = _merge(Object.assign({}, targetValue), sourceValue, _isMergingArrays);
            } else {
                _target[key] = sourceValue;
            }
        });

        return _target;
    };

    const isMergingArrays = typeof objects[objects.length - 1] == 'boolean' ? objects.pop() : false;

    if (objects.length < 2) throw new Error('mergeEx: this function expects at least 2 objects to be provided');

    if (objects.some((object) => !isObject(object))) throw new Error('mergeEx: all values should be of type "object"');

    const target = objects.shift();
    let source;

    while ((source = objects.shift())) {
        _merge(target, source, isMergingArrays);
    }

    return target;
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {Window} [windowOrFrame=getTopWindow()]
 * @param {Window[]} [allFrameArray=[]]
 * @returns {Window[]}
 */
function getAllFrames(windowOrFrame = getTopWindow(), allFrameArray = []) {
    allFrameArray.push(windowOrFrame.frames);

    for (var i = 0; i < windowOrFrame.frames.length; i++) {
        getAllFrames(windowOrFrame.frames[i], allFrameArray);
    }

    return allFrameArray;
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @returns {boolean}
 */
function windowIsFocused() {
    let frames = getAllFrames();

    for (let i = 0; i < frames.length; i++) {
        const frame = frames[i];

        try {
            if (frame.document.hasFocus()) {
                return true;
            }
        } catch (error) {}
    }

    return false;
}

function setupWindowHasFocused() {
    let frames = getAllFrames();

    for (let i = 0; i < frames.length; i++) {
        const frame = frames[i];

        try {
            frame.hasFocus = windowIsFocused;
        } catch (error) {}
    }
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {[]} array
 * @param {any} element
 * @param {number} index
 */
function moveArrayElement(array, filter, index) {
    let item = array.filter((item) => item === filter)[0];

    if (item) {
        array = array.filter((item) => item !== filter);

        array.unshift(item);
    }

    return array;
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {HTMLElement} elem
 * @param {(elem: HTMLElement, level: number) => void} callback
 * @param {number} [level=0]
 */
function walkDom(elem, callback, level = 0) {
    let children = elem.children;

    callback(elem, level);

    for (let i = 0; i < children.length; i++) {
        /** @type {HTMLElement} */
        let child = children[i];

        walkDom(child, callback, level + 1);

        if (child.shadowRoot) {
            walkDom(child.shadowRoot, callback, level + 2);
        }
    }
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {T} obj
 * @param {(key: string | number, value: any, keyPath: string, callbackRes: { doBreak: boolean, returnValue: any | null }, obj: T) => boolean} callback
 * @template T
 * @returns {{ dottedObj: T, returnValue: any }}
 */
function walkObj(obj, callback) {
    let callbackRes = {
        doBreak: false,
        returnValue: null,
    };

    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {{}} _obj
     * @param {string[]} keyPath
     * @param {{}} newObj
     */
    function _walk(_obj, keyPath, newObj) {
        keyPath = typeof keyPath === 'undefined' ? [] : keyPath;
        newObj = typeof newObj === 'undefined' ? {} : newObj;

        for (let key in _obj) {
            if (_obj.hasOwnProperty(key)) {
                let value = _obj[key];

                keyPath.push(key);

                callback.apply(this, [key, value, keyPath.join('.'), callbackRes, obj]);

                if (typeof value === 'object' && value !== null) {
                    newObj = _walk(value, keyPath, newObj);
                } else {
                    let newKey = keyPath.join('.');

                    newObj[newKey] = value;
                }

                keyPath.pop();

                if (callbackRes.doBreak) {
                    break;
                }
            }
        }

        return newObj;
    }

    let newObj = _walk(obj);

    return {
        dottedObj: newObj,
        returnValue: callbackRes.returnValue,
    };
}

/**
 * A function to take a string written in dot notation style, and use it to
 * find a nested object property inside of an object.
 *
 * Useful in a plugin or module that accepts a JSON array of objects, but
 * you want to let the user specify where to find various bits of data
 * inside of each custom object instead of forcing a standardized
 * property list.
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {{}} obj
 * @param {string} dotPath
 * @returns {*}
 */
function getNestedDot(obj, dotPath) {
    let parts = dotPath.split('.');
    let length = parts.length;
    let property = obj || this;

    for (let i = 0; i < length; i++) {
        property = property[parts[i]];
    }

    return property;
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {{}} obj
 * @param {number} maxLevel
 */
function getDottedObj(obj, maxLevel = 50) {
    /**
     *
     *
     * @author Michael Barros <michaelcbarros@gmail.com>
     * @param {{}} _obj
     * @param {string[]} keyPath
     * @param {{}} newObj
     * @param {number} level
     */
    function _worker(_obj, keyPath, newObj, level = 0) {
        keyPath = typeof keyPath === 'undefined' ? [] : keyPath;
        newObj = typeof newObj === 'undefined' ? {} : newObj;

        for (let key in _obj) {
            if (_obj.hasOwnProperty(key)) {
                let value = _obj[key];

                keyPath.push(key);

                if (typeof value === 'object' && value !== null) {
                    newObj = _worker(value, keyPath, newObj, level++);
                } else {
                    let newKey = keyPath.join('.');

                    newObj[newKey] = value;
                }

                keyPath.pop();

                if (maxLevel > 0 && level >= maxLevel) {
                    break;
                }
            }
        }

        return newObj;
    }

    let dottedObj = _worker(obj);

    return dottedObj;
}

function isNode() {
    return !(typeof window !== 'undefined' && typeof window.document !== 'undefined');
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @returns {number}
 */
function getCurrentTimeMs() {
    if (isNode()) {
        const NS_PER_MS = 1e6;

        let time = process.hrtime();

        return time[0] * 1000 + time[1] / NS_PER_MS;
    } else {
        return performance.now();
    }
}

const intervalIDsMap = new Map();
const timeoutIDsMap = new Map();

/**
 * Schedules the repeated execution of a function (callback) with a fixed time delay between each call.
 * @param {TimerHandler} handler - A function to be executed repeatedly.
 * @param {number} [timeout] - The time, in milliseconds, between each function call. Default is 0.
 * @param {...any} [args] - Additional arguments to be passed to the function.
 * @returns {number} - An identifier representing the interval. This value can be used with clearInterval to cancel the interval.
 */
function setIntervalEx(handler, timeout, ...args) {
    if (isNode()) {
        return setInterval(handler, timeout, ...args);
    } else {
        let startTime = getCurrentTimeMs();
        let elapsedTime = 0;
        /** @type {number} */
        let intervalId;
        let intervalIdTemp;

        function loop(currentTime) {
            if (intervalIDsMap.get(intervalId)) {
                const deltaTime = currentTime - startTime;

                elapsedTime += deltaTime;

                if (elapsedTime >= timeout) {
                    handler(...args);

                    elapsedTime = 0;
                }

                startTime = currentTime;

                intervalIdTemp = window.requestAnimationFrame(loop);
            } else {
                window.cancelAnimationFrame(intervalIdTemp);
                intervalIDsMap.delete(intervalId);
            }
        }

        intervalId = window.requestAnimationFrame(loop);

        intervalIDsMap.set(intervalId, true);

        return intervalId;
    }
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {number} intervalId
 */
function clearIntervalEx(intervalId) {
    if (isNode()) {
        clearInterval(intervalId);
    } else {
        intervalIDsMap.set(intervalId, false);

        window.cancelAnimationFrame(intervalId);
    }
}

/**
 * Schedules the execution of a function (callback) after a specified time delay.
 * @param {TimerHandler} handler - A function to be executed.
 * @param {number} [timeout] - The time, in milliseconds, to wait before executing the function. Default is 0.
 * @param {...any} [args] - Additional arguments to be passed to the function.
 * @returns {number} - An identifier representing the timeout. This value can be used with clearTimeout to cancel the timeout.
 */
function setTimeoutEx(handler, timeout, ...args) {
    if (isNode()) {
        return setTimeout(handler, timeout, ...args);
    } else {
        let startTime = getCurrentTimeMs();
        /** @type {number} */
        let timeoutId;
        let timeoutIdTemp;

        function loop(currentTime) {
            if (timeoutIDsMap.get(timeoutId)) {
                const deltaTime = currentTime - startTime;

                if (deltaTime >= timeout) {
                    handler(...args);
                } else {
                    timeoutIdTemp = window.requestAnimationFrame(loop);
                }
            } else {
                window.cancelAnimationFrame(timeoutIdTemp);
                timeoutIDsMap.delete(timeoutId);
            }
        }

        timeoutId = window.requestAnimationFrame(loop);

        timeoutIDsMap.set(timeoutId, true);

        return timeoutId;
    }
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {number} timeoutId
 */
function clearTimeoutEx(timeoutId) {
    if (isNode()) {
        clearTimeout(timeoutId);
    } else {
        timeoutIDsMap.set(timeoutId, false);

        window.cancelAnimationFrame(timeoutId);
    }
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {string} attribute
 * @param {string} value
 * @param {string} elementType
 * @returns {HTMLElement[]}
 */
function findByAttributeValue(attribute, value, elementType) {
    elementType = elementType || '*';

    let all = document.getElementsByTagName(elementType);
    let foundElements = [];

    for (let i = 0; i < all.length; i++) {
        if (all[i].getAttribute(attribute).includes(value)) {
            foundElements.push(all[i]);
        }
    }

    return foundElements;
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @returns {number}
 */
function getLocalStorageSize() {
    let total = 0;

    for (let x in localStorage) {
        // Value is multiplied by 2 due to data being stored in `utf-16` format, which requires twice the space.
        let amount = localStorage[x].length * 2;

        if (!isNaN(amount) && localStorage.hasOwnProperty(x)) {
            total += amount;
        }
    }

    return total;
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {number} bytes
 * @param {boolean} [si=false]
 * @returns {string}
 */
function bytes2HumanReadable(bytes, si = false) {
    let thresh = si ? 1000 : 1024;

    if (Math.abs(bytes) < thresh) {
        return bytes + ' B';
    }

    let units = si ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
    let u = -1;

    while (true) {
        bytes = bytes / thresh;
        u++;

        if (!(Math.abs(bytes) >= thresh && u < units.length - 1)) {
            break;
        }
    }

    return bytes.toFixed(1) + ' ' + units[u];
}

async function GM_fetch(url, fetchInit = {}) {
    if (!window.GM_xmlhttpRequest) {
        console.warn('GM_xmlhttpRequest not required. Using native fetch.');

        return await fetch(url, fetchInit);
    }

    let parseHeaders = function (headersString) {
        const headers = new Headers();

        for (const line of headersString.trim().split('\n')) {
            const [key, ...valueParts] = line.split(':');

            if (key) {
                headers.set(key.trim().toLowerCase(), valueParts.join(':').trim());
            }
        }

        return headers;
    };

    const defaultFetchInit = { method: 'get' };
    const { headers, method } = { ...defaultFetchInit, ...fetchInit };
    const isStreamSupported = GM_xmlhttpRequest?.RESPONSE_TYPE_STREAM;
    const HEADERS_RECEIVED = 2;

    if (!isStreamSupported) {
        return new Promise((resolve, _reject) => {
            const blobPromise = new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    url,
                    method,
                    headers,
                    responseType: 'blob',
                    onload: async (response) => resolve(response.response),
                    onerror: reject,
                    onreadystatechange: onHeadersReceived,
                });
            });

            blobPromise.catch(_reject);

            function onHeadersReceived(gmResponse) {
                const { readyState, responseHeaders, status, statusText } = gmResponse;

                if (readyState === HEADERS_RECEIVED) {
                    const headers = parseHeaders(responseHeaders);

                    resolve({
                        headers,
                        status,
                        statusText,
                        arrayBuffer: () => blobPromise.then((blob) => blob.arrayBuffer()),
                        blob: () => blobPromise,
                        json: () => blobPromise.then((blob) => blob.text()).then((text) => JSON.parse(text)),
                        text: () => blobPromise.then((blob) => blob.text()),
                    });
                }
            }
        });
    } else {
        return new Promise((resolve, _reject) => {
            const responsePromise = new Promise((resolve, reject) => {
                void GM_xmlhttpRequest({
                    url,
                    method,
                    headers,
                    responseType: 'stream',
                    onerror: reject,
                    onreadystatechange: onHeadersReceived,
                    // onloadstart: (gmResponse) => logDebug('[onloadstart]', gmResponse), // debug
                });
            });

            responsePromise.catch(_reject);

            function onHeadersReceived(gmResponse) {
                const { readyState, responseHeaders, status, statusText, response: readableStream } = gmResponse;

                if (readyState === HEADERS_RECEIVED) {
                    const headers = parseHeaders(responseHeaders);
                    let newResp;

                    if (status === 0) {
                        newResp = new Response(readableStream, { headers /*status, statusText*/ });
                    } else {
                        newResp = new Response(readableStream, { headers, status, statusText });
                    }

                    resolve(newResp);
                }
            }
        });
    }
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {T} items
 * @param {{name: string, desc: boolean, case_sensitive: boolean}[]} columns
 * @param {{cmpFunc: any}} [{ cmpFunc = cmp }={}]
 * @returns {T}
 * @template T
 */
function multiKeySort(items, columns, { cmpFunc = null } = {}) {
    function cmp(a, b) {
        if (a < b) {
            return -1;
        } else {
            if (a > b) {
                return 1;
            } else {
                return 0;
            }
        }
    }

    cmpFunc = cmpFunc != null ? cmpFunc : cmp;

    let comparers = [];

    columns.forEach((col) => {
        let column = col.name;
        let desc = 'desc' in col ? col.desc : false;
        let case_sensitive = 'case_sensitive' in col ? col.case_sensitive : true;

        comparers.push([column, desc ? -1 : 1, case_sensitive]);
    });

    function comparer(left, right) {
        for (let i = 0; i < comparers.length; i++) {
            const column = comparers[i][0];
            const polarity = comparers[i][1];
            const case_sensitive = comparers[i][2];

            let result = 0;

            if (case_sensitive) {
                result = cmpFunc(left[column], right[column]);
            } else {
                result = cmpFunc(left[column].toLowerCase(), right[column].toLowerCase());
            }

            if (result) {
                return polarity * result;
            }
        }

        return 0;
    }

    return items.sort(comparer);
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {string} text
 * @param {string} [nodeType='div']
 * @returns {HTMLElement}
 */
function getElementByTextContent(text, nodeType = 'div') {
    let xpath = `//${nodeType}[text()='${text}']`;

    /** @type {HTMLElement} */
    let elem = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;

    return elem;
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {string} text
 * @param {string} [nodeType='div']
 * @returns {HTMLElement}
 */
function getElementByTextContentContains(text, nodeType = 'div') {
    let xpath = `//${nodeType}[contains(text(),'${text}')]`;

    /** @type {HTMLElement} */
    let elem = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;

    return elem;
}

/**
 *
 *
 * @author Michael Barros <michaelcbarros@gmail.com>
 * @param {HTMLElement} element
 * @param {(elem: HTMLElement) => void} callback
 */
function onRemove(element, callback) {
    const parent = element.parentNode;

    if (!parent) {
        throw new Error('The node must already be attached');
    }

    const observer = new MutationObserver((mutations) => {
        let removed = false;

        for (const mutation of mutations) {
            for (const node of mutation.removedNodes) {
                if (node === element) {
                    observer.disconnect();

                    callback(element);

                    removed = true;

                    break;
                }
            }

            if (removed) {
                break;
            }
        }
    });

    observer.observe(parent, {
        childList: true,
    });
}

// #endregion Helper Functions

// #region Prototype Functions

// #region Array

/**
 * Function to setup custom prototype functions for the Array class
 * (For no conflicts, Object.defineProperty must be used)
 *
 */
function setupArrayPrototypes() {
    let funcs = [
        function pushUnique(item) {
            let index = -1;

            for (let i = 0; i < this.length; i++) {
                if (equals(this[i], item)) index = i;
            }

            if (index === -1) this.push(item);
        },
    ];

    for (let i = 0; i < funcs.length; i++) {
        let func = funcs[i];

        Object.defineProperty(Array.prototype, func.name, {
            enumerable: false,
            configurable: true,
            writable: true,
            value: func,
        });
    }
}

setupArrayPrototypes();

// #endregion Array

// #endregion Prototype Functions

// #region jQuery

function setupJqueryExtendedFuncs() {
    if ('jQuery' in getWindow() || 'jQuery' in window) {
        jQuery.fn.extend({
            /**
             *
             *
             * @author Michael Barros <michaelcbarros@gmail.com>
             * @returns {boolean}
             * @this {JQuery<HTMLElement>}
             */
            exists: function exists() {
                return this.length !== 0;
            },

            /**
             *
             *
             * @author Michael Barros <michaelcbarros@gmail.com>
             * @param {() => void} callback
             * @returns {JQuery<HTMLElement>}
             * @this {JQuery<HTMLElement>}
             */
            ready: function ready(callback) {
                let cb = function cb() {
                    return setTimeout(callback, 0, jQuery);
                };

                if (document.readyState !== 'loading') {
                    cb();
                } else {
                    document.addEventListener('DOMContentLoaded', cb);
                }

                return this;
            },

            /**
             *
             *
             * @author Michael Barros <michaelcbarros@gmail.com>
             * @param {string} method
             * @param {{}} options
             * @returns {number | string | null}
             * @this {JQuery<HTMLElement>}
             */
            actual: function actual(method, options) {
                // check if the jQuery method exist
                if (!this[method]) {
                    throw '$.actual => The jQuery method "' + method + '" you called does not exist';
                }

                let defaults = {
                    absolute: false,
                    clone: false,
                    includeMargin: false,
                    display: 'block',
                };

                let configs = jQuery.extend(defaults, options);

                let $target = this.eq(0);
                let fix;
                let restore;

                if (configs.clone === true) {
                    fix = function () {
                        let style = 'position: absolute !important; top: -1000 !important; ';

                        // this is useful with css3pie
                        $target = $target.clone().attr('style', style).appendTo('body');
                    };

                    restore = function () {
                        // remove DOM element after getting the width
                        $target.remove();
                    };
                } else {
                    let tmp = [];
                    let style = '';
                    let $hidden;

                    fix = function () {
                        // get all hidden parents
                        $hidden = $target.parents().addBack().filter(':hidden');
                        style += 'visibility: hidden !important; display: ' + configs.display + ' !important; ';

                        if (configs.absolute === true) {
                            style += 'position: absolute !important; ';
                        }

                        // save the origin style props
                        // set the hidden el css to be got the actual value later
                        $hidden.each(function () {
                            // Save original style. If no style was set, attr() returns undefined
                            let $this = jQuery(this);
                            let thisStyle = $this.attr('style');

                            tmp.push(thisStyle);

                            // Retain as much of the original style as possible, if there is one
                            $this.attr('style', thisStyle ? thisStyle + ';' + style : style);
                        });
                    };

                    restore = function () {
                        // restore origin style values
                        $hidden.each(function (i) {
                            let $this = jQuery(this);
                            let _tmp = tmp[i];

                            if (_tmp === undefined) {
                                $this.removeAttr('style');
                            } else {
                                $this.attr('style', _tmp);
                            }
                        });
                    };
                }

                fix();
                // get the actual value with user specific methed
                // it can be 'width', 'height', 'outerWidth', 'innerWidth'... etc
                // configs.includeMargin only works for 'outerWidth' and 'outerHeight'
                let actual = /(outer)/.test(method) ? $target[method](configs.includeMargin) : $target[method]();

                restore();
                // IMPORTANT, this plugin only return the value of the first element
                return actual;
            },

            /**
             *
             *
             * @author Michael Barros <michaelcbarros@gmail.com>
             * @returns {DOMRect}
             * @this {JQuery<HTMLElement>}
             */
            rect: function rect() {
                return this[0].getBoundingClientRect();
            },

            /**
             *
             *
             * @author Michael Barros <michaelcbarros@gmail.com>
             * @param {number} levels
             * @returns {JQuery<HTMLElement>}
             * @this {JQuery<HTMLElement>}
             */
            parentEx: function parentEx(levels = 1) {
                let parent = this;

                for (let i = 0; i < levels; i++) {
                    if (parent.parent().length == 0) {
                        break;
                    }

                    parent = parent.parent();
                }

                return parent;
            },
        });
    }
}

setupJqueryExtendedFuncs();

// #endregion jQuery