/* ----------------------------------------------------------------------------

 Online Moodle/Elearning/KMOOC test help
 Greasyfork: <https://greasyfork.org/en/scripts/38999-moodle-elearning-kmooc-test-help>
 GitLab: <https://gitlab.com/MrFry/moodle-test-userscript>

 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program. If not, see <https://www.gnu.org/licenses/>.

 ------------------------------------------------------------------------- */

// ==UserScript==
// @name         Moodle/Elearning/KMOOC test help
// @version      2.0.1.11
// @description  Online Moodle/Elearning/KMOOC test help
// @author       MrFry
// @match        https://elearning.uni-obuda.hu/main/*
// @match        https://elearning.uni-obuda.hu/kmooc/*
// @match        https://mooc.unideb.hu/*
// @match        https://itc.semmelweis.hu/moodle/*
// @match        https://qmining.frylabs.net/*
// @match        http://qmining.frylabs.net/*
// @noframes
// @run-at       document-start
// @grant        GM_getResourceText
// @grant        GM_info
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_xmlhttpRequest
// @grant        GM_openInTab
// @grant        unsafeWindow
// @license      GNU General Public License v3.0 or later
// @supportURL   qmining.frylabs.net
// @contributionURL qmining.frylabs.net
// @namespace    https://qmining.frylabs.net
// @updateURL    https://qmining.frylabs.net/moodle-test-userscript/stable.user.js?up
// ==/UserScript==
//
// TODO:
// grabboxes test on quiz page

(function() {
    // eslint-disable-line
    // GM functions, only to disable ESLINT errors
    /* eslint-disable  */
    const a = Main;
    const usf = unsafeWindow;
    function getVal(name) {
        return GM_getValue(name);
    }
    function setVal(name, val) {
        return GM_setValue(name, val);
    }
    function delVal(name) {
        return GM_deleteValue(name);
    }
    function openInTab(address, options) {
        GM_openInTab(address, options);
    }
    function xmlhttpRequest(opts) {
        GM_xmlhttpRequest(opts);
    }
    function info() {
        return GM_info;
    }
    /* eslint-enable */

    var addEventListener; // add event listener function
    let serverAdress = "https://qmining.frylabs.net/";
    let apiAdress = "https://api.frylabs.net/";
    const ircAddress = "https://kiwiirc.com/nextclient/irc.sub.fm/#qmining";

    // forcing pages for testing. unless you test, do not set these to true!
    // only one of these should be true for testing
    const forceTestPage = false;
    const forceResultPage = false;
    const forceDefaultPage = false;
    const logElementGetting = false;
    const log = true;

    const motdShowCount = 3; /* Ammount of times to show motd */
    let infoExpireTime = 60; // Every n seconds basic info should be loaded from server
    var motd = "";
    var lastestVersion = "";
    var subjInfo;

    if (getVal("ISDEVEL")) {
        infoExpireTime = 1;
        serverAdress = "http://localhost:8080/";
        apiAdress = "http://localhost:80/";
    }

    const huTexts = {
        lastChangeLog: "",
        fatalError:
            "Fatál error. Check console (f12). Kattints az üzenetre az összes kérdés/válaszért manuális kereséshez!",
        consoleErrorInfo:
            "Itteni hibák 100% a moodle hiba. Kivéve, ha oda van írva hogy script error ;) Ha ilyesmi szerepel itt, akkor olvasd el a segítség szekciót!",
        freshStartWarning:
            '<h1>Moodle teszt userscript:<h1><h3>1.5.0 verzió: a script mostantól XMLHTTP kéréseket küld szerver fele! Erre a userscript futtató kiegészítőd is figyelmeztetni fog! Ha ez történik, a script rendes működése érdekében engedélyezd (Always allow domain)! Ha nem akarod, hogy ez történjen, akkor ne engedélyezd, vagy a menüben válaszd ki a "helyi fájl használata" opciót!</h3> <h3>Elküldött adatok: minden teszt után a kérdés-válasz páros. Fogadott adatok: Az összes eddig ismert kérdés. Érdemes help-et elolvasni!!!</h3><h5>Ez az ablak frissités után eltűnik. Ha nem, akkor a visza gombbal próbálkozz.</h5>',
        noResult:
            "Nincs találat :( Kattints az üzenetre az összes kérdés/válaszért manuális kereséshez!",
        videoHelp:
            "Miután elindítottad: Play/pause: space. Seek: Bal/jobb nyíl.",
        menuButtonText: "Kérdések Menu",
        couldntLoadDataPopupMenuText:
            "A kérdéseket nem lehetett beolvasni, ellenőrizd hogy elérhető-e a szerver",
        showGreetingOnEveryPage: "Üdvözlő üzenet mutatása minden oldalon",
        close: "Bezárás",
        help: "Help",
        websiteBugreport: "Weboldal / Bug report",
        contribute: "Contribute",
        donate: "Donate",
        retry: "Újrapróbálás",
        ircButton: "IRC",
        invalidPW: "Hibás jelszó: ",
        search: "Keresés ...",
        loading: "Betöltés ...",
        login: "Belépés",
        requestPWInsteadOfLogin: "Jelszó igénylés",
        contributeTitle: "Hozzájárulás a script és weboldal fejleszétéshez",
        newPWTitle: "Új jelszó új felhasználónak",
        pwRequest: "Új jelszó",
        noServer: "Nem elérhető a szerver!",
        noUser: "Nem vagy bejelentkezve!",
        noServerConsoleMessage: `Nem elérhető a szerver, vagy kis eséllyel kezeletlen hiba történt! Ha elérhető a weboldal, akkor ott meg bírod nézni a kérdéseket itt: ${serverAdress}legacy`,
    };

    var texts = huTexts;

    // : question-classes {{{
    const specialChars = ["&", "\\+"];

    const assert = val => {
        if (!val) {
            throw new Error("Assertion failed");
        }
    };

    class StringUtils {
        RemoveStuff(value, removableStrings, toReplace) {
            removableStrings.forEach(x => {
                var regex = new RegExp(x, "g");
                value = value.replace(regex, toReplace || "");
            });
            return value;
        }

        SimplifyQuery(q) {
            assert(q);

            var result = q.replace(/\n/g, " ").replace(/\s/g, " ");
            return this.RemoveUnnecesarySpaces(result);
        }

        ShortenString(toShorten, ammount) {
            assert(toShorten);

            var result = "";
            var i = 0;
            while (i < toShorten.length && i < ammount) {
                result += toShorten[i];
                i++;
            }
            return result;
        }

        ReplaceCharsWithSpace(val, char) {
            assert(val);
            assert(char);

            var toremove = this.NormalizeSpaces(val);

            var regex = new RegExp(char, "g");
            toremove = toremove.replace(regex, " ");

            return this.RemoveUnnecesarySpaces(toremove);
        }

        // removes whitespace from begining and and, and replaces multiple spaces with one space
        RemoveUnnecesarySpaces(toremove) {
            assert(toremove);

            toremove = this.NormalizeSpaces(toremove);
            while (toremove.includes("  ")) {
                toremove = toremove.replace(/ {2}/g, " ");
            }
            return toremove.trim();
        }

        RemoveSpecialChars(value) {
            assert(value);

            return this.RemoveStuff(value, specialChars, " ");
        }

        // if the value is empty, or whitespace
        EmptyOrWhiteSpace(value) {
            // replaces /n-s with "". then replaces spaces with "". if it equals "", then its empty, or only consists of white space
            if (value === undefined) {
                return true;
            }
            return (
                value
                    .replace(/\n/g, "")
                    .replace(/ /g, "")
                    .replace(/\s/g, " ") === ""
            );
        }

        // damn nonbreaking space
        NormalizeSpaces(input) {
            assert(input);

            return input.replace(/\s/g, " ");
        }

        SimplifyStack(stack) {
            return this.SimplifyQuery(stack);
        }
    }

    const SUtils = new StringUtils();

    // : }}}

    // : DOM getting stuff {{{
    // all dom getting stuff are in this sections, so on
    // moodle dom change, stuff breaks here

    //Stealth by An0 with love
    function StealthOverlay() {
        //call this before the document scripts
        const document = window.document;

        const neverEqualPlaceholder = Symbol(`never equal`); //block probing for undefined values in the hooks
        let shadowRootHost = neverEqualPlaceholder;
        let shadowRootNewHost = neverEqualPlaceholder;

        const apply = Reflect.apply; //save some things in case they get hooked (only for unsafe contexts)

        if (usf.Error.hasOwnProperty("stackTraceLimit")) {
            Reflect.defineProperty(usf.Error, "stackTraceLimit", {
                value: undefined,
                writable: false,
                enumerable: false,
                configurable: false,
            });
        }

        const shadowGetHandler = {
            apply: (target, thisArg, argumentsList) =>
                apply(
                    target,
                    thisArg === shadowRootHost ? shadowRootNewHost : thisArg,
                    argumentsList
                ),
        };

        const original_attachShadow = usf.Element.prototype.attachShadow;
        const attachShadowProxy = new Proxy(
            original_attachShadow,
            shadowGetHandler
        );
        usf.Element.prototype.attachShadow = attachShadowProxy;

        const getShadowRootProxy = new Proxy(
            Object.getOwnPropertyDescriptor(
                usf.Element.prototype,
                "shadowRoot"
            ).get,
            shadowGetHandler
        );
        Object.defineProperty(usf.Element.prototype, "shadowRoot", {
            get: getShadowRootProxy,
        });

        const getHostHandler = {
            apply: function() {
                let result = apply(...arguments);
                return result === shadowRootNewHost ? shadowRootHost : result;
            },
        };
        const getHostProxy = new Proxy(
            Object.getOwnPropertyDescriptor(
                usf.ShadowRoot.prototype,
                "host"
            ).get,
            getHostHandler
        );
        Object.defineProperty(usf.ShadowRoot.prototype, "host", {
            get: getHostProxy,
        });

        const shadowRootSetInnerHtml = Object.getOwnPropertyDescriptor(
            ShadowRoot.prototype,
            "innerHTML"
        ).set;
        const documentFragmentGetChildren = Object.getOwnPropertyDescriptor(
            DocumentFragment.prototype,
            "children"
        ).get;
        const documentGetBody = Object.getOwnPropertyDescriptor(
            Document.prototype,
            "body"
        ).get;
        const nodeAppendChild = Node.prototype.appendChild;

        const overlay = document.createElement("div");
        overlay.style.cssText = "position:absolute;left:0;top:0";

        const addOverlay = () => {
            shadowRootHost = apply(documentGetBody, document, []);
            const shadowRoot = apply(original_attachShadow, shadowRootHost, [
                { mode: "closed" },
            ]);
            apply(shadowRootSetInnerHtml, shadowRoot, [
                `<div><slot></slot></div>`,
            ]);
            shadowRootNewHost = apply(
                documentFragmentGetChildren,
                shadowRoot,
                []
            )[0];
            apply(nodeAppendChild, shadowRoot, [overlay]);
        };

        if (!document.body) {
            document.addEventListener("DOMContentLoaded", addOverlay);
        } else {
            addOverlay();
        }
        return overlay;
    }

    const overlay = StealthOverlay();

    function createHoverOver(target) {
        const overlayElement = document.createElement("div");
        overlayElement.style.cssText =
            "position:fixed; pointer-events: none; user-select: none; z-index:10000";
        overlay.append(overlayElement);
        let currX, currY, currWidth, currHeight;
        const copyBoundingRect = () => {
            let { x, y, width, height } = target.getBoundingClientRect();
            if (x !== currX) {
                overlayElement.style.left = x + "px";
                currX = x;
            }
            if (y !== currY) {
                overlayElement.style.top = y + "px";
                currY = y;
            }
            if (width !== currWidth) {
                overlayElement.style.width = width + "px";
                currWidth = width;
            }
            if (height !== currHeight) {
                overlayElement.style.height = height + "px";
                currHeight = height;
            }
        };
        copyBoundingRect();
        const interval = setInterval(copyBoundingRect, 30);
        overlayElement.destroy = () => {
            clearInterval(interval);
            overlayElement.remove();
        };
        return overlayElement;
    }

    class QuestionsPageModell {
        GetAllQuestionsDropdown() {
            if (logElementGetting) {
                Log("getting dropdown question");
            }
            let items = document
                .getElementById("responseform")
                .getElementsByTagName("p")[0].childNodes;
            let r = "";
            items.forEach(item => {
                if (item.tagName === undefined) {
                    r += item.nodeValue;
                }
            });
            return r;
        }

        GetAllQuestionsQtext() {
            if (logElementGetting) {
                Log("getting all questions qtext");
            }
            return document
                .getElementById("responseform")
                .getElementsByClassName("qtext"); // getting questions
        }

        GetAllQuestionsP() {
            if (logElementGetting) {
                Log("getting all questions by tag p");
            }
            return document
                .getElementById("responseform")
                .getElementsByTagName("p");
        }

        GetFormulationClearfix() {
            if (logElementGetting) {
                Log("getting formulation clearfix lol");
            }
            return document.getElementsByClassName("formulation clearfix");
        }

        GetAnswerOptions() {
            if (logElementGetting) {
                Log("getting all answer options");
            }
            return this.GetFormulationClearfix()[0].childNodes[3].innerText;
        }

        GetQuestionImages() {
            if (logElementGetting) {
                Log("getting question images");
            }
            return this.GetFormulationClearfix()[0].getElementsByTagName("img");
        }

        // this function should return the question, posible answers, and image names
        GetQuestionFromTest() {
            var questions; // the important questions
            var allQuestions; // all questions
            try {
                allQuestions = this.GetAllQuestionsQtext(); // getting questions
                if (allQuestions.length === 0) {
                    var ddq = this.GetAllQuestionsDropdown();
                    if (SUtils.EmptyOrWhiteSpace(ddq)) {
                        var questionData = "";
                        for (var j = 0; j < allQuestions.length; j++) {
                            let subAllQuestions = allQuestions[j].childNodes;
                            for (let i = 0; i < subAllQuestions.length; i++) {
                                if (
                                    subAllQuestions[i].data !== undefined &&
                                    !SUtils.EmptyOrWhiteSpace(
                                        subAllQuestions[i].data
                                    )
                                ) {
                                    questionData +=
                                        subAllQuestions[i].data + " "; // adding text to question data
                                }
                            }
                        }
                        questions = [questionData];
                    } else {
                        questions = [ddq];
                    }
                } else {
                    questions = [];
                    for (let i = 0; i < allQuestions.length; i++) {
                        questions.push(allQuestions[i].innerText);
                    }
                }
            } catch (e) {
                Exception(e, "script error at getting question:");
            }
            var imgNodes = ""; // the image nodes for questions
            try {
                imgNodes = this.GetQuestionImages(); // getting question images, if there is any
                AddImageNamesToImages(imgNodes); // adding image names to images, so its easier to search for, or even guessing
            } catch (e) {
                Log(e);
                Log("Some error with images");
            }

            questions = questions.map(item => {
                if (item) {
                    return SUtils.ReplaceCharsWithSpace(item, "\n");
                }
            });

            return {
                imgnodes: imgNodes,
                allQ: allQuestions,
                q: questions,
            };
        }
    }

    class ResultsPageModell {
        GetFormulationClearfix() {
            if (logElementGetting) {
                Log("getting formulation clearfix lol");
            }
            return document.getElementsByClassName("formulation clearfix");
        }

        GetGrade(i) {
            if (logElementGetting) {
                Log("getting grade");
            }
            const fcf = QPM.GetFormulationClearfix()[i];
            return fcf.parentNode.parentNode.childNodes[0].childNodes[2]
                .innerText;
        }

        DetermineQuestionType(nodes) {
            let qtype = "";
            let i = 0;

            while (i < nodes.length && qtype === "") {
                let inps = nodes[i].getElementsByTagName("input");

                if (inps.length > 0) {
                    qtype = inps[0].type;
                }

                i++;
            }

            return qtype;
        }

        GetSelectAnswer(i) {
            if (logElementGetting) {
                Log("getting selected answer");
            }
            var t = document.getElementsByTagName("select");
            if (t.length > 0) {
                return t[i].options[t[i].selectedIndex].innerText;
            }
        }

        GetCurrQuestion(i) {
            if (logElementGetting) {
                Log("getting curr questions by index: " + i);
            }
            return document.getElementsByTagName("form")[0].childNodes[0]
                .childNodes[i].childNodes[1].childNodes[0].innerText;
        }

        GetFormResult() {
            if (logElementGetting) {
                Log("getting form result");
            }
            var t = document.getElementsByTagName("form")[0].childNodes[0]
                .childNodes;
            if (t.length > 0 && t[0].tagName === undefined) {
                // debreceni moodle
                return document.getElementsByTagName("form")[1].childNodes[0]
                    .childNodes;
            } else {
                return t;
            }
        }

        GetAnswerNode(i) {
            if (logElementGetting) {
                Log("getting answer node");
            }

            var results = this.GetFormResult(); // getting results element

            var r = results[i].getElementsByClassName("answer")[0].childNodes;
            var ret = [];
            for (var j = 0; j < r.length; j++) {
                if (
                    r[j].tagName !== undefined &&
                    r[j].tagName.toLowerCase() === "div"
                ) {
                    ret.push(r[j]);
                }
            }

            let qtype = this.DetermineQuestionType(ret);

            return {
                nodes: ret,
                type: qtype,
            };
        }

        GetCurrentAnswer(i) {
            if (logElementGetting) {
                Log("getting curr answer by index: " + i);
            }
            var results = this.GetFormResult(); // getting results element
            var t = results[i]
                .getElementsByClassName("formulation clearfix")[0]
                .getElementsByTagName("span");
            if (t.length > 2) {
                return t[1].innerHTML.split("<br>")[1];
            }
        }

        GetQText(i) {
            if (logElementGetting) {
                Log("getting qtext by index: " + i);
            }
            var results = this.GetFormResult(); // getting results element
            return results[i].getElementsByClassName("qtext");
        }

        GetDropboxes(i) {
            if (logElementGetting) {
                Log("getting dropboxes by index: " + i);
            }
            var results = this.GetFormResult(); // getting results element
            return results[i].getElementsByTagName("select");
        }

        GetAllAnswer(index) {
            if (logElementGetting) {
                Log("getting all answers, ind: " + index);
            }
            return document.getElementsByClassName("answer")[index].childNodes;
        }

        GetPossibleAnswers(i) {
            if (logElementGetting) {
                Log("getting possible answers");
            }
            var results = this.GetFormResult(); // getting results element
            var items = results[i].getElementsByTagName("label");
            var r = [];
            for (var j = 0; j < items.length; j++) {
                const TryGetCorrect = j => {
                    var cn = items[j].parentNode.className;
                    if (cn.includes("correct")) {
                        return (
                            cn.includes("correct") && !cn.includes("incorrect")
                        );
                    }
                };
                r.push({
                    value: items[j].innerText,
                    iscorrect: TryGetCorrect(j),
                });
            }
            return r;
        }

        GetAnswersFromGrabBox(i) {
            try {
                if (logElementGetting) {
                    Log("testing if question is grab-box");
                }
                let results = this.GetFormResult(); // getting results element
                let t = results[i].getElementsByClassName("dragitems")[0]
                    .childNodes;
                if (t.length !== 1) {
                    Log("grab box drag items group length is not 1!");
                    Log(results[i].getElementsByClassName("dragitems")[0]);
                }
                let placedItems = t[0].getElementsByClassName("placed");
                let res = [];
                for (let i = 0; i < placedItems.length; i++) {
                    let item = placedItems[i];
                    res.push({
                        text: item.innerText,
                        left: item.style.left,
                        top: item.style.top,
                    });
                }
                return res;
            } catch (e) {
                console.info(e);
            }
        }

        GetRightAnswerIfCorrectShown(i) {
            if (logElementGetting) {
                Log("getting right answer if correct shown");
            }
            var results = this.GetFormResult(); // getting results element
            return results[i].getElementsByClassName("rightanswer");
        }

        GetWrongAnswerIfCorrectNotShown(i) {
            if (logElementGetting) {
                Log("getting wrong answer if correct not shown");
            }
            var results = this.GetFormResult(); // getting results element
            var n = results[i].getElementsByTagName("i")[0].parentNode;
            if (n.className.includes("incorrect")) {
                return results[i].getElementsByTagName("i")[0].parentNode
                    .innerText;
            } else {
                return "";
            }
        }

        GetRightAnswerIfCorrectNotShown(i) {
            if (logElementGetting) {
                Log("Getting right answer if correct not shown");
            }
            var results = this.GetFormResult(); // getting results element
            var n = results[i].getElementsByTagName("i")[0].parentNode;
            if (
                n.className.includes("correct") &&
                !n.className.includes("incorrect")
            ) {
                return results[i].getElementsByTagName("i")[0].parentNode
                    .innerText;
            }
        }

        GetFormCFOfResult(result) {
            if (logElementGetting) {
                Log("getting formulation clearfix");
            }
            return result.getElementsByClassName("formulation clearfix")[0];
        }

        GetResultText(i) {
            if (logElementGetting) {
                Log("getting result text");
            }
            var results = this.GetFormResult(); // getting results element
            return this.GetFormCFOfResult(results[i]).getElementsByTagName("p");
        }

        GetResultImage(i) {
            if (logElementGetting) {
                Log("getting result image");
            }
            var results = this.GetFormResult(); // getting results element
            return this.GetFormCFOfResult(results[i]).getElementsByTagName(
                "img"
            );
        }

        // gets the question from the result page
        // i is the index of the question
        GetQuestionFromResult(i) {
            var temp = this.GetQText(i);
            var currQuestion = "";
            if (temp.length > 0) {
                currQuestion = temp[0].innerText; // adding the question to curr question as .q
            } else {
                // this is black magic fuckery a bit
                if (this.GetDropboxes(i).length > 0) {
                    var allNodes = this.GetResultText(i);
                    currQuestion = "";
                    for (var k = 0; k < allNodes.length; k++) {
                        var allQuestions = this.GetResultText(i)[k].childNodes;
                        for (var j = 0; j < allQuestions.length; j++) {
                            if (
                                allQuestions[j].data !== undefined &&
                                !SUtils.EmptyOrWhiteSpace(allQuestions[j].data)
                            ) {
                                currQuestion += allQuestions[j].data + " ";
                            }
                        }
                    }
                } else {
                    try {
                        currQuestion = this.GetCurrQuestion(i);
                    } catch (e) {
                        currQuestion = "REEEEEEEEEEEEEEEEEEEEE"; // this shouldnt really happen sry guys
                        Log("Unable to get question in GetQuestionFromResult");
                    }
                }
            }
            return currQuestion;
        }

        // tries to get right answer from result page
        // i is the index of the question
        GetRightAnswerFromResult(i) {
            var fun = [];

            // "húzza oda ..." skip
            fun.push(i => {
                let temp = RPM.GetAnswersFromGrabBox(i);
                return temp
                    .map(x => {
                        return x.text;
                    })
                    .join(", ");
            });

            // the basic type of getting answers
            fun.push(i => {
                var temp = RPM.GetRightAnswerIfCorrectShown(i); // getting risht answer
                if (temp.length > 0) {
                    return temp[0].innerText;
                } // adding the answer to curr question as .a
            });

            // if there is dropdown list in the current question
            fun.push(i => {
                if (RPM.GetDropboxes(i).length > 0) {
                    return RPM.GetCurrentAnswer(i);
                }
            });

            // if the correct answers are not shown, and the selected answer
            // is correct
            fun.push(i => {
                return RPM.GetRightAnswerIfCorrectNotShown(i);
            });

            // if there is dropbox in the question
            fun.push(i => {
                return RPM.GetSelectAnswer(i);
            });

            // if the correct answers are not shown, and the selected answer
            // is incorrect, and there are only 2 options
            fun.push(i => {
                var possibleAnswers = RPM.GetPossibleAnswers(i);
                if (possibleAnswers.length === 2) {
                    for (var k = 0; k < possibleAnswers.length; k++) {
                        if (possibleAnswers[k].iscorrect === undefined) {
                            return possibleAnswers[k].value;
                        }
                    }
                }
            });

            // if everything fails
            fun.push(i => {
                return undefined;
            });

            var j = 0;
            var currAnswer;
            while (j < fun.length && SUtils.EmptyOrWhiteSpace(currAnswer)) {
                try {
                    currAnswer = fun[j](i);
                } catch (e) {
                    console.info(e);
                }
                j++;
            }

            return currAnswer;
        }

        GuessCorrectIn2LengthAnswersByIncorrect(items) {
            const first = items[0];
            const second = items[1];
            if (first.className.includes("incorrect")) {
                return second.innerText;
            }
            if (second.className.includes("incorrect")) {
                return first.innerText;
            }
        }

        GuessCorrectIn2LengthAnswersByPoints(i, items) {
            const first = {
                elem: items[0],
                val: items[0].childNodes[0].checked,
                text: items[0].innerText,
            };
            const second = {
                elem: items[1],
                val: items[1].childNodes[0].checked,
                text: items[1].innerText,
            };

            const grade = RPM.GetGrade(i); // 1,00 közül 1,00 leosztályozva
            const grades = grade.split(" ").reduce((acc, text) => {
                if (text.includes(",")) {
                    // FIXME: fancy regexp
                    acc.push(parseInt(text));
                } else if (text.includes(".")) {
                    // FIXME: fancy regexp
                    acc.push(parseInt(text));
                }
                return acc;
            }, []);

            if (grades[0] === 1) {
                if (first.val) {
                    return first.text;
                } else {
                    return second.text;
                }
            } else {
                if (!first.val) {
                    return first.text;
                } else {
                    return second.text;
                }
            }
        }

        // version 2 of getting right answer from result page
        // i is the index of the question
        GetRightAnswerFromResultv2(i) {
            try {
                var answerNodes = this.GetAnswerNode(i);
                let items = answerNodes.nodes;

                if (answerNodes.type === "checkbox") {
                    return RPM.GetRightAnswerFromResult(i);
                }

                for (let j = 0; j < items.length; j++) {
                    let cn = items[j].className;
                    if (cn.includes("correct") && !cn.includes("incorrect")) {
                        return items[j].getElementsByTagName("label")[0]
                            .innerText;
                    }
                }
                if (items.length === 2) {
                    const resByIncorrect = this.GuessCorrectIn2LengthAnswersByIncorrect(
                        items
                    );
                    if (!resByIncorrect) {
                        const resPoints = this.GuessCorrectIn2LengthAnswersByPoints(
                            i,
                            items
                        );
                        return resPoints;
                    }
                    return resByIncorrect;
                }
            } catch (e) {
                Log("error at new nodegetting, trying the oldschool way");
            }
        }
    }

    class MiscPageModell {
        GetCurrentSubjectName() {
            if (logElementGetting) {
                Log("getting current subjects name");
            }
            return (
                document
                    .getElementById("page-header")
                    .innerText.split("\n")[0] || ""
            );
        }

        GetVideo() {
            if (logElementGetting) {
                Log("getting video stuff");
            }
            return document.getElementsByTagName("video")[0];
        }

        GetVideoElement() {
            if (logElementGetting) {
                Log("getting video element");
            }
            return document.getElementById("videoElement").parentNode;
        }

        GetInputType(answers, i) {
            if (logElementGetting) {
                Log("getting input type");
            }
            return answers[i].getElementsByTagName("input")[0].type;
        }
    }

    var QPM = new QuestionsPageModell();
    var RPM = new ResultsPageModell();
    var MPM = new MiscPageModell();

    // : }}}

    // : Main function {{{
    let timerStarted = false;

    Main();
    function Main() {
        "use strict";
        console.log("Moodle / E-Learning script");
        console.time("main");
        timerStarted = true;

        if (document.readyState === "loading") {
            document.addEventListener("DOMContentLoaded", Init);
        } else {
            Init();
        }
    }

    function AfterLoad() {
        const url = location.href; // eslint-disable-line

        try {
            if (
                (url.includes("/quiz/") && url.includes("attempt.php")) ||
                forceTestPage
            ) {
                // if the current page is a test
                HandleQuiz();
            } else if (
                (url.includes("/quiz/") && url.includes("review.php")) ||
                forceResultPage
            ) {
                // if the current window is a test-s result
                HandleResults(url);
            } else if (
                (!url.includes("/quiz/") &&
                    !url.includes("review.php") &&
                    !url.includes(".pdf")) ||
                forceDefaultPage
            ) {
                // if the current window is any other window than a quiz or pdf.
                HandleUI(url);
            }
        } catch (e) {
            ShowMessage(
                {
                    m: texts.fatalError,
                    isSimple: true,
                },
                undefined,
                () => {
                    OpenErrorPage(e);
                }
            );

            Exception(e, "script error at main:");
        }
        if (url.includes("eduplayer")) {
            AddVideoHotkeys(url);
        } // adding video hotkeys
        Log(texts.consoleErrorInfo);

        if (timerStarted) {
            console.log("Moodle Test Script run time:");
            console.timeEnd("main");
            timerStarted = false;
        }

        if (forceTestPage || forceResultPage || forceDefaultPage) {
            if (overlay.querySelector("#scriptMessage")) {
                overlay.querySelector("#scriptMessage").style.background =
                    "green";
            }
        }
    }
    // : }}}

    // : Main logic stuff {{{

    // : Loading {{{
    function HandleQminingSite(url) {
        try {
            const idInput = document.getElementById("cid");
            if (idInput) {
                idInput.value = getVal("clientId");
            }
        } catch (e) {
            console.info("Error filling client ID input", e);
        }
        try {
            const sideLinks = document.getElementById("sideBarLinks");
            if (!sideLinks) {
                return;
            }
            Array.from(sideLinks.childNodes).forEach(link => {
                link.addEventListener("mousedown", () => {
                    FillFeedbackCID(url, link);
                });
            });

            FillFeedbackCID(
                url,
                document
                    .getElementById("sideBarLinks")
                    .getElementsByClassName("active")[0]
            );
        } catch (e) {
            console.info("Error filling client ID input", e);
        }
    }

    function FillFeedbackCID(url, link) {
        try {
            if (link.id === "feedback") {
                const cidSetInterval = setInterval(() => {
                    const cid = document.getElementById("cid");
                    if (cid) {
                        cid.value = GetId() + "|" + info().script.version;
                        window.clearInterval(cidSetInterval);
                    }
                }, 100);
            }
        } catch (e) {
            console.info("Error filling client ID input", e);
        }
    }

    function Init() {
        const url = location.href; // eslint-disable-line

        if (url.includes(serverAdress.split("/")[2])) {
            HandleQminingSite(url);
            return;
        }

        // if (false) {
        //     // eslint-disable-line
        //     setVal("version16", undefined);
        //     setVal("version15", undefined);
        //     setVal("firstRun", undefined);
        //     setVal("showQuestions", undefined);
        //     setVal("showSplash", undefined);
        // }
        // --------------------------------------------------------------------------------------
        // event listener fuckery
        // --------------------------------------------------------------------------------------
        try {
            // adding addeventlistener stuff, for the ability to add more event listeners for the same event
            addEventListener = (function() {
                if (document.addEventListener) {
                    return function(element, event, handler) {
                        element.addEventListener(event, handler, false);
                    };
                } else {
                    return function(element, event, handler) {
                        element.attachEvent("on" + event, handler);
                    };
                }
            })();
        } catch (e) {
            Exception(e, "script error at addEventListener:");
        }
        VersionActions();
        if (!url.includes(".pdf")) {
            ShowMenu();
        }
        ConnectToServer(AfterLoad);
    }

    function Auth(pw) {
        SendXHRMessage("login", { pw: pw, script: true }).then(res => {
            if (res.result === "success") {
                ConnectToServer(AfterLoad);
                ClearAllMessages();
                resetMenu();
            } else {
                SafeGetElementById("infoMainDiv", elem => {
                    elem.innerText = texts.invalidPW + pw;
                });
            }
        });
    }

    function resetMenu() {
        SafeGetElementById("menuButtonDiv", elem => {
            elem.style.backgroundColor = "#262626";
        });
        SafeGetElementById("ircButton", elem => {
            elem.style.display = "none";
        });
        SafeGetElementById("retryButton", elem => {
            elem.style.display = "none";
        });
        SafeGetElementById("loginDiv", elem => {
            elem.style.display = "none";
        });
        SafeGetElementById("infoMainDiv", elem => {
            elem.innerText = texts.loading;
        });
    }

    function ConnectToServer(cwith) {
        ClearAllMessages();
        GetXHRInfos()
            .then(inf => {
                if (inf.result === "nouser") {
                    NoUserAction();
                    return;
                }
                lastestVersion = inf.version;
                motd = inf.motd;
                subjInfo = inf.subjinfo;
                overlay.querySelector(
                    "#infoMainDiv"
                ).innerText = `${subjInfo.subjects} tárgy, ${subjInfo.questions} kérdés. Felh #${inf.uid}`;
                // FIXME: if cwith() throws an unhandled error it sais server is not avaible
                cwith();
            })
            .catch(() => {
                NoServerAction();
            });
    }

    function NoUserAction() {
        SafeGetElementById("menuButtonDiv", elem => {
            elem.style.backgroundColor = "#44cc00";
        });
        SafeGetElementById("infoMainDiv", elem => {
            elem.innerText = texts.noUser;
            if (getVal("clientId")) {
                elem.innerText += ` (${getVal("clientId")})`;
            }
        });
        SafeGetElementById("loginDiv", elem => {
            elem.style.display = "";
        });
    }

    function NoServerAction() {
        SafeGetElementById("menuButtonDiv", elem => {
            elem.style.backgroundColor = "red";
        });
        SafeGetElementById("infoMainDiv", elem => {
            elem.innerText = texts.noServer;
        });
        SafeGetElementById("ircButton", elem => {
            elem.style.display = "";
        });
        SafeGetElementById("retryButton", elem => {
            elem.style.display = "";
        });
        Log(texts.noServerConsoleMessage);
    }

    function VersionActions() {
        // FOR TESTING ONLY
        // setVal("version15", true);
        // setVal("firstRun", true);
        // setVal("version16", true);
        // throw "asd";

        FreshStart();
    }

    // : Version action functions {{{

    function FreshStart() {
        var firstRun = getVal("firstRun"); // if the current run is the frst
        if (firstRun === undefined || firstRun === true) {
            setVal("firstRun", false);
            ShowHelp(); // showing help

            document.write(texts.freshStartWarning);
            document.close();
            throw new Error("something, so this stuff stops");
        }
    }

    // : }}}

    // : UI handling {{{
    function HandleUI(url) {
        // FIXME: normal string building with localisation :/
        var newVersion = false; // if the script is newer than last start

        try {
            newVersion = info().script.version !== getVal("lastVerson");
        } catch (e) {
            Log("Some weird error trying to set new verison");
        }

        let showMOTD = false;
        if (!SUtils.EmptyOrWhiteSpace(motd)) {
            var prevmotd = getVal("motd");
            if (prevmotd !== motd) {
                showMOTD = true;
                setVal("motdcount", motdShowCount);
                setVal("motd", motd);
            } else {
                var motdcount = getVal("motdcount");
                if (motdcount === undefined) {
                    setVal("motdcount", motdShowCount);
                    motdcount = motdShowCount;
                }

                motdcount--;
                if (motdcount > 0) {
                    showMOTD = true;
                    setVal("motdcount", motdcount);
                }
            }
        }
        let isNewVersionAvaible =
            lastestVersion !== undefined &&
            info().script.version !== lastestVersion;
        var greetMsg = ""; // message to show at the end
        var timeout = null; // the timeout. if null, it wont be hidden

        if (isNewVersionAvaible || newVersion || showMOTD) {
            greetMsg =
                "Moodle/Elearning/KMOOC segéd v. " +
                info().script.version +
                ". ";
        }
        if (isNewVersionAvaible) {
            timeout = 5;
            greetMsg += "Új verzió elérhető: " + lastestVersion;
            timeout = undefined;
        }
        if (newVersion) {
            // --------------------------------------------------------------------------------------------------------------
            greetMsg +=
                "Verzió frissítve " +
                info().script.version +
                "-re. Changelog:\n" +
                texts.lastChangeLog;
            setVal("lastVerson", info().script.version); // setting lastVersion
        }
        if (showMOTD) {
            greetMsg += "\nMOTD:\n" + motd;
            timeout = null;
        }

        ShowMessage(
            {
                m: greetMsg,
                isSimple: true,
            },
            timeout
        ); // showing message. If "m" is empty it wont show it, thats how showSplash works.
    }

    // : }}}

    // : Answering stuffs {{{

    function HandleQuiz() {
        var q = QPM.GetQuestionFromTest();
        var questions = q.q;
        var imgNodes = q.imgnodes;
        // ------------------------------------------------------------------------------------------------------
        let promises = [];
        questions.forEach(x => {
            let question = SUtils.EmptyOrWhiteSpace(x)
                ? ""
                : SUtils.RemoveUnnecesarySpaces(x); // simplifying question
            promises.push(
                GetXHRQuestionAnswer({
                    q: question,
                    data: GetImageDataFromImgNodes(imgNodes),
                    subj: MPM.GetCurrentSubjectName(),
                })
            );
        });

        // FIXME: promise.all promise resolve order same as original?
        Promise.all(promises).then(res => {
            let answers = [];

            res.forEach((result, j) => {
                var r = PrepareAnswers(result, j);
                if (r !== undefined) {
                    answers.push(r);
                }
                HighLightAnswer(result, j); // highlights the answer for the current result
            });

            ShowAnswers(answers, q.q);
        });
    }

    function PrepareAnswers(result, j) {
        assert(result);

        if (result.length > 0) {
            var allMessages = []; // preparing all messages
            for (var k = 0; k < result.length; k++) {
                var msg = ""; // the current message
                if (
                    getVal("showQuestions") === undefined ||
                    getVal("showQuestions")
                ) {
                    msg += result[k].q.Q + "\n"; // adding the question if yes
                }
                msg += result[k].q.A.replace(/, /g, "\n"); // adding answer
                if (result[k].q.data.type === "image") {
                    msg +=
                        "\n\nKépek fenti válaszok sorrendjében: " +
                        result[k].q.data.images.join(", "); // if it has image part, adding that too
                }
                allMessages.push({
                    m: msg,
                    p: result[k].match,
                });
            }
            return allMessages;
        }
    }

    function ShowAnswers(answers, question) {
        assert(answers);

        if (answers.length > 0) {
            // if there are more than 0 answer
            ShowMessage(answers);
        } else {
            ShowMessage(
                {
                    m: texts.noResult,
                    isSimple: true,
                },
                undefined,
                function() {
                    OpenErrorPage({
                        message: "No result found",
                        stack: JSON.stringify(question),
                    });
                }
            );
        }
    }

    // : }}}

    // : Quiz saving {{{

    function HandleResults(url) {
        SaveQuiz(GetQuiz(), ShowSaveQuizDialog); // saves the quiz questions and answers
    }

    function ShowSaveQuizDialog(sendResult, sentData, newQuestions) {
        // FIXME: normal string building with localisation :/
        var msg = "";
        if (sendResult) {
            msg = "Kérdések elküldve, katt az elküldött adatokért.";
            if (newQuestions > 0) {
                msg += " " + newQuestions + " új kérdés";
            } else {
                msg += " Nincs új kérdés";
            }
        } else {
            msg =
                "Szerver nem elérhető, vagy egyéb hiba kérdések elküldésénél! (F12 -> Console)";
        }
        // showing a message wit the click event, and the generated page
        ShowMessage(
            {
                m: msg,
                isSimple: true,
            },
            null,
            function() {
                let towrite = "";
                try {
                    towrite +=
                        "</p>Elküldött adatok:</p> " + JSON.stringify(sentData);
                } catch (e) {
                    towrite += "</p>Elküldött adatok:</p> " + sentData;
                }
                document.write(towrite);
                document.close();
            }
        );
    }

    // this should get the image url from a result page
    // i is the index of the question
    // FIXME: move this to RPM class ??? and refactor this
    function GetImageFormResult(i) {
        try {
            var imgElements = RPM.GetResultImage(i); // trying to get image
            var imgURL = []; // image urls
            for (var j = 0; j < imgElements.length; j++) {
                if (!imgElements[j].src.includes("brokenfile")) {
                    var filePart = imgElements[j].src.split("/"); // splits the link by "/"
                    filePart = filePart[filePart.length - 1]; // the last one is the image name
                    imgURL.push(decodeURI(SUtils.ShortenString(filePart, 30)));
                }
            }
            if (imgURL.length > 0) {
                return imgURL;
            }
        } catch (e) {
            Log("Couldn't get images from result");
        }
    }

    function GetDataFormResult(i) {
        let data = { type: "simple" };

        let img = GetImageFormResult(i);
        let grabbox = RPM.GetAnswersFromGrabBox(i);
        if (img) {
            data = {
                type: "image",
                images: img,
            };
        }
        if (grabbox) {
            data = {
                type: "grabbox",
                images: img,
                grabbox: grabbox,
            };
        }

        return data;
    }

    // saves the current quiz. questionData contains the active subjects questions
    function SaveQuiz(quiz, next) {
        try {
            let sentData = {};
            if (quiz.length === 0) {
                throw new Error("quiz length is zero!");
            }
            try {
                try {
                    sentData.subj = MPM.GetCurrentSubjectName();
                } catch (e) {
                    sentData.subj = "NOSUBJ";
                    Log("unable to get subject name :c");
                }
                sentData.version = info().script.version;
                sentData.id = GetId();
                sentData.quiz = quiz;
                console.log("SENT DATA", sentData);
                SendXHRMessage("isAdding", sentData).then(res => {
                    next(res.success, sentData, res.newQuestions);
                });
            } catch (e) {
                Exception(e, "error at sending data to server.");
            }
        } catch (e) {
            Exception(e, "script error at saving quiz");
        }
    }

    // getting quiz from finish page
    function GetQuiz() {
        try {
            var quiz = []; // final quiz stuff
            var results = RPM.GetFormResult(); // getting results element
            for (var i = 0; i < results.length - 2; i++) {
                var question = {}; // the current question
                // QUESTION --------------------------------------------------------------------------------------------------------------------
                question.Q = RPM.GetQuestionFromResult(i);

                // RIGHTANSWER ---------------------------------------------------------------------------------------------------------------------
                question.A = RPM.GetRightAnswerFromResultv2(i);
                if (question.A === undefined) {
                    question.A = RPM.GetRightAnswerFromResult(i);
                }
                // DATA ---------------------------------------------------------------------------------------------------------------------
                question.data = GetDataFormResult(i);

                if (question.A !== undefined) {
                    quiz.push(question); // adding current question to quiz
                } else {
                    Log(
                        "error getting queston, no correct answer given, or its incorrect"
                    );
                    Log(question);
                }
            }
            return quiz;
        } catch (e) {
            Exception(e, "script error at quiz parsing:");
        }
    }

    // : }}}

    // : Helpers {{{

    function GetImageDataFromImgNodes(imgs) {
        var questionImages = []; // the array for the image names in question
        for (var i = 0; i < imgs.length; i++) {
            if (!imgs[i].src.includes("brokenfile")) {
                var filePart = imgs[i].src.split("/"); // splits the link by "/"
                filePart = filePart[filePart.length - 1]; // the last one is the image name
                questionImages.push(
                    decodeURI(
                        SUtils.RemoveUnnecesarySpaces(
                            SUtils.ShortenString(filePart, 30)
                        )
                    )
                ); // decodes uri codes, and removes exess spaces, and shortening it
            }
        }
        if (questionImages.length > 0) {
            return {
                type: "image",
                images: questionImages,
            };
        } else {
            return {
                type: "simple",
            };
        }
    }

    // adds image names to image nodes
    function AddImageNamesToImages(imgs) {
        for (var i = 0; i < imgs.length; i++) {
            if (!imgs[i].src.includes("brokenfile")) {
                var filePart = imgs[i].src.split("/"); // splits the link by "/"
                filePart = filePart[filePart.length - 1]; // the last one is the image name
                var appedtTo = imgs[i].parentNode; // it will be appended here
                var mainDiv = document.createElement("div");
                var fileName = SUtils.ShortenString(decodeURI(filePart), 15); // shortening name, couse it can be long as fuck
                var textNode = document.createTextNode("(" + fileName + ")");
                mainDiv.appendChild(textNode);
                appedtTo.appendChild(mainDiv);
            }
        }
    }

    // this function adds basic hotkeys for video controll.
    function AddVideoHotkeys(url) {
        var seekTime = 20;
        document.addEventListener("keydown", function(e) {
            try {
                var video = MPM.GetVideo();
                var keyCode = e.keyCode; // getting keycode
                if (keyCode === 32) {
                    // if the keycode is 32 (space)
                    e.preventDefault(); // preventing default action (space scrolles down)
                    if (video.paused && video.buffered.length > 0) {
                        video.play();
                    } else {
                        video.pause();
                    }
                }
                if (keyCode === 39) {
                    // rigth : 39
                    video.currentTime += seekTime;
                }
                if (keyCode === 37) {
                    // left : 37
                    video.currentTime -= seekTime;
                }
            } catch (err) {
                Log("Hotkey error.");
                Log(err.message);
            }
        });
        var toadd = MPM.GetVideoElement();
        var node = CreateNodeWithText(toadd, texts.videoHelp);
        node.style.margin = "5px 5px 5px 5px"; // fancy margin
    }

    // removes stuff like " a. q1" -> "q1"
    function RemoveLetterMarking(inp) {
        let dotIndex = inp.indexOf(".");
        let doubledotIndex = inp.indexOf(":");
        let maxInd = 4; // inp.length * 0.2;

        if (dotIndex < maxInd) {
            return SUtils.RemoveUnnecesarySpaces(
                inp.substr(inp.indexOf(".") + 1, inp.length)
            );
        } else if (doubledotIndex < maxInd) {
            return SUtils.RemoveUnnecesarySpaces(
                inp.substr(inp.indexOf(":") + 1, inp.length)
            );
        } else {
            return inp;
        }
    }

    // highlights the possible solutions to the current question
    function HighLightAnswer(results, currQuestionNumber) {
        try {
            if (results.length > 0) {
                var answers = RPM.GetAllAnswer(currQuestionNumber); // getting all answers
                var toColor = []; // the numberth in the array will be colored, and .length items will be colored
                var type = ""; // type of the question. radio or ticbox or whatitscalled
                for (let i = 0; i < answers.length; i++) {
                    // going thtough answers
                    if (
                        answers[i].tagName &&
                        answers[i].tagName.toLowerCase() === "div"
                    ) {
                        // if its not null and is "div"
                        var correct = results[0].q.A.toLowerCase(); // getting current correct answer from data
                        var answer = answers[i].innerText
                            .replace(/\n/g, "")
                            .toLowerCase(); // getting current answer

                        // removing stuff like "a."
                        answer = RemoveLetterMarking(answer);

                        if (
                            SUtils.EmptyOrWhiteSpace(correct) ||
                            SUtils.EmptyOrWhiteSpace(answer)
                        ) {
                            continue;
                        }

                        if (
                            SUtils.NormalizeSpaces(
                                SUtils.RemoveUnnecesarySpaces(correct)
                            ).includes(answer)
                        ) {
                            // if the correct answer includes the current answer
                            toColor.push(i); // adding the index
                            type = MPM.GetInputType(answers, i); // setting the type
                        }
                    }
                }
                if (results[0].match === 100) {
                    // if the result is 100% correct
                    if (type !== "radio" || toColor.length === 1) {
                        // FIXME why not radio
                        for (let i = 0; i < toColor.length; i++) {
                            // going through "toColor"
                            let highlight = createHoverOver(
                                answers[toColor[i]]
                            );
                            Object.assign(highlight.style, {
                                border: "7px solid rgba(100, 240, 100, 0.8)",
                                borderRadius: "10px",
                                margin: "-13px 0 0 -8px",
                            });
                        }
                    }
                } // and coloring the correct index
            }
        } catch (e) {
            // catching errors. Sometimes there are random errors, wich i did not test, but they are rare, and does not break the main script.
            Log("script error at highlightin answer: " + e.message);
        }
    }

    // : }}}

    function Log(value) {
        if (log) {
            console.log(value);
        }
    }

    function Exception(e, msg) {
        Log("------------------------------------------");
        Log(msg);
        Log(e.message);
        Log("------------------------------------------");
        Log(e.stack);
        Log("------------------------------------------");
    }

    // : }}}

    // : Minor UI stuff {{{
    function ClearAllMessages() {
        overlay.querySelectorAll("#scriptMessage").forEach(x => x.remove());
    }

    // shows a message with "msg" text, "matchPercent" tip and transp, and "timeout" time
    function ShowMessage(msgItem, timeout, funct) {
        // msgItem help:
        // [ [ {}{}{}{} ] [ {}{}{} ] ]
        // msgItem[] <- a questions stuff
        // msgItem[][] <- a questions relevant answers array
        // msgItem[][].p <- a questions precent
        // msgItem[][].m <- a questions message
        try {
            var defMargin = "0px 5px 0px 5px";
            var isSimpleMessage = false;
            var simpleMessageText = "";
            if (msgItem.isSimple) {
                // parsing msgItem for easier use
                simpleMessageText = msgItem.m;
                if (simpleMessageText === "") {
                    // if msg is empty
                    return;
                }
                msgItem = [
                    [
                        {
                            m: simpleMessageText,
                        },
                    ],
                ];
                isSimpleMessage = true;
            }

            var appedtTo = overlay; // will be appended here
            var width = window.innerWidth - window.innerWidth / 6; // with of the box
            var startFromTop = 25; // top distance

            var mainDiv = document.createElement("div"); // the main divider, wich items will be attached to
            mainDiv.setAttribute("id", "messageMainDiv");
            if (funct) {
                // if there is a function as parameter
                addEventListener(mainDiv, "click", funct); // adding it as click
            }
            // lotsa crap style
            SetStyle(mainDiv, {
                position: "fixed",
                zIndex: 999999,
                textAlign: "center",
                width: width + "px",
                padding: "0px",
                background: "#222d32",
                color: "#ffffff",
                borderColor: "#035a8f",
                border: "none",
                top: startFromTop + "px",
                left: (window.innerWidth - width) / 2 + "px",
                opacity: "0.9",
            });
            mainDiv.setAttribute("id", "scriptMessage");
            var matchPercent = msgItem[0][0].p;
            if (isSimpleMessage) {
                var simpleMessageParagrapg = document.createElement("p"); // new paragraph
                simpleMessageParagrapg.style.margin = defMargin; // fancy margin

                var mesageNode = document.createElement("p"); // new paragraph
                mesageNode.innerHTML = simpleMessageText.replace(
                    /\n/g,
                    "</br>"
                );
                simpleMessageParagrapg.appendChild(mesageNode);
                mesageNode.style.margin = defMargin; // fancy margin

                mainDiv.appendChild(simpleMessageParagrapg); // adding text box to main div
            } else {
                // if its a fucking complicated message
                // TABLE SETUP ------------------------------------------------------------------------------------------------------------
                var table = document.createElement("table");
                table.style.width = "100%";
                // ROWS -----------------------------------------------------------------------------------------------------
                var rowOne = table.insertRow(); // previous suggestion, question text, and prev question
                var rowTwo = table.insertRow(); // next question button
                var rowThree = table.insertRow(); // next suggetsion button
                // CELLS -----------------------------------------------------------------------------------------------------
                // row one
                var numberTextCell = rowOne.insertCell();
                var questionCell = rowOne.insertCell(); // QUESTION CELL
                questionCell.setAttribute("id", "questionCell");
                questionCell.rowSpan = 3;
                questionCell.style.width = "90%";
                var prevQuestionCell = rowOne.insertCell();
                // row two
                var percentTextCell = rowTwo.insertCell();
                var nextQuestionCell = rowTwo.insertCell();
                // row three
                var prevSuggestionCell = rowThree.insertCell();
                var nextSuggestionCell = rowThree.insertCell();
                // adding finally
                mainDiv.appendChild(table);
                // PERCENT TEXT SETUP -----------------------------------------------------------------------------------------------------
                var percentTextBox = CreateNodeWithText(percentTextCell, "");
                percentTextBox.setAttribute("id", "percentTextBox");

                if (matchPercent) {
                    // if match percent param is not null
                    percentTextBox.innerText = matchPercent + "%";
                }
                // NUMBER SETUP -----------------------------------------------------------------------------------------------------
                var numberTextBox = CreateNodeWithText(numberTextCell, "1.");
                numberTextBox.setAttribute("id", "numberTextBox");

                // ANSWER NODE SETUP -------------------------------------------------------------------------------------------------------------
                var questionTextElement = CreateNodeWithText(
                    questionCell,
                    "ur question goes here, mister OwO"
                );
                questionTextElement.setAttribute("id", "questionTextElement");

                // BUTTON SETUP -----------------------------------------------------------------------------------------------------------
                var currItem = 0;
                var currRelevantQuestion = 0;

                const GetRelevantQuestion = () => {
                    // returns the currItemth questions currRelevantQuestionth relevant question
                    return msgItem[currItem][currRelevantQuestion];
                };

                const ChangeCurrItemIndex = to => {
                    currItem += to;
                    if (currItem < 0) {
                        currItem = 0;
                    }
                    if (currItem > msgItem.length - 1) {
                        currItem = msgItem.length - 1;
                    }
                    currRelevantQuestion = 0;
                };

                const ChangeCurrRelevantQuestionIndex = to => {
                    currRelevantQuestion += to;
                    if (currRelevantQuestion < 0) {
                        currRelevantQuestion = 0;
                    }
                    if (currRelevantQuestion > msgItem[currItem].length - 1) {
                        currRelevantQuestion = msgItem[currItem].length - 1;
                    }
                };

                const SetQuestionText = () => {
                    var relevantQuestion = GetRelevantQuestion();
                    questionTextElement.innerText = relevantQuestion.m;
                    if (currItem === 0 && currRelevantQuestion === 0) {
                        numberTextBox.innerText =
                            currRelevantQuestion + 1 + ".";
                    } else {
                        numberTextBox.innerText =
                            currItem +
                            1 +
                            "./" +
                            (currRelevantQuestion + 1) +
                            ".";
                    }
                    percentTextBox.innerText = relevantQuestion.p + "%";
                };

                var buttonMargin = "2px 2px 2px 2px"; // uniform button margin
                if (msgItem[currItem].length > 1) {
                    // PREV SUGG BUTTON ------------------------------------------------------------------------------------------------------------
                    var prevSuggButton = CreateNodeWithText(
                        prevSuggestionCell,
                        "<",
                        "button"
                    );
                    prevSuggButton.style.margin = buttonMargin; // fancy margin

                    prevSuggButton.addEventListener("click", function() {
                        ChangeCurrRelevantQuestionIndex(-1);
                        SetQuestionText();
                    });
                    // NEXT SUGG BUTTON ------------------------------------------------------------------------------------------------------------
                    var nextSuggButton = CreateNodeWithText(
                        nextSuggestionCell,
                        ">",
                        "button"
                    );
                    nextSuggButton.style.margin = buttonMargin; // fancy margin

                    nextSuggButton.addEventListener("click", function() {
                        ChangeCurrRelevantQuestionIndex(1);
                        SetQuestionText();
                    });
                }
                // deciding if has multiple questions ------------------------------------------------------------------------------------------------
                if (msgItem.length === 1) {
                    SetQuestionText();
                } else {
                    // if there are multiple items to display
                    // PREV QUESTION BUTTON ------------------------------------------------------------------------------------------------------------
                    var prevButton = CreateNodeWithText(
                        prevQuestionCell,
                        "^",
                        "button"
                    );
                    prevButton.style.margin = buttonMargin; // fancy margin

                    // event listener
                    prevButton.addEventListener("click", function() {
                        ChangeCurrItemIndex(-1);
                        SetQuestionText();
                    });
                    // NEXT QUESTION BUTTON ------------------------------------------------------------------------------------------------------------
                    var nextButton = CreateNodeWithText(
                        nextQuestionCell,
                        "ˇ",
                        "button"
                    );
                    nextButton.style.margin = buttonMargin; // fancy margin

                    // event listener
                    nextButton.addEventListener("click", function() {
                        ChangeCurrItemIndex(1);
                        SetQuestionText();
                    });
                    SetQuestionText();
                }
            }
            appedtTo.appendChild(mainDiv); // THE FINAL APPEND

            // setting some events
            // addEventListener(window, 'scroll', function () {
            //   mainDiv.style.top = (pageYOffset + startFromTop) + 'px';
            // })
            addEventListener(window, "resize", function() {
                mainDiv.style.left = (window.innerWidth - width) / 2 + "px";
            });
            var timeOut;
            if (timeout && timeout > 0) {
                // setting timeout if not zero or null
                timeOut = setTimeout(function() {
                    mainDiv.parentNode.removeChild(mainDiv);
                }, timeout * 1000);
            }
            // middle click close event listener
            addEventListener(mainDiv, "mousedown", function(e) {
                if (e.which === 2) {
                    mainDiv.parentNode.removeChild(mainDiv);
                    if (timeOut) {
                        clearTimeout(timeOut);
                    }
                }
            });
        } catch (e) {
            Exception(e, "script error at showing message:");
        }
    }

    // shows a fancy menu
    function ShowMenu() {
        try {
            var appedtTo = overlay; // will be appended here

            // mainDiv.style.left = (window.innerWidth - width) / 2 + 'px';

            var menuButtonDiv = document.createElement("div");
            menuButtonDiv.setAttribute("id", "menuButtonDiv");
            SetStyle(menuButtonDiv, {
                width: "600px",
                // height: buttonHeight + 'px',
                top: window.innerHeight - 120 + "px",
                left: "10px",
                zIndex: 999999,
                position: "fixed",
                textAlign: "center",
                padding: "0px",
                margin: "0px",
                background: "#262626",
            });

            var tbl = document.createElement("table");
            tbl.style.margin = "5px 5px 5px 5px";
            tbl.style.textAlign = "left";
            tbl.style.width = "98%";
            menuButtonDiv.appendChild(tbl);

            var buttonRow = tbl.insertRow();
            var buttonCell = buttonRow.insertCell();
            buttonCell.style.textAlign = "center";

            let buttonStyle = {
                position: "",
                margin: "5px 5px 5px 5px",
                border: "none",
                backgroundColor: "#222d32",
                color: "#ffffff",
                cursor: "pointer",
            };
            // help button ----------------------------------------------------------------------------------------------------------------
            let helpButton = CreateNodeWithText(
                buttonCell,
                texts.help,
                "button"
            );
            SetStyle(helpButton, buttonStyle);

            helpButton.addEventListener("click", function() {
                ShowHelp();
            }); // adding clicktextNode

            // site link ----------------------------------------------------------------------------------------------------------------

            let contributeLink = CreateNodeWithText(
                buttonCell,
                texts.contribute,
                "button"
            );
            contributeLink.title = texts.contributeTitle;
            SetStyle(contributeLink, buttonStyle);

            contributeLink.addEventListener("click", function() {
                openInTab(serverAdress + "contribute?scriptMenu", {
                    active: true,
                });
            });

            // pw request ----------------------------------------------------------------------------------------------------------------

            let pwRequest = CreateNodeWithText(
                buttonCell,
                texts.pwRequest,
                "button"
            );
            pwRequest.title = texts.newPWTitle;
            SetStyle(pwRequest, buttonStyle);

            pwRequest.addEventListener("click", function() {
                openInTab(serverAdress + "pwRequest", {
                    active: true,
                });
            });

            // site link ----------------------------------------------------------------------------------------------------------------

            let siteLink = CreateNodeWithText(
                buttonCell,
                texts.websiteBugreport,
                "button"
            );
            SetStyle(siteLink, buttonStyle);

            siteLink.addEventListener("click", function() {
                openInTab(serverAdress + "menuClick", {
                    active: true,
                });
            });

            // donate link ----------------------------------------------------------------------------------------------------------------
            let donateLink = CreateNodeWithText(
                buttonCell,
                texts.donate,
                "button"
            );
            SetStyle(donateLink, buttonStyle);

            donateLink.addEventListener("click", function() {
                openInTab(serverAdress + "donate?scriptMenu", {
                    active: true,
                });
            });

            addEventListener(window, "resize", function() {
                menuButtonDiv.style.top = window.innerHeight - 70 + "px";
            });

            // INFO TABEL --------------------------------------------------------------------
            var itbl = document.createElement("table");
            SetStyle(itbl, {
                margin: "5px 5px 5px 5px",
                textAlign: "left",
                width: "98%",
            });
            menuButtonDiv.appendChild(itbl);
            var ibuttonRow = tbl.insertRow();
            var ibuttonCell = ibuttonRow.insertCell();
            ibuttonCell.style.textAlign = "center";

            // INFO DIV ---------------------------------------------------------------------------------
            let infoDiv = CreateNodeWithText(
                ibuttonCell,
                texts.loading,
                "span"
            );
            infoDiv.setAttribute("id", "infoMainDiv");
            SetStyle(infoDiv, {
                color: "#ffffff",
                margin: "5px",
            });

            // login div ----------------------------------------------------------------------------------------------------------------
            const loginDiv = document.createElement("div");
            loginDiv.style.display = "none";
            loginDiv.setAttribute("id", "loginDiv");
            const loginButton = document.createElement("button");
            loginButton.innerText = texts.login;
            const loginInput = document.createElement("input");
            loginInput.type = "text";
            loginInput.style.width = "400px";
            loginInput.style.textAlign = "center";
            const clientId = getVal("clientId");
            if (clientId && clientId.toString()[0] !== "0") {
                loginInput.value = clientId || "";
                loginButton.innerText = texts.requestPWInsteadOfLogin;
            }
            loginDiv.appendChild(loginInput);
            loginDiv.appendChild(loginButton);

            SetStyle(loginButton, buttonStyle);

            loginInput.addEventListener("keyup", e => {
                console.log(e.target.value);
                if (e.target.value === clientId) {
                    loginButton.innerText = texts.requestPWInsteadOfLogin;
                } else if (e.target.value !== "") {
                    loginButton.innerText = texts.login;
                }
            });

            loginButton.addEventListener("click", function() {
                if (loginInput.value === clientId.toString()) {
                    openInTab(serverAdress + "getVeteranPw?cid=" + clientId, {
                        active: true,
                    });
                } else {
                    Auth(loginInput.value);
                }
            });

            ibuttonCell.appendChild(loginDiv);

            // irc button ----------------------------------------------------------------------------------------------------------------
            let ircButton = CreateNodeWithText(
                ibuttonCell,
                texts.ircButton,
                "button"
            );
            SetStyle(ircButton, buttonStyle);
            ircButton.style.display = "none";
            ircButton.setAttribute("id", "ircButton");

            ircButton.addEventListener("click", function() {
                openInTab(ircAddress, {
                    active: true,
                });
            });

            // retry button ----------------------------------------------------------------------------------------------------------------
            let retryButton = CreateNodeWithText(
                ibuttonCell,
                texts.retry,
                "button"
            );
            SetStyle(retryButton, buttonStyle);
            retryButton.style.display = "none";
            retryButton.setAttribute("id", "retryButton");

            retryButton.addEventListener("click", function() {
                menuButtonDiv.style.background = "#262626";
                infoDiv.innerText = texts.loading;
                retryButton.style.display = "none";
                ircButton.style.display = "none";
                ConnectToServer(AfterLoad);
            });

            // window resize event listener ---------------------------------------
            addEventListener(window, "resize", function() {
                menuButtonDiv.style.top = window.innerHeight - 70 + "px";
            });

            // APPEND EVERYTHING
            appedtTo.appendChild(menuButtonDiv);
        } catch (e) {
            Exception(e, "script error at showing menu:");
        }
    }

    // : }}}

    // : Generic utils {{{
    function GetId() {
        let currId = getVal("clientId");
        if (currId) {
            return currId;
        } else {
            currId = new Date();
            currId =
                currId.getTime() + Math.floor(Math.random() * 1000000000000);
            currId = currId.toString().split("");
            currId.shift();
            currId = "0" + currId.join("");
            setVal("clientId", currId);
            return currId;
        }
    }

    function SafeGetElementById(id, next) {
        let element = overlay.querySelector("#" + id);
        if (element) {
            next(element);
        } else {
            Log(`Unable to safe get element by id: ${id}`);
        }
    }

    function SetStyle(target, style) {
        Object.keys(style)
            .sort()
            .forEach(key => {
                target.style[key] = style[key];
            });
    }

    function CreateNodeWithText(to, text, type) {
        var paragraphElement = document.createElement(type || "p"); // new paragraph
        var textNode = document.createTextNode(text);
        paragraphElement.appendChild(textNode);
        to.appendChild(paragraphElement);
        return paragraphElement;
    }

    function GetXHRInfos() {
        const now = new Date().getTime();
        const lastCheck = getVal("lastInfoCheckTime");
        if (!lastCheck) {
            setVal("lastInfoCheckTime", now);
        }

        let lastInfo = { result: "noLastInfo" };
        try {
            lastInfo = JSON.parse(getVal("lastInfo"));
        } catch (e) {
            console.info(e);
        }
        if (
            lastInfo.result !== "success" ||
            now > lastCheck + infoExpireTime * 1000
        ) {
            return new Promise((resolve, reject) => {
                const url =
                    apiAdress +
                    "infos?version=true&motd=true&subjinfo=true&cversion=" +
                    info().script.version +
                    "&cid=" +
                    GetId();

                xmlhttpRequest({
                    method: "GET",
                    url: url,
                    crossDomain: true,
                    xhrFields: { withCredentials: true },
                    headers: {
                        "Content-Type": "application/json",
                    },
                    onload: function(response) {
                        try {
                            setVal("lastInfoCheckTime", now);
                            const res = JSON.parse(response.responseText);
                            setVal("lastInfo", response.responseText);
                            resolve(res);
                        } catch (e) {
                            Log("Errro paring JSON in GetXHRInfos");
                            Log(response.responseText);
                            Log(e);
                            reject(e);
                        }
                    },
                    onerror: e => {
                        Log("Info get Error", e);
                        reject(e);
                    },
                });
            });
        } else {
            return new Promise((resolve, reject) => {
                try {
                    resolve(lastInfo);
                } catch (e) {
                    Log(
                        "Errro paring JSON in GetXHRInfos, when using old data!"
                    );
                    Log(e);
                    reject(e);
                }
            });
        }
    }

    function GetXHRQuestionAnswer(question) {
        return new Promise((resolve, reject) => {
            let url = apiAdress + "ask?";
            let params = [];
            Object.keys(question).forEach(key => {
                let val = question[key];
                if (typeof val !== "string") {
                    val = JSON.stringify(val);
                }
                params.push(key + "=" + encodeURIComponent(val));
            });
            url +=
                params.join("&") +
                "&cversion=" +
                info().script.version +
                "&cid=" +
                GetId();

            xmlhttpRequest({
                method: "GET",
                url: url,
                onload: function(response) {
                    try {
                        let res = JSON.parse(response.responseText);
                        // FIXME: check if res is a valid answer array
                        // res.json({
                        //   result: r,
                        //   success: true
                        // })
                        // ERROR:
                        // res.json({
                        //   message: `Invalid question :(`,
                        //   result: [],
                        //   recievedData: JSON.stringify(req.query),
                        //   success: false
                        // })
                        resolve(res.result);
                    } catch (e) {
                        reject(e);
                    }
                },
                onerror: e => {
                    Log("Errro paring JSON in GetXHRQuestionAnswer");
                    Log(e);
                    reject(e);
                    reject(e);
                },
            });
        });
    }

    function SendXHRMessage(path, message) {
        // message = SUtils.RemoveSpecialChars(message) // TODO: check this
        if (typeof message === "object") {
            message = JSON.stringify(message);
        }
        const url = apiAdress + path;
        return new Promise((resolve, reject) => {
            xmlhttpRequest({
                method: "POST",
                url: url,
                crossDomain: true,
                xhrFields: { withCredentials: true },
                data: message,
                headers: {
                    "Content-Type": "application/json",
                },
                onerror: function(e) {
                    Log("Data send error", e);
                    reject(e);
                },
                onload: resp => {
                    try {
                        const res = JSON.parse(resp.responseText);
                        resolve(res);
                    } catch (e) {
                        Log("Error paring JSON in SendXHRMessage");
                        Log(resp.responseText);
                        Log(e);
                        reject(e);
                    }
                },
            });
        });
    }

    function OpenErrorPage(e) {
        let path = "lred";
        try {
            if (e.message || e.stack) {
                path += "?";
            }
            if (e.message) {
                path += "msg:" + SUtils.SimplifyQuery(e.message);
            }
            if (e.stack) {
                path += "___stack:" + SUtils.SimplifyStack(e.stack);
            }
            path += "___version:" + info().script.version;
            path = SUtils.RemoveSpecialChars(path);
        } catch (e) {
            Exception(e, "error at setting error stack/msg link");
        }
        path = path.replace(/ /g, "_");
        openInTab(serverAdress + path, {
            active: true,
        });
    }

    // : }}}

    // : Help {{{

    // shows some neat help
    function ShowHelp() {
        openInTab(serverAdress + "manual?scriptMenu", {
            active: true,
        });
    }

    // : }}}

    // I am not too proud to cry that He and he
    // Will never never go out of my mind.
    // All his bones crying, and poor in all but pain,

    // Being innocent, he dreaded that he died
    // Hating his God, but what he was was plain:
    // An old kind man brave in his burning pride.

    // The sticks of the house were his; his books he owned.
    // Even as a baby he had never cried;
    // Nor did he now, save to his secret wound.

    // Out of his eyes I saw the last light glide.
    // Here among the liught of the lording sky
    // An old man is with me where I go

    // Walking in the meadows of his son's eye
    // Too proud to cry, too frail to check the tears,
    // And caught between two nights, blindness and death.

    // O deepest wound of all that he should die
    // On that darkest day.
})(); // eslint-disable-line