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

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

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

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

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

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

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

// TODO: test if this ; does not fuck up things (it seams it does not)
;(function() {
  // eslint-disable-line
  // GM functions, only to disable ESLINT errors
  /* eslint-disable  */
  const a = Main
  const usf = unsafeWindow
  function getVal(name) {
    return GM_getValue(name)
  }
  function setVal(name, val) {
    return GM_setValue(name, val)
  }
  function delVal(name) {
    return GM_deleteValue(name)
  }
  function openInTab(address, options) {
    GM_openInTab(address, options)
  }
  function xmlhttpRequest(opts) {
    GM_xmlhttpRequest(opts)
  }
  function info() {
    return GM_info
  }
  /* eslint-enable */

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

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

  const motdShowCount = 3 /* Ammount of times to show motd */
  let infoExpireTime = 60 // Every n seconds basic info should be loaded from server
  var uid = 0
  var cid = 0
  var motd = ''
  var userSpecificMotd = ''
  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 (getVal('ISDEVEL')) {
    console.log('Moodle script running in developement mode!')
    infoExpireTime = 1
    serverAdress = 'http://localhost:8080/'
    apiAdress = 'http://localhost:8080/'
  }

  const huTexts = {
    lastChangeLog: '',
    fatalError:
      'Fatál error. Check console (f12). Kattints az üzenetre az összes kérdés/válaszért manuális kereséshez!',
    consoleErrorInfo:
      'Itteni hibák 100% a moodle hiba. Kivéve, ha oda van írva hogy script error ;) Ha ilyesmi szerepel itt, akkor olvasd el a segítség szekciót! 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!',
    videoHelp: 'Miután elindítottad: Play/pause: space. Seek: Bal/jobb nyíl.',
    menuButtonText: 'Kérdések Menu',
    couldntLoadDataPopupMenuText:
      'A kérdéseket nem lehetett beolvasni, ellenőrizd hogy elérhető-e a szerver',
    showGreetingOnEveryPage: 'Üdvözlő üzenet mutatása minden oldalon',
    close: 'Bezárás',
    help: 'Help',
    websiteBugreport: 'Weboldal / Bug report',
    contribute: 'Contribute',
    donate: 'Donate',
    retry: 'Újrapróbálás',
    ircButton: 'IRC',
    invalidPW: 'Hibás jelszó: ',
    search: 'Keresés ...',
    loading: 'Betöltés ...',
    login: 'Belépés',
    requestPWInsteadOfLogin: 'Jelszó igénylés',
    contributeTitle: 'Hozzájárulás a script és weboldal fejleszétéshez',
    newPWTitle: 'Új jelszó új felhasználónak',
    pwRequest: 'Új jelszó',
    noServer: 'Nem elérhető a szerver!',
    noUser: 'Nem vagy bejelentkezve!',
    noServerConsoleMessage: `Nem elérhető a szerver, vagy kis eséllyel kezeletlen hiba történt! Ha elérhető a weboldal, akkor ott meg bírod nézni a kérdéseket itt: ${serverAdress}legacy`,
  }

  var texts = huTexts

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

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

  // ----------------------------------------------------------------------------------------------
  // Basic processing helpers
  // ----------------------------------------------------------------------------------------------

  function getTextPromisesFromNode(node) {
    const promises = []
    Array.from(node.childNodes).forEach(elem => {
      if (elem.tagName === 'IMG') {
        promises.push(digestMessage(getBase64Image(elem)))
      } else if (elem.tagName === undefined) {
        promises.push({ type: 'txt', val: elem.nodeValue })
      } else {
        promises.push({ type: 'txt', val: elem.innerText })
      }
    })
    return promises
  }

  function makeTextFromElements(item) {
    // TODO!
    // if (emptyOrWhiteSpace(item)) {
    //   return ''
    // }

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

  function getImagesFromElements(elements) {
    return elements.reduce((acc, element) => {
      if (element.type === 'img') {
        acc.push(element.val)
      }
      return acc
    }, [])
  }

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

  function uniq(a) {
    return [...new Set(a)]
  }

  // ----------------------------------------------------------------------------------------------
  // Test page processing functions
  // ----------------------------------------------------------------------------------------------

  function handleQuiz() {
    // TODO: dropdown in question
    // TODO: multiple selects, img in question
    // TODO:
    const promises = []
    const subjName = getCurrentSubjectName()
    const questionNodes = Array.from(
      document.getElementsByTagName('form')[0].childNodes[0].childNodes
    )

    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(res => {
        console.log(res)
      })
      .catch(err => {
        console.warn('Error in handleQuiz()')
        console.warn(err)
      })
  }

  function getPossibleAnswersFromTest(node) {
    const promises = []
    const answers = Array.from(
      node.getElementsByClassName('answer')[0].childNodes
    )

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

    return promises
  }

  function getQuestionPromiseForSingleQuestion(node) {
    return new Promise(resolve => {
      const qtextNode = node.getElementsByClassName('qtext')[0]

      const questionPromises = getTextPromisesFromNode(qtextNode)
      const possibleAnswerPromises = getPossibleAnswersFromTest(node)

      Promise.all([
        Promise.all(questionPromises),
        Promise.all(possibleAnswerPromises),
      ])
        .then(([question, possibleAnswerArray]) => {
          const questionText = question.map(makeTextFromElements).join(' ')
          const images = getImagesFromElements(question)
          const data = getDataFromTest(images)
          const possibleAnswers = possibleAnswerArray.map(x => {
            return removeUnnecesarySpaces(x.map(makeTextFromElements).join(' '))
          })

          console.log('\n\n\n')
          console.log(questionText)
          console.log(images)
          possibleAnswers.forEach(x => {
            console.log(x)
          })

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

  function getDataFromTest(images) {
    if (images.length > 0) {
      return {
        type: 'image',
        images: images,
      }
    } else {
      return {
        type: 'simple',
      }
    }
  }

  // ----------------------------------------------------------------------------------------------
  // Result page processing functions
  // ----------------------------------------------------------------------------------------------

  function getQuiz() {
    return new Promise(resolve => {
      const promises = []
      const questionNodes = Array.from(
        document.getElementsByTagName('form')[0].childNodes[0].childNodes
      )

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

      // [{
      //    "Q": "Mekkora tényezővel kell számolnunk, ha 100.000 Ft jelenértékét keressük 24% kamatláb, havi tőkésítés és 2,5 éves futamidő mellett?\n\n\n",
      //    "A": "c.\n\n0,552\n",
      //    "data": {
      //      "type": "simple"
      //    }
      // }]
      Promise.all(promises)
        .then(result => {
          resolve(result)
        })
        .catch(err => {
          console.warn('Error in getQuiz()')
          console.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)
          return true
        } catch (e) {
          Log(`${key} failed`)
        }
      } else {
        Log(`${key} did not pass`)
      }
    })

    return res
  }

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

      if (!answerPromises || !questionPromises) {
        Log('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.map(makeTextFromElements).join(' ')
          const answerText = answer.map(makeTextFromElements).join(' ')
          let images = getImagesFromElements([...question, ...answer])
          images = uniq(images)

          resolve({
            Q: removeUnnecesarySpaces(questionText),
            A: removeUnnecesarySpaces(answerText),
            data: getData(images),
            success: true,
          })
        })
        .catch(err => {
          console.warn('Error in getQuizFromNode()')
          console.warn(err)
        })
    })
  }

  function getData(images) {
    if (!images || images.length === 0) {
      return {
        type: 'simple',
      }
    } else {
      return {
        type: 'image',
        images: images,
      }
    }
  }

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

  const answerGetters = {
    getSimpleAnswer: {
      description: 'Basic answer getter',
      index: 0,
      requirement: node => {
        return node.getElementsByClassName('rightanswer').length > 0
      },
      getterFunction: node => {
        let answer = node.getElementsByClassName('rightanswer')[0]
        return getTextPromisesFromNode(answer)
      },
    },
    noCorrect: {
      description: 'Gets correct answer, even if the correct is not shown',
      index: 2,
      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 {
          return [
            {
              type: 'txt',
              val: possibleAnswers.find(x => {
                return x.selectedByUser === true
              }).text,
            },
          ]
        }
      },
    },
    getDropdownAnswer: {
      description: 'Dropdown answer getter',
      index: 1,
      requirement: node => {
        return false
      },
      getterFunction: node => {
        // TODO dropdown kérdés.html
        return 'asd'
      },
    },
    getTextareaAnswer: {
      description: 'Get complex answer',
      index: 1,
      requirement: node => {
        return false
      },
      getterFunction: node => {
        // TODO Ugrás... bug.html
        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) {
    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
      }

      acc.push({
        text: answerNode.innerText,
        selectedByUser: selectedByUser,
      })
      return acc
    }, [])
  }

  function digestMessage(message) {
    return new Promise(resolve => {
      const encoder = new TextEncoder()
      const data = encoder.encode(message)
      const hash = 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({ type: 'img', val: res })
      })
    })
  }

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

  // ----------------------------------------------------------------------------------------------
  // String utils 2
  // ----------------------------------------------------------------------------------------------

  function removeUnnecesarySpaces(toremove) {
    assert(toremove)

    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(/\n/g, '')
        .replace(/\t/g, '')
        .replace(/ /g, '')
        .replace(/\s/g, ' ') === ''
    )
  }

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

  // : }}}

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

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

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

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

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

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

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

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

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

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

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

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

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

  const overlay = StealthOverlay()

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

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

    overlay.appendChild(toAppend)
  }

  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 currX, currY, currWidth, currHeight
          let { left, top, 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
  }

  // ----------------------------------------------------------------------------------------------
  // Misc
  // ----------------------------------------------------------------------------------------------

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

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

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

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

  // : }}}

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

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

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

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

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

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

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

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

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

  // : Main logic stuff {{{

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

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

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

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

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

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

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

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

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

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

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

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

    FreshStart()
  }

  // : Version action functions {{{

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

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

  // : }}}

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

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

    let showMOTD = false
    if (!emptyOrWhiteSpace(motd)) {
      var prevmotd = getVal('motd')
      if (prevmotd !== motd) {
        showMOTD = true
        setVal('motdcount', motdShowCount)
        setVal('motd', motd)
      } else {
        var motdcount = getVal('motdcount')
        if (motdcount === undefined) {
          setVal('motdcount', motdShowCount)
          motdcount = motdShowCount
        }

        motdcount--
        if (motdcount > 0) {
          showMOTD = true
          setVal('motdcount', motdcount)
        }
      }
    }
    const showUserSpecificMOTD = !!userSpecificMotd

    let isNewVersionAvaible =
      lastestVersion !== undefined && info().script.version !== lastestVersion
    var greetMsg = '' // message to show at the end
    var timeout = null // the timeout. if null, it wont be hidden

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

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

  // : }}}

  // : Answering stuffs {{{

  // TODO
  function PrepareAnswers(result, j) {
    assert(result)

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

  // TODO
  function ShowAnswers(answers, question) {
    assert(answers)

    if (answers.length > 0) {
      // if there are more than 0 answer
      ShowMessage(answers)
    } else {
      ShowMessage(
        {
          m: texts.noResult,
          isSimple: true,
        },
        undefined,
        function() {
          OpenErrorPage({
            message: 'No result found',
            question: Array.isArray(question)
              ? question[0].replace(/"/g, '').replace(/:/g, '')
              : question,
          })
        }
      )
    }
  }

  // : }}}

  // : Quiz saving {{{

  function HandleResults(url) {
    getQuiz().then(res => {
      console.log('\n\n\n')
      res.forEach((r, i) => {
        console.log('')
        console.log(i + 1)
        console.log(r.Q)
        console.log(r.A)
        console.log(r.data)
      })
      SaveQuiz(res, ShowSaveQuizDialog) // saves the quiz questions and answers
    })
  }

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

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

  // : }}}

  // : Helpers {{{

  // adds image names to image nodes
  function AddImageNamesToImages(imgs) {
    // TODO
    // for (var i = 0; i < imgs.length; i++) {
    //   if (!imgs[i].src.includes('brokenfile')) {
    //     // TODO: add this to shadowroot
    //     var filePart = imgs[i].src.split('/') // splits the link by "/"
    //     // console.log(imgs[i].src.split("base64,")[1])
    //     // TODO: base64
    //     filePart = filePart[filePart.length - 1] // the last one is the image name
    //     var appendTo = 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)
    //     appendBelowElement(appendTo, mainDiv)
    //   }
    // }
  }

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

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

    if (dotIndex < maxInd) {
      return removeUnnecesarySpaces(
        inp.substr(inp.indexOf('.') + 1, inp.length)
      )
    } else if (doubledotIndex < maxInd) {
      return removeUnnecesarySpaces(
        inp.substr(inp.indexOf(':') + 1, inp.length)
      )
    } else {
      return inp
    }
  }

  // highlights the possible solutions to the current question
  function HighLightAnswer(results, currQuestionNumber) {
    // TODO: fix this
  }

  // : }}}

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

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

  // : }}}

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

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

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

      var mainDiv = document.createElement('div') // the main divider, wich items will be attached to
      mainDiv.setAttribute('id', 'messageMainDiv')
      if (funct) {
        // if there is a function as parameter
        addEventListener(mainDiv, 'click', funct) // adding it as click
      }
      // lotsa crap style
      SetStyle(mainDiv, {
        position: 'fixed',
        zIndex: 999999,
        textAlign: 'center',
        width: width + 'px',
        padding: '0px',
        background: '#222d32',
        color: '#ffffff',
        border: '3px solid #99f',
        borderRadius: '5px',
        top: startFromTop + 'px',
        left: (window.innerWidth - width) / 2 + 'px',
        opacity: '1',
        cursor: 'move',
      })
      mainDiv.setAttribute('id', 'scriptMessage')
      // ------------------------------------------------------------------
      // moving msg
      // ------------------------------------------------------------------
      let isMouseDown = false
      let offset = [0, 0]
      let mousePosition
      mainDiv.addEventListener('mousedown', e => {
        isMouseDown = true
        offset = [mainDiv.offsetLeft - e.clientX, mainDiv.offsetTop - e.clientY]
      })
      mainDiv.addEventListener('mouseup', e => {
        isMouseDown = false
      })
      mainDiv.addEventListener('mousemove', e => {
        if (isMouseDown) {
          mousePosition = {
            x: e.clientX,
            y: e.clientY,
          }
          mainDiv.style.left = mousePosition.x + offset[0] + 'px'
          mainDiv.style.top = mousePosition.y + offset[1] + 'px'
        }
      })

      const xrow = document.createElement('div')
      SetStyle(xrow, {
        height: '20px',
        display: 'flex',
        justifyContent: 'flex-end',
      })
      mainDiv.appendChild(xrow)

      const xButton = CreateNodeWithText(xrow, '❌', 'div')
      SetStyle(xButton, {
        margin: '5px',
        cursor: 'pointer',
      })
      xButton.addEventListener('mousedown', e => {
        e.stopPropagation()
        mainDiv.parentNode.removeChild(mainDiv)
      })

      // ------------------------------------------------------------------
      var matchPercent = msgItem[0][0].p
      if (isSimpleMessage) {
        var simpleMessageParagrapg = document.createElement('p') // new paragraph
        simpleMessageParagrapg.style.margin = defMargin // fancy margin

        var mesageNode = document.createElement('p') // new paragraph
        mesageNode.innerHTML = simpleMessageText.replace(/\n/g, '</br>')
        simpleMessageParagrapg.appendChild(mesageNode)
        SetStyle(mesageNode, {
          margin: defMargin,
        })

        Array.from(mesageNode.getElementsByTagName('a')).forEach(anchorElem => {
          anchorElem.style.color = 'lightblue'
        })

        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.addEventListener('mousedown', e => {
          e.stopPropagation()
        })

        SetStyle(questionTextElement, {
          cursor: 'auto',
        })

        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 = removeUnnecesarySpaces(
            relevantQuestion.m
          )
          if (currItem === 0 && currRelevantQuestion === 0) {
            numberTextBox.innerText = currRelevantQuestion + 1 + '.'
          } else {
            numberTextBox.innerText =
              currItem + 1 + './' + (currRelevantQuestion + 1) + '.'
          }
          percentTextBox.innerText = relevantQuestion.p + '%'
        }

        const buttonStyle = {
          color: 'white',
          backgroundColor: 'transparent',
          margin: buttonMargin,
          border: 'none',
          fontSize: '30px',
          cursor: 'pointer',
          userSelect: 'none',
        }
        var buttonMargin = '2px 2px 2px 2px' // uniform button margin
        if (msgItem[currItem].length > 1) {
          // PREV SUGG BUTTON ------------------------------------------------------------------------------------------------------------
          var prevSuggButton = CreateNodeWithText(
            prevSuggestionCell,
            '⬅️',
            'div'
          )
          SetStyle(prevSuggButton, buttonStyle)

          prevSuggButton.addEventListener('mousedown', function(e) {
            e.stopPropagation()
            ChangeCurrRelevantQuestionIndex(-1)
            SetQuestionText()
          })
          // NEXT SUGG BUTTON ------------------------------------------------------------------------------------------------------------
          var nextSuggButton = CreateNodeWithText(
            nextSuggestionCell,
            '➡️',
            'div'
          )
          SetStyle(nextSuggButton, buttonStyle)

          nextSuggButton.addEventListener('mousedown', function(e) {
            e.stopPropagation()
            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, '⬆️', 'div')
          SetStyle(prevButton, buttonStyle)

          // event listener
          prevButton.addEventListener('click', function() {
            ChangeCurrItemIndex(-1)
            SetQuestionText()
          })
          // NEXT QUESTION BUTTON ------------------------------------------------------------------------------------------------------------
          var nextButton = CreateNodeWithText(nextQuestionCell, '⬇️', 'div')
          SetStyle(nextButton, buttonStyle)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      SetStyle(loginButton, buttonStyle)

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

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

      ibuttonCell.appendChild(loginDiv)

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

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

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

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

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

      // APPEND EVERYTHING
      appedtTo.appendChild(menuButtonDiv)

      addEventListener(menuButtonDiv, 'mousedown', function(e) {
        if (e.which === 2) {
          menuButtonDiv.parentNode.removeChild(menuButtonDiv)
        }
      })
    } catch (e) {
      Exception(e, 'script error at showing menu:')
    }
  }

  // : }}}

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

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

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

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

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

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

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

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

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

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

  function OpenErrorPage(e) {
    const queries = []
    try {
      Object.keys(e).forEach(key => {
        if (e[key]) {
          queries.push(`${key}=${encodeURIComponent(e[key])}`)
        }
      })
      queries.push('version=' + encodeURIComponent(info().script.version))
      queries.push('uid=' + encodeURIComponent(uid))
      queries.push('cid=' + encodeURIComponent(cid))
    } 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() {
    openInTab(serverAdress + 'manual?scriptMenu', {
      active: true,
    })
  }

  // : }}}

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

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

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

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

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

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