"use strict";

import {Check} from "./Check.ecma.js";

/**
 * @class Utils
 * @constructor
 * @returns {void}
 * @access public
 * @static
 */
export const Utils = (function(){

    /**
     * @callback utils_execute_callback
     * @param  {...(any|null)} inputs 
     * @returns {any|null}
     */

    /**
     * @callback utils_default_callback
     * @returns {void}
     */

    /**
     * @callback utils_execute_array_each_callback
     * @param {?any} item 
     * @param {!utils_default_callback} callback 
     * @returns {void}
     */

    /**
     * @constructs Utils
     * @returns {void}
     * @access private
     * @static
     */
    const Utils = function(){};

    /** @type {string} */
    Utils.BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
    /** @type {string} */
    Utils.RANDOM_ALPHABET = "bHMnuamw/RUBk+xNvCXghsPdlSFG12rLoT0O3VZ=5QeWyI8pADqjcEfJ9Kt64i7Yz";

    /**
     * @param  {...(any|null)} items 
     * @returns {Array<string>}
     * @access public
     * @static
     */
    Utils.get_keys = (...items) => items.reduce((keys, item) => {

        if(Check.is_key(item))
            keys.push(item);
        else if(Check.is_array(item))
            keys.push(...Utils.get_keys(...item));

        return keys;
    }, []);

    /**
     * @param  {...(any|null)} items 
     * @returns {Array.<Object.<string, any|null>>}
     * @access public
     * @static
     */
    Utils.get_dictionaries = (...items) => items.reduce((dictionaries, item) => {

        if(Check.is_dictionary(item))
            dictionaries.push(item);
        else if(Check.is_array(item))
            dictionaries.push(...Utils.get_dictionaries(...item));

        return dictionaries;
    }, []);

    /**
     * @param {?any} items
     * @param {!boolean} [overwrite = false]
     * @returns {Object.<string, any|null>}
     * @access public
     * @static
     */
    Utils.get_dictionary = (items, overwrite = false) => (
        Check.is_dictionary ? items : 
        Check.is_array(items) ? items.reduce((dictionary, item) => {

            if(Check.is_dictionary(item))
                dictionary = overwrite ? {...dictionary, ...item} : {...item, ...dictionary};
            else if(Check.is_array(item))
                dictionary = {...dictionary, ...Utils.get_dictionary(item, overwrite)};

            return dictionary;
        }) : 
    {});

    /**
     * @param {!(string|Array.<string>)} keys 
     * @param {!(Object.<string, any|null>|Array.<any|null>)} inputs 
     * @param {?any} [_default = null] 
     * @returns {any|null}
     * @access public
     * @static
     */
    Utils.get_value = (keys, inputs, _default = null) => {

        /** @type {number} */
        const l = (keys = Utils.get_keys(keys)).length;

        if(l){

            /** @type {number} */
            const m = (inputs = Utils.get_dictionaries(inputs)).length;

            for(let i = 0; i < l; i++)
                for(let j = 0; j < m; j++)
                    if(keys[i] in inputs[j])
                        return inputs[j][keys[i]];
        };
        return _default;
    };

    /**
     * @param {!string} string 
     * @param {!(Object.<string, any|null>|Array.<any|null>)} variables 
     * @param {?any} _default 
     * @returns {string}
     * @access public
     * @static
     */
    Utils.string_variables = (string, variables, _default = null) => {

        variables = Utils.get_dictionary(variables || {});

        return ("" + string).replace(/\{([a-z_][a-z0-9_]*)\}/gi, (all, key) => (
            key in variables ? variables[key] : 
            _default === null ? all : 
        _default));
    };

    /**
     * @param {!string} string 
     * @returns {string}
     * @access public
     * @static
     */
    Utils.to_kebab_case = string => ("" + string).replace(/([A-Z]+)|[^a-z0-9]+/g, (_, upper) => (
        upper ? "-" + upper.toLowerCase() : 
    "-"));

    /**
     * @param {!string} string 
     * @returns {string}
     * @access public
     * @static
     */
    Utils.to_snake_case = string => ("" + string).replace(/([A-Z]+)|[^a-z0-9]+/g, (_, upper) => (
        upper ? "_" + upper.toLowerCase() : 
    "_"));

    /**
     * @param {!HTMLElement} item 
     * @param  {...Object.<string, any|null>} attributes 
     * @returns {void}
     * @access public
     * @static
     */
    Utils.attributes = (item, attributes) => {
        for(const [key, value] of Object.entries(attributes)){
            if(/^on[_\-]?/i.test(key) && Check.is_function(value))
                item.addEventListener(key.toLowerCase().replace(/^on[_\-]?/, ""), event => {
                    Utils.execute(value, item, event);
                });
            else 
                item.setAttribute(Utils.to_kebab_case(key), value);
        };
    };

    /**
     * @param {!HTMLElement} item 
     * @param {...(string|HTMLElement|[string, Object.<string, any|null>|null, string|Array.<string>|null]|null)} structure 
     * @returns {Array.<HTMLElement>}
     * @access public
     * @static
     */
    Utils.html = (item, ...structure) => {

        /** @type {Array.<HTMLElement>} */
        const items = [];

        item || (item = document.createDocumentFragment());

        for(const node of structure)
            if(Check.is_string(node))
                item.innerHTML += node;
            else if(Check.is_html_item(node))
                items.push(item.appendChild(node));
            else if(Check.is_array(node)){

                /** @type {[string, Object.<string, any|null>|null, Array.<any|null>|null]} */
                const [tag, attributes, children] = node.concat(null, null).slice(0, 3), 
                      /** @type {HTMLElement} */
                      element = document.createElement(tag);

                attributes && Utils.attributes(element, attributes);
                if(children && children.length){
                    if(Check.is_string(children))
                        element.innerHTML += children;
                    else
                        Utils.html(element, ...children);
                };
                items.push(item.appendChild(element));

            };

        return items;
    };

    /**
     * @param {?any} item 
     * @returns {any|null}
     * @access public
     * @static
     */
    Utils.unique = item => (
        Check.is_array(item) ? item.filter((item, i, array) => array.indexOf(item) == i) : 
        Check.is_string(item) ? item.split("").filter((char, i, array) => array.indexOf(char) == i).join("") : 
    item);

    /**
     * @param {?(number|Array.<any|null>|string)} [from = null] 
     * @param {?number} [to = null] 
     * @returns {any|null}
     * @access public
     * @static
     */
    Utils.get_random = (from = null, to = null) => (
        Check.is_array(from) || Check.is_string(from) ? from.length ? from[Math.random() * from.length >> 0] : null : 
        Check.is_integer(from) ? (
            to === null ? Math.random() * from >> 0 : 
            Check.is_integer(to) ? from + (Math.random() * (to - from + 1) >> 0) : 
            Check.is_number(to) ? from + Math.random() * (to - from) : 
        null) : 
        Check.is_number(from) ? (
            to === null ? Math.random() * from : 
            Check.is_number(to) ? from + Math.random() * (to - from) : 
        null) : 
        from === null ? Math.random() : 
    null);

    /**
     * @param {?any} data 
     * @returns {string}
     * @access public
     * @static
     */
    Utils.encode_data = data => btoa(encodeURIComponent(JSON.stringify(data)).replace(/%([0-9A-F]{2})/g, (_, p1) => String.fromCharCode("0x" + p1)));

    /**
     * @param {!string} code 
     * @param {!boolean} [is_json = true]
     * @returns {any|null}
     * @access public
     * @static
     */
    Utils.decode_data = (code, is_json = true) => {
        
        /** @type {string} */
        const data = decodeURIComponent(atob(code).split("").map(c => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2)).join(""));

        if(is_json)
            try{
                return JSON.parse(data) || data;
            }catch(exception){};

        return data;
    };

    /**
     * @param {!utils_execute_callback} callback 
     * @param  {...(any|null)} inputs 
     * @returns {any|null}
     * @access public
     * @static
     */
    Utils.execute = (callback, ...inputs) => (
        Check.is_function(callback) ? callback(...inputs) : 
    null);

    /**
     * @param {!Array.<any|null>} array 
     * @param {!utils_execute_array_each_callback} each_callback 
     * @param {?utils_default_callback} [end_callback = null] 
     * @param {!number} [i = 0]
     * @returns {void}
     * @access public
     * @static
     */
    Utils.execute_array = (array, each_callback, end_callback = null, i = 0) => {
        if(i < array.length)
            Utils.execute(each_callback, array[i], () => {
                Utils.execute_array(array, each_callback, end_callback, i + 1);
            });
        else 
            Utils.execute(end_callback);
    };

    /**
     * @param {?any} item 
     * @returns {Array.<any|null>}
     * @access public
     * @static
     */
    Utils.get_array = item => Check.is_array(item) ? item : [item];

    /**
     * @param {!string} string 
     * @returns {string}
     * @access public
     * @static
     */
    Utils.randomize_string = string => string.split("").sort(() => Utils.get_random() - 0.5).join("");

    /**
     * @param {?any} data 
     * @returns {string}
     * @access public
     * @static
     */
    Utils.encrypt_data = data => Utils.encode_data(data).split("").map(character => Utils.RANDOM_ALPHABET[Utils.BASE64_ALPHABET.indexOf(character)]).join("");

    /**
     * @param {string} data 
     * @returns {any|null}
     * @access public
     * @static
     */
    Utils.decrypt_data = data => {

        /** @type {string} */
        const results = Utils.decode_data(data.split("").map(character => Utils.BASE64_ALPHABET[Utils.RANDOM_ALPHABET.indexOf(character)]).join(""), false);

        try{
            return JSON.parse(results) || results;
        }catch(exception){};

        return results;
    };

    /**
     * @param {!Array.<any|null>} array 
     * @returns {void}
     * @access public
     * @static
     */
    Utils.randomize_array = array => {
        
        /** @type {number} */
        const l = array.length - 1;
        
        array.forEach((item, i) => {

            /** @type {number} */
            const j = Utils.get_random(l);

            i != j && ([array[i], array[j]] = [array[j], array[i]]);

        });

    };

    return Utils;
})();