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

 Online Moodle/Elearning/KMOOC test help
 Greasyfork: <https://greasyfork.org/en/scripts/38999-moodle-elearning-kmooc-test-help>
 GitLab: <https://gitlab.com/YourFriendlyNeighborhoodDealer/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      1.6.2.4
// @description  Online Moodle/Elearning/KMOOC test help
// @author       YourFriendlyNeighborhoodDealer
// @match        https://elearning.uni-obuda.hu/main/*
// @match        https://elearning.uni-obuda.hu/kmooc/*
// @match        https://mooc.unideb.hu/*
// @grant        GM_getResourceText
// @grant        GM_info
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @grant        GM_openInTab
// @license      GNU General Public License v3.0 or later
// @supportURL	 qmining.tk
// @contributionURL qmining.tk
// @resource     data file:///<file path space is %20, and use "/"-s plz not "\" ty (and add .txt)// UTF-8 PLZ>
// @namespace    https://greasyfork.org/users/153067
// ==/UserScript==

(function() {
	var data; // all data, which is in the resource txt
	var addEventListener; // add event listener function
	const lastChangeLog =
		'- Passzív mód: ha bepipálod a menü gomb alatt, akkor nem tölti be minden alkalommal a kérdéseket (csak csendben vár).\n - Pár lényeges bugfix\n - Ha találkoztok bugokkal, akkor pls report! thanx';
	const serverAdress = "https://qmining.tk/";

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

	const motdShowCount = 3;
	var motd = "";
	var lastestVersion = "";

	const minMatchAmmount = 55;
	const lengthDiffMultiplier = 10;

	//: Class descriptions {{{
	class Question {
		constructor(q, a, i) {
			this.Q = q;
			this.A = a;
			this.I = i;
		}
		toString() {
			var r = "?" + this.Q + "\n!" + this.A;
			if (this.I)
				r += "\n>" + this.I;
			return r;
		}
		HasQuestion() {
			return this.Q != undefined;
		}
		HasAnswer() {
			return this.A != undefined;
		}
		HasImage() {
			return this.I != undefined;
		}
		IsComplete() {
			return this.HasQuestion() && this.HasAnswer();
		}
		Compare(q2, i) {
			if (typeof q2 == 'string') {
				var qmatchpercent = Question.CompareString(this.Q, q2);

				if (i == undefined || i.length == 0)
					return qmatchpercent;
				else {
					if (this.HasImage()) {
						const imatchpercent = this.HasImage() ? Question.CompareString(this.I.join(" "), i.join(" ")) :
							0;
						return (qmatchpercent + imatchpercent) / 2;
					} else {
						qmatchpercent -= 30;
						if (qmatchpercent < 0)
							return 0;
						else
							return qmatchpercent;
					}
				}
			} else {
				const qmatchpercent = Question.CompareString(this.Q, q2.Q);
				const amatchpercent = Question.CompareString(this.A, q2.A);
				if (this.I != undefined) {
					const imatchpercent = this.I == undefined ? Question.CompareString(this.I.join(" "), q2.I.join(
						" ")) : 0;
					return (qmatchpercent + amatchpercent + imatchpercent) / 3;
				} else {
					return (qmatchpercent + amatchpercent) / 2;
				}
			}
		}
		static CompareString(s1, s2) {
			s1 = SimplifyStringForComparison(s1).split(" ");
			s2 = SimplifyStringForComparison(s2).split(" ");
			var match = 0;
			for (var i = 0; i < s1.length; i++)
				if (s2.includes(s1[i]))
					match++;
			var percent = Math.round(((match / s1.length) * 100).toFixed(2)); // matched words percent
			var lengthDifference = Math.abs(s2.length - s1.length);
			percent -= lengthDifference * lengthDiffMultiplier;
			if (percent < 0)
				percent = 0;
			return percent;
		}
	}

	class Subject {
		constructor(n) {
			this.Name = n;
			this.Questions = [];
		}
		get length() {
			return this.Questions.length;
		}
		AddQuestion(q) {
			this.Questions.push(q);
		}
		Search(q, img) {
			var r = [];
			for (var i = 0; i < this.length; i++) {
				let percent = this.Questions[i].Compare(q, img);
				if (percent > minMatchAmmount)
					r.push({
						q: this.Questions[i],
						match: percent
					});
			}

			for (var i = 0; i < r.length; i++)
				for (var j = i; j < r.length; j++)
					if (r[i].match < r[j].match) {
						var tmp = r[i];
						r[i] = r[j];
						r[j] = tmp;
					}

			return r;
		}
		toString() {
			var r = [];
			for (var i = 0; i < this.Questions.length; i++)
				r.push(this.Questions[i].toString());
			return "+" + this.Name + "\n" + r.join("\n");
		}
	}

	class QuestionDB {
		constructor() {
			this.Subjects = [];
		}
		get length() {
			return this.Subjects.length;
		}
		get activeIndexes() {
			var r = [];
			for (var i = 0; i < this.length; i++) {
				if (GM_getValue("Is" + i + "Active")) {
					r.push(i);
				}
			}
			return r;
		}
		GetIfActive(ind) {
			return GM_getValue("Is" + ind + "Active");
		}
		ChangeActive(i, value) {
			GM_setValue("Is" + i + "Active", !!value);
		}
		AddQuestion(subj, q) {
			var i = 0;
			while (i < this.Subjects.length && this.Subjects[i].Name != subj)
				i++;
			if (i < this.Subjects.length)
				this.Subjects[i].AddQuestion(q);
			else {
				const n = new Subject(subj);
				n.AddQuestion(q);
				this.Subjects.push(n);
			}
		}
		Search(q, img) {
			var r = [];
			for (var i = 0; i < this.length; i++)
				if (this.GetIfActive(i))
					r = r.concat(this.Subjects[i].Search(q, img));

			for (var i = 0; i < r.length; i++)
				for (var j = i; j < r.length; j++)
					if (r[i].match < r[j].match) {
						var tmp = r[i];
						r[i] = r[j];
						r[j] = tmp;
					}

			return r;
		}
		AddSubject(subj) {
			var i = 0;
			while (i < this.length && subj.Name != this.Subjects[i].Name)
				i++;

			if (i < this.length) {
				this.Subjects.concat(subj.Questions);
			} else {
				this.Subjects.push(subj);
			}
		}
		toString() {
			var r = [];
			for (var i = 0; i < this.Subjects.length; i++)
				r.push(this.Subjects[i].toString());
			return r.join("\n\n");
		}
	}
	//: }}}

	Main();

	//: Main function {{{
	function Main() {
		'use strict';
		Init(function(count, subjCount) {
			var url = location.href;
			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, count, subjCount);
				}
			} catch (e) {
				ShowMessage({
					m: "Fatál error. Check console (f12). Kattints az üzenetre az összes kérdés/válaszért manuális kereséshez!",
					isSimple: true
				}, undefined, function() {
					GM_openInTab(serverAdress + 'legacy', {
						active: true
					});
				});
				Exception(e, "script error at main:");
			}
			if (url.includes("eduplayer")) // if the current site is a video site
				AddVideoHotkeys(url); // adding video hotkeys
			Log(
				"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! Nagy esélyel a kérdéseket nem lehetett beolvasni."
			);
		});
	}
	//: }}}

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

	function 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;
	}

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

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

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

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

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

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

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

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

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

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

	function 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;
	}

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

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

	function 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;
	}

	function GetCurrentAnswer(i) {
		if (logElementGetting)
			Log("getting curr answer by index: " + i);
		var results = 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];
	}

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

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

		var results = 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 = DetermineQuestionType(ret);

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

	function 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;

	}

	function GetPossibleAnswers(i) {
		if (logElementGetting)
			Log("getting possible answers");
		var results = GetFormResult(); // getting results element
		var items = GetFormResult()[i].getElementsByTagName("label");
		var r = [];
		for (var j = 0; j < items.length; j++) {

			function 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;
	}

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

	function GetWrongAnswerIfCorrectNotShown(i) {
		if (logElementGetting)
			Log("getting wrong answer if correct not shown");
		var results = 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 "";
	}

	function GetRightAnswerIfCorrectNotShown(i) {
		if (logElementGetting)
			Log("Getting right answer if correct not shown");
		var results = 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;
		else
			return "";
	}

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

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

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

	// this function should return the question, posible answers, and image names
	function GetQuestionFromTest() {
		var questions; // the important questions
		var allQuestions; // all questions
		try // trying to get tha current questions
		{
			allQuestions = GetAllQuestionsQtext(); // getting questions
			if (allQuestions.length == 0) // if there are no found questions
			{
				var ddq = GetAllQuestionsDropdown();
				if (EmptyOrWhiteSpace(ddq)) {
					var questionData = "";
					for (var j = 0; j < allQuestions.length; j++) {
						allQuestions = GetAllQuestionsQtext()[j].childNodes;
						for (var i = 0; i < allQuestions.length; i++) {
							if (allQuestions[i].data != undefined && !EmptyOrWhiteSpace(allQuestions[i].data)) // if the current element has some text data to add
							{
								questionData += allQuestions[i].data + " "; // adding text to question data
							}
						}
					}
					questions = [questionData];
				} else
					questions = [ddq];
			} else // if there is
			{
				questions = [];
				for (var 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 = 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, ind) => {
			return ReplaceCharsWithSpace(item, "\n");
		});

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

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

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

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

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

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

		// if there is dropbox in the question
		fun.push(function TryGet3(i) {
			return GetSelectAnswer();
		});

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

		fun.push(function TryGetFinal(i) {
			return undefined;
		});

		var j = 0;
		var currAnswer;
		while (j < fun.length && EmptyOrWhiteSpace(currAnswer)) {
			currAnswer = fun[j](i);
			j++;
		}

		return currAnswer;
	}

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

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

			for (var j = 0; j < items.length; j++) {
				var cn = items[j].className;
				if (cn.includes("correct") && !cn.includes("incorrect"))
					return items[j].innerText;
			}
			if (items.length == 2) {
				for (var j = 0; j < items.length; j++) {
					var cn = items[j].className;
					if (!cn.includes("correct"))
						return items[j].innerText;
				}
			}
		} catch (e) {
			Log("error at new nodegetting, trying the oldschool way");
		}
	}

	//: }}}

	//: Main logic stuff {{{

	//: Loading {{{

	function Init(cwith) {
		if (false) // reset, only for testing!
		{
			GM_setValue("version16", undefined);
			GM_setValue("version15", undefined);
			GM_setValue("firstRun", undefined); // GM_getValue("lastVerson") == undefined => firstrun
			GM_setValue("showQuestions", undefined);
			GM_setValue("showSplash", undefined);
		}
		var url = location.href; // window location
		var count = -1; // loaded question count. stays -1 if the load failed.
		// --------------------------------------------------------------------------------------
		// 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();
		count = Load(cwith); // loads resources
		if (!url.includes(".pdf"))
			ShowMenu();
		return count;
	}

	function VersionActions() {

		//	FOR TESTING ONLY
		//	GM_setValue("version15", true); 
		//	GM_setValue("firstRun", true); 
		//	GM_setValue("version16", true); 
		//	GM_setValue("version161", true);
		//	throw "asd";

		let r = FreshStart();
		if (r != true)
			GM_setValue("version161", false);

		Version15();
		Version16();
		Version161();
	}

	//: Version action functions {{{

	function FreshStart() {
		var firstRun = GM_getValue("firstRun"); // if the current run is the frst
		if (firstRun == undefined || firstRun == true) // if its undefined, or true
		{
			GM_setValue("firstRun", false);
			ShowHelp(); // showing help
			return true;
		}
	}

	function Version15() {
		var version15 = GM_getValue("version15"); // if the current run is the frst
		if (version15 == undefined || version15 == true) // if its undefined, or true
		{
			GM_setValue("useNetDB", "0");
			GM_setValue("version15", false);
			document.write(
				'<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észitő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>'
			);
			document.close();
			throw "something, so this stuff stops";
		}
	}

	function Version16() {
		var version16 = GM_getValue("version16"); // if the current run is the frst
		if (version16 == undefined || version16 == true) // if its undefined, or true
		{
			var i = 0;
			while (GM_getValue("Is" + i + "Active") != undefined) {
				GM_setValue("Is" + i + "Active", false);
				i++;
			}
			GM_setValue("version16", false);
		}
	}

	function Version161() {
		var version161 = GM_getValue("version161"); // if the current run is the frst
		if (version161 == undefined || version161 == true) // if its undefined, or true
		{
			GM_setValue("useNetDB", "0");
			GM_setValue("version161", false);
			document.write(
				'<h1>Moodle teszt userscript:<h1><h3>1.6.1.0 verzió: Új domain név: qmining.tk. Ha frissíted az oldalt, akkor tampremonkey rá fog kérdezni, hpgy engedélyezed-e a kérdések külését erre az új domain-re. A rendes működés érdekében kattints a "Allow always domain"-gombra</h3>'
			);
			document.close();
			throw "something, so this stuff stops";
		}
	}

	//: }}}

	function ReadFile(cwith) {
		var resource = "";
		try {
			resource = GM_getResourceText("data"); // getting data from txt
			if (resource == undefined) {

				ShowMessage({
					m: "Nem lehetett beolvasni a fájlt :c Ellenőrizd az elérési utat, vagy a fájl jogosultságokat",
					isSimple: true
				});
				return;
			}
			if (EmptyOrWhiteSpace(resource)) {
				throw {
					message: "data file empty"
				};
			}
		} catch (e) {
			Exception(e, "script error at reading file:");
		}
		NLoad(resource, cwith);
	}

	function ReadNetDB(cwith, useNetDB) {
		function NewXMLHttpRequest() {
			const url = serverAdress + "data.json";
			GM_xmlhttpRequest({
				method: "GET",
				synchronous: true,
				url: url,
				onload: function(response) {
					NLoad(response.responseText, cwith);
				},
				onerror: function() {
					NLoad(undefined, cwith); // server down
				}
			});
		}
		try {
			Log("Sending XMLHTTP Request...");
			return NewXMLHttpRequest();
		} catch (e) {
			Exception(e, "script error at reading online database:");
		}
	}

	/*
	 * Returns a question database from the given data.
	 * Parameter should be raw read file in string with "\n"-s
	 * */
	function ParseRawData(data) {

		const d = data.split("\n");
		const r = new QuestionDB();
		var logs = [];
		var currSubj = ""; // the current subjects name
		var ExpectedIdentifier = ['+', '?'];
		let currQuestion = new Question();

		var i = -1;
		while (i < d.length) {
			let currIdentifier;
			let skipped = 0;
			do {
				if (skipped >= 1)
					logs.push(i + ": " + d[i]);
				i++;
				if (i >= d.length) {
					if (currQuestion.IsComplete())
						r.AddQuestion(currSubj, currQuestion);
					return {
						result: r,
						logs: logs
					};
				}
				currIdentifier = d[i][0];
				skipped++;
			} while (!ExpectedIdentifier.includes(currIdentifier) && i < d.length);

			let currData = d[i].substring(1).trim();

			if (currIdentifier == '+') {
				if (currQuestion.IsComplete())
					r.AddQuestion(currSubj, currQuestion);
				currQuestion = new Question();
				currSubj = currData;
				ExpectedIdentifier = ['?'];
				continue;
			}

			if (currIdentifier == '?') {
				if (currQuestion.IsComplete()) {
					r.AddQuestion(currSubj, currQuestion);
					currQuestion = new Question();
				}
				// overwriting is allowed here, bcus:
				// ?????!>
				currQuestion.Q = currData;
				ExpectedIdentifier = ['!', '?'];
				continue;
			}

			if (currIdentifier == '!') {
				// if dont have question continue
				if (!currQuestion.HasQuestion())
					throw "No question! (A)";
				// dont allow overwriting
				// ?!!!! 
				if (!currQuestion.HasAnswer()) {
					currData = currData.replace("A helyes válaszok: ", "");
					currData = currData.replace("A helyes válasz: ", "");

					currQuestion.A = currData;
				}
				ExpectedIdentifier = ['?', '>', '+'];
				continue;
			}

			if (currIdentifier == '>') {
				// if dont have question or answer continue
				if (!currQuestion.HasQuestion())
					throw "No question! (I)";
				if (!currQuestion.HasAnswer())
					throw "No asnwer! (I)";
				// dont allow overwriting
				// ?!>>>
				if (!currQuestion.HasImage()) {
					try {
						currQuestion.I = JSON.parse(currData);
					} catch (e) {
						currQuestion.I = [currData];
					}
				}
				ExpectedIdentifier = ['?', '+'];
				continue;
			}
		}

		return {
			result: r,
			logs: logs
		};
	}

	function Load(cwith) {
		var useNetDB = GM_getValue("useNetDB");
		let skipLoad = GM_getValue("skipLoad");

		if (skipLoad)
			return -1;

		if (useNetDB != undefined && useNetDB == 1)
			return ReadNetDB(cwith, useNetDB);
		else
			return ReadFile(cwith);
	}

	function LoadMOTD(resource) {
		try {
			motd = resource.motd;
		} catch (e) {
			Log("Error loading motd :c");
			Log(e);
		}
	}

	function LoadVersion(resource) {
		try {
			lastestVersion = resource.version;
		} catch (e) {
			Log("Error loading version :c");
			Log(e);
		}
	}

	// loading stuff
	function NLoad(resource, cwith) {
		var count = -1;
		var subjCount = 0;
		try {
			var d = {};
			try {
				d = JSON.parse(resource);
			} catch (e) {
				Log("Old data, trying with old methods....");
				try {
					d = ParseRawData(resource).result;
				} catch (e2) {
					Log("Couldt parse data!");
					ShowMessage({
						m: "Nem sikerült betölteni az adatokat! Ellenőriz a megadott fájlt, vagy az internetelérésed!",
						isSimple: true
					});
					return;
				}
			}
			var r = new QuestionDB();
			var rt = [];
			var allCount = -1;
			LoadMOTD(d);
			LoadVersion(d);

			for (var i = 0; i < d.Subjects.length; i++) {
				let s = new Subject(d.Subjects[i].Name);
				if (GM_getValue("Is" + i + "Active")) {
					var j = 0;
					for (j = 0; j < d.Subjects[i].Questions.length; j++) {
						var currQ = d.Subjects[i].Questions[j];
						s.AddQuestion(new Question(currQ.Q, currQ.A, currQ.I));
					}
					rt.push({
						name: d.Subjects[i].Name,
						count: j
					});
					allCount += j;
					subjCount++;
				}
				r.AddSubject(s);
			}
			data = r;
			count = allCount + 1; // couse starting with -1 to show errors

			var i = 0;
			while (i < data.length && !GM_getValue("Is" + i + "Active")) {
				i++;
			}
			if (i >= data.length)
				document.getElementById("HelperMenuButton").style.background = "yellow";

		} catch (e) {
			Exception(e, "script error at loading:");
			count = -1; // returns -1 if error
		}
		cwith(count, subjCount);
	}

	//: }}}

	//: UI handling {{{
	function HandleUI(url, count, subjCount) {
		var newVersion = false; // if the script is newer than last start
		var loaded = count != -1; // if script could load stuff

		try // try, cus im suspicious
		{
			newVersion = GM_info.script.version !== GM_getValue("lastVerson");
		} catch (e) {}
		var greetMsg = ""; // message to show at the end
		var timeout = null; // the timeout. if null, it wont be hidden
		// no new version, nothing loaded
		if (!newVersion && !loaded) // --------------------------------------------------------------------------------------------------------------
		{
			greetMsg =
				"Hiba a @resource tagnál, vagy a fileval van gond! (Lehet át lett helyezve, vagy üres.) Vagy válaszd a netes adatok használatát menüben. Ellenőrizd az elérési utat, vagy hogy a Tampermonkey bővítmény eléri-e a fájlokat. Ha netes forrást használsz, akkor nem elérhető a szerver! Segítségért kattints!";
		}
		var showSplash = (GM_getValue("showSplash") == undefined) || GM_getValue("showSplash"); // getting value, if splash screen should be shown. Its true, if its undefined, or true
		// no new version, everything loaded, and show splash is enabled. otherwise something happened, so showing it
		if (!newVersion && loaded && showSplash) // ------------------------------------------------------------------------------------------------
		{
			timeout = 5;
			greetMsg = "Moodle/Elearning/KMOOC segéd v. " + GM_info.script.version + ". ";

			if (lastestVersion != undefined && GM_info.script.version != lastestVersion) {
				greetMsg += "Új verzió elérhető: " + lastestVersion + " ";
				timeout = undefined;
			}
			greetMsg += count + " kérdés és " + subjCount + " tárgy betöltve. (click for help).";
			if (data.length > 0) {
				var toAdd = [];
				for (var i = 0; i < data.length; i++) {
					if (data.GetIfActive(i)) {
						toAdd.push(data.Subjects[i].Name + " (" + data.Subjects[i].length + ")");
					}
				}
				if (toAdd.length != 0) {
					greetMsg += "\nAktív tárgyak: " + toAdd.join(", ") + ".";
				} else {
					greetMsg += "\nNincs aktív tárgyad. Menüből válassz ki eggyet!";
					timeout = undefined;
				}
			} else {
				greetMsg +=
					" Az adatfájlban nem adtál meg nevet. Vagy nem elérhető a szerver. Katt a helpért!";
			}
		}
		// new version, nothing loaded
		if (newVersion && !loaded) // --------------------------------------------------------------------------------------------------------------
		{
			greetMsg = "Moodle/Elearning/KMOOC segéd v. " + GM_info.script.version +
				". Új verzió!\n Írd át a @resouce tagnál az elírési utat! Kivéve ha üres a file, akkor töltsd fel :) Nincs kérdés betöltve! Segítséghez kattints. Changelog:\n" +
				lastChangeLog; // showing changelog too
		}
		// new version, everything loaded -> set lastVerson to current
		if (newVersion && loaded) // --------------------------------------------------------------------------------------------------------------
		{
			greetMsg = "Moodle/Elearning/KMOOC segéd v. " + GM_info.script.version + ". " + count +
				" kérdés és " + subjCount + " tárgy betöltve. Verzió frissítve " + GM_info.script.version +
				"-re. Changelog:\n" + lastChangeLog;
			GM_setValue("lastVerson", GM_info.script.version); // setting lastVersion
		}
		if (!EmptyOrWhiteSpace(motd)) {

			var prevmotd = GM_getValue("motd");
			if (prevmotd != motd) {
				greetMsg += "\nMOTD:\n" + motd;
				timeout = null;
				GM_setValue("motdcount", motdShowCount);
				GM_setValue("motd", motd);
			} else {
				var motdcount = GM_getValue("motdcount");
				if (motdcount == undefined) {
					GM_setValue("motdcount", motdShowCount);
					motdcount = motdShowCount;
				}

				motdcount--;
				if (motdcount > 0) {
					greetMsg += "\nMOTD:\n" + motd;
					timeout = null;
					GM_setValue("motdcount", motdcount);
				}
			}
		}
		ShowMessage({
			m: greetMsg,
			isSimple: true
		}, timeout, ShowHelp); // showing message. If "m" is empty it wont show it, thats how showSplash works.
	}

	//: }}}

	//: Answering stuffs {{{

	function HandleQuiz() {
		var q = GetQuestionFromTest();
		var questions = q.q;
		var allQuestions = q.allQ;
		var imgNodes = q.imgnodes;
		// ------------------------------------------------------------------------------------------------------
		var answers = [];
		for (var j = 0; j < questions.length; j++) // going thru all answers
		{
			var question = RemoveUnnecesarySpaces(questions[j]); // simplifying question
			var result = data.Search(question, SimplifyImages(imgNodes));
			var r = PrepareAnswers(result, j);
			if (r != undefined)
				answers.push(r);
			HighLightAnswer(result, j); // highlights the answer for the current result
		}
		ShowAnswers(answers);
	}

	function PrepareAnswers(result, j) {
		if (result.length > 0) // if there are more than zero results
		{
			var allMessages = []; // preparing all messages
			for (var k = 0; k < result.length; k++) // going throuh all results
			{
				var msg = ""; // the current message
				if ((GM_getValue("showQuestions") == undefined) || GM_getValue("showQuestions")) // if the question should be shown
				{
					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.HasImage()) // and adding image, if it exists
				{
					msg += "\n" + result[k].q.I; // if it has image part, adding that too
				}
				allMessages.push({
					m: msg,
					p: result[k].match
				});
			}
			return allMessages;
		}
	}

	function ShowAnswers(answers) {
		if (answers.length > 0) { // if there are more than 0 answer
			ShowMessage(answers);
		} else {
			ShowMessage({
				m: "Nincs találat :( Kattints az üzenetre az összes kérdés/válaszért manuális kereséshez! Előfordulhat, hogy a tárgyat nem válsztottad ki a menüben.",
				isSimple: true
			}, undefined, function() {
				GM_openInTab(serverAdress + 'legacy', {
					active: true
				});
			});
		}
	}

	//: }}}

	//: Quiz saving {{{

	function HandleResults(url) {
		var allResults = new QuestionDB();
		SaveQuiz(GetQuiz(), data); // saves the quiz questions and answers
	}

	function ShowSaveQuizDialog(addedQ, allQ, allOutput, output, sendSuccess, sentData) {
		var msg = "";
		if (addedQ > 0) // if there are more than 0 new question
		{
			msg = "Klikk ide a nyers adatokhoz. " + addedQ +
				" új kérdés!";

			var useNetDB = GM_getValue("useNetDB");
			if (useNetDB != undefined && useNetDB == 1) {
				if (!sendSuccess)
					msg += " Nem sikerült kérdéseket elküldeni szervernek. Ha gondolod utánanézhetsz.";
				else
					msg += "Az új kérdések elküldve.";
			} else
				msg += "Ne felejtsd el bemásolni a fő txt-be!";

		} else // if there is 0 or less new question
		{
			msg = "A kérdőívben nincsen új kérdés. Ha mégis le akarod menteni klikk ide.";
			if (!data)
				msg += " Lehet azért, mert nincs kérdés betöltve.";
		}
		// showing a message wit the click event, and the generated page
		ShowMessage({
			m: msg,
			isSimple: true
		}, null, function() {
			var towrite = '<h3>TXT-ben nem szereplő kérdések: ' + addedQ + '/' + allQ + '</h3><br>' +
				output.replace(/\n/g, '<br>') + '<br><h3>Összes kérdés/válasz:</h3>' + allOutput.replace(
					/\n/g, '<br>');

			var useNetDB = GM_getValue("useNetDB");
			if (useNetDB != undefined && useNetDB == 1)
				towrite += "</p>Elküldött adatok:</p> " + sentData;
			document.write(towrite);
			document.close();
		});
	}

	function SearchSameQuestion(questionData, quiz, i) {
		var r = questionData.Search(quiz[i].Q);
		return r.length == 0 ? -1 : r.length;
	}

	// this should get the image url from a result page
	// i is the index of the question
	function GetImageFormResult(i) {
		var temp = null;
		try {
			var imgElements = GetResultImage(i); // trying to get image
			var imgURL = []; // image urls
			for (var j = 0; j < imgElements.length; j++) {
				if (!imgElements[j].src.includes("brokenfile")) // idk why brokenfile is in some urls, which are broken, so why tf are they there damn moodle
				{
					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(ShortenString(filePart, 30)));
				}
			}
			if (imgURL.length > 0) {
				temp = JSON.stringify(imgURL);
				return temp;
			}
		} catch (e) {}
	}

	// saves the current quiz. questionData contains the active subjects questions
	function SaveQuiz(quiz, questionData) {
		try {
			if (quiz.length == 0)
				throw {
					message: "quiz length is zero!",
					stack: "no stack."
				};
			var output = ""; // thefinal output
			var allOutput = ""; // thefinal output with all questions
			var allQ = 0;
			var addedQ = 0;
			var newQuestions = [];
			for (var i = 0; i < quiz.length; i++) // going though quiz
			{
				// searching for same questions in questionData
				var toAdd = ""; // this will be added to some variable depending on if its already in the database
				toAdd += "?" + RemoveUnnecesarySpaces(quiz[i].Q) + "\n"; // adding quiz question
				toAdd += "!" + RemoveUnnecesarySpaces(quiz[i].A) + "\n"; // adding quiz answer
				if (quiz[i].HasImage()) {
					toAdd += ">" + RemoveUnnecesarySpaces(quiz[i].I) + "\n"; // adding quiz image if there is any
				}
				if (SearchSameQuestion(questionData, quiz, i) == -1) // if there is no such item in the database (w/ same q and a)
				{
					output += toAdd; // adding to output
					newQuestions.push(quiz[i]);
					addedQ++;
				}
				allOutput += toAdd; // adding to all
				allQ++;
			}
			var sendSuccess = false;
			var sentData = {};
			try {
				try {
					sentData.subj = GetCurrentSubjectName();
				} catch (e) {
					sentData.subj = "NOSUBJ";
					Log("unable to get subject name :c");
				}
				var useNetDB = GM_getValue("useNetDB");
				if (useNetDB != undefined && useNetDB == 1) {
					sentData.allData = quiz;
					sentData.data = newQuestions;
					sentData.version = GM_info.script.version;
					SendXHRMessage("datatoadd=" + JSON.stringify(sentData));
					sendSuccess = true;
				}
			} catch (e) {
				Exception(e, "error at sending data to server.");
			}
			ShowSaveQuizDialog(addedQ, allQ, allOutput, output, sendSuccess, sentData);
		} catch (e) {
			Exception(e, "script error at saving quiz");
		}
	}


	// getting quiz from finish page
	function GetQuiz() {
		try {
			var quiz = []; // final quiz stuff
			var results = GetFormResult(); // getting results element
			for (var i = 0; i < results.length - 2; i++) // going through results, except last two, idk why, dont remember, go check it man
			{
				var question = {}; // the current question
				// QUESTION --------------------------------------------------------------------------------------------------------------------
				var q = GetQuestionFromResult(i);
				if (q != undefined)
					question.q = SimplifyQuery(q);

				// RIGHTANSWER ---------------------------------------------------------------------------------------------------------------------
				var a = GetRightAnswerFromResultv2(i);
				if (a == undefined)
					a = GetRightAnswerFromResult(i);
				if (a != undefined)
					question.a = SimplifyQuery(a);
				// IMG ---------------------------------------------------------------------------------------------------------------------
				var img = GetImageFormResult(i);
				question.i = img;

				q = ReplaceCharsWithSpace(q, "\n");
				a = ReplaceCharsWithSpace(a, "\n");

				if (question.a != undefined) // adding only if has question
				{
					quiz.push(new Question(question.q, question.a, question.i)); // adding current question to quiz
				} else {
					Log("error getting queston, or its incorrect");
					Log(question);
				}
			}
			return quiz;
		} catch (e) {
			Exception(e, "script error at quiz parsing:");
		}
	}


	//: }}}

	//: Helpers {{{

	function SimplifyImages(imgs) {
		var questionImages = []; // the array for the image names in question
		for (var i = 0; i < imgs.length; i++) // going through all image
		{
			if (!imgs[i].src.includes("brokenfile")) // if its includes borken file its broken. Its another moodle crap. So i just wont check those
			{
				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(RemoveUnnecesarySpaces(ShortenString(filePart, 30)))); // decodes uri codes, and removes exess spaces, and shortening it
			}
		}
		return questionImages;
	}

	// adds image names to image nodes
	function AddImageNamesToImages(imgs) {
		for (var i = 0; i < imgs.length; i++) // going through all image
		{
			if (!imgs[i].src.includes("brokenfile")) // if its includes borken file its broken. Its another moodle crap. So i just wont check those
			{
				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 = 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) // keydown event listener
			{
				try {
					var video = 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 = GetVideoElement();
		var node = CreateNodeWithText(toadd,
			"Miután elindítottad: Play/pause: space. Seek: Bal/jobb nyíl.");
		node.style.margin = "5px 5px 5px 5px"; // fancy margin
	}

	function GetMatchPercent(currData, questionParts) {
		var currQuestion = SimplifyQuery(currData.q); // current question simplified
		var match = 0; // how many times the current question matches the current question in the database
		for (var i = 0; i < questionParts.length; i++) // going through the question parts
		{
			if (currQuestion.includes(questionParts[i])) // if the current question from questionData includes one of the question parts
			{
				match++;
			}
		}
		var percent = Math.round(((match / questionParts.length) * 100).toFixed(2)); // matched words percent
		var lengthDifference = RemoveMultipleItems(SimplifyQuery(currQuestion).split(" ")).length -
			questionParts.length;
		percent -= Math.abs(lengthDifference) * 2;
		return percent;
	}

	// simple sort.
	function SortByPercent(results) {
		for (var i = 0; i < results.length; i++) {
			for (var j = results.length - 1; j > i; j--) {
				if (results[i].p < results[j].p) {
					var temp = results[i];
					results[i] = results[j];
					results[j] = temp;
				}
			}
		}
		return results;
	}

	// 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 RemoveUnnecesarySpaces(inp.substr(inp.indexOf(".") + 1, inp.length));
		else if (doubledotIndex < maxInd)
			return 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) // if there are items in the result
			{
				var answers = 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 (var 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 (EmptyOrWhiteSpace(correct) || EmptyOrWhiteSpace(answer))
							continue;

						if (NormalizeSpaces(RemoveUnnecesarySpaces(correct)).includes(answer)) // if the correct answer includes the current answer
						{
							toColor.push(i); // adding the index
							type = GetInputType(answers, i); // setting the type
						}
					}
				}
				if (results[0].match == 100) // if the result is 100% correct
					if (type !== "radio" || toColor.length == 1) // if the type is not radio or there is exactly one correct solution
						for (var i = 0; i < toColor.length; i++) // going through "toColor"
							answers[toColor[i]].style.backgroundColor = "#8cff66"; // 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 {{{

	// 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 == "") {
					return;
				}
				msgItem = [
					[{
						m: simpleMessageText
					}]
				];
				isSimpleMessage = true;
			}

			var appedtTo = document.body; // will be appended here
			var width = window.innerWidth - window.innerWidth / 6; // with of the box
			var startFromLeft = window.innerWidth / 2 - width / 2; // dont change this
			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
			mainDiv.style.position = "fixed";
			mainDiv.style.zIndex = 999999;
			mainDiv.style.textAlign = "center";
			mainDiv.style.width = width + 'px';
			//mainDiv.style.height = height + 'px';
			mainDiv.style.padding = "0px";
			mainDiv.style.background = "#222d32"; // background color
			mainDiv.style.color = "#ffffff"; // text color
			mainDiv.style.borderColor = "#035a8f"; // border color
			mainDiv.style.border = "solid";
			mainDiv.style.top = (startFromTop) + 'px';
			mainDiv.style.left = (window.innerWidth - width) / 2 + 'px';
			mainDiv.style.opacity = "0.9"; // setting starting opacity
			var matchPercent = msgItem[0][0].p;
			if (isSimpleMessage) {
				var simpleMessageParagrapg = document.createElement("p"); // new paragraph
				simpleMessageParagrapg.style.margin = defMargin; // fancy margin
				var splitText = simpleMessageText.split("\n");
				for (var i = 0; i < splitText.length; i++) {
					var mesageNode = CreateNodeWithText(simpleMessageParagrapg, splitText[i]);
					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;

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

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

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

				function 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 buttonWidth = 100; // button size ;)
			var buttonHeight = 85;
			var appedtTo = document.body; // will be appended here

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

			var menuButtonDiv = document.createElement("div");
			menuButtonDiv.setAttribute("id", "HelperMenuButton");
			menuButtonDiv.style.width = buttonWidth + 'px';
			menuButtonDiv.style.height = buttonHeight + 'px';
			menuButtonDiv.style.top = (window.innerHeight - buttonHeight * 1.5) + 'px';
			menuButtonDiv.style.left = window.innerWidth - buttonWidth * 1.5 + 'px';
			menuButtonDiv.style.zIndex = 999999; // TO THE MAX
			menuButtonDiv.style.position = "fixed";
			// menuButtonDiv.style.borderStyle = "solid";
			// menuButtonDiv.style.borderWidth = "1px";

			// design
			menuButtonDiv.style.textAlign = "center";
			menuButtonDiv.style.padding = "0px";
			menuButtonDiv.style.margin = "0px";
			menuButtonDiv.style.background = "transparent"; // background color

			// menu text
			// var menuTextBox = CreateNodeWithText(menuButtonDiv, "Kérdések\nMenü");

			var menuButton = CreateNodeWithText(menuButtonDiv, "Kérdések Menu", "button");
			menuButton.style.width = buttonWidth + 'px';
			menuButton.style.border = 'none';
			menuButton.style.height = buttonHeight - 20 + 'px';
			menuButton.style.background = "#222d32"; // background color
			menuButton.style.color = "#ffffff"; // background color



			menuButton.addEventListener("click", function() {
				if (document.getElementById("HelperMenu") == null) {
					ShowMenuList();
				} else {
					CloseMenu();
				}
			}); // adding click

			// passive mode stuff
			var questionsTickBox = document.createElement("input");
			questionsTickBox.type = "checkbox";
			questionsTickBox.checked = GM_getValue("skipLoad");
			questionsTickBox.style.position = "";
			questionsTickBox.style.left = 10 + 'px';
			questionsTickBox.style.margin = "5px 5px 5px 5px"; // fancy margin
			questionsTickBox.style.top = 0 + 'px';

			menuButtonDiv.appendChild(questionsTickBox); // adding to main div

			questionsTickBox.addEventListener("click", function() {
				GM_setValue("skipLoad", questionsTickBox.checked);
				if (GM_getValue("skipLoad")) {
					ShowMessage({
						m: "Passzív mód bekapcsolva, mostantól kérdések nem lesznek betöltve/lekérve.",
						isSimple: true
					}, 10);
				}

			});
			var loadDataCheckBoxText = CreateNodeWithText(questionsTickBox,
				"Passzív mód", "span");
			loadDataCheckBoxText.style.fontSize = "12px";

			menuButtonDiv.appendChild(loadDataCheckBoxText);

			addEventListener(window, 'resize', function() {
				menuButtonDiv.style.left = window.innerWidth - buttonWidth * 2 + 'px';
			});

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

	// shows a fancy menu list with the subjects
	function ShowMenuList() {
		try {
			var appedtTo = document.body; // will be appended here

			var menuDiv = document.createElement("div");
			menuDiv.setAttribute("id", "HelperMenu");
			menuDiv.style.width = (window.innerWidth / 2) + 'px';
			menuDiv.style.top = (window.innerHeight / 10) + 'px';
			menuDiv.style.left = window.innerWidth / 2 - (window.innerWidth / 2) / 2 + 'px';
			menuDiv.style.zIndex = 999999;
			menuDiv.style.position = "fixed";

			//design
			menuDiv.style.textAlign = "center";
			menuDiv.style.padding = "0px";
			menuDiv.style.background = "#222d32"; // background color
			menuDiv.style.color = "#ffffff"; // text color
			menuDiv.style.borderColor = "#035a8f"; // border color
			menuDiv.style.border = "solid";
			menuDiv.style.opacity = "1"; // setting starting opacity

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

			// adding headers ---------------------------------------------------------------------------------------------------------------
			var subjTable = document.createElement('table');
			subjTable.style.margin = fiveMargin;
			subjTable.style.textAlign = "left";
			subjTable.style.width = "98%";

			var tr = subjTable.insertRow();
			var header1 = tr.insertCell();

			var headerSubjInfoParagraph = CreateNodeWithText(header1, "Tárgynév [darab kérdés]", "center");
			headerSubjInfoParagraph.style.margin = fiveMargin; // fancy margin

			var header2 = tr.insertCell();
			var headerSubjInfoParagraph = CreateNodeWithText(header2, "Aktív");
			headerSubjInfoParagraph.style.margin = fiveMargin; // fancy margin

			if (data && data.length > 0) {

				for (let i = 0; i < data.length; i++) {
					var subjRow = subjTable.insertRow();
					subjRow.style.border = "1px solid #131319";

					var td = subjRow.insertCell();
					var text = data.Subjects[i].Name;
					if (data.Subjects[i].length != 0)
						text += " [ " + data.Subjects[i].length + "db ]";

					var textBox = CreateNodeWithText(td, text);

					textBox.style.margin = fiveMargin; // fancy margin

					td = subjRow.insertCell();
					var checkbox = document.createElement("input"); // new paragraph
					checkbox.type = "checkbox";
					checkbox.style.background = "white";
					checkbox.style.margin =
						"5px 5px 5px 5px"; // fancy margin
					td.appendChild(checkbox); // adding text box to main td

					var active = data.GetIfActive(i);
					checkbox.checked = active;

					checkbox.setAttribute("id", "HelperTextNode" + i);

					checkbox.addEventListener("click", function() {
						var checked = document.getElementById("HelperTextNode" + i).checked;
						data.ChangeActive(i, checked);
					}); // adding click
				}

				var scrollDiv = document.createElement("div");
				scrollDiv.style.width = '100%';
				scrollDiv.style.height = window.innerHeight - (window.innerHeight * 0.4) + "px";
				scrollDiv.style.overflow = "auto";

				scrollDiv.appendChild(subjTable);

				var subjtblrow = tbl.insertRow();
				var subjtbltd = subjtblrow.insertCell();
				subjtbltd.appendChild(scrollDiv);

			} else // if no data
			{
				var noDataRow = tbl.insertRow();
				var noDataRowCell = noDataRow.insertCell();
				var textBox;

				if (GM_getValue("skipLoad"))
					textBox = CreateNodeWithText(noDataRowCell,
						"Passszív mód bekapcsolva. Kapcsold ki a kérdések betöltéséhez!"
					);
				else
					textBox = CreateNodeWithText(noDataRowCell,
						"A kérdéseket nem lehetett beolvasni. Vagy nem elérhető a szerver, vagy ha offline módot használsz, akkor hibás a fájl elérési útja, vagy a fájl maga. Olvasd el a manualt!"
					);
				textBox.style.margin = fiveMargin; // fancy margin

			}

			// show splash tickbox -----------------------------------------------------------------------------------------------------------------------------
			var splasTickboxRow = tbl.insertRow();
			var splashTickboxCell = splasTickboxRow.insertCell();

			var splashTickBox = document.createElement("input");
			splashTickBox.type = "checkbox";
			splashTickBox.checked = GM_getValue("showSplash") || false;
			splashTickBox.style.position = "";
			//splashTickBox.style.background = "white";
			splashTickBox.style.left = 10 + 'px';
			splashTickBox.style.margin = "5px 5px 5px 5px"; // fancy margin
			splashTickBox.style.top = menuDiv.offsetHeight + 'px';
			splashTickboxCell.appendChild(splashTickBox); // adding to main div

			splashTickBox.addEventListener("click", function() {
				GM_setValue("showSplash", splashTickBox.checked);
			}); // adding clicktextNode

			var splashTickBoxTextSpan = CreateNodeWithText(splashTickboxCell,
				"Üdvözlő üzenet mutatása minden oldalon", "span");

			// show questons tickbox -----------------------------------------------------------------------------------------------------------------------------
			var questionTickboxRow = tbl.insertRow();
			var questionTickboxCell = questionTickboxRow.insertCell();

			var questionsTickBox = document.createElement("input");
			questionsTickBox.type = "checkbox";
			questionsTickBox.checked = GM_getValue("showQuestions");
			questionsTickBox.style.position = "";
			//questionsTickBox.style.background = "white";
			questionsTickBox.style.left = 10 + 'px';
			questionsTickBox.style.margin = "5px 5px 5px 5px"; // fancy margin
			questionsTickBox.style.top = menuDiv.offsetHeight + 'px';
			questionTickboxCell.appendChild(questionsTickBox); // adding to main div

			questionsTickBox.addEventListener("click", function() {
				GM_setValue("showQuestions", questionsTickBox.checked);
				if (!questionsTickBox.checked) {
					ShowMessage({
						m: "Szinte mindég jó az talált válasz a kérdésre, de attól még könnyen előfordulhat, hogy rosz kérdésre írja ki a választ! Ez a opció nélkül ezt az ellenőrzési lehetőséget nem tudod kihasználni",
						isSimple: true
					}, 7);
				}
			}); // adding clicktextNode
			var questionsTickBoxTextSpan = CreateNodeWithText(questionTickboxCell,
				"Kérdések mutatása válaszhoz", "span");

			// database mode listbox -----------------------------------------------------------------------------------------------------------------------------
			var databasemodeListboxRow = tbl.insertRow();
			var databasemodeListboxCell = databasemodeListboxRow.insertCell();

			var databasemodeListbox = document.createElement("select");
			databasemodeListbox.type = "checkbox";
			//databasemodeListbox.checked = GM_getValue("showSplash") || false;
			databasemodeListbox.style.position = "";
			//databasemodeListbox.style.background = "white";
			databasemodeListbox.style.left = 10 + 'px';
			databasemodeListbox.style.margin = "5px 5px 5px 5px"; // fancy margin
			databasemodeListbox.style.top = menuDiv.offsetHeight + 'px';

			var databasemodeListboxText = CreateNodeWithText(questionTickboxCell,
				"Kérdések beszerzése:", "span");
			databasemodeListboxCell.appendChild(databasemodeListboxText);

			databasemodeListboxCell.appendChild(databasemodeListbox); // adding to main div

			databasemodeListbox.addEventListener("change", function(e) {
				// sorry for using selectedindex :c
				GM_setValue("useNetDB", databasemodeListbox.selectedIndex);
			});

			var uselocal = document.createElement('option');
			uselocal.text = "Helyi fájlból (old school)";
			uselocal.value = 2;
			databasemodeListbox.add(uselocal, 0);

			var usenetsafe = document.createElement('option');
			usenetsafe.text = "Netről";
			usenetsafe.value = 0;
			databasemodeListbox.add(usenetsafe, 1);

			var selected = GM_getValue("useNetDB");
			if (selected != undefined)
				databasemodeListbox.selectedIndex = selected;

			var databasemodeListboxElement = document.createElement("span"); // new paragraph
			databasemodeListboxCell.appendChild(databasemodeListboxElement);

			// setting up buttons
			var buttonRow = tbl.insertRow();
			var buttonCell = buttonRow.insertCell();
			buttonCell.style.textAlign = 'center';
			// x button ------------------------------------------------------------------------------------------------------------------------------
			var xButton = CreateNodeWithText(buttonCell, "Bezárás", "button");

			xButton.style.position = "";
			xButton.style.left = 10 + 'px';
			xButton.style.margin = "5px 5px 5px 5px"; // fancy margin
			xButton.style.top = menuDiv.offsetHeight + 'px';

			xButton.addEventListener("click", function() {
				CloseMenu();
			}); // adding clicktextNode
			// help button ----------------------------------------------------------------------------------------------------------------
			var helpButton = CreateNodeWithText(buttonCell, "Help", "button");

			helpButton.style.position = "";
			helpButton.style.left = 10 + 'px';
			helpButton.style.margin = "5px 5px 5px 5px"; // fancy margin
			helpButton.style.top = menuDiv.offsetHeight + 'px';

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


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

			var siteLink = CreateNodeWithText(buttonCell, "Help", "button");
			siteLink.innerText = "Weboldal";

			siteLink.addEventListener("click", function() {
				location.href = serverAdress + "menuClick";
			});

			//addEventListener(window, 'scroll', function () {
			//	menuDiv.style.top = (pageYOffset + window.innerHeight / 3) + 'px';
			//})
			addEventListener(window, 'resize', function() {
				menuDiv.style.left = window.innerWidth / 2 + 'px';
			});

			menuDiv.appendChild(tbl);
			appedtTo.appendChild(menuDiv);
		} catch (e) {
			Exception(e, "script error at showing menu list:");
		}

		document.addEventListener("keydown", EscClose);
	}

	function EscClose(e) {
		if (e.keyCode == 27)
			CloseMenu();
	}

	function CloseMenu() {
		document.getElementById("HelperMenu").parentNode.removeChild(document.getElementById(
			"HelperMenu"));

		document.removeEventListener("keydown", EscClose);
	}

	//: }}}

	//: Generic utils {{{

	function RemoveMultipleItems(array) {
		var newArray = [];
		for (var i = 0; i < array.length; i++) {
			var j = 0;
			while (j < newArray.length && newArray[j] !== array[i]) {
				j++;
			}
			if (j >= newArray.length) {
				newArray.push(array[i]);
			}
		}
		return newArray;
	}

	// removes some crap from "q"
	function SimplifyQuery(q) {
		var result = q.replace(/\n/g, "").replace(/\s/g, ' '); // WHY TF ARE THERE TWO KINDA SPACES??? (charcode 160 n 32)
		return RemoveUnnecesarySpaces(result);
	}

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

	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 ReplaceCharsWithSpace(val, char) {
		toremove = NormalizeSpaces(val);

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

		return RemoveUnnecesarySpaces(toremove);
	}

	// removes whitespace from begining and and, and replaces multiple spaces with one space
	function RemoveUnnecesarySpaces(toremove) {
		toremove = NormalizeSpaces(toremove);
		while (toremove.includes("  ")) // while the text includes double spaces replaces all of them with a single one
		{
			toremove = toremove.replace(/  /g, " ");
		}
		return toremove.trim();
	}

	// simplifies a string for easier comparison
	function SimplifyStringForComparison(value) {
		value = RemoveUnnecesarySpaces(value).toLowerCase();
		var removableChars = [",", ".", ":", "!"];
		for (var i = 0; i < removableChars.length; i++) {
			var regex = new RegExp(removableChars[i], "g");
			value.replace(regex, "");
		}
		return value;
	}

	// if the value is empty, or whitespace
	function 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
	function NormalizeSpaces(input) {
		return input.replace(/\s/g, ' ');
	}

	function SendXHRMessage(message) {
		var url = serverAdress + "isAdding";
		GM_xmlhttpRequest({
			method: "POST",
			url: url,
			data: message,
			headers: {
				"Content-Type": "application/x-www-form-urlencoded"
			},
			onerror: function(response) {
				Log("XMLHTTP request POST error");
			}
		});
	}

	//: }}}

	//: Help {{{

	// shows some neat help
	function ShowHelp() {
		GM_openInTab(serverAdress + 'manual', {
			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.


})();