let wordsList = [];
let currentWordIndex = 0;
let currentWordElementIndex = 0;
let inputHistory = [];
let correctedHistory = [];
let currentCorrected = "";
let currentInput = "";
let time = 0;
let timer = null;
let testActive = false;
let testStart, testEnd;
let testInvalid = false;
let wpmHistory = [];
let rawHistory = [];
let restartCount = 0;
let incompleteTestSeconds = 0;
let currentTestLine = 0;
let pageTransition = false;
let lineTransition = false;
let keypressPerSecond = [];
let currentKeypress = {
  count: 0,
  mod: 0,
  words: [],
};
let errorsPerSecond = [];
let currentError = {
  count: 0,
  words: [],
};
let resultVisible = false;
let activeWordTopBeforeJump = 0;
let activeWordTop = 0;
let activeWordJumped = false;
let sameWordset = false;
let quotes = null;
let focusState = false;
let activeFunBox = "none";
let manualRestart = false;
let bailout = false;
let notSignedInLastResult = null;
let caretAnimating = true;
let lastSecondNotRound = false;
let paceCaret = null;
let missedWords = [];
let verifyUserWhenLoggedIn = null;
let modeBeforePractise = null;
let punctuationBeforePractise = null;
let numbersBeforePractise = null;
let memoryFunboxTimer = null;
let memoryFunboxInterval = null;
let textHasTab = false;

let themeColors = {
  bg: "#323437",
  main: "#e2b714",
  caret: "#e2b714",
  sub: "#646669",
  text: "#d1d0c5",
  error: "#ca4754",
  errorExtra: "#7e2a33",
  colorfulError: "#ca4754",
  colorfulErrorExtra: "#7e2a33",
};

let accuracyStats = {
  correct: 0,
  incorrect: 0,
};

let keypressStats = {
  spacing: {
    current: -1,
    array: [],
  },
  duration: {
    current: -1,
    array: [],
  },
};

let errorSound = new Audio("../sound/error.wav");
let clickSounds = null;

let isPreviewingTheme = false;

function initClickSounds() {
  clickSounds = {
    1: [
      {
        sounds: [
          new Audio("../sound/click1/click1_1.wav"),
          new Audio("../sound/click1/click1_1.wav"),
        ],
        counter: 0,
      },
      {
        sounds: [
          new Audio("../sound/click1/click1_2.wav"),
          new Audio("../sound/click1/click1_2.wav"),
        ],
        counter: 0,
      },
      {
        sounds: [
          new Audio("../sound/click1/click1_3.wav"),
          new Audio("../sound/click1/click1_3.wav"),
        ],
        counter: 0,
      },
    ],
    2: [
      {
        sounds: [
          new Audio("../sound/click2/click2_1.wav"),
          new Audio("../sound/click2/click2_1.wav"),
        ],
        counter: 0,
      },
      {
        sounds: [
          new Audio("../sound/click2/click2_2.wav"),
          new Audio("../sound/click2/click2_2.wav"),
        ],
        counter: 0,
      },
      {
        sounds: [
          new Audio("../sound/click2/click2_3.wav"),
          new Audio("../sound/click2/click2_3.wav"),
        ],
        counter: 0,
      },
    ],
    3: [
      {
        sounds: [
          new Audio("../sound/click3/click3_1.wav"),
          new Audio("../sound/click3/click3_1.wav"),
        ],
        counter: 0,
      },
      {
        sounds: [
          new Audio("../sound/click3/click3_2.wav"),
          new Audio("../sound/click3/click3_2.wav"),
        ],
        counter: 0,
      },
      {
        sounds: [
          new Audio("../sound/click3/click3_3.wav"),
          new Audio("../sound/click3/click3_3.wav"),
        ],
        counter: 0,
      },
    ],
    4: [
      {
        sounds: [
          new Audio("../sound/click4/click4_1.wav"),
          new Audio("../sound/click4/click4_1.wav"),
        ],
        counter: 0,
      },
      {
        sounds: [
          new Audio("../sound/click4/click4_2.wav"),
          new Audio("../sound/click4/click4_2.wav"),
        ],
        counter: 0,
      },
      {
        sounds: [
          new Audio("../sound/click4/click4_3.wav"),
          new Audio("../sound/click4/click4_3.wav"),
        ],
        counter: 0,
      },
      {
        sounds: [
          new Audio("../sound/click4/click4_4.wav"),
          new Audio("../sound/click4/click4_4.wav"),
        ],
        counter: 0,
      },
      {
        sounds: [
          new Audio("../sound/click4/click4_5.wav"),
          new Audio("../sound/click4/click4_5.wav"),
        ],
        counter: 0,
      },
      {
        sounds: [
          new Audio("../sound/click4/click4_6.wav"),
          new Audio("../sound/click4/click4_6.wav"),
        ],
        counter: 0,
      },
    ],
  };
}

let customText = {
  text: "The quick brown fox jumps over the lazy dog".split(" "),
  isWordRandom: false,
  isTimeRandom: false,
  word: "",
  time: "",
};

// let customText = "The quick brown fox jumps over the lazy dog".split(" ");
// let customText.isWordRandom = false;
// let customText.word = 1;
let randomQuote = null;

function refreshThemeColorObject() {
  let st = getComputedStyle(document.body);

  themeColors.bg = st.getPropertyValue("--bg-color").replace(" ", "");
  themeColors.main = st.getPropertyValue("--main-color").replace(" ", "");
  themeColors.caret = st.getPropertyValue("--caret-color").replace(" ", "");
  themeColors.sub = st.getPropertyValue("--sub-color").replace(" ", "");
  themeColors.text = st.getPropertyValue("--text-color").replace(" ", "");
  themeColors.error = st.getPropertyValue("--error-color").replace(" ", "");
  themeColors.errorExtra = st
    .getPropertyValue("--error-extra-color")
    .replace(" ", "");
  themeColors.colorfulError = st
    .getPropertyValue("--colorful-error-color")
    .replace(" ", "");
  themeColors.colorfulErrorExtra = st
    .getPropertyValue("--colorful-error-extra-color")
    .replace(" ", "");
  updateChartColors();
}

function copyResultToClipboard() {
  $(".pageTest .ssWatermark").removeClass("hidden");
  $(".pageTest .buttons").addClass("hidden");
  let src = $("#middle");
  var sourceX = src.position().left; /*X position from div#target*/
  var sourceY = src.position().top; /*Y position from div#target*/
  var sourceWidth = src.width(); /*clientWidth/offsetWidth from div#target*/
  var sourceHeight = src.height(); /*clientHeight/offsetHeight from div#target*/
  $(".notification").addClass("hidden");
  $(".pageTest .loginTip").addClass("hidden");
  try {
    html2canvas(document.body, {
      backgroundColor: themeColors.bg,
      height: sourceHeight + 50,
      width: sourceWidth + 50,
      x: sourceX - 25,
      y: sourceY - 25,
    }).then(function (canvas) {
      canvas.toBlob(function (blob) {
        try {
          navigator.clipboard
            .write([
              new ClipboardItem(
                Object.defineProperty({}, blob.type, {
                  value: blob,
                  enumerable: true,
                })
              ),
            ])
            .then((f) => {
              $(".notification").removeClass("hidden");
              Notifications.add("Copied to clipboard", 1, 2);
              $(".pageTest .ssWatermark").addClass("hidden");
              $(".pageTest .buttons").removeClass("hidden");
              if (firebase.auth().currentUser == null)
                $(".pageTest .loginTip").removeClass("hidden");
            })
            .catch((f) => {
              open(URL.createObjectURL(blob));
              $(".notification").removeClass("hidden");
              Notifications.add(
                "Error saving image to clipboard: " + f.message,
                -1
              );
              $(".pageTest .ssWatermark").addClass("hidden");
              $(".pageTest .buttons").removeClass("hidden");
              if (firebase.auth().currentUser == null)
                $(".pageTest .loginTip").removeClass("hidden");
            });
        } catch (e) {
          open(URL.createObjectURL(blob));
          $(".notification").removeClass("hidden");
          Notifications.add(
            "Error saving image to clipboard: " + e.message,
            -1
          );
          $(".pageTest .ssWatermark").addClass("hidden");
          $(".pageTest .buttons").removeClass("hidden");
          if (firebase.auth().currentUser == null)
            $(".pageTest .loginTip").removeClass("hidden");
        }
      });
    });
  } catch (e) {
    $(".notification").removeClass("hidden");
    Notifications.add("Error creating image: " + e.message, -1);
    $(".pageTest .ssWatermark").addClass("hidden");
    $(".pageTest .buttons").removeClass("hidden");
    if (firebase.auth().currentUser == null)
      $(".pageTest .loginTip").removeClass("hidden");
  }
}

async function activateFunbox(funbox, mode) {
  if (testActive || resultVisible) {
    Notifications.add(
      "You can only change the funbox before starting a test.",
      0
    );
    return false;
  }
  if (Misc.getCurrentLanguage().ligatures) {
    if (funbox == "choo_choo" || funbox == "earthquake") {
      Notifications.add(
        "Current language does not support this funbox mode",
        0
      );
      activateFunbox("none", null);
      return;
    }
  }
  $("#funBoxTheme").attr("href", ``);
  $("#words").removeClass("nospace");
  // if (funbox === "none") {
  activeFunBox = "none";
  memoryFunboxInterval = clearInterval(memoryFunboxInterval);
  memoryFunboxTimer = null;
  $("#wordsWrapper").removeClass("hidden");
  // }

  if (mode === null || mode === undefined) {
    let list = await Misc.getFunboxList();
    mode = list.filter((f) => f.name === funbox)[0].type;
  }

  manualRestart = true;
  if (mode === "style") {
    if (funbox != undefined) {
      $("#funBoxTheme").attr("href", `funbox/${funbox}.css`);
      activeFunBox = funbox;
    }

    if (funbox === "simon_says") {
      setKeymapMode("next");
      settingsGroups.keymapMode.updateButton();
      restartTest();
    }

    if (funbox === "read_ahead" || funbox === "read_ahead_easy") {
      setHighlightMode("letter", true);
      restartTest();
    }
  } else if (mode === "script") {
    if (funbox === "tts") {
      $("#funBoxTheme").attr("href", `funbox/simon_says.css`);
      config.keymapMode = "off";
      settingsGroups.keymapMode.updateButton();
      restartTest();
    } else if (funbox === "layoutfluid") {
      config.keymapMode = "on";
      setKeymapMode("next");
      settingsGroups.keymapMode.updateButton();
      config.savedLayout = config.layout;
      setLayout("qwerty");
      settingsGroups.layout.updateButton();
      setKeymapLayout("qwerty");
      settingsGroups.keymapLayout.updateButton();
      restartTest();
    } else if (funbox === "memory") {
      setMode("words");
      setShowAllLines(true, true);
      restartTest(false, true);
      if (config.keymapMode === "next") {
        setKeymapMode("react");
      }
    } else if (funbox === "nospace") {
      $("#words").addClass("nospace");
      setHighlightMode("letter", true);
      restartTest(false, true);
    }
    activeFunBox = funbox;
  }

  if (funbox !== "layoutfluid" || mode !== "script") {
    if (config.layout !== config.savedLayout) {
      setLayout(config.savedLayout);
      settingsGroups.layout.updateButton();
    }
  }
  updateTestModesNotice();
  return true;
}

function toggleScriptFunbox(...params) {
  if (activeFunBox === "tts") {
    var msg = new SpeechSynthesisUtterance();
    msg.text = params[0];
    msg.lang = "en-US";
    window.speechSynthesis.cancel();
    window.speechSynthesis.speak(msg);
  }
}

function getuid() {
  console.error("Only share this uid with Miodec and nobody else!");
  console.log(firebase.auth().currentUser.uid);
  console.error("Only share this uid with Miodec and nobody else!");
}

function setFocus(foc) {
  if (foc && !focusState) {
    focusState = true;
    stopCaretAnimation();
    $("#top").addClass("focus");
    $("#bottom").addClass("focus");
    $("body").css("cursor", "none");
    $("#middle").addClass("focus");
  } else if (!foc && focusState) {
    focusState = false;
    if (testActive) {
      stopCaretAnimation();
    } else {
      startCaretAnimation();
    }
    $("#top").removeClass("focus");
    $("#bottom").removeClass("focus");
    $("body").css("cursor", "default");
    $("#middle").removeClass("focus");
  }
}

async function initWords() {
  testActive = false;
  wordsList = [];
  currentWordIndex = 0;
  currentWordElementIndex = 0;
  accuracyStats = {
    correct: 0,
    incorrect: 0,
  };
  inputHistory = [];
  correctedHistory = [];
  currentCorrected = "";
  currentInput = "";

  let language = await Misc.getLanguage(config.language);
  if (language && language.name !== config.language) {
    config.language = "english";
  }

  if (
    config.mode === "quote" &&
    (quotes === null ||
      quotes.language !== config.language.replace(/_\d*k$/g, ""))
  ) {
    // if (config.language.split("_")[0] !== "code") {
    setLanguage(config.language.replace(/_\d*k$/g, ""), true);
    // }
    showBackgroundLoader();
    $.ajax({
      url: `quotes/${config.language}.json`,
      async: false,
      success: function (data) {
        hideBackgroundLoader();
        try {
          if (data.quotes.length === 0) {
            throw new Error("No quotes");
          }
          quotes = data;
          quotes.groups.forEach((qg, i) => {
            let lower = qg[0];
            let upper = qg[1];
            quotes.groups[i] = quotes.quotes.filter((q) => {
              if (q.length >= lower && q.length <= upper) {
                q.group = i;
                return true;
              } else {
                return false;
              }
            });
          });
          quotes.quotes = [];
        } catch (e) {
          console.error(e);
          Notifications.add(
            `No ${config.language.replace(/_\d*k$/g, "")} quotes found`,
            0
          );
          return;
        }
      },
      error: (e) => {
        Notifications.add(
          `Error while loading ${config.language.replace(
            /_\d*k$/g,
            ""
          )} quotes: ${e}`,
          -1
        );
        return;
      },
    });
  }

  if (!language) {
    config.language = "english";
    language = words[config.language];
  }

  if (
    config.mode == "time" ||
    config.mode == "words" ||
    config.mode == "custom"
  ) {
    let wordsBound = 100;
    if (config.showAllLines) {
      if (config.mode === "custom") {
        if (customText.isWordRandom) {
          wordsBound = customText.word;
        } else if (customText.isTimeRandom) {
          wordsBound = 100;
        } else {
          wordsBound = customText.text.length;
        }
      } else if (config.mode != "time") {
        wordsBound = config.words;
      }
    } else {
      if (config.mode === "words" && config.words < wordsBound) {
        wordsBound = config.words;
      }
      if (
        config.mode == "custom" &&
        customText.isWordRandom &&
        customText.word < wordsBound
      ) {
        wordsBound = customText.word;
      }
      if (
        config.mode == "custom" &&
        customText.isTimeRandom &&
        customText.time < wordsBound
      ) {
        wordsBound = 100;
      }
      if (
        config.mode == "custom" &&
        !customText.isWordRandom &&
        customText.text.length < wordsBound
      ) {
        wordsBound = customText.text.length;
      }
    }

    if (
      (config.mode === "custom" &&
        customText.isWordRandom &&
        customText.word == 0) ||
      (config.mode === "custom" &&
        customText.isTimeRandom &&
        customText.time == 0)
    ) {
      wordsBound = 100;
    }

    if (config.mode === "words" && config.words === 0) {
      wordsBound = 100;
    }
    if (activeFunBox === "plus_one") {
      wordsBound = 2;
    }
    let wordset = language.words;
    if (config.mode == "custom") {
      wordset = customText.text;
    }
    for (let i = 0; i < wordsBound; i++) {
      let randomWord = wordset[Math.floor(Math.random() * wordset.length)];
      const previousWord = wordsList[i - 1];
      const previousWord2 = wordsList[i - 2];
      if (
        config.mode == "custom" &&
        (customText.isWordRandom || customText.isTimeRandom)
      ) {
        randomWord = wordset[Math.floor(Math.random() * wordset.length)];
      } else if (config.mode == "custom" && !customText.isWordRandom) {
        randomWord = customText.text[i];
      } else {
        while (
          randomWord == previousWord ||
          randomWord == previousWord2 ||
          (!config.punctuation && randomWord == "I") ||
          randomWord.indexOf(" ") > -1
        ) {
          randomWord = wordset[Math.floor(Math.random() * wordset.length)];
        }
      }

      if (activeFunBox === "rAnDoMcAsE") {
        let randomcaseword = "";
        for (let i = 0; i < randomWord.length; i++) {
          if (i % 2 != 0) {
            randomcaseword += randomWord[i].toUpperCase();
          } else {
            randomcaseword += randomWord[i];
          }
        }
        randomWord = randomcaseword;
      } else if (activeFunBox === "gibberish") {
        randomWord = Misc.getGibberish();
      } else if (activeFunBox === "58008") {
        setToggleSettings(false, true);
        randomWord = Misc.getNumbers(7);
      } else if (activeFunBox === "specials") {
        setToggleSettings(false, true);
        randomWord = Misc.getSpecials();
      } else if (activeFunBox === "ascii") {
        setToggleSettings(false, true);
        randomWord = Misc.getASCII();
      }

      if (config.punctuation) {
        randomWord = punctuateWord(previousWord, randomWord, i, wordsBound);
      }
      if (config.numbers) {
        if (Math.random() < 0.1) {
          randomWord = Misc.getNumbers(4);
        }
      }

      if (/\t/g.test(randomWord)) {
        textHasTab = true;
      }

      wordsList.push(randomWord);
    }
  } else if (config.mode == "quote") {
    let group = config.quoteLength;

    if (config.quoteLength === -1) {
      group = Math.floor(Math.random() * quotes.groups.length);
      while (quotes.groups[group].length === 0) {
        group = Math.floor(Math.random() * quotes.groups.length);
      }
    } else {
      if (quotes.groups[group].length === 0) {
        Notifications.add("No quotes found for selected quote length", 0);
        return;
      }
    }

    let rq =
      quotes.groups[group][
        Math.floor(Math.random() * quotes.groups[group].length)
      ];
    if (randomQuote != null && rq.id === randomQuote.id) {
      rq =
        quotes.groups[group][
          Math.floor(Math.random() * quotes.groups[group].length)
        ];
    }
    randomQuote = rq;
    randomQuote.text = randomQuote.text.replace(/ +/gm, " ");
    randomQuote.text = randomQuote.text.replace(/\\\\t/gm, "\t");
    randomQuote.text = randomQuote.text.replace(/\\\\n/gm, "\n");
    randomQuote.text = randomQuote.text.replace(/\\t/gm, "\t");
    randomQuote.text = randomQuote.text.replace(/\\n/gm, "\n");
    randomQuote.text = randomQuote.text.replace(/( *(\r\n|\r|\n) *)/g, "\n ");
    let w = randomQuote.text.trim().split(" ");
    for (let i = 0; i < w.length; i++) {
      if (/\t/g.test(w[i])) {
        textHasTab = true;
      }
      wordsList.push(w[i]);
    }
  }
  //handle right-to-left languages
  if (language.leftToRight) {
    arrangeCharactersLeftToRight();
  } else {
    arrangeCharactersRightToLeft();
  }
  if (language.ligatures) {
    $("#words").addClass("withLigatures");
  } else {
    $("#words").removeClass("withLigatures");
  }
  showWords();
}

function arrangeCharactersRightToLeft() {
  $("#words").addClass("rightToLeftTest");
}

function arrangeCharactersLeftToRight() {
  $("#words").removeClass("rightToLeftTest");
}

function setToggleSettings(state, nosave) {
  setPunctuation(state, nosave);
  setNumbers(state, nosave);
}

function emulateLayout(event) {
  function emulatedLayoutShouldShiftKey(event, newKeyPreview) {
    if (config.capsLockBackspace) return event.shiftKey;
    const isCapsLockHeld = event.originalEvent.getModifierState("CapsLock");
    if (isCapsLockHeld)
      return Misc.isASCIILetter(newKeyPreview) !== event.shiftKey;
    return event.shiftKey;
  }

  function replaceEventKey(event, keyCode) {
    const newKey = String.fromCharCode(keyCode);
    event.keyCode = keyCode;
    event.charCode = keyCode;
    event.which = keyCode;
    event.key = newKey;
    event.code = "Key" + newKey.toUpperCase();
  }

  let newEvent = event;

  try {
    if (config.layout === "default") {
      //override the caps lock modifier for the default layout if needed
      if (config.capsLockBackspace && Misc.isASCIILetter(newEvent.key)) {
        replaceEventKey(
          newEvent,
          newEvent.shiftKey
            ? newEvent.key.toUpperCase().charCodeAt(0)
            : newEvent.key.toLowerCase().charCodeAt(0)
        );
      }
      return newEvent;
    }
    const keyEventCodes = [
      "Backquote",
      "Digit1",
      "Digit2",
      "Digit3",
      "Digit4",
      "Digit5",
      "Digit6",
      "Digit7",
      "Digit8",
      "Digit9",
      "Digit0",
      "Minus",
      "Equal",
      "KeyQ",
      "KeyW",
      "KeyE",
      "KeyR",
      "KeyT",
      "KeyY",
      "KeyU",
      "KeyI",
      "KeyO",
      "KeyP",
      "BracketLeft",
      "BracketRight",
      "Backslash",
      "KeyA",
      "KeyS",
      "KeyD",
      "KeyF",
      "KeyG",
      "KeyH",
      "KeyJ",
      "KeyK",
      "KeyL",
      "Semicolon",
      "Quote",
      "IntlBackslash",
      "KeyZ",
      "KeyX",
      "KeyC",
      "KeyV",
      "KeyB",
      "KeyN",
      "KeyM",
      "Comma",
      "Period",
      "Slash",
      "Space",
    ];
    const layoutMap = layouts[config.layout].keys;

    let mapIndex;
    for (let i = 0; i < keyEventCodes.length; i++) {
      if (newEvent.code == keyEventCodes[i]) {
        mapIndex = i;
      }
    }
    const newKeyPreview = layoutMap[mapIndex][0];
    const shift = emulatedLayoutShouldShiftKey(newEvent, newKeyPreview) ? 1 : 0;
    const newKey = layoutMap[mapIndex][shift];
    replaceEventKey(newEvent, newKey.charCodeAt(0));
  } catch (e) {
    return event;
  }
  return newEvent;
}

function punctuateWord(previousWord, currentWord, index, maxindex) {
  let word = currentWord;

  if (
    (index == 0 ||
      Misc.getLastChar(previousWord) == "." ||
      Misc.getLastChar(previousWord) == "?" ||
      Misc.getLastChar(previousWord) == "!") &&
    config.language.split("_")[0] != "code"
  ) {
    //always capitalise the first word or if there was a dot unless using a code alphabet
    word = Misc.capitalizeFirstLetter(word);
  } else if (
    (Math.random() < 0.1 &&
      Misc.getLastChar(previousWord) != "." &&
      Misc.getLastChar(previousWord) != "," &&
      index != maxindex - 2) ||
    index == maxindex - 1
  ) {
    let rand = Math.random();
    if (rand <= 0.8) {
      word += ".";
    } else if (rand > 0.8 && rand < 0.9) {
      if (config.language.split("_")[0] == "french") {
        word = "?";
      } else {
        word += "?";
      }
    } else {
      if (config.language.split("_")[0] == "french") {
        word = "!";
      } else {
        word += "!";
      }
    }
  } else if (
    Math.random() < 0.01 &&
    Misc.getLastChar(previousWord) != "," &&
    Misc.getLastChar(previousWord) != "." &&
    config.language.split("_")[0] !== "russian"
  ) {
    word = `"${word}"`;
  } else if (
    Math.random() < 0.01 &&
    Misc.getLastChar(previousWord) != "," &&
    Misc.getLastChar(previousWord) != "." &&
    config.language.split("_")[0] !== "russian"
  ) {
    word = `'${word}'`;
  } else if (
    Math.random() < 0.01 &&
    Misc.getLastChar(previousWord) != "," &&
    Misc.getLastChar(previousWord) != "."
  ) {
    if (config.language.split("_")[0] == "code") {
      let r = Math.random();
      if (r < 0.25) {
        word = `(${word})`;
      } else if (r < 0.5) {
        word = `{${word}}`;
      } else if (r < 0.75) {
        word = `[${word}]`;
      } else {
        word = `<${word}>`;
      }
    } else {
      word = `(${word})`;
    }
  } else if (Math.random() < 0.01) {
    if (config.language.split("_")[0] == "french") {
      word = ":";
    } else {
      word += ":";
    }
  } else if (
    Math.random() < 0.01 &&
    Misc.getLastChar(previousWord) != "," &&
    Misc.getLastChar(previousWord) != "." &&
    previousWord != "-"
  ) {
    word = "-";
  } else if (
    Math.random() < 0.01 &&
    Misc.getLastChar(previousWord) != "," &&
    Misc.getLastChar(previousWord) != "." &&
    Misc.getLastChar(previousWord) != ";"
  ) {
    if (config.language.split("_")[0] == "french") {
      word = ";";
    } else {
      word += ";";
    }
  } else if (Math.random() < 0.2 && Misc.getLastChar(previousWord) != ",") {
    word += ",";
  } else if (Math.random() < 0.25 && config.language.split("_")[0] == "code") {
    let specials = ["{", "}", "[", "]", "(", ")", ";", "=", "%", "/"];

    word = specials[Math.floor(Math.random() * 10)];
  }
  return word;
}

function addWord() {
  let bound = 100;
  if (activeFunBox === "plus_one") bound = 1;
  if (
    wordsList.length - inputHistory.length > bound ||
    (config.mode === "words" &&
      wordsList.length >= config.words &&
      config.words > 0) ||
    (config.mode === "custom" &&
      customText.isWordRandom &&
      wordsList.length >= customText.word &&
      customText.word != 0) ||
    (config.mode === "custom" &&
      !customText.isWordRandom &&
      wordsList.length >= customText.text.length)
  )
    return;
  const language =
    config.mode !== "custom"
      ? Misc.getCurrentLanguage()
      : {
          //borrow the direction of the current language
          leftToRight: Misc.getCurrentLanguage().leftToRight,
          words: customText.text,
        };
  const wordset = language.words;
  let randomWord = wordset[Math.floor(Math.random() * wordset.length)];
  const previousWord = wordsList[wordsList.length - 1];
  const previousWordStripped = previousWord
    .replace(/[.?!":\-,]/g, "")
    .toLowerCase();
  const previousWord2Stripped = wordsList[wordsList.length - 2]
    .replace(/[.?!":\-,]/g, "")
    .toLowerCase();

  if (
    config.mode === "custom" &&
    customText.isWordRandom &&
    wordset.length < 3
  ) {
    randomWord = wordset[Math.floor(Math.random() * wordset.length)];
  } else if (config.mode == "custom" && !customText.isWordRandom) {
    randomWord = customText.text[wordsList.length];
  } else {
    while (
      previousWordStripped == randomWord ||
      previousWord2Stripped == randomWord ||
      randomWord.indexOf(" ") > -1 ||
      (!config.punctuation && randomWord == "I")
    ) {
      randomWord = wordset[Math.floor(Math.random() * wordset.length)];
    }
  }

  if (activeFunBox === "rAnDoMcAsE") {
    let randomcaseword = "";
    for (let i = 0; i < randomWord.length; i++) {
      if (i % 2 != 0) {
        randomcaseword += randomWord[i].toUpperCase();
      } else {
        randomcaseword += randomWord[i];
      }
    }
    randomWord = randomcaseword;
  } else if (activeFunBox === "gibberish") {
    randomWord = Misc.getGibberish();
  } else if (activeFunBox === "58008") {
    randomWord = Misc.getNumbers(7);
  } else if (activeFunBox === "specials") {
    randomWord = Misc.getSpecials();
  } else if (activeFunBox === "ascii") {
    randomWord = Misc.getASCII();
  }

  if (config.punctuation && config.mode != "custom") {
    randomWord = punctuateWord(previousWord, randomWord, wordsList.length, 0);
  }
  if (config.numbers && config.mode != "custom") {
    if (Math.random() < 0.1) {
      randomWord = Misc.getNumbers(4);
    }
  }

  wordsList.push(randomWord);

  let w = "<div class='word'>";
  for (let c = 0; c < randomWord.length; c++) {
    w += "<letter>" + randomWord.charAt(c) + "</letter>";
  }
  w += "</div>";
  $("#words").append(w);
}

function showWords() {
  $("#words").empty();

  let wordsHTML = "";
  let newlineafter = false;
  for (let i = 0; i < wordsList.length; i++) {
    newlineafter = false;
    wordsHTML += `<div class='word'>`;
    for (let c = 0; c < wordsList[i].length; c++) {
      if (wordsList[i].charAt(c) === "\t") {
        wordsHTML += `<letter class='tabChar'><i class="fas fa-long-arrow-alt-right"></i></letter>`;
      } else if (wordsList[i].charAt(c) === "\n") {
        newlineafter = true;
        wordsHTML += `<letter class='nlChar'><i class="fas fa-angle-down"></i></letter>`;
      } else {
        wordsHTML += "<letter>" + wordsList[i].charAt(c) + "</letter>";
      }
    }
    wordsHTML += "</div>";
    if (newlineafter) wordsHTML += "<div class='newline'></div>";
  }
  $("#words").html(wordsHTML);

  $("#wordsWrapper").removeClass("hidden");
  const wordHeight = $(document.querySelector(".word")).outerHeight(true);
  const wordsHeight = $(document.querySelector("#words")).outerHeight(true);
  if (
    config.showAllLines &&
    config.mode != "time" &&
    !(customText.isWordRandom && customText.word == 0) &&
    !customText.isTimeRandom
  ) {
    $("#words").css("height", "auto");
    $("#wordsWrapper").css("height", "auto");
    let nh = wordHeight * 3;

    if (nh > wordsHeight) {
      nh = wordsHeight;
    }
    $(".outOfFocusWarning").css("line-height", nh + "px");
  } else {
    $("#words")
      .css("height", wordHeight * 4 + "px")
      .css("overflow", "hidden");
    $("#wordsWrapper")
      .css("height", wordHeight * 3 + "px")
      .css("overflow", "hidden");
    $(".outOfFocusWarning").css("line-height", wordHeight * 3 + "px");
  }

  if (config.keymapMode === "next") {
    updateHighlightedKeymapKey();
  }

  updateActiveElement();
  updateCaretPosition();
}

(function (history) {
  var pushState = history.pushState;
  history.pushState = function (state) {
    if (activeFunBox === "memory" && state !== "/") {
      memoryFunboxInterval = clearInterval(memoryFunboxInterval);
      memoryFunboxTimer = null;
    }
    return pushState.apply(history, arguments);
  };
})(window.history);

function updateActiveElement() {
  let active = document.querySelector("#words .active");
  if (active !== null) {
    if (config.highlightMode == "word") {
      active.querySelectorAll("letter").forEach((e) => {
        e.classList.remove("correct");
      });
    }
    active.classList.remove("active");
  }
  try {
    let activeWord = document.querySelectorAll("#words .word")[
      currentWordElementIndex
    ];
    activeWord.classList.add("active");
    activeWord.classList.remove("error");
    activeWordTop = document.querySelector("#words .active").offsetTop;
    if (config.highlightMode == "word") {
      activeWord.querySelectorAll("letter").forEach((e) => {
        e.classList.add("correct");
      });
    }
  } catch (e) {}
  toggleScriptFunbox(wordsList[currentWordIndex]);
}

function updateWordElement(showError) {
  let input = currentInput;
  let wordAtIndex;
  let currentWord;
  wordAtIndex = document.querySelector("#words .word.active");
  currentWord = wordsList[currentWordIndex];
  let ret = "";

  if (config.highlightMode == "word") {
    //only for word highlight

    let correctSoFar = false;
    if (currentWord.slice(0, input.length) == input) {
      // this is when input so far is correct
      correctSoFar = true;
    }
    let classString = correctSoFar ? "correct" : "incorrect";
    if (config.blindMode) {
      classString = "correct";
    }

    //show letters in the current word
    for (let i = 0; i < currentWord.length; i++) {
      ret += `<letter class="${classString}">` + currentWord[i] + `</letter>`;
    }

    //show any extra letters if hide extra letters is disabled
    if (currentInput.length > currentWord.length && !config.hideExtraLetters) {
      for (let i = currentWord.length; i < currentInput.length; i++) {
        let letter = currentInput[i];
        if (letter == " ") {
          letter = "_";
        }
        ret += `<letter class="${classString}">${letter}</letter>`;
      }
    }
  } else {
    for (let i = 0; i < input.length; i++) {
      let charCorrect;
      if (currentWord[i] == input[i]) {
        charCorrect = true;
      } else {
        charCorrect = false;
      }

      try {
        if (config.language === "russian" && charCorrect === false) {
          if (
            (currentWord[i].toLowerCase() === "е" &&
              input[i].toLowerCase() === "ё") ||
            (currentWord[i].toLowerCase() === "ё" &&
              input[i].toLowerCase() === "е")
          ) {
            charCorrect = true;
          }
        }
      } catch (e) {}

      let currentLetter = currentWord[i];
      let tabChar = "";
      let nlChar = "";
      if (currentLetter === "\t") {
        tabChar = "tabChar";
        currentLetter = `<i class="fas fa-long-arrow-alt-right"></i>`;
      } else if (currentLetter === "\n") {
        nlChar = "nlChar";
        currentLetter = `<i class="fas fa-angle-down"></i>`;
      }

      if (charCorrect) {
        ret += `<letter class="correct ${tabChar}${nlChar}">${currentLetter}</letter>`;
      } else {
        // if (config.difficulty == "master") {
        //   if (!resultVisible) {
        //     failTest();
        //   }
        // }
        if (!showError) {
          if (currentLetter == undefined) {
          } else {
            ret += `<letter class="correct ${tabChar}${nlChar}">${currentLetter}</letter>`;
          }
        } else {
          if (currentLetter == undefined) {
            if (!config.hideExtraLetters) {
              let letter = input[i];
              if (letter == " " || letter == "\t" || letter == "\n") {
                letter = "_";
              }
              ret += `<letter class="incorrect extra ${tabChar}${nlChar}">${letter}</letter>`;
            }
          } else {
            ret +=
              `<letter class="incorrect ${tabChar}${nlChar}">` +
              currentLetter +
              (config.indicateTypos ? `<hint>${input[i]}</hint>` : "") +
              "</letter>";
          }
        }
      }
    }

    if (input.length < currentWord.length) {
      for (let i = input.length; i < currentWord.length; i++) {
        if (currentWord[i] === "\t") {
          ret += `<letter class='tabChar'><i class="fas fa-long-arrow-alt-right"></i></letter>`;
        } else if (currentWord[i] === "\n") {
          ret += `<letter class='nlChar'><i class="fas fa-angle-down"></i></letter>`;
        } else {
          ret += "<letter>" + currentWord[i] + "</letter>";
        }
      }
    }
  }
  wordAtIndex.innerHTML = ret;
}

function highlightBadWord(index, showError) {
  if (!showError) return;
  $($("#words .word")[index]).addClass("error");
}

function showTimer() {
  let op = config.showTimerProgress ? config.timerOpacity : 0;
  if (config.timerStyle === "bar") {
    $("#timerWrapper").stop(true, true).removeClass("hidden").animate(
      {
        opacity: op,
      },
      125
    );
  } else if (config.timerStyle === "text") {
    $("#timerNumber")
      .stop(true, true)
      .removeClass("hidden")
      .css("opacity", 0)
      .animate(
        {
          opacity: op,
        },
        125
      );
  } else if (config.timerStyle === "mini") {
    if (op > 0) {
      $("#miniTimerAndLiveWpm .time")
        .stop(true, true)
        .removeClass("hidden")
        .animate(
          {
            opacity: op,
          },
          125
        );
    }
  }
}

function hideTimer() {
  $("#timerWrapper").stop(true, true).animate(
    {
      opacity: 0,
    },
    125
  );
  $("#miniTimerAndLiveWpm .time")
    .stop(true, true)
    .animate(
      {
        opacity: 0,
      },
      125,
      () => {
        $("#miniTimerAndLiveWpm .time").addClass("hidden");
      }
    );
  $("#timerNumber").stop(true, true).animate(
    {
      opacity: 0,
    },
    125
  );
}

function restartTimer() {
  if (config.timerStyle === "bar") {
    if (config.mode === "time") {
      $("#timer").stop(true, true).animate(
        {
          width: "100vw",
        },
        0
      );
    } else if (config.mode === "words" || config.mode === "custom") {
      $("#timer").stop(true, true).animate(
        {
          width: "0vw",
        },
        0
      );
    }
  }
}

function updateTimer() {
  if (!config.showTimerProgress) return;
  if (
    config.mode === "time" ||
    (config.mode === "custom" && customText.isTimeRandom)
  ) {
    let maxtime = config.time;
    if (config.mode === "custom" && customText.isTimeRandom) {
      maxtime = customText.time;
    }
    if (config.timerStyle === "bar") {
      let percent = 100 - ((time + 1) / maxtime) * 100;
      $("#timer")
        .stop(true, true)
        .animate(
          {
            width: percent + "vw",
          },
          1000,
          "linear"
        );
    } else if (config.timerStyle === "text") {
      let displayTime = Misc.secondsToString(maxtime - time);
      if (maxtime === 0) {
        displayTime = Misc.secondsToString(time);
      }
      $("#timerNumber").html("<div>" + displayTime + "</div>");
    } else if (config.timerStyle === "mini") {
      let displayTime = Misc.secondsToString(maxtime - time);
      if (maxtime === 0) {
        displayTime = Misc.secondsToString(time);
      }
      $("#miniTimerAndLiveWpm .time").html(displayTime);
    }
  } else if (
    config.mode === "words" ||
    config.mode === "custom" ||
    config.mode === "quote"
  ) {
    if (config.timerStyle === "bar") {
      let outof = wordsList.length;
      if (config.mode === "words") {
        outof = config.words;
      }
      if (config.mode === "custom") {
        if (customText.isWordRandom) {
          outof = customText.word;
        } else {
          outof = customText.text.length;
        }
      }
      let percent = Math.floor(((currentWordIndex + 1) / outof) * 100);
      $("#timer")
        .stop(true, true)
        .animate(
          {
            width: percent + "vw",
          },
          250
        );
    } else if (config.timerStyle === "text") {
      let outof = wordsList.length;
      if (config.mode === "words") {
        outof = config.words;
      }
      if (config.mode === "custom") {
        if (customText.isWordRandom) {
          outof = customText.word;
        } else {
          outof = customText.text.length;
        }
      }
      if (outof === 0) {
        $("#timerNumber").html("<div>" + `${inputHistory.length}` + "</div>");
      } else {
        $("#timerNumber").html(
          "<div>" + `${inputHistory.length}/${outof}` + "</div>"
        );
      }
    } else if (config.timerStyle === "mini") {
      let outof = wordsList.length;
      if (config.mode === "words") {
        outof = config.words;
      }
      if (config.mode === "custom") {
        if (customText.isWordRandom) {
          outof = customText.word;
        } else {
          outof = customText.text.length;
        }
      }
      if (config.words === 0) {
        $("#miniTimerAndLiveWpm .time").html(`${inputHistory.length}`);
      } else {
        $("#miniTimerAndLiveWpm .time").html(`${inputHistory.length}/${outof}`);
      }
    }
  }
}

function hideCaret() {
  $("#caret").addClass("hidden");
}

function showCaret() {
  if ($("#result").hasClass("hidden")) {
    updateCaretPosition();
    $("#caret").removeClass("hidden");
    startCaretAnimation();
  }
}

function stopCaretAnimation() {
  if (caretAnimating === true) {
    $("#caret").css("animation-name", "none");
    $("#caret").css("opacity", "1");
    caretAnimating = false;
  }
}

function startCaretAnimation() {
  if (caretAnimating === false) {
    if (config.smoothCaret) {
      $("#caret").css("animation-name", "caretFlashSmooth");
    } else {
      $("#caret").css("animation-name", "caretFlashHard");
    }
    caretAnimating = true;
  }
}

function hideKeymap() {
  $(".keymap").addClass("hidden");
  // $("#liveWpm").removeClass("lower");
}

function showKeymap() {
  $(".keymap").removeClass("hidden");
  // $("#liveWpm").addClass("lower");
}

function flashPressedKeymapKey(key, correct) {
  if (key == undefined) return;
  switch (key) {
    case "\\":
    case "|":
      key = "#KeyBackslash";
      break;
    case "}":
    case "]":
      key = "#KeyRightBracket";
      break;
    case "{":
    case "[":
      key = "#KeyLeftBracket";
      break;
    case '"':
    case "'":
      key = "#KeyQuote";
      break;
    case ":":
    case ";":
      key = "#KeySemicolon";
      break;
    case "<":
    case ",":
      key = "#KeyComma";
      break;
    case ">":
    case ".":
      key = "#KeyPeriod";
      break;
    case "?":
    case "/":
      key = "#KeySlash";
      break;
    case "" || "Space":
      key = "#KeySpace";
      break;
    default:
      key = `#Key${key.toUpperCase()}`;
  }

  if (key == "#KeySpace") {
    key = ".key-split-space";
  }

  try {
    if (correct || config.blindMode) {
      $(key)
        .stop(true, true)
        .css({
          color: themeColors.bg,
          backgroundColor: themeColors.main,
          borderColor: themeColors.main,
        })
        .animate(
          {
            color: themeColors.sub,
            backgroundColor: "transparent",
            borderColor: themeColors.sub,
          },
          500,
          "easeOutExpo"
        );
    } else {
      $(key)
        .stop(true, true)
        .css({
          color: themeColors.bg,
          backgroundColor: themeColors.error,
          borderColor: themeColors.error,
        })
        .animate(
          {
            color: themeColors.sub,
            backgroundColor: "transparent",
            borderColor: themeColors.sub,
          },
          500,
          "easeOutExpo"
        );
    }
  } catch (e) {}
}

function updateHighlightedKeymapKey() {
  try {
    if ($(".active-key") != undefined) {
      $(".active-key").removeClass("active-key");
    }

    var currentKey = wordsList[currentWordIndex]
      .substring(currentInput.length, currentInput.length + 1)
      .toString()
      .toUpperCase();

    let highlightKey;
    switch (currentKey) {
      case "\\":
      case "|":
        highlightKey = "#KeyBackslash";
        break;
      case "}":
      case "]":
        highlightKey = "#KeyRightBracket";
        break;
      case "{":
      case "[":
        highlightKey = "#KeyLeftBracket";
        break;
      case '"':
      case "'":
        highlightKey = "#KeyQuote";
        break;
      case ":":
      case ";":
        highlightKey = "#KeySemicolon";
        break;
      case "<":
      case ",":
        highlightKey = "#KeyComma";
        break;
      case ">":
      case ".":
        highlightKey = "#KeyPeriod";
        break;
      case "?":
      case "/":
        highlightKey = "#KeySlash";
        break;
      case "":
        highlightKey = "#KeySpace";
        break;
      default:
        highlightKey = `#Key${currentKey}`;
    }

    $(highlightKey).addClass("active-key");
    if (highlightKey === "#KeySpace") {
      $("#KeySpace2").addClass("active-key");
    }
  } catch (e) {
    console.log("could not update highlighted keymap key: " + e.message);
  }
}

function updateCaretPosition() {
  if ($("#wordsWrapper").hasClass("hidden")) return;
  if ($("#caret").hasClass("off")) {
    return;
  }

  let caret = $("#caret");

  let inputLen = currentInput.length;
  let currentLetterIndex = inputLen - 1;
  if (currentLetterIndex == -1) {
    currentLetterIndex = 0;
  }
  try {
    let currentWordNodeList = document
      .querySelector("#words .active")
      .querySelectorAll("letter");
    let currentLetter = currentWordNodeList[currentLetterIndex];
    if (inputLen > currentWordNodeList.length) {
      currentLetter = currentWordNodeList[currentWordNodeList.length - 1];
    }

    if ($(currentLetter).length == 0) return;
    const isLanguageLeftToRight = Misc.getCurrentLanguage().leftToRight;
    let currentLetterPosLeft = isLanguageLeftToRight
      ? currentLetter.offsetLeft
      : currentLetter.offsetLeft + $(currentLetter).width();
    let currentLetterPosTop = currentLetter.offsetTop;
    let letterHeight = $(currentLetter).height();
    let newTop = 0;
    let newLeft = 0;

    newTop = currentLetterPosTop - Math.round(letterHeight / 5);
    if (inputLen == 0) {
      newLeft = isLanguageLeftToRight
        ? currentLetterPosLeft - caret.width() / 2
        : currentLetterPosLeft + caret.width() / 2;
    } else {
      newLeft = isLanguageLeftToRight
        ? currentLetterPosLeft + $(currentLetter).width() - caret.width() / 2
        : currentLetterPosLeft - $(currentLetter).width() + caret.width() / 2;
    }

    let smoothlinescroll = $("#words .smoothScroller").height();
    if (smoothlinescroll === undefined) smoothlinescroll = 0;

    if (config.smoothCaret) {
      caret.stop(true, false).animate(
        {
          top: newTop - smoothlinescroll,
          left: newLeft,
        },
        100
      );
    } else {
      caret.stop(true, true).animate(
        {
          top: newTop - smoothlinescroll,
          left: newLeft,
        },
        0
      );
    }

    if (config.showAllLines) {
      let browserHeight = window.innerHeight;
      let middlePos = browserHeight / 2 - $("#caret").outerHeight() / 2;
      let contentHeight = document.body.scrollHeight;

      if (newTop >= middlePos && contentHeight > browserHeight) {
        window.scrollTo({
          left: 0,
          top: newTop - middlePos,
          behavior: "smooth",
        });
      }
    }
  } catch (e) {
    console.log("could not move caret: " + e.message);
  }
}

function countChars() {
  let correctWordChars = 0;
  let correctChars = 0;
  let incorrectChars = 0;
  let extraChars = 0;
  let missedChars = 0;
  let spaces = 0;
  let correctspaces = 0;
  for (let i = 0; i < inputHistory.length; i++) {
    let word = wordsList[i];
    if (inputHistory[i] === "") {
      //last word that was not started
      continue;
    }
    if (inputHistory[i] == word) {
      //the word is correct
      correctWordChars += word.length;
      correctChars += word.length;
      if (
        i < inputHistory.length - 1 &&
        Misc.getLastChar(inputHistory[i]) !== "\n"
      ) {
        correctspaces++;
      }
    } else if (inputHistory[i].length >= word.length) {
      //too many chars
      for (let c = 0; c < inputHistory[i].length; c++) {
        if (c < word.length) {
          //on char that still has a word list pair
          if (inputHistory[i][c] == word[c]) {
            correctChars++;
          } else {
            incorrectChars++;
          }
        } else {
          //on char that is extra
          extraChars++;
        }
      }
    } else {
      //not enough chars
      let toAdd = {
        correct: 0,
        incorrect: 0,
        missed: 0,
      };
      for (let c = 0; c < word.length; c++) {
        if (c < inputHistory[i].length) {
          //on char that still has a word list pair
          if (inputHistory[i][c] == word[c]) {
            toAdd.correct++;
          } else {
            toAdd.incorrect++;
          }
        } else {
          //on char that is extra
          toAdd.missed++;
        }
      }
      correctChars += toAdd.correct;
      incorrectChars += toAdd.incorrect;
      if (i === inputHistory.length - 1 && config.mode == "time") {
        //last word - check if it was all correct - add to correct word chars
        if (toAdd.incorrect === 0) correctWordChars += toAdd.correct;
      } else {
        missedChars += toAdd.missed;
      }
    }
    if (i < inputHistory.length - 1) {
      spaces++;
    }
  }
  if (activeFunBox === "nospace") {
    spaces = 0;
    correctspaces = 0;
  }
  return {
    spaces: spaces,
    correctWordChars: correctWordChars,
    allCorrectChars: correctChars,
    incorrectChars: incorrectChars,
    extraChars: extraChars,
    missedChars: missedChars,
    correctSpaces: correctspaces,
  };
}

function calculateStats() {
  let testSeconds = (testEnd - testStart) / 1000;
  let chars = countChars();
  let wpm = Misc.roundTo2(
    ((chars.correctWordChars + chars.correctSpaces) * (60 / testSeconds)) / 5
  );
  let wpmraw = Misc.roundTo2(
    ((chars.allCorrectChars +
      chars.spaces +
      chars.incorrectChars +
      chars.extraChars) *
      (60 / testSeconds)) /
      5
  );
  let acc = Misc.roundTo2(
    (accuracyStats.correct /
      (accuracyStats.correct + accuracyStats.incorrect)) *
      100
  );
  return {
    wpm: isNaN(wpm) ? 0 : wpm,
    wpmRaw: isNaN(wpmraw) ? 0 : wpmraw,
    acc: acc,
    correctChars: chars.correctWordChars,
    incorrectChars: chars.incorrectChars,
    missedChars: chars.missedChars,
    extraChars: chars.extraChars,
    allChars:
      chars.allCorrectChars +
      chars.spaces +
      chars.incorrectChars +
      chars.extraChars,
    time: testSeconds,
    spaces: chars.spaces,
    correctSpaces: chars.correctSpaces,
  };
}

function hideCrown() {
  $("#result .stats .wpm .crown").css("opacity", 0).addClass("hidden");
}

function showCrown() {
  $("#result .stats .wpm .crown")
    .removeClass("hidden")
    .css("opacity", "0")
    .animate(
      {
        opacity: 1,
      },
      250,
      "easeOutCubic"
    );
}

function failTest() {
  inputHistory.push(currentInput);
  correctedHistory.push(currentCorrected);
  lastSecondNotRound = true;
  showResult(true);
  let testNow = performance.now();
  let testSeconds = Misc.roundTo2((testNow - testStart) / 1000);
  let afkseconds = keypressPerSecond.filter((x) => x.count == 0 && x.mod == 0)
    .length;
  incompleteTestSeconds += testSeconds - afkseconds;
  restartCount++;
}

let resultCalculating = false;
function showResult(difficultyFailed = false) {
  resultCalculating = true;
  resultVisible = true;
  testEnd = performance.now();
  testActive = false;
  setFocus(false);
  hideCaret();
  hideLiveWpm();
  hideLiveAcc();
  hideTimer();
  hideKeymap();
  testInvalid = false;
  let stats = calculateStats();
  if (stats === undefined) {
    stats = {
      wpm: 0,
      wpmRaw: 0,
      acc: 0,
      correctChars: 0,
      incorrectChars: 0,
      missedChars: 0,
      extraChars: 0,
      time: 0,
      spaces: 0,
      correctSpaces: 0,
    };
  }
  let inf = false;
  if (stats.wpm >= 1000) {
    inf = true;
  }
  clearTimeout(timer);
  let testtime = stats.time;
  let afkseconds = keypressPerSecond.filter((x) => x.count == 0 && x.mod == 0)
    .length;
  let afkSecondsPercent = Misc.roundTo2((afkseconds / testtime) * 100);

  wpmOverTimeChart.options.annotation.annotations = [];

  $("#result #resultWordsHistory").addClass("hidden");

  if (config.alwaysShowDecimalPlaces) {
    if (config.alwaysShowCPM == false) {
      $("#result .stats .wpm .top .text").text("wpm");
      if (inf) {
        $("#result .stats .wpm .bottom").text("Infinite");
      } else {
        $("#result .stats .wpm .bottom").text(Misc.roundTo2(stats.wpm));
      }
      $("#result .stats .raw .bottom").text(Misc.roundTo2(stats.wpmRaw));
      $("#result .stats .wpm .bottom").attr(
        "aria-label",
        Misc.roundTo2(stats.wpm * 5) + " cpm"
      );
    } else {
      $("#result .stats .wpm .top .text").text("cpm");
      if (inf) {
        $("#result .stats .wpm .bottom").text("Infinite");
      } else {
        $("#result .stats .wpm .bottom").text(Misc.roundTo2(stats.wpm * 5));
      }
      $("#result .stats .raw .bottom").text(Misc.roundTo2(stats.wpmRaw * 5));
      $("#result .stats .wpm .bottom").attr(
        "aria-label",
        Misc.roundTo2(stats.wpm) + " wpm"
      );
    }

    $("#result .stats .acc .bottom").text(Misc.roundTo2(stats.acc) + "%");
    let time = Misc.roundTo2(testtime) + "s";
    if (testtime > 61) {
      time = Misc.secondsToString(Misc.roundTo2(testtime));
    }
    $("#result .stats .time .bottom .text").text(time);
    $("#result .stats .raw .bottom").removeAttr("aria-label");
    $("#result .stats .acc .bottom").removeAttr("aria-label");
    $("#result .stats .time .bottom").attr(
      "aria-label",
      `${afkseconds}s afk ${afkSecondsPercent}%`
    );
  } else {
    //not showing decimal places
    if (config.alwaysShowCPM == false) {
      $("#result .stats .wpm .top .text").text("wpm");
      $("#result .stats .wpm .bottom").attr(
        "aria-label",
        stats.wpm + ` (${Misc.roundTo2(stats.wpm * 5)} cpm)`
      );
      if (inf) {
        $("#result .stats .wpm .bottom").text("Infinite");
      } else {
        $("#result .stats .wpm .bottom").text(Math.round(stats.wpm));
      }
      $("#result .stats .raw .bottom").text(Math.round(stats.wpmRaw));
      $("#result .stats .raw .bottom").attr("aria-label", stats.wpmRaw);
    } else {
      $("#result .stats .wpm .top .text").text("cpm");
      $("#result .stats .wpm .bottom").attr(
        "aria-label",
        Misc.roundTo2(stats.wpm * 5) + ` (${Misc.roundTo2(stats.wpm)} wpm)`
      );
      if (inf) {
        $("#result .stats .wpm .bottom").text("Infinite");
      } else {
        $("#result .stats .wpm .bottom").text(Math.round(stats.wpm * 5));
      }
      $("#result .stats .raw .bottom").text(Math.round(stats.wpmRaw * 5));
      $("#result .stats .raw .bottom").attr("aria-label", stats.wpmRaw * 5);
    }

    $("#result .stats .acc .bottom").text(Math.floor(stats.acc) + "%");
    $("#result .stats .acc .bottom").attr("aria-label", stats.acc + "%");
    let time = Math.round(testtime) + "s";
    if (testtime > 61) {
      time = Misc.secondsToString(Math.round(testtime));
    }
    $("#result .stats .time .bottom .text").text(time);
    $("#result .stats .time .bottom").attr(
      "aria-label",
      `${Misc.roundTo2(testtime)}s (${afkseconds}s afk ${afkSecondsPercent}%)`
    );
  }
  $("#result .stats .time .bottom .afk").text("");
  if (afkSecondsPercent > 0) {
    $("#result .stats .time .bottom .afk").text(afkSecondsPercent + "% afk");
  }
  $("#result .stats .key .bottom").text(testtime + "s");
  $("#words").removeClass("blurred");
  $(".outOfFocusWarning").addClass("hidden");
  $("#result .stats .key .bottom").text(
    stats.correctChars +
      stats.correctSpaces +
      "/" +
      stats.incorrectChars +
      "/" +
      stats.extraChars +
      "/" +
      stats.missedChars
  );

  setTimeout(function () {
    $("#resultExtraButtons").removeClass("hidden").css("opacity", 0).animate(
      {
        opacity: 1,
      },
      125
    );
  }, 125);

  $("#testModesNotice").addClass("hidden");

  $("#result .stats .leaderboards .bottom").text("");
  $("#result .stats .leaderboards").addClass("hidden");

  let mode2 = "";
  if (config.mode === "time") {
    mode2 = config.time;
  } else if (config.mode === "words") {
    mode2 = config.words;
  } else if (config.mode === "custom") {
    mode2 = "custom";
  } else if (config.mode === "quote") {
    mode2 = randomQuote.id;
  }

  if (lastSecondNotRound) {
    let wpmAndRaw = liveWpmAndRaw();
    wpmHistory.push(wpmAndRaw.wpm);
    rawHistory.push(wpmAndRaw.raw);
    keypressPerSecond.push(currentKeypress);
    currentKeypress = {
      mod: 0,
      count: 0,
      words: [],
    };
    errorsPerSecond.push(currentError);
    currentError = {
      count: 0,
      words: [],
    };
  }

  let labels = [];
  for (let i = 1; i <= wpmHistory.length; i++) {
    if (lastSecondNotRound && i === wpmHistory.length) {
      labels.push(Misc.roundTo2(testtime).toString());
    } else {
      labels.push(i.toString());
    }
  }

  if (themeColors.main == "") {
    refreshThemeColorObject();
  }

  wpmOverTimeChart.options.scales.xAxes[0].ticks.minor.fontColor =
    themeColors.sub;
  wpmOverTimeChart.options.scales.xAxes[0].scaleLabel.fontColor =
    themeColors.sub;
  wpmOverTimeChart.options.scales.yAxes[0].ticks.minor.fontColor =
    themeColors.sub;
  wpmOverTimeChart.options.scales.yAxes[2].ticks.minor.fontColor =
    themeColors.sub;
  wpmOverTimeChart.options.scales.yAxes[0].scaleLabel.fontColor =
    themeColors.sub;
  wpmOverTimeChart.options.scales.yAxes[2].scaleLabel.fontColor =
    themeColors.sub;

  wpmOverTimeChart.data.labels = labels;

  let rawWpmPerSecondRaw = keypressPerSecond.map((f) =>
    Math.round((f.count / 5) * 60)
  );

  let rawWpmPerSecond = Misc.smooth(rawWpmPerSecondRaw, 1);

  let stddev = Misc.stdDev(rawWpmPerSecondRaw);
  let avg = Misc.mean(rawWpmPerSecondRaw);

  let consistency = Misc.roundTo2(Misc.kogasa(stddev / avg));
  let keyConsistency = Misc.roundTo2(
    Misc.kogasa(
      Misc.stdDev(keypressStats.spacing.array) /
        Misc.mean(keypressStats.spacing.array)
    )
  );

  if (isNaN(consistency)) {
    consistency = 0;
  }

  if (config.alwaysShowDecimalPlaces) {
    $("#result .stats .consistency .bottom").text(
      Misc.roundTo2(consistency) + "%"
    );
    $("#result .stats .consistency .bottom").attr(
      "aria-label",
      `${keyConsistency}% key`
    );
  } else {
    $("#result .stats .consistency .bottom").text(
      Math.round(consistency) + "%"
    );
    $("#result .stats .consistency .bottom").attr(
      "aria-label",
      `${consistency}% (${keyConsistency}% key)`
    );
  }

  wpmOverTimeChart.data.datasets[0].borderColor = themeColors.main;
  wpmOverTimeChart.data.datasets[0].pointBackgroundColor = themeColors.main;
  wpmOverTimeChart.data.datasets[0].data = wpmHistory;
  wpmOverTimeChart.data.datasets[1].borderColor = themeColors.sub;
  wpmOverTimeChart.data.datasets[1].pointBackgroundColor = themeColors.sub;
  wpmOverTimeChart.data.datasets[1].data = rawWpmPerSecond;

  let maxChartVal = Math.max(
    ...[Math.max(...rawWpmPerSecond), Math.max(...wpmHistory)]
  );

  let minChartVal = Math.min(
    ...[Math.min(...rawWpmPerSecond), Math.min(...wpmHistory)]
  );

  if (!config.startGraphsAtZero) {
    wpmOverTimeChart.options.scales.yAxes[0].ticks.min = Math.min(
      ...wpmHistory
    );
    wpmOverTimeChart.options.scales.yAxes[1].ticks.min = Math.min(
      ...wpmHistory
    );
  } else {
    wpmOverTimeChart.options.scales.yAxes[0].ticks.min = 0;
    wpmOverTimeChart.options.scales.yAxes[1].ticks.min = 0;
  }

  // let errorsNoZero = [];

  // for (let i = 0; i < errorsPerSecond.length; i++) {
  //   errorsNoZero.push({
  //     x: i + 1,
  //     y: errorsPerSecond[i].count,
  //   });
  // }

  let errorsArray = [];
  for (let i = 0; i < errorsPerSecond.length; i++) {
    errorsArray.push(errorsPerSecond[i].count);
  }

  wpmOverTimeChart.data.datasets[2].data = errorsArray;

  let kps = keypressPerSecond.slice(Math.max(keypressPerSecond.length - 5, 0));

  kps = kps.map((a) => a.count);

  kps = kps.reduce((a, b) => a + b, 0);

  let afkDetected = kps === 0 ? true : false;

  if (bailout) afkDetected = false;

  if (difficultyFailed) {
    Notifications.add("Test failed", 0);
  } else if (afkDetected) {
    Notifications.add("Test invalid - AFK detected", 0);
  } else if (sameWordset) {
    Notifications.add("Test invalid - repeated", 0);
  } else {
    let activeTags = [];
    let activeTagsIds = [];
    try {
      db_getSnapshot().tags.forEach((tag) => {
        if (tag.active === true) {
          activeTags.push(tag);
          activeTagsIds.push(tag.id);
        }
      });
    } catch (e) {}

    let chartData = {
      wpm: wpmHistory,
      raw: rawWpmPerSecond,
      err: errorsArray,
    };

    if (testtime > 122) {
      chartData = "toolong";
      keypressStats.spacing.array = "toolong";
      keypressStats.duration.array = "toolong";
    }

    let lang = config.language;

    let quoteLength = -1;
    if (config.mode === "quote") {
      quoteLength = randomQuote.group;
    }

    let completedEvent = {
      wpm: stats.wpm,
      rawWpm: stats.wpmRaw,
      correctChars: stats.correctChars + stats.correctSpaces,
      incorrectChars: stats.incorrectChars,
      allChars: stats.allChars,
      acc: stats.acc,
      mode: config.mode,
      mode2: mode2,
      quoteLength: quoteLength,
      punctuation: config.punctuation,
      numbers: config.numbers,
      timestamp: Date.now(),
      language: lang,
      restartCount: restartCount,
      incompleteTestSeconds: incompleteTestSeconds,
      difficulty: config.difficulty,
      testDuration: testtime,
      afkDuration: afkseconds,
      blindMode: config.blindMode,
      theme: config.theme,
      tags: activeTagsIds,
      keySpacing: keypressStats.spacing.array,
      keyDuration: keypressStats.duration.array,
      consistency: consistency,
      keyConsistency: keyConsistency,
      funbox: activeFunBox,
      bailedOut: bailout,
      chartData: chartData,
    };
    if (
      config.difficulty == "normal" ||
      ((config.difficulty == "master" || config.difficulty == "expert") &&
        !difficultyFailed)
    ) {
      restartCount = 0;
      incompleteTestSeconds = 0;
    }
    if (
      stats.wpm > 0 &&
      stats.wpm < 350 &&
      stats.acc > 50 &&
      stats.acc <= 100
    ) {
      if (firebase.auth().currentUser != null) {
        completedEvent.uid = firebase.auth().currentUser.uid;

        //check local pb
        accountIconLoading(true);
        let localPb = false;
        let dontShowCrown = false;
        let pbDiff = 0;
        db_getLocalPB(
          config.mode,
          mode2,
          config.punctuation,
          config.language,
          config.difficulty
        ).then((lpb) => {
          db_getUserHighestWpm(
            config.mode,
            mode2,
            config.punctuation,
            config.language,
            config.difficulty
          ).then((highestwpm) => {
            hideCrown();
            $("#result .stats .wpm .crown").attr("aria-label", "");
            if (lpb < stats.wpm && stats.wpm < highestwpm) {
              dontShowCrown = true;
            }
            if (lpb < stats.wpm) {
              //new pb based on local
              pbDiff = Math.abs(stats.wpm - lpb);
              if (!dontShowCrown) {
                hideCrown();
                showCrown();
                $("#result .stats .wpm .crown").attr(
                  "aria-label",
                  "+" + Misc.roundTo2(pbDiff)
                );
              }
              localPb = true;
            }
            if (lpb > 0) {
              wpmOverTimeChart.options.annotation.annotations.push({
                enabled: false,
                type: "line",
                mode: "horizontal",
                scaleID: "wpm",
                value: lpb,
                borderColor: themeColors.sub,
                borderWidth: 1,
                borderDash: [2, 2],
                label: {
                  backgroundColor: themeColors.sub,
                  fontFamily: "Roboto Mono",
                  fontSize: 11,
                  fontStyle: "normal",
                  fontColor: themeColors.bg,
                  xPadding: 6,
                  yPadding: 6,
                  cornerRadius: 3,
                  position: "center",
                  enabled: true,
                  content: `PB: ${lpb}`,
                },
              });
              if (maxChartVal >= lpb - 15 && maxChartVal <= lpb + 15) {
                maxChartVal = lpb + 15;
              }
              wpmOverTimeChart.options.scales.yAxes[0].ticks.max = Math.round(
                maxChartVal
              );
              wpmOverTimeChart.options.scales.yAxes[1].ticks.max = Math.round(
                maxChartVal
              );
              wpmOverTimeChart.update({ duration: 0 });
            }
            $("#result .stats .leaderboards").removeClass("hidden");
            $("#result .stats .leaderboards .bottom").html("checking...");

            if (activeTags.length == 0) {
              $("#result .stats .tags").addClass("hidden");
            } else {
              $("#result .stats .tags").removeClass("hidden");
            }
            $("#result .stats .tags .bottom").text("");
            let annotationSide = "left";
            activeTags.forEach(async (tag) => {
              let tpb = await db_getLocalTagPB(
                tag.id,
                config.mode,
                mode2,
                config.punctuation,
                config.language,
                config.difficulty
              );
              $("#result .stats .tags .bottom").append(`
                <div tagid="${tag.id}" aria-label="PB: ${tpb}" data-balloon-pos="up">${tag.name}<i class="fas fa-crown hidden"></i></div>
              `);
              if (tpb < stats.wpm) {
                //new pb for that tag
                db_saveLocalTagPB(
                  tag.id,
                  config.mode,
                  mode2,
                  config.punctuation,
                  config.language,
                  config.difficulty,
                  stats.wpm,
                  stats.acc,
                  stats.wpmRaw,
                  consistency
                );
                $(
                  `#result .stats .tags .bottom div[tagid="${tag.id}"] .fas`
                ).removeClass("hidden");
                $(`#result .stats .tags .bottom div[tagid="${tag.id}"]`).attr(
                  "aria-label",
                  "+" + Misc.roundTo2(stats.wpm - tpb)
                );
                console.log("new pb for tag " + tag.name);
              } else {
                wpmOverTimeChart.options.annotation.annotations.push({
                  enabled: false,
                  type: "line",
                  mode: "horizontal",
                  scaleID: "wpm",
                  value: tpb,
                  borderColor: themeColors.sub,
                  borderWidth: 1,
                  borderDash: [2, 2],
                  label: {
                    backgroundColor: themeColors.sub,
                    fontFamily: "Roboto Mono",
                    fontSize: 11,
                    fontStyle: "normal",
                    fontColor: themeColors.bg,
                    xPadding: 6,
                    yPadding: 6,
                    cornerRadius: 3,
                    position: annotationSide,
                    enabled: true,
                    content: `${tag.name} PB: ${tpb}`,
                  },
                });
                if (annotationSide === "left") {
                  annotationSide = "right";
                } else {
                  annotationSide = "left";
                }
              }
            });

            CloudFunctions.testCompleted({
              uid: firebase.auth().currentUser.uid,
              obj: completedEvent,
            })
              .then((e) => {
                accountIconLoading(false);
                if (e.data == null) {
                  Notifications.add(
                    "Unexpected response from the server: " + e.data,
                    -1
                  );
                  return;
                }
                if (e.data.resultCode === -1) {
                  Notifications.add("Could not save result", -1);
                } else if (e.data.resultCode === -2) {
                  Notifications.add(
                    "Possible bot detected. Result not saved.",
                    -1
                  );
                } else if (e.data.resultCode === -3) {
                  Notifications.add(
                    "Could not verify keypress stats. Result not saved.",
                    -1
                  );
                } else if (e.data.resultCode === -4) {
                  Notifications.add(
                    "Result data does not make sense. Result not saved.",
                    -1
                  );
                } else if (e.data.resultCode === -999) {
                  console.error("internal error: " + e.data.message);
                  Notifications.add(
                    "Internal error. Result might not be saved. " +
                      e.data.message,
                    -1
                  );
                } else if (e.data.resultCode === 1 || e.data.resultCode === 2) {
                  completedEvent.id = e.data.createdId;
                  if (e.data.resultCode === 2) {
                    completedEvent.isPb = true;
                  }
                  if (
                    db_getSnapshot() !== null &&
                    db_getSnapshot().results !== undefined
                  ) {
                    db_getSnapshot().results.unshift(completedEvent);
                    if (db_getSnapshot().globalStats.time == undefined) {
                      db_getSnapshot().globalStats.time =
                        testtime +
                        completedEvent.incompleteTestSeconds -
                        afkseconds;
                    } else {
                      db_getSnapshot().globalStats.time +=
                        testtime +
                        completedEvent.incompleteTestSeconds -
                        afkseconds;
                    }
                    if (db_getSnapshot().globalStats.started == undefined) {
                      db_getSnapshot().globalStats.started = restartCount + 1;
                    } else {
                      db_getSnapshot().globalStats.started += restartCount + 1;
                    }
                    if (db_getSnapshot().globalStats.completed == undefined) {
                      db_getSnapshot().globalStats.completed = 1;
                    } else {
                      db_getSnapshot().globalStats.completed += 1;
                    }
                  }
                  try {
                    firebase
                      .analytics()
                      .logEvent("testCompleted", completedEvent);
                  } catch (e) {
                    console.log("Analytics unavailable");
                  }

                  if (
                    config.mode === "time" &&
                    (mode2 == "15" || mode2 == "60") &&
                    db_getSnapshot() !== null
                  ) {
                    const lbUpIcon = `<i class="fas fa-angle-up"></i>`;
                    const lbDownIcon = `<i class="fas fa-angle-down"></i>`;
                    const lbRightIcon = `<i class="fas fa-angle-right"></i>`;

                    //global
                    let globalLbString = "";
                    const glb = e.data.globalLeaderboard;
                    const glbMemory = db_getSnapshot().lbMemory[
                      config.mode + mode2
                    ].global;
                    let dontShowGlobalDiff =
                      glbMemory == null || glbMemory === -1 ? true : false;
                    let globalLbDiff = null;
                    if (glb === null) {
                      globalLbString = "global: not found";
                    } else if (glb.insertedAt === -1) {
                      dontShowGlobalDiff = true;
                      globalLbDiff = glbMemory - glb.insertedAt;
                      updateLbMemory(
                        config.mode,
                        mode2,
                        "global",
                        glb.insertedAt
                      );

                      globalLbString = "global: not qualified";
                    } else if (glb.insertedAt >= 0) {
                      if (glb.newBest) {
                        globalLbDiff = glbMemory - glb.insertedAt;
                        updateLbMemory(
                          config.mode,
                          mode2,
                          "global",
                          glb.insertedAt
                        );
                        let str = Misc.getPositionString(glb.insertedAt + 1);
                        globalLbString = `global: ${str}`;
                      } else {
                        globalLbDiff = glbMemory - glb.foundAt;
                        updateLbMemory(
                          config.mode,
                          mode2,
                          "global",
                          glb.foundAt
                        );
                        let str = Misc.getPositionString(glb.foundAt + 1);
                        globalLbString = `global: ${str}`;
                      }
                    }
                    if (!dontShowGlobalDiff) {
                      let sString =
                        globalLbDiff === 1 || globalLbDiff === -1 ? "" : "s";
                      if (globalLbDiff > 0) {
                        globalLbString += ` <span class="lbChange" aria-label="You've gained ${globalLbDiff} position${sString}" data-balloon-pos="up">(${lbUpIcon}${globalLbDiff})</span>`;
                      } else if (globalLbDiff === 0) {
                        globalLbString += ` <span class="lbChange" aria-label="Your position remained the same" data-balloon-pos="up">(${lbRightIcon}${globalLbDiff})</span>`;
                      } else if (globalLbDiff < 0) {
                        globalLbString += ` <span class="lbChange" aria-label="You've lost ${globalLbDiff} position${sString}" data-balloon-pos="up">(${lbDownIcon}${globalLbDiff})</span>`;
                      }
                    }

                    //daily
                    let dailyLbString = "";
                    const dlb = e.data.dailyLeaderboard;
                    const dlbMemory = db_getSnapshot().lbMemory[
                      config.mode + mode2
                    ].daily;
                    let dontShowDailyDiff =
                      dlbMemory == null || dlbMemory === -1 ? true : false;
                    let dailyLbDiff = null;
                    if (dlb === null) {
                      dailyLbString = "daily: not found";
                    } else if (dlb.insertedAt === -1) {
                      dontShowDailyDiff = true;
                      dailyLbDiff = dlbMemory - dlb.insertedAt;
                      updateLbMemory(
                        config.mode,
                        mode2,
                        "daily",
                        dlb.insertedAt
                      );
                      dailyLbString = "daily: not qualified";
                    } else if (dlb.insertedAt >= 0) {
                      if (dlb.newBest) {
                        dailyLbDiff = dlbMemory - dlb.insertedAt;
                        updateLbMemory(
                          config.mode,
                          mode2,
                          "daily",
                          dlb.insertedAt
                        );
                        let str = Misc.getPositionString(dlb.insertedAt + 1);
                        dailyLbString = `daily: ${str}`;
                      } else {
                        dailyLbDiff = dlbMemory - dlb.foundAt;
                        updateLbMemory(
                          config.mode,
                          mode2,
                          "daily",
                          dlb.foundAt
                        );
                        let str = Misc.getPositionString(dlb.foundAt + 1);
                        dailyLbString = `daily: ${str}`;
                      }
                    }
                    if (!dontShowDailyDiff) {
                      let sString =
                        dailyLbDiff === 1 || dailyLbDiff === -1 ? "" : "s";
                      if (dailyLbDiff > 0) {
                        dailyLbString += ` <span class="lbChange" aria-label="You've gained ${dailyLbDiff} position${sString}" data-balloon-pos="up">(${lbUpIcon}${dailyLbDiff})</span>`;
                      } else if (dailyLbDiff === 0) {
                        dailyLbString += ` <span class="lbChange" aria-label="Your position remained the same" data-balloon-pos="up">(${lbRightIcon}${dailyLbDiff})</span>`;
                      } else if (dailyLbDiff < 0) {
                        dailyLbString += ` <span class="lbChange" aria-label="You've lost ${dailyLbDiff} position${sString}" data-balloon-pos="up">(${lbDownIcon}${dailyLbDiff})</span>`;
                      }
                    }
                    $("#result .stats .leaderboards .bottom").html(
                      globalLbString + "<br>" + dailyLbString
                    );

                    // CloudFunctions.saveLbMemory({
                    //   uid: firebase.auth().currentUser.uid,
                    //   obj: db_getSnapshot().lbMemory,
                    // }).then((d) => {
                    //   if (d.data.returnCode === 1) {
                    //   } else {
                    //     Notifications.add(
                    //       `Error saving lb memory ${d.data.message}`,
                    //       4000
                    //     );
                    //   }
                    // });
                  }
                  if (
                    e.data.dailyLeaderboard === null &&
                    e.data.globalLeaderboard === null
                  ) {
                    $("#result .stats .leaderboards").addClass("hidden");
                  }
                  if (e.data.needsToVerifyEmail === true) {
                    $("#result .stats .leaderboards").removeClass("hidden");
                    $("#result .stats .leaderboards .bottom").html(
                      `please verify your email<br>to access leaderboards - <a onClick="sendVerificationEmail()">resend email</a>`
                    );
                  } else if (e.data.lbBanned) {
                    $("#result .stats .leaderboards").removeClass("hidden");
                    $("#result .stats .leaderboards .bottom").html("banned");
                  } else if (e.data.name === false) {
                    $("#result .stats .leaderboards").removeClass("hidden");
                    $("#result .stats .leaderboards .bottom").html(
                      "update your name to access leaderboards"
                    );
                  } else if (e.data.needsToVerify === true) {
                    $("#result .stats .leaderboards").removeClass("hidden");
                    $("#result .stats .leaderboards .bottom").html(
                      "verification needed to access leaderboards"
                    );
                  }

                  if (e.data.resultCode === 2) {
                    //new pb
                    showCrown();
                    if (!localPb) {
                    }
                    db_saveLocalPB(
                      config.mode,
                      mode2,
                      config.punctuation,
                      config.language,
                      config.difficulty,
                      stats.wpm,
                      stats.acc,
                      stats.wpmRaw,
                      consistency
                    );
                  } else if (e.data.resultCode === 1) {
                    hideCrown();
                    // if (localPb) {
                    //   Notifications.add(
                    //     "Local PB data is out of sync! Refresh the page to resync it or contact Miodec on Discord.",
                    //     15000
                    //   );
                    // }
                  }
                }
              })
              .catch((e) => {
                console.error(e);
                Notifications.add("Could not save result. " + e, -1);
              });
          });
        });
      } else {
        try {
          firebase.analytics().logEvent("testCompletedNoLogin", completedEvent);
        } catch (e) {
          console.log("Analytics unavailable");
        }
        notSignedInLastResult = completedEvent;
      }
    } else {
      Notifications.add("Test invalid", 0);
      testInvalid = true;
      try {
        firebase.analytics().logEvent("testCompletedInvalid", completedEvent);
      } catch (e) {
        console.log("Analytics unavailable");
      }
    }
  }

  if (firebase.auth().currentUser != null) {
    $("#result .loginTip").addClass("hidden");
  } else {
    $("#result .stats .leaderboards").addClass("hidden");
    $("#result .loginTip").removeClass("hidden");
  }

  let testType = "";

  if (config.mode === "quote") {
    let qlen = "";
    if (config.quoteLength === 0) {
      qlen = "short ";
    } else if (config.quoteLength === 1) {
      qlen = "medium ";
    } else if (config.quoteLength === 2) {
      qlen = "long ";
    } else if (config.quoteLength === 3) {
      qlen = "thicc ";
    }
    testType += qlen + config.mode;
  } else {
    testType += config.mode;
  }
  if (config.mode == "time") {
    testType += " " + config.time;
  } else if (config.mode == "words") {
    testType += " " + config.words;
  }
  if (
    config.mode != "custom" &&
    activeFunBox !== "gibberish" &&
    activeFunBox !== "58008"
  ) {
    testType += "<br>" + config.language.replace(/_/g, " ");
  }
  if (config.punctuation) {
    testType += "<br>punctuation";
  }
  if (config.numbers) {
    testType += "<br>numbers";
  }
  if (config.blindMode) {
    testType += "<br>blind";
  }
  if (activeFunBox !== "none") {
    testType += "<br>" + activeFunBox.replace(/_/g, " ");
  }
  if (config.difficulty == "expert") {
    testType += "<br>expert";
  } else if (config.difficulty == "master") {
    testType += "<br>master";
  }

  $("#result .stats .testType .bottom").html(testType);

  let otherText = "";
  if (config.layout !== "default") {
    otherText += "<br>" + config.layout;
  }
  if (difficultyFailed) {
    otherText += "<br>failed";
  }
  if (afkDetected) {
    otherText += "<br>afk detected";
  }
  if (testInvalid) {
    otherText += "<br>invalid";
  }
  if (sameWordset) {
    otherText += "<br>repeated";
  }
  if (bailout) {
    otherText += "<br>bailed out";
  }

  if (otherText == "") {
    $("#result .stats .info").addClass("hidden");
  } else {
    $("#result .stats .info").removeClass("hidden");
    otherText = otherText.substring(4);
    $("#result .stats .info .bottom").html(otherText);
  }

  if (
    $("#result .stats .tags").hasClass("hidden") &&
    $("#result .stats .info").hasClass("hidden")
  ) {
    $("#result .stats .infoAndTags").addClass("hidden");
  } else {
    $("#result .stats .infoAndTags").removeClass("hidden");
  }

  if (config.mode === "quote") {
    $("#result .stats .source").removeClass("hidden");
    $("#result .stats .source .bottom").html(randomQuote.source);
  } else {
    $("#result .stats .source").addClass("hidden");
  }

  wpmOverTimeChart.options.scales.yAxes[0].ticks.max = maxChartVal;
  wpmOverTimeChart.options.scales.yAxes[1].ticks.max = maxChartVal;

  wpmOverTimeChart.update({ duration: 0 });
  wpmOverTimeChart.resize();
  swapElements($("#typingTest"), $("#result"), 250, () => {
    resultCalculating = false;
    $("#words").empty();
    wpmOverTimeChart.resize();
    if (config.alwaysShowWordsHistory) {
      toggleResultWordsDisplay();
    }
  });
}

function startTest() {
  if (pageTransition) {
    return;
  }
  if (!dbConfigLoaded) {
    configChangedBeforeDb = true;
  }
  try {
    if (firebase.auth().currentUser != null) {
      firebase.analytics().logEvent("testStarted");
    } else {
      firebase.analytics().logEvent("testStartedNoLogin");
    }
  } catch (e) {
    console.log("Analytics unavailable");
  }
  testActive = true;
  testStart = performance.now();
  restartTimer();
  showTimer();
  $("#liveWpm").text("0");
  showLiveWpm();
  showLiveAcc();
  updateTimer();
  clearTimeout(timer);
  keypressStats = {
    spacing: {
      current: -1,
      array: [],
    },
    duration: {
      current: -1,
      array: [],
    },
  };

  if (activeFunBox === "memory") {
    memoryFunboxInterval = clearInterval(memoryFunboxInterval);
    memoryFunboxTimer = null;
    $("#wordsWrapper").addClass("hidden");
  }

  try {
    if (config.paceCaret !== "off")
      movePaceCaret(performance.now() + paceCaret.spc * 1000);
  } catch (e) {}
  //use a recursive self-adjusting timer to avoid time drift
  const stepIntervalMS = 1000;
  (function loop(expectedStepEnd) {
    const delay = expectedStepEnd - performance.now();
    timer = setTimeout(function () {
      time++;
      $(".pageTest #premidSecondsLeft").text(config.time - time);
      if (
        config.mode === "time" ||
        (config.mode === "custom" && customText.isTimeRandom)
      ) {
        updateTimer();
      }
      let wpmAndRaw = liveWpmAndRaw();
      updateLiveWpm(wpmAndRaw.wpm, wpmAndRaw.raw);
      wpmHistory.push(wpmAndRaw.wpm);
      rawHistory.push(wpmAndRaw.raw);

      let acc = Misc.roundTo2(
        (accuracyStats.correct /
          (accuracyStats.correct + accuracyStats.incorrect)) *
          100
      );

      if (activeFunBox === "layoutfluid" && config.mode === "time") {
        const layouts = ["qwerty", "dvorak", "colemak"];
        let index = 0;
        index = Math.floor(time / (config.time / 3));

        if (
          time == Math.floor(config.time / 3) - 3 ||
          time == (config.time / 3) * 2 - 3
        ) {
          Notifications.add("3", 0, 1);
        }
        if (
          time == Math.floor(config.time / 3) - 2 ||
          time == Math.floor(config.time / 3) * 2 - 2
        ) {
          Notifications.add("2", 0, 1);
        }
        if (
          time == Math.floor(config.time / 3) - 1 ||
          time == Math.floor(config.time / 3) * 2 - 1
        ) {
          Notifications.add("1", 0, 1);
        }

        if (config.layout !== layouts[index] && layouts[index] !== undefined) {
          Notifications.add(`--- !!! ${layouts[index]} !!! ---`, 0);
        }
        setLayout(layouts[index]);
        setKeymapLayout(layouts[index]);
        updateHighlightedKeymapKey();
        settingsGroups.layout.updateButton();
      }

      keypressPerSecond.push(currentKeypress);
      currentKeypress = {
        mod: 0,
        count: 0,
        words: [],
      };
      errorsPerSecond.push(currentError);
      currentError = {
        count: 0,
        words: [],
      };
      if (
        (config.minWpm === "custom" &&
          wpmAndRaw.wpm < parseInt(config.minWpmCustomSpeed) &&
          currentWordIndex > 3) ||
        (config.minAcc === "custom" && acc < parseInt(config.minAccCustom))
      ) {
        clearTimeout(timer);
        failTest();
        return;
      }
      if (
        config.mode == "time" ||
        (config.mode === "custom" && customText.isTimeRandom)
      ) {
        if (
          (time >= config.time &&
            config.time !== 0 &&
            config.mode === "time") ||
          (time >= customText.time &&
            customText.time !== 0 &&
            config.mode === "custom")
        ) {
          //times up
          clearTimeout(timer);
          hideCaret();
          testActive = false;
          inputHistory.push(currentInput);
          correctedHistory.push(currentCorrected);
          showResult();
          return;
        }
      }
      loop(expectedStepEnd + stepIntervalMS);
    }, delay);
  })(testStart + stepIntervalMS);
}

function restartTest(withSameWordset = false, nosave = false) {
  if (!manualRestart) {
    if (
      (config.mode === "words" && config.words < 1000 && config.words > 0) ||
      (config.mode === "time" && config.time < 3600 && config.time > 0) ||
      config.mode === "quote" ||
      (config.mode === "custom" &&
        customText.isWordRandom &&
        customText.word < 1000 &&
        customText.word != 0) ||
      (config.mode === "custom" &&
        customText.isTimeRandom &&
        customText.time < 3600 &&
        customText.time != 0) ||
      (config.mode === "custom" &&
        !customText.isWordRandom &&
        customText.text.length < 1000)
    ) {
    } else {
      if (testActive) {
        Notifications.add(
          "Restart disabled for long tests. Use your mouse to confirm.",
          0
        );
        return;
      }
    }
  }

  if (modeBeforePractise !== null && !withSameWordset) {
    Notifications.add("Reverting to previous settings.", 0);
    setMode(modeBeforePractise);
    setPunctuation(punctuationBeforePractise);
    setNumbers(numbersBeforePractise);
    modeBeforePractise = null;
    punctuationBeforePractise = null;
    numbersBeforePractise = null;
  }

  manualRestart = false;
  clearTimeout(timer);
  time = 0;
  wpmHistory = [];
  rawHistory = [];
  missedWords = {};
  correctedHistory = [];
  currentCorrected = "";
  setFocus(false);
  hideCaret();
  testActive = false;
  hideLiveWpm();
  hideLiveAcc();
  hideTimer();
  bailout = false;
  paceCaret = null;
  if (paceCaret !== null) clearTimeout(paceCaret.timeout);
  $("#showWordHistoryButton").removeClass("loaded");
  focusWords();
  keypressPerSecond = [];
  lastSecondNotRound = false;
  currentKeypress = {
    mod: 0,
    count: 0,
    words: [],
  };
  errorsPerSecond = [];
  currentError = {
    count: 0,
    words: [],
  };
  currentTestLine = 0;
  activeWordJumped = false;
  keypressStats = {
    spacing: {
      current: -1,
      array: [],
    },
    duration: {
      current: -1,
      array: [],
    },
  };
  $("#timerNumber").css("opacity", 0);
  let el = null;
  if (resultVisible) {
    //results are being displayed
    el = $("#result");
  } else {
    //words are being displayed
    el = $("#typingTest");
  }
  if (resultVisible) {
    if (
      config.randomTheme !== "off" &&
      !pageTransition &&
      !config.customTheme
    ) {
      randomiseTheme();
    }
  }
  resultVisible = false;

  el.stop(true, true).animate(
    {
      opacity: 0,
    },
    125,
    async () => {
      $("#typingTest").css("opacity", 0).removeClass("hidden");
      if (!withSameWordset) {
        sameWordset = false;
        textHasTab = false;
        await initWords();
        initPaceCaret(nosave);
      } else {
        sameWordset = true;
        testActive = false;
        currentWordIndex = 0;
        currentWordElementIndex = 0;
        accuracyStats = {
          correct: 0,
          incorrect: 0,
        };
        inputHistory = [];
        currentInput = "";
        initPaceCaret();
        showWords();
      }
      if (config.keymapMode !== "off") {
        showKeymap();
      } else {
        hideKeymap();
      }
      document.querySelector("#miniTimerAndLiveWpm .wpm").innerHTML = "0";
      document.querySelector("#miniTimerAndLiveWpm .acc").innerHTML = "100%";
      document.querySelector("#liveWpm").innerHTML = "0";
      document.querySelector("#liveAcc").innerHTML = "100%";

      if (activeFunBox === "memory") {
        memoryFunboxInterval = clearInterval(memoryFunboxInterval);
        memoryFunboxTimer = Math.round(Math.pow(wordsList.length, 1.2));
        memoryFunboxInterval = setInterval(() => {
          memoryFunboxTimer -= 1;
          Notifications.add(
            memoryFunboxTimer == 0 ? "Times up" : memoryFunboxTimer,
            0,
            1
          );
          if (memoryFunboxTimer <= 0) {
            memoryFunboxInterval = clearInterval(memoryFunboxInterval);
            memoryFunboxTimer = null;
            $("#wordsWrapper").addClass("hidden");
          }
        }, 1000);

        if (config.keymapMode === "next") {
          setKeymapMode("react");
        }
      }

      let mode2 = "";
      if (config.mode === "time") {
        mode2 = config.time;
      } else if (config.mode === "words") {
        mode2 = config.words;
      } else if (config.mode === "custom") {
        mode2 = "custom";
      } else if (config.mode === "quote") {
        mode2 = randomQuote.id;
      }
      let fbtext = "";
      if (activeFunBox !== "none") {
        fbtext = " " + activeFunBox;
      }
      $(".pageTest #premidTestMode").text(
        `${config.mode} ${mode2} ${config.language}${fbtext}`
      );
      $(".pageTest #premidSecondsLeft").text(config.time);

      if (activeFunBox === "layoutfluid") {
        setLayout("qwerty");
        settingsGroups.layout.updateButton();
        setKeymapLayout("qwerty");
        settingsGroups.keymapLayout.updateButton();
        updateHighlightedKeymapKey();
      }

      $("#result").addClass("hidden");
      $("#testModesNotice").removeClass("hidden").css({
        opacity: 1,
      });
      resetPaceCaret();
      $("#typingTest")
        .css("opacity", 0)
        .removeClass("hidden")
        .stop(true, true)
        .animate(
          {
            opacity: 1,
          },
          125,
          () => {
            resetPaceCaret();
            hideCrown();
            clearTimeout(timer);
            if ($("#commandLineWrapper").hasClass("hidden")) focusWords();
            wpmOverTimeChart.update();
            updateTestModesNotice();
          }
        );
    }
  );
}

function focusWords() {
  if (!$("#wordsWrapper").hasClass("hidden")) {
    $("#wordsInput").focus();
  }
}

function setCustomText() {
  customText.text = prompt("Custom text").trim();
  customText.text = customText.text.replace(/[\n\r\t ]/gm, " ");
  customText.text = customText.text.replace(/ +/gm, " ");
  customText.text = customText.text.split(" ");
  if (customText.text.text.length >= 10000) {
    Notifications.add("Custom text cannot be longer than 10000 words.", 0);
    setMode("time");
    customText.text = "The quick brown fox jumped over the lazy dog".split(" ");
  }
}

function changePage(page) {
  if (pageTransition) {
    return;
  }
  let activePage = $(".page.active");
  $(".page").removeClass("active");
  $("#wordsInput").focusout();
  if (page == "test" || page == "") {
    pageTransition = true;
    swapElements(activePage, $(".page.pageTest"), 250, () => {
      pageTransition = false;
      focusWords();
      $(".page.pageTest").addClass("active");
      history.pushState("/", null, "/");
    });
    showTestConfig();
    hideSignOutButton();
    restartCount = 0;
    incompleteTestSeconds = 0;
    manualRestart = true;
    restartTest();
  } else if (page == "about") {
    pageTransition = true;
    restartTest();
    swapElements(activePage, $(".page.pageAbout"), 250, () => {
      pageTransition = false;
      history.pushState("about", null, "about");
      $(".page.pageAbout").addClass("active");
    });
    hideTestConfig();
    hideSignOutButton();
  } else if (page == "settings") {
    pageTransition = true;
    restartTest();
    swapElements(activePage, $(".page.pageSettings"), 250, () => {
      pageTransition = false;
      history.pushState("settings", null, "settings");
      $(".page.pageSettings").addClass("active");
    });
    updateSettingsPage();
    hideTestConfig();
    hideSignOutButton();
  } else if (page == "account") {
    if (!firebase.auth().currentUser) {
      changePage("login");
    } else {
      pageTransition = true;
      restartTest();
      swapElements(activePage, $(".page.pageAccount"), 250, () => {
        pageTransition = false;
        history.pushState("account", null, "account");
        $(".page.pageAccount").addClass("active");
      });
      refreshAccountPage();
      hideTestConfig();
      showSignOutButton();
    }
  } else if (page == "login") {
    if (firebase.auth().currentUser != null) {
      changePage("account");
    } else {
      pageTransition = true;
      restartTest();
      swapElements(activePage, $(".page.pageLogin"), 250, () => {
        pageTransition = false;
        history.pushState("login", null, "login");
        $(".page.pageLogin").addClass("active");
      });
      hideTestConfig();
      hideSignOutButton();
    }
  }
}

function setMode(mode, nosave) {
  if (mode !== "words" && activeFunBox === "memory") {
    Notifications.add("Memory funbox can only be used with words mode.", 0);
    return;
  }

  config.mode = mode;
  $("#top .config .mode .text-button").removeClass("active");
  $("#top .config .mode .text-button[mode='" + mode + "']").addClass("active");
  if (config.mode == "time") {
    $("#top .config .wordCount").addClass("hidden");
    $("#top .config .time").removeClass("hidden");
    $("#top .config .customText").addClass("hidden");
    $("#top .config .punctuationMode").removeClass("hidden");
    $("#top .config .numbersMode").removeClass("hidden");
    $("#top .config .quoteLength").addClass("hidden");
  } else if (config.mode == "words") {
    $("#top .config .wordCount").removeClass("hidden");
    $("#top .config .time").addClass("hidden");
    $("#top .config .customText").addClass("hidden");
    $("#top .config .punctuationMode").removeClass("hidden");
    $("#top .config .numbersMode").removeClass("hidden");
    $("#top .config .quoteLength").addClass("hidden");
  } else if (config.mode == "custom") {
    if (
      activeFunBox === "58008" ||
      activeFunBox === "gibberish" ||
      activeFunBox === "ascii"
    ) {
      activeFunBox = "none";
      updateTestModesNotice();
    }
    $("#top .config .wordCount").addClass("hidden");
    $("#top .config .time").addClass("hidden");
    $("#top .config .customText").removeClass("hidden");
    $("#top .config .punctuationMode").removeClass("hidden");
    $("#top .config .numbersMode").removeClass("hidden");
    $("#top .config .quoteLength").addClass("hidden");
    setPunctuation(false, true);
    setNumbers(false, true);
  } else if (config.mode == "quote") {
    setToggleSettings(false, nosave);
    $("#top .config .wordCount").addClass("hidden");
    $("#top .config .time").addClass("hidden");
    $("#top .config .customText").addClass("hidden");
    $("#top .config .punctuationMode").addClass("hidden");
    $("#top .config .numbersMode").addClass("hidden");
    $("#result .stats .source").removeClass("hidden");
    $("#top .config .quoteLength").removeClass("hidden");
  }
  if (!nosave) saveConfigToCookie();
}

function liveWpmAndRaw() {
  let chars = 0;
  let correctWordChars = 0;
  let spaces = 0;
  for (let i = 0; i < inputHistory.length; i++) {
    let word = wordsList[i];
    if (inputHistory[i] == word) {
      //the word is correct
      //+1 for space
      correctWordChars += word.length;
      if (
        i < inputHistory.length - 1 &&
        Misc.getLastChar(inputHistory[i]) !== "\n"
      ) {
        spaces++;
      }
    }
    chars += inputHistory[i].length;
  }
  if (wordsList[currentWordIndex] == currentInput) {
    correctWordChars += currentInput.length;
  }
  if (activeFunBox === "nospace") {
    spaces = 0;
  }
  chars += currentInput.length;
  let testNow = performance.now();
  let testSeconds = (testNow - testStart) / 1000;
  let wpm = Math.round(((correctWordChars + spaces) * (60 / testSeconds)) / 5);
  let raw = Math.round(((chars + spaces) * (60 / testSeconds)) / 5);
  return {
    wpm: wpm,
    raw: raw,
  };
}

function updateLiveWpm(wpm, raw) {
  if (!testActive || !config.showLiveWpm) {
    hideLiveWpm();
  } else {
    showLiveWpm();
  }
  let number = wpm;
  if (config.blindMode) {
    number = raw;
  }
  if (config.alwaysShowCPM) {
    number = Math.round(number * 5);
  }
  document.querySelector("#miniTimerAndLiveWpm .wpm").innerHTML = number;
  document.querySelector("#liveWpm").innerHTML = number;
}

function updateLiveAcc(acc) {
  if (!testActive || !config.showLiveAcc) {
    hideLiveAcc();
  } else {
    showLiveAcc();
  }
  let number = Math.floor(acc);
  if (config.blindMode) {
    number = 100;
  }
  document.querySelector("#miniTimerAndLiveWpm .acc").innerHTML = number + "%";
  document.querySelector("#liveAcc").innerHTML = number + "%";
}

function showLiveWpm() {
  if (!config.showLiveWpm) return;
  if (!testActive) return;
  if (config.timerStyle === "mini") {
    // $("#miniTimerAndLiveWpm .wpm").css("opacity", config.timerOpacity);
    if (!$("#miniTimerAndLiveWpm .wpm").hasClass("hidden")) return;
    $("#miniTimerAndLiveWpm .wpm")
      .removeClass("hidden")
      .css("opacity", 0)
      .animate(
        {
          opacity: config.timerOpacity,
        },
        125
      );
  } else {
    // $("#liveWpm").css("opacity", config.timerOpacity);
    if (!$("#liveWpm").hasClass("hidden")) return;
    $("#liveWpm").removeClass("hidden").css("opacity", 0).animate(
      {
        opacity: config.timerOpacity,
      },
      125
    );
  }
}

function hideLiveWpm() {
  // $("#liveWpm").css("opacity", 0);
  // $("#miniTimerAndLiveWpm .wpm").css("opacity", 0);
  $("#liveWpm").animate(
    {
      opacity: config.timerOpacity,
    },
    125,
    () => {
      $("#liveWpm").addClass("hidden");
    }
  );
  $("#miniTimerAndLiveWpm .wpm").animate(
    {
      opacity: config.timerOpacity,
    },
    125,
    () => {
      $("#miniTimerAndLiveWpm .wpm").addClass("hidden");
    }
  );
}

function showLiveAcc() {
  if (!config.showLiveAcc) return;
  if (!testActive) return;
  if (config.timerStyle === "mini") {
    // $("#miniTimerAndLiveWpm .wpm").css("opacity", config.timerOpacity);
    if (!$("#miniTimerAndLiveWpm .acc").hasClass("hidden")) return;
    $("#miniTimerAndLiveWpm .acc")
      .removeClass("hidden")
      .css("opacity", 0)
      .animate(
        {
          opacity: config.timerOpacity,
        },
        125
      );
  } else {
    // $("#liveWpm").css("opacity", config.timerOpacity);
    if (!$("#liveAcc").hasClass("hidden")) return;
    $("#liveAcc").removeClass("hidden").css("opacity", 0).animate(
      {
        opacity: config.timerOpacity,
      },
      125
    );
  }
}

function hideLiveAcc() {
  // $("#liveWpm").css("opacity", 0);
  // $("#miniTimerAndLiveWpm .wpm").css("opacity", 0);
  $("#liveAcc").animate(
    {
      opacity: config.timerOpacity,
    },
    125,
    () => {
      $("#liveAcc").addClass("hidden");
    }
  );
  $("#miniTimerAndLiveWpm .acc").animate(
    {
      opacity: config.timerOpacity,
    },
    125,
    () => {
      $("#miniTimerAndLiveWpm .acc").addClass("hidden");
    }
  );
}

function swapElements(
  el1,
  el2,
  totalDuration,
  callback = function () {
    return;
  }
) {
  if (
    (el1.hasClass("hidden") && !el2.hasClass("hidden")) ||
    (!el1.hasClass("hidden") && el2.hasClass("hidden"))
  ) {
    //one of them is hidden and the other is visible
    if (el1.hasClass("hidden")) {
      callback();
      return false;
    }
    $(el1)
      .removeClass("hidden")
      .css("opacity", 1)
      .animate(
        {
          opacity: 0,
        },
        totalDuration / 2,
        () => {
          $(el1).addClass("hidden");
          $(el2)
            .removeClass("hidden")
            .css("opacity", 0)
            .animate(
              {
                opacity: 1,
              },
              totalDuration / 2,
              () => {
                callback();
              }
            );
        }
      );
  } else if (el1.hasClass("hidden") && el2.hasClass("hidden")) {
    //both are hidden, only fade in the second
    $(el2)
      .removeClass("hidden")
      .css("opacity", 0)
      .animate(
        {
          opacity: 1,
        },
        totalDuration,
        () => {
          callback();
        }
      );
  } else {
    callback();
  }
}

function updateAccountLoginButton() {
  if (firebase.auth().currentUser != null) {
    swapElements(
      $("#menu .icon-button.login"),
      $("#menu .icon-button.account"),
      250
    );
  } else {
    swapElements(
      $("#menu .icon-button.account"),
      $("#menu .icon-button.login"),
      250
    );
  }
}

function accountIconLoading(truefalse) {
  if (truefalse) {
    $("#top #menu .account .icon").html(
      '<i class="fas fa-fw fa-spin fa-circle-notch"></i>'
    );
    $("#top #menu .account").css("opacity", 1);
  } else {
    $("#top #menu .account .icon").html('<i class="fas fa-fw fa-user"></i>');
    $("#top #menu .account").css("opacity", 1);
  }
}

function toggleResultWordsDisplay() {
  if (resultVisible) {
    if ($("#resultWordsHistory").stop(true, true).hasClass("hidden")) {
      //show

      if (!$("#showWordHistoryButton").hasClass("loaded")) {
        $("#words").html(
          `<div class="preloader"><i class="fas fa-fw fa-spin fa-circle-notch"></i></div>`
        );
        loadWordsHistory().then(() => {
          $("#resultWordsHistory")
            .removeClass("hidden")
            .css("display", "none")
            .slideDown(250);
        });
      } else {
        $("#resultWordsHistory")
          .removeClass("hidden")
          .css("display", "none")
          .slideDown(250);
      }
    } else {
      //hide

      $("#resultWordsHistory").slideUp(250, () => {
        $("#resultWordsHistory").addClass("hidden");
      });
    }
  }
}

async function loadWordsHistory() {
  $("#resultWordsHistory .words").empty();
  let wordsHTML = "";
  for (let i = 0; i < inputHistory.length + 2; i++) {
    let input = inputHistory[i];
    let word = wordsList[i];
    let wordEl = "";
    try {
      if (input === "") throw new Error("empty input word");
      if (correctedHistory[i] !== undefined && correctedHistory[i] !== "") {
        wordEl = `<div class='word' input="${correctedHistory[i]
          .replace(/"/g, "&quot;")
          .replace(/ /g, "_")}">`;
      } else {
        wordEl = `<div class='word' input="${input
          .replace(/"/g, "&quot;")
          .replace(/ /g, "_")}">`;
      }
      if (i === inputHistory.length - 1) {
        //last word
        let wordstats = {
          correct: 0,
          incorrect: 0,
          missed: 0,
        };
        for (let c = 0; c < word.length; c++) {
          if (c < inputHistory[i].length) {
            //on char that still has a word list pair
            if (inputHistory[i][c] == word[c]) {
              wordstats.correct++;
            } else {
              wordstats.incorrect++;
            }
          } else {
            //on char that is extra
            wordstats.missed++;
          }
        }
        if (wordstats.incorrect !== 0 || config.mode !== "time") {
          if (input !== word) {
            wordEl = `<div class='word error' input="${input
              .replace(/"/g, "&quot;")
              .replace(/ /g, "_")}">`;
          }
        }
      } else {
        if (input !== word) {
          wordEl = `<div class='word error' input="${input
            .replace(/"/g, "&quot;")
            .replace(/ /g, "_")}">`;
        }
      }

      let loop;
      if (input.length > word.length) {
        //input is longer - extra characters possible (loop over input)
        loop = input.length;
      } else {
        //input is shorter or equal (loop over word list)
        loop = word.length;
      }

      for (let c = 0; c < loop; c++) {
        let correctedChar;
        try {
          correctedChar = correctedHistory[i][c];
        } catch (e) {
          correctedChar = undefined;
        }
        let extraCorrected = "";
        if (
          c + 1 === loop &&
          correctedHistory[i] !== undefined &&
          correctedHistory[i].length > input.length
        ) {
          extraCorrected = "extraCorrected";
        }
        if (word[c] !== undefined) {
          if (input[c] === word[c]) {
            if (correctedChar === input[c] || correctedChar === undefined) {
              wordEl += `<letter class="correct ${extraCorrected}">${word[c]}</letter>`;
            } else {
              wordEl +=
                `<letter class="corrected ${extraCorrected}">` +
                word[c] +
                "</letter>";
            }
          } else {
            if (input[c] === currentInput) {
              wordEl +=
                `<letter class='correct ${extraCorrected}'>` +
                word[c] +
                "</letter>";
            } else if (input[c] === undefined) {
              wordEl += "<letter>" + word[c] + "</letter>";
            } else {
              wordEl +=
                `<letter class="incorrect ${extraCorrected}">` +
                word[c] +
                "</letter>";
            }
          }
        } else {
          wordEl += '<letter class="incorrect extra">' + input[c] + "</letter>";
        }
      }
      wordEl += "</div>";
    } catch (e) {
      try {
        wordEl = "<div class='word'>";
        for (let c = 0; c < word.length; c++) {
          wordEl += "<letter>" + word[c] + "</letter>";
        }
        wordEl += "</div>";
      } catch {}
    }
    wordsHTML += wordEl;
  }
  $("#resultWordsHistory .words").html(wordsHTML);
  $("#showWordHistoryButton").addClass("loaded");
  return true;
}

function flipTestColors(tf) {
  if (tf) {
    $("#words").addClass("flipped");
  } else {
    $("#words").removeClass("flipped");
  }
}

function applyColorfulMode(tc) {
  if (tc) {
    $("#words").addClass("colorfulMode");
  } else {
    $("#words").removeClass("colorfulMode");
  }
}

function showEditTags(action, id, name) {
  if (action === "add") {
    $("#tagsWrapper #tagsEdit").attr("action", "add");
    $("#tagsWrapper #tagsEdit .title").html("Add new tag");
    $("#tagsWrapper #tagsEdit .button").html(`<i class="fas fa-plus"></i>`);
    $("#tagsWrapper #tagsEdit input").val("");
    $("#tagsWrapper #tagsEdit input").removeClass("hidden");
  } else if (action === "edit") {
    $("#tagsWrapper #tagsEdit").attr("action", "edit");
    $("#tagsWrapper #tagsEdit").attr("tagid", id);
    $("#tagsWrapper #tagsEdit .title").html("Edit tag name");
    $("#tagsWrapper #tagsEdit .button").html(`<i class="fas fa-pen"></i>`);
    $("#tagsWrapper #tagsEdit input").val(name);
    $("#tagsWrapper #tagsEdit input").removeClass("hidden");
  } else if (action === "remove") {
    $("#tagsWrapper #tagsEdit").attr("action", "remove");
    $("#tagsWrapper #tagsEdit").attr("tagid", id);
    $("#tagsWrapper #tagsEdit .title").html("Remove tag " + name);
    $("#tagsWrapper #tagsEdit .button").html(`<i class="fas fa-check"></i>`);
    $("#tagsWrapper #tagsEdit input").addClass("hidden");
  }

  if ($("#tagsWrapper").hasClass("hidden")) {
    $("#tagsWrapper")
      .stop(true, true)
      .css("opacity", 0)
      .removeClass("hidden")
      .animate({ opacity: 1 }, 100, () => {
        $("#tagsWrapper #tagsEdit input").focus();
      });
  }
}

function hideEditTags() {
  if (!$("#tagsWrapper").hasClass("hidden")) {
    $("#tagsWrapper #tagsEdit").attr("action", "");
    $("#tagsWrapper #tagsEdit").attr("tagid", "");
    $("#tagsWrapper")
      .stop(true, true)
      .css("opacity", 1)
      .animate(
        {
          opacity: 0,
        },
        100,
        () => {
          $("#tagsWrapper").addClass("hidden");
        }
      );
  }
}

function updateTestModesNotice() {
  let anim = false;
  if ($(".pageTest #testModesNotice").text() === "") anim = true;

  $(".pageTest #testModesNotice").empty();

  if (sameWordset) {
    $(".pageTest #testModesNotice").append(
      `<div class="text-button" onClick="restartTest()" style="color:var(--error-color);"><i class="fas fa-sync-alt"></i>repeated</div>`
    );
  }

  if (textHasTab) {
    $(".pageTest #testModesNotice").append(
      `<div class="text-button"><i class="fas fa-long-arrow-alt-right"></i>shift + tab to restart</div>`
    );
  }

  if (config.language === "english_1k" || config.language === "english_10k") {
    $(".pageTest #testModesNotice").append(
      `<div class="text-button" commands="commandsLanguages"><i class="fas fa-globe-americas"></i>${config.language.replace(
        "_",
        " "
      )}</div>`
    );
  }

  if (config.difficulty === "expert") {
    $(".pageTest #testModesNotice").append(
      `<div class="text-button" commands="commandsDifficulty"><i class="fas fa-star-half-alt"></i>expert</div>`
    );
  } else if (config.difficulty === "master") {
    $(".pageTest #testModesNotice").append(
      `<div class="text-button" commands="commandsDifficulty"><i class="fas fa-star"></i>master</div>`
    );
  }

  if (config.blindMode) {
    $(".pageTest #testModesNotice").append(
      `<div class="text-button" onClick="toggleBlindMode()"><i class="fas fa-eye-slash"></i>blind</div>`
    );
  }

  if (config.paceCaret !== "off") {
    let speed = "";
    try {
      speed = ` (${Math.round(paceCaret.wpm)} wpm)`;
    } catch {}
    $(".pageTest #testModesNotice").append(
      `<div class="text-button" commands="commandsPaceCaret"><i class="fas fa-tachometer-alt"></i>${
        config.paceCaret === "average"
          ? "average"
          : config.paceCaret === "pb"
          ? "pb"
          : "custom"
      } pace${speed}</div>`
    );
  }

  if (config.minWpm !== "off") {
    $(".pageTest #testModesNotice").append(
      `<div class="text-button" commands="commandsMinWpm"><i class="fas fa-bomb"></i>min ${config.minWpmCustomSpeed} wpm</div>`
    );
  }

  if (config.minAcc !== "off") {
    $(".pageTest #testModesNotice").append(
      `<div class="text-button" commands="commandsMinAcc"><i class="fas fa-bomb"></i>min ${config.minAccCustom}% acc</div>`
    );
  }

  if (activeFunBox !== "none") {
    $(".pageTest #testModesNotice").append(
      `<div class="text-button" commands="commandsFunbox"><i class="fas fa-gamepad"></i>${activeFunBox.replace(
        /_/g,
        " "
      )}</div>`
    );
  }

  if (config.confidenceMode === "on") {
    $(".pageTest #testModesNotice").append(
      `<div class="text-button" commands="commandsConfidenceMode"><i class="fas fa-backspace"></i>confidence</div>`
    );
  }
  if (config.confidenceMode === "max") {
    $(".pageTest #testModesNotice").append(
      `<div class="text-button" commands="commandsConfidenceMode"><i class="fas fa-backspace"></i>max confidence</div>`
    );
  }

  if (config.stopOnError != "off") {
    $(".pageTest #testModesNotice").append(
      `<div class="text-button" commands="commandsStopOnError"><i class="fas fa-hand-paper"></i>stop on ${config.stopOnError}</div>`
    );
  }

  if (config.layout !== "default") {
    $(".pageTest #testModesNotice").append(
      `<div class="text-button" commands="commandsLayouts"><i class="fas fa-keyboard"></i>${config.layout}</div>`
    );
  }

  let tagsString = "";
  try {
    db_getSnapshot().tags.forEach((tag) => {
      if (tag.active === true) {
        tagsString += tag.name + ", ";
      }
    });

    if (tagsString !== "") {
      $(".pageTest #testModesNotice").append(
        `<div class="text-button" commands="commandsTags"><i class="fas fa-tag"></i>${tagsString.substring(
          0,
          tagsString.length - 2
        )}</div>`
      );
    }
  } catch {}

  if (anim) {
    $(".pageTest #testModesNotice")
      .css("transition", "none")
      .css("opacity", 0)
      .animate(
        {
          opacity: 1,
        },
        125,
        () => {
          $(".pageTest #testModesNotice").css("transition", ".125s");
        }
      );
  }
}

$("#tagsWrapper").click((e) => {
  if ($(e.target).attr("id") === "tagsWrapper") {
    hideEditTags();
  }
});

$("#tagsWrapper #tagsEdit .button").click(() => {
  tagsEdit();
});

$("#tagsWrapper #tagsEdit input").keypress((e) => {
  if (e.keyCode == 13) {
    tagsEdit();
  }
});

function tagsEdit() {
  let action = $("#tagsWrapper #tagsEdit").attr("action");
  let inputVal = $("#tagsWrapper #tagsEdit input").val();
  let tagid = $("#tagsWrapper #tagsEdit").attr("tagid");
  hideEditTags();
  if (action === "add") {
    showBackgroundLoader();
    CloudFunctions.addTag({
      uid: firebase.auth().currentUser.uid,
      name: inputVal,
    }).then((e) => {
      hideBackgroundLoader();
      let status = e.data.resultCode;
      if (status === 1) {
        Notifications.add("Tag added", 1, 2);
        db_getSnapshot().tags.push({
          name: inputVal,
          id: e.data.id,
        });
        updateResultEditTagsPanelButtons();
        updateSettingsPage();
        updateFilterTags();
      } else if (status === -1) {
        Notifications.add("Invalid tag name", 0);
      } else if (status < -1) {
        Notifications.add("Unknown error: " + e.data.message, -1);
      }
    });
  } else if (action === "edit") {
    showBackgroundLoader();
    CloudFunctions.editTag({
      uid: firebase.auth().currentUser.uid,
      name: inputVal,
      tagid: tagid,
    }).then((e) => {
      hideBackgroundLoader();
      let status = e.data.resultCode;
      if (status === 1) {
        Notifications.add("Tag updated", 1);
        db_getSnapshot().tags.forEach((tag) => {
          if (tag.id === tagid) {
            tag.name = inputVal;
          }
        });
        updateResultEditTagsPanelButtons();
        updateSettingsPage();
        updateFilterTags();
      } else if (status === -1) {
        Notifications.add("Invalid tag name", 0);
      } else if (status < -1) {
        Notifications.add("Unknown error: " + e.data.message, -1);
      }
    });
  } else if (action === "remove") {
    showBackgroundLoader();
    CloudFunctions.removeTag({
      uid: firebase.auth().currentUser.uid,
      tagid: tagid,
    }).then((e) => {
      hideBackgroundLoader();
      let status = e.data.resultCode;
      if (status === 1) {
        Notifications.add("Tag removed", 1);
        db_getSnapshot().tags.forEach((tag, index) => {
          if (tag.id === tagid) {
            db_getSnapshot().tags.splice(index, 1);
          }
        });
        updateResultEditTagsPanelButtons();
        updateSettingsPage();
        updateFilterTags();
      } else if (status < -1) {
        Notifications.add("Unknown error: " + e.data.message, -1);
      }
    });
  }
}

function showCapsWarning() {
  if ($("#capsWarning").hasClass("hidden")) {
    $("#capsWarning").removeClass("hidden");
  }
}

function hideCapsWarning() {
  if (!$("#capsWarning").hasClass("hidden")) {
    $("#capsWarning").addClass("hidden");
  }
}

function showCustomTextPopup() {
  if ($("#customTextPopupWrapper").hasClass("hidden")) {
    if ($("#customTextPopup .check input").prop("checked")) {
      $("#customTextPopup .inputs .randomInputFields").removeClass("hidden");
    } else {
      $("#customTextPopup .inputs .randomInputFields").addClass("hidden");
    }
    $("#customTextPopupWrapper")
      .stop(true, true)
      .css("opacity", 0)
      .removeClass("hidden")
      .animate({ opacity: 1 }, 100, () => {
        let newtext = customText.text.join(" ");
        newtext = newtext.replace(/\n /g, "\n");
        $("#customTextPopup textarea").val(newtext);
        $("#customTextPopup .wordcount input").val(customText.word);
        $("#customTextPopup .time input").val(customText.time);
        $("#customTextPopup textarea").focus();
      });
  }
}

function hideCustomTextPopup() {
  if (!$("#customTextPopupWrapper").hasClass("hidden")) {
    $("#customTextPopupWrapper")
      .stop(true, true)
      .css("opacity", 1)
      .animate(
        {
          opacity: 0,
        },
        100,
        (e) => {
          $("#customTextPopupWrapper").addClass("hidden");
        }
      );
  }
}

$("#customTextPopupWrapper").mousedown((e) => {
  if ($(e.target).attr("id") === "customTextPopupWrapper") {
    hideCustomTextPopup();
  }
});

$("#customTextPopup .inputs .check input").change(() => {
  if ($("#customTextPopup .check input").prop("checked")) {
    $("#customTextPopup .inputs .randomInputFields").removeClass("hidden");
  } else {
    $("#customTextPopup .inputs .randomInputFields").addClass("hidden");
  }
});

$("#customTextPopup textarea").keypress((e) => {
  if (e.code === "Enter" && e.ctrlKey) {
    $("#customTextPopup .button").click();
  }
});

$("#customTextPopup .randomInputFields .wordcount input").keypress((e) => {
  $("#customTextPopup .randomInputFields .time input").val("");
});

$("#customTextPopup .randomInputFields .time input").keypress((e) => {
  $("#customTextPopup .randomInputFields .wordcount input").val("");
});

$("#customTextPopup .button").click(() => {
  let text = $("#customTextPopup textarea").val();
  text = text.trim();
  // text = text.replace(/[\r]/gm, " ");
  text = text.replace(/\\\\t/gm, "\t");
  text = text.replace(/\\\\n/gm, "\n");
  text = text.replace(/\\t/gm, "\t");
  text = text.replace(/\\n/gm, "\n");
  text = text.replace(/ +/gm, " ");
  // text = text.replace(/(\r\n)+/g, "\r\n");
  // text = text.replace(/(\n)+/g, "\n");
  // text = text.replace(/(\r)+/g, "\r");
  text = text.replace(/( *(\r\n|\r|\n) *)/g, "\n ");
  if ($("#customTextPopup .typographyCheck input").prop("checked")) {
    text = Misc.cleanTypographySymbols(text);
  }
  // text = Misc.remove_non_ascii(text);
  text = text.replace(/[\u2060]/g, "");
  text = text.split(" ");
  customText.text = text;
  customText.word = parseInt($("#customTextPopup .wordcount input").val());
  customText.time = parseInt($("#customTextPopup .time input").val());

  customText.isWordRandom =
    $("#customTextPopup .check input").prop("checked") &&
    !isNaN(customText.word);
  customText.isTimeRandom =
    $("#customTextPopup .check input").prop("checked") &&
    !isNaN(customText.time);

  if (
    isNaN(customText.word) &&
    isNaN(customText.time) &&
    (customText.isTimeRandom || customText.isWordRandom)
  ) {
    Notifications.add(
      "You need to specify word count or time in seconds to start a random custom test.",
      0,
      5
    );
    return;
  }

  if (
    !isNaN(customText.word) &&
    !isNaN(customText.time) &&
    (customText.isTimeRandom || customText.isWordRandom)
  ) {
    Notifications.add(
      "You need to pick between word count or time in seconds to start a random custom test.",
      0,
      5
    );
    return;
  }

  if (
    (customText.isWordRandom && parseInt(customText.word) === 0) ||
    (customText.isTimeRandom && parseInt(customText.time) === 0)
  ) {
    Notifications.add(
      "Infinite words! Make sure to use Bail Out from the command line to save your result.",
      0,
      7
    );
  }

  manualRestart = true;
  restartTest();
  hideCustomTextPopup();
});

function showCustomMode2Popup(mode) {
  if ($("#customMode2PopupWrapper").hasClass("hidden")) {
    $("#customMode2PopupWrapper")
      .stop(true, true)
      .css("opacity", 0)
      .removeClass("hidden")
      .animate({ opacity: 1 }, 100, (e) => {
        if (mode == "time") {
          $("#customMode2Popup .text").text("Test length");
          $("#customMode2Popup").attr("mode", "time");
        } else if (mode == "words") {
          $("#customMode2Popup .text").text("Word amount");
          $("#customMode2Popup").attr("mode", "words");
        }
        $("#customMode2Popup input").focus().select();
      });
  }
}

function hideCustomMode2Popup() {
  if (!$("#customMode2PopupWrapper").hasClass("hidden")) {
    $("#customMode2PopupWrapper")
      .stop(true, true)
      .css("opacity", 1)
      .animate(
        {
          opacity: 0,
        },
        100,
        (e) => {
          $("#customMode2PopupWrapper").addClass("hidden");
        }
      );
  }
}

function playClickSound() {
  if (config.playSoundOnClick === "off") return;
  if (clickSounds === null) initClickSounds();

  let rand = Math.floor(
    Math.random() * clickSounds[config.playSoundOnClick].length
  );
  let randomSound = clickSounds[config.playSoundOnClick][rand];
  randomSound.counter++;
  if (randomSound.counter === 2) randomSound.counter = 0;
  randomSound.sounds[randomSound.counter].currentTime = 0;
  randomSound.sounds[randomSound.counter].play();
}

function playErrorSound() {
  if (!config.playSoundOnError) return;
  errorSound.currentTime = 0;
  errorSound.play();
}

async function initPaceCaret() {
  let mode2 = "";
  if (config.mode === "time") {
    mode2 = config.time;
  } else if (config.mode === "words") {
    mode2 = config.words;
  } else if (config.mode === "custom") {
    mode2 = "custom";
  } else if (config.mode === "quote") {
    mode2 = randomQuote.id;
  }
  let wpm;
  if (config.paceCaret === "pb") {
    wpm = await db_getLocalPB(
      config.mode,
      mode2,
      config.punctuation,
      config.language,
      config.difficulty
    );
  } else if (config.paceCaret === "average") {
    let mode2 = "";
    if (config.mode === "time") {
      mode2 = config.time;
    } else if (config.mode === "words") {
      mode2 = config.words;
    } else if (config.mode === "custom") {
      mode2 = "custom";
    } else if (config.mode === "quote") {
      mode2 = randomQuote.id;
    }
    wpm = await db_getUserAverageWpm10(
      config.mode,
      mode2,
      config.punctuation,
      config.language,
      config.difficulty
    );
    console.log("avg pace " + wpm);
  } else if (config.paceCaret === "custom") {
    wpm = config.paceCaretCustomSpeed;
  }

  if (wpm < 1 || wpm == false || wpm == undefined || Number.isNaN(wpm)) {
    paceCaret = null;
    return;
  }

  let characters = wpm * 5;
  let cps = characters / 60; //characters per step
  let spc = 60 / characters; //seconds per character

  paceCaret = {
    wpm: wpm,
    cps: cps,
    spc: spc,
    correction: 0,
    currentWordIndex: 0,
    currentLetterIndex: -1,
    wordsStatus: {},
    timeout: null,
  };

  updateTestModesNotice();
}

function movePaceCaret(expectedStepEnd) {
  if (paceCaret === null || !testActive || resultVisible) {
    return;
  }
  if ($("#paceCaret").hasClass("hidden")) {
    $("#paceCaret").removeClass("hidden");
  }
  if ($("#paceCaret").hasClass("off")) {
    return;
  }
  try {
    paceCaret.currentLetterIndex++;
    if (
      paceCaret.currentLetterIndex >=
      wordsList[paceCaret.currentWordIndex].length
    ) {
      //go to the next word
      paceCaret.currentLetterIndex = -1;
      paceCaret.currentWordIndex++;
    }
    if (!config.blindMode) {
      if (paceCaret.correction < 0) {
        while (paceCaret.correction < 0) {
          paceCaret.currentLetterIndex--;
          if (paceCaret.currentLetterIndex <= -2) {
            //go to the previous word
            paceCaret.currentLetterIndex =
              wordsList[paceCaret.currentWordIndex - 1].length - 1;
            paceCaret.currentWordIndex--;
          }
          paceCaret.correction++;
        }
      } else if (paceCaret.correction > 0) {
        while (paceCaret.correction > 0) {
          paceCaret.currentLetterIndex++;
          if (
            paceCaret.currentLetterIndex >=
            wordsList[paceCaret.currentWordIndex].length
          ) {
            //go to the next word
            paceCaret.currentLetterIndex = -1;
            paceCaret.currentWordIndex++;
          }
          paceCaret.correction--;
        }
      }
    }
  } catch (e) {
    //out of words
    paceCaret = null;
    $("#paceCaret").addClass("hidden");
    return;
  }

  try {
    let caret = $("#paceCaret");
    let currentLetter;
    let newTop;
    let newLeft;
    try {
      if (paceCaret.currentLetterIndex === -1) {
        currentLetter = document
          .querySelectorAll("#words .word")
          [
            paceCaret.currentWordIndex -
              (currentWordIndex - currentWordElementIndex)
          ].querySelectorAll("letter")[0];
      } else {
        currentLetter = document
          .querySelectorAll("#words .word")
          [
            paceCaret.currentWordIndex -
              (currentWordIndex - currentWordElementIndex)
          ].querySelectorAll("letter")[paceCaret.currentLetterIndex];
      }
      newTop = currentLetter.offsetTop - $(currentLetter).height() / 20;
      newLeft;
      if (paceCaret.currentLetterIndex === -1) {
        newLeft = currentLetter.offsetLeft;
      } else {
        newLeft =
          currentLetter.offsetLeft +
          $(currentLetter).width() -
          caret.width() / 2;
      }
      caret.removeClass("hidden");
    } catch (e) {
      caret.addClass("hidden");
    }

    let smoothlinescroll = $("#words .smoothScroller").height();
    if (smoothlinescroll === undefined) smoothlinescroll = 0;

    $("#paceCaret").css({
      top: newTop - smoothlinescroll,
    });

    let duration = expectedStepEnd - performance.now();

    if (config.smoothCaret) {
      caret.stop(true, true).animate(
        {
          left: newLeft,
        },
        duration,
        "linear"
      );
    } else {
      caret.stop(true, true).animate(
        {
          left: newLeft,
        },
        0,
        "linear"
      );
    }
    paceCaret.timeout = setTimeout(() => {
      try {
        movePaceCaret(expectedStepEnd + paceCaret.spc * 1000);
      } catch (e) {
        paceCaret = null;
      }
    }, duration);
  } catch (e) {
    console.error(e);
    $("#paceCaret").addClass("hidden");
  }
}

function resetPaceCaret() {
  if (config.paceCaret === "off") return;
  if (!$("#paceCaret").hasClass("hidden")) {
    $("#paceCaret").addClass("hidden");
  }

  let caret = $("#paceCaret");
  let firstLetter = document
    .querySelector("#words .word")
    .querySelector("letter");

  caret.stop(true, true).animate(
    {
      top: firstLetter.offsetTop - $(firstLetter).height() / 4,
      left: firstLetter.offsetLeft,
    },
    0,
    "linear"
  );
}

$("#customMode2PopupWrapper").click((e) => {
  if ($(e.target).attr("id") === "customMode2PopupWrapper") {
    hideCustomMode2Popup();
  }
});

$("#customMode2Popup input").keypress((e) => {
  if (e.keyCode == 13) {
    applyMode2Popup();
  }
});

$("#customMode2Popup .button").click(() => {
  applyMode2Popup();
});

function updateKeytips() {
  if (config.swapEscAndTab) {
    $(".pageSettings .tip").html(`
    tip: You can also change all these settings quickly using the
    command line (
    <key>tab</key>
    )`);
    $("#bottom .keyTips").html(`
    <key>esc</key> - restart test<br>
      <key>tab</key> - command line`);
  } else {
    $(".pageSettings .tip").html(`
    tip: You can also change all these settings quickly using the
    command line (
    <key>esc</key>
    )`);
    $("#bottom .keyTips").html(`
    <key>tab</key> - restart test<br>
      <key>esc</key> - command line`);
  }
}

function applyMode2Popup() {
  let mode = $("#customMode2Popup").attr("mode");
  let val = parseInt($("#customMode2Popup input").val());

  if (mode == "time") {
    if (val !== null && !isNaN(val) && val >= 0) {
      setTimeConfig(val);
      manualRestart = true;
      restartTest();
      if (val >= 1800) {
        Notifications.add("Stay safe and take breaks!", 0);
      } else if (val == 0) {
        Notifications.add(
          "Infinite time! Make sure to use Bail Out from the command line to save your result.",
          0,
          7
        );
      }
    } else {
      Notifications.add("Custom time must be at least 1", 0);
    }
  } else if (mode == "words") {
    if (val !== null && !isNaN(val) && val >= 0) {
      setWordCount(val);
      manualRestart = true;
      restartTest();
      if (val > 2000) {
        Notifications.add("Stay safe and take breaks!", 0);
      } else if (val == 0) {
        Notifications.add(
          "Infinite words! Make sure to use Bail Out from the command line to save your result.",
          0,
          7
        );
      }
    } else {
      Notifications.add("Custom word amount must be at least 1", 0);
    }
  }

  hideCustomMode2Popup();
}

$(document).on("click", "#top .logo", (e) => {
  changePage("test");
});

$(document).on("click", "#top .config .wordCount .text-button", (e) => {
  const wrd = $(e.currentTarget).attr("wordCount");
  if (wrd == "custom") {
    showCustomMode2Popup("words");
  } else {
    setWordCount(wrd);
    manualRestart = true;
    restartTest();
  }
});

$(document).on("click", "#top .config .time .text-button", (e) => {
  let mode = $(e.currentTarget).attr("timeConfig");
  if (mode == "custom") {
    showCustomMode2Popup("time");
  } else {
    setTimeConfig(mode);
    manualRestart = true;

    restartTest();
  }
});

$(document).on("click", "#top .config .quoteLength .text-button", (e) => {
  let len = $(e.currentTarget).attr("quoteLength");
  setQuoteLength(len);
  manualRestart = true;
  restartTest();
});

$(document).on("click", "#top .config .customText .text-button", () => {
  showCustomTextPopup();
});

$(document).on("click", "#top .config .punctuationMode .text-button", () => {
  togglePunctuation();
  manualRestart = true;

  restartTest();
});

$(document).on("click", "#top .config .numbersMode .text-button", () => {
  toggleNumbers();
  manualRestart = true;

  restartTest();
});

$("#wordsWrapper").on("click", () => {
  focusWords();
});

$(document).on("click", "#top .config .mode .text-button", (e) => {
  if ($(e.currentTarget).hasClass("active")) return;
  const mode = $(e.currentTarget).attr("mode");
  setMode(mode);
  manualRestart = true;
  restartTest();
});

$(document).on("click", "#top #menu .icon-button", (e) => {
  if ($(e.currentTarget).hasClass("discord")) return;
  if ($(e.currentTarget).hasClass("leaderboards")) {
    showLeaderboards();
  } else {
    const href = $(e.currentTarget).attr("href");
    manualRestart = true;
    changePage(href.replace("/", ""));
  }
});

$(window).on("popstate", (e) => {
  let state = e.originalEvent.state;
  if (state == "" || state == "/") {
    // show test
    changePage("test");
  } else if (state == "about") {
    // show about
    changePage("about");
  } else if (state == "account" || state == "login") {
    if (firebase.auth().currentUser) {
      changePage("account");
    } else {
      changePage("login");
    }
  }
});

$(document).on("keypress", "#restartTestButton", (event) => {
  if (event.keyCode == 13) {
    if (
      (config.mode === "words" && config.words < 1000) ||
      (config.mode === "time" && config.time < 3600) ||
      config.mode === "quote" ||
      (config.mode === "custom" &&
        customText.isWordRandom &&
        customText.word < 1000) ||
      (config.mode === "custom" &&
        customText.isTimeRandom &&
        customText.time < 3600) ||
      (config.mode === "custom" &&
        !customText.isWordRandom &&
        customText.text.length < 1000)
    ) {
      if (testActive) {
        let testNow = performance.now();
        let testSeconds = Misc.roundTo2((testNow - testStart) / 1000);
        let afkseconds = keypressPerSecond.filter(
          (x) => x.count == 0 && x.mod == 0
        ).length;
        incompleteTestSeconds += testSeconds - afkseconds;
        restartCount++;
      }
      if (resultCalculating) return;
      restartTest();
    } else {
      Notifications.add("Quick restart disabled for long tests", 0);
    }
  }
});

$(document.body).on("click", "#restartTestButton", () => {
  manualRestart = true;
  if (resultCalculating) return;
  restartTest();
});

function initPractiseMissedWords() {
  let mode = modeBeforePractise === null ? config.mode : modeBeforePractise;
  let punctuation =
    punctuationBeforePractise === null
      ? config.punctuation
      : punctuationBeforePractise;
  let numbers =
    numbersBeforePractise === null ? config.numbers : numbersBeforePractise;
  setMode("custom");
  let newCustomText = [];
  Object.keys(missedWords).forEach((missedWord) => {
    for (let i = 0; i < missedWords[missedWord]; i++) {
      newCustomText.push(missedWord);
    }
  });
  customText.text = newCustomText;
  customText.isWordRandom = true;
  customText.word = 50;

  modeBeforePractise = null;
  punctuationBeforePractise = null;
  numbersBeforePractise = null;
  restartTest();
  modeBeforePractise = mode;
  punctuationBeforePractise = punctuation;
  numbersBeforePractise = numbers;
}

$(document).on("keypress", "#practiseMissedWordsButton", (event) => {
  if (event.keyCode == 13) {
    if (Object.keys(missedWords).length > 0) {
      initPractiseMissedWords();
    } else {
      Notifications.add("You haven't missed any words.", 0);
    }
  }
});

$(document.body).on("click", "#practiseMissedWordsButton", () => {
  if (Object.keys(missedWords).length > 0) {
    initPractiseMissedWords();
  } else {
    Notifications.add("You haven't missed any words.", 0);
  }
});

$(document).on("keypress", "#nextTestButton", (event) => {
  if (event.keyCode == 13) {
    restartTest();
  }
});

$(document.body).on("click", "#nextTestButton", () => {
  manualRestart = true;
  restartTest();
});

$(document).on("keypress", "#showWordHistoryButton", (event) => {
  if (event.keyCode == 13) {
    toggleResultWordsDisplay();
  }
});

$(document.body).on("click", "#showWordHistoryButton", () => {
  toggleResultWordsDisplay();
});

$(document.body).on("click", "#restartTestButtonWithSameWordset", () => {
  manualRestart = true;

  restartTest(true);
});

$(document).on("keypress", "#restartTestButtonWithSameWordset", (event) => {
  if (event.keyCode == 13) {
    restartTest(true);
  }
});

$(document.body).on("click", "#copyResultToClipboardButton", () => {
  copyResultToClipboard();
});

$(document.body).on("click", ".version", () => {
  $("#versionHistoryWrapper")
    .css("opacity", 0)
    .removeClass("hidden")
    .animate({ opacity: 1 }, 125);
});

$(document.body).on("click", "#versionHistoryWrapper", () => {
  $("#versionHistoryWrapper")
    .css("opacity", 1)
    .animate({ opacity: 0 }, 125, () => {
      $("#versionHistoryWrapper").addClass("hidden");
    });
});

$(document.body).on("click", "#supportMeButton", () => {
  $("#supportMeWrapper")
    .css("opacity", 0)
    .removeClass("hidden")
    .animate({ opacity: 1 }, 125);
});

$(document.body).on("click", "#supportMeWrapper", () => {
  $("#supportMeWrapper")
    .css("opacity", 1)
    .animate({ opacity: 0 }, 125, () => {
      $("#supportMeWrapper").addClass("hidden");
    });
});

$(document.body).on("click", "#supportMeWrapper .button.ads", () => {
  currentCommands.push(commandsEnableAds);
  showCommandLine();
  $("#supportMeWrapper")
    .css("opacity", 1)
    .animate({ opacity: 0 }, 125, () => {
      $("#supportMeWrapper").addClass("hidden");
    });
});

$(document.body).on("click", "#supportMeWrapper a.button", () => {
  $("#supportMeWrapper")
    .css("opacity", 1)
    .animate({ opacity: 0 }, 125, () => {
      $("#supportMeWrapper").addClass("hidden");
    });
});

$(document.body).on("click", ".pageAbout .aboutEnableAds", () => {
  currentCommands.push(commandsEnableAds);
  showCommandLine();
});

$("#wordsInput").keypress((event) => {
  event.preventDefault();
});

let outOfFocusTimeouts = [];

function clearTimeouts(timeouts) {
  timeouts.forEach((to) => {
    clearTimeout(to);
    to = null;
  });
}

$("#wordsInput").on("focus", () => {
  if (!resultVisible && config.showOutOfFocusWarning) {
    $("#words").css("transition", "none").removeClass("blurred");
    $(".outOfFocusWarning").addClass("hidden");
    clearTimeouts(outOfFocusTimeouts);
  }
  showCaret();
});

$("#wordsInput").on("focusout", () => {
  if (!resultVisible && config.showOutOfFocusWarning) {
    outOfFocusTimeouts.push(
      setTimeout(() => {
        $("#words").css("transition", "0.25s").addClass("blurred");
        $(".outOfFocusWarning").removeClass("hidden");
      }, 1000)
    );
  }
  hideCaret();
});

$(window).resize(() => {
  updateCaretPosition();
});

$(document).mousemove(function (event) {
  if (
    $("#top").hasClass("focus") &&
    (event.originalEvent.movementX > 0 || event.originalEvent.movementY > 0)
  ) {
    setFocus(false);
  }
});

$(document).on("click", "#testModesNotice .text-button", (event) => {
  let commands = eval($(event.currentTarget).attr("commands"));
  if (commands !== undefined) {
    if ($(event.currentTarget).attr("commands") === "commandsTags") {
      updateCommandsTagsList();
    }
    currentCommands.push(commands);
    showCommandLine();
  }
});

$(document).on("click", "#commandLineMobileButton", () => {
  showCommandLine();
});

let dontInsertSpace = false;

$(document).keyup(() => {
  if (resultVisible) return;
  let now = performance.now();
  let diff = Math.abs(keypressStats.duration.current - now);
  if (keypressStats.duration.current !== -1) {
    keypressStats.duration.array.push(diff);
  }
  keypressStats.duration.current = now;
  Monkey.stop();
});

$(document).keydown(function (event) {
  if (!resultVisible) {
    let now = performance.now();
    let diff = Math.abs(keypressStats.spacing.current - now);
    if (keypressStats.spacing.current !== -1) {
      keypressStats.spacing.array.push(diff);
    }
    keypressStats.spacing.current = now;
  }

  Monkey.type();

  //autofocus
  let pageTestActive = !$(".pageTest").hasClass("hidden");
  let commandLineVisible = !$("#commandLineWrapper").hasClass("hidden");
  let wordsFocused = $("#wordsInput").is(":focus");
  let modePopupVisible =
    !$("#customTextPopupWrapper").hasClass("hidden") ||
    !$("#customMode2PopupWrapper").hasClass("hidden");
  if (
    pageTestActive &&
    !commandLineVisible &&
    !modePopupVisible &&
    !resultVisible &&
    !wordsFocused &&
    event.key !== "Enter"
  ) {
    focusWords();
    if (config.showOutOfFocusWarning) return;
  }

  //tab
  if (
    (event.key == "Tab" && !config.swapEscAndTab) ||
    (event.key == "Escape" && config.swapEscAndTab)
  ) {
    handleTab(event);
    // event.preventDefault();
  }

  //blocking firefox from going back in history with backspace
  if (event.key === "Backspace" && wordsFocused) {
    let t = /INPUT|SELECT|TEXTAREA/i;
    if (
      !t.test(event.target.tagName) ||
      event.target.disabled ||
      event.target.readOnly
    ) {
      event.preventDefault();
    }
  }

  keypressStats.duration.current = performance.now();
  try {
    if (
      !config.capsLockBackspace &&
      event.originalEvent.getModifierState("CapsLock")
    ) {
      showCapsWarning();
    } else {
      hideCapsWarning();
    }
  } catch {}

  //backspace
  const isBackspace =
    event.key === "Backspace" ||
    (config.capsLockBackspace && event.key === "CapsLock");
  if (isBackspace && wordsFocused) {
    handleBackspace(event);
  }

  if (event.key === "Enter" && activeFunBox === "58008" && wordsFocused) {
    event.key = " ";
  }

  //space or enter
  if (event.key === " " && wordsFocused) {
    handleSpace(event, false);
  }

  if (wordsFocused && !commandLineVisible) {
    handleAlpha(event);
  }

  let acc = Misc.roundTo2(
    (accuracyStats.correct /
      (accuracyStats.correct + accuracyStats.incorrect)) *
      100
  );
  updateLiveAcc(acc);
});

function handleTab(event) {
  if (resultCalculating) {
    event.preventDefault();
  }
  if ($("#customTextPopup .textarea").is(":focus")) {
    event.preventDefault();

    let area = $("#customTextPopup .textarea")[0];

    var start = area.selectionStart;
    var end = area.selectionEnd;

    // set textarea value to: text before caret + tab + text after caret
    area.value =
      area.value.substring(0, start) + "\t" + area.value.substring(end);

    // put caret at right position again
    area.selectionStart = area.selectionEnd = start + 1;

    // event.preventDefault();
    // $("#customTextPopup .textarea").val(
    //   $("#customTextPopup .textarea").val() + "\t"
    // );
    return;
  } else if (
    !event.ctrlKey &&
    ((!event.shiftKey && !textHasTab) ||
      (event.shiftKey && textHasTab) ||
      resultVisible) &&
    config.quickTab &&
    !$(".pageLogin").hasClass("active") &&
    !resultCalculating &&
    $("#commandLineWrapper").hasClass("hidden") &&
    $("#simplePopupWrapper").hasClass("hidden")
  ) {
    event.preventDefault();
    if ($(".pageTest").hasClass("active")) {
      if (
        (config.mode === "words" && config.words < 1000) ||
        (config.mode === "time" && config.time < 3600) ||
        config.mode === "quote" ||
        (config.mode === "custom" &&
          customText.isWordRandom &&
          customText.word < 1000) ||
        (config.mode === "custom" &&
          customText.isTimeRandom &&
          customText.time < 3600) ||
        (config.mode === "custom" &&
          !customText.isWordRandom &&
          customText.text.length < 1000)
      ) {
        if (testActive) {
          let testNow = performance.now();
          let testSeconds = Misc.roundTo2((testNow - testStart) / 1000);
          let afkseconds = keypressPerSecond.filter(
            (x) => x.count == 0 && x.mod == 0
          ).length;
          incompleteTestSeconds += testSeconds - afkseconds;
          restartCount++;
        }
        restartTest();
      } else {
        Notifications.add("Quick restart disabled for long tests", 0);
      }
    } else {
      changePage("test");
    }
  } else if (
    !config.quickTab &&
    textHasTab &&
    event.shiftKey &&
    !resultVisible
  ) {
    event.preventDefault();
    $("#restartTestButton").focus();
  }
}

function handleBackspace(event) {
  event.preventDefault();
  if (!testActive) return;
  if (
    currentInput == "" &&
    inputHistory.length > 0 &&
    currentWordElementIndex > 0
  ) {
    //if nothing is inputted and its not the first word
    if (
      (inputHistory[currentWordIndex - 1] == wordsList[currentWordIndex - 1] &&
        !config.freedomMode) ||
      $($(".word")[currentWordIndex - 1]).hasClass("hidden")
    ) {
      return;
    } else {
      if (config.confidenceMode === "on" || config.confidenceMode === "max")
        return;
      if (event["ctrlKey"] || event["altKey"]) {
        currentInput = "";
        inputHistory.pop();
        correctedHistory.pop();
      } else {
        currentInput = inputHistory.pop();
        currentCorrected = correctedHistory.pop();
        if (activeFunBox === "nospace") {
          currentInput = currentInput.substring(0, currentInput.length - 1);
        }
      }
      currentWordIndex--;
      currentWordElementIndex--;
      updateActiveElement();
      updateWordElement(!config.blindMode);
    }
  } else {
    if (config.confidenceMode === "max") return;
    if (event["ctrlKey"] || event["altKey"]) {
      let split = currentInput.replace(/ +/g, " ").split(" ");
      if (split[split.length - 1] == "") {
        split.pop();
      }
      let addspace = false;
      if (split.length > 1) {
        addspace = true;
      }
      split.pop();
      currentInput = split.join(" ");

      if (addspace) {
        currentInput += " ";
      }
    } else if (event.metaKey) {
      currentInput = "";
    } else {
      currentInput = currentInput.substring(0, currentInput.length - 1);
    }
    updateWordElement(!config.blindMode);
  }
  playClickSound();
  if (config.keymapMode === "react") {
    flashPressedKeymapKey(event.code, true);
  } else if (config.keymapMode === "next") {
    updateHighlightedKeymapKey();
  }
  updateCaretPosition();
}

function handleSpace(event, isEnter) {
  if (!testActive) return;
  if (currentInput === "") return;
  let nextWord = wordsList[currentWordIndex + 1];
  // if ((isEnter && nextWord !== "\n") && (isEnter && activeFunBox !== "58008")) return;
  // if (!isEnter && nextWord === "\n") return;
  event.preventDefault();
  let currentWord = wordsList[currentWordIndex];
  if (activeFunBox === "layoutfluid" && config.mode !== "time") {
    const layouts = ["qwerty", "dvorak", "colemak"];
    let index = 0;
    let outof = wordsList.length;
    index = Math.floor((inputHistory.length + 1) / (outof / 3));
    if (config.layout !== layouts[index] && layouts[index] !== undefined) {
      Notifications.add(`--- !!! ${layouts[index]} !!! ---`, 0);
    }
    setLayout(layouts[index]);
    setKeymapLayout(layouts[index]);
    updateHighlightedKeymapKey();
    settingsGroups.layout.updateButton();
  }
  if (config.blindMode) $("#words .word.active letter").addClass("correct");
  dontInsertSpace = true;
  if (currentWord == currentInput) {
    //correct word
    if (
      paceCaret !== null &&
      paceCaret.wordsStatus[currentWordIndex] === true &&
      !config.blindMode
    ) {
      paceCaret.wordsStatus[currentWordIndex] = undefined;
      paceCaret.correction -= currentWord.length + 1;
    }
    accuracyStats.correct++;
    inputHistory.push(currentInput);
    currentInput = "";
    currentWordIndex++;
    currentWordElementIndex++;
    updateActiveElement();
    updateCaretPosition();
    currentKeypress.count++;
    currentKeypress.words.push(currentWordIndex);
    if (activeFunBox !== "nospace") {
      playClickSound();
    }
  } else {
    //incorrect word
    if (
      paceCaret !== null &&
      paceCaret.wordsStatus[currentWordIndex] === undefined &&
      !config.blindMode
    ) {
      paceCaret.wordsStatus[currentWordIndex] = true;
      paceCaret.correction += currentWord.length + 1;
    }
    if (activeFunBox !== "nospace") {
      if (!config.playSoundOnError || config.blindMode) {
        playClickSound();
      } else {
        playErrorSound();
      }
    }
    accuracyStats.incorrect++;
    let cil = currentInput.length;
    if (cil <= wordsList[currentWordIndex].length) {
      if (cil >= currentCorrected.length) {
        currentCorrected += "_";
      } else {
        currentCorrected =
          currentCorrected.substring(0, cil) +
          "_" +
          currentCorrected.substring(cil + 1);
      }
    }
    if (config.stopOnError != "off") {
      if (config.difficulty == "expert" || config.difficulty == "master") {
        //failed due to diff when pressing space
        failTest();
        return;
      }
      if (config.stopOnError == "word") {
        currentInput += " ";
        updateWordElement(true);
        updateCaretPosition();
      }
      return;
    }
    inputHistory.push(currentInput);
    highlightBadWord(currentWordElementIndex, !config.blindMode);
    currentInput = "";
    currentWordIndex++;
    currentWordElementIndex++;
    updateActiveElement();
    updateCaretPosition();
    currentKeypress.count++;
    currentKeypress.words.push(currentWordIndex);
    if (config.difficulty == "expert" || config.difficulty == "master") {
      failTest();
      return;
    } else if (currentWordIndex == wordsList.length) {
      //submitted last word that is incorrect
      lastSecondNotRound = true;
      showResult();
      return;
    }
  }

  correctedHistory.push(currentCorrected);
  currentCorrected = "";

  if (
    !config.showAllLines ||
    config.mode == "time" ||
    (customText.isWordRandom && customText.word == 0) ||
    customText.isTimeRandom
  ) {
    let currentTop = Math.floor(
      document.querySelectorAll("#words .word")[currentWordElementIndex - 1]
        .offsetTop
    );
    let nextTop;
    try {
      nextTop = Math.floor(
        document.querySelectorAll("#words .word")[currentWordElementIndex]
          .offsetTop
      );
    } catch (e) {
      nextTop = 0;
    }

    if ((nextTop > currentTop || activeWordJumped) && !lineTransition) {
      //last word of the line
      if (currentTestLine > 0) {
        let hideBound = currentTop;
        if (activeWordJumped) {
          hideBound = activeWordTopBeforeJump;
        }
        activeWordJumped = false;

        let toHide = [];
        let wordElements = $("#words .word");
        for (let i = 0; i < currentWordElementIndex; i++) {
          if ($(wordElements[i]).hasClass("hidden")) continue;
          let forWordTop = Math.floor(wordElements[i].offsetTop);
          if (forWordTop < hideBound - 10) {
            toHide.push($($("#words .word")[i]));
          }
        }
        const wordHeight = $(document.querySelector(".word")).outerHeight(true);
        if (config.smoothLineScroll && toHide.length > 0) {
          lineTransition = true;
          $("#words").prepend(
            `<div class="smoothScroller" style="position: fixed;height:${wordHeight}px;width:100%"></div>`
          );
          $("#words .smoothScroller").animate(
            {
              height: 0,
            },
            125,
            () => {
              $("#words .smoothScroller").remove();
            }
          );
          $("#paceCaret").animate(
            {
              top: document.querySelector("#paceCaret").offsetTop - wordHeight,
            },
            125
          );
          $("#words").animate(
            {
              marginTop: `-${wordHeight}px`,
            },
            125,
            () => {
              activeWordTop = document.querySelector("#words .active")
                .offsetTop;

              currentWordElementIndex -= toHide.length;
              lineTransition = false;
              toHide.forEach((el) => el.remove());
              $("#words").css("marginTop", "0");
            }
          );
        } else {
          toHide.forEach((el) => el.remove());
          currentWordElementIndex -= toHide.length;
          $("#paceCaret").css({
            top: document.querySelector("#paceCaret").offsetTop - wordHeight,
          });
        }
      }
      currentTestLine++;
    }
  } //end of line wrap

  updateCaretPosition();

  if (config.keymapMode === "react") {
    flashPressedKeymapKey(event.code, true);
  } else if (config.keymapMode === "next") {
    updateHighlightedKeymapKey();
  }
  if (
    config.mode === "words" ||
    config.mode === "custom" ||
    config.mode === "quote"
  ) {
    updateTimer();
  }
  if (
    config.mode == "time" ||
    config.mode == "words" ||
    config.mode == "custom"
  ) {
    addWord();
  }
}

function handleAlpha(event) {
  if (
    [
      "ContextMenu",
      "Escape",
      "Shift",
      "Control",
      "Meta",
      "Alt",
      "AltGraph",
      "CapsLock",
      "Backspace",
      "PageUp",
      "PageDown",
      "Home",
      "ArrowUp",
      "ArrowLeft",
      "ArrowRight",
      "ArrowDown",
      "OS",
      "Insert",
      "Home",
      "Undefined",
      "Control",
      "Fn",
      "FnLock",
      "Hyper",
      "NumLock",
      "ScrollLock",
      "Symbol",
      "SymbolLock",
      "Super",
      "Unidentified",
      "Process",
      "Delete",
      undefined,
    ].includes(event.key)
  ) {
    currentKeypress.mod++;
    return;
  }

  //insert space for expert and master or strict space,
  //otherwise dont do anything
  if (event.key === " ") {
    if (config.difficulty !== "normal" || config.strictSpace) {
      if (dontInsertSpace) {
        dontInsertSpace = false;
        return;
      }
    } else {
      return;
    }
  }

  if (event.key === "Tab") {
    if (!textHasTab || (textHasTab && event.shiftKey)) {
      return;
    }
    event.key = "\t";
    event.preventDefault();
  }

  if (event.key === "Enter") {
    event.key = "\n";
  }

  // if (event.key.length > 1) return;
  if (/F\d+/.test(event.key)) return;
  if (/Numpad/.test(event.key)) return;
  if (/Volume/.test(event.key)) return;
  if (/Media/.test(event.key)) return;
  if (
    event.ctrlKey != event.altKey &&
    (event.ctrlKey || /Linux/.test(window.navigator.platform))
  )
    return;
  if (event.metaKey) return;
  event = emulateLayout(event);

  //start the test
  if (currentInput == "" && inputHistory.length == 0 && !testActive) {
    startTest();
  } else {
    if (!testActive) return;
  }

  setFocus(true);
  stopCaretAnimation();

  //show dead keys
  if (event.key === "Dead") {
    playClickSound();
    $(
      document.querySelector("#words .word.active").querySelectorAll("letter")[
        currentInput.length
      ]
    ).toggleClass("dead");
    return;
  }

  //check if the char typed was correct
  let thisCharCorrect;
  let nextCharInWord = wordsList[currentWordIndex].substring(
    currentInput.length,
    currentInput.length + 1
  );
  if (
    config.language === "russian" &&
    (event["key"].toLowerCase() == "e" || event["key"].toLowerCase() == "ё")
  ) {
    if (
      nextCharInWord.toLowerCase() == "e" ||
      nextCharInWord.toLowerCase() == "ё"
    ) {
      thisCharCorrect = true;
    } else {
      thisCharCorrect = false;
    }
  } else {
    if (nextCharInWord == event["key"]) {
      thisCharCorrect = true;
    } else {
      thisCharCorrect = false;
    }
  }

  if (!thisCharCorrect) {
    accuracyStats.incorrect++;
    currentError.count++;
    currentError.words.push(currentWordIndex);
    thisCharCorrect = false;
    if (!Object.keys(missedWords).includes(wordsList[currentWordIndex])) {
      missedWords[wordsList[currentWordIndex]] = 1;
    } else {
      missedWords[wordsList[currentWordIndex]]++;
    }
  } else {
    accuracyStats.correct++;
    thisCharCorrect = true;
  }

  if (thisCharCorrect) {
    playClickSound();
  } else {
    if (!config.playSoundOnError || config.blindMode) {
      playClickSound();
    } else {
      playErrorSound();
    }
  }

  //update current corrected verison. if its empty then add the current key. if its not then replace the last character with the currently pressed one / add it
  if (currentCorrected === "") {
    currentCorrected = currentInput + event["key"];
  } else {
    let cil = currentInput.length;
    if (cil >= currentCorrected.length) {
      currentCorrected += event["key"];
    } else if (!thisCharCorrect) {
      currentCorrected =
        currentCorrected.substring(0, cil) +
        event["key"] +
        currentCorrected.substring(cil + 1);
    }
  }

  currentKeypress.count++;
  currentKeypress.words.push(currentWordIndex);

  if (config.stopOnError == "letter" && !thisCharCorrect) {
    return;
  }

  //update the active word top, but only once
  if (currentInput.length === 1 && currentWordIndex === 0) {
    activeWordTop = document.querySelector("#words .active").offsetTop;
  }

  //max length of the input is 20
  if (currentInput.length < wordsList[currentWordIndex].length + 20) {
    currentInput += event["key"];
  }

  if (!thisCharCorrect && config.difficulty == "master") {
    failTest();
    return;
  }

  //keymap
  if (config.keymapMode === "react") {
    flashPressedKeymapKey(event.key, thisCharCorrect);
  } else if (config.keymapMode === "next") {
    updateHighlightedKeymapKey();
  }

  activeWordTopBeforeJump = activeWordTop;
  updateWordElement(!config.blindMode);

  //auto stop the test if the last word is correct
  let currentWord = wordsList[currentWordIndex];
  let lastindex = currentWordIndex;
  if (
    (currentWord == currentInput ||
      (config.quickEnd &&
        currentWord.length == currentInput.length &&
        config.stopOnError == "off")) &&
    lastindex == wordsList.length - 1
  ) {
    inputHistory.push(currentInput);
    currentInput = "";
    correctedHistory.push(currentCorrected);
    currentCorrected = "";
    lastSecondNotRound = true;
    showResult();
  }

  //simulate space press in nospace funbox
  if (
    (activeFunBox === "nospace" &&
      currentInput.length === wordsList[currentWordIndex].length) ||
    (event.key === "\n" && thisCharCorrect)
  ) {
    $.event.trigger({
      type: "keydown",
      which: " ".charCodeAt(0),
      key: " ",
    });
  }

  //check if the word jumped
  let newActiveTop = document.querySelector("#words .word.active").offsetTop;
  if (activeWordTopBeforeJump < newActiveTop && !lineTransition) {
    activeWordJumped = true;
  } else {
    activeWordJumped = false;
  }

  //stop the word jump by slicing off the last character, update word again
  if (activeWordJumped && currentInput.length > 1) {
    currentInput = currentInput.slice(0, -1);
    updateWordElement(!config.blindMode);
    activeWordJumped = false;
  }

  updateCaretPosition();
}

window.addEventListener("beforeunload", (event) => {
  // Cancel the event as stated by the standard.
  if (
    (config.mode === "words" && config.words < 1000) ||
    (config.mode === "time" && config.time < 3600) ||
    config.mode === "quote" ||
    (config.mode === "custom" &&
      customText.isWordRandom &&
      customText.word < 1000) ||
    (config.mode === "custom" &&
      customText.isTimeRandom &&
      customText.time < 1000) ||
    (config.mode === "custom" &&
      !customText.isWordRandom &&
      customText.text.length < 1000)
  ) {
  } else {
    if (testActive) {
      event.preventDefault();
      // Chrome requires returnValue to be set.
      event.returnValue = "";
    }
  }
});

if (firebase.app().options.projectId === "monkey-type-dev-67af4") {
  $("#top .logo .bottom").text("monkey-dev");
  $("head title").text("Monkey Dev");
  $("body").append(
    `<div class="devIndicator tr">DEV</div><div class="devIndicator bl">DEV</div>`
  );
}

if (window.location.hostname === "localhost") {
  window.onerror = function (error) {
    Notifications.add(error, -1);
  };
  $("#top .logo .top").text("localhost");
  $("head title").text($("head title").text() + " (localhost)");
  firebase.functions().useFunctionsEmulator("http://localhost:5001");
  $("body").append(
    `<div class="devIndicator tl">local</div><div class="devIndicator br">local</div>`
  );
}

manualRestart = true;
loadConfigFromCookie();
Misc.getReleasesFromGitHub();
// getPatreonNames();

$(document).on("mouseenter", "#resultWordsHistory .words .word", (e) => {
  if (resultVisible) {
    let input = $(e.currentTarget).attr("input");
    if (input != undefined)
      $(e.currentTarget).append(
        `<div class="wordInputAfter">${input
          .replace(/\t/g, "_")
          .replace(/\n/g, "_")}</div>`
      );
  }
});

$(document).on("click", "#bottom .leftright .right .current-theme", (e) => {
  if (e.shiftKey) {
    togglePresetCustomTheme();
  } else {
    // if (config.customTheme) {
    //   togglePresetCustomTheme();
    // }
    currentCommands.push(commandsThemes);
    showCommandLine();
  }
});

$(document).on("click", ".keymap .r5 #KeySpace", (e) => {
  currentCommands.push(commandsKeymapLayouts);
  showCommandLine();
});

$(document).on("mouseleave", "#resultWordsHistory .words .word", (e) => {
  $(".wordInputAfter").remove();
});

$("#wpmChart").on("mouseleave", (e) => {
  $(".wordInputAfter").remove();
});

$(document).ready(() => {
  updateFavicon(32, 14);
  $("body").css("transition", ".25s");
  if (config.quickTab) {
    $("#restartTestButton").addClass("hidden");
  }
  if (!Misc.getCookie("merchbannerclosed")) {
    $(".merchBanner").removeClass("hidden");
  } else {
    $(".merchBanner").remove();
  }
  $("#centerContent")
    .css("opacity", "0")
    .removeClass("hidden")
    .stop(true, true)
    .animate({ opacity: 1 }, 250, () => {
      if (window.location.pathname === "/verify") {
        const fragment = new URLSearchParams(window.location.hash.slice(1));
        if (fragment.has("access_token")) {
          const accessToken = fragment.get("access_token");
          const tokenType = fragment.get("token_type");
          verifyUserWhenLoggedIn = {
            accessToken: accessToken,
            tokenType: tokenType,
          };
          history.replaceState("/", null, "/");
        }
      } else if (window.location.pathname === "/account") {
        history.replaceState("/", null, "/");
      } else if (/challenge_.+/g.test(window.location.pathname)) {
        //do nothing
      } else if (window.location.pathname !== "/") {
        let page = window.location.pathname.replace("/", "");
        changePage(page);
      }
    });
});

$(".scrollToTopButton").click((event) => {
  window.scrollTo(0, 0);
});

$(".merchBanner a").click((event) => {
  $(".merchBanner").remove();
  Misc.setCookie("merchbannerclosed", true, 365);
});

$(".merchBanner .fas").click((event) => {
  $(".merchBanner").remove();
  Misc.setCookie("merchbannerclosed", true, 365);
  Notifications.add(
    "Won't remind you anymore. Thanks for continued support <3",
    0,
    5
  );
});

$(".pageTest #copyWordsListButton").click(async (event) => {
  try {
    await navigator.clipboard.writeText(
      wordsList.slice(0, inputHistory.length).join(" ")
    );
    Notifications.add("Copied to clipboard", 0, 2);
  } catch (e) {
    Notifications.add("Could not copy to clipboard: " + e, -1);
  }
});

//stop space scrolling
window.addEventListener("keydown", function (e) {
  if (e.keyCode == 32 && e.target == document.body) {
    e.preventDefault();
  }
});

async function setupChallenge(challengeName) {
  let list = await Misc.getChallengeList();
  let challenge = list.filter((c) => c.name === challengeName)[0];
  let notitext;
  try {
    if (challenge === undefined) {
      throw "Challenge not found";
    }
    if (challenge.type === "customTime") {
      setTimeConfig(challenge.parameters[0], true);
      setMode("time", true);
      setDifficulty("normal", true);
      if (challenge.name === "englishMaster") {
        setLanguage("english_10k", true);
        setNumbers(true, true);
        setPunctuation(true, true);
      }
    }
    if (challenge.type === "customWords") {
      setWordCount(challenge.parameters[0], true);
      setMode("words", true);
      setDifficulty("normal", true);
    } else if (challenge.type === "customText") {
      customText.text = challenge.parameters[0].split(" ");
      customText.isWordRandom = challenge.parameters[1];
      customText.word = parseInt(challenge.parameters[2]);
      setMode("custom", true);
      setDifficulty("normal", true);
    } else if (challenge.type === "script") {
      let scriptdata = await fetch("/challenges/" + challenge.parameters[0]);
      scriptdata = await scriptdata.text();
      let text = scriptdata.trim();
      text = text.replace(/[\n\r\t ]/gm, " ");
      text = text.replace(/ +/gm, " ");
      customText.text = text.split(" ");
      customText.isWordRandom = false;
      setMode("custom", true);
      setDifficulty("normal", true);
      if (challenge.parameters[1] != null) {
        setTheme(challenge.parameters[1]);
      }
      if (challenge.parameters[2] != null) {
        activateFunbox(challenge.parameters[2]);
      }
    } else if (challenge.type === "accuracy") {
      setTimeConfig(0, true);
      setMode("time", true);
      setDifficulty("master", true);
    } else if (challenge.type === "funbox") {
      activateFunbox(challenge.parameters[0]);
      setDifficulty("normal", true);
      if (challenge.parameters[1] === "words") {
        setWordCount(challenge.parameters[2], true);
      } else if (challenge.parameters[1] === "time") {
        setTimeConfig(challenge.parameters[2], true);
      }
      setMode(challenge.parameters[1], true);
      if (challenge.parameters[3] !== undefined) {
        setDifficulty(challenge.parameters[3], true);
      }
    }
    manualRestart = true;
    restartTest(false, true);
    notitext = challenge.message;
    if (notitext === undefined) {
      Notifications.add(`Challenge '${challengeName}' loaded.`, 0);
    } else {
      Notifications.add("Challenge loaded. " + notitext, 0);
    }
  } catch (e) {
    Notifications.add("Something went wrong: " + e, -1);
  }
}

let ctx = $("#wpmChart");
let wpmOverTimeChart = new Chart(ctx, {
  type: "line",
  data: {
    labels: [],
    datasets: [
      {
        label: "wpm",
        data: [],
        borderColor: "rgba(125, 125, 125, 1)",
        borderWidth: 2,
        yAxisID: "wpm",
        order: 2,
        radius: 2,
      },
      {
        label: "raw",
        data: [],
        borderColor: "rgba(125, 125, 125, 1)",
        borderWidth: 2,
        yAxisID: "raw",
        order: 3,
        radius: 2,
      },
      {
        label: "errors",
        data: [],
        borderColor: "rgba(255, 125, 125, 1)",
        pointBackgroundColor: "rgba(255, 125, 125, 1)",
        borderWidth: 2,
        order: 1,
        yAxisID: "error",
        maxBarThickness: 10,
        type: "scatter",
        pointStyle: "crossRot",
        radius: function (context) {
          var index = context.dataIndex;
          var value = context.dataset.data[index];
          return value <= 0 ? 0 : 3;
        },
        pointHoverRadius: function (context) {
          var index = context.dataIndex;
          var value = context.dataset.data[index];
          return value <= 0 ? 0 : 5;
        },
      },
    ],
  },
  options: {
    tooltips: {
      titleFontFamily: "Roboto Mono",
      bodyFontFamily: "Roboto Mono",
      mode: "index",
      intersect: false,
      callbacks: {
        afterLabel: function (ti, data) {
          try {
            $(".wordInputAfter").remove();

            let wordsToHighlight =
              keypressPerSecond[parseInt(ti.xLabel) - 1].words;

            let unique = [...new Set(wordsToHighlight)];
            unique.forEach((wordIndex) => {
              let wordEl = $($("#resultWordsHistory .words .word")[wordIndex]);
              let input = wordEl.attr("input");
              if (input != undefined)
                wordEl.append(`<div class="wordInputAfter">${input}</div>`);
            });
          } catch (e) {}
        },
      },
    },
    legend: {
      display: false,
      labels: {
        defaultFontFamily: "Roboto Mono",
      },
    },
    responsive: true,
    maintainAspectRatio: false,
    scales: {
      xAxes: [
        {
          ticks: {
            fontFamily: "Roboto Mono",
            autoSkip: true,
            autoSkipPadding: 40,
          },
          display: true,
          scaleLabel: {
            display: false,
            labelString: "Seconds",
            fontFamily: "Roboto Mono",
          },
        },
      ],
      yAxes: [
        {
          id: "wpm",
          display: true,
          scaleLabel: {
            display: true,
            labelString: "Words per Minute",
            fontFamily: "Roboto Mono",
          },
          ticks: {
            fontFamily: "Roboto Mono",
            beginAtZero: true,
            min: 0,
            autoSkip: true,
            autoSkipPadding: 40,
          },
          gridLines: {
            display: true,
          },
        },
        {
          id: "raw",
          display: false,
          scaleLabel: {
            display: true,
            labelString: "Raw Words per Minute",
            fontFamily: "Roboto Mono",
          },
          ticks: {
            fontFamily: "Roboto Mono",
            beginAtZero: true,
            min: 0,
            autoSkip: true,
            autoSkipPadding: 40,
          },
          gridLines: {
            display: false,
          },
        },
        {
          id: "error",
          display: true,
          position: "right",
          scaleLabel: {
            display: true,
            labelString: "Errors",
            fontFamily: "Roboto Mono",
          },
          ticks: {
            precision: 0,
            fontFamily: "Roboto Mono",
            beginAtZero: true,
            autoSkip: true,
            autoSkipPadding: 40,
          },
          gridLines: {
            display: false,
          },
        },
      ],
    },
    annotation: {
      annotations: [],
    },
  },
});
