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

 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      1.6.3.3
// @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/*
// @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.frylabs.net
// @contributionURL qmining.frylabs.net
// @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() {
  // GM functions, only to disable ESLINT errors
  /* eslint-disable  */
  const a = Main
  function getVal (name) { return GM_getValue(name) }
  function setVal (name, val) { return GM_setValue(name, val) }
  function openInTab (address, options) { GM_openInTab(address, options) }
  function getResourceText (name) { return GM_getResourceText(name) }
  function xmlhttpRequest (opts) { GM_xmlhttpRequest(opts) }
  function info () { return GM_info }
  /* eslint-enable */

  var data // all data, which is in the resource txt
  var addEventListener // add event listener function
  const lastChangeLog = 'Félév szerinti csoportosítás menüben'
  const serverAdress = 'https://qmining.frylabs.net/'

  // 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 */
  var motd = ''
  var lastestVersion = ''

  const minMatchAmmount = 60 /* Minimum ammount to consider that two questions match during answering */
  const minResultMatchPercent = 99 /* Minimum ammount to consider that two questions match during saving */
  const lengthDiffMultiplier = 10 /* Percent minus for length difference */

  // : Class descriptions {{{

  class StringUtils {
    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.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()
    }

    // simplifies a string for easier comparison
    SimplifyStringForComparison (value) {
      assert(value)

      value = this.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
    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, ' ')
    }

    CompareString (s1, s2) {
      assert(s1)
      assert(s2)

      s1 = this.SimplifyStringForComparison(s1).split(' ')
      s2 = this.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 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) {
      assert(q2)

      if (typeof q2 === 'string') {
        var qmatchpercent = SUtils.CompareString(this.Q, q2)

        if (i === undefined || i.length === 0) { return qmatchpercent } else {
          if (this.HasImage()) {
            const imatchpercent = this.HasImage() ? SUtils.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 = SUtils.CompareString(this.Q, q2.Q)
        const amatchpercent = SUtils.CompareString(this.A, q2.A)
        if (this.I !== undefined) {
          const imatchpercent = this.I === undefined ? SUtils.CompareString(this.I.join(' '), q2.I.join(
            ' ')) : 0
          return (qmatchpercent + amatchpercent + imatchpercent) / 3
        } else {
          return (qmatchpercent + amatchpercent) / 2
        }
      }
    }
  }

  class Subject {
    constructor (n) {
      assert(n)

      this.Name = n
      this.Questions = []
      this.active = false
    }

    setIndex (i) {
      this.index = i
    }

    getIndex () {
      return this.index || -1
    }

    get length () {
      return this.Questions.length
    }

    markActive () {
      this.active = true
    }

    getIfActive () {
      return this.active
    }

    AddQuestion (q) {
      assert(q)

      this.Questions.push(q)
    }

    getSubjNameWithoutYear () {
      let t = this.Name.split(' - ')
      if (t[0].match(/^[0-9]{4}\/[0-9]{2}\/[0-9]{1}$/i)) {
        return t[1] || ''
      } else {
        return ''
      }
    }

    getYear () {
      let t = this.Name.split(' - ')[0]
      if (t.match(/^[0-9]{4}\/[0-9]{2}\/[0-9]{1}$/i)) {
        return t
      } else {
        return ''
      }
    }

    Search (q, img) {
      assert(q)

      var r = []
      for (let 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 (let 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 (getVal('Is' + i + 'Active')) {
          r.push(i)
        }
      }
      return r
    }

    GetIfActive (ind) {
      return getVal('Is' + ind + 'Active')
    }

    ChangeActive (i, value) {
      setVal('Is' + i + 'Active', !!value)
    }

    AddQuestion (subj, q) {
      assert(subj)

      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) {
      assert(q)

      var r = []
      for (let i = 0; i < this.length; i++) {
        if (this.GetIfActive(i)) { r = r.concat(this.Subjects[i].Search(q, img)) }
      }

      for (let 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) {
      assert(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')
    }
  }

  var SUtils = new StringUtils()

  // : }}}

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

  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++) {
              // TODO: test dis
              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, ind) => {
        return SUtils.ReplaceCharsWithSpace(item, '\n')
      })

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

  class ResultsPageModell {
    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 () {
      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
      }
    }

    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
    }

    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')
    }

    GetOnlyImageQuestionResult (i) {
      console.log('############################################################')
      console.log(i)
      const results = this.GetFormResult() // getting results element
      const n = results[i]
      console.log(n)
      console.log('############################################################')
    }

    // 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 = []

      // the basic type of getting answers
      fun.push(function TryGet0 (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(function TryGet1 (i) {
        if (RPM.GetDropboxes(i).length > 0) { return RPM.GetCurrentAnswer(i) }
      })

      // image and text only question
      fun.push(function TryGet5 (i) {
        return RPM.GetOnlyImageQuestionResult(i)
      })

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

      // if there is dropbox in the question
      fun.push(function TryGet3 (i) {
        return RPM.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 = RPM.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 && SUtils.EmptyOrWhiteSpace(currAnswer)) {
        try {
          currAnswer = fun[j](i)
        } catch (e) {
        }
        j++
      }

      return currAnswer
    }

    // 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].innerText }
        }
        if (items.length === 2) {
          for (let j = 0; j < items.length; j++) {
            let cn = items[j].className
            if (!cn.includes('correct')) { return items[j].innerText }
          }
        }
      } 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 {{{
  Main()
  function Main () {
    'use strict'
    console.time('main')

    Init(function (count, subjCount) {
      var url = location.href // eslint-disable-line

      let skipLoad = getVal('skipLoad')
      if (count === -2 && subjCount === -2 && skipLoad) {
        if (url.includes('/quiz/') && url.includes('attempt.php')) {
          ShowMessage({
            m: 'Passzív mód bekapcsolva, válaszok megjelenítéséhez menü gomb alatt kapcsold ki, és frissíts!',
            isSimple: true
          })
        }
      } else {
        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 () {
            openInTab(serverAdress + 'lred', {
              active: true
            })
          })
          Exception(e, 'script error at main:')
        }
        if (url.includes('eduplayer')) { 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.'
        )
      }
    })

    console.log('Moodle Test Script run time:')
    console.timeEnd('main')

    if (forceTestPage || forceResultPage || forceDefaultPage) {
      if (document.getElementById('scriptMessage')) { document.getElementById('scriptMessage').style.background = 'green' }
    }
  }
  // : }}}

  // : Main logic stuff {{{

  // : Loading {{{

  function Init (cwith) {
    if (false) { // eslint-disable-line
      setVal('version16', undefined)
      setVal('version15', undefined)
      setVal('firstRun', undefined)
      setVal('showQuestions', undefined)
      setVal('showSplash', undefined)
    }
    var url = location.href // eslint-disable-line
    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
    // setVal("version15", true);
    // setVal("firstRun", true);
    // setVal("version16", true);
    // throw "asd";

    FreshStart()

    Version15()
    Version16()
  }

  // : 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
      return true
    }
  }

  function Version15 () {
    var version15 = getVal('version15') // if the current run is the frst
    if (version15 === undefined || version15 === true) {
      setVal('useNetDB', '1')
      setVal('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' // eslint-disable-line
    }
  }

  function Version16 () {
    var version16 = getVal('version16') // if the current run is the frst
    if (version16 === undefined || version16 === true) {
      var i = 0
      while (getVal('Is' + i + 'Active') !== undefined) {
        setVal('Is' + i + 'Active', false)
        i++
      }
      setVal('version16', false)
    }
  }

  // : }}}

  function GetFileData () {
    return getResourceText('data')
  }

  function ReadFile (cwith) {
    var resource = ''
    try {
      resource = GetFileData() // 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 (SUtils.EmptyOrWhiteSpace(resource)) {
        throw 'data file empty'  // eslint-disable-line
      }
    } catch (e) {
      Exception(e, 'script error at reading file:')
    }
    NLoad(resource, cwith)
  }

  function ReadNetDB (cwith, useNetDB) {
    function NewXMLHttpRequest () {
      const url = serverAdress + 'data.json'
      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)' // eslint-disable-line
        }
        // 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)' // eslint-disable-line
        }
        if (!currQuestion.HasAnswer()) {
          throw 'No asnwer! (I)' // eslint-disable-line
        }
        // dont allow overwriting
        // ?!>>>
        if (!currQuestion.HasImage()) {
          try {
            currQuestion.I = JSON.parse(currData)
          } catch (e) {
            currQuestion.I = currData.split(',')
          }
        }
        ExpectedIdentifier = ['?', '+']
        continue
      }
    }

    return {
      result: r,
      logs: logs
    }
  }

  function Load (cwith) {
    var useNetDB = getVal('useNetDB')
    let skipLoad = getVal('skipLoad')

    if (skipLoad) {
      cwith(-2, -2)
      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) {
    assert(resource)

    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 (let i = 0; i < d.Subjects.length; i++) {
        let s = new Subject(d.Subjects[i].Name)
        s.setIndex(i)
        if (getVal('Is' + i + 'Active')) {
          s.markActive()
          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

      let i = 0
      while (i < data.length && !getVal('Is' + i + 'Active')) {
        i++
      }
    } catch (e) {
      Exception(e, 'script error at loading:')
      count = -1 // returns -1 if error
    }
    cwith(count, subjCount)
  }

  function AlertOnNoQuestion () {
    try {
      document.getElementById('HelperMenuButton').style.background = 'yellow'
    } catch (e) {
      Log('Unable to get helper menu button')
    }
  }

  // : }}}

  // : 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 {
      newVersion = info().script.version !== getVal('lastVerson')
    } catch (e) {
      Log('Some weird error trying to set new verison')
    }
    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 nincs tárgy kiválasztva) 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 = (getVal('showSplash') === undefined) || getVal('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. ' + info().script.version + '. '

      if (lastestVersion !== undefined && info().script.version !== lastestVersion) {
        greetMsg += 'Új verzió elérhető: ' + lastestVersion + '\n'
        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 {
          AlertOnNoQuestion()
          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. ' + 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. ' + info().script.version + '. ' + count + ' kérdés és ' + subjCount + ' tárgy betöltve. Verzió frissítve ' + info().script.version + '-re. Changelog:\n' + lastChangeLog
      setVal('lastVerson', info().script.version) // setting lastVersion
    }
    if (!SUtils.EmptyOrWhiteSpace(motd)) {
      var prevmotd = getVal('motd')
      if (prevmotd !== motd) {
        greetMsg += '\nMOTD:\n' + motd
        timeout = null
        setVal('motdcount', motdShowCount)
        setVal('motd', motd)
      } else {
        var motdcount = getVal('motdcount')
        if (motdcount === undefined) {
          setVal('motdcount', motdShowCount)
          motdcount = motdShowCount
        }

        motdcount--
        if (motdcount > 0) {
          greetMsg += '\nMOTD:\n' + motd
          timeout = null
          setVal('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 = QPM.GetQuestionFromTest()
    var questions = q.q
    var imgNodes = q.imgnodes
    // ------------------------------------------------------------------------------------------------------
    var answers = []
    for (var j = 0; j < questions.length; j++) {
      var question = SUtils.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) {
    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.HasImage()) {
          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) {
    assert(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 () {
        openInTab(serverAdress + 'lred', {
          active: true
        })
      })
    }
  }

  // : }}}

  // : Quiz saving {{{

  function HandleResults (url) {
    var d = SaveQuiz(GetQuiz(), data) // saves the quiz questions and answers

    if (d) { ShowSaveQuizDialog(d.addedQ, d.allQ, d.allOutput, d.output, d.sendSuccess, d.sentData) }
  }

  function ShowSaveQuizDialog (addedQ, allQ, allOutput, output, sendSuccess, sentData) {
    var msg = ''
    if (addedQ > 0) {
      msg = 'Klikk ide a nyers adatokhoz. ' + addedQ + ' új kérdés!'

      var useNetDB = getVal('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 {
      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>' + sentData.subj + '<br>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 = getVal('useNetDB')
      if (useNetDB !== undefined && useNetDB === 1) {
        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()
    })
  }

  function SearchSameQuestion (questionData, quiz, i) {
    var r = questionData.Search(quiz[i])

    let count = 0
    r.forEach((item) => {
      if (item.match > minResultMatchPercent) { count++ }
    })

    return count === 0 ? -1 : count
  }

  // 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 = 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) {
        temp = JSON.stringify(imgURL)
        return temp
      }
    } catch (e) {
      Log("Couldn't get images from result")
    }
  }

  // saves the current quiz. questionData contains the active subjects questions
  function SaveQuiz (quiz, questionData) {
    try {
      if (quiz.length === 0) {
        throw { // eslint-disable-line
          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++) {
        // searching for same questions in questionData
        var toAdd = '' // this will be added to some variable depending on if its already in the database
        toAdd += '?' + SUtils.RemoveUnnecesarySpaces(quiz[i].Q) + '\n' // adding quiz question
        toAdd += '!' + SUtils.RemoveUnnecesarySpaces(quiz[i].A) + '\n' // adding quiz answer
        if (quiz[i].HasImage()) {
          toAdd += '>' + SUtils.RemoveUnnecesarySpaces(quiz[i].I) + '\n' // adding quiz image if there is any
        }
        if (SearchSameQuestion(questionData, quiz, i) === -1) {
          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 = MPM.GetCurrentSubjectName()
        } catch (e) {
          sentData.subj = 'NOSUBJ'
          Log('unable to get subject name :c')
        }
        var useNetDB = getVal('useNetDB')
        if (useNetDB !== undefined && useNetDB === 1) {
          sentData.allData = quiz
          sentData.data = newQuestions
          sentData.version = info().script.version
          SendXHRMessage('datatoadd=' + JSON.stringify(sentData))
          sendSuccess = true
        }
      } catch (e) {
        Exception(e, 'error at sending data to server.')
      }
      return {
        addedQ: addedQ,
        allQ: allQ,
        allOutput: allOutput,
        output: output,
        sendSuccess: sendSuccess,
        sentData: 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 = RPM.GetFormResult() // getting results element
      for (var i = 0; i < results.length - 2; i++) {
        var question = {} // the current question
        // QUESTION --------------------------------------------------------------------------------------------------------------------
        var q = RPM.GetQuestionFromResult(i)
        if (q !== undefined) { question.q = SUtils.SimplifyQuery(q) }

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

        if (q !== undefined) { q = SUtils.ReplaceCharsWithSpace(q, '\n') }
        if (a !== undefined) { a = SUtils.ReplaceCharsWithSpace(a, '\n') }

        if (question.a !== undefined) {
          quiz.push(new Question(question.q, question.a, question.i)) // 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 SimplifyImages (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
      }
    }
    return questionImages
  }

  // 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,
      'Miután elindítottad: Play/pause: space. Seek: Bal/jobb nyíl.')
    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) { // TODO why not radio
            for (let 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 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 = 'none'
      mainDiv.style.top = (startFromTop) + 'px'
      mainDiv.style.left = (window.innerWidth - width) / 2 + 'px'
      mainDiv.style.opacity = '0.9' // setting starting opacity
      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 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

        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 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.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.setAttribute('id', 'HelperMenuButton')

      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 = getVal('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 () {
        setVal('skipLoad', questionsTickBox.checked)
        var msg = ''
        if (getVal('skipLoad')) { msg = 'Passzív mód bekapcsolva, mostantól kérdések nem lesznek betöltve/lekérve.' } else { msg = 'Passzív mód kikapcsolva, frissíts az érvénybe lépéshez!' }

        ShowMessage({
          m: msg,
          isSimple: true
        }, 6)
      })
      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 = 'none'
      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('div')
      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 headerSubjInfoParagraph2 = CreateNodeWithText(header2, 'Aktív')
      // headerSubjInfoParagraph2.style.margin = fiveMargin // fancy margin

      if (data && data.length > 0) {
        let grouped = data.Subjects.reduce((res, s) => {
          let sName = s.getSubjNameWithoutYear()
          if (sName) {
            if (!res[sName]) {
              res[sName] = []
            }
            res[sName].push(s)
          } else {
            res.others.push(s)
          }
          return res
        }, {
          others: []
        })

        let collapsibles = []

        Object.entries(grouped).forEach(([subjName, subjGroup]) => {
          let b = CreateNodeWithText(subjTable, subjName, 'button')
          b.style.backgroundColor = '#222d32'
          b.style.color = '#ffffff'
          b.style.cursor = 'pointer'
          b.style.padding = '5px'
          b.style.width = '100%'
          b.style.border = 'none'
          b.style.textAlign = 'left'
          b.style.outline = 'none'
          collapsibles.push(b)

          let content = document.createElement('div')
          content.style.padding = '0 18px'
          content.style.overflow = 'hidden'
          content.style.backgroundColor = '#222d32'
          content.style.borderColor = '#212127'
          content.style.borderStyle = 'solid'
          content.style.borderWidth = '5px'
          let ifGroupActive = subjGroup.some((x) => {
            return x.getIfActive()
          })
          content.style.display = ifGroupActive ? 'block' : 'none'

          subjTable.appendChild(content)

          subjGroup.forEach((subj) => {
            let tbl = document.createElement('table')
            content.appendChild(tbl)

            var row = tbl.insertRow()
            let td = row.insertCell()
            let text = subj.getYear() || subj.Name
            if (subj.length !== 0) { text += ' [ ' + subj.length + 'db ]' }
            CreateNodeWithText(td, text)

            td = row.insertCell()
            let 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

            checkbox.checked = subj.active
            let i = subj.getIndex()
            checkbox.setAttribute('id', 'HelperTextNode' + i)
            checkbox.addEventListener('click', function () {
              var checked = document.getElementById('HelperTextNode' + i).checked
              data.ChangeActive(i, checked)
            }) // adding click
          })
        })

        collapsibles.forEach((x) => {
          x.addEventListener('click', function () {
            this.classList.toggle('active')
            var content = this.nextElementSibling
            if (content.style.display === 'block') {
              content.style.display = 'none'
            } else {
              content.style.display = 'block'
            }
          })
        })

        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()
        let textBox

        if (getVal('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 = getVal('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 () {
        setVal('showSplash', splashTickBox.checked)
      }) // adding clicktextNode

      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 = getVal('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 () {
        setVal('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

      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 = getVal("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
        setVal('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 = getVal('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' // eslint-disable-line
      })

      // 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 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 SendXHRMessage (message) {
    var url = serverAdress + 'isAdding'
    xmlhttpRequest({
      method: 'POST',
      url: url,
      data: message,
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
      },
      onerror: function (response) {
        Log('XMLHTTP request POST error')
      }
    })
  }

  var assert = (val) => {
    if (!val) { throw new Error('Assertion failed') }
  }

  // : }}}

  // : Help {{{

  // shows some neat help
  function ShowHelp () {
    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.
})();