"use strict";

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

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

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

    /**
     * @callback components_event_callback
     * @param {!HTMLElement} item 
     * @param {!Event} event 
     * @returns {boolean|void}
     */

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

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

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

        /**
         * @param {!(string|Array.<string>)} i18n 
         * @param {!string} [tag = "span"] 
         * @param {?string} [_default = null] 
         * @returns {[string, Object.<string, string>, string]}
         * @access public
         */
        this.i18n = (i18n, tag = "span", _default = null) => [tag, {data_i18n : i18n}, ot.i18n.get(i18n, null, _default)];

        /**
         * @param {!string} name 
         * @param {!string} [tag = "span"] 
         * @returns {[string, Object.<string, string>]}
         * @access public
         */
        this.icon = (name, tag = "span") => [tag, {data_icon : name}];

        /**
         * @param {!string} name 
         * @param {?components_event_callback} [on_click = null] 
         * @param {!string} [type = "button"] 
         * @param {?string} [default_text = null] 
         * @returns {[string, Object.<string, (string|function)>, Array.<(string|[string, Object.<string, string>, string])>]}
         * @access public
         */
        this.button = (name, on_click = null, type = "button", default_text) => ["button", {
            type : type, 
            data_i18n : name,
            data_i18n_without : true, 
            title : ot.i18n.get(name, null, default_text),
            ...(on_click ? {on_click : on_click} : {})
        }, [
            self.icon(name), 
            self.i18n(name, "span", default_text)
        ]];

        /**
         * @param {!string} type 
         * @param {!string} name 
         * @param {!boolean} checked 
         * @param {?components_event_callback} on_change 
         * @param {?string} default_text 
         * @param {!boolean} [role_button = false] 
         * @returns {[string, Object.<string, (string|function)>, Array.<(string|[string, Object.<string, string>, string])>]}
         * @access private
         */
        const select_item = (type, name, checked, on_change, default_text, role_button = false) => {

            /** @type {string} */
            const id = name.slice(-2) == "[]" ? ot.identifiers.get() : name;

            return ["label", {
                for : id, 
                class : type + "-input", 
                data_i18n : name, 
                data_i18n_without : true, 
                title : ot.i18n.get(name, null, default_text), 
                ...(role_button ? {role : "button"} : {})
            }, [
                ["input", {
                    type : type, 
                    id : id, 
                    name : name, 
                    ...(on_change ? {on_change : on_change} : {}), 
                    ...(checked ? {checked : "checked"} : {})
                }],
                self.icon(type, "span"),
                self.i18n(name, "span", default_text)
            ]];
        };

        /**
         * @param {!string} name 
         * @param {!boolean} [checked = false] 
         * @param {?components_event_callback} [on_change = null] 
         * @param {?string} [default_text = null] 
         * @param {!boolean} [role_button = false] 
         * @returns {[string, Object.<string, (string|function)>, Array.<(string|[string, Object.<string, string>, string])>]}
         */
        this.checkbox = (name, checked = false, on_change = null, default_text, role_button = false) => (
            select_item("checkbox", name, checked, on_change, default_text, role_button)
        );

        /**
         * @param {!string} name 
         * @param {!boolean} [checked = false] 
         * @param {?components_event_callback} [on_change = null] 
         * @param {?string} [default_text = null] 
         * @returns {[string, Object.<string, (string|function)>, Array.<(string|[string, Object.<string, string>, string])>]}
         */
        this.radio = (name, checked = false, on_change = null, default_text) => (
            select_item("radio", name, checked, on_change, default_text)
        );

        /**
         * @param {!string} name 
         * @param {?number} [value = null] 
         * @param {?number} [minimum = null] 
         * @param {?number} [maximum = null] 
         * @param {?number} [step = null] 
         * @param {?components_event_callback} [on_change = null] 
         * @param {?string} [default_text = null] 
         * @returns {[string, Object.<string, (string|number|function)>, Array.<(string|[string, Object.<string, string>, string])>]}
         * @access public
         */
        this.number = (name, value = null, minimum = null, maximum = null, step = null, on_change = null, default_text) => {

            /** @type {string} */
            const text = ot.i18n.get(name, null, default_text);
            
            return ["label", {
                for : name,
                class : "number-input",
                data_i18n : name, 
                data_i18n_without : true, 
                title : text, 
            }, [
                ["input", {
                    type : "number", 
                    id : name, 
                    name : name, 
                    ...(value !== null ? {value : value} : {}), 
                    ...(minimum !== null ? {min : minimum} : {}),
                    ...(maximum !== null ? {max : maximum} : {}),
                    ...(step !== null ? {step : step} : {}),
                    ...(on_change ? {on_change : on_change} : {}), 
                    data_i18n : name, 
                    data_i18n_without : true, 
                    placeholder : text + "..."
                }], 
                ["span", {class : "minimum"}, minimum || "-∞"],
                ["span", {class : "maximum"}, maximum || "∞"]
            ]];
        };

        /**
         * @param {!string} type
         * @param {!string} name 
         * @param {?string} [value = null] 
         * @param {?number} [maximum_length = null] 
         * @param {?string} [pattern = null] 
         * @param {?components_event_callback} [on_change = null] 
         * @param {?string} [default_text = null] 
         * @returns {[string, Object.<string, (string|function)>, Array.<(string|[string, Object.<string, string>, string])>]}
         * @access private
         */
        const text = (type, name, value = null, maximum_length = null, pattern = null, on_change = null, default_text = null) => {

            /** @type {string} */
            const text = ot.i18n.get(name, null, default_text);

            return ["label", {
                for : name, 
                class : "input-" + type
            }, [
                ["input", {
                    type : type, 
                    id : name, 
                    name : name, 
                    ...(value !== null ? {value : value} : {}), 
                    ...(pattern !== null ? {pattern : pattern} : {}),
                    ...(on_change ? {on_input : on_change} : {}), 
                    data_i18n : name, 
                    data_i18n_without : true,
                    placeholder : text + "..."
                }], 
                ["span", {class : "length"}, 0],
                ...(maximum_length ? [["span", {class : "maximum-length"}, maximum_length || "∞"]] : [])
            ]];
        };

        /**
         * @param {!string} name 
         * @param {?string} [value = null] 
         * @param {?number} [maximum_length = null] 
         * @param {?string} [pattern = null] 
         * @param {?components_event_callback} [on_change = null] 
         * @param {?string} [default_text = null] 
         * @returns {[string, Object.<string, (string|function)>, Array.<(string|[string, Object.<string, string>, string])>]}
         * @access public
         */
        this.text = (name, value = null, maximum_length = null, pattern = null, on_change = null, default_text = null) => (
            text("text", name, value, maximum_length, pattern, on_change, default_text)
        );

        /**
         * @param {!string} name 
         * @param {?string} [value = null] 
         * @param {?number} [maximum_length = null] 
         * @param {?string} [pattern = null] 
         * @param {?components_event_callback} [on_change = null] 
         * @param {?string} [default_text = null] 
         * @returns {[string, Object.<string, (string|function)>, Array.<(string|[string, Object.<string, string>, string])>]}
         * @access public
         */
        this.password = (name, value = null, maximum_length = null, pattern = null, on_change = null, default_text = null) => (
            text("password", name, value, maximum_length, pattern, on_change, default_text)
        );

        /**
         * @param  {...[string, components_event_callback|null, string, string|null]} buttons 
         * @returns {[string, Object.<string, string>, Array.<(string|[string, Object.<string, string>, string])>]}
         * @access public
         */
        this.buttons = (...buttons) => ["div", {class : "buttons"}, buttons.map(button => self.button(...button))];

        /**
         * @param {!string} name
         * @param  {...any} items 
         * @returns {[string, Object.<string, string>, Array.<(string|[string, Object.<string, string>, string])>]}
         */
        this.group = (name, ...items) => ["div", {
            class : "group", 
            data_i18n : name, 
            data_i18n_without : true, 
            title : ot.i18n.get(name)
        }, items];

        /**
         * @param {!string} name
         * @param {!Array.<any|null>} structure
         * @param {?components_event_callback} on_submit
         * @param  {...(components_event_callback|null)} [extra_actions] 
         * @returns {[string, Object.<string, (string|function)>, Array.<(string|[string, Object.<string, string>, string])>]}
         * @access public
         */
        this.form = (name, structure, on_submit, ...extra_actions) => ["form", {
            class : "form", 
            data_name : name, 
            method : "get", 
            action : "#",
            ...(on_submit ? {on_submit : on_submit} : {})
        }, [
            ["fieldset", {}, [
                self.i18n(name, "legend"), 
                self.i18n(name + "_text", "p"), 
                ["div", {class : "structure"}, structure.map(([type, name, ...item], i) => {
                    return ["div", {
                        data_i : i, 
                        data_type : type, 
                        data_i18n : name, 
                        data_i18n_without : true,
                        title : ot.i18n.get(name, null, item[1] || name)
                    }, [
                        ["label", {for : name}, [
                            self.i18n(name), 
                            self.i18n(name + "_description")
                        ]], 
                        ["span", {class : "input"}, self[type] ? [self[type](name, ...item)] : item], 
                        ["ul", {class : "errors"}]
                    ]];
                })], 
                ["ul", {class : "form-errors"}], 
                self.buttons(
                    ["clean", null, "clean"], 
                    ...extra_actions, 
                    on_submit ? ["submit", null, "submit"] : null
                )
            ]]
        ]];

        /**
         * @param {!HTMLElement} form 
         * @returns {HTMLFormElement|null}
         * @access public
         */
        this.get_form = form => {

            if(form)
                while(form.tagName.toLowerCase() != "form" && (form = form.parentNode));

            return form;
        };

        /**
         * @param {!HTMLElement} form 
         * @returns {Object.<string, (string|number|boolean|Array.<(string|number|boolean)>)>}
         * @access public
         */
        this.get_form_data = form => (
            [...self.get_form(form).querySelectorAll("[name]")].reduce((data, item) => {

                /** @type {string} */
                let name_value = item.getAttribute("name");
                /** @type {string} */
                const name = name_value.replace(/\[\]$/i, ""), 
                      /** @type {boolean} */
                      is_array = name_value != name;

                is_array && !(name in data) && (data[name] = []);

                switch(item.getAttribute("type")){
                    case "radio":
                        data[name] == [] && (data[name] = false);
                        item.checked && (data[name] = Number(item.value));
                    break;
                    case "checkbox":
                        if(is_array)
                            data[name].push(item.checked);
                        else 
                            data[name] = item.checked;
                    break;
                    case "number":
                        if(is_array)
                            data[name].push(Number(item.value));
                        else 
                            data[name] = Number(item.value);
                    break;
                    default:
                        if(is_array)
                            data[name].push(item.value);
                        else
                            data[name] = item.value;
                    break;
                };

                return data;
            }, {})
        );

        /**
         * @param {!(string|Array.<string>)} sources 
         * @param {?(string|Array.<string>)} [i18n = null] 
         * @param {?string} [default_text = null] 
         * @returns {[string, Object.<string, (string|number|function)>, (string|[string, Object.<string, string>, string])>]}
         * @access public
         */
        this.image = (sources, i18n = null, default_text = null) => {

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

            Check.is_array(sources) || (sources = [sources]);
            
            return ["span", {
                class : "image", 
                data_status : "loading", 
                ...(
                    i18n ? {data_i18n : i18n, title : ot.i18n.get(i18n, null, default_text)} : 
                    default_text ? {title : default_text} : 
                {}), 
            }, [
                ["img", {
                    src : sources[i], 
                    ...(
                        i18n ? {data_i18n : i18n, alt : ot.i18n.get(i18n, null, default_text)} : 
                        default_text ? {alt : default_text} : 
                    {}), 
                    on_error : (image, event) => {
                        if(i >= sources.length)
                            image.parentNode.setAttribute("data-status", "error");
                        else 
                            image.parentNode.setAttribute("src", sources[++ i]);
                    }, 
                    on_load : (image, event) => {
                        image.parentNode.setAttribute("data-status", "loaded");
                        image.parentNode.querySelector("span").style.backgroundImage = "url('" + sources[i] + "')";
                    }
                }], 
                ["span"]
            ]];
        };

        constructor();

    };

    return Components;
})();