import {
  evaluateDirectoryPath,
  evaluateFilePath,
  getFirstParentDirectory,
  isInRootDirectory,
  isValidDirectoryPath,
  removeLeadingSlash,
  removeTrailingSlash,
} from "./Terminal/DirectoryHelpers";
import { determineAllPossibilitiesForTabCompletion } from "./Terminal/determineAllPossibilitiesForTabCompletion";
import { TerminalHelpText, HelpTexts } from "./Terminal/HelpText";
import { tabCompletion } from "./Terminal/tabCompletion";
import { createFconf } from "./Fconf/Fconf";

import { parseAliasDeclaration, printAliases, removeAlias, substituteAliases } from "./Alias";
import { BitNodeMultipliers } from "./BitNode/BitNodeMultipliers";
import { CodingContractResult } from "./CodingContracts";
import { CONSTANTS } from "./Constants";
import { Programs } from "./Programs/Programs";
import { executeDarkwebTerminalCommand, checkIfConnectedToDarkweb } from "./DarkWeb/DarkWeb";
import { Engine } from "./engine";
import { FconfSettings } from "./Fconf/FconfSettings";
import {
  calculateHackingChance,
  calculateHackingExpGain,
  calculatePercentMoneyHacked,
  calculateHackingTime,
  calculateGrowTime,
  calculateWeakenTime,
} from "./Hacking";
import { HacknetServer } from "./Hacknet/HacknetServer";
import { iTutorialNextStep, iTutorialSteps, ITutorial } from "./InteractiveTutorial";
import { showLiterature } from "./Literature/LiteratureHelpers";
import { Message } from "./Message/Message";
import { showMessage } from "./Message/MessageHelpers";
import { startWorkerScript } from "./NetscriptWorker";
import { killWorkerScript } from "./Netscript/killWorkerScript";
import { WorkerScriptStartStopEventEmitter } from "./Netscript/WorkerScriptStartStopEventEmitter";
import { Player } from "./Player";
import { hackWorldDaemon } from "./RedPill";
import { RunningScript } from "./Script/RunningScript";
import { compareArrays } from "../utils/helpers/compareArrays";
import { getRamUsageFromRunningScript } from "./Script/RunningScriptHelpers";
import { findRunningScript, findRunningScriptByPid } from "./Script/ScriptHelpers";
import { isScriptFilename } from "./Script/ScriptHelpersTS";
import { AllServers } from "./Server/AllServers";
import { GetServerByHostname, getServer, getServerOnNetwork } from "./Server/ServerHelpers";
import { SpecialServerIps, SpecialServerNames } from "./Server/SpecialServerIps";
import { setTimeoutRef } from "./utils/SetTimeoutRef";
import { Page, routing } from "./ui/navigationTracking";
import { numeralWrapper } from "./ui/numeralFormat";
import { KEY } from "../utils/helpers/keyCodes";
import { arrayToString } from "../utils/helpers/arrayToString";
import { getTimestamp } from "../utils/helpers/getTimestamp";
import { logBoxCreate } from "../utils/LogBox";
import { post, postElement, postContent, postError, hackProgressBarPost, hackProgressPost } from "./ui/postToTerminal";
import { convertTimeMsToTimeElapsedString } from "../utils/StringHelperFunctions";
import { Money } from "./ui/React/Money";
import { createPopup } from "./ui/React/createPopup";
import { BitFlumePopup } from "./BitNode/ui/BitFlumePopup";

import autosize from "autosize";
import * as JSZip from "jszip";
import * as FileSaver from "file-saver";
import * as libarg from "arg";
import React from "react";

function postVersion() {
  post("Bitburner v" + CONSTANTS.Version);
}

// Helper function that checks if an argument (which is a string) is a valid number
function isNumber(str) {
  if (typeof str != "string") {
    return false;
  } // Only process strings
  return !isNaN(str) && !isNaN(parseFloat(str));
}

function getTerminalInput() {
  return document.getElementById("terminal-input-text-box").value;
}

// Defines key commands in terminal
$(document).keydown(function (event) {
  // Terminal
  if (routing.isOn(Page.Terminal)) {
    var terminalInput = document.getElementById("terminal-input-text-box");
    if (terminalInput != null && !event.ctrlKey && !event.shiftKey && !Terminal.contractOpen) {
      terminalInput.focus();
    }

    if (event.keyCode === KEY.ENTER) {
      event.preventDefault(); // Prevent newline from being entered in Script Editor
      const command = getTerminalInput();
      const dir = Terminal.currDir;
      post(
        "<span class='prompt'>[" +
          (FconfSettings.ENABLE_TIMESTAMPS ? getTimestamp() + " " : "") +
          Player.getCurrentServer().hostname +
          ` ~${dir}]&gt;</span> ${command}`,
      );

      if (command.length > 0) {
        Terminal.resetTerminalInput(); // Clear input first
        Terminal.executeCommands(command);
      }
    }

    if (event.keyCode === KEY.C && event.ctrlKey) {
      if (Engine._actionInProgress) {
        // Cancel action
        post("Cancelling...");
        Engine._actionInProgress = false;
        Terminal.finishAction(true);
      } else if (FconfSettings.ENABLE_BASH_HOTKEYS) {
        // Dont prevent default so it still copies
        Terminal.resetTerminalInput(); // Clear Terminal
      }
    }

    if (event.keyCode === KEY.L && event.ctrlKey) {
      event.preventDefault();
      Terminal.executeCommand("clear"); // Clear screen
    }

    // Ctrl p same as up arrow
    // Ctrl n same as down arrow

    if (
      event.keyCode === KEY.UPARROW ||
      (FconfSettings.ENABLE_BASH_HOTKEYS && event.keyCode === KEY.P && event.ctrlKey)
    ) {
      if (FconfSettings.ENABLE_BASH_HOTKEYS) {
        event.preventDefault();
      }
      // Cycle through past commands
      if (terminalInput == null) {
        return;
      }
      var i = Terminal.commandHistoryIndex;
      var len = Terminal.commandHistory.length;

      if (len == 0) {
        return;
      }
      if (i < 0 || i > len) {
        Terminal.commandHistoryIndex = len;
      }

      if (i != 0) {
        --Terminal.commandHistoryIndex;
      }
      var prevCommand = Terminal.commandHistory[Terminal.commandHistoryIndex];
      terminalInput.value = prevCommand;
      setTimeoutRef(function () {
        terminalInput.selectionStart = terminalInput.selectionEnd = 10000;
      }, 10);
    }

    if (
      event.keyCode === KEY.DOWNARROW ||
      (FconfSettings.ENABLE_BASH_HOTKEYS && event.keyCode === KEY.M && event.ctrlKey)
    ) {
      if (FconfSettings.ENABLE_BASH_HOTKEYS) {
        event.preventDefault();
      }
      // Cycle through past commands
      if (terminalInput == null) {
        return;
      }
      var i = Terminal.commandHistoryIndex;
      var len = Terminal.commandHistory.length;

      if (len == 0) {
        return;
      }
      if (i < 0 || i > len) {
        Terminal.commandHistoryIndex = len;
      }

      // Latest command, put nothing
      if (i == len || i == len - 1) {
        Terminal.commandHistoryIndex = len;
        terminalInput.value = "";
      } else {
        ++Terminal.commandHistoryIndex;
        var prevCommand = Terminal.commandHistory[Terminal.commandHistoryIndex];
        terminalInput.value = prevCommand;
      }
    }

    if (event.keyCode === KEY.TAB) {
      event.preventDefault();

      // Autocomplete
      if (terminalInput == null) {
        return;
      }
      let input = terminalInput.value;
      if (input == "") {
        return;
      }

      const semiColonIndex = input.lastIndexOf(";");
      if (semiColonIndex !== -1) {
        input = input.slice(semiColonIndex + 1);
      }

      input = input.trim();
      input = input.replace(/\s\s+/g, " ");

      const commandArray = input.split(" ");
      let index = commandArray.length - 2;
      if (index < -1) {
        index = 0;
      }
      const allPos = determineAllPossibilitiesForTabCompletion(Player, input, index, Terminal.currDir);
      if (allPos.length == 0) {
        return;
      }

      let arg = "";
      let command = "";
      if (commandArray.length == 0) {
        return;
      }
      if (commandArray.length == 1) {
        command = commandArray[0];
      } else if (commandArray.length == 2) {
        command = commandArray[0];
        arg = commandArray[1];
      } else if (commandArray.length == 3) {
        command = commandArray[0] + " " + commandArray[1];
        arg = commandArray[2];
      } else {
        arg = commandArray.pop();
        command = commandArray.join(" ");
      }

      tabCompletion(command, arg, allPos);
      terminalInput.focus();
    }

    // Extra Bash Emulation Hotkeys, must be enabled through .fconf
    if (FconfSettings.ENABLE_BASH_HOTKEYS) {
      if (event.keyCode === KEY.A && event.ctrlKey) {
        event.preventDefault();
        Terminal.moveTextCursor("home");
      }

      if (event.keyCode === KEY.E && event.ctrlKey) {
        event.preventDefault();
        Terminal.moveTextCursor("end");
      }

      if (event.keyCode === KEY.B && event.ctrlKey) {
        event.preventDefault();
        Terminal.moveTextCursor("prevchar");
      }

      if (event.keyCode === KEY.B && event.altKey) {
        event.preventDefault();
        Terminal.moveTextCursor("prevword");
      }

      if (event.keyCode === KEY.F && event.ctrlKey) {
        event.preventDefault();
        Terminal.moveTextCursor("nextchar");
      }

      if (event.keyCode === KEY.F && event.altKey) {
        event.preventDefault();
        Terminal.moveTextCursor("nextword");
      }

      if ((event.keyCode === KEY.H || event.keyCode === KEY.D) && event.ctrlKey) {
        Terminal.modifyInput("backspace");
        event.preventDefault();
      }

      // TODO AFTER THIS:
      // alt + d deletes word after cursor
      // ^w deletes word before cursor
      // ^k clears line after cursor
      // ^u clears line before cursor
    }
  }
});

// Keep terminal in focus
let terminalCtrlPressed = false,
  shiftKeyPressed = false;
$(document).ready(function () {
  if (routing.isOn(Page.Terminal)) {
    $(".terminal-input").focus();
  }
});

$(document).keydown(function (e) {
  if (routing.isOn(Page.Terminal)) {
    if (e.which == KEY.CTRL) {
      terminalCtrlPressed = true;
    } else if (e.shiftKey) {
      shiftKeyPressed = true;
    } else if (terminalCtrlPressed || shiftKeyPressed || Terminal.contractOpen) {
      // Don't focus
    } else {
      var inputTextBox = document.getElementById("terminal-input-text-box");
      if (inputTextBox != null) {
        inputTextBox.focus();
      }

      terminalCtrlPressed = false;
      shiftKeyPressed = false;
    }
  }
});

$(document).keyup(function (e) {
  if (routing.isOn(Page.Terminal)) {
    if (e.which == KEY.CTRL) {
      terminalCtrlPressed = false;
    }
    if (e.shiftKey) {
      shiftKeyPressed = false;
    }
  }
});

let Terminal = {
  // Flags to determine whether the player is currently running a hack or an analyze
  hackFlag: false,
  backdoorFlag: false,
  analyzeFlag: false,
  actionStarted: false,
  actionTime: 0,

  commandHistory: [],
  commandHistoryIndex: 0,

  // True if a Coding Contract prompt is opened
  contractOpen: false,

  // Full Path of current directory
  // Excludes the trailing forward slash
  currDir: "/",

  resetTerminalInput: function (keepInput = false) {
    let input = "";
    if (keepInput) {
      input = getTerminalInput();
    }
    const dir = Terminal.currDir;
    if (FconfSettings.WRAP_INPUT) {
      document.getElementById("terminal-input-td").innerHTML =
        `<div id='terminal-input-header' class='prompt'>[${Player.getCurrentServer().hostname} ~${dir}]$ </div>` +
        `<textarea type="text" id="terminal-input-text-box" class="terminal-input" tabindex="1"  value=\"${input}\"  autocomplete="off" />`;

      // Auto re-size the line element as it wraps
      autosize(document.getElementById("terminal-input-text-box"));
    } else {
      document.getElementById("terminal-input-td").innerHTML =
        `<div id='terminal-input-header' class='prompt'>[${Player.getCurrentServer().hostname} ~${dir}]$ </div>` +
        `<input type="text" id="terminal-input-text-box" class="terminal-input" tabindex="1"  value=\"${input}\" autocomplete="off" />`;
    }
    const hdr = document.getElementById("terminal-input-header");
    hdr.style.display = "inline";

    const terminalInput = document.getElementById("terminal-input-text-box");
    if (typeof terminalInput.selectionStart == "number") {
      terminalInput.selectionStart = terminalInput.selectionEnd = terminalInput.value.length;
    } else if (typeof terminalInput.createTextRange != "undefined") {
      terminalInput.focus();
      var range = el.createTextRange();
      range.collapse(false);
      range.select();
    }
  },

  modifyInput: function (mod) {
    try {
      var terminalInput = document.getElementById("terminal-input-text-box");
      if (terminalInput == null) {
        return;
      }
      terminalInput.focus();

      var inputLength = terminalInput.value.length;
      var start = terminalInput.selectionStart;
      var inputText = terminalInput.value;

      switch (mod.toLowerCase()) {
        case "backspace":
          if (start > 0 && start <= inputLength + 1) {
            terminalInput.value = inputText.substr(0, start - 1) + inputText.substr(start);
          }
          break;
        case "deletewordbefore": // Delete rest of word before the cursor
          for (var delStart = start - 1; delStart > 0; --delStart) {
            if (inputText.charAt(delStart) === " ") {
              terminalInput.value = inputText.substr(0, delStart) + inputText.substr(start);
              return;
            }
          }
          break;
        case "deletewordafter": // Delete rest of word after the cursor
          for (var delStart = start + 1; delStart <= text.length + 1; ++delStart) {
            if (inputText.charAt(delStart) === " ") {
              terminalInput.value = inputText.substr(0, start) + inputText.substr(delStart);
              return;
            }
          }
          break;
        case "clearafter": // Deletes everything after cursor
          break;
        case "clearbefore:": // Deleetes everything before cursor
          break;
      }
    } catch (e) {
      console.error("Exception in Terminal.modifyInput: " + e);
    }
  },

  moveTextCursor: function (loc) {
    try {
      var terminalInput = document.getElementById("terminal-input-text-box");
      if (terminalInput == null) {
        return;
      }
      terminalInput.focus();

      var inputLength = terminalInput.value.length;
      var start = terminalInput.selectionStart;

      switch (loc.toLowerCase()) {
        case "home":
          terminalInput.setSelectionRange(0, 0);
          break;
        case "end":
          terminalInput.setSelectionRange(inputLength, inputLength);
          break;
        case "prevchar":
          if (start > 0) {
            terminalInput.setSelectionRange(start - 1, start - 1);
          }
          break;
        case "prevword":
          for (var i = start - 2; i >= 0; --i) {
            if (terminalInput.value.charAt(i) === " ") {
              terminalInput.setSelectionRange(i + 1, i + 1);
              return;
            }
          }
          terminalInput.setSelectionRange(0, 0);
          break;
        case "nextchar":
          terminalInput.setSelectionRange(start + 1, start + 1);
          break;
        case "nextword":
          for (var i = start + 1; i <= inputLength; ++i) {
            if (terminalInput.value.charAt(i) === " ") {
              terminalInput.setSelectionRange(i, i);
              return;
            }
          }
          terminalInput.setSelectionRange(inputLength, inputLength);
          break;
        default:
          console.warn("Invalid loc argument in Terminal.moveTextCursor()");
          break;
      }
    } catch (e) {
      console.error("Exception in Terminal.moveTextCursor: " + e);
    }
  },

  startHack: function () {
    Terminal.hackFlag = true;

    // Hacking through Terminal should be faster than hacking through a script
    Terminal.actionTime = calculateHackingTime(Player.getCurrentServer(), Player) / 4;
    Terminal.startAction();
  },

  startBackdoor: function () {
    Terminal.backdoorFlag = true;

    // Backdoor should take the same amount of time as hack
    Terminal.actionTime = calculateHackingTime(Player.getCurrentServer(), Player) / 4;
    Terminal.startAction();
  },

  startAnalyze: function () {
    Terminal.analyzeFlag = true;
    Terminal.actionTime = 1;
    post("Analyzing system...");
    Terminal.startAction();
  },

  startAction: function () {
    Terminal.actionStarted = true;

    hackProgressPost("Time left:");
    hackProgressBarPost("[");

    // Disable terminal
    document.getElementById("terminal-input-td").innerHTML = '<input type="text" class="terminal-input"/>';
    $("input[class=terminal-input]").prop("disabled", true);
  },

  finishAction: function (cancelled = false) {
    if (Terminal.hackFlag) {
      Terminal.finishHack(cancelled);
    } else if (Terminal.backdoorFlag) {
      Terminal.finishBackdoor(cancelled);
    } else if (Terminal.analyzeFlag) {
      Terminal.finishAnalyze(cancelled);
    }

    // Rename the progress bar so that the next hacks dont trigger it. Re-enable terminal
    $("#hack-progress-bar").attr("id", "old-hack-progress-bar");
    $("#hack-progress").attr("id", "old-hack-progress");
    Terminal.resetTerminalInput();
    $("input[class=terminal-input]").prop("disabled", false);
  },

  // Complete the hack/analyze command
  finishHack: function (cancelled = false) {
    if (!cancelled) {
      var server = Player.getCurrentServer();

      // Calculate whether hack was successful
      var hackChance = calculateHackingChance(server, Player);
      var rand = Math.random();
      var expGainedOnSuccess = calculateHackingExpGain(server, Player);
      var expGainedOnFailure = expGainedOnSuccess / 4;
      if (rand < hackChance) {
        // Success!
        if (
          SpecialServerIps[SpecialServerNames.WorldDaemon] &&
          SpecialServerIps[SpecialServerNames.WorldDaemon] == server.ip
        ) {
          if (Player.bitNodeN == null) {
            Player.bitNodeN = 1;
          }
          hackWorldDaemon(Player.bitNodeN);
          Terminal.hackFlag = false;
          return;
        }
        server.backdoorInstalled = true;
        var moneyGained = calculatePercentMoneyHacked(server, Player);
        moneyGained = Math.floor(server.moneyAvailable * moneyGained);

        if (moneyGained <= 0) {
          moneyGained = 0;
        } // Safety check

        server.moneyAvailable -= moneyGained;
        Player.gainMoney(moneyGained);
        Player.recordMoneySource(moneyGained, "hacking");
        Player.gainHackingExp(expGainedOnSuccess);
        Player.gainIntelligenceExp(expGainedOnSuccess / CONSTANTS.IntelligenceTerminalHackBaseExpGain);

        server.fortify(CONSTANTS.ServerFortifyAmount);

        postElement(
          <>
            Hack successful! Gained <Money money={moneyGained} /> and {numeralWrapper.formatExp(expGainedOnSuccess)}{" "}
            hacking exp
          </>,
        );
      } else {
        // Failure
        // Player only gains 25% exp for failure? TODO Can change this later to balance
        Player.gainHackingExp(expGainedOnFailure);
        post(`Failed to hack ${server.hostname}. Gained ${numeralWrapper.formatExp(expGainedOnFailure)} hacking exp`);
      }
    }
    Terminal.hackFlag = false;
  },

  finishBackdoor: function (cancelled = false) {
    if (!cancelled) {
      const server = Player.getCurrentServer();
      if (
        SpecialServerIps[SpecialServerNames.WorldDaemon] &&
        SpecialServerIps[SpecialServerNames.WorldDaemon] == server.ip
      ) {
        if (Player.bitNodeN == null) {
          Player.bitNodeN = 1;
        }
        hackWorldDaemon(Player.bitNodeN);
        Terminal.backdoorFlag = false;
        return;
      }
      server.backdoorInstalled = true;
      postElement(<>Backdoor successful!</>);
    }
    Terminal.backdoorFlag = false;
  },

  finishAnalyze: function (cancelled = false) {
    if (!cancelled) {
      let currServ = Player.getCurrentServer();
      const isHacknet = currServ instanceof HacknetServer;
      post(currServ.hostname + ": ");
      const org = currServ.organizationName;
      post("Organization name: " + (!isHacknet ? org : "Player"));
      let hasAdminRights = (!isHacknet && currServ.hasAdminRights) || isHacknet;
      post("Root Access: " + (hasAdminRights ? "YES" : "NO"));
      const hackingSkill = currServ.requiredHackingSkill;
      post("Required hacking skill: " + (!isHacknet ? hackingSkill : "N/A"));
      const security = currServ.hackDifficulty;
      post("Server security level: " + (!isHacknet ? numeralWrapper.formatServerSecurity(security) : "N/A"));
      const hackingChance = calculateHackingChance(currServ, Player);
      post("Chance to hack: " + (!isHacknet ? numeralWrapper.formatPercentage(hackingChance) : "N/A"));
      const hackingTime = calculateHackingTime(currServ, Player) * 1000;
      post("Time to hack: " + (!isHacknet ? convertTimeMsToTimeElapsedString(hackingTime, true) : "N/A"));
      postElement(
        <>Total money available on server: {!isHacknet ? <Money money={currServ.moneyAvailable} /> : "N/A"}</>,
      );
      const numPort = currServ.numOpenPortsRequired;
      post("Required number of open ports for NUKE: " + (!isHacknet ? numPort : "N/A"));
      post("SSH port: " + (currServ.sshPortOpen ? "Open" : "Closed"));
      post("FTP port: " + (currServ.ftpPortOpen ? "Open" : "Closed"));
      post("SMTP port: " + (currServ.smtpPortOpen ? "Open" : "Closed"));
      post("HTTP port: " + (currServ.httpPortOpen ? "Open" : "Closed"));
      post("SQL port: " + (currServ.sqlPortOpen ? "Open" : "Closed"));
    }
    Terminal.analyzeFlag = false;
  },

  executeCommands: function (commands) {
    // Sanitize input
    commands = commands.trim();
    commands = commands.replace(/\s\s+/g, " "); // Replace all extra whitespace in command with a single space

    // Handle Terminal History - multiple commands should be saved as one
    if (Terminal.commandHistory[Terminal.commandHistory.length - 1] != commands) {
      Terminal.commandHistory.push(commands);
      if (Terminal.commandHistory.length > 50) {
        Terminal.commandHistory.splice(0, 1);
      }
    }
    Terminal.commandHistoryIndex = Terminal.commandHistory.length;

    // Split commands and execute sequentially
    commands = commands
      .match(/(?:'[^']*'|"[^"]*"|[^;"])*/g)
      .map(substituteAliases)
      .map((c) => c.match(/(?:'[^']*'|"[^"]*"|[^;"])*/g))
      .flat();
    for (let i = 0; i < commands.length; i++) {
      if (commands[i].match(/^\s*$/)) {
        continue;
      } // Don't run commands that only have whitespace
      Terminal.executeCommand(commands[i].trim());
    }
  },

  parseCommandArguments: function (command) {
    // This will be used to keep track of whether we're in a quote. This is for situations
    // like the alias command:
    //      alias run="run NUKE.exe"
    // We want the run="run NUKE.exe" to be parsed as a single command, so this flag
    // will keep track of whether we have a quote in
    let inQuote = ``;

    // Returns an array with the command and its arguments in each index
    // Properly handles quotation marks (e.g. `run foo.script "the sun"` will return [run, foo.script, the sun])
    const args = [];
    let start = 0,
      i = 0;
    let prevChar = ""; // Previous character
    while (i < command.length) {
      let escaped = false; // Check for escaped quotation marks
      if (i >= 1) {
        prevChar = command.charAt(i - 1);
        if (prevChar === "\\") {
          escaped = true;
        }
      }

      const c = command.charAt(i);
      if (c === '"') {
        // Double quotes
        if (!escaped && prevChar === " ") {
          const endQuote = command.indexOf('"', i + 1);
          if (endQuote !== -1 && (endQuote === command.length - 1 || command.charAt(endQuote + 1) === " ")) {
            args.push(command.substr(i + 1, endQuote - i - 1));
            if (endQuote === command.length - 1) {
              start = i = endQuote + 1;
            } else {
              start = i = endQuote + 2; // Skip the space
            }
            continue;
          }
        } else {
          if (inQuote === ``) {
            inQuote = `"`;
          } else if (inQuote === `"`) {
            inQuote = ``;
          }
        }
      } else if (c === "'") {
        // Single quotes, same thing as above
        if (!escaped && prevChar === " ") {
          const endQuote = command.indexOf("'", i + 1);
          if (endQuote !== -1 && (endQuote === command.length - 1 || command.charAt(endQuote + 1) === " ")) {
            args.push(command.substr(i + 1, endQuote - i - 1));
            if (endQuote === command.length - 1) {
              start = i = endQuote + 1;
            } else {
              start = i = endQuote + 2; // Skip the space
            }
            continue;
          }
        } else {
          if (inQuote === ``) {
            inQuote = `'`;
          } else if (inQuote === `'`) {
            inQuote = ``;
          }
        }
      } else if (c === " " && inQuote === ``) {
        let arg = command.substr(start, i - start);

        // If this is a number, convert it from a string to number
        if (isNumber(arg)) {
          args.push(parseFloat(arg));
        } else {
          args.push(arg);
        }

        start = i + 1;
      }
      ++i;
    }

    // Add the last argument
    if (start !== i) {
      let arg = command.substr(start, i - start);

      // If this is a number, convert it from string to number
      if (isNumber(arg)) {
        args.push(parseFloat(arg));
      } else {
        args.push(arg);
      }
    }

    return args;
  },

  executeCommand: function (command) {
    if (Terminal.hackFlag || Terminal.backdoorFlag || Terminal.analyzeFlag) {
      postError(`Cannot execute command (${command}) while an action is in progress`);
      return;
    }

    // Allow usage of ./
    if (command.startsWith("./")) {
      command = "run " + command.slice(2);
    }

    // Only split the first space
    var commandArray = Terminal.parseCommandArguments(command);
    if (commandArray.length == 0) {
      return;
    }

    /****************** Interactive Tutorial Terminal Commands ******************/
    if (ITutorial.isRunning) {
      var n00dlesServ = GetServerByHostname("n00dles");
      if (n00dlesServ == null) {
        throw new Error("Could not get n00dles server");
        return;
      }

      switch (ITutorial.currStep) {
        case iTutorialSteps.TerminalHelp:
          if (commandArray.length === 1 && commandArray[0] == "help") {
            post(TerminalHelpText);
            iTutorialNextStep();
          } else {
            post("Bad command. Please follow the tutorial");
          }
          break;
        case iTutorialSteps.TerminalLs:
          if (commandArray.length === 1 && commandArray[0] == "ls") {
            Terminal.executeListCommand(commandArray);
            iTutorialNextStep();
          } else {
            post("Bad command. Please follow the tutorial");
          }
          break;
        case iTutorialSteps.TerminalScan:
          if (commandArray.length === 1 && commandArray[0] == "scan") {
            Terminal.executeScanCommand(commandArray);
            iTutorialNextStep();
          } else {
            post("Bad command. Please follow the tutorial");
          }
          break;
        case iTutorialSteps.TerminalScanAnalyze1:
          if (commandArray.length == 1 && commandArray[0] == "scan-analyze") {
            Terminal.executeScanAnalyzeCommand(1);
            iTutorialNextStep();
          } else {
            post("Bad command. Please follow the tutorial");
          }
          break;
        case iTutorialSteps.TerminalScanAnalyze2:
          if (commandArray.length == 2 && commandArray[0] == "scan-analyze" && commandArray[1] === 2) {
            Terminal.executeScanAnalyzeCommand(2);
            iTutorialNextStep();
          } else {
            post("Bad command. Please follow the tutorial");
          }
          break;
        case iTutorialSteps.TerminalConnect:
          if (commandArray.length == 2) {
            if (commandArray[0] == "connect" && (commandArray[1] == "n00dles" || commandArray[1] == n00dlesServ.ip)) {
              Player.getCurrentServer().isConnectedTo = false;
              Player.currentServer = n00dlesServ.ip;
              Player.getCurrentServer().isConnectedTo = true;
              post("Connected to n00dles");
              iTutorialNextStep();
            } else {
              post("Wrong command! Try again!");
              return;
            }
          } else {
            post("Bad command. Please follow the tutorial");
          }
          break;
        case iTutorialSteps.TerminalAnalyze:
          if (commandArray.length === 1 && commandArray[0] === "analyze") {
            if (commandArray.length !== 1) {
              post("Incorrect usage of analyze command. Usage: analyze");
              return;
            }
            Terminal.startAnalyze();
            iTutorialNextStep();
          } else {
            post("Bad command. Please follow the tutorial");
          }
          break;
        case iTutorialSteps.TerminalNuke:
          if (commandArray.length == 2 && commandArray[0] == "run" && commandArray[1] == "NUKE.exe") {
            n00dlesServ.hasAdminRights = true;
            post("NUKE successful! Gained root access to n00dles");
            iTutorialNextStep();
          } else {
            post("Bad command. Please follow the tutorial");
          }
          break;
        case iTutorialSteps.TerminalManualHack:
          if (commandArray.length == 1 && commandArray[0] == "hack") {
            Terminal.startHack();
            iTutorialNextStep();
          } else {
            post("Bad command. Please follow the tutorial");
          }
          break;
        case iTutorialSteps.TerminalCreateScript:
          if (commandArray.length == 2 && commandArray[0] == "nano" && commandArray[1] == "n00dles.script") {
            Engine.loadScriptEditorContent("n00dles.script", "");
            iTutorialNextStep();
          } else {
            post("Bad command. Please follow the tutorial");
          }
          break;
        case iTutorialSteps.TerminalFree:
          if (commandArray.length == 1 && commandArray[0] == "free") {
            Terminal.executeFreeCommand(commandArray);
            iTutorialNextStep();
          } else {
            post("Bad command. Please follow the tutorial");
          }
          break;
        case iTutorialSteps.TerminalRunScript:
          if (commandArray.length == 2 && commandArray[0] == "run" && commandArray[1] == "n00dles.script") {
            Terminal.runScript(commandArray);
            iTutorialNextStep();
          } else {
            post("Bad command. Please follow the tutorial");
          }
          break;
        case iTutorialSteps.ActiveScriptsToTerminal:
          if (commandArray.length == 2 && commandArray[0] == "tail" && commandArray[1] == "n00dles.script") {
            // Check that the script exists on this machine
            var runningScript = findRunningScript("n00dles.script", [], Player.getCurrentServer());
            if (runningScript == null) {
              post("Error: No such script exists");
              return;
            }
            logBoxCreate(runningScript);
            iTutorialNextStep();
          } else {
            post("Bad command. Please follow the tutorial");
          }
          break;
        default:
          post("Please follow the tutorial, or click 'Exit Tutorial' if you'd like to skip it");
          return;
      }
      return;
    }

    /****************** END INTERACTIVE TUTORIAL ******************/

    /* Command parser */
    var s = Player.getCurrentServer();
    switch (commandArray[0].toLowerCase()) {
      case "alias":
        if (commandArray.length === 1) {
          printAliases();
          return;
        }
        if (commandArray.length === 2) {
          if (parseAliasDeclaration(commandArray[1])) {
            post(`Set alias ${commandArray[1]}`);
            return;
          }
        }
        if (commandArray.length === 3) {
          if (commandArray[1] === "-g") {
            if (parseAliasDeclaration(commandArray[2], true)) {
              post(`Set global alias ${commandArray[2]}`);
              return;
            }
          }
        }
        postError('Incorrect usage of alias command. Usage: alias [-g] [aliasname="value"]');
        break;
      case "analyze":
        if (commandArray.length !== 1) {
          post("Incorrect usage of analyze command. Usage: analyze");
          return;
        }
        Terminal.startAnalyze();
        break;
      case "backdoor":
        if (commandArray.length !== 1) {
          post("Incorrect usage of backdoor command. Usage: backdoor");
          return;
        }

        if (s.purchasedByPlayer) {
          postError(
            "Cannot use backdoor on your own machines! You are currently connected to your home PC or one of your purchased servers",
          );
        } else if (!s.hasAdminRights) {
          postError("You do not have admin rights for this machine! Cannot backdoor");
        } else if (s.requiredHackingSkill > Player.hacking_skill) {
          postError(
            "Your hacking skill is not high enough to use backdoor on this machine. Try analyzing the machine to determine the required hacking skill",
          );
        } else if (s instanceof HacknetServer) {
          postError("Cannot use backdoor on this type of Server");
        } else {
          Terminal.startBackdoor();
        }
        break;
      case "buy":
        if (SpecialServerIps.hasOwnProperty("Darkweb Server")) {
          executeDarkwebTerminalCommand(commandArray);
        } else {
          postError(
            "You need to be able to connect to the Dark Web to use the buy command. (Maybe there's a TOR router you can buy somewhere)",
          );
        }
        break;
      case "cat": {
        try {
          if (commandArray.length !== 2) {
            postError("Incorrect usage of cat command. Usage: cat [file]");
            return;
          }
          const filename = Terminal.getFilepath(commandArray[1]);
          if (!filename.endsWith(".msg") && !filename.endsWith(".lit") && !filename.endsWith(".txt")) {
            postError(
              "Only .msg, .txt, and .lit files are viewable with cat (filename must end with .msg, .txt, or .lit)",
            );
            return;
          }

          if (filename.endsWith(".msg") || filename.endsWith(".lit")) {
            for (let i = 0; i < s.messages.length; ++i) {
              if (filename.endsWith(".lit") && s.messages[i] === filename) {
                showLiterature(s.messages[i]);
                return;
              } else if (filename.endsWith(".msg") && s.messages[i].filename === filename) {
                showMessage(s.messages[i]);
                return;
              }
            }
          } else if (filename.endsWith(".txt")) {
            const txt = Terminal.getTextFile(filename);
            if (txt != null) {
              txt.show();
              return;
            }
          }

          postError(`No such file ${filename}`);
        } catch (e) {
          Terminal.postThrownError(e);
        }
        break;
      }
      case "cd": {
        if (commandArray.length > 2) {
          postError("Incorrect number of arguments. Usage: cd [dir]");
        } else {
          let dir = commandArray.length === 2 ? commandArray[1] : "/";

          let evaledDir;
          if (dir === "/") {
            evaledDir = "/";
          } else {
            // Ignore trailing slashes
            dir = removeTrailingSlash(dir);

            evaledDir = evaluateDirectoryPath(dir, Terminal.currDir);
            if (evaledDir == null || evaledDir === "") {
              postError("Invalid path. Failed to change directories");
              return;
            }

            const server = Player.getCurrentServer();
            if (
              !server.scripts.some((script) => script.filename.startsWith(evaledDir)) &&
              !server.textFiles.some((file) => file.fn.startsWith(evaledDir))
            ) {
              postError("Invalid path. Failed to change directories");
              return;
            }
          }

          Terminal.currDir = evaledDir;

          // Reset input to update current directory on UI
          Terminal.resetTerminalInput();
        }
        break;
      }
      case "check": {
        try {
          if (commandArray.length < 2) {
            postError("Incorrect number of arguments. Usage: check [script] [arg1] [arg2]...");
          } else {
            const scriptName = Terminal.getFilepath(commandArray[1]);
            // Can only tail script files
            if (!isScriptFilename(scriptName)) {
              postError("tail can only be called on .script files (filename must end with .script)");
              return;
            }

            // Get args
            let args = [];
            for (var i = 2; i < commandArray.length; ++i) {
              args.push(commandArray[i]);
            }

            // Check that the script exists on this machine
            var runningScript = findRunningScript(scriptName, args, s);
            if (runningScript == null) {
              postError("No such script exists");
              return;
            }
            runningScript.displayLog();
          }
        } catch (e) {
          Terminal.postThrownError(e);
        }

        break;
      }
      case "clear":
      case "cls":
        if (commandArray.length !== 1) {
          postError("Incorrect usage of clear/cls command. Usage: clear/cls");
          return;
        }
        $("#terminal tr:not(:last)").remove();
        postNetburnerText();
        break;
      case "connect": {
        // Disconnect from current server in terminal and connect to new one
        if (commandArray.length !== 2) {
          postError("Incorrect usage of connect command. Usage: connect [ip/hostname]");
          return;
        }

        let ip = commandArray[1];

        for (let i = 0; i < s.serversOnNetwork.length; i++) {
          if (getServerOnNetwork(s, i).ip == ip || getServerOnNetwork(s, i).hostname == ip) {
            Terminal.connectToServer(ip);
            return;
          }
        }

        postError("Host not found");
        break;
      }
      case "download": {
        try {
          if (commandArray.length !== 2) {
            postError("Incorrect usage of download command. Usage: download [script/text file]");
            return;
          }
          const fn = commandArray[1];
          if (fn === "*" || fn === "*.script" || fn === "*.txt") {
            // Download all scripts as a zip
            var zip = new JSZip();
            if (fn === "*" || fn === "*.script") {
              for (var i = 0; i < s.scripts.length; ++i) {
                var file = new Blob([s.scripts[i].code], {
                  type: "text/plain",
                });
                zip.file(s.scripts[i].filename + ".js", file);
              }
            }
            if (fn === "*" || fn === "*.txt") {
              for (var i = 0; i < s.textFiles.length; ++i) {
                var file = new Blob([s.textFiles[i].text], {
                  type: "text/plain",
                });
                zip.file(s.textFiles[i].fn, file);
              }
            }

            let zipFn;
            switch (fn) {
              case "*.script":
                zipFn = "bitburnerScripts.zip";
                break;
              case "*.txt":
                zipFn = "bitburnerTexts.zip";
                break;
              default:
                zipFn = "bitburnerFiles.zip";
                break;
            }

            zip.generateAsync({ type: "blob" }).then(function (content) {
              FileSaver.saveAs(content, zipFn);
            });
            return;
          } else if (isScriptFilename(fn)) {
            // Download a single script
            const script = Terminal.getScript(fn);
            if (script != null) {
              return script.download();
            }
          } else if (fn.endsWith(".txt")) {
            // Download a single text file
            const txt = Terminal.getTextFile(fn);
            if (txt != null) {
              return txt.download();
            }
          } else {
            postError(`Cannot download this filetype`);
            return;
          }
          postError(`${fn} does not exist`);
        } catch (e) {
          Terminal.postThrownError(e);
        }
        break;
      }
      case "expr": {
        if (commandArray.length <= 1) {
          postError("Incorrect usage of expr command. Usage: expr [math expression]");
          return;
        }
        let expr = commandArray.slice(1).join("");

        // Sanitize the math expression
        let sanitizedExpr = expr.replace(/s+/g, "").replace(/[^-()\d/*+.]/g, "");
        let result;
        try {
          result = eval(sanitizedExpr);
        } catch (e) {
          postError(`Could not evaluate expression: ${sanitizedExpr}`);
          return;
        }
        post(result);
        break;
      }
      case "free":
        Terminal.executeFreeCommand(commandArray);
        break;
      case "hack": {
        if (commandArray.length !== 1) {
          postError("Incorrect usage of hack command. Usage: hack");
          return;
        }
        // Hack the current PC (usually for money)
        // You can't hack your home pc or servers you purchased
        if (s.purchasedByPlayer) {
          postError(
            "Cannot hack your own machines! You are currently connected to your home PC or one of your purchased servers",
          );
        } else if (s.hasAdminRights == false) {
          postError("You do not have admin rights for this machine! Cannot hack");
        } else if (s.requiredHackingSkill > Player.hacking_skill) {
          postError(
            "Your hacking skill is not high enough to attempt hacking this machine. Try analyzing the machine to determine the required hacking skill",
          );
        } else if (s instanceof HacknetServer) {
          postError("Cannot hack this type of Server");
        } else {
          Terminal.startHack();
        }
        break;
      }
      case "help":
        if (commandArray.length !== 1 && commandArray.length !== 2) {
          postError("Incorrect usage of help command. Usage: help");
          return;
        }
        if (commandArray.length === 1) {
          post(TerminalHelpText);
        } else {
          var cmd = commandArray[1];
          var txt = HelpTexts[cmd];
          if (txt == null) {
            postError("No help topics match '" + cmd + "'");
            return;
          }
          post(txt);
        }
        break;
      case "home":
        if (commandArray.length !== 1) {
          postError("Incorrect usage of home command. Usage: home");
          return;
        }
        Player.getCurrentServer().isConnectedTo = false;
        Player.currentServer = Player.getHomeComputer().ip;
        Player.getCurrentServer().isConnectedTo = true;
        post("Connected to home");
        Terminal.currDir = "/";
        Terminal.resetTerminalInput();
        break;
      case "hostname":
        if (commandArray.length !== 1) {
          postError("Incorrect usage of hostname command. Usage: hostname");
          return;
        }
        post(Player.getCurrentServer().hostname);
        break;
      case "ifconfig":
        if (commandArray.length !== 1) {
          postError("Incorrect usage of ifconfig command. Usage: ifconfig");
          return;
        }
        post(Player.getCurrentServer().ip);
        break;
      case "kill": {
        Terminal.executeKillCommand(commandArray);
        break;
      }
      case "killall": {
        for (let i = s.runningScripts.length - 1; i >= 0; --i) {
          killWorkerScript(s.runningScripts[i], s.ip, false);
        }
        WorkerScriptStartStopEventEmitter.emitEvent();
        post("Killing all running scripts");
        break;
      }
      case "ls": {
        Terminal.executeListCommand(commandArray);
        break;
      }
      case "lscpu": {
        post(Player.getCurrentServer().cpuCores + " Core(s)");
        break;
      }
      case "mem": {
        Terminal.executeMemCommand(commandArray);
        break;
      }
      case "mv": {
        if (commandArray.length !== 3) {
          postError(`Incorrect number of arguments. Usage: mv [src] [dest]`);
          return;
        }

        try {
          const source = commandArray[1];
          const dest = commandArray[2];

          if (!isScriptFilename(source) && !source.endsWith(".txt")) {
            postError(`'mv' can only be used on scripts and text files (.txt)`);
            return;
          }

          const srcFile = Terminal.getFile(source);
          if (srcFile == null) {
            postError(`Source file ${source} does not exist`);
            return;
          }

          const sourcePath = Terminal.getFilepath(source);
          const destPath = Terminal.getFilepath(dest);

          const destFile = Terminal.getFile(dest);

          // 'mv' command only works on scripts and txt files.
          // Also, you can't convert between different file types
          if (isScriptFilename(source)) {
            if (!isScriptFilename(dest)) {
              postError(`Source and destination files must have the same type`);
              return;
            }

            // Command doesnt work if script is running
            if (s.isRunning(sourcePath)) {
              postError(`Cannot use 'mv' on a script that is running`);
              return;
            }

            if (destFile != null) {
              // Already exists, will be overwritten, so we'll delete it
              const status = s.removeFile(destPath);
              if (!status.res) {
                postError(`Something went wrong...please contact game dev (probably a bug)`);
                return;
              } else {
                post("Warning: The destination file was overwritten");
              }
            }

            srcFile.filename = destPath;
          } else if (source.endsWith(".txt")) {
            if (!dest.endsWith(".txt")) {
              postError(`Source and destination files must have the same type`);
              return;
            }

            if (destFile != null) {
              // Already exists, will be overwritten, so we'll delete it
              const status = s.removeFile(destPath);
              if (!status.res) {
                postError(`Something went wrong...please contact game dev (probably a bug)`);
                return;
              } else {
                post("Warning: The destination file was overwritten");
              }
            }

            srcFile.fn = destPath;
          }
        } catch (e) {
          Terminal.postThrownError(e);
        }

        break;
      }
      case "nano":
        Terminal.executeNanoCommand(commandArray);
        break;
      case "ps":
        if (commandArray.length !== 1) {
          postError("Incorrect usage of ps command. Usage: ps");
          return;
        }
        for (let i = 0; i < s.runningScripts.length; i++) {
          let rsObj = s.runningScripts[i];
          let res = `(PID - ${rsObj.pid}) ${rsObj.filename}`;
          for (let j = 0; j < rsObj.args.length; ++j) {
            res += " " + rsObj.args[j].toString();
          }
          post(res);
        }
        break;
      case "rm": {
        if (commandArray.length !== 2) {
          postError("Incorrect number of arguments. Usage: rm [program/script]");
          return;
        }

        // Check programs
        let delTarget, status;
        try {
          delTarget = Terminal.getFilepath(commandArray[1]);
          status = s.removeFile(delTarget);
        } catch (err) {
          status = {
            res: false,
            msg: "No such file exists",
          };
        }

        if (!status.res) {
          postError(status.msg);
        }
        break;
      }
      case "run":
        // Run a program or a script
        if (commandArray.length < 2) {
          postError("Incorrect number of arguments. Usage: run [program/script] [-t] [num threads] [arg1] [arg2]...");
        } else {
          var executableName = commandArray[1];

          // Secret Music player!
          if (executableName === "musicplayer") {
            post(
              '<iframe src="https://open.spotify.com/embed/user/danielyxie/playlist/1ORnnL6YNvXOracUaUV2kh" width="300" height="380" frameborder="0" allowtransparency="true"></iframe>',
              false,
            );
            return;
          }

          // Check if its a script or just a program/executable
          if (isScriptFilename(executableName)) {
            Terminal.runScript(commandArray);
          } else if (executableName.endsWith(".cct")) {
            Terminal.runContract(executableName);
          } else {
            Terminal.runProgram(commandArray);
          }
        }
        break;
      case "scan":
        Terminal.executeScanCommand(commandArray);
        break;
      case "scan-analyze":
        if (commandArray.length === 1) {
          Terminal.executeScanAnalyzeCommand(1);
        } else {
          // # of args must be 2 or 3
          if (commandArray.length > 3) {
            postError("Incorrect usage of scan-analyze command. usage: scan-analyze [depth]");
            return;
          }
          let all = false;
          if (commandArray.length === 3 && commandArray[2] === "-a") {
            all = true;
          }

          let depth = parseInt(commandArray[1]);

          if (isNaN(depth) || depth < 0) {
            postError("Incorrect usage of scan-analyze command. depth argument must be positive numeric");
            return;
          }
          if (
            depth > 3 &&
            !Player.hasProgram(Programs.DeepscanV1.name) &&
            !Player.hasProgram(Programs.DeepscanV2.name)
          ) {
            postError("You cannot scan-analyze with that high of a depth. Maximum depth is 3");
            return;
          } else if (depth > 5 && !Player.hasProgram(Programs.DeepscanV2.name)) {
            postError("You cannot scan-analyze with that high of a depth. Maximum depth is 5");
            return;
          } else if (depth > 10) {
            postError("You cannot scan-analyze with that high of a depth. Maximum depth is 10");
            return;
          }
          Terminal.executeScanAnalyzeCommand(depth, all);
        }
        break;
      /* eslint-disable no-case-declarations */
      case "scp":
        Terminal.executeScpCommand(commandArray);
        break;
      /* eslint-enable no-case-declarations */
      case "sudov":
        if (commandArray.length !== 1) {
          postError("Incorrect number of arguments. Usage: sudov");
          return;
        }

        if (s.hasAdminRights) {
          post("You have ROOT access to this machine");
        } else {
          post("You do NOT have root access to this machine");
        }
        break;
      case "tail": {
        try {
          if (commandArray.length < 2) {
            postError("Incorrect number of arguments. Usage: tail [script] [arg1] [arg2]...");
          } else if (typeof commandArray[1] === "string") {
            const scriptName = Terminal.getFilepath(commandArray[1]);
            if (!isScriptFilename(scriptName)) {
              postError("tail can only be called on .script, .ns, .js files, or by pid");
              return;
            }

            // Get script arguments
            const args = [];
            for (let i = 2; i < commandArray.length; ++i) {
              args.push(commandArray[i]);
            }

            // go over all the running scripts. If there's a perfect
            // match, use it!
            for (var i = 0; i < s.runningScripts.length; ++i) {
              if (s.runningScripts[i].filename === scriptName && compareArrays(s.runningScripts[i].args, args)) {
                logBoxCreate(s.runningScripts[i]);
                return;
              }
            }

            // Find all scripts that are potential candidates.
            const candidates = [];
            for (var i = 0; i < s.runningScripts.length; ++i) {
              // only scripts that have more arguments (equal arguments is already caught)
              if (s.runningScripts[i].args.length < args.length) continue;
              // make a smaller copy of the args.
              const args2 = s.runningScripts[i].args.slice(0, args.length);
              if (s.runningScripts[i].filename === scriptName && compareArrays(args2, args)) {
                candidates.push(s.runningScripts[i]);
              }
            }

            // If there's only 1 possible choice, use that.
            if (candidates.length === 1) {
              logBoxCreate(candidates[0]);
              return;
            }

            // otherwise lists all possible conflicting choices.
            if (candidates.length > 1) {
              postError("Found several potential candidates:");
              for (const candidate of candidates) postError(`${candidate.filename} ${candidate.args.join(" ")}`);
              postError("Script arguments need to be specified.");
              return;
            }

            // if there's no candidate then we just don't know.
            postError("No such script exists.");
          } else {
            const runningScript = findRunningScriptByPid(commandArray[1], Player.getCurrentServer());
            if (runningScript == null) {
              postError("No such script exists");
              return;
            }
            logBoxCreate(runningScript);
          }
        } catch (e) {
          Terminal.postThrownError(e);
        }

        break;
      }
      case "theme": {
        let args = commandArray.slice(1);
        if (args.length !== 1 && args.length !== 3) {
          postError("Incorrect number of arguments.");
          postError(
            "Usage: theme [default|muted|solarized] | #[background color hex] #[text color hex] #[highlight color hex]",
          );
        } else if (args.length === 1) {
          var themeName = args[0];
          if (themeName == "default") {
            document.body.style.setProperty("--my-highlight-color", "#ffffff");
            document.body.style.setProperty("--my-font-color", "#66ff33");
            document.body.style.setProperty("--my-background-color", "#000000");
            document.body.style.setProperty("--my-prompt-color", "#f92672");
          } else if (themeName == "muted") {
            document.body.style.setProperty("--my-highlight-color", "#ffffff");
            document.body.style.setProperty("--my-font-color", "#66ff33");
            document.body.style.setProperty("--my-background-color", "#252527");
          } else if (themeName == "solarized") {
            document.body.style.setProperty("--my-highlight-color", "#6c71c4");
            document.body.style.setProperty("--my-font-color", "#839496");
            document.body.style.setProperty("--my-background-color", "#002b36");
          } else {
            return postError("Theme not found");
          }
          FconfSettings.THEME_HIGHLIGHT_COLOR = document.body.style.getPropertyValue("--my-highlight-color");
          FconfSettings.THEME_FONT_COLOR = document.body.style.getPropertyValue("--my-font-color");
          FconfSettings.THEME_BACKGROUND_COLOR = document.body.style.getPropertyValue("--my-background-color");
          FconfSettings.THEME_PROMPT_COLOR = document.body.style.getPropertyValue("--my-prompt-color");
        } else {
          var inputBackgroundHex = args[0];
          var inputTextHex = args[1];
          var inputHighlightHex = args[2];
          if (
            /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(inputBackgroundHex) &&
            /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(inputTextHex) &&
            /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(inputHighlightHex)
          ) {
            document.body.style.setProperty("--my-highlight-color", inputHighlightHex);
            document.body.style.setProperty("--my-font-color", inputTextHex);
            document.body.style.setProperty("--my-background-color", inputBackgroundHex);
            FconfSettings.THEME_HIGHLIGHT_COLOR = document.body.style.getPropertyValue("--my-highlight-color");
            FconfSettings.THEME_FONT_COLOR = document.body.style.getPropertyValue("--my-font-color");
            FconfSettings.THEME_BACKGROUND_COLOR = document.body.style.getPropertyValue("--my-background-color");
          } else {
            return postError("Invalid Hex Input for theme");
          }
        }
        break;
      }
      case "top": {
        if (commandArray.length !== 1) {
          postError("Incorrect usage of top command. Usage: top");
          return;
        }

        // Headers
        const scriptWidth = 40;
        const pidWidth = 10;
        const threadsWidth = 16;

        const scriptTxt = "Script";
        const pidTxt = "PID";
        const threadsTxt = "Threads";
        const ramTxt = "RAM Usage";

        const spacesAfterScriptTxt = " ".repeat(scriptWidth - scriptTxt.length);
        const spacesAfterPidTxt = " ".repeat(pidWidth - pidTxt.length);
        const spacesAfterThreadsTxt = " ".repeat(threadsWidth - threadsTxt.length);

        const headers = `${scriptTxt}${spacesAfterScriptTxt}${pidTxt}${spacesAfterPidTxt}${threadsTxt}${spacesAfterThreadsTxt}${ramTxt}`;

        post(headers);

        let currRunningScripts = s.runningScripts;
        // Iterate through scripts on current server
        for (let i = 0; i < currRunningScripts.length; i++) {
          let script = currRunningScripts[i];

          // Calculate name padding
          const numSpacesScript = Math.max(0, scriptWidth - script.filename.length);
          const spacesScript = " ".repeat(numSpacesScript);

          // Calculate PID padding
          const numSpacesPid = Math.max(0, pidWidth - (script.pid + "").length);
          const spacesPid = " ".repeat(numSpacesPid);

          // Calculate thread padding
          const numSpacesThread = Math.max(0, threadsWidth - (script.threads + "").length);
          const spacesThread = " ".repeat(numSpacesThread);

          // Calculate and transform RAM usage
          const ramUsage = numeralWrapper.formatRAM(getRamUsageFromRunningScript(script) * script.threads);

          const entry = [
            script.filename,
            spacesScript,
            script.pid,
            spacesPid,
            script.threads,
            spacesThread,
            ramUsage,
          ].join("");
          post(entry);
        }
        break;
      }
      case "unalias": {
        if (commandArray.length !== 2) {
          postError("Incorrect usage of unalias name. Usage: unalias [alias]");
          return;
        } else {
          if (removeAlias(commandArray[1])) {
            post(`Removed alias ${commandArray[1]}`);
          } else {
            postError(`No such alias exists: ${commandArray[1]}`);
          }
        }
        break;
      }
      case "wget": {
        if (commandArray.length !== 3) {
          postError("Incorrect usage of wget command. Usage: wget [url] [target file]");
          return;
        }

        let url = commandArray[1];
        let target = Terminal.getFilepath(commandArray[2]);
        if (!isScriptFilename(target) && !target.endsWith(".txt")) {
          return post(`wget failed: Invalid target file. Target file must be script or text file`);
        }
        $.get(
          url,
          function (data) {
            let res;
            if (isScriptFilename(target)) {
              res = s.writeToScriptFile(target, data);
            } else {
              res = s.writeToTextFile(target, data);
            }
            if (!res.success) {
              return post("wget failed");
            }
            if (res.overwritten) {
              return post(`wget successfully retrieved content and overwrote ${target}`);
            }
            return post(`wget successfully retrieved content to new file ${target}`);
          },
          "text",
        ).fail(function (e) {
          return postError("wget failed: " + JSON.stringify(e));
        });
        break;
      }
      default:
        postError(`Command ${commandArray[0]} not found`);
    }
  },

  connectToServer: function (ip) {
    var serv = getServer(ip);
    if (serv == null) {
      post("Invalid server. Connection failed.");
      return;
    }
    Player.getCurrentServer().isConnectedTo = false;
    Player.currentServer = serv.ip;
    Player.getCurrentServer().isConnectedTo = true;
    post("Connected to " + serv.hostname);
    Terminal.currDir = "/";
    if (Player.getCurrentServer().hostname == "darkweb") {
      checkIfConnectedToDarkweb(); // Posts a 'help' message if connecting to dark web
    }
    Terminal.resetTerminalInput();
  },

  executeFreeCommand: function (commandArray) {
    if (commandArray.length !== 1) {
      postError("Incorrect usage of free command. Usage: free");
      return;
    }
    const ram = numeralWrapper.formatRAM(Player.getCurrentServer().maxRam);
    const used = numeralWrapper.formatRAM(Player.getCurrentServer().ramUsed);
    const avail = numeralWrapper.formatRAM(Player.getCurrentServer().maxRam - Player.getCurrentServer().ramUsed);
    const maxLength = Math.max(ram.length, Math.max(used.length, avail.length));
    const usedPercent = numeralWrapper.formatPercentage(
      Player.getCurrentServer().ramUsed / Player.getCurrentServer().maxRam,
    );

    post(`Total:     ${" ".repeat(maxLength - ram.length)}${ram}`);
    post(`Used:      ${" ".repeat(maxLength - used.length)}${used} (${usedPercent})`);
    post(`Available: ${" ".repeat(maxLength - avail.length)}${avail}`);
  },

  executeKillCommand: function (commandArray) {
    try {
      if (commandArray.length < 2) {
        postError("Incorrect usage of kill command. Usage: kill [scriptname] [arg1] [arg2]...");
        return;
      }

      // Kill by PID
      if (typeof commandArray[1] === "number") {
        const pid = commandArray[1];
        const res = killWorkerScript(pid);
        if (res) {
          post(`Killing script with PID ${pid}`);
        } else {
          post(`Failed to kill script with PID ${pid}. No such script exists`);
        }

        return;
      }

      const s = Player.getCurrentServer();
      const scriptName = Terminal.getFilepath(commandArray[1]);
      const args = [];
      for (let i = 2; i < commandArray.length; ++i) {
        args.push(commandArray[i]);
      }
      const runningScript = s.getRunningScript(scriptName, args);
      if (runningScript == null) {
        postError("No such script is running. Nothing to kill");
        return;
      }
      killWorkerScript(runningScript, s.ip);
      post(`Killing ${scriptName}`);
    } catch (e) {
      Terminal.postThrownError(e);
    }
  },

  executeListCommand: function (commandArray) {
    const numArgs = commandArray.length;
    function incorrectUsage() {
      postError("Incorrect usage of ls command. Usage: ls [dir] [| grep pattern]");
    }

    if (numArgs <= 0 || numArgs > 5 || numArgs === 3) {
      return incorrectUsage();
    }

    // Grep
    let filter = null; // Grep

    // Directory path
    let prefix = Terminal.currDir;
    if (!prefix.endsWith("/")) {
      prefix += "/";
    }

    // If there are 4+ arguments, then the last 3 must be for grep
    if (numArgs >= 4) {
      if (commandArray[numArgs - 2] !== "grep" || commandArray[numArgs - 3] !== "|") {
        return incorrectUsage();
      }
      filter = commandArray[numArgs - 1];
    }

    // If the second argument is not a pipe, then it must be for listing a directory
    if (numArgs >= 2 && commandArray[1] !== "|") {
      prefix = evaluateDirectoryPath(commandArray[1], Terminal.currDir);
      if (prefix != null) {
        if (!prefix.endsWith("/")) {
          prefix += "/";
        }
        if (!isValidDirectoryPath(prefix)) {
          return incorrectUsage();
        }
      }
    }

    // Root directory, which is the same as no 'prefix' at all
    if (prefix === "/") {
      prefix = null;
    }

    // Display all programs and scripts
    const allPrograms = [];
    const allScripts = [];
    const allTextFiles = [];
    const allContracts = [];
    const allMessages = [];
    const folders = [];

    function handleFn(fn, dest) {
      let parsedFn = fn;
      if (prefix) {
        if (!fn.startsWith(prefix)) {
          return;
        } else {
          parsedFn = fn.slice(prefix.length, fn.length);
        }
      }

      if (filter && !parsedFn.includes(filter)) {
        return;
      }

      // If the fn includes a forward slash, it must be in a subdirectory.
      // Therefore, we only list the "first" directory in its path
      if (parsedFn.includes("/")) {
        const firstParentDir = getFirstParentDirectory(parsedFn);
        if (filter && !firstParentDir.includes(filter)) {
          return;
        }

        if (!folders.includes(firstParentDir)) {
          folders.push(firstParentDir);
        }

        return;
      }

      dest.push(parsedFn);
    }

    // Get all of the programs and scripts on the machine into one temporary array
    const s = Player.getCurrentServer();
    for (const program of s.programs) handleFn(program, allPrograms);
    for (const script of s.scripts) handleFn(script.filename, allScripts);
    for (const txt of s.textFiles) handleFn(txt.fn, allTextFiles);
    for (const contract of s.contracts) handleFn(contract.fn, allContracts);
    for (const msgOrLit of s.messages)
      msgOrLit instanceof Message ? handleFn(msgOrLit.filename, allMessages) : handleFn(msgOrLit, allMessages);

    // Sort the files/folders alphabetically then print each
    allPrograms.sort();
    allScripts.sort();
    allTextFiles.sort();
    allContracts.sort();
    allMessages.sort();
    folders.sort();

    function postSegments(segments, config) {
      const maxLength = Math.max(...segments.map((s) => s.length)) + 1;
      const filesPerRow = Math.floor(80 / maxLength);
      for (let i = 0; i < segments.length; i++) {
        let row = "";
        for (let col = 0; col < filesPerRow; col++) {
          if (!(i < segments.length)) break;
          row += segments[i];
          row += " ".repeat(maxLength * (col + 1) - row.length);
          i++;
        }
        i--;
        postContent(row, config);
      }
    }

    const config = { color: "#0000FF" };
    const groups = [
      { segments: folders, config: config },
      { segments: allMessages },
      { segments: allTextFiles },
      { segments: allPrograms },
      { segments: allContracts },
      { segments: allScripts },
    ].filter((g) => g.segments.length > 0);
    for (let i = 0; i < groups.length; i++) {
      if (i !== 0) postElement(<br />);
      postSegments(groups[i].segments, groups[i].config);
    }
  },

  executeMemCommand: function (commandArray) {
    try {
      if (commandArray.length !== 2 && commandArray.length !== 4) {
        postError("Incorrect usage of mem command. usage: mem [scriptname] [-t] [number threads]");
        return;
      }

      const scriptName = commandArray[1];
      let numThreads = 1;
      if (commandArray.length === 4 && commandArray[2] === "-t") {
        numThreads = Math.round(parseInt(commandArray[3]));
        if (isNaN(numThreads) || numThreads < 1) {
          postError("Invalid number of threads specified. Number of threads must be greater than 1");
          return;
        }
      }

      const script = Terminal.getScript(scriptName);
      if (script == null) {
        postError("No such script exists!");
        return;
      }

      const ramUsage = script.ramUsage * numThreads;

      post(`This script requires ${numeralWrapper.formatRAM(ramUsage)} of RAM to run for ${numThreads} thread(s)`);
    } catch (e) {
      Terminal.postThrownError(e);
    }
  },

  executeNanoCommand: function (commandArray) {
    if (commandArray.length !== 2) {
      postError("Incorrect usage of nano command. Usage: nano [scriptname]");
      return;
    }

    try {
      const filename = commandArray[1];
      if (filename === ".fconf") {
        let text = createFconf();
        Engine.loadScriptEditorContent(filename, text);
        return;
      } else if (isScriptFilename(filename)) {
        const filepath = Terminal.getFilepath(filename);
        const script = Terminal.getScript(filename);
        if (script == null) {
          let code = "";
          if (filename.endsWith(".ns") || filename.endsWith(".js")) {
            code = `export async function main(ns) {

}`;
          }
          Engine.loadScriptEditorContent(filepath, code);
        } else {
          Engine.loadScriptEditorContent(filepath, script.code);
        }
      } else if (filename.endsWith(".txt")) {
        const filepath = Terminal.getFilepath(filename);
        const txt = Terminal.getTextFile(filename);
        if (txt == null) {
          Engine.loadScriptEditorContent(filepath);
        } else {
          Engine.loadScriptEditorContent(filepath, txt.text);
        }
      } else {
        postError(
          "Invalid file. Only scripts (.script, .ns, .js), text files (.txt), or .fconf can be edited with nano",
        );
        return;
      }
    } catch (e) {
      Terminal.postThrownError(e);
    }
  },

  executeScanCommand: function (commandArray) {
    if (commandArray.length !== 1) {
      postError("Incorrect usage of netstat/scan command. Usage: netstat/scan");
      return;
    }

    // Displays available network connections using TCP
    const currServ = Player.getCurrentServer();
    const servers = currServ.serversOnNetwork.map((_, i) => {
      const server = getServerOnNetwork(currServ, i);
      return {
        hostname: server.hostname,
        ip: server.ip,
        hasRoot: server.hasAdminRights ? "Y" : "N",
      };
    });
    servers.unshift({
      hostname: "Hostname",
      ip: "IP",
      hasRoot: "Root Access",
    });
    const maxHostname = Math.max(...servers.map((s) => s.hostname.length));
    const maxIP = Math.max(...servers.map((s) => s.ip.length));
    for (const server of servers) {
      let entry = server.hostname;
      entry += " ".repeat(maxHostname - server.hostname.length + 1);
      entry += server.ip;
      entry += " ".repeat(maxIP - server.ip.length + 1);
      entry += server.hasRoot;
      post(entry);
    }
  },

  executeScanAnalyzeCommand: function (depth = 1, all = false) {
    // TODO Using array as stack for now, can make more efficient
    post("~~~~~~~~~~ Beginning scan-analyze ~~~~~~~~~~");
    post(" ");

    // Map of all servers to keep track of which have been visited
    var visited = {};
    for (const ip in AllServers) {
      visited[ip] = 0;
    }

    const stack = [];
    const depthQueue = [0];
    const currServ = Player.getCurrentServer();
    stack.push(currServ);
    while (stack.length != 0) {
      const s = stack.pop();
      const d = depthQueue.pop();
      const isHacknet = s instanceof HacknetServer;
      if (!all && s.purchasedByPlayer && s.hostname != "home") {
        continue; // Purchased server
      } else if (visited[s.ip] || d > depth) {
        continue; // Already visited or out-of-depth
      } else if (!all && isHacknet) {
        continue; // Hacknet Server
      } else {
        visited[s.ip] = 1;
      }
      for (var i = s.serversOnNetwork.length - 1; i >= 0; --i) {
        stack.push(getServerOnNetwork(s, i));
        depthQueue.push(d + 1);
      }
      if (d == 0) {
        continue;
      } // Don't print current server
      var titleDashes = Array((d - 1) * 4 + 1).join("-");
      if (Player.hasProgram(Programs.AutoLink.name)) {
        post("<strong>" + titleDashes + "> <a class='scan-analyze-link'>" + s.hostname + "</a></strong>", false);
      } else {
        post("<strong>" + titleDashes + ">" + s.hostname + "</strong>");
      }

      var dashes = titleDashes + "--";
      var c = "NO";
      if (s.hasAdminRights) {
        c = "YES";
      }
      post(`${dashes}Root Access: ${c}${!isHacknet ? ", Required hacking skill: " + s.requiredHackingSkill : ""}`);
      if (!isHacknet) {
        post(dashes + "Number of open ports required to NUKE: " + s.numOpenPortsRequired);
      }
      post(dashes + "RAM: " + numeralWrapper.formatRAM(s.maxRam));
      post(" ");
    }

    var links = document.getElementsByClassName("scan-analyze-link");
    for (let i = 0; i < links.length; ++i) {
      (function () {
        var hostname = links[i].innerHTML.toString();
        links[i].onclick = function () {
          if (Terminal.analyzeFlag || Terminal.hackFlag || Terminal.backdoorFlag) {
            return;
          }
          Terminal.connectToServer(hostname);
        };
      })(); // Immediate invocation
    }
  },

  executeScpCommand(commandArray) {
    try {
      if (commandArray.length !== 3) {
        postError("Incorrect usage of scp command. Usage: scp [file] [destination hostname/ip]");
        return;
      }
      const scriptname = Terminal.getFilepath(commandArray[1]);
      if (!scriptname.endsWith(".lit") && !isScriptFilename(scriptname) && !scriptname.endsWith(".txt")) {
        postError("scp only works for scripts, text files (.txt), and literature files (.lit)");
        return;
      }

      const destServer = getServer(commandArray[2]);
      if (destServer == null) {
        postError(`Invalid destination. ${commandArray[2]} not found`);
        return;
      }
      const currServ = Player.getCurrentServer();

      // Scp for lit files
      if (scriptname.endsWith(".lit")) {
        var found = false;
        for (var i = 0; i < currServ.messages.length; ++i) {
          if (!(currServ.messages[i] instanceof Message) && currServ.messages[i] == scriptname) {
            found = true;
            break;
          }
        }

        if (!found) {
          return postError("No such file exists!");
        }

        for (var i = 0; i < destServer.messages.length; ++i) {
          if (destServer.messages[i] === scriptname) {
            post(scriptname + " copied over to " + destServer.hostname);
            return; // Already exists
          }
        }
        destServer.messages.push(scriptname);
        return post(scriptname + " copied over to " + destServer.hostname);
      }

      // Scp for txt files
      if (scriptname.endsWith(".txt")) {
        var found = false,
          txtFile;
        for (var i = 0; i < currServ.textFiles.length; ++i) {
          if (currServ.textFiles[i].fn === scriptname) {
            found = true;
            txtFile = currServ.textFiles[i];
            break;
          }
        }

        if (!found) {
          return postError("No such file exists!");
        }

        let tRes = destServer.writeToTextFile(txtFile.fn, txtFile.text);
        if (!tRes.success) {
          postError("scp failed");
          return;
        }
        if (tRes.overwritten) {
          post(`WARNING: ${scriptname} already exists on ${destServer.hostname} and will be overwriten`);
          post(`${scriptname} overwritten on ${destServer.hostname}`);
          return;
        }
        post(`${scriptname} copied over to ${destServer.hostname}`);
        return;
      }

      // Get the current script
      let sourceScript = null;
      for (let i = 0; i < currServ.scripts.length; ++i) {
        if (scriptname == currServ.scripts[i].filename) {
          sourceScript = currServ.scripts[i];
          break;
        }
      }
      if (sourceScript == null) {
        postError("scp() failed. No such script exists");
        return;
      }

      let sRes = destServer.writeToScriptFile(scriptname, sourceScript.code);
      if (!sRes.success) {
        postError(`scp failed`);
        return;
      }
      if (sRes.overwritten) {
        post(`WARNING: ${scriptname} already exists on ${destServer.hostname} and will be overwritten`);
        post(`${scriptname} overwritten on ${destServer.hostname}`);
        return;
      }
      post(`${scriptname} copied over to ${destServer.hostname}`);
    } catch (e) {
      Terminal.postThrownError(e);
    }
  },

  // First called when the "run [program]" command is called. Checks to see if you
  // have the executable and, if you do, calls the executeProgram() function
  runProgram: function (commandArray) {
    if (commandArray.length < 2) {
      return;
    }

    // Check if you have the program on your computer. If you do, execute it, otherwise
    // display an error message
    const programName = commandArray[1];

    if (Player.hasProgram(programName)) {
      Terminal.executeProgram(commandArray);
      return;
    }
    post("ERROR: No such executable on home computer (Only programs that exist on your home computer can be run)");
  },

  // Contains the implementations of all possible programs
  executeProgram: function (commandArray) {
    if (commandArray.length < 2) {
      return;
    }

    var s = Player.getCurrentServer();
    const programName = commandArray[1];
    const splitArgs = commandArray.slice(1);

    // TODO: refactor this/these out of Terminal. This logic could reside closer to the Programs themselves.
    /**
     * @typedef {function (server=, args=)} ProgramHandler
     * @param {Server} server The current server the program is being executed against
     * @param {string[]} args The command line arguments passed in to the program
     * @returns {void}
     */
    /**
     * @type {Object.<string, ProgramHandler}
     */
    const programHandlers = {};
    programHandlers[Programs.NukeProgram.name] = (server) => {
      if (server.hasAdminRights) {
        post("You already have root access to this computer. There is no reason to run NUKE.exe");
        return;
      }

      if (server.openPortCount >= Player.getCurrentServer().numOpenPortsRequired) {
        server.hasAdminRights = true;
        post("NUKE successful! Gained root access to " + Player.getCurrentServer().hostname);
        // TODO: Make this take time rather than be instant
        return;
      }

      post("NUKE unsuccessful. Not enough ports have been opened");
    };
    programHandlers[Programs.BruteSSHProgram.name] = (server) => {
      if (server.sshPortOpen) {
        post("SSH Port (22) is already open!");
        return;
      }

      server.sshPortOpen = true;
      post("Opened SSH Port(22)!");
      server.openPortCount++;
    };
    programHandlers[Programs.FTPCrackProgram.name] = (server) => {
      if (server.ftpPortOpen) {
        post("FTP Port (21) is already open!");
        return;
      }

      server.ftpPortOpen = true;
      post("Opened FTP Port (21)!");
      server.openPortCount++;
    };
    programHandlers[Programs.RelaySMTPProgram.name] = (server) => {
      if (server.smtpPortOpen) {
        post("SMTP Port (25) is already open!");
        return;
      }

      server.smtpPortOpen = true;
      post("Opened SMTP Port (25)!");
      server.openPortCount++;
    };
    programHandlers[Programs.HTTPWormProgram.name] = (server) => {
      if (server.httpPortOpen) {
        post("HTTP Port (80) is already open!");
        return;
      }

      server.httpPortOpen = true;
      post("Opened HTTP Port (80)!");
      server.openPortCount++;
    };
    programHandlers[Programs.SQLInjectProgram.name] = (server) => {
      if (server.sqlPortOpen) {
        post("SQL Port (1433) is already open!");
        return;
      }

      server.sqlPortOpen = true;
      post("Opened SQL Port (1433)!");
      server.openPortCount++;
    };
    programHandlers[Programs.ServerProfiler.name] = (server, args) => {
      if (args.length !== 2) {
        post("Must pass a server hostname or IP as an argument for ServerProfiler.exe");
        return;
      }

      const targetServer = getServer(args[1]);
      if (targetServer == null) {
        post("Invalid server IP/hostname");
        return;
      }

      if (targetServer instanceof HacknetServer) {
        post(`${Programs.ServerProfiler.name} cannot be run on a Hacknet Server.`);
        return;
      }

      post(targetServer.hostname + ":");
      post("Server base security level: " + targetServer.baseDifficulty);
      post("Server current security level: " + targetServer.hackDifficulty);
      post("Server growth rate: " + targetServer.serverGrowth);
      post(
        `Netscript hack() execution time: ${convertTimeMsToTimeElapsedString(
          calculateHackingTime(targetServer, Player) * 1000,
          true,
        )}`,
      );
      post(
        `Netscript grow() execution time: ${convertTimeMsToTimeElapsedString(
          calculateGrowTime(targetServer, Player) * 1000,
          true,
        )}`,
      );
      post(
        `Netscript weaken() execution time: ${convertTimeMsToTimeElapsedString(
          calculateWeakenTime(targetServer, Player) * 1000,
          true,
        )}`,
      );
    };
    programHandlers[Programs.AutoLink.name] = () => {
      post("This executable cannot be run.");
      post("AutoLink.exe lets you automatically connect to other servers when using 'scan-analyze'.");
      post("When using scan-analyze, click on a server's hostname to connect to it.");
    };
    programHandlers[Programs.DeepscanV1.name] = () => {
      post("This executable cannot be run.");
      post("DeepscanV1.exe lets you run 'scan-analyze' with a depth up to 5.");
    };
    programHandlers[Programs.DeepscanV2.name] = () => {
      post("This executable cannot be run.");
      post("DeepscanV2.exe lets you run 'scan-analyze' with a depth up to 10.");
    };
    programHandlers[Programs.Flight.name] = () => {
      const numAugReq = Math.round(BitNodeMultipliers.DaedalusAugsRequirement * 30);
      const fulfilled =
        Player.augmentations.length >= numAugReq && Player.money.gt(1e11) && Player.hacking_skill >= 2500;
      if (!fulfilled) {
        post(`Augmentations: ${Player.augmentations.length} / ${numAugReq}`);
        postElement(
          <>
            Money: <Money money={Player.money.toNumber()} /> / <Money money={1e11} />
          </>,
        );
        post(`Hacking skill: ${Player.hacking_skill} / 2500`);
        return;
      }

      post("We will contact you.");
      post("-- Daedalus --");
    };
    programHandlers[Programs.BitFlume.name] = () => {
      const popupId = "bitflume-popup";
      createPopup(popupId, BitFlumePopup, {
        player: Player,
        popupId: popupId,
      });
    };

    if (!programHandlers.hasOwnProperty(programName)) {
      post("Invalid executable. Cannot be run");
      return;
    }

    programHandlers[programName](s, splitArgs);
  },

  /**
   * Given a filename, returns that file's full path. This takes into account
   * the Terminal's current directory.
   */
  getFilepath: function (filename) {
    const path = evaluateFilePath(filename, Terminal.currDir);
    if (path == null) {
      throw new Error(`Invalid file path specified: ${filename}`);
    }

    if (isInRootDirectory(path)) {
      return removeLeadingSlash(path);
    }

    return path;
  },

  /**
   * Given a filename, searches and returns that file. File-type agnostic
   */
  getFile: function (filename) {
    if (isScriptFilename(filename)) {
      return Terminal.getScript(filename);
    }

    if (filename.endsWith(".lit")) {
      return Terminal.getLitFile(filename);
    }

    if (filename.endsWith(".txt")) {
      return Terminal.getTextFile(filename);
    }

    return null;
  },

  /**
   * Processes a file path referring to a literature file, taking into account the terminal's
   * current directory + server. Returns the lit file if it exists, and null otherwise
   */
  getLitFile: function (filename) {
    const s = Player.getCurrentServer();
    const filepath = Terminal.getFilepath(filename);
    for (const lit of s.messages) {
      if (typeof lit === "string" && filepath === lit) {
        return lit;
      }
    }

    return null;
  },

  /**
   * Processes a file path referring to a script, taking into account the terminal's
   * current directory + server. Returns the script if it exists, and null otherwise.
   */
  getScript: function (filename) {
    const s = Player.getCurrentServer();
    const filepath = Terminal.getFilepath(filename);
    for (const script of s.scripts) {
      if (filepath === script.filename) {
        return script;
      }
    }

    return null;
  },

  /**
   * Processes a file path referring to a text file, taking into account the terminal's
   * current directory + server. Returns the text file if it exists, and null otherwise.
   */
  getTextFile: function (filename) {
    const s = Player.getCurrentServer();
    const filepath = Terminal.getFilepath(filename);
    for (const txt of s.textFiles) {
      if (filepath === txt.fn) {
        return txt;
      }
    }

    return null;
  },

  postThrownError: function (e) {
    if (e instanceof Error) {
      const errorLabel = "Error: ";
      const errorString = e.toString();
      if (errorString.startsWith(errorLabel)) {
        postError(errorString.slice(errorLabel.length));
      } else {
        postError(errorString);
      }
    }
  },

  runScript: function (commandArray) {
    if (commandArray.length < 2) {
      dialogBoxCreate(
        `Bug encountered with Terminal.runScript(). Command array has a length of less than 2: ${commandArray}`,
      );
      return;
    }

    const server = Player.getCurrentServer();
    const scriptName = Terminal.getFilepath(commandArray[1]);

    const runArgs = { "--tail": Boolean, "-t": Number };
    const flags = libarg(runArgs, {
      permissive: true,
      argv: commandArray.slice(2),
    });
    const threadFlag = Math.round(parseFloat(flags["-t"]));
    const tailFlag = flags["--tail"] === true;
    if (flags["-t"] !== undefined && (threadFlag < 0 || isNaN(threadFlag))) {
      postError("Invalid number of threads specified. Number of threads must be greater than 0");
      return;
    }
    const numThreads = !isNaN(threadFlag) && threadFlag > 0 ? threadFlag : 1;
    const args = flags["_"];

    // Check if this script is already running
    if (findRunningScript(scriptName, args, server) != null) {
      post("ERROR: This script is already running. Cannot run multiple instances");
      return;
    }

    // Check if the script exists and if it does run it
    for (let i = 0; i < server.scripts.length; i++) {
      if (server.scripts[i].filename !== scriptName) {
        continue;
      }
      // Check for admin rights and that there is enough RAM availble to run
      const script = server.scripts[i];
      const ramUsage = script.ramUsage * numThreads;
      const ramAvailable = server.maxRam - server.ramUsed;

      if (!server.hasAdminRights) {
        post("Need root access to run script");
        return;
      }

      if (ramUsage > ramAvailable) {
        post(
          "This machine does not have enough RAM to run this script with " +
            numThreads +
            " threads. Script requires " +
            ramUsage +
            "GB of RAM",
        );
        return;
      }

      // Able to run script
      const runningScript = new RunningScript(script, args);
      runningScript.threads = numThreads;

      const success = startWorkerScript(runningScript, server);
      if (!success) {
        postError(`Failed to start script`);
        return;
      }

      post(`Running script with ${numThreads} thread(s), pid ${runningScript.pid} and args: ${arrayToString(args)}.`);
      if (tailFlag) {
        logBoxCreate(runningScript);
      }
      return;
    }

    post("ERROR: No such script");
  },

  runContract: async function (contractName) {
    // There's already an opened contract
    if (Terminal.contractOpen) {
      return post("ERROR: There's already a Coding Contract in Progress");
    }

    const serv = Player.getCurrentServer();
    const contract = serv.getContract(contractName);
    if (contract == null) {
      return post("ERROR: No such contract");
    }

    Terminal.contractOpen = true;
    const res = await contract.prompt();

    switch (res) {
      case CodingContractResult.Success:
        var reward = Player.gainCodingContractReward(contract.reward, contract.getDifficulty());
        post(`Contract SUCCESS - ${reward}`);
        serv.removeContract(contract);
        break;
      case CodingContractResult.Failure:
        ++contract.tries;
        if (contract.tries >= contract.getMaxNumTries()) {
          post("Contract <p style='color:red;display:inline'>FAILED</p> - Contract is now self-destructing");
          serv.removeContract(contract);
        } else {
          post(
            `Contract <p style='color:red;display:inline'>FAILED</p> - ${
              contract.getMaxNumTries() - contract.tries
            } tries remaining`,
          );
        }
        break;
      case CodingContractResult.Cancelled:
      default:
        post("Contract cancelled");
        break;
    }
    Terminal.contractOpen = false;
  },
};

export { postVersion, Terminal };
