"use strict";

import {Check} from "../Utils/Check.ecma.js";
import {Utils} from "../Utils/Utils.ecma.js";
import {RandomFormat} from "./Format/RandomFormat.ecma.js";
import {RangeFormat} from "./Format/RangeFormat.ecma.js";
import {MixFormat} from "./Format/MixFormat.ecma.js";
import {SerieFormat} from "./Format/SerieFormat.ecma.js";
import {SelectFormat} from "./Format/SelectFormat.ecma.js";
import {PlainFormat} from "./Format/PlainFormat.ecma.js";
import {CapitalizeFormat} from "./Format/CapitalizeFormat.ecma.js";
import {BlockFormat} from "./Format/BlockFormat.ecma.js";
import {UniquesFormat} from "./Format/UniquesFormat.ecma.js";

/**
 * @typedef {import("../OpoTests.ecma.js").OpoTests} OpoTests
 */

/**
 * @class FormatModule
 * @constructor
 * @param {!OpoTests} ot 
 * @returns {void}
 * @access public
 * @static
 */
export const FormatModule = (function(){

    /**
     * @callback format_recursive_callback
     * @param {?any} data
     * @return {?any}
     */

    /**
     * @callback format_mode_callback
     * @param {!(string|Array.<any|null>)} inputs 
     * @param {!Object.<string, any|null>} [shared = {}] 
     * @param {!Array.<string>} [fragments = []] 
     * @return {string}
     */

    /**
     * @callback format_check_callback
     * @param {!string} string 
     * @param {!(string|Array.<string>)} inputs 
     * @param {!Object.<string, any|null>} [shared = {}] 
     * @param {!Array.<string>} [fragments = []] 
     * @return {number}
     */

    /**
     * @constructs FormatModule
     * @param {!OpoTests} ot 
     * @returns {void}
     * @access private
     * @static
     */
    const FormatModule = function(ot){

        /** @type {FormatModule} */
        const self = this;

        /** @type {Object.<string, format_mode_callback>} */
        this.modes = {};
        /** @type {Object.<string, format_check_callback>} */
        this.checks = {};

        /** @type {RandomFormat} */
        this.random = new RandomFormat(self);
        /** @type {RangeFormat} */
        this.range = new RangeFormat(self);
        /** @type {MixFormat} */
        this.mix = new MixFormat(self);
        /** @type {SerieFormat} */
        this.serie = new SerieFormat(self);
        /** @type {SelectFormat} */
        this.select = new SelectFormat(self);
        /** @type {PlainFormat} */
        this.plain = new PlainFormat(self);
        /** @type {CapitalizeFormat} */
        this.capitalize = new CapitalizeFormat(self);
        /** @type {BlockFormat} */
        this.block = new BlockFormat(self);
        /** @type {UniquesFormat} */
        this.uniques = new UniquesFormat(self);

        /**
         * @returns {void}
         * @access private
         */
        const constructor = () => {};

        /**
         * @param {![number, number]} coordenates 
         * @param {!string} key 
         * @returns {boolean}
         * @access public
         */
        this.variable_exists = ([i, j], key) => (
            key in ot.variables || 
            (ot.group_variables[ot.database[i].group] && key in ot.group_variables[ot.database[i].group]) || 
            (ot.database[i].queries[j].own_variables && key in ot.database[i].queries[j].own_variables) 
        );

        /**
         * @param {![number, number]} coordenates 
         * @param {!string} key 
         * @returns {[string, ...(any|null)]|null}
         * @access public
         */
        this.get_variable = ([i, j], key) => {
            // key == "l_40_2015" && console.log([i, j, key, ot.variables]);
            return (
                (ot.database[i].queries[j].own_variables && ot.database[i].queries[j].own_variables[key]) || 
                (ot.group_variables[ot.database[i].group] && ot.group_variables[ot.database[i].group][key]) || 
                ot.variables[key] 
            );
        };

        /**
         * @param {[number, number]} coordenates 
         * @param {!string} string
         * @param {!(string|Array.<string>)} options
         * @param {!Object.<string, any|null>} [shared = {}]
         * @param {!Array.<string>} [fragments = []]
         * @param {!boolean} [check_full = true]
         * @returns {number}
         * @access public
         */
        this.get_check_length = ([i, j], string, options, shared = {}, fragments = [], check_full = true) => {

            /** @type {[boolean, number]} */
            const [has, length] = self.check([i, j], string, options, shared, fragments, check_full);

            return has ? length : -1;
        };

        /**
         * @param {[number, number]} coordenates 
         * @param {!string} string
         * @param {!(string|Array.<string>)} options
         * @param {!Object.<string, any|null>} [shared = {}]
         * @param {!Array.<string>} [fragments = []]
         * @param {!boolean} [check_full = true]
         * @returns {[boolean, number]}
         * @access public
         */
        this.check = ([i, j], string, options, shared = {}, fragments = [], check_full = true) => {

            /** @type {number} */
            let length = 0;

            string = string.toLowerCase();

            return [options.some(option => {

                /** @type {string} */
                let clone = "" + string;
                /** @type {Array.<string>} */
                const parts = [];

                self.build_fragments(option, fragments).replace(/\{[^\{\}]+\}|[^\{]+|\{/g, part => {
                    parts.push(
                        /^\{.+\}$/.test(part) ? part : 
                    part.toLowerCase());
                    return "";
                });
                length = 0;

                let done =  parts.every((part, k) => {
                    if(clone.indexOf(part) == 0){
                        clone = clone.slice(part.length);
                        return true;
                    };

                    /** @type {number} */
                    let m = -1;
                    /** @type {RegExpMatchArray} */
                    const matches = part.match(/^\{([^\{\}]+)\}$/);

                    if(matches){

                        /** @type {string} */
                        const key = matches[1];
                        
                        if(/^[a-z0-9_]+$/i.test(key)){
                            if(self.variable_exists([i, j], key)){

                                /** @type {[string, ...(any|null)]} */
                                const [method, ...data] = self.get_variable([i, j], key);

                                method in self.checks && 
                                // self.checks[method]([i, j], clone, data, shared, fragments) != -1 && 
                                (m = self.checks[method]([i, j], clone, data, shared, fragments));

                            }else if(parts[k + 1]){
                                // TEMPORARY SOLUTION. NEEDS BUILD REAL SOLUTION.
                                m = clone.indexOf(parts[k + 1]);
                            };
                        }else{

                            /** @type {RegExpMatchArray} */
                            const matches = key.match(/^([^,:]+)[,:](.*)$/);

                            if(matches){

                                /** @type {[string, string]} */
                                const [method, data] = matches.slice(1, 3);

                                method in self.checks && 
                                (m = self.checks[method]([i, j], clone, data, shared, fragments));

                            };
                        };
                    };
                    m == -1 && (m = FormatModule.prepare_result(clone, part, shared));

                    if(m != -1){
                        clone = clone.slice(m);
                        length += m;
                        return true;
                    };
                    return false;
                }) && (!check_full || !clone.length);

                return done;
            }), length];
        };

        /**
         * @param {!string} string 
         * @param {!Array.<string>} fragments 
         * @returns {string}
         * @access public
         */
        this.set_fragments_level = (string, fragments) => {
            return (
                !fragments.length ? string : 
            string.replace(/\${3}([0-9]+)\${3}/g, (_, i) => fragments[Number(i)]));
        };

        /**
         * @param {!string} string 
         * @param {!Array.<string>} [fragments = []]
         * @returns {string}
         * @access public
         */
        this.build_fragments = (string, fragments = []) => (
            self.set_fragments_level(FormatModule.recursive(string, string => (
                string.replace(/\{[^\{\}]+\}/g, fragment => {
                    fragments.push(fragment);
                    return "$$$" + (fragments.length - 1) + "$$$";
                })
            )), fragments)
        );

        /**
         * @param {[number, number]} coordenates 
         * @param {!string} string 
         * @param {!Object.<string, any|null>} [shared = {}]
         * @param {!Array.<string>} [fragments = []]
         * @returns {string}
         * @access public
         */
        this.execute = ([i, j], string, shared = {}, fragments = []) => (
            FormatModule.recursive(self.build_fragments(string, fragments), substring => {
                try{
                    return substring.replace(/\{([^\{\}]+)\}/g, (original, key) => {
                        if(/^[a-z0-9_]+$/i.test(key)){
                            if(key in shared)
                                return shared[key];

                            /** @type {[string, ...(any|null)]|null} */
                            const variable = self.get_variable([i, j], key);

                            if(variable){

                                /** @type {[string, ...(any|null)]} */
                                const [method, ...data] = variable;

                                if(method in self.modes)
                                    return self.modes[method]([i, j], data, shared, fragments);
                            };
                        }else{

                            /** @type {RegExpMatchArray} */
                            const matches = key.match(/^([^,:]+)[,:](.*)$/);

                            if(matches){

                                /** @type {[string, string]} */
                                const [method, data] = matches.slice(1, 3);

                                if(method in self.modes)
                                    return self.modes[method]([i, j], data, shared, fragments);
                            };
                        };
                        return original;
                    });
                }catch(exception){
                    console.error([exception, fragments, string, substring]);
                    throw new Error("STOPPED");
                };
                return substring;
            })
        );
        /**
         * @param {[number, number]} coordenates 
         * @param {!string} string 
         * @param {[[number, number], string, Array.<string>]} parameters 
         * @param {!Object.<string, any|null>} [shared = {}]
         * @param {!Array.<string>} [fragments = []]
         * @returns {number}
         * @access public
         */
        this.check_select = ([i, j], string, [[minimum, maximum], separator, items], shared = {}, fragments = []) => {

            /** @type {number} */
            let length = 0, 
                /** @type {number} */
                k = 0, 
                /** @type {boolean} */
                check_last = false;
            /** @type {Array.<string>} */
            const clone_items = [...items], 
                  separator_string = " " + separator + " ";

            for(let done = false; !done && k < maximum; k ++){

                /** @type {number} */
                const l = clone_items.length;

                if(!l)
                    break;

                /** @type {boolean} */
                let ok = false;

                if(check_last || (k && k == l - 1)){

                    // if(string.startsWith(" " + separator + " ")){
                    //     length += separator.length + 2;
                    //     string = string.substring(0, separator.length + 2);
                    // }else if(minimum > 1 || k > 1)
                    //     return length || -1;
                    // else 
                    //     break;

                    if(minimum > 1 || k > 1)
                        break;

                };

                for(let m = 0; m < l; m ++){

                    /** @type {number} */
                    const item_length = (
                        /\{[^\{\}]+\}/.test(self.set_fragments_level(clone_items[m], shared, fragments)) ? self.get_check_length([i, j], string, [items[m]], shared, fragments, false) : 
                    FormatModule.prepare_result(string, clone_items[m], shared));

                    if(item_length != -1){
                        string = string.slice(item_length);
                        clone_items.splice(m, 1);
                        length += item_length;
                        ok = true;
                        if(!string.indexOf(separator_string)){
                            length += separator_string.length;
                            string = string.slice(separator_string.length);
                        }else if(!string.indexOf(", ")){
                            length += 2;
                            string = string.slice(2);
                        }else 
                            done = true;
                        break;
                    };

                };

                if(!ok){
                    k --;
                    break;
                };
                shared.capitalized && (shared.capitalized = false);

                // if(ok)
                //     shared.capitalized && (shared.capitalized = false);
                // else if(check_last || !k)
                //     return length || -1;
                // else{
                //     check_last = true;
                //     k --;
                // };

            };

            return !length || k < minimum ? -1 : length;
        };

        /**
         * @param {[number, number]} coordenates 
         * @param {!Array.<string>} items 
         * @returns {Array.<string>}
         * @access public
         */
        this.get_list = ([i, j], items) => items.reduce((items, item) => {
            
            /** @type {RegExpMatchArray} */
            const matches = item.match(/^list\:([a-z0-9_]+)$/i);

            if(matches){

                /** @type {string} */
                const key = matches[1], 
                      /** @type {[string, ...(any|null)]|null} */
                      variable = self.get_variable([i, j], key);

                if(variable[0] == "list")
                    return items.concat(self.get_list([i, j], variable[1]));
            };
            return items.concat([item]);
        }, []);

        /**
         * @param {!string} string 
         * @param {!Array.<string>} fragments 
         * @returns {string}
         * @access public
         */
        this.restore_fragments = (string, fragments) => FormatModule.recursive(string, substring => (
            self.set_fragments_level(substring, fragments)
        ));

        /**
         * @param {[number, number]} coordenates 
         * @param {!Array.<string>} items 
         * @param {?number} k 
         * @param {?string} separator 
         * @param {!Array.<string>} fragments 
         * @param {?Object.<string, any|null>} shared 
         * @returns {string}
         * @access public
         */
        this.get_items = ([i, j], items, k, separator, fragments, shared) => {

            /** @type {number} */
            const l = (items = self.get_list([i, j], items)).length, 
                  /** @type {Array.<string>} */
                  selected = [], 
                  /** @type {Object.<string, any|null>} */
                  subshared = {
                      ...(shared || (shared = {})), 
                      /** @type {boolean} */
                      capitalized : false, 
                      /** @type {boolean} */
                      uniques : false
                  };

            while(Check.is_array(k))
                Check.is_string(k = Utils.get_random(...(
                    k.every(Check.is_number) && k.length < 3 ? k : [k]
                ))) && 
                (k = k.includes("-") ? k.split("-").map(Number) : Number(k));
            k || (k = 1);

            if((shared || (shared = {})).uniques){
                shared.uniques_cache = [];
                for(let m = 0; m < k && items.length; m ++){

                    /** @type {string} */
                    const item = Utils.get_random(items);

                    if(/\${3}[0-9]+\${3}/.test(item)){

                        /** @type {boolean} */
                        let done = false;

                        for(let t = 0; t < 100; t ++){

                            /** @type {string} */
                            const item_string = self.execute([i, j], item, subshared, fragments);

                            if(done = !selected.includes(item_string) && (
                                !shared.uniques_cache || !shared.uniques_cache.includes(item_string)
                            )){
                                selected.push(item_string);
                                break;
                            };

                        };

                        if(!done){
                            items.splice(items.indexOf(item), 1);
                            m --;
                        };

                    }else{
                        !selected.includes(item) && (
                            !shared.uniques_cache || !shared.uniques_cache.includes(item_string)
                        ) && selected.push(item);
                        items.splice(items.indexOf(item), 1);
                    };

                };
            }else{
                for(let m = 0; m < k && items.length; m ++){

                    /** @type {number} */
                    const x = Utils.get_random(items.length - 1);

                    if(shared.uniques_cache && shared.uniques_cache.includes(items[x]))
                        m --;
                    else 
                        selected.push(self.execute([i, j], items[x], subshared, fragments));
                    items.splice(x, 1);

                };
                // Utils.randomize_array(items);
                // selected.push(...items.slice(0, k > l ? l : k).map(item => self.execute([i, j], item, subshared, fragments)));
            };

            return FormatModule.process_item((selected.length > 1 ? selected.slice(0, selected.length - 1).join(", ") + (
                !separator ? " " : 
                /^[a-z 0-9]+$/i.test(separator.trim()) ? " " + separator.trim() + " " : 
            separator) + selected[selected.length - 1] : selected[0]), shared);
        };

        constructor();

    };

    /** @type {RegExp} */
    FormatModule.PATTERN = new RegExp("9876543210[".split("").reduce((pattern, character) => {

        /** @type {[string, string]} */
        const [start, end] = character != "[" ? ["\\[" + character, character + "\\]"] : ["\\[{2}", "\\]{2}"];

        return (pattern ? pattern + "|" : "") + start + "((?:(?!(?:" + start + "|" + end + ")).)+)" + end;
    }, ""), "g");

    /**
     * @param {?any} data 
     * @param {!format_recursive_callback} callback 
     * @returns {any|null}
     * @access public
     * @static
     */
    FormatModule.recursive = (data, callback) => {

        /** @type {any|null} */
        let cache = data;
        const x = data;

        do{
            cache === undefined && console.warn([x, cache]);
            cache = callback(data = cache);
        } while(cache != data);

        return data;
    };

    /**
     * @param {!string} string 
     * @returns {string}
     * @access public
     * @static
     */
    FormatModule.update = string => FormatModule.recursive(string, string => string.replace(FormatModule.PATTERN, (original, ...data) => {
        for(let i = 0; i < data.length; i ++)
            if(data[i])
                return "{" + data[i].replace(/:{3}/g, "|") + "}";
        return original;
    }));

    /**
     * @param {!string} string 
     * @param {!string} option 
     * @param {!object.<string, any|null>} shared 
     * @returns {string}
     * @access public
     * @static
     */
    FormatModule.prepare_result = (string, option, shared) => {
        if(string.toLowerCase().startsWith(option.toLowerCase())){
            shared.capitalized && (shared.capitalized = false);
            return option.length;
        };
        return -1;
    };

    /**
     * @param {!string} string 
     * @param {!(string|Array.<any|null>)} inputs 
     * @returns {number}
     * @access public
     * @static
     */
    FormatModule.check_range = (string, inputs) => {

        /** @type {RegExpMatchArray} */
        let number_match = string.match(/^[0-9]+/);

        if(number_match){

            /** @type {number} */
            const number = Number(number_match[0]);

            for(const range of (
                Check.is_string(inputs) ? Utils.get_random(inputs.split("|")).split("-").map(Number) : 
                Check.is_array(inputs) ? inputs : 
            [])){
                if(Check.is_number(range)){
                    if(number == range)
                        return ("" + number).length;
                }else if(range.length == 1){
                    if(number == range[0])
                        return ("" + number).length;
                }else{
                    if(number >= range[0] && number <= range[1])
                        return ("" + number).length;
                };
            };
        };
        return -1;
    };

    /**
     * @param {!string} item 
     * @param {!Object.<string, any|null>} shared 
     * @returns {string}
     * @access public
     * @static
     */
    FormatModule.process_item = (item, shared) => shared && shared.capitalized ? item.replace(/^./, character => character.toUpperCase()) : item;

    return FormatModule;
})();