<commit_before>const labels = require('./labels');
const textTools = require('./textTools');
const mapTools = require('./mapTools');
const storage = require('./storage');
const layoutChanger = require('./layoutChanger');
const socketHandler = require('./socketHandler');
const messenger = require('./messenger');
const commandHandler = require('./commandHandler');
const domManipulator = require('./domManipulator');
const clickHandler = require('./clickHandler');
const videoPlayer = require('./videoPlayer');

/**
 * Queue of all the commands used by the user that will be handled and printed
 * @type {Object[]}
 */
const commandQueue = [];
/**
 * Check every * amount of milliseconds to see if Javascript is still responding
 * It will trigger a function if the response is delayed
 * @type {Number}
 */
const screenOffIntervalTime = 1000;
/**
 * Get GPS coordinates for * amount of milliseconds
 * @type {Number}
 */
const watchPositionTime = 15000;
/**
 * Get GPS coordinates every * milliseconds
 * @type {Number}
 */
const pausePositionTime = 40000;
/**
 * Queue of all sounds to be consumed and played
 * @type {Object[]}
 */
const soundQueue = [];
/**
 * Timeout between each command to be run
 * @type {Number}
 */
const commandTime = 1000;
const dot = '.';
const dash = '-';
const triggerKeysPressed = [];
/**
 * Symbolizes space between words in morse string
 * @type {string}
 */
const morseSeparator = '#';
let audioCtx;
let oscillator;
let gainNode;
let soundTimeout = 0;
let previousCommandPointer;
let watchId = null;
// Is geolocation tracking on?
let isTracking = false;
let firstConnection = true;
let positions = [];
/**
 * Used by isScreenOff() to force reconnect when phone screen is off
 * for a longer period of time
 */
let lastScreenOff = (new Date()).getTime();
let commmandUsed = false;
/**
 * Used to block repeat of key presses
 */
let keyPressed;
let trackingInterval;
let isScreenOffInterval;
let serverDownTimeout;

function queueCommand(command, data, commandMsg) {
  commandQueue.push({
    command,
    data,
    commandMsg,
  });
}

function pushCommandHistory(command) {
  const commandHistory = storage.getCommandHistory();

  commandHistory.push(command);
  storage.setCommandHistory(commandHistory);
}

function enterRoom(roomName) {
  storage.setRoom(roomName);

  if (!storage.getStaticInputStart()) {
    domManipulator.setInputStart(roomName);
  }

  messenger.queueMessage({
    text: [`Entered ${roomName}`],
    text_se: [`Gick in i ${roomName}`],
  });
}

function resetPreviousCommandPointer() {
  const commandHistory = storage.getCommandHistory();

  previousCommandPointer = commandHistory ? commandHistory.length : 0;
}

function setGain(value) {
  gainNode.gain.value = value;
}

function playMorse(morseCode, silent) {
  function finishSoundQueue(timeouts) {
    const cleanMorse = morseCode.replace(/#/g, '');

    soundQueue.splice(0, timeouts);

    if (!silent) {
      messenger.queueMessage({
        text: [`Morse code message received: ${cleanMorse}`],
        text_se: [`Morse mottaget: ${cleanMorse}`],
      });
    }
  }

  let duration;
  let shouldPlay;

  if (soundQueue.length === 0) {
    soundTimeout = 0;
  }

  for (const code of morseCode) {
    shouldPlay = false;
    duration = 0;

    if (dot === code) {
      duration = 50;
      shouldPlay = true;
    } else if (dash === code) {
      duration = 150;
      shouldPlay = true;
    } else if (morseSeparator === code) {
      duration = 50;
    } else {
      duration = 75;
    }

    if (shouldPlay) {
      soundQueue.push(setTimeout(setGain, soundTimeout, 1));
      soundQueue.push(setTimeout(setGain, soundTimeout + duration, 0));
    }

    soundTimeout += duration;
  }

  setTimeout(finishSoundQueue, soundTimeout, (2 * morseCode.length), morseCode);
}

/**
 * Geolocation object is empty when sent through Socket.IO
 * This is a fix for that
 * @param {object} position . -
 * @returns {object} Returns position
 */
function preparePosition(position) {
  const preparedPosition = {};
  preparedPosition.latitude = position.coords.latitude;
  preparedPosition.longitude = position.coords.longitude;
  preparedPosition.speed = position.coords.speed;
  preparedPosition.accuracy = position.coords.accuracy;
  preparedPosition.heading = position.coords.heading;
  preparedPosition.timestamp = position.timestamp;

  return preparedPosition; // geolocation
}

function retrievePosition() {
  const clearingWatch = () => {
    navigator.geolocation.clearWatch(watchId);
    watchId = null;
    trackingInterval = setTimeout(sendLocation, pausePositionTime); // eslint-disable-line no-use-before-define
  };

  watchId = navigator.geolocation.watchPosition((position) => {
    if (position !== undefined) {
      isTracking = true;
      positions.push(position);

      mapTools.setUserPosition({
        latitude: position.coords.latitude,
        longitude: position.coords.longitude,
      });
    }
  }, (err) => {
    console.log(err);
  }, { enableHighAccuracy: true });

  if (isTracking) {
    trackingInterval = setTimeout(clearingWatch, watchPositionTime);
  }
}

function sendLocation() {
  let mostAccuratePos;

  if (storage.getUser() !== null && positions.length > 0) {
    mostAccuratePos = positions[positions.length - 1];

    for (let i = positions.length - 2; i >= 0; i--) {
      const position = positions[i];
      const accuracy = positions[i].coords.accuracy;

      if (mostAccuratePos.coords.accuracy > accuracy) {
        mostAccuratePos = position;
      }
    }

    positions = [];

    socketHandler.emit('updateLocation', {
      type: 'user',
      position: preparePosition(mostAccuratePos),
    });
  }

  retrievePosition();
}

/**
 * Some devices disable Javascript when screen is off (iOS)
 * They also fail to notice that they have been disconnected
 * We check the time between heartbeats and if the time i
 * over 10 seconds (example: when screen is turned off and then on)
 * we force them to reconnect
 */
function isScreenOff() {
  const now = (new Date()).getTime();
  const diff = now - lastScreenOff;
  // FIXME Hard coded
  const offBy = diff - 1000;
  lastScreenOff = now;

  // FIXME Hard coded
  if (offBy > 10000) {
    socketHandler.reconnect();
  }
}

/**
 * Set intervals at boot and recreate them when the window is focused
 * This is to make sure that nothing has been killed in the background
 */
function setIntervals() {
  if (trackingInterval !== null) {
    clearTimeout(trackingInterval);
  }

  if (watchId !== null) {
    navigator.geolocation.clearWatch(watchId);
  }

  if (storage.getGpsTracking() && navigator.geolocation) {
    // Gets new geolocation data
    sendLocation();
  }

  // Should not be recreated on focus
  if (isScreenOffInterval === null) {
    /**
     * Checks time between when JS stopped and started working again
     * This will be most frequently triggered when a user turns off the
     * screen on their phone and turns it back on
     */
    isScreenOffInterval = setInterval(isScreenOff, screenOffIntervalTime);
  }
}

/**
 * Resets intervals and keyPressed (to not have it true after a user tabbed out and into the site)
 */
function refocus() {
  keyPressed = false;
  triggerKeysPressed.ctrl = false;
  triggerKeysPressed.alt = false;
  setIntervals();
}

function buildMorsePlayer() {
  // Not supported in Spartan nor IE11 or lower
  if (window.AudioContext || window.webkitAudioContext) {
    if (window.AudioContext) {
      audioCtx = new window.AudioContext();
    } else if (window.webkitAudioContext) {
      audioCtx = new window.webkitAudioContext(); // eslint-disable-line
    }

    oscillator = audioCtx.createOscillator();
    gainNode = audioCtx.createGain();

    oscillator.connect(gainNode);
    gainNode.connect(audioCtx.destination);

    gainNode.gain.value = 0;
    oscillator.type = 'sine';
    oscillator.frequency.value = '440';
    // oscillator.type = 'square';
    // oscillator.frequency.value = '300';

    oscillator.start(0);
  }
}

function triggerAutoComplete(text, textChar) {
  if (text.charAt(text.length - 1) === ' ' && textChar === ' ') {
    domManipulator.setCommandInput(textTools.trimSpace(text));

    return true;
  }

  return false;
}

function setCommandUsed(used) {
  commmandUsed = used;
}

function consumeCommandQueue() {
  if (commandQueue.length > 0) {
    const storedCommand = commandQueue.shift();
    const commmand = storedCommand.command;
    const commandMessage = storedCommand.commandMsg;

    if (commandMessage) {
      messenger.queueMessage(commandMessage);
    }

    setCommandUsed(true);
    commmand(storedCommand.data);
    setTimeout(consumeCommandQueue, commandTime);
  } else {
    setCommandUsed(false);
  }
}

function startCommandQueue() {
  if (!commmandUsed) {
    consumeCommandQueue();
  }
}

function combineSequences(commandName, phrases) {
  const aliases = storage.getAliases();

  return aliases[commandName] ? aliases[commandName].concat(phrases.slice(1)) : phrases.slice(1);
}

function expandPartialMatch(matchedCommands, partialMatch, sign) {
  const firstCommand = matchedCommands[0];
  let expanded = '';
  let matched = true;

  for (let i = partialMatch.length; i < firstCommand.length; i++) {
    const commandChar = firstCommand.charAt(i);

    for (let j = 0; j < matchedCommands.length; j++) {
      if (matchedCommands[j].charAt(i) !== commandChar) {
        matched = false;

        break;
      }
    }

    if (matched) {
      expanded += commandChar;
    } else {
      return commandHandler.isCommandChar(sign) ? sign + partialMatch + expanded : partialMatch + expanded;
    }
  }

  return '';
}

// TODO autoCompleteCommand should use this
/**
 * Match partial string against one to many strings and return matches
 * @param {string} partial - Partial string to match
 * @param {string[]} items - All matchable items
 * @returns {string[]} - Matched strings
 */
function match(partial, items) {
  const matched = [];
  let matches = false;

  for (const name of items) {
    for (let i = 0; i < partial.length; i++) {
      if (partial.charAt(i) === name.charAt(i)) {
        matches = true;
      } else {
        matches = false;

        break;
      }
    }

    if (matches) {
      matched.push(name);
    }
  }

  return matched;
}

/**
 * Matches partial string against available options for a command.
 * Appends input with matched option or sends message with multiple matches
 * @param {string[]} phrases - Input from user
 * @param {Object} options - Options from command
 */
function autoCompleteOption(phrases = [], options = {}) {
  const option = options[`${phrases[phrases.length - 2]}`];
  const partial = phrases[phrases.length - 1];
  /**
   * @type {string[]}
   */
  let matched = [];

  if (option && option.next) {
    const nextKeys = Object.keys(option.next);
    matched = match(partial, nextKeys);

    if (matched.length === 1) {
      domManipulator.replaceLastInputPhrase(`${matched[0]} `);
    } else if (matched.length > 0) {
      messenger.queueMessage({ text: [matched.join('\t')] });
    } else if (nextKeys.length > 0) {
      if (partial !== '') {
        domManipulator.setCommandInput(`${domManipulator.getInputText()} `);
      }

      messenger.queueMessage({ text: [nextKeys.join('\t')] });
    }
  } else if (phrases.length <= 2) {
    const firstLevelOptions = Object.keys(options);
    matched = match(partial, firstLevelOptions);

    if (matched.length === 1) {
      domManipulator.replaceLastInputPhrase(`${matched[0]} `);
    } else if (matched.length > 0) {
      messenger.queueMessage({ text: [matched.join('\t')] });
    } else if (partial === '') {
      messenger.queueMessage({ text: [firstLevelOptions.join('\t')] });
    }
  }
}

function autoCompleteCommand() {
  const phrases = textTools.trimSpace(domManipulator.getInputText().toLowerCase()).split(' ');
  // TODO Change from Object.keys for compatibility with older Android
  const allCommands = commandHandler.getCommands({ aliases: true, filtered: true });
  const matched = [];
  const sign = phrases[0].charAt(0);
  let newText = '';
  let matches;
  let partialCommand = phrases[0];

  /**
   * Auto-complete should only trigger when one phrase is in the input
   * It will not auto-complete flags
   * If chat mode and the command is prepended or normal mode
   */
  if (phrases.length === 1 && partialCommand.length > 0 && (commandHandler.isCommandChar(sign) || (storage.getMode() === 'cmd') || storage.getUser() === null)) {
    // Removes prepend sign
    if (commandHandler.isCommandChar(sign)) {
      partialCommand = partialCommand.slice(1);
    }

    for (const command of allCommands) {
      matches = false;

      for (let j = 0; j < partialCommand.length; j++) {
        const commandAccesssLevel = commandHandler.getCommandAccessLevel(command);
        const commandVisibility = commandHandler.getCommandVisibility(command);

        if ((isNaN(commandAccesssLevel) || storage.getAccessLevel() >= commandAccesssLevel) && storage.getAccessLevel() >= commandVisibility && partialCommand.charAt(j) === command.charAt(j)) {
          matches = true;
        } else {
          matches = false;

          break;
        }
      }

      if (matches) {
        matched.push(command);
      }
    }

    if (matched.length === 1) {
      const commandChars = commandHandler.getCommandChars();
      const commandIndex = commandChars.indexOf(sign);

      if (commandIndex >= 0) {
        newText += commandChars[commandIndex];
      }

      newText += `${matched[0]} `;

      domManipulator.clearInput();
      domManipulator.setCommandInput(newText);
    } else if (matched.length > 0) {
      domManipulator.setCommandInput(expandPartialMatch(matched, partialCommand, sign));
      messenger.queueMessage({ text: [matched.join('\t')] });
    }

    // No input? Show all available commands
  } else if (partialCommand.length === 0) {
    commandHandler.triggerCommand({ cmd: 'help' });
  }
}

function printUsedCommand(clearAfterUse, inputText) {
  if (clearAfterUse) {
    return null;
  }

  /**
   * Print input if the command shouldn't clear
   * after use
   */
  return {
    text: [`${domManipulator.getInputStart()}${domManipulator.getModeText()}$ ${inputText}`],
  };
}

function isFullscreen() {
  return (!window.screenTop && !window.screenY);
}

/**
 * Goes into full screen with sent element
 * This is not supported in iOS Safari
 * @param {object} element - The element which should be maximized to full screen
 * @returns {undefined} Returns nothing
 */
function goFullScreen(element) {
  if (element.requestFullscreen) {
    element.requestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
  } else if (element.webkitRequestFullscreen) {
    element.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
  } else if (element.mozRequestFullScreen) {
    element.mozRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
  }
}

function isAndroid() {
  return navigator.userAgent.match(/Android/i);
}

function isStandalone() {
  return window.navigator.standalone;
}

function isIos() {
  return navigator.userAgent.match(/iP(hone|ad|od)/i);
}

function padMenu() {
  if (isIos() && isStandalone()) {
    domManipulator.getMenu().classList.add('iosMenuPadding');
  }
}

/**
 * Fix for Android.
 * Expands the spacer so that the virtual keyboard doesn't block the rest of the site
 * @param {boolean} keyboardShown - Is the virtual keyboard visible?
 */
function fullscreenResize(keyboardShown) {
  /**
   * Used for Android when it shows/hides the keyboard
   * The soft keyboard will block part of the site without this fix
   */
  if (isFullscreen() && isAndroid()) {
    const spacer = domManipulator.getSpacer();

    domManipulator.getMainView().classList.add('fullscreen');

    if (keyboardShown) {
      spacer.classList.add('keyboardFix');
      spacer.classList.remove('fullFix');
    } else {
      spacer.classList.remove('keyboardFix');
      spacer.classList.add('fullFix');
    }

    domManipulator.scrollView();
  }
}

function enterKeyHandler() {
  const commandHelper = commandHandler.commandHelper;
  const user = storage.getUser();
  const inputText = domManipulator.getInputText();
  let phrases;
  keyPressed = true;

  if (!commandHelper.keysBlocked) {
    if (commandHelper.command !== null) {
      phrases = textTools.trimSpace(inputText).split(' ');

      // TODO Hard coded
      if (phrases[0] === 'exit' || phrases[0] === 'abort') {
        commandHandler.abortCommand(commandHelper.command);
        commandHandler.resetCommand(true);
      } else {
        if (!commandHelper.hideInput) {
          messenger.queueMessage({ text: [inputText] });
        }

        commandHandler.triggerCommandStep(phrases);
      }
    } else {
      phrases = textTools.trimSpace(inputText).split(' ');

      if (phrases[0].length > 0) {
        const command = commandHandler.getCommand(phrases[0]);

        if (!storage.getDisableCommands() && (command && (isNaN(command.accessLevel) || storage.getAccessLevel() >= command.accessLevel))) {
          // Store the command for usage with up/down arrows
          pushCommandHistory(phrases.join(' '));

          if (command.steps) {
            commandHelper.command = command.commandName;
            commandHelper.maxSteps = command.steps.length;
          }

          if (command.clearBeforeUse) {
            commandHandler.triggerCommand({ cmd: 'clear' });
          }

          queueCommand(command.func, combineSequences(command.commandName, phrases), printUsedCommand(command.clearAfterUse, inputText));
          startCommandQueue();
          /**
           * User is logged in and in chat mode
           */
        } else if (user !== null && storage.getMode() === 'chat' && phrases[0].length > 0) {
          if (commandHandler.getCommandChars().indexOf(phrases[0].charAt(0)) < 0) {
            queueCommand(commandHandler.getCommand('msg').func, phrases);
            startCommandQueue();

            /**
             * User input commandChar but didn't type
             * a proper command
             */
          } else {
            messenger.queueMessage({
              text: [`${phrases[0]}: ${labels.getText('errors', 'commandFail')}`],
            });
          }
        } else if (user === null) {
          messenger.queueMessage({ text: [phrases.toString()] });
          messenger.queueMessage({ text: labels.getText('info', 'mustRegister') });

          /**
           * Sent command was not found.
           * Print the failed input
           */
        } else {
          pushCommandHistory(phrases.join(' '));
          messenger.queueMessage({
            text: [`- ${phrases[0]}: ${labels.getText('errors', 'commandFail')}`],
          });
        }
      } else {
        messenger.queueMessage(printUsedCommand(false, ' '));
      }
    }
  }

  domManipulator.removeSubMenu();
  resetPreviousCommandPointer();
  domManipulator.clearInput();
  domManipulator.clearModeText();
}

function specialKeyPress(event) {
  const keyCode = typeof event.which === 'number' ? event.which : event.keyCode;
  const commandHistory = storage.getCommandHistory();
  const commandHelper = commandHandler.commandHelper;

  if (!keyPressed) {
    switch (keyCode) {
      case 9: { // Tab
        const phrases = domManipulator.getInputText().split(' ');
        keyPressed = true;

        if (!commandHelper.keysBlocked && commandHelper.command === null && phrases.length === 1) {
          autoCompleteCommand();
          domManipulator.changeModeText();
        } else if (commandHelper.allowAutoComplete) {
          const command = commandHandler.getCommand(commandHelper.command);
          const partial = phrases[0];

          if (command && command.autocomplete) {
            switch (command.autocomplete.type) {
              case 'users': {
                socketHandler.emit('matchPartialUser', { partialName: partial });

                break;
              }
              case 'rooms': {
                socketHandler.emit('matchPartialRoom', { partialName: partial });

                break;
              }
              case 'myRooms': {
                socketHandler.emit('matchPartialMyRoom', { partialName: partial });

                break;
              }
              default: {
                break;
              }
            }
          }
        } else if (phrases.length >= 2) {
          const command = commandHandler.getCommand(phrases[0]);

          if (command) {
            const options = command.options;

            autoCompleteOption(phrases, options, phrases.length);
          }
        }

        event.preventDefault();

        break;
      }
      case 13: { // Enter
        enterKeyHandler();

        event.preventDefault();

        break;
      }
      case 17: { // Ctrl
        triggerKeysPressed.ctrl = true;

        break;
      }
      case 18: { // Alt
        triggerKeysPressed.alt = true;

        break;
      }
      case 91: { // Left Command key in OS X
        triggerKeysPressed.ctrl = true;

        break;
      }
      case 93: { // Right Command key in OS X
        triggerKeysPressed.ctrl = true;

        break;
      }
      case 224: { // Command key in OS X (Firefox)
        triggerKeysPressed.ctrl = true;

        break;
      }
      case 33: { // Page up
        domManipulator.getMainView().scrollTop -= (window.innerHeight - 100);

        event.preventDefault();

        break;
      }
      case 34: { // Page down
        domManipulator.getMainView().scrollTop += (window.innerHeight - 100);

        event.preventDefault();

        break;
      }
      case 38: { // Up arrow
        keyPressed = true;

        if (triggerKeysPressed.ctrl) {
          domManipulator.getMainView().scrollTop -= (window.innerHeight - 100);
        } else if (!commandHelper.keysBlocked && commandHelper.command === null && previousCommandPointer > 0) {
          domManipulator.clearInput();
          previousCommandPointer--;
          domManipulator.setCommandInput(commandHistory[previousCommandPointer]);
        }

        event.preventDefault();

        break;
      }
      case 40: { // Down arrow
        keyPressed = true;

        if (triggerKeysPressed.ctrl) {
          domManipulator.getMainView().scrollTop += (window.innerHeight - 100);
        } else {
          if (!commandHelper.keysBlocked && commandHelper.command === null) {
            if (previousCommandPointer < commandHistory.length - 1) {
              domManipulator.clearInput();
              previousCommandPointer++;
              domManipulator.setCommandInput(commandHistory[previousCommandPointer]);
            } else if (previousCommandPointer === commandHistory.length - 1) {
              domManipulator.clearInput();
              previousCommandPointer++;
            } else {
              domManipulator.clearInput();
            }
          }
        }

        event.preventDefault();

        break;
      }<commit_msg>Refactored scrolling of text<commit_after>const labels = require('./labels');
const textTools = require('./textTools');
const mapTools = require('./mapTools');
const storage = require('./storage');
const layoutChanger = require('./layoutChanger');
const socketHandler = require('./socketHandler');
const messenger = require('./messenger');
const commandHandler = require('./commandHandler');
const domManipulator = require('./domManipulator');
const clickHandler = require('./clickHandler');
const videoPlayer = require('./videoPlayer');

/**
 * Queue of all the commands used by the user that will be handled and printed
 * @type {Object[]}
 */
const commandQueue = [];
/**
 * Check every * amount of milliseconds to see if Javascript is still responding
 * It will trigger a function if the response is delayed
 * @type {Number}
 */
const screenOffIntervalTime = 1000;
/**
 * Get GPS coordinates for * amount of milliseconds
 * @type {Number}
 */
const watchPositionTime = 15000;
/**
 * Get GPS coordinates every * milliseconds
 * @type {Number}
 */
const pausePositionTime = 40000;
/**
 * Queue of all sounds to be consumed and played
 * @type {Object[]}
 */
const soundQueue = [];
/**
 * Timeout between each command to be run
 * @type {Number}
 */
const commandTime = 1000;
const dot = '.';
const dash = '-';
const triggerKeysPressed = [];
/**
 * Symbolizes space between words in morse string
 * @type {string}
 */
const morseSeparator = '#';
let audioCtx;
let oscillator;
let gainNode;
let soundTimeout = 0;
let previousCommandPointer;
let watchId = null;
// Is geolocation tracking on?
let isTracking = false;
let firstConnection = true;
let positions = [];
/**
 * Used by isScreenOff() to force reconnect when phone screen is off
 * for a longer period of time
 */
let lastScreenOff = (new Date()).getTime();
let commmandUsed = false;
/**
 * Used to block repeat of key presses
 */
let keyPressed;
let trackingInterval;
let isScreenOffInterval;
let serverDownTimeout;

function queueCommand(command, data, commandMsg) {
  commandQueue.push({
    command,
    data,
    commandMsg,
  });
}

function pushCommandHistory(command) {
  const commandHistory = storage.getCommandHistory();

  commandHistory.push(command);
  storage.setCommandHistory(commandHistory);
}

function enterRoom(roomName) {
  storage.setRoom(roomName);

  if (!storage.getStaticInputStart()) {
    domManipulator.setInputStart(roomName);
  }

  messenger.queueMessage({
    text: [`Entered ${roomName}`],
    text_se: [`Gick in i ${roomName}`],
  });
}

function resetPreviousCommandPointer() {
  const commandHistory = storage.getCommandHistory();

  previousCommandPointer = commandHistory ? commandHistory.length : 0;
}

function setGain(value) {
  gainNode.gain.value = value;
}

function playMorse(morseCode, silent) {
  function finishSoundQueue(timeouts) {
    const cleanMorse = morseCode.replace(/#/g, '');

    soundQueue.splice(0, timeouts);

    if (!silent) {
      messenger.queueMessage({
        text: [`Morse code message received: ${cleanMorse}`],
        text_se: [`Morse mottaget: ${cleanMorse}`],
      });
    }
  }

  let duration;
  let shouldPlay;

  if (soundQueue.length === 0) {
    soundTimeout = 0;
  }

  for (const code of morseCode) {
    shouldPlay = false;
    duration = 0;

    if (dot === code) {
      duration = 50;
      shouldPlay = true;
    } else if (dash === code) {
      duration = 150;
      shouldPlay = true;
    } else if (morseSeparator === code) {
      duration = 50;
    } else {
      duration = 75;
    }

    if (shouldPlay) {
      soundQueue.push(setTimeout(setGain, soundTimeout, 1));
      soundQueue.push(setTimeout(setGain, soundTimeout + duration, 0));
    }

    soundTimeout += duration;
  }

  setTimeout(finishSoundQueue, soundTimeout, (2 * morseCode.length), morseCode);
}

/**
 * Geolocation object is empty when sent through Socket.IO
 * This is a fix for that
 * @param {object} position . -
 * @returns {object} Returns position
 */
function preparePosition(position) {
  const preparedPosition = {};
  preparedPosition.latitude = position.coords.latitude;
  preparedPosition.longitude = position.coords.longitude;
  preparedPosition.speed = position.coords.speed;
  preparedPosition.accuracy = position.coords.accuracy;
  preparedPosition.heading = position.coords.heading;
  preparedPosition.timestamp = position.timestamp;

  return preparedPosition; // geolocation
}

function retrievePosition() {
  const clearingWatch = () => {
    navigator.geolocation.clearWatch(watchId);
    watchId = null;
    trackingInterval = setTimeout(sendLocation, pausePositionTime); // eslint-disable-line no-use-before-define
  };

  watchId = navigator.geolocation.watchPosition((position) => {
    if (position !== undefined) {
      isTracking = true;
      positions.push(position);

      mapTools.setUserPosition({
        latitude: position.coords.latitude,
        longitude: position.coords.longitude,
      });
    }
  }, (err) => {
    console.log(err);
  }, { enableHighAccuracy: true });

  if (isTracking) {
    trackingInterval = setTimeout(clearingWatch, watchPositionTime);
  }
}

function sendLocation() {
  let mostAccuratePos;

  if (storage.getUser() !== null && positions.length > 0) {
    mostAccuratePos = positions[positions.length - 1];

    for (let i = positions.length - 2; i >= 0; i--) {
      const position = positions[i];
      const accuracy = positions[i].coords.accuracy;

      if (mostAccuratePos.coords.accuracy > accuracy) {
        mostAccuratePos = position;
      }
    }

    positions = [];

    socketHandler.emit('updateLocation', {
      type: 'user',
      position: preparePosition(mostAccuratePos),
    });
  }

  retrievePosition();
}

/**
 * Some devices disable Javascript when screen is off (iOS)
 * They also fail to notice that they have been disconnected
 * We check the time between heartbeats and if the time i
 * over 10 seconds (example: when screen is turned off and then on)
 * we force them to reconnect
 */
function isScreenOff() {
  const now = (new Date()).getTime();
  const diff = now - lastScreenOff;
  // FIXME Hard coded
  const offBy = diff - 1000;
  lastScreenOff = now;

  // FIXME Hard coded
  if (offBy > 10000) {
    socketHandler.reconnect();
  }
}

/**
 * Set intervals at boot and recreate them when the window is focused
 * This is to make sure that nothing has been killed in the background
 */
function setIntervals() {
  if (trackingInterval !== null) {
    clearTimeout(trackingInterval);
  }

  if (watchId !== null) {
    navigator.geolocation.clearWatch(watchId);
  }

  if (storage.getGpsTracking() && navigator.geolocation) {
    // Gets new geolocation data
    sendLocation();
  }

  // Should not be recreated on focus
  if (isScreenOffInterval === null) {
    /**
     * Checks time between when JS stopped and started working again
     * This will be most frequently triggered when a user turns off the
     * screen on their phone and turns it back on
     */
    isScreenOffInterval = setInterval(isScreenOff, screenOffIntervalTime);
  }
}

/**
 * Resets intervals and keyPressed (to not have it true after a user tabbed out and into the site)
 */
function refocus() {
  keyPressed = false;
  triggerKeysPressed.ctrl = false;
  triggerKeysPressed.alt = false;
  setIntervals();
}

function buildMorsePlayer() {
  // Not supported in Spartan nor IE11 or lower
  if (window.AudioContext || window.webkitAudioContext) {
    if (window.AudioContext) {
      audioCtx = new window.AudioContext();
    } else if (window.webkitAudioContext) {
      audioCtx = new window.webkitAudioContext(); // eslint-disable-line
    }

    oscillator = audioCtx.createOscillator();
    gainNode = audioCtx.createGain();

    oscillator.connect(gainNode);
    gainNode.connect(audioCtx.destination);

    gainNode.gain.value = 0;
    oscillator.type = 'sine';
    oscillator.frequency.value = '440';
    // oscillator.type = 'square';
    // oscillator.frequency.value = '300';

    oscillator.start(0);
  }
}

function triggerAutoComplete(text, textChar) {
  if (text.charAt(text.length - 1) === ' ' && textChar === ' ') {
    domManipulator.setCommandInput(textTools.trimSpace(text));

    return true;
  }

  return false;
}

function setCommandUsed(used) {
  commmandUsed = used;
}

function consumeCommandQueue() {
  if (commandQueue.length > 0) {
    const storedCommand = commandQueue.shift();
    const commmand = storedCommand.command;
    const commandMessage = storedCommand.commandMsg;

    if (commandMessage) {
      messenger.queueMessage(commandMessage);
    }

    setCommandUsed(true);
    commmand(storedCommand.data);
    setTimeout(consumeCommandQueue, commandTime);
  } else {
    setCommandUsed(false);
  }
}

function startCommandQueue() {
  if (!commmandUsed) {
    consumeCommandQueue();
  }
}

function combineSequences(commandName, phrases) {
  const aliases = storage.getAliases();

  return aliases[commandName] ? aliases[commandName].concat(phrases.slice(1)) : phrases.slice(1);
}

function expandPartialMatch(matchedCommands, partialMatch, sign) {
  const firstCommand = matchedCommands[0];
  let expanded = '';
  let matched = true;

  for (let i = partialMatch.length; i < firstCommand.length; i++) {
    const commandChar = firstCommand.charAt(i);

    for (let j = 0; j < matchedCommands.length; j++) {
      if (matchedCommands[j].charAt(i) !== commandChar) {
        matched = false;

        break;
      }
    }

    if (matched) {
      expanded += commandChar;
    } else {
      return commandHandler.isCommandChar(sign) ? sign + partialMatch + expanded : partialMatch + expanded;
    }
  }

  return '';
}

// TODO autoCompleteCommand should use this
/**
 * Match partial string against one to many strings and return matches
 * @param {string} partial - Partial string to match
 * @param {string[]} items - All matchable items
 * @returns {string[]} - Matched strings
 */
function match(partial, items) {
  const matched = [];
  let matches = false;

  for (const name of items) {
    for (let i = 0; i < partial.length; i++) {
      if (partial.charAt(i) === name.charAt(i)) {
        matches = true;
      } else {
        matches = false;

        break;
      }
    }

    if (matches) {
      matched.push(name);
    }
  }

  return matched;
}

/**
 * Matches partial string against available options for a command.
 * Appends input with matched option or sends message with multiple matches
 * @param {string[]} phrases - Input from user
 * @param {Object} options - Options from command
 */
function autoCompleteOption(phrases = [], options = {}) {
  const option = options[`${phrases[phrases.length - 2]}`];
  const partial = phrases[phrases.length - 1];
  /**
   * @type {string[]}
   */
  let matched = [];

  if (option && option.next) {
    const nextKeys = Object.keys(option.next);
    matched = match(partial, nextKeys);

    if (matched.length === 1) {
      domManipulator.replaceLastInputPhrase(`${matched[0]} `);
    } else if (matched.length > 0) {
      messenger.queueMessage({ text: [matched.join('\t')] });
    } else if (nextKeys.length > 0) {
      if (partial !== '') {
        domManipulator.setCommandInput(`${domManipulator.getInputText()} `);
      }

      messenger.queueMessage({ text: [nextKeys.join('\t')] });
    }
  } else if (phrases.length <= 2) {
    const firstLevelOptions = Object.keys(options);
    matched = match(partial, firstLevelOptions);

    if (matched.length === 1) {
      domManipulator.replaceLastInputPhrase(`${matched[0]} `);
    } else if (matched.length > 0) {
      messenger.queueMessage({ text: [matched.join('\t')] });
    } else if (partial === '') {
      messenger.queueMessage({ text: [firstLevelOptions.join('\t')] });
    }
  }
}

function autoCompleteCommand() {
  const phrases = textTools.trimSpace(domManipulator.getInputText().toLowerCase()).split(' ');
  // TODO Change from Object.keys for compatibility with older Android
  const allCommands = commandHandler.getCommands({ aliases: true, filtered: true });
  const matched = [];
  const sign = phrases[0].charAt(0);
  let newText = '';
  let matches;
  let partialCommand = phrases[0];

  /**
   * Auto-complete should only trigger when one phrase is in the input
   * It will not auto-complete flags
   * If chat mode and the command is prepended or normal mode
   */
  if (phrases.length === 1 && partialCommand.length > 0 && (commandHandler.isCommandChar(sign) || (storage.getMode() === 'cmd') || storage.getUser() === null)) {
    // Removes prepend sign
    if (commandHandler.isCommandChar(sign)) {
      partialCommand = partialCommand.slice(1);
    }

    for (const command of allCommands) {
      matches = false;

      for (let j = 0; j < partialCommand.length; j++) {
        const commandAccesssLevel = commandHandler.getCommandAccessLevel(command);
        const commandVisibility = commandHandler.getCommandVisibility(command);

        if ((isNaN(commandAccesssLevel) || storage.getAccessLevel() >= commandAccesssLevel) && storage.getAccessLevel() >= commandVisibility && partialCommand.charAt(j) === command.charAt(j)) {
          matches = true;
        } else {
          matches = false;

          break;
        }
      }

      if (matches) {
        matched.push(command);
      }
    }

    if (matched.length === 1) {
      const commandChars = commandHandler.getCommandChars();
      const commandIndex = commandChars.indexOf(sign);

      if (commandIndex >= 0) {
        newText += commandChars[commandIndex];
      }

      newText += `${matched[0]} `;

      domManipulator.clearInput();
      domManipulator.setCommandInput(newText);
    } else if (matched.length > 0) {
      domManipulator.setCommandInput(expandPartialMatch(matched, partialCommand, sign));
      messenger.queueMessage({ text: [matched.join('\t')] });
    }

    // No input? Show all available commands
  } else if (partialCommand.length === 0) {
    commandHandler.triggerCommand({ cmd: 'help' });
  }
}

function printUsedCommand(clearAfterUse, inputText) {
  if (clearAfterUse) {
    return null;
  }

  /**
   * Print input if the command shouldn't clear
   * after use
   */
  return {
    text: [`${domManipulator.getInputStart()}${domManipulator.getModeText()}$ ${inputText}`],
  };
}

function isFullscreen() {
  return (!window.screenTop && !window.screenY);
}

/**
 * Goes into full screen with sent element
 * This is not supported in iOS Safari
 * @param {object} element - The element which should be maximized to full screen
 * @returns {undefined} Returns nothing
 */
function goFullScreen(element) {
  if (element.requestFullscreen) {
    element.requestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
  } else if (element.webkitRequestFullscreen) {
    element.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
  } else if (element.mozRequestFullScreen) {
    element.mozRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
  }
}

function isAndroid() {
  return navigator.userAgent.match(/Android/i);
}

function isStandalone() {
  return window.navigator.standalone;
}

function isIos() {
  return navigator.userAgent.match(/iP(hone|ad|od)/i);
}

function padMenu() {
  if (isIos() && isStandalone()) {
    domManipulator.getMenu().classList.add('iosMenuPadding');
  }
}

/**
 * Fix for Android.
 * Expands the spacer so that the virtual keyboard doesn't block the rest of the site
 * @param {boolean} keyboardShown - Is the virtual keyboard visible?
 */
function fullscreenResize(keyboardShown) {
  /**
   * Used for Android when it shows/hides the keyboard
   * The soft keyboard will block part of the site without this fix
   */
  if (isFullscreen() && isAndroid()) {
    const spacer = domManipulator.getSpacer();

    domManipulator.getMainView().classList.add('fullscreen');

    if (keyboardShown) {
      spacer.classList.add('keyboardFix');
      spacer.classList.remove('fullFix');
    } else {
      spacer.classList.remove('keyboardFix');
      spacer.classList.add('fullFix');
    }

    domManipulator.scrollView();
  }
}

function enterKeyHandler() {
  const commandHelper = commandHandler.commandHelper;
  const user = storage.getUser();
  const inputText = domManipulator.getInputText();
  let phrases;
  keyPressed = true;

  if (!commandHelper.keysBlocked) {
    if (commandHelper.command !== null) {
      phrases = textTools.trimSpace(inputText).split(' ');

      // TODO Hard coded
      if (phrases[0] === 'exit' || phrases[0] === 'abort') {
        commandHandler.abortCommand(commandHelper.command);
        commandHandler.resetCommand(true);
      } else {
        if (!commandHelper.hideInput) {
          messenger.queueMessage({ text: [inputText] });
        }

        commandHandler.triggerCommandStep(phrases);
      }
    } else {
      phrases = textTools.trimSpace(inputText).split(' ');

      if (phrases[0].length > 0) {
        const command = commandHandler.getCommand(phrases[0]);

        if (!storage.getDisableCommands() && (command && (isNaN(command.accessLevel) || storage.getAccessLevel() >= command.accessLevel))) {
          // Store the command for usage with up/down arrows
          pushCommandHistory(phrases.join(' '));

          if (command.steps) {
            commandHelper.command = command.commandName;
            commandHelper.maxSteps = command.steps.length;
          }

          if (command.clearBeforeUse) {
            commandHandler.triggerCommand({ cmd: 'clear' });
          }

          queueCommand(command.func, combineSequences(command.commandName, phrases), printUsedCommand(command.clearAfterUse, inputText));
          startCommandQueue();
          /**
           * User is logged in and in chat mode
           */
        } else if (user !== null && storage.getMode() === 'chat' && phrases[0].length > 0) {
          if (commandHandler.getCommandChars().indexOf(phrases[0].charAt(0)) < 0) {
            queueCommand(commandHandler.getCommand('msg').func, phrases);
            startCommandQueue();

            /**
             * User input commandChar but didn't type
             * a proper command
             */
          } else {
            messenger.queueMessage({
              text: [`${phrases[0]}: ${labels.getText('errors', 'commandFail')}`],
            });
          }
        } else if (user === null) {
          messenger.queueMessage({ text: [phrases.toString()] });
          messenger.queueMessage({ text: labels.getText('info', 'mustRegister') });

          /**
           * Sent command was not found.
           * Print the failed input
           */
        } else {
          pushCommandHistory(phrases.join(' '));
          messenger.queueMessage({
            text: [`- ${phrases[0]}: ${labels.getText('errors', 'commandFail')}`],
          });
        }
      } else {
        messenger.queueMessage(printUsedCommand(false, ' '));
      }
    }
  }

  domManipulator.removeSubMenu();
  resetPreviousCommandPointer();
  domManipulator.clearInput();
  domManipulator.clearModeText();
}

/**
 * Scrolls the view (with rows) a specific amount of pixels
 * Used to scroll the view with page up/down keys
 * @param {number} amount - Amount of pixels to scroll the view with
 */
function scrollText(amount) {
  const modifiedAmount = amount < 0 ? (amount + 100) : (amount - 100);

  domManipulator.getMainView().scrollTop += modifiedAmount;
}

function specialKeyPress(event) {
  const keyCode = typeof event.which === 'number' ? event.which : event.keyCode;
  const commandHistory = storage.getCommandHistory();
  const commandHelper = commandHandler.commandHelper;

  if (!keyPressed) {
    switch (keyCode) {
      case 9: { // Tab
        const phrases = domManipulator.getInputText().split(' ');
        keyPressed = true;

        if (!commandHelper.keysBlocked && commandHelper.command === null && phrases.length === 1) {
          autoCompleteCommand();
          domManipulator.changeModeText();
        } else if (commandHelper.allowAutoComplete) {
          const command = commandHandler.getCommand(commandHelper.command);
          const partial = phrases[0];

          if (command && command.autocomplete) {
            switch (command.autocomplete.type) {
              case 'users': {
                socketHandler.emit('matchPartialUser', { partialName: partial });

                break;
              }
              case 'rooms': {
                socketHandler.emit('matchPartialRoom', { partialName: partial });

                break;
              }
              case 'myRooms': {
                socketHandler.emit('matchPartialMyRoom', { partialName: partial });

                break;
              }
              default: {
                break;
              }
            }
          }
        } else if (phrases.length >= 2) {
          const command = commandHandler.getCommand(phrases[0]);

          if (command) {
            const options = command.options;

            autoCompleteOption(phrases, options, phrases.length);
          }
        }

        event.preventDefault();

        break;
      }
      case 13: { // Enter
        enterKeyHandler();

        event.preventDefault();

        break;
      }
      case 17: { // Ctrl
        triggerKeysPressed.ctrl = true;

        break;
      }
      case 18: { // Alt
        triggerKeysPressed.alt = true;

        break;
      }
      case 91: { // Left Command key in OS X
        triggerKeysPressed.ctrl = true;

        break;
      }
      case 93: { // Right Command key in OS X
        triggerKeysPressed.ctrl = true;

        break;
      }
      case 224: { // Command key in OS X (Firefox)
        triggerKeysPressed.ctrl = true;

        break;
      }
      case 33: { // Page up
        scrollText(-window.innerHeight);

        event.preventDefault();

        break;
      }
      case 34: { // Page down
        scrollText(window.innerHeight);

        event.preventDefault();

        break;
      }
      case 38: { // Up arrow
        keyPressed = true;

        if (triggerKeysPressed.ctrl) {
          scrollText(-window.innerHeight);
        } else if (!commandHelper.keysBlocked && commandHelper.command === null && previousCommandPointer > 0) {
          domManipulator.clearInput();
          previousCommandPointer--;
          domManipulator.setCommandInput(commandHistory[previousCommandPointer]);
        }

        event.preventDefault();

        break;
      }
      case 40: { // Down arrow
        keyPressed = true;

        if (triggerKeysPressed.ctrl) {
          scrollText(window.innerHeight);
        } else {
          if (!commandHelper.keysBlocked && commandHelper.command === null) {
            if (previousCommandPointer < commandHistory.length - 1) {
              domManipulator.clearInput();
              previousCommandPointer++;
              domManipulator.setCommandInput(commandHistory[previousCommandPointer]);
            } else if (previousCommandPointer === commandHistory.length - 1) {
              domManipulator.clearInput();
              previousCommandPointer++;
            } else {
              domManipulator.clearInput();
            }
          }
        }

        event.preventDefault();

        break;
      }
{'fraction_non_alphanumeric': 0.12454267962678328, 'fraction_numerical': 0.0060831727750957935, 'mean_word_length': 2.8572144288577155, 'pattern_counts': {'":': 0, '<': 18, '<?xml version=': 0, '>': 45, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4518087', 'n_tokens_mistral': 13527, 'n_tokens_neox': 13121, 'n_words': 3259}