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 {
    getCurrentEditor,
    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 {
    yesNoBoxCreate,
    yesNoBoxGetYesButton,
    yesNoBoxGetNoButton,
    yesNoBoxClose,
} from "../utils/YesNoBox";
import {
    post,
    postElement,
    postContent,
    postError,
    hackProgressBarPost,
    hackProgressPost,
} from "./ui/postToTerminal";
import { convertTimeMsToTimeElapsedString } from "../utils/StringHelperFunctions";
import { Money } from "./ui/React/Money";

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


function postNetburnerText() {
    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(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(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);
                    getCurrentEditor().setCursor({row: 1, column: 4});
                } 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(Player.money.toNumber())} / {Money(1e11)}</>);
                post(`Hacking skill: ${Player.hacking_skill} / 2500`);
                return;
            }

            post("We will contact you.");
            post("-- Daedalus --");
        };
        programHandlers[Programs.BitFlume.name] = () => {
            const yesBtn = yesNoBoxGetYesButton();
            const noBtn = yesNoBoxGetNoButton();
            yesBtn.innerHTML = "Travel to BitNode Nexus";
            noBtn.innerHTML = "Cancel";
            yesBtn.addEventListener("click", function() {
                hackWorldDaemon(Player.bitNodeN, true);
                return yesNoBoxClose();
            });
            noBtn.addEventListener("click", function() {
                return yesNoBoxClose();
            });
            yesNoBoxCreate("WARNING: USING THIS PROGRAM WILL CAUSE YOU TO LOSE ALL OF YOUR PROGRESS ON THE CURRENT BITNODE.<br><br>" +
                            "Do you want to travel to the BitNode Nexus? This allows you to reset the current BitNode " +
                            "and select a new one.");
        };

        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();

        let numThreads = 1;
        const args = [];
        const scriptName = Terminal.getFilepath(commandArray[1]);

        if (commandArray.length > 2) {
            if (commandArray.length >= 4 && commandArray[2] == "-t") {
                numThreads = Math.round(parseFloat(commandArray[3]));
                if (isNaN(numThreads) || numThreads < 1) {
                    postError("Invalid number of threads specified. Number of threads must be greater than 0");
                    return;
                }
                for (let i = 4; i < commandArray.length; ++i) {
                    args.push(commandArray[i]);
                }
            } else {
                for (let i = 2; i < commandArray.length; ++i) {
                    args.push(commandArray[i])
                }
            }
        }


        // 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 (var i = 0; i < server.scripts.length; i++) {
            if (server.scripts[i].filename === scriptName) {
                // Check for admin rights and that there is enough RAM availble to run
                var script = server.scripts[i];
                var ramUsage = script.ramUsage * numThreads;
                var ramAvailable = server.maxRam - server.ramUsed;

                if (server.hasAdminRights == false) {
                    post("Need root access to run script");
                    return;
                } else 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;
                } else {
                    // Able to run script
                    var runningScriptObj = new RunningScript(script, args);
                    runningScriptObj.threads = numThreads;

                    if (startWorkerScript(runningScriptObj, server)) {
                        post(`Running script with ${numThreads} thread(s), pid ${runningScriptObj.pid} and args: ${arrayToString(args)}.`);
                    } else {
                        postError(`Failed to start script`);
                    }
                    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 {postNetburnerText, Terminal};
