// vim:foldmethod=marker
/* ----------------------------------------------------------------------------

 Online 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/>.

 ------------------------------------------------------------------------- */
// : Install info {{{

// ===============================================================================
// ===============================================================================
//
//          HA EZT LÁTOD, ÉS TELEPÍTENI AKARTAD A SCRIPTET, AKKOR
//                 NINCS USERSCRIPT KEZELŐ BŐVÍTMÉNYED.
//
//        Telepíts egy userscript kezelőt, például a Tampermonkey-t:
//                      https://www.tampermonkey.net/
//
// ===============================================================================
//
//             IF YOU ARE SEEING THIS MESSAGE, AND YOU WANTED TO
//          INSTALL THIS SCRIPT, THEN YOU DON'T HAVE ANY USERSCRIPT
//                          MANAGER INSTALLED.
//
//           Install a userscript manager, for example Tampermonkey:
//                      https://www.tampermonkey.net/
//
// ===============================================================================
// ===============================================================================

// : }}}

// : Script header {{{
// ==UserScript==
// @name         Moodle/Elearning/KMOOC test help
// @version      2.1.3.10
// @description  Online Moodle/Elearning/KMOOC test help
// @author       MrFry
// @match        https://elearning.uni-obuda.hu/*
// @match        https://exam.elearning.uni-obuda.hu/*
// @match        https://mooc.unideb.hu/*
// @match        https://elearning.unideb.hu/*
// @match        https://itc.semmelweis.hu/moodle/*
// @match        https://moodle.gtk.uni-pannon.hu/*
// @match        https://oktatas.mai.kvk.uni-obuda.hu/*
// @match        https://moodle.pte.hu/*
// @match        https://szelearning.sze.hu/*
// @match        https://moodle.kre.hu/*
// @match        https://moodle.pte.hu/*
// @match        https://portal.kgk.uni-obuda.hu/*
// @match        https://elearning.uni-miskolc.hu/*
// @match        https://elearning.uni-mate.hu/*
// @match        https://edu.gpk.bme.hu/*
// @match        https://edu.gtk.bme.hu/*
// @match        https://iktblog.hu/*
// @match        https://moodle.ms.sapientia.ro/*
// @match        https://moodle.uni-corvinus.hu/*
// @match        https://v39.moodle.uniduna.hu/*
// @noframes
// @match        https://qmining.frylabs.net/*
// @run-at       document-start
// @grant        GM_getResourceText
// @grant        GM_info
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_xmlhttpRequest
// @grant        GM_openInTab
// @grant        unsafeWindow
// @license      GNU General Public License v3.0 or later
// @supportURL   qmining.frylabs.net
// @contributionURL qmining.frylabs.net
// @namespace    https://qmining.frylabs.net
// @updateURL    https://qmining.frylabs.net/moodle-test-userscript/stable.user.js?up
// ==/UserScript==
// : }}}

;(function () {
  // : ESLINT bs {{{

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

  // : }}}

  // : Constants and global variables {{{

  // ------------------------------------------------------------------------------
  // Devel vars
  // ------------------------------------------------------------------------------
  // forcing pages for testing. unless you test, do not set these to true!
  const isDevel = false
  const forcedMatchString = isDevel ? 'default' : null
  // only one of these should be true for testing
  const forceTestPage = isDevel && false
  const forceResultPage = isDevel && false
  const forceDefaultPage = isDevel && false
  // ------------------------------------------------------------------------------

  const logElementGetting = false
  const logEnabled = true
  const showErrors = true

  var addEventListener // add event listener function
  let serverAdress = 'https://qmining.frylabs.net/'
  let apiAdress = 'https://api.frylabs.net/'

  const motdShowCount = 5 /* Ammount of times to show motd */
  const messageOpacityDelta = 0.1
  const minMessageOpacity = 0.2
  let infoExpireTime = 60 * 5 // Every n seconds basic info should be loaded from server
  var motd = ''
  var lastestVersion = ''
  var subjInfo

  // array, where elems are added to shadow-root, but its position should be at target.
  var updatableElements = [] // { elem: ..., target: ... }
  var elementUpdaterInterval = -1
  const overlayElemUpdateInterval = 2 // seconds

  if (isDevel) {
    warn('Moodle script running in developement mode!')
    infoExpireTime = 1
    serverAdress = 'http://localhost:8080/'
    apiAdress = 'http://localhost:8080/'
  }

  const currUrl = location.href.includes('file:///')
    ? 'https://elearning.uni-obuda.hu/'
    : location.href

  // : Localisation {{{

  const huTexts = {
    fatalError:
      'Fatál error. Check console (f12). Kattints az üzenetre az összes kérdés/válaszért manuális kereséshez! (új böngésző tab-ban)',
    consoleErrorInfo:
      'Itteni hibák 100% a moodle hiba. Kivéve, ha oda van írva hogy script error ;) Ha ilyesmi szerepel itt, akkor olvasd el a segítség szekciót! https://qmining.frylabs.net/manual?scriptcmd',
    freshStartWarning:
      '<h1>Moodle teszt userscript:<h1><h3>1.5.0 verzió: a script mostantól XMLHTTP kéréseket küld szerver fele! Erre a userscript futtató kiegészítőd is figyelmeztetni fog! Ha ez történik, a script rendes működése érdekében engedélyezd (Always allow domain)! Ha nem akarod, hogy ez történjen, akkor ne engedélyezd, vagy a menüben válaszd ki a "helyi fájl használata" opciót!</h3> <h3>Elküldött adatok: minden teszt után a kérdés-válasz páros. Fogadott adatok: Az összes eddig ismert kérdés. Érdemes help-et elolvasni!!!</h3><h5>Ez az ablak frissités után eltűnik. Ha nem, akkor a visza gombbal próbálkozz.</h5>',
    noResult:
      'Nincs találat :( Kattints az üzenetre az összes kérdés/válaszért manuális kereséshez! (új böngésző tab-ban)',
    videoHelp: 'Miután elindítottad: Play/pause: space. Seek: Bal/jobb nyíl.',
    menuButtonText: 'Kérdések Menu',
    couldntLoadDataPopupMenuText:
      'A kérdéseket nem lehetett beolvasni, ellenőrizd hogy elérhető-e a szerver',
    showGreetingOnEveryPage: 'Üdvözlő üzenet mutatása minden oldalon',
    close: 'Bezárás',
    help: 'Help',
    websiteBugreport: 'Weboldal',
    contribute: 'Bug report / Szavazás következő feature-re',
    ranklist: 'Ranklista',
    donate: 'Donate',
    retry: 'Újrapróbálás',
    dataEditor: 'Data editor',
    dataEditorTitle: 'Adatbázisban lévő kérdések szerkesztése',
    invalidPW: 'Hibás jelszó: ',
    search: 'Keresés ...',
    loading: 'Betöltés ...',
    login: 'Belépés',
    newPWTitle: 'Új jelszó új felhasználónak',
    pwRequest: 'Jelszó új felhasználónak',
    noServer: 'Nem elérhető a szerver!',
    pwHere: 'Jelszó ...',
    noServerConsoleMessage: `Nem elérhető a szerver, vagy kis eséllyel kezeletlen hiba történt! Ha elérhető a weboldal, akkor ott meg bírod nézni a kérdéseket itt: ${serverAdress}legacy`,
    noParseableQuestionResult:
      'A tesztben nem találhatók kérdések, amit fel lehet dolgozni, vagy hiba történt feldolgozásuk közben',
    unableToParseTestQuestion:
      'Hiba történt a kérdések beolvasása közben :/ Kattints az üzenetre a manuális kereséshez (új böngésző tab-ban)',
    loadingAnswer: 'Válaszok betöltése ...',
    reinstallFromCorrectSource:
      'Scriptet nem a qmining weboldalról raktad fel. Könnyebb kezelhetőség szempontjából kérlek onnan telepítsd. <a href="https://qmining.frylabs.net/manual.html#scriptreinstall">Részletes leírás</a>',
    versionUpdated: 'Verzió frissítve ',
    newVersionAvaible: 'Új verzió elérhető: ',
    userSpecifitMotdAvailable:
      'Új üzeneted van, kattints 📬-ra bal alul megtekintéséhez!',
    scriptName: 'Moodle/Elearning/KMOOC segéd ',
    userMOTD: 'Felhasználó MOTD (ezt csak te látod):\n',
    motd: 'MOTD:\n',
  }

  var texts = huTexts

  // : }}}

  // : }}}

  // : HTML parsers {{{

  // : Moodle {{{

  // : Basic processing helpers {{{

  function getTextPromisesFromNode(inputNode) {
    const nodes = Array.from(inputNode.childNodes)
      .map((x) => flattenNode(x))
      .flat()

    return nodes.reduce((promises, elem) => {
      let img = elem
      if (elem.tagName !== 'IMG') {
        const t = elem.tagName ? elem.getElementsByTagName('img') : []
        if (t.length > 0) {
          img = t[0]
        }
      }

      const select = elem.tagName ? elem.getElementsByTagName('select') : []
      if (select.length > 0) {
        // test: 2c1d92a7-0ea2-4990-9451-7f19299bbbe4
        const question = []
        Array.from(elem.childNodes).forEach((cn) => {
          if (cn.nodeValue) {
            question.push(cn.nodeValue)
          }
        })
        promises.push({
          type: 'txt',
          val: question.join('...'),
          node: select[0],
        })
        return promises
      }

      if (img.tagName === 'IMG') {
        if (img.title) {
          promises.push({ type: 'txt', val: img.title, node: elem })
        } else {
          promises.push(
            new Promise((resolve) => {
              digestMessage(getBase64Image(img)).then((res) => {
                resolve({
                  type: 'img',
                  val: res,
                  node: img,
                })
              })
            })
          )
        }
      } else if (elem.tagName === undefined) {
        promises.push({ type: 'txt', val: elem.nodeValue, node: elem })
      } else {
        promises.push({ type: 'txt', val: elem.innerText, node: elem })
      }

      return promises
    }, [])
  }

  function flattenNode(node) {
    if (node.childNodes && node.childNodes.length > 0) {
      return Array.from(node.childNodes)
        .map((x) => flattenNode(x))
        .flat()
    } else {
      return node
    }
  }

  function makeTextFromElements(acc, item) {
    if (emptyOrWhiteSpace(item.val)) {
      return acc
    }

    if (item.type === 'img') {
      acc.push('[' + item.val + ']')
    } else {
      acc.push(item.val)
    }
    return acc
  }

  function getImagesFromElements(elements) {
    return elements.reduce((acc, element) => {
      if (element.type === 'img') {
        // FIXME: include check needed?
        if (!acc.includes(element.val)) {
          acc.push({ val: element.val, node: element.node })
        }
      }
      return acc
    }, [])
  }

  function getLegacyImageID(imgArray) {
    try {
      return imgArray.map((img) => {
        if (!img.src.includes('brokenfile')) {
          let filePart = img.src.split('/')
          filePart = filePart[filePart.length - 1]

          // shorten string
          let result = ''
          let i = 0
          while (i < filePart.length && i < 30) {
            result += filePart[i]
            i++
          }

          return decodeURI(result)
        }
      })
    } catch (e) {
      warn(e)
      warn("Couldn't get images from result (old)")
    }
  }

  function getCurrentSubjectName() {
    if (logElementGetting) {
      debugLog('getting current subjects name')
    }
    return document.getElementById('page-header').innerText.split('\n')[0] || ''
  }

  // : }}}

  // : Test page processing functions {{{

  function handleMoodleQuiz() {
    const { removeMessage: removeLoadingMessage } = ShowMessage(
      texts.loadingAnswer
    )

    getQuizData()
      .then((readQuestions) => {
        if (readQuestions.length === 0) {
          warn('readQuestions length is zero, no questions found on page!')
          ShowMessage(
            texts.unableToParseTestQuestion,

            undefined,
            () => {
              OpenErrorPage({
                message: 'No result found',
              })
            }
          )
          return
        }

        const questions = readQuestions.map((question) => {
          return {
            Q: question.question,
            data: Object.assign(
              {},
              {
                possibleAnswers: question.possibleAnswers,
              },
              question.data
            ),
          }
        })

        const sentData = {
          questions: questions,
          subj: getCurrentSubjectName(),
          testUrl: currUrl,
          version: info().script.version,
          cid: getCid(),
          uid: getUid(),
        }

        log('Sent data', sentData)
        post('ask', sentData).then((results) => {
          removeLoadingMessage()
          ShowAnswers(
            results.map((res, i) => {
              return {
                answers: res.answers,
                question: readQuestions[i],
              }
            })
          )
        })
      })
      .catch((err) => {
        warn(err)
        warn('Error in handleMoodleQuiz()')
      })
  }

  const questionNodeVariants = {
    hasInformationText: {
      criteria: () => {
        const firstChild =
          document.getElementsByTagName('form')[1].childNodes[0].childNodes[0]
        if (!firstChild.className.includes('informationitem')) {
          return false
        }
        const questionNodes = Array.from(
          document.getElementsByTagName('form')[1].childNodes[0].childNodes
        )
        return questionNodes.length > 0
      },
      getter: () => {
        return Array.from(
          document.getElementsByTagName('form')[1].childNodes[0].childNodes
        ).filter((node) => {
          return !node.className.includes('informationitem')
        })
      },
    },
    formFirst: {
      criteria: () => {
        const questionNodes = Array.from(
          document.getElementsByTagName('form')[0].childNodes[0].childNodes
        )
        // test: e2c01ff4-d97a-4ab9-8f7f-e28812541097
        const notOnlyTextNodes = questionNodes.every((node) => {
          return node.tagName !== undefined
        })
        return notOnlyTextNodes && questionNodes.length > 0
      },
      getter: () => {
        return Array.from(
          document.getElementsByTagName('form')[0].childNodes[0].childNodes
        )
      },
    },
    formSecond: {
      criteria: () => {
        const questionNodes = Array.from(
          document.getElementsByTagName('form')[1].childNodes[0].childNodes
        )
        return questionNodes.length > 0
      },
      getter: () => {
        return Array.from(
          document.getElementsByTagName('form')[1].childNodes[0].childNodes
        )
      },
    },
  }

  function getQuestionNodes() {
    try {
      let questionNodes
      Object.keys(questionNodeVariants).some((key) => {
        const variant = questionNodeVariants[key]
        let criteriaPassed = false
        try {
          criteriaPassed = variant.criteria()
        } catch (e) {
          debugLog('Criteria check failed with error (question nodes)', e)
        }
        if (criteriaPassed) {
          questionNodes = variant.getter()
          if (questionNodes.length === 0) {
            warn(
              `question nodes ${key} criteria was true, but result is an empty array!`
            )
          } else {
            debugLog(`Using question node getter variant: ${key}`)
            return true
          }
        }
      })
      return questionNodes
    } catch (e) {
      warn('Error in getQuestionNodes')
      warn(e)
    }
  }

  function getQuizData() {
    return new Promise((resolve) => {
      // TODO: dropdown in question
      // TODO: get possible answers too
      const promises = []
      const questionNodes = getQuestionNodes()

      let i = 0
      while (
        i < questionNodes.length &&
        questionNodes[i].tagName === 'DIV' &&
        questionNodes[i].className !== 'submitbtns'
      ) {
        promises.push(getQuestionPromiseForSingleQuestion(questionNodes[i]))
        i++
      }

      Promise.all(promises)
        .then((result) => {
          const errorsRemoved = result.reduce((acc, res) => {
            if (res.success) {
              acc.push(res)
            }
            return acc
          }, [])
          resolve(errorsRemoved)
        })
        .catch((err) => {
          warn('Error in handleMoodleQuiz()')
          warn(err)
        })
    })
  }

  function getPossibleAnswersFromTest(node) {
    try {
      const promises = []
      let answerRoot = node.getElementsByClassName('answer')[0]

      if (!answerRoot) {
        answerRoot = node.getElementsByClassName('subquestion')[0]
        if (answerRoot) {
          // FIXME: is this needed, what is this lol
          const options = Array.from(answerRoot.getElementsByTagName('option'))
          const possibleAnswers = options.reduce((acc, option) => {
            if (!emptyOrWhiteSpace(option.innerText)) {
              acc.push([{ type: 'txt', val: option.innerText }])
            }
            return acc
          }, [])

          return possibleAnswers
        } else {
          const select = node.getElementsByTagName('select')[0]
          if (select) {
            const options = []
            Array.from(select).forEach((opt) => {
              if (!emptyOrWhiteSpace(opt.innerText)) {
                options.push([{ type: 'txt', val: opt.innerText }])
              }
            })
            return options
          }
        }
      } else if (answerRoot.tagName === 'DIV') {
        const answers = Array.from(answerRoot.childNodes)

        answers.forEach((answer) => {
          if (answer.tagName) {
            promises.push(getTextPromisesFromNode(answer))
          }
        })

        return promises
      } else if (answerRoot.tagName === 'TABLE') {
        const answers = Array.from(answerRoot.childNodes[0].childNodes)
        // test: 002203ca-581b-445c-b45d-85374f212e8e NOT WORING

        answers.forEach((answer) => {
          if (answer.tagName) {
            // test: 817434df-a103-4edc-870e-c9ac953404dc
            promises.push(
              getTextPromisesFromNode(answer.getElementsByClassName('text')[0])
            )
            // here elements with classname 'control' could be added too. Those should be a dropdown,
            // containing possible choices
          }
        })

        return promises
      }
    } catch (e) {
      warn('Error in getPossibleAnswersFromTest()!')
      warn(e)
    }
  }

  function getImgNodesFromArray(arr) {
    return arr.reduce((acc, x) => {
      if (Array.isArray(x)) {
        x.forEach((y) => {
          if (y.type === 'img') {
            acc.push(y.node)
          }
        })
      } else {
        if (x.type === 'img') {
          acc.push(x.node)
        }
      }
      return acc
    }, [])
  }

  const rootVariants = {
    qtext: {
      criteria: (node) => {
        return node.getElementsByClassName('qtext').length > 0
      },
      getter: (node) => {
        return node.getElementsByClassName('qtext')[0]
      },
    },
    subquestion: {
      criteria: (node) => {
        return node.getElementsByClassName('subquestion').length > 0
      },
      getter: (node) => {
        return node.getElementsByClassName('subquestion')[0].parentNode
      },
    },
    content: {
      // test: 002203ca-581b-445c-b45d-85374f212e8e
      criteria: (node) => {
        return node.getElementsByClassName('content').length > 0
      },
      getter: (node) => {
        const content = node.getElementsByClassName('content')[0].childNodes[0]
        const pNodes = Array.from(content.childNodes).filter((node) => {
          return node.tagName === 'P'
        })

        const parent = document.createElement('div')
        pNodes.forEach((node) => {
          parent.appendChild(node.cloneNode(true))
        })

        return pNodes[0]
      },
    },
  }

  function getQuestionRootNode(node) {
    try {
      let qtextNode
      Object.keys(rootVariants).some((key) => {
        const variant = rootVariants[key]
        if (variant.criteria(node)) {
          qtextNode = variant.getter(node)
          if (!qtextNode) {
            warn(
              `question root node ${key} criteria was true, but result is null`
            )
          } else {
            return true
          }
        }
      })
      return qtextNode
    } catch (e) {
      warn('Error in getQuestionRootNode')
      warn(e)
    }
  }

  function getQuestionPromiseForSingleQuestion(node) {
    return new Promise((resolve) => {
      try {
        const qtextNode = getQuestionRootNode(node)
        const questionPromises = getTextPromisesFromNode(qtextNode)
        const possibleAnswerPromises = getPossibleAnswersFromTest(node)

        const unflattenedPossibleAnswerPromises = possibleAnswerPromises
          ? possibleAnswerPromises.map((x) => {
              return Promise.all(x)
            })
          : []

        Promise.all([
          Promise.all(questionPromises),
          Promise.all(unflattenedPossibleAnswerPromises),
        ])
          .then(([question, possibleAnswerArray]) => {
            const questionText = removeUnnecesarySpaces(
              question.reduce(makeTextFromElements, []).join(' ')
            )
            const possibleAnswers = possibleAnswerArray.map((x) => {
              return {
                type: 'txt',
                val: removeUnnecesarySpaces(
                  x.reduce(makeTextFromElements, []).join(' ')
                ),
              }
            })
            const images = getImagesFromElements([
              ...question,
              ...possibleAnswerArray.reduce((acc, x) => {
                return [...acc, ...x]
              }, []),
            ])
            const imageNodes = getImgNodesFromArray([
              ...question,
              ...possibleAnswerArray,
            ])
            const data = getDataFromTest(
              node,
              images,
              getLegacyImageID(imageNodes)
            )

            resolve({
              question: questionText,
              possibleAnswers: possibleAnswers,
              images: images,
              data: data,
              success: true,
            })
          })
          .catch((err) => {
            warn('Error in getQuestionPromiseForSingleQuestion()')
            warn(err)
            resolve({ success: false })
          })
      } catch (err) {
        warn('Error in getQuestionPromiseForSingleQuestion()')
        warn(err)
        resolve({ success: false })
      }
    })
  }

  function getDataFromTest(_node, hashedImages, legacyImages) {
    if (hashedImages.length > 0) {
      return {
        type: 'image',
        hashedImages: hashedImages.map((x) => {
          return x.val
        }),
        images: legacyImages,
      }
    } else {
      return {
        type: 'simple',
      }
    }
  }

  // : }}}

  // : Result page processing functions {{{

  const resultNodeVariants = questionNodeVariants

  function getResultNodes() {
    try {
      let resultNodes
      Object.keys(resultNodeVariants).some((key) => {
        const variant = resultNodeVariants[key]
        let criteriaPassed = false
        try {
          criteriaPassed = variant.criteria()
        } catch (e) {
          debugLog('Criteria check failed with error (result nodes)', e)
        }
        if (criteriaPassed) {
          resultNodes = variant.getter()
          if (resultNodes.length === 0) {
            warn(
              `result nodes ${key} criteria was true, but result is an empty array!`
            )
          } else {
            debugLog(`Using question node getter variant: ${key}`)
            return true
          }
        }
      })
      return resultNodes
    } catch (e) {
      warn('Error in getResultNodes')
      warn(e)
    }
  }

  function getQuiz() {
    return new Promise((resolve) => {
      const promises = []
      const questionNodes = getResultNodes()
      // let questionNodes = Array.from(
      //   document.getElementsByTagName('form')[0].childNodes[0].childNodes
      // )
      // if (questionNodes.length === 0) {
      //   questionNodes = Array.from(
      //     document.getElementsByTagName('form')[1].childNodes[0].childNodes
      //   )
      // }

      let i = 0
      while (i < questionNodes.length && questionNodes[i].tagName === 'DIV') {
        promises.push(getQuizFromNode(questionNodes[i]))
        i++
      }

      Promise.all(promises)
        .then((result) => {
          const errorsRemoved = result.reduce((acc, res) => {
            if (res.success) {
              acc.push(res)
            }
            return acc
          }, [])
          resolve(errorsRemoved)
        })
        .catch((err) => {
          warn('Error in getQuiz()')
          warn(err)
        })
    })
  }

  function getPromisesThatMeetsRequirements(getters, node) {
    let res
    Object.keys(getters).some((key) => {
      const getter = getters[key]
      if (getter.requirement(node)) {
        try {
          res = getter.getterFunction(node)
          debugLog(`[Question getter] Using ${key}`)
          return true
        } catch (e) {
          debugLog(`[Question getter] ${key} failed`)
        }
      } else {
        debugLog(`[Question getter] ${key} did not pass`)
      }
    })

    return res
  }

  function getQuizFromNode(node) {
    return new Promise((resolve) => {
      try {
        const questionPromises = getPromisesThatMeetsRequirements(
          questionGetters,
          node
        )
        const answerPromises = getPromisesThatMeetsRequirements(
          answerGetters,
          node
        )
        const possibleAnswers = getPossibleAnswers(node)

        if (!answerPromises || !questionPromises) {
          debugLog('Answer or question array is empty, skipping question')
          resolve({ success: false })
        }

        Promise.all([
          Promise.all(questionPromises),
          Promise.all(answerPromises),
        ])
          .then(([question, answer]) => {
            const questionText = question
              .reduce(makeTextFromElements, [])
              .join(' ')
            const answerText = answer.reduce(makeTextFromElements, []).join(' ')
            const images = getImagesFromElements([...question, ...answer])

            const result = {
              Q: removeUnnecesarySpaces(questionText),
              A: removeUnnecesarySpaces(answerText),
              data: getDataFromResultImages(images),
              success: true,
            }
            result.data.possibleAnswers = possibleAnswers
            resolve(result)
          })
          .catch((err) => {
            warn('Error in getQuizFromNode() (creating question)')
            warn(err)
            resolve({ success: false })
          })
      } catch (e) {
        warn('Error in getQuizFromNode() (creating promises)')
        warn(e)
        warn(node)
      }
    })
  }

  function getDataFromResultImages(images) {
    if (images && images.length > 0) {
      return {
        type: 'image',
        hashedImages: images.map((x) => {
          return x.val
        }),
        source: 'script',
      }
    } else {
      return {
        type: 'simple',
        source: 'script',
      }
    }
  }

  const questionGetters = {
    getSimpleQuestion: {
      description: 'Basic question getter',
      requirement: (node) => {
        return node.getElementsByClassName('qtext').length > 0
      },
      getterFunction: (node) => {
        const question = node.getElementsByClassName('qtext')[0]
        return getTextPromisesFromNode(question)
      },
    },
  }

  const answerGetters = {
    getSimpleAnswer: {
      description: 'Basic answer getter',
      requirement: (node) => {
        return node.getElementsByClassName('rightanswer').length > 0
      },
      getterFunction: (node) => {
        const answer = node.getElementsByClassName('rightanswer')[0]
        return getTextPromisesFromNode(answer)
      },
    },
    noCorrect: {
      description: 'Gets correct answer, even if the correct is not shown',
      requirement: (node) => {
        return (
          node.getElementsByClassName('rightanswer').length === 0 &&
          node.getElementsByClassName('answer').length > 0
        )
      },
      getterFunction: (node) => {
        const possibleAnswers = getPossibleAnswers(node)

        if (getIfSolutionIsCorrect(node)) {
          if (possibleAnswers.length === 2) {
            return [
              {
                type: 'txt',
                val: possibleAnswers.find((x) => {
                  return x.selectedByUser === false
                }).text,
              },
            ]
          }
        } else {
          const state = node.getElementsByClassName('state')[0]
          if (state && state.innerText === 'Hibás') {
            return false
          }
          return [
            {
              type: 'txt',
              val: possibleAnswers.find((x) => {
                return x.selectedByUser === true
              }).text,
            },
          ]
        }
      },
    },
    /* getDropdownAnswer: {
      description: 'Dropdown answer getter',
      requirement: (node) => {
        return false
      },
      getterFunction: (node) => {
        // TODO dropdown kérdés.html
        return 'asd'
      },
    },
    getTextareaAnswer: {
      description: 'Get complex answer',
      requirement: (node) => {
        return false
      },
      getterFunction: (node) => {
        // TODO Ugrás... bug.html
        return 'asd'
      },
    },
    getDragBoxAnswer: {
      description: 'Get complex answer',
      requirement: (node) => {
        return false
      },
      getterFunction: (node) => {
        // TODO dragboxes
        return 'asd'
      }, 
    },*/
  }

  function getIfSolutionIsCorrect(node) {
    const gradeText = node.getElementsByClassName('grade')[0].innerText
    const stateText = node.getElementsByClassName('state')[0].innerText
    return !(stateText.includes('Helyes') || !gradeText.includes('0,00'))
  }

  function getPossibleAnswers(node) {
    try {
      if (node.getElementsByClassName('answer').length > 0) {
        const answerNodes = Array.from(
          node.getElementsByClassName('answer')[0].childNodes
        )

        return answerNodes.reduce((acc, answerNode) => {
          let selectedByUser
          if (answerNode.childNodes.length > 0) {
            selectedByUser = answerNode.childNodes[0].checked
          }

          const text = removeUnnecesarySpaces(answerNode.innerText)

          if (text !== '') {
            acc.push({
              text: text,
              selectedByUser: selectedByUser,
            })
          }
          return acc
        }, [])
      } else {
        const select = node.getElementsByTagName('select')[0]
        if (select) {
          const options = []
          Array.from(select.childNodes).forEach((opt) => {
            if (!emptyOrWhiteSpace(opt.innerText)) {
              options.push(opt.innerText)
            }
          })
          return options
        }
      }
    } catch (e) {
      warn('Error in getPossibleAnswers()!')
      warn(e)
      warn(node)
    }
  }

  function digestMessage(message) {
    return new Promise((resolve) => {
      const encoder = new TextEncoder()
      const data = encoder.encode(message)
      crypto.subtle.digest('SHA-256', data).then((buf) => {
        let res = String.fromCharCode.apply(null, new Uint8Array(buf))
        res = btoa(res)
          .replace(/=/g, '')
          .replace(/\+/g, '-')
          .replace(/\//g, '_')
        resolve(res)
      })
    })
  }

  function getBase64Image(img) {
    img.crossOrigin = 'Anonymous'
    const canvas = document.createElement('canvas')
    canvas.width = img.width
    canvas.height = img.height
    const ctx = canvas.getContext('2d')
    ctx.drawImage(img, 0, 0)
    const dataURL = canvas.toDataURL('image/png')
    img.crossOrigin = undefined
    return dataURL.replace(/^data:image\/(png|jpg);base64,/, '')
  }

  // : }}}

  // : }}}

  // : AVR {{{

  function getAVRTextFromImg(img) {
    return decodeURIComponent(img.src).split('|')[1]
  }

  function getAVRPossibleAnswersFromQuiz() {
    let i = 1
    let currElem = null
    const elems = []
    do {
      currElem = document.getElementsByClassName(`kvalasz${i}`)[0]
      if (currElem) {
        const img = currElem.getElementsByTagName('img')[0]
        if (img) {
          elems.push({ type: 'txt', val: getAVRTextFromImg(img) })
        } else {
          elems.push({ type: 'txt', val: currElem.innerText })
        }
      }
      i++
    } while (currElem !== undefined)
    return elems
  }

  function getAVRQuestionFromQuiz() {
    const q = document.getElementsByClassName('kkerdes')[0]
    const img = q.getElementsByTagName('img')[0]
    if (img) {
      return getAVRTextFromImg(img)
    } else {
      return simplifyAVRQuestionString(q.innerText)
    }
  }

  function getAVRSubjName() {
    return document.getElementsByTagName('header')[0].innerText
  }

  function HandleAVRResults(url) {
    const tableChilds =
      document.getElementsByTagName('table')[0].childNodes[0].childNodes
    const question = removeUnnecesarySpaces(
      tableChilds[0].innerText.split(':')[1]
    )
    const answer = removeUnnecesarySpaces(
      tableChilds[1].innerText.split(':')[1]
    )
    const correct = removeUnnecesarySpaces(
      tableChilds[2].innerText.split(':')[1]
    )
    if (correct.toLowerCase() === 'helyes') {
      const sentData = {
        subj: getAVRSubjName(),
        version: info().script.version,
        id: getCid(),
        location: url,
        quiz: [
          {
            Q: question.includes('.')
              ? question.split('.').splice(1).join('.').trim()
              : question,
            A: answer,
            data: {
              type: 'simple',
              date: new Date().getTime(),
              source: 'script',
            },
          },
        ],
      }

      log(sentData)
      post('isAdding', sentData).then((res) => {
        ShowSaveQuizDialog(res.success, sentData, res.totalNewQuestions)
      })
    } else {
      ShowMessage('Nem eldönthető a helyes válasz')
    }
  }

  function simplifyAVRQuestionString(val) {
    // FIXME: this is ugly
    let x = val.split('\n')
    x.shift()
    x = x.join('\n').split(' ')
    x.pop()
    return x.join(' ')
  }

  function determineCurrentSite() {
    const tdElems = document.getElementsByTagName('td')
    const kkerdesElements = document.getElementsByClassName('kkerdes')
    if (kkerdesElements.length > 0) {
      return 'TEST'
    } else if (tdElems.length === 10) {
      return 'RESULT'
    } else {
      return 'UI'
    }
  }

  function handleAVRSite(url) {
    let prevLength = -1

    const handler = () => {
      const kkerdesElements = document.getElementsByClassName('kkerdes')
      if (prevLength !== kkerdesElements.length) {
        prevLength = kkerdesElements.length
        clearAllMessages()
        if (determineCurrentSite() === 'TEST') {
          debugLog('AVR: handling test')
          handleAVRQuiz(url)
        } else if (determineCurrentSite() === 'RESULT') {
          debugLog('AVR: handling result')
          HandleAVRResults(url)
        } else {
          debugLog('AVR: handling UI')
          HandleUI()
        }
      }
      setTimeout(handler, 1 * 1000)
    }

    handler()
  }

  function handleAVRQuiz(url) {
    try {
      const { removeMessage: removeLoadingMessage } = ShowMessage(
        texts.loadingAnswer
      )
      const possibleAnswers = getAVRPossibleAnswersFromQuiz()
      const question = getAVRQuestionFromQuiz()

      const sentData = {
        questions: [
          {
            Q: question,
            subj: 'AVR',
            data: { type: 'simple', possibleAnswers: possibleAnswers },
          },
        ],
        testUrl: url,
      }

      log('Sent data', sentData)
      post('ask', sentData).then((results) => {
        removeLoadingMessage()
        ShowAnswers(
          results.map((res, i) => {
            return {
              answers: res.answers,
              question: sentData.questions[i],
            }
          })
        )
      })
    } catch (e) {
      warn('Error in handleAVRQuiz')
      warn(e)
    }
  }

  // : }}}

  // : Canvas {{{

  function handleCanvasQuiz() {
    console.trace()
  }

  function HandleCanvasResults() {
    console.trace()
  }

  // : }}}
  // : Misc {{{

  function getVideo() {
    if (logElementGetting) {
      debugLog('getting video stuff')
    }
    return document.getElementsByTagName('video')[0]
  }

  function getVideoElement() {
    if (logElementGetting) {
      debugLog('getting video element')
    }
    return document.getElementById('videoElement').parentNode
  }

  // : }}}

  // : }}}

  // : Stealth by An0 with love {{{

  function StealthOverlay() {
    //call this before the document scripts
    const document = window.document

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

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

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

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

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

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

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

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

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

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

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

  const overlay = StealthOverlay()

  function createHoverOver(appendTo) {
    const overlayElement = document.createElement('div')
    overlay.append(overlayElement)

    updatableElements.push({ elem: overlayElement, target: appendTo })

    if (elementUpdaterInterval === -1) {
      elementUpdaterInterval = setInterval(() => {
        updatableElements.forEach(({ elem, target }) => {
          let { left, top } = target.getBoundingClientRect()
          const { width, height } = target.getBoundingClientRect()
          left += window.scrollX
          top += window.scrollY

          SetStyle(elem, {
            pointerEvents: 'none',
            userSelect: 'none',
            position: 'absolute',
            zIndex: 999999,
            top: top + 'px',
            left: left + 'px',
            width: width + 'px',
            height: height - 10 + 'px',
          })
        })
      }, overlayElemUpdateInterval * 1000)
    }

    return overlayElement
  }

  function appendBelowElement(el, toAppend) {
    const rect = el.getBoundingClientRect()
    const correction = 8
    const left = rect.left + window.scrollX - correction
    const top = rect.top + window.scrollY - correction

    SetStyle(toAppend, {
      position: 'absolute',
      zIndex: 1,
      top: top + 'px',
      left: left + 'px',
    })

    overlay.appendChild(toAppend)
  }

  // : }}}

  // : Main logic stuff {{{

  // : Main function {{{

  // window.addEventListener("load", () => {})
  Main()

  function Main() {
    'use strict'

    log('Moodle / E-Learning script')

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

  const pageMatchers = [
    {
      matchString: 'canvas',
      testPage: {
        match: (url) => {
          return false // TODO :insert real url
        },
        action: (url) => {
          debugLog('Handling canvas quiz')
          handleCanvasQuiz(url)
        },
      },
      resultPage: {
        match: (url) => {
          return false // TODO :insert real url
        },
        action: (url) => {
          debugLog('Handling canvas results')
          HandleCanvasResults(url)
        },
      },
      default: {
        match: (url) => {
          return false // TODO :insert real url
        },
        action: (url) => {
          debugLog('Handling canvas default action')
          HandleUI(url)
        },
      },
    },
    {
      matchString: 'portal.kgk',
      // testPage: {
      //   match: (url) => {
      //     return url.includes('vizsga')
      //   },
      //   action: (url) => {
      //     handleAVRSite(url)
      //   },
      // },
      // resultPage: {
      //   match: (url) => {
      //     return false // TODO :insert real url
      //   },
      //   action: (url) => {
      //     handleAVRSite(url)
      //   },
      // },
      default: {
        match: (url) => {
          return true // TODO :insert real url
        },
        action: (url) => {
          debugLog('Handling AVR default action')
          handleAVRSite(url)
        },
      },
    },
    {
      matchString: 'default', // moodle, elearning, mooc
      testPage: {
        match: (url) => {
          return (
            (url.includes('/quiz/') && url.includes('attempt.php')) ||
            forceTestPage
          )
        },
        action: () => {
          debugLog('Handling moodle quiz')
          handleMoodleQuiz()
        },
      },
      resultPage: {
        match: (url) => {
          return (
            (url.includes('/quiz/') && url.includes('review.php')) ||
            forceResultPage
          )
        },
        action: (url) => {
          debugLog('Handling moodle results')
          HandleMoodleResults(url)
        },
      },
      default: {
        match: (url) => {
          return (
            (!url.includes('/quiz/') &&
              !url.includes('review.php') &&
              !url.includes('.pdf')) ||
            forceDefaultPage
          )
        },
        action: (url) => {
          debugLog('Handling moodle default action')
          HandleUI(url)
        },
      },
    },
  ]

  function AfterLoad() {
    const url = currUrl

    try {
      pageMatchers.some((matcher) => {
        if (
          url.includes(matcher.matchString) ||
          matcher.matchString === 'default' ||
          matcher.matchString.includes(forcedMatchString)
        ) {
          debugLog(`trying '${matcher.matchString}'`)
          if (matcher.testPage && matcher.testPage.match(url)) {
            matcher.testPage.action(url)
            return true
          } else if (matcher.resultPage && matcher.resultPage.match(url)) {
            matcher.resultPage.action(url)
            return true
          } else if (matcher.default && matcher.default.match(url)) {
            matcher.default.action(url)
            return true
          } else {
            warn(
              'Matcher did not have matched handler implemented, or there was no match!',
              matcher
            )
          }
        }
      })
    } catch (e) {
      ShowMessage(texts.fatalError, undefined, () => {
        OpenErrorPage(e)
      })
      Exception(e, 'script error at main:')
    }
    if (url.includes('eduplayer')) {
      AddVideoHotkeys(url)
    } // adding video hotkeys
    log(texts.consoleErrorInfo)
  }
  // : }}}

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

      FillFeedbackCID(
        url,
        document
          .getElementById('sideBarLinks')
          .getElementsByClassName('active')[0]
      )
    } catch (e) {
      warn('Error filling client ID input', e)
    }
  }

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

  function Init() {
    const url = currUrl

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

    try {
      addEventListener = (function () {
        if (document.addEventListener) {
          return function (element, event, handler) {
            element.addEventListener(event, handler, false)
          }
        } else {
          return function (element, event, handler) {
            element.attachEvent('on' + event, handler)
          }
        }
      })()
    } catch (e) {
      Exception(e, 'script error at addEventListener:')
    }
    VersionActions()
    if (!url.includes('.pdf')) {
      ShowMenu()
    }
    ConnectToServer(AfterLoad)
  }

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

  function resetMenu() {
    SafeGetElementById('scriptMenuDiv', (elem) => {
      elem.style.backgroundColor = '#262626'
    })
    SafeGetElementById('retryContainer', (elem) => {
      elem.style.display = 'none'
    })
    SafeGetElementById('loginDiv', (elem) => {
      elem.style.display = 'none'
    })
    SafeGetElementById('infoMainDiv', (elem) => {
      elem.innerText = texts.loading
    })
  }

  function ConnectToServer(cwith) {
    clearAllMessages()
    GetXHRInfos()
      .then((inf) => {
        if (inf.result === 'nouser') {
          NoUserAction()
          return
        }
        lastestVersion = inf.version.replace(/\n/g, '')
        motd = inf.motd
        if (getUid() !== inf.uid) {
          setVal('userId', inf.uid)
        }
        subjInfo = inf.subjinfo
        setVal('userId', inf.uid)
        overlay.querySelector(
          '#infoMainDiv'
        ).innerText = `${subjInfo.subjects.toLocaleString(
          'hu'
        )} tárgy, ${subjInfo.questions.toLocaleString(
          'hu'
        )} kérdés.\nUser ID: ${getUid()}`
        if (inf.unreads.length > 0) {
          overlay.querySelector('#mailButton').innerText = '📬'
        }
        // FIXME: if cwith() throws an unhandled error it sais server is not avaible
        cwith()
      })
      .catch(() => {
        NoServerAction()
      })
  }

  function NoUserAction() {
    SafeGetElementById('scriptMenuDiv', (elem) => {
      elem.style.backgroundColor = '#44cc00'
    })
    SafeGetElementById('infoMainDiv', (elem) => {
      elem.innerText = ''
    })
    SafeGetElementById('loginDiv', (elem) => {
      elem.style.display = 'flex'
    })
  }

  function NoServerAction() {
    SafeGetElementById('scriptMenuDiv', (elem) => {
      elem.style.backgroundColor = 'red'
    })
    SafeGetElementById('infoMainDiv', (elem) => {
      elem.innerText = texts.noServer
    })
    SafeGetElementById('retryContainer', (elem) => {
      elem.style.display = 'flex'
    })
    log(texts.noServerConsoleMessage)
  }

  function VersionActions() {
    FreshStart()
  }

  // : }}}

  // : UI handling {{{

  function isNewDay() {
    const now = new Date()
    const lastLoad = getVal('lastLoadDate')

    if (new Date(lastLoad).getDay() !== now.getDay()) {
      setVal('lastLoadDate', now.toString())
      return true
    }

    return false
  }

  function shouldShowMotd() {
    if (!emptyOrWhiteSpace(motd)) {
      var prevmotd = getVal('motd')
      if (prevmotd !== motd || isNewDay()) {
        setVal('motdcount', motdShowCount)
        setVal('motd', motd)
        return true
      } else {
        var motdcount = getVal('motdcount')
        if (motdcount === undefined) {
          setVal('motdcount', motdShowCount)
          motdcount = motdShowCount
        }

        motdcount--
        if (motdcount > 0) {
          setVal('motdcount', motdcount)
          return true
        }
      }
    }
  }

  function HandleUI() {
    const newVersion = info().script.version !== getVal('lastVerson')
    const showMOTD = shouldShowMotd()
    const isNewVersionAvaible =
      lastestVersion !== undefined && info().script.version !== lastestVersion

    let timeout = null
    const greetMsg = []

    if (isNewVersionAvaible || newVersion || showMOTD) {
      greetMsg.push(texts.scriptName + info().script.version)
    }
    if (isNewVersionAvaible) {
      timeout = 5
      greetMsg.push(texts.newVersionAvaible + lastestVersion)
      timeout = undefined
    }
    if (newVersion) {
      greetMsg.push(texts.versionUpdated + info().script.version)
      setVal('lastVerson', info().script.version) // setting lastVersion
    }
    if (showMOTD) {
      greetMsg.push(texts.motd + motd)
      timeout = null
    }

    ShowMessage(
      greetMsg.join('\n'),

      timeout
    )
  }

  // : }}}

  // : Answering stuffs {{{

  function PrepareAnswers(result) {
    const { answers, question } = result
    if (answers.length > 0) {
      return answers.map((answer) => {
        const { Q, A, data } = answer.q
        let msg = Q + '\n' + A

        // TODO: show 'képek sorrendben' if there are no new kind of image ids
        if (data.type === 'image') {
          question.images.forEach((img, i) => {
            const regex = new RegExp(`\\[${img.val}\\]`, 'g')
            msg = msg.replace(regex, '[' + i.toString() + ']')
          })
        }

        return {
          m: msg,
          p: answer.match,
          header:
            answer.detailedMatch.matchedSubjName +
            ' - ' +
            answer.detailedMatch.qdb,
        }
      })
    } else {
      return [
        {
          m: 'Erre a kérdésre nincs találat :c',
        },
      ]
    }
  }

  function addImageIdsToImageNodes(imgs) {
    if (!imgs || !Array.isArray(imgs.images)) {
      return
    }

    imgs.images.forEach((img, i) => {
      const text = document.createElement('div')
      text.innerText = `[${i}]`
      SetStyle(text, {
        backgroundColor: '#333',
        borderRadius: '5px',
        color: 'white',
        opacity: 0.7,
        fontSize: '13px',
      })
      appendBelowElement(img.node, text)
    })
  }

  // results = Array<{
  //   answers: Array<{
  //     detailedMatch: {
  //        qMatch: Number,
  //        aMatch: Number,
  //        dMatch: Number,
  //        matchedSubjName: String,
  //        avg: Number
  //     },
  //     match: Number,
  //     q: {
  //        Q: String,
  //        A: String,
  //        cache: {
  //            Q: Array<String>,
  //            A: Array<String>,
  //        },
  //        data: {
  //            type: String,
  //            date: Number
  //            images?: Array<String>
  //        }
  //     }
  //   }>,
  //   question: {
  //     question: String,
  //     success: Boolean,
  //     images: Array<{
  //        val: String,
  //        node: HtmlNode
  //     }>,
  //     data: {
  //        type: String,
  //        date: Number
  //        hashedImages?: Array<String>,
  //        images?: Array<String>
  //     },
  //     possibleAnswers: Array<String>
  //   }
  // }>
  function ShowAnswers(results) {
    log(results)
    try {
      const answers = results.reduce((acc, res) => {
        const prepared = PrepareAnswers(res)
        addImageIdsToImageNodes(res.question)
        if (prepared) {
          acc.push(prepared)
        }
        return acc
      }, [])

      if (answers.length > 0) {
        ShowMessage(answers)
      } else {
        ShowMessage(
          texts.noResult,

          undefined,
          function () {
            OpenErrorPage({
              message: 'No result found',
              question: Array.isArray(answers[0])
                ? answers[0][0].replace(/"/g, '').replace(/:/g, '')
                : answers[0],
            })
          }
        )
      }
    } catch (e) {
      warn('Error showing answers')
      warn(e)
    }
  }

  // : }}}

  // : Quiz saving {{{

  function HandleMoodleResults() {
    getQuiz().then((res) => {
      SaveQuiz(res) // saves the quiz questions and answers
    })
  }

  function ShowSaveQuizDialog(sendResult, sentData, newQuestions) {
    var msg = ''
    if (sendResult) {
      msg = 'Kérdések elküldve, katt az elküldött adatokért.'
      if (newQuestions > 0) {
        msg += ' ' + newQuestions + ' új kérdés'
      } else {
        msg += ' Nincs új kérdés'
      }
    } else {
      msg =
        'Szerver nem elérhető, vagy egyéb hiba kérdések elküldésénél! (F12 -> Console)'
    }
    // showing a message wit the click event, and the generated page
    ShowMessage(
      msg,

      null,
      function () {
        let towrite = ''
        try {
          towrite += '</p>Elküldött adatok:</p> ' + JSON.stringify(sentData)
        } catch (e) {
          towrite += '</p>Elküldött adatok:</p> ' + sentData
        }
        document.write(towrite)
        document.close()
      }
    )
  }

  // saves the current quiz. questionData contains the active subjects questions
  function SaveQuiz(quiz) {
    try {
      let sentData = {}
      if (quiz.length === 0) {
        ShowMessage(texts.noParseableQuestionResult)
        return
      }
      try {
        sentData = {
          version: info().script.version,
          id: getCid(),
          quiz: quiz,
          location: currUrl,
        }
        try {
          sentData.subj = getCurrentSubjectName()
        } catch (e) {
          sentData.subj = 'NOSUBJ'
          warn('unable to get subject name :c')
        }
        log('SENT DATA', sentData)
        post('isAdding', sentData).then((res) => {
          ShowSaveQuizDialog(res.success, sentData, res.totalNewQuestions)
        })
      } catch (e) {
        Exception(e, 'error at sending data to server.')
      }
    } catch (e) {
      Exception(e, 'script error at saving quiz')
    }
  }

  // : }}}

  // : Misc {{{

  // : 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(true) // showing help
      registerScript()

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

  function registerScript() {
    try {
      // uncomment to re-register again every page refresh
      // setVal('registeredWithCid', false)
      // setVal('registeredWithUid', false)

      if (getVal('registeredWithCid')) {
        if (getVal('registeredWithUid')) {
          return
        } else if (!getUid()) {
          return
        }
      }

      setVal('registeredWithCid', true)
      if (getUid()) {
        setVal('registeredWithUid', true)
      }

      post('registerscript', {
        cid: getCid(),
        uid: getUid(),
        version: info().script.version,
        date: new Date(),
        installSource: info().script.updateURL,
      })
    } catch (err) {
      warn('Unexpected error while registering script')
      warn(err)
    }
  }

  // : }}}

  // : Video hotkey stuff {{{

  // this function adds basic hotkeys for video controll.
  function AddVideoHotkeys() {
    var seekTime = 20
    document.addEventListener('keydown', function (e) {
      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) {
        warn('Hotkey error.')
        warn(err.message)
      }
    })
    var toadd = getVideoElement()
    var node = CreateNodeWithText(toadd, texts.videoHelp)
    node.style.margin = '5px 5px 5px 5px' // fancy margin
  }

  // : }}}

  // : }}}

  // : }}}

  // : Show message, and script menu stuff {{{

  function clearAllMessages() {
    overlay.querySelectorAll('#scriptMessage').forEach((x) => x.remove())
  }

  function getConvertedMessageNode(message) {
    const messageNode = document.createElement('p')
    const resultNode = document.createElement('p')
    messageNode.innerHTML = message.replace(/\n/g, '</br>')

    Array.from(messageNode.childNodes).forEach((node) => {
      if (node.tagName === 'A') {
        const linkNode = document.createElement('span')
        SetStyle(linkNode, {
          color: 'lightblue',
          textDecoration: 'underline',
          cursor: 'pointer',
        })
        linkNode.innerText = node.innerText
        linkNode.addEventListener('mousedown', (e) => {
          e.stopPropagation()
          openInTab(node.href, {
            active: true,
          })
        })
        resultNode.appendChild(linkNode)
      } else {
        resultNode.appendChild(node)
      }
    })

    return resultNode
  }

  function addOpacityChangeEvent(elem) {
    if (!elem.id) {
      warn('element must have ID to add opacity change event!')
      return
    }

    let currOpacity = getVal(`${elem.id}_opacity`) || 1
    elem.addEventListener(
      'wheel',
      (e) => {
        e.preventDefault()
        const isUp = e.deltaY < 0
        if (isUp) {
          if (currOpacity + messageOpacityDelta <= 1) {
            currOpacity = currOpacity + messageOpacityDelta
          }
        } else {
          if (currOpacity - messageOpacityDelta > minMessageOpacity) {
            currOpacity = currOpacity - messageOpacityDelta
          }
        }
        elem.style.opacity = currOpacity
        setVal(`${elem.id}_opacity`, currOpacity)
      },
      { capture: true }
    )
  }

  function addMoveEventListener(elem) {
    let isMouseDown = false
    let offset = [0, 0]
    let mousePosition
    elem.addEventListener('mousedown', (e) => {
      isMouseDown = true
      offset = [elem.offsetLeft - e.clientX, elem.offsetTop - e.clientY]
    })
    elem.addEventListener('mouseup', () => {
      isMouseDown = false
    })
    elem.addEventListener('mousemove', (e) => {
      if (isMouseDown) {
        mousePosition = {
          x: e.clientX,
          y: e.clientY,
        }
        elem.style.left = mousePosition.x + offset[0] + 'px'
        elem.style.top = mousePosition.y + offset[1] + 'px'
      }
    })
  }

  function ShowMessage(msgItem, timeout, funct) {
    let isSimpleMessage = false
    let simpleMessageText = ''
    const movingEnabled = !funct
    if (typeof msgItem === 'string') {
      simpleMessageText = msgItem
      if (simpleMessageText === '') {
        // if msg is empty
        return
      }
      msgItem = [
        [
          {
            m: simpleMessageText,
          },
        ],
      ]
      isSimpleMessage = true
    }
    // -------------------------------------------------------------------------
    const id = 'scriptMessage'
    const messageElem = document.createElement('div')
    messageElem.setAttribute('id', id)
    addOpacityChangeEvent(messageElem)
    let width = window.innerWidth - window.innerWidth / 6 // with of the box
    if (width > 900) {
      width = 900
    }
    SetStyle(messageElem, {
      position: 'fixed',
      zIndex: 999999,
      color: '#fff',
      textAlign: 'center',
      border: '2px solid #f2cb05',
      borderRadius: '0px',
      backgroundColor: '#222426',
      top: '30px',
      left: (window.innerWidth - width) / 2 + 'px',
      width: width + 'px',
      opacity: getVal(`${id}_opacity`),
      cursor: funct ? 'pointer' : 'move',
    })
    if (funct) {
      addEventListener(messageElem, 'click', funct)
    }
    if (movingEnabled) {
      addMoveEventListener(messageElem)
    }
    addEventListener(window, 'resize', function () {
      messageElem.style.left = (window.innerWidth - width) / 2 + 'px'
    })

    let timeOut
    if (timeout && timeout > 0) {
      timeOut = setTimeout(function () {
        messageElem.parentNode.removeChild(messageElem)
      }, timeout * 1000)
    }

    addEventListener(messageElem, 'mousedown', function (e) {
      if (e.which === 2) {
        messageElem.parentNode.removeChild(messageElem)
        if (timeOut) {
          clearTimeout(timeOut)
        }
      }
    })

    let currQuestionIndex = 0
    let currPossibleAnswerIndex = 0
    const getCurrMsg = () => {
      return msgItem[currQuestionIndex][currPossibleAnswerIndex]
    }

    // -------------------------------------------------------------------------

    const sidesWidth = '10%'
    const arrowStyle = {
      display: 'flex',
      justifyContent: 'center',
      alignItems: 'center',
      fontSize: '22px',
      userSelect: 'none',
      flex: 1,
    }
    const infoTextStyle = {
      display: 'flex',
      justifyContent: 'center',
      alignItems: 'center',
      flex: 1,
    }

    const childrenTree = {
      header: {
        style: {
          display: getCurrMsg().header ? '' : 'none',
          padding: '5px 0px',
        },
      },
      msgContainer: {
        style: {
          display: 'flex',
          width: '100%',
          padding: '5px 0px',
        },
        children: {
          leftSideDiv: {
            style: {
              display: isSimpleMessage ? 'none' : 'flex',
              flexFlow: 'column',
              width: sidesWidth,
            },
            children: {
              questionIndex: {
                title: 'Kérdés sorszáma / talált válasz sorszáma',
                style: infoTextStyle,
              },
              matchPercent: {
                title: 'Talált kérdés egyezés',
                style: infoTextStyle,
              },
              prevPossible: {
                title: 'Előző lehetséges válasz',
                style: Object.assign(
                  {
                    cursor: 'pointer',
                  },
                  arrowStyle
                ),
                innerText: msgItem[currQuestionIndex].length > 1 ? '⬅️' : '',
                onClick: (e) => {
                  e.stopPropagation()
                  if (currPossibleAnswerIndex > 0) {
                    currPossibleAnswerIndex--
                    updateMessageText()
                  }
                },
              },
            },
          },
          msgDiv: {
            style: {
              flex: '1',
              whiteSpace: 'pre-line',
              cursor: funct ? 'pointer' : 'auto',
            },
          },
          rightSideDiv: {
            style: {
              display: isSimpleMessage ? 'none' : 'flex',
              flexFlow: 'column',
              width: sidesWidth,
            },
            children: {
              prevQuestion: {
                title: 'Előző kérdés',
                style: Object.assign(
                  {
                    cursor: msgItem.length > 1 ? 'pointer' : '',
                  },
                  arrowStyle
                ),
                innerText: msgItem.length > 1 ? '⬆️' : '',
                onClick: (e) => {
                  if (msgItem.length > 1) {
                    e.stopPropagation()
                    if (currQuestionIndex > 0) {
                      currQuestionIndex--
                      updateMessageText()
                    }
                  }
                },
              },
              nextQuestion: {
                title: 'Következő kérdés',
                style: Object.assign(
                  {
                    cursor: msgItem.length > 1 ? 'pointer' : '',
                  },
                  arrowStyle
                ),
                innerText: msgItem.length > 1 ? '⬇️' : '',
                onClick: (e) => {
                  if (msgItem.length > 1) {
                    e.stopPropagation()
                    if (currQuestionIndex < msgItem.length - 1) {
                      currQuestionIndex++
                      updateMessageText()
                    }
                  }
                },
              },
              nextPossible: {
                title: 'Következő lehetséges válasz',
                style: Object.assign(
                  {
                    cursor: 'pointer',
                  },
                  arrowStyle
                ),
                innerText: msgItem[currQuestionIndex].length > 1 ? '➡️' : '',
                onClick: (e) => {
                  e.stopPropagation()
                  if (
                    currPossibleAnswerIndex <
                    msgItem[currQuestionIndex].length - 1
                  ) {
                    currPossibleAnswerIndex++
                    updateMessageText()
                  }
                },
              },
            },
          },
        },
      },
    }

    const result = {}
    createHtml(childrenTree, messageElem, result)

    // -------------------------------------------------------------------------

    result.msgContainer.child.msgDiv.elem.addEventListener('mousedown', (e) => {
      e.stopPropagation()
    })

    const updateMessageText = () => {
      try {
        result.header.elem.innerText = getCurrMsg().header
        result.msgContainer.child.msgDiv.elem.innerText = getCurrMsg().m

        if (msgItem.length !== 1 || msgItem[0].length !== 1) {
          result.msgContainer.child.leftSideDiv.child.questionIndex.elem.innerText =
            (currQuestionIndex + 1).toString() +
            './' +
            (currPossibleAnswerIndex + 1) +
            '.'
        }

        result.msgContainer.child.leftSideDiv.child.matchPercent.elem.innerText =
          isNaN(getCurrMsg().p) ? '' : getCurrMsg().p + '%'

        result.msgContainer.child.msgDiv.elem.innerText = getCurrMsg().m
      } catch (e) {
        warn('Error in message updating')
        warn(e)
      }
    }
    updateMessageText()

    // -------------------------------------------------------------------------

    overlay.appendChild(messageElem)

    return {
      messageElement: messageElem,
      removeMessage: () => {
        messageElem.parentNode.removeChild(messageElem)
      },
    }
  }

  // shows a fancy menu
  function ShowMenu() {
    try {
      // Script menu -----------------------------------------------------------------
      const scriptMenuDiv = document.createElement('div')
      const id = 'scriptMenuDiv'
      scriptMenuDiv.setAttribute('id', id)
      SetStyle(scriptMenuDiv, {
        width: '300px',
        height: '90px',
        position: 'fixed',
        padding: '3px 0px',
        bottom: '30px',
        left: '10px',
        zIndex: 999999,
        border: '2px solid #f2cb05',
        borderRadius: '0px',
        backgroundColor: '#222426',
        opacity: getVal(`${id}_opacity`),
        fontSize: '14px',
      })

      addEventListener(scriptMenuDiv, 'mousedown', function (e) {
        if (e.which === 2) {
          scriptMenuDiv.parentNode.removeChild(scriptMenuDiv)
        }
      })
      addOpacityChangeEvent(scriptMenuDiv)

      const buttonStyle = {
        position: '',
        margin: '3px',
        padding: '4px 8px',
        border: '1px solid #333',
        borderRadius: '2px',
        color: '#ffffff',
        cursor: 'pointer',
      }

      // -----------------------------------------------------------------------------

      const childrenTree = {
        xButton: {
          innerText: '❌',
          style: {
            position: 'absolute',
            display: 'inline',
            right: '0px',
            margin: '5px',
            cursor: 'pointer',
            fontSize: '18px',
          },
          onClick: () => {
            scriptMenuDiv.parentNode.removeChild(scriptMenuDiv)
          },
        },
        mailButton: {
          id: 'mailButton',
          innerText: '📭',
          style: {
            position: 'absolute',
            display: 'inline',
            left: '5px',
            bottom: '15px',
            margin: '5px',
            fontSize: '30px',
            cursor: 'pointer',
          },
          title: 'Messages',
          onClick: () => {
            openInTab(serverAdress + 'chat', {
              active: true,
            })
          },
        },
        buttonContainer: {
          style: {
            display: 'flex',
            justifyContent: 'center',
          },
          children: {
            website: {
              innerText: 'Weboldal',
              style: buttonStyle,
              onClick: () => {
                openInTab(serverAdress + '?menuClick')
              },
            },
            help: {
              innerText: 'Help',
              style: buttonStyle,
              onClick: () => {
                ShowHelp()
              },
            },
            donate: {
              innerText: 'Donate',
              style: buttonStyle,
              onClick: () => {
                openInTab(serverAdress + '?donate=true&scriptMenu=true', {
                  active: true,
                })
              },
            },
          },
        },
        infoContainer: {
          id: 'infoMainDiv',
          innerText: texts.loading,
          style: {
            color: '#ffffff',
            margin: '5px 50px',
            textAlign: 'center',
          },
        },
        loginContainer: {
          id: 'loginDiv',
          style: {
            display: 'none',
            margin: '0px 50px',
          },
          children: {
            loginInput: {
              customElem: () => {
                const loginInput = document.createElement('input')
                loginInput.setAttribute('id', 'pwInput')
                loginInput.type = 'text'
                loginInput.placeholder = texts.pwHere
                SetStyle(loginInput, {
                  width: '100%',
                  textAlign: 'center',
                })
                return loginInput
              },
            },
            loginButton: {
              innerText: texts.login,
              style: buttonStyle,
              onClick: () => {
                SafeGetElementById('pwInput', (elem) => {
                  Auth(elem.value)
                })
              },
            },
          },
        },
        retryContainer: {
          id: 'retryContainer',
          style: {
            display: 'none',
            justifyContent: 'center',
          },
          children: {
            retryButton: {
              innerText: texts.retry,
              style: {
                position: '',
                padding: '0px 8px',
                border: '1px solid #333',
                borderRadius: '2px',
                color: '#ffffff',
                cursor: 'pointer',
              },
              onClick: () => {
                scriptMenuDiv.style.background = '#262626'
                SafeGetElementById('infoMainDiv', (elem) => {
                  elem.innerText = texts.loading
                })
                SafeGetElementById('retryContainer', (elem) => {
                  elem.style.display = 'none'
                })
                ConnectToServer(AfterLoad)
              },
            },
          },
        },
      }

      const result = {}
      createHtml(childrenTree, scriptMenuDiv, result)
      overlay.appendChild(scriptMenuDiv)
    } catch (e) {
      Exception(e, 'script error at showing menu:')
    }
  }

  // : }}}

  // : Generic utils {{{

  // : String utils 2 {{{

  function removeUnnecesarySpaces(toremove) {
    if (!toremove) {
      return ''
    }

    toremove = normalizeSpaces(toremove).replace(/\t/g, '')
    while (toremove.includes('  ')) {
      toremove = toremove.replace(/ {2}/g, ' ')
    }
    while (toremove.includes('\n\n')) {
      toremove = toremove.replace(/\n{2}/g, ' ')
    }
    return toremove.trim()
  }

  function normalizeSpaces(input) {
    assert(input)

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

  function emptyOrWhiteSpace(value) {
    if (value === undefined) {
      return true
    }

    return (
      value
        .replace(/\s/g, '')
        .replace(/\t/g, '')
        .replace(/ /g, '')
        .replace(/\n/g, ' ') === ''
    )
  }

  // : }}}

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

  function logHelper(logMethod, ...value) {
    if (logEnabled) {
      logMethod('[Moodle Script]: ', ...value)
    }
  }

  function warn(value) {
    logHelper(console.warn, value)
  }

  function log() {
    logHelper(console.log, ...arguments)
  }

  function debugLog() {
    if (isDevel) {
      logHelper(console.log, ...arguments)
    }
  }

  function Exception(e, msg) {
    log('------------------------------------------')
    log(msg)
    log(e.message)
    log('------------------------------------------')
    log(e.stack)
    log('------------------------------------------')
  }

  function getUid() {
    return getVal('userId')
  }

  function getCid() {
    let currId = getVal('clientId')
    if (currId) {
      return currId
    } else {
      currId = new Date()
      currId = currId.getTime() + Math.floor(Math.random() * 1000000000000)
      currId = currId.toString().split('')
      currId.shift()
      currId = '0' + currId.join('')
      setVal('clientId', currId)
      return currId
    }
  }

  function SafeGetElementById(id, next) {
    const element = overlay.querySelector('#' + id)
    if (element) {
      next(element)
    } else {
      log(`Unable to safe get element by id: ${id}`)
    }
  }

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

  function createHtml(children, appendTo, result) {
    try {
      Object.keys(children).forEach((key) => {
        const currElem = children[key]
        const elem = currElem.customElem
          ? currElem.customElem()
          : document.createElement('div')
        appendTo.appendChild(elem)
        result[key] = {
          elem: elem,
          child: {},
        }

        if (!currElem.customElem) {
          if (currElem.id) {
            elem.setAttribute('id', currElem.id)
          }
          if (currElem.title) {
            elem.title = currElem.title
          }
          if (currElem.innerText) {
            elem.innerText = currElem.innerText
          }
          if (currElem.onClick) {
            elem.addEventListener('mousedown', (e) => {
              currElem.onClick(e)
            })
          }
          if (currElem.style) {
            SetStyle(elem, currElem.style)
          }
        }

        if (currElem.children) {
          createHtml(currElem.children, elem, result[key].child)
        }
      })
    } catch (e) {
      warn('Error in createHtml')
      warn(e)
    }
  }

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

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

    let lastInfo = { result: 'noLastInfo' }
    try {
      lastInfo = JSON.parse(getVal('lastInfo'))
    } catch (e) {
      if (showErrors) {
        warn(e)
      }
    }

    if (
      lastInfo.result !== 'success' ||
      now > lastCheck + infoExpireTime * 1000
    ) {
      return new Promise((resolve, reject) => {
        const url =
          apiAdress +
          'infos?version=true&motd=true&subjinfo=true&cversion=' +
          info().script.version +
          '&cid=' +
          getCid()

        Promise.all([get(url), get(apiAdress + 'hasNewMsg')])
          .then(([{ responseText: infos }, { responseText: hasNewMsg }]) => {
            try {
              setVal('lastInfoCheckTime', now)
              const infosObj = JSON.parse(infos)
              const hasNewMsgsObj = JSON.parse(hasNewMsg)
              const merged = Object.assign({}, infosObj, hasNewMsgsObj)
              setVal('lastInfo', JSON.stringify(merged))
              resolve(merged)
            } catch (e) {
              log('Errro paring JSON in GetXHRInfos')
              log({ infos: infos, hasNewMsg: hasNewMsg })
              log(e)
              reject(e)
            }
          })
          .catch((e) => {
            log('Info get Error', e)
            reject(e)
          })
      })
    } else {
      return new Promise((resolve, reject) => {
        try {
          resolve(lastInfo)
        } catch (e) {
          log('Errro paring JSON in GetXHRInfos, when using old data!')
          log(e)
          reject(e)
        }
      })
    }
  }

  function get(url) {
    return new Promise((resolve, reject) => {
      xmlhttpRequest({
        method: 'GET',
        url: url,
        crossDomain: true,
        xhrFields: { withCredentials: true },
        headers: {
          'Content-Type': 'application/json',
        },
        onload: function (response) {
          resolve(response)
        },
        onerror: (e) => {
          reject(e)
        },
      })
    })
  }

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

  function OpenErrorPage(e) {
    const queries = []
    try {
      Object.keys(e).forEach((key) => {
        if (e[key]) {
          queries.push(`${key}=${encodeURIComponent(e[key])}`)
          queries.push('db=all')
        }
      })
      queries.push('version=' + encodeURIComponent(info().script.version))
      queries.push('uid=' + encodeURIComponent(getUid()))
      queries.push('cid=' + encodeURIComponent(getCid()))
    } catch (e) {
      Exception(e, 'error at setting error stack/msg link')
    }
    openInTab(serverAdress + 'lred?' + queries.join('&'), {
      active: true,
    })
  }

  // : }}}

  // : Help {{{

  // shows some neat help
  function ShowHelp(firstRun) {
    let q = '?tab=script'
    if (firstRun) {
      q += '&firstRun=true'
    } else {
      q += '&fromScript=true'
    }
    openInTab(serverAdress + q, {
      active: true,
    })
  }

  // : }}}

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

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

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

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

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

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