"use strict";

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

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

    /**
     * @callback test_view_mode_callback
     * @param {...(any|null)} [parameters] 
     * @returns {string}
     */

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

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

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

        /**
         * @param {!string} name 
         * @param {!number} value 
         * @param {?number} [maximum = null] 
         * @returns {Array.<any>}
         * @access private
         */
        const set_info_value = (name, value, maximum = null) => ["li", {
            data_i18n : name, 
            data_i18n_without : true, 
            title : ot.i18n.get(name)
        }, [
            ot.comp.i18n(name, "strong"),
            ["span", {class : "value"}, "" + value], 
            ...(maximum === null ? [] : [
                ["span", {class : "maximum"}, "" + maximum], 
                ["span", {class : "relative"}, "" + value]
            ])
        ]];

        /**
         * @param {![number, number]} coordenates 
         * @param {!Array.<[string, boolean, boolean]>} originals 
         * @param {!Array.<[string, boolean]>} raw_answers 
         * @param {boolean} right 
         * @returns {void}
         * @access public
         * @static
         */
        const replace_answer = ([i, j], originals, raw_answers, right, number_of_answers) => {

            /** @type {Array.<[string, boolean, boolean]>} */
            const set = originals.filter(([_, ok]) => right ? ok : !ok);

            if(set.length)
                while(true){

                    /** @type {number} */
                    const k = Utils.get_random(raw_answers.length), 
                        /** @type {[string, boolean, boolean]} */
                        [[answer, probability], ok, dynamic] = Utils.get_random(set);

                    if(!probability || Math.random() < probability){

                        /** @type {string} */
                        const string_answer = dynamic ? ot.format.execute([i, j], answer) : answer;

                        if(raw_answers.length < number_of_answers)
                            raw_answers.some(([originals, ok]) => ok && originals == string_answer) || 
                            // originals.some(([[base, __], ok, _]) => ok && base == answer) || 
                            raw_answers.push([string_answer, ok]);
                        else
                            raw_answers.splice(k, 1, [string_answer, ok]);
                        break;
                    };

                };

        };

        /**
         * @param {!Object.<string, number|boolean|Array.<boolean>>} data 
         * @param {?(Object.<string, any|null>|Array.<any|null>)} [inputs = null] 
         * @returns {Array.<any>}
         * @access public
         */
        this.create = (data, inputs = null) => {
            
            /** @type {Array.<[number, number]>} */
            const all_questions = data.blocks.reduce((total, i) => (
                      total.concat(ot.database[i].queries.map((_, j) => [i, j]))
                  ), []), 
                  /** @type {Array.<[number, number]>} */
                  questions = [], 
                  /** @type {number} */
                  number_of_questions = Utils.get_random(data.minimum_questions, data.maximum_questions);

            do{
                questions.push(...all_questions.sort(() => Math.random() - .5));
            }while(data.allow_repeated_questions && questions.length < number_of_questions);
            questions.splice(number_of_questions, questions.length);

            return ["form", {
                class : "test-form", 
                method : "get", 
                action : "#", 
                onsubmit : (item, event) => {
                    event.preventDefault();
                    return false;
                }, 
                data_show_rights : data.show_rights, 
                data_show_results : data.show_results, 
                data_fixed : false, 
                data_lock_mode : data.lock_mode, 
                data_marks_maximum : data.marks_over, 
                data_marks_rest : data.marks_rest, 
                data_marks_decimals : data.marks_decimals
            }, [
                ["fieldset", null, [
                    ot.comp.i18n("test", "legend"), 
                    ot.comp.i18n("test_text", "p"), 
                    ["ul", {class : "test-info"}, [
                        set_info_value("number", questions.length),
                        set_info_value("answered", 0),
                        set_info_value("unanswered", questions.length),
                        set_info_value("rights", 0),
                        set_info_value("wrongs", 0),
                        set_info_value("partially", 0),
                        set_info_value("marks", 0, data.marks_over),
                    ]], 
                    ["section", null, questions.map(([i, j], q) => {

                        /** @type {number} */
                        let x = 0;
                        /** @type {Array.<string>} */
                        const answers = [], 
                              /** @type {Array.<[string, boolean]>} */
                              raw_answers = [], 
                              /** @type {Object.<[string, number|null], Array.<[string, boolean]>>} */
                              originals = ["rights", "wrongs"].reduce((all, key) => {

                                  if(ot.database[i].queries[j][key]){
                                  
                                      /** @type {boolean} */
                                      const ok = key == "rights";

                                      Check.is_array(ot.database[i].queries[j][key]) || 
                                      (ot.database[i].queries[j][key] = [ot.database[i].queries[j][key]]);
  
                                      ot.database[i].queries[j][key].forEach(answer => {
                                          TestsView.add_answer_to(all, answer, ok, false);
                                      });
  
                                      ot.database[i].queries[j].brothers_are_wrongs && 
                                      ot.database[i].queries.forEach((query, k) => {
                                          k == j || query.rights.forEach(answer => {
                                              TestsView.add_answer_to(all, answer, false, true);
                                          });
                                      });
  
                                  };

                                  return all;
                              }, []), 
                              /** @type {Array.<[[string, number|null], boolean, boolean, boolean]>} */
                              wrongs_list = originals.map(([_, ok, __, is_brother], i) => !is_brother && !ok ? i : null).filter(item => item !== null), 
                              /** @type {Object.<[string, number|null], Array.<[string, boolean]>>} */
                              originals_clone = [...originals], 
                              /** @type {number} */
                              number_of_answers = Utils.get_random(data.minimum_answers, data.maximum_answers), 
                              /** @type {boolean} */
                              multiple_answers = data.allow_multiple_answers && (!data.allow_unique_right || Math.random() < .5);

                        const memory = [];

                        for(let y = 0, tries = 0; y < number_of_answers && originals.length; y ++){

                            /** @type {number} */
                            let k = Utils.get_random(originals.length), 
                                /** @type {[[string, number|null], boolean, boolean, boolean]} */
                                [[answer, probability], ok, dynamic, is_brother] = [...originals[k]];

                            if(is_brother && wrongs_list.length && Math.random() < .5){

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

                                [[answer, probability], ok, dynamic] = [...originals[k = wrongs_list[l]]];
                                wrongs_list.splice(l, 1);

                            };

                            if(probability !== null && Math.random() > probability){
                                y --;
                                continue;
                            };

                            if(dynamic)
                                answer = ot.format.execute([i, j], answer);
                            else {
                                originals.splice(k, 1);
                                wrongs_list.forEach((value, i) => {
                                    value >= k && (wrongs_list[i] --);
                                });
                            };

                            memory.push([k, answer]);

                            // console.log([q, tries, y, answer, ok, [...originals], 
                                
                            //     originals.map(([[base, __], right, _]) => (
                            //         right ? base : null
                            //     )).filter(base => base !== null).concat(
                            //         ok ? [] : ot.database[i].queries[j].not || []
                            //     ), 
                                    
                            //     ot.format.check(
                            //         [i, j],
                            //         answer, 
                            //         originals.map(([[base, __], right, _]) => (
                            //             right ? base : null
                            //         )).filter(base => base !== null).concat(
                            //             ok ? [] : ot.database[i].queries[j].not || []
                            //         )
                            //     )[0], 
                                
                            //     raw_answers.some(([base, _]) => answer == base),

                            //     answer && 
                            //     !raw_answers.some(([base, _]) => answer == base) && 
                            //     !ot.format.check(
                            //         [i, j],
                            //         answer, 
                            //         originals.map(([[base, __], right, _]) => (
                            //             right ? base : null
                            //         )).filter(base => base !== null).concat(
                            //             ok ? [] : ot.database[i].queries[j].not || []
                            //         )
                            //     )[0]]
                            // );

                            // console.log(["XXX A", i, j, originals_clone.map(([[base, __], right, _]) => (
                            //         right ? base : null
                            //     )).filter(base => base !== null).concat(
                            //         ok ? [] : ot.database[i].queries[j].not || []
                            //     ), !!answer, !(raw_answers.length && raw_answers.some(([base, _]) => answer == base)), answer]);
                            // console.log(["XXX A", i, j, !!answer, !(raw_answers.length && raw_answers.some(([base, _]) => answer == base)), answer]);

                            // answer && 
                            // !(raw_answers.length && raw_answers.some(([base, _]) => answer == base)) && 
                            // console.log(["XXX B", ...(ok ? ot.format.check(
                            //     [i, j],
                            //     answer, 
                            //     originals_clone.map(([[base, __], right, _]) => (
                            //         right ? base : null
                            //     )).filter(base => base !== null).concat(
                            //         ok ? [] : ot.database[i].queries[j].not || []
                            //     )
                            // ) : [true, 0]), answer]);

                            if(
                                answer && 
                                !(raw_answers.length && raw_answers.some(([base, _]) => answer == base)) && 
                                (ok || !ot.format.check(
                                    [i, j],
                                    answer, 
                                    originals_clone.map(([[base, __], right, _]) => (
                                        right ? base : null
                                    )).filter(base => base !== null).concat(
                                        ot.database[i].queries[j].not || []
                                    )
                                )[0])
                            ){
                                if(ok){
                                    if(!multiple_answers)
                                        for(let z = originals.length - 1; z >= 0; z --)
                                            if(originals[z][1]){
                                                originals.splice(z, 1);
                                                wrongs_list.forEach((value, i) => {
                                                    value >= z && (wrongs_list[i] --);
                                                });
                                            };
                                };
                                raw_answers.push([answer, ok]);
                            }else 
                                ++ tries < 100 && y --;

                        };

                        if(!raw_answers.some(([_, ok]) => ok) && (!data.allow_all_answers_false || !multiple_answers))
                            replace_answer([i, j], originals, raw_answers, true, number_of_answers);
                        else if(!raw_answers.some(([_, ok]) => !ok) && !data.allow_all_answers_true)
                            replace_answer([i, j], originals, raw_answers, false, number_of_answers);

                        raw_answers.length < number_of_answers && console.warn([q, raw_answers, memory]);

                        raw_answers.sort(() => Math.random() - .5).forEach(([answer, ok], i) => {
                            answers.push(answer);
                            ok && (x |= 1 << i);
                        });

                        return ["fieldset", {
                            data_q : q, 
                            data_block : i, 
                            data_question : j, 
                            data_status : "unanswered", 
                            data_x : x
                        }, [
                            ["legend", null, "#" + (q + 1)], 
                            ["ul", {class : "block-info"}, [
                                ["li", null, [
                                    ot.comp.i18n("block", "strong"), 
                                    ["span", null, ot.database[i].title]
                                ]], 
                                ["li", null, [
                                    ot.comp.i18n("question", "strong"), 
                                    ["span", null, "#" + (j + 1)]
                                ]], 
                                ["li", null, [
                                    ot.comp.i18n("status", "strong"), 
                                    ot.comp.i18n("unanswered")
                                ]]
                            ]], 
                            ["p", null, TestsView.to_html(ot.format.execute([i, j], Check.is_array(ot.database[i].queries[j].question) ? Utils.get_random(ot.database[i].queries[j].question) : ot.database[i].queries[j].question))], 
                            ["ul", {class : "answers"}, answers.map((answer, i) => ["li", {data_i : i}, [
                                ot.comp[multiple_answers ? "checkbox" : "radio"]("q" + q + "[]", false, (item, event) => {

                                    /** @type {HTMLFieldSetElement} */
                                    const block = item.parentNode.parentNode.parentNode.parentNode, 
                                          /** @type {HTMLLiElement} */
                                          status_field = block.querySelector("strong[data-i18n=status]+span"), 
                                          /** @type {HTMLFormElement} */
                                          test_box = block.parentNode.parentNode.parentNode;
                                    /** @type {string} */
                                    let status, 
                                        /** @type {number} */
                                        results;

                                    if(item.getAttribute("type") == "radio")
                                        block.querySelectorAll("[type=radio]").forEach(check_answer);
                                    else 
                                        check_answer(item);

                                    results = [...block.querySelectorAll("[data-right]")].reduce((status, item) => {

                                        /** @type {string} */
                                        const status_key = item.getAttribute("data-right");
                                        
                                        return (
                                            status_key == "false" ? status | 1 : 
                                            status_key == "true" ? status | 2 : 
                                            status_key == "null" ? status | 4 : 
                                        status);
                                    }, 0);
                                    status = (
                                        results & 4 || test_box.getAttribute("data-show-results") == "false" ? "answered" : 
                                        results == 2 ? "right" : 
                                        results == 1 || item.getAttribute("type") == "radio" ? "wrong" : 
                                    "partially");

                                    if(status_field.getAttribute("data-i18n") != status){
                                        status_field.setAttribute("data-i18n", status);
                                        status_field.innerText = ot.i18n.get(status);
                                        block.setAttribute("data-status", status);
                                    };
                                    test_box.querySelector(".graphic [data-q='" + block.getAttribute("data-q") + "']").setAttribute("data-status", status);

                                }, TestsView.to_html(answer), null, "answer_" + (i + 1)), 
                                ["span", {data_is_ok : !!(x & (1 << i))}], 
                                ["span", {data_right : null}]
                            ]])]
                        ]];
                    })], 
                    ["ul", {class : "graphic"}, questions.map((_, q) => ["li", {
                        data_q : q, 
                        data_status : "unanswered"
                    }])], 
                    ot.comp.buttons(
                        ["change_rights_mode", change_rights_mode], 
                        ["change_results_mode", change_results_mode], 
                        ["fix", fix], 
                        ["restart", restart], 
                        ["do_other_test", do_other_test]
                    )
                ]]
            ]];
        };

        /**
         * @param {!Object.<string, number|boolean|Array.<boolean>>} data 
         * @param {?(Object.<string, any|null>|Array.<any|null>)} [inputs = null] 
         * @returns {void}
         * @access public
         */
        this.build = (data, inputs = null) => {

            /** @type {HTMLMainElement} */
            const main = document.querySelector("main");

            main.innerHTML = ``;
            Utils.html(main, self.create(data, inputs));

        };

        /**
         * @param {!HTMLInputElement} item 
         * @returns {void}
         * @access private
         */
        const check_answer = item => {

            /** @type {HTMLLiElement} */
            const level = item.parentNode.parentNode, 
                  /** @type {boolean} */
                  is_right = level.querySelector("[data-is-ok]").getAttribute("data-is-ok") == "true";

            level.querySelector("[data-right]").setAttribute("data-right", (item.checked && is_right) || (!item.checked && !is_right));

            setTimeout(() => {

                /** @type {HTMLFormElement} */
                const form = document.querySelector(".test-form"), 
                      /** @type {Object.<string, number>} */
                      values = {number : Number(form.querySelector(".test-info li[data-i18n=number] .value").innerText)}, 
                      /** @type {[number, number, number]} */
                      [maximum, rest, decimals] = ["maximum", "rest", "decimals"].map(key => Number(form.getAttribute("data-marks-" + key)));

                ["rights", "wrongs", "unanswered", "partially"].forEach(name => {
                    form.querySelector(".test-info li[data-i18n=" + name + "] .value").innerText = "" + (values[name] = form.querySelectorAll(".graphic [data-status=" + name.replace(/s$/, "") + "]").length);
                });
                form.querySelector(".test-info li[data-i18n=answered] .value").innerText = "" + (values.answered = form.querySelectorAll(".graphic [data-status]:not([data-status=unanswered])").length);

                form.querySelector(".test-info li[data-i18n=marks] .value").innerText = ((maximum * values.rights / values.number) - values.wrongs * rest - values.partially * rest * .5).toFixed(decimals);
                form.querySelector(".test-info li[data-i18n=marks] .relative").innerText = ((maximum * values.rights / values.answered) - values.wrongs * rest - values.partially * rest * .5).toFixed(decimals);

            }, 1);

        };

        /**
         * @param {!HTMLElement} item 
         * @param {!Event} event 
         * @returns {void}
         * @access private
         */
        const fix = (item, event) => {
            
            /** @type {HTMLFormElement} */
            const form = document.querySelector(".test-form");

            if(form.getAttribute("data-fixed") == "false"){
                form.setAttribute("data-fixed", "true");
                form.querySelectorAll("[type=checkbox],[type=radio]").forEach(input => {
                    input.setAttribute("disabled", true);
                });
            };

        };

        /**
         * @param {!HTMLElement} item 
         * @param {!Event} event 
         * @returns {void}
         * @access private
         */
        const restart = (item, event) => {
            
            /** @type {HTMLFormElement} */
            const form = document.querySelector(".test-form");

            if(form.getAttribute("data-fixed") == "true"){
                form.setAttribute("data-fixed", "false");
                form.querySelectorAll("[type=checkbox],[type=radio]").forEach(input => {
                    input.removeAttribute("disabled");
                    input.checked = false;
                });
                form.querySelectorAll("[data-right]").forEach(span => {
                    span.setAttribute("data-right", "null");
                });
                form.querySelectorAll("strong[data-i18n=status]+span").forEach(span => {
                    span.setAttribute("data-i18n", "unanswered");
                    span.innerText = ot.i18n.get("unanswered");
                });
                form.querySelectorAll("fieldset[data-status],li[data-status]").forEach(fieldset => {
                    fieldset.setAttribute("data-status", "unanswered");
                });
            };

        };

        /**
         * @param {!HTMLElement} item 
         * @param {!Event} event 
         * @returns {void}
         * @access private
         */
        const change_results_mode = (item, event) => {
            
            /** @type {HTMLFormElement} */
            const form = document.querySelector(".test-form");

            form.getAttribute("data-lock-mode") != "true" && 
            form.getAttribute("data-fixed") != "true" && 
            form.setAttribute("data-show-results", form.getAttribute("data-show-results") == "true" ? "false" : "true");

        };

        /**
         * @param {!HTMLElement} item 
         * @param {!Event} event 
         * @returns {void}
         * @access private
         */
        const change_rights_mode = (item, event) => {

            /** @type {HTMLFormElement} */
            const form = document.querySelector(".test-form");

            form.getAttribute("data-lock-mode") != "true" && 
            form.getAttribute("data-fixed") != "true" && 
            form.setAttribute("data-show-rights", form.getAttribute("data-show-rights") == "true" ? "false" : "true");

        };

        /**
         * @param {!HTMLElement} item 
         * @param {!Event} event 
         * @returns {void}
         * @access private
         */
        const do_other_test = (item, event) => {
            ot.main_form.build();
        };

        constructor();

    };

    /**
     * @param {!Array.<[[string, number|null], boolean, boolean]>} answers 
     * @param {!(string|[string, number|null])} answer 
     * @param {!boolean} right 
     * @param {!boolean} [is_brother = false] 
     * @returns {void}
     * @access public
     * @static
     */
    TestsView.add_answer_to = (answers, answer, right, is_brother = false) => {
                                      
        /** @type {[string, number|null]} */
        const [text, probability] = Check.is_array(answer) ? answer : [answer, null];

        answers.some(([[base, __], ..._]) => base == text) || 
        answers.push([[text, probability], right, text.includes("[[") || text.includes("{"), is_brother]);

    };

    TestsView.to_html = string => string.replace(/`{3}([a-z0-9]+)`((?:(?!(?:```))(?:.|[\r\n]+))*)`{3}/gi, (_, language, content) => (`<code data-language="` + language + `">` + [
        [/&/g, "&amp;"],
        [/</g, "&lt;"],
        [/>/g, "&gt;"],
        [/\r\n|\r|\n/g, "<br>"]
    ].reduce((content, [pattern, value]) => content.replace(pattern, value), content) + `</code>`));

    return TestsView;
})();