"use strict";

import {SettingsManager} from "../Managers/SettingsManager.ecma.js";
import {I18NManager} from "../Managers/I18NManager.ecma.js";
import {IdentifiersManager} from "../Managers/IdentifiersManager.ecma.js";
import {Components} from "./Components.ecma.js";
import {Utils} from "../Utils/Utils.ecma.js";
import {Check} from "../Utils/Check.ecma.js";
import {BaseView} from "../Views/BaseView.ecma.js";
import {MainFormView} from "../Views/MainFormView.ecma.js";
import {TestsView} from "../Views/TestsView.ecma.js";
import {UserView} from "../Views/UserView.ecma.js";
import {CookiesManager} from "../Managers/CookiesManager.ecma.js";
import {FormatModule} from "../Modules/FormatModule.ecma.js";

/**
 * @class OpoTests
 * @constructor
 * @param {?(Object.<string, any|null>|Array.<any|null>)} inputs
 * @returns {void}
 * @access public
 * @static
 */
export const OpoTests = (function(){

    /**
     * @callback opo_tests_preload_callback
     * @param {?HTMLElement} item 
     * @param {!boolean} asynchronous 
     * @param {!boolean} success 
     * @returns {void}
     */

    /**
     * @callback opo_tests_load_file_callback
     * @param {?string} responseText 
     * @param {!number} status 
     * @param {!number} readyState 
     * @param {!string} message 
     * @param {!boolean} success 
     * @returns {void}
     */

    /**
     * @callback opo_tests_load_file_end_callback
     * @param {!string} message 
     * @returns {void}
     */

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

    /**
     * @callback opo_tests_json_each_callback
     * @param {!(Object.<string, any|null>|Array.<any|null>)} item
     * @param {!opo_tests_default_callback} callback 
     * @returns {void}
     */

    /**
     * @constructs OpoTests
     * @param {?(Object.<string, any|null>|Array.<any|null>)} inputs
     * @returns {void}
     * @access private
     * @static
     */
    const OpoTests = function(inputs = null){

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

        /** @type {Array.<Object.<string, any>>} */
        this.database = [];
        /** @type {Object.<string, string|Array.<string|number|Array.<any|null>>>} */
        this.variables = {};
        /** @type {Object.<string, string|Array.<string|number|Array.<any|null>>>} */
        this.group_variables = {};

        /** @type {SettingsManager} */
        this.settings = new SettingsManager(self, inputs);
        /** @type {I18NManager} */
        this.i18n = new I18NManager(self);
        /** @type {IdentifiersManager} */
        this.identifiers = new IdentifiersManager(self);
        /** @type {Components} */
        this.components = this.comp = new Components(self);
        /** @type {BaseView} */
        this.base = new BaseView(self);
        /** @type {MainFormView} */
        this.main_form = new MainFormView(self);
        /** @type {TestsView} */
        this.tests = new TestsView(self);
        /** @type {UserView} */
        this.user = new UserView(self);
        /** @type {CookiesManager} */
        this.cookies = new CookiesManager(self);
        /** @type {FormatModule} */
        this.format = new FormatModule(self);

        /**
         * @returns {void}
         * @access private
         */
        const end_constructor = () => {
            Utils.execute_array(["default_database_files", "database_files"], (key, next_callback) => {
                Utils.execute_array(self.settings.get(key, null, []), (file, next_callback) => {
                    self.load_file(file, (data, ..._) => {

                        /** @type {Array.<string>|null} */
                        const json = data ? JSON.parse(data) : null;

                        if(Check.is_array(json))
                            load_database(json, () => {
                                next_callback();
                            });
                        else 
                            next_callback();

                    });
                }, next_callback);
            }, () => {
                console.log(["database", self.database, self.variables, self.group_variables]);
                self.main_form.build();
            });
        };

        /**
         * @returns {void}
         * @access private
         */
        const constructor = () => {
            self.settings.start(() => {
                self.i18n.start(() => {
                    self.cookies.start(() => {
                        self.preload(self.settings.get("position", null, "body"), (position, asynchronous, success) => {
                            if(success && position){
                                Utils.html(position, self.base.build());
                                self.user.build_login(end_constructor);
                            };
                        });
                    });
                });
            });
        };

        /**
         * @param {!(string|HTMLElement)} selector 
         * @param {!opo_tests_preload_callback} callback 
         * @param {?(Object.<string, any|null>|Array.<any|null>)} [inputs = null] 
         * @returns {void}
         * @access public
         */
        this.preload = (selector, callback, inputs = null) => {
            if(!Check.is_function(callback))
                return;
            if(Check.is_html_item(selector))
                return Utils.execute(callback, selector, false, true);
            if(!Check.is_string(selector))
                return Utils.execute(callback, null, false, false);

            /** @type {HTMLElement|null} */
            let item;

            try{
                item = document.querySelector(selector)
            }catch(e){
                return Utils.execute(callback, null, false, false);
            };

            if(item)
                return Utils.execute(callback, item, false, true);

            /** @type {number} */
            const date = Date.now(), 
                  /** @type {number} */
                  timeout = self.settings.get(["preload_timeout", "timeout"], inputs, 2000);
            /** @type {number} */
            let interval = setInterval(() => {
                if(item = document.querySelector(selector)){
                    clearInterval(interval);
                    Utils.execute(callback, item, true, true);
                }else if(Date.now() - date >= timeout){
                    clearInterval(interval);
                    Utils.execute(callback, null, true, false);
                };
            }, 1000 / self.settings.get(["frames_per_second", "fps"], inputs, 24));

        };

        /**
         * @param {!string} url 
         * @param {!opo_tests_load_file_callback} callback 
         * @param {?(Object.<string, any|null>|Array.<any|null>)} [inputs = null] 
         * @returns {XMLHttpRequest}
         * @access public
         */
        this.load_file = (url, callback, inputs = null) => {

            /** @type {boolean} */
            let ended = false;
            /** @type {XMLHttpRequest} */
            const ajax = new XMLHttpRequest(), 
                  /** @type {opo_tests_load_file_end_callback} */
                  end = message => {
                      !ended && (ended = true) && Utils.execute(callback, ajax.responseText, ajax.status, ajax.readyState, message, message == "OK");
                  }, 
                  /** @type {number} */
                  date = Date.now(), 
                  /** @type {number} */
                  timeout = self.settings.get(["ajax_timeout", "timeout"], inputs, 2000);

            ajax.open("GET", url, true);
            ajax.timeout = timeout;
            ajax.onload = () => {
                if(ended)
                    return;
                if(ajax.readyState == 4)
                    end((ajax.status >= 200 && ajax.status < 300) || [301, 302, 304].includes(ajax.status) ? "OK" : "HTTP_ERROR");
                else if(Date.now() - date >= timeout)
                    end("FORCED_TIMEOUT");
            };
            ajax.send(null);

            ajax.ontimeout = () => end("TIMEOUT");
            ajax.onerror = () => end("ERROR");
            ajax.onabort = () => end("ABORTED");

            return ajax;
        };

        /**
         * @param {!Object.<string, Array.<[string, ...any|null]>>} set 
         * @returns {void}
         * @access private
         */
        const add_to_variables = (variables, set, group = null) => {
            group && (variables = variables[group] || (variables[group] = {}));
            Check.is_dictionary(set) && 
            Object.entries(set).forEach(([key, value]) => {
                variables[key] = value;
            });
        };

        /**
         * @param {!Array.<string>} files 
         * @param {!opo_tests_default_callback} callback 
         * @returns {void}
         * @access private
         */
        const load_database = (files, callback) => {
            Utils.execute_array(Check.is_array(files) ? files : [files], (file, next_callback) => {
                self.load_file(file, (responseText, status, readyState, message, success) => {
                    if(success && responseText){
                        try{
                            self.database.push(...JSON.parse(responseText).map(item => {

                                /** @type {string} */
                                const group = item.group || Utils.to_snake_case(item.origin + "_" + item.title);

                                add_to_variables(self.variables, item.variables);
                                item.group_variables && add_to_variables(self.group_variables, item.group_variables, item.group || (item.group = group));
                                item.queries = (item.queries || []).filter(subitem => {
                                    if(Check.is_dictionary(subitem)){
                                        add_to_variables(self.variables, subitem.variables);
                                        subitem.group_variables && add_to_variables(self.group_variables, item.group_variables, item.group || (item.group = group));
                                        return true;
                                    };
                                    return false;
                                });
                                return item;
                            }).filter(item => item.queries.length));
                        }catch(exception){
                            console.error([file, exception]);
                        };
                    }else 
                        console.error([file, responseText, message.toLowerCase(), status, readyState]);
                    next_callback();
                });
            }, () => {
                Utils.execute(callback);
            });
        };

        /**
         * @param {!string} message 
         * @param {?opo_tests_default_callback} [callback = null]
         * @param {?string} [default_text = null] 
         * @return {void}
         * @access public
         */
        this.alert = (message, callback, default_text = null) => {
            alert(self.i18n.get(message, null, default_text));
            Utils.execute(callback);
        };

        /**
         * @param {?(Object.<string, any|null>|Array.<any|null>|string)} data 
         * @param {!opo_tests_json_each_callback} each_callback 
         * @param {?opo_tests_default_callback} [end_callback = null] 
         * @param {!boolean} [only_dictionaries = false] 
         * @returns {void}
         * @access public
         */
        this.load_json = (data, each_callback, end_callback = null, only_dictionaries = false) => {
            if(Check.is_array(data)){
                if(only_dictionaries)
                    Utils.execute_array(data, (item, next_callback) => {
                        self.load_json(item, each_callback, next_callback, only_dictionaries);
                    }, end_callback);
                else{
                    Utils.execute(each_callback, data);
                    Utils.execute(end_callback);
                };
            }else if(Check.is_dictionary(data)){
                Utils.execute(each_callback, data);
                Utils.execute(end_callback);
            }else if(Check.is_string(data)){

                /** @type {Array.<any|null>|Object.<string, any|null>|null} */
                let json;

                try{
                    if(json = JSON.parse(data)){
                        self.load_json(json, each_callback, end_callback, only_dictionaries);
                        return;
                    };
                }catch(exception){};

                self.load_file(data, (responseText, status, readyState, message, success) => {
                    if(success && responseText){
                        try{
                            if(json = JSON.parse(responseText)){
                                self.load_json(json, each_callback, end_callback, only_dictionaries);
                                return;
                            };
                        }catch(exception){};
                    };
                    Utils.execute(end_callback);
                });

            }else 
                Utils.execute(end_callback);
        };

        constructor();

    };

    return OpoTests;
})();