/**
 * Web worker to handle the inputs.
 * Handles storage, modification and retrieval of the inputs.
 *
 * @author j433866 [j433866@gmail.com]
 * @copyright Crown Copyright 2019
 * @license Apache-2.0
 */

import Utils from "../../core/Utils.mjs";
import {detectFileType} from "../../core/lib/FileType.mjs";

// Default max values
// These will be correctly calculated automatically
self.maxWorkers = 4;
self.maxTabs = 1;

self.pendingFiles = [];
self.inputs = {};
self.loaderWorkers = [];
self.currentInputNum = 1;
self.numInputs = 0;
self.pendingInputs = 0;
self.loadingInputs = 0;

/**
 * Respond to message from parent thread.
 *
 * @param {MessageEvent} e
 */
self.addEventListener("message", function(e) {
    const r = e.data;
    if (!("action" in r)) {
        log.error("No action");
        return;
    }

    log.debug(`Receiving ${r.action} from InputWaiter.`);

    switch (r.action) {
        case "loadUIFiles":
            self.loadFiles(r.data);
            break;
        case "loaderWorkerReady":
            self.loaderWorkerReady(r.data);
            break;
        case "updateMaxWorkers":
            self.maxWorkers = r.data;
            break;
        case "updateMaxTabs":
            self.updateMaxTabs(r.data.maxTabs, r.data.activeTab);
            break;
        case "updateInputValue":
            self.updateInputValue(r.data);
            break;
        case "updateInputObj":
            self.updateInputObj(r.data);
            break;
        case "updateInputProgress":
            self.updateInputProgress(r.data);
            break;
        case "bakeAll":
            self.bakeAllInputs();
            break;
        case "bakeNext":
            self.bakeInput(r.data.inputNum, r.data.bakeId);
            break;
        case "getLoadProgress":
            self.getLoadProgress(r.data);
            break;
        case "setInput":
            self.setInput(r.data);
            break;
        case "setLogLevel":
            log.setLevel(r.data, false);
            break;
        case "addInput":
            self.addInput(r.data, "string");
            break;
        case "refreshTabs":
            self.refreshTabs(r.data.inputNum, r.data.direction);
            break;
        case "removeInput":
            self.removeInput(r.data);
            break;
        case "changeTabRight":
            self.changeTabRight(r.data.activeTab);
            break;
        case "changeTabLeft":
            self.changeTabLeft(r.data.activeTab);
            break;
        case "autobake":
            self.autoBake(r.data.activeTab, 0, false);
            break;
        case "filterTabs":
            self.filterTabs(r.data);
            break;
        case "loaderWorkerMessage":
            self.handleLoaderMessage(r.data);
            break;
        case "inputSwitch":
            self.inputSwitch(r.data);
            break;
        case "updateTabHeader":
            self.updateTabHeader(r.data);
            break;
        case "step":
            self.autoBake(r.data.activeTab, r.data.progress, true);
            break;
        case "getInput":
            self.getInput(r.data);
            break;
        case "getInputNums":
            self.getInputNums(r.data);
            break;
        default:
            log.error(`Unknown action '${r.action}'.`);
    }
});

/**
 * Gets the load progress of the input files, and the
 * load progress for the input given in inputNum
 *
 * @param {number} inputNum - The input to get the file loading progress for
 */
self.getLoadProgress = function(inputNum) {
    const total = self.numInputs;
    const pending = self.pendingFiles.length;
    const loading = self.loadingInputs;
    const loaded = total - pending - loading;

    self.postMessage({
        action: "loadingInfo",
        data: {
            pending: pending,
            loading: loading,
            loaded: loaded,
            total: total,
            activeProgress: {
                inputNum: inputNum,
                progress: self.getInputProgress(inputNum)
            }
        }
    });
};

/**
 * Fired when an autobake is initiated.
 * Queues the active input and sends a bake command.
 *
 * @param {number} inputNum - The input to be baked
 * @param {number} progress - The current progress of the bake through the recipe
 * @param {boolean} [step=false] - Set to true if we should only execute one operation instead of the
 * whole recipe
 */
self.autoBake = function(inputNum, progress, step=false) {
    const input = self.getInputObj(inputNum);
    if (input) {
        self.postMessage({
            action: "bakeAllInputs",
            data: {
                nums: [parseInt(inputNum, 10)],
                step: step,
                progress: progress
            }
        });
    }
};

/**
 * Fired when we want to bake all inputs (bake button clicked)
 * Sends a list of inputNums to the workerwaiter
 */
self.bakeAllInputs = function() {
    const inputNums = Object.keys(self.inputs),
        nums = [];

    for (let i = 0; i < inputNums.length; i++) {
        if (self.inputs[inputNums[i]].status === "loaded") {
            nums.push(parseInt(inputNums[i], 10));
        }
    }
    self.postMessage({
        action: "bakeAllInputs",
        data: {
            nums: nums,
            step: false,
            progress: 0
        }
    });
};

/**
 * Gets the data for the provided inputNum and sends it to the WorkerWaiter
 *
 * @param {number} inputNum
 * @param {number} bakeId
 */
self.bakeInput = function(inputNum, bakeId) {
    const inputObj = self.getInputObj(inputNum);
    if (inputObj === null ||
        inputObj === undefined ||
        inputObj.status !== "loaded") {

        self.postMessage({
            action: "queueInputError",
            data: {
                inputNum: inputNum,
                bakeId: bakeId
            }
        });
        return;
    }

    let inputData = inputObj.data;
    if (typeof inputData !== "string") inputData = inputData.fileBuffer;

    self.postMessage({
        action: "queueInput",
        data: {
            input: inputData,
            inputNum: inputNum,
            bakeId: bakeId
        }
    });
};

/**
 * Gets the stored object for a specific inputNum
 *
 * @param {number} inputNum - The input we want to get the object for
 * @returns {object}
 */
self.getInputObj = function(inputNum) {
    return self.inputs[inputNum];
};

/**
 * Gets the stored value for a specific inputNum.
 *
 * @param {number} inputNum - The input we want to get the value of
 * @returns {string | ArrayBuffer}
 */
self.getInputValue = function(inputNum) {
    if (self.inputs[inputNum]) {
        if (typeof self.inputs[inputNum].data === "string") {
            return self.inputs[inputNum].data;
        } else {
            return self.inputs[inputNum].data.fileBuffer;
        }
    }
    return "";
};

/**
 * Gets the stored value or object for a specific inputNum and sends it to the inputWaiter.
 *
 * @param {object} inputData - Object containing data about the input to retrieve
 * @param {number} inputData.inputNum - The inputNum of the input to get
 * @param {boolean} inputData.getObj - If true, returns the entire input object instead of just the value
 * @param {number} inputData.id - The callback ID for the callback to run when returned to the inputWaiter
 */
self.getInput = function(inputData) {
    const inputNum = inputData.inputNum,
        data = (inputData.getObj) ? self.getInputObj(inputNum) : self.getInputValue(inputNum);
    self.postMessage({
        action: "getInput",
        data: {
            data: data,
            id: inputData.id
        }
    });
};

/**
 * Gets a list of the stored inputNums, along with the minimum and maximum
 *
 * @param {number} id - The callback ID to be executed when returned to the inputWaiter
 */
self.getInputNums = function(id) {
    const inputNums = Object.keys(self.inputs),
        min = self.getSmallestInputNum(inputNums),
        max = self.getLargestInputNum(inputNums);

    self.postMessage({
        action: "getInputNums",
        data: {
            inputNums: inputNums,
            min: min,
            max: max,
            id: id
        }
    });
};

/**
 * Gets the load progress for a specific inputNum
 *
 * @param {number} inputNum - The input we want to get the progress of
 * @returns {number | string} - Returns "error" if there was a load error
 */
self.getInputProgress = function(inputNum) {
    const inputObj = self.getInputObj(inputNum);
    if (inputObj === undefined || inputObj === null) return;
    if (inputObj.status === "error") {
        return "error";
    }
    return inputObj.progress;
};

/**
 * Gets the largest inputNum of all the inputs
 *
 * @param {string[]} inputNums - The numbers to find the largest of
 * @returns {number}
 */
self.getLargestInputNum = function(inputNums) {
    return inputNums.reduce((acc, val) => {
        val = parseInt(val, 10);
        return val > acc ? val : acc;
    }, -1);
};

/**
 * Gets the smallest inputNum of all the inputs
 *
 * @param {string[]} inputNums - The numbers to find the smallest of
 * @returns {number}
 */
self.getSmallestInputNum = function(inputNums) {
    const min = inputNums.reduce((acc, val) => {
        val = parseInt(val, 10);
        return val < acc ? val : acc;
    }, Number.MAX_SAFE_INTEGER);

    // Assume we don't have this many tabs!
    if (min === Number.MAX_SAFE_INTEGER) return -1;

    return min;
};

/**
 * Gets the next smallest inputNum
 *
 * @param {number} inputNum - The current input number
 * @returns {number}
 */
self.getPreviousInputNum = function(inputNum) {
    const inputNums = Object.keys(self.inputs);
    if (inputNums.length === 0) return -1;

    return inputNums.reduce((acc, val) => {
        val = parseInt(val, 10);
        return (val < inputNum && val > acc) ? val : acc;
    }, self.getSmallestInputNum(inputNums));
};

/**
 * Gets the next largest inputNum
 *
 * @param {number} inputNum - The current input number
 * @returns {number}
 */
self.getNextInputNum = function(inputNum) {
    const inputNums = Object.keys(self.inputs);

    return inputNums.reduce((acc, val) => {
        val = parseInt(val, 10);
        return (val > inputNum && val < acc) ? val : acc;
    }, self.getLargestInputNum(inputNums));
};

/**
 * Gets a list of inputNums starting from the provided inputNum.
 * If direction is "left", gets the inputNums higher than the provided number.
 * If direction is "right", gets the inputNums lower than the provided number.
 * @param {number} inputNum - The inputNum we want to get the neighbours of
 * @param {string} direction - Either "left" or "right". Determines which direction we search for nearby numbers in
 * @returns {number[]}
 */
self.getNearbyNums = function(inputNum, direction) {
    const nums = [];
    for (let i = 0; i < self.maxTabs; i++) {
        let newNum;
        if (i === 0 && self.inputs[inputNum] !== undefined) {
            newNum = inputNum;
        } else {
            switch (direction) {
                case "left":
                    newNum = self.getNextInputNum(nums[i - 1]);
                    if (newNum === nums[i - 1]) {
                        direction = "right";
                        newNum = self.getPreviousInputNum(nums[0]);
                    }
                    break;
                case "right":
                    newNum = self.getPreviousInputNum(nums[i - 1]);
                    if (newNum === nums[i - 1]) {
                        direction = "left";
                        newNum = self.getNextInputNum(nums[0]);
                    }
            }
        }
        if (!nums.includes(newNum) && (newNum > 0)) {
            nums.push(newNum);
        }
    }
    nums.sort(function(a, b) {
        return a - b;
    });
    return nums;
};

/**
 * Gets the data to display in the tab header for an input, and
 * posts it back to the inputWaiter
 *
 * @param {number} inputNum - The inputNum of the tab header
 */
self.updateTabHeader = function(inputNum) {
    const input = self.getInputObj(inputNum);
    if (input === null || input === undefined) return;
    let inputData = input.data;
    if (typeof inputData !== "string") {
        inputData = input.data.name;
    }
    inputData = inputData.replace(/[\n\r]/g, "");

    self.postMessage({
        action: "updateTabHeader",
        data: {
            inputNum: inputNum,
            input: inputData.slice(0, 100)
        }
    });
};

/**
 * Gets the input for a specific inputNum, and posts it to the inputWaiter
 * so that it can be displayed in the input area
 *
 * @param {object} inputData
 * @param {number} inputData.inputNum - The input to get the data for
 * @param {boolean} inputData.silent - If false, the manager statechange event will be fired
 */
self.setInput = function(inputData) {
    const inputNum = inputData.inputNum;
    const silent = inputData.silent;
    const input = self.getInputObj(inputNum);
    if (input === undefined || input === null) return;

    let inputVal = input.data;
    const inputObj = {
        inputNum: inputNum,
        input: inputVal
    };
    if (typeof inputVal !== "string") {
        inputObj.name = inputVal.name;
        inputObj.size = inputVal.size;
        inputObj.type = inputVal.type;
        inputObj.progress = input.progress;
        inputObj.status = input.status;
        inputVal = inputVal.fileBuffer;
        const fileSlice = inputVal.slice(0, 512001);
        inputObj.input = fileSlice;

        self.postMessage({
            action: "setInput",
            data: {
                inputObj: inputObj,
                silent: silent
            }
        }, [fileSlice]);
    } else {
        self.postMessage({
            action: "setInput",
            data: {
                inputObj: inputObj,
                silent: silent
            }
        });
    }
    self.updateTabHeader(inputNum);
};

/**
 * Gets the nearby inputNums to the provided number, and posts them
 * to the inputWaiter to be displayed on the page.
 *
 * @param {number} inputNum - The inputNum to find the nearby numbers for
 * @param {string} direction - The direction to search for inputNums in. Either "left" or "right"
 */
self.refreshTabs = function(inputNum, direction) {
    const nums = self.getNearbyNums(inputNum, direction),
        inputNums = Object.keys(self.inputs),
        tabsLeft = (self.getSmallestInputNum(inputNums) !== nums[0] && nums.length > 0),
        tabsRight = (self.getLargestInputNum(inputNums) !== nums[nums.length - 1] && nums.length > 0);

    self.postMessage({
        action: "refreshTabs",
        data: {
            nums: nums,
            activeTab: (nums.includes(inputNum)) ? inputNum : self.getNextInputNum(inputNum),
            tabsLeft: tabsLeft,
            tabsRight: tabsRight
        }
    });

    // Update the tab headers for the new tabs
    for (let i = 0; i < nums.length; i++) {
        self.updateTabHeader(nums[i]);
    }
};

/**
 * Update the stored status for an input
 *
 * @param {number} inputNum - The input that's having its status changed
 * @param {string} status - The status of the input
 */
self.updateInputStatus = function(inputNum, status) {
    if (self.inputs[inputNum] !== undefined) {
        self.inputs[inputNum].status = status;
    }
};

/**
 * Update the stored load progress of an input
 *
 * @param {object} inputData
 * @param {number} inputData.inputNum - The input that's having its progress updated
 * @param {number} inputData.progress - The load progress of the input
 */
self.updateInputProgress = function(inputData) {
    const inputNum = inputData.inputNum;
    const progress = inputData.progress;

    if (self.inputs[inputNum] !== undefined) {
        self.inputs[inputNum].progress = progress;
    }
};

/**
 * Update the stored value of an input.
 *
 * @param {object} inputData
 * @param {number} inputData.inputNum - The input that's having its value updated
 * @param {string | ArrayBuffer} inputData.value - The new value of the input
 * @param {boolean} inputData.force - If true, still updates the input value if the input type is different to the stored value
 */
self.updateInputValue = function(inputData) {
    const inputNum = inputData.inputNum;
    if (inputNum < 1) return;
    if (Object.prototype.hasOwnProperty.call(self.inputs[inputNum].data, "fileBuffer") &&
    typeof inputData.value === "string" && !inputData.force) return;
    const value = inputData.value;
    if (self.inputs[inputNum] !== undefined) {
        if (typeof value === "string") {
            self.inputs[inputNum].data = value;
        } else {
            self.inputs[inputNum].data.fileBuffer = value;
        }
        self.inputs[inputNum].status = "loaded";
        self.inputs[inputNum].progress = 100;
        return;
    }

    // If we get to here, an input for inputNum could not be found,
    // so create a new one. Only do this if the value is a string, as
    // loadFiles will create the input object for files
    if (typeof value === "string") {
        self.inputs.push({
            inputNum: inputNum,
            data: value,
            status: "loaded",
            progress: 100
        });
    }
};

/**
 * Update the stored data object for an input.
 * Used if we need to change a string to an ArrayBuffer
 *
 * @param {object} inputData
 * @param {number} inputData.inputNum - The number of the input we're updating
 * @param {object} inputData.data - The new data object for the input
 */
self.updateInputObj = function(inputData) {
    const inputNum = inputData.inputNum;
    const data = inputData.data;

    if (self.getInputObj(inputNum) === undefined) return;

    self.inputs[inputNum].data = data;
};

/**
 * Get the index of a loader worker object.
 * Returns -1 if the worker could not be found
 *
 * @param {number} workerId - The ID of the worker we're searching for
 * @returns {number}
 */
self.getLoaderWorkerIdx = function(workerId) {
    for (let i = 0; i < self.loaderWorkers.length; i++) {
        if (self.loaderWorkers[i].id === workerId) {
            return i;
        }
    }
    return -1;
};

/**
 * Fires when a loaderWorker is ready to load files.
 * Stores data about the new loaderWorker in the loaderWorkers array,
 * and sends the next file to the loaderWorker to be loaded.
 *
 * @param {object} workerData
 * @param {number} workerData.id - The ID of the new loaderWorker
 */
self.loaderWorkerReady = function(workerData) {
    const newWorkerObj = {
        id: workerData.id,
        inputNum: -1,
        active: true
    };
    self.loaderWorkers.push(newWorkerObj);
    self.loadNextFile(self.loaderWorkers.indexOf(newWorkerObj));
};

/**
 * Handler for messages sent by loaderWorkers.
 * (Messages are sent between the inputWorker and
 * loaderWorkers via the main thread)
 *
 * @param {object} r - The data sent by the loaderWorker
 * @param {number} r.inputNum - The inputNum which the message corresponds to
 * @param {string} r.error - Present if an error is fired by the loaderWorker. Contains the error message string.
 * @param {ArrayBuffer} r.fileBuffer - Present if a file has finished loading. Contains the loaded file buffer.
 */
self.handleLoaderMessage = function(r) {
    let inputNum = 0;

    if ("inputNum" in r) {
        inputNum = r.inputNum;
    }

    if ("error" in r) {
        self.updateInputProgress(r.inputNum, 0);
        self.updateInputStatus(r.inputNum, "error");

        log.error(r.error);
        self.loadingInputs--;

        self.terminateLoaderWorker(r.id);
        self.activateLoaderWorker();

        self.setInput({inputNum: inputNum, silent: true});
        return;
    }

    if ("fileBuffer" in r) {
        log.debug(`Input file ${inputNum} loaded.`);
        self.loadingInputs--;

        self.updateInputValue({
            inputNum: inputNum,
            value: r.fileBuffer
        });

        self.postMessage({
            action: "fileLoaded",
            data: {
                inputNum: inputNum
            }
        });

        const idx = self.getLoaderWorkerIdx(r.id);
        self.loadNextFile(idx);
    } else if ("progress" in r) {
        self.updateInputProgress(r);
    }
};

/**
 * Loads the next file using a loaderWorker
 *
 * @param {number} - The loaderWorker which will load the file
 */
self.loadNextFile = function(workerIdx) {
    if (workerIdx === -1) return;
    const id = self.loaderWorkers[workerIdx].id;
    if (self.pendingFiles.length === 0) {
        const workerObj = self.loaderWorkers.splice(workerIdx, 1)[0];
        self.terminateLoaderWorker(workerObj.id);
        return;
    }

    const nextFile = self.pendingFiles.splice(0, 1)[0];
    self.loaderWorkers[workerIdx].inputNum = nextFile.inputNum;
    self.loadingInputs++;
    self.postMessage({
        action: "loadInput",
        data: {
            file: nextFile.file,
            inputNum: nextFile.inputNum,
            workerId: id
        }
    });
};

/**
 * Sends a message to the inputWaiter to create a new loaderWorker.
 * If there's an inactive loaderWorker that already exists, use that instead.
 */
self.activateLoaderWorker = function() {
    for (let i = 0; i < self.loaderWorkers.length; i++) {
        if (!self.loaderWorkers[i].active) {
            self.loaderWorkers[i].active = true;
            self.loadNextFile(i);
            return;
        }
    }
    self.postMessage({
        action: "activateLoaderWorker"
    });
};

/**
 * Sends a message to the inputWaiter to terminate a loaderWorker.
 *
 * @param {number} id - The ID of the worker to be terminated
 */
self.terminateLoaderWorker = function(id) {
    self.postMessage({
        action: "terminateLoaderWorker",
        data: id
    });
    // If we still have pending files, spawn a worker
    if (self.pendingFiles.length > 0) {
        self.activateLoaderWorker();
    }
};

/**
 * Loads files using LoaderWorkers
 *
 * @param {object} filesData
 * @param {FileList} filesData.files - The list of files to be loaded
 * @param {number} filesData.activeTab - The active tab in the UI
 */
self.loadFiles = function(filesData) {
    const files = filesData.files;
    const activeTab = filesData.activeTab;
    let lastInputNum = -1;
    const inputNums = [];
    for (let i = 0; i < files.length; i++) {
        if (i === 0 && self.getInputValue(activeTab) === "") {
            self.removeInput({
                inputNum: activeTab,
                refreshTabs: false,
                removeChefWorker: false
            });
            lastInputNum = self.addInput(false, "file", {
                name: files[i].name,
                size: files[i].size.toLocaleString(),
                type: files[i].type || "unknown"
            }, activeTab);
        } else {
            lastInputNum = self.addInput(false, "file", {
                name: files[i].name,
                size: files[i].size.toLocaleString(),
                type: files[i].type || "unknown"
            });
        }
        inputNums.push(lastInputNum);

        self.pendingFiles.push({
            file: files[i],
            inputNum: lastInputNum
        });
    }
    let max = self.maxWorkers;
    if (self.pendingFiles.length < self.maxWorkers) max = self.pendingFiles.length;

    // Create loaderWorkers to load the new files
    for (let i = 0; i < max; i++) {
        self.activateLoaderWorker();
    }

    self.getLoadProgress();
    self.setInput({inputNum: activeTab, silent: true});
};

/**
 * Adds an input to the input dictionary
 *
 * @param {boolean} [changetab=false] - Whether or not to change to the new input
 * @param {string} type - Either "string" or "file"
 * @param {Object} fileData - Contains information about the file to be added to the input (only used when type is "file")
 * @param {string} fileData.name - The filename of the input being added
 * @param {number} fileData.size - The file size (in bytes) of the input being added
 * @param {string} fileData.type - The MIME type of the input being added
 * @param {number} inputNum - Defaults to auto-incrementing self.currentInputNum
 */
self.addInput = function(
    changeTab = false,
    type,
    fileData = {
        name: "unknown",
        size: "unknown",
        type: "unknown"
    },
    inputNum = self.currentInputNum++
) {
    self.numInputs++;
    const newInputObj = {
        inputNum: inputNum
    };

    switch (type) {
        case "string":
            newInputObj.data = "";
            newInputObj.status = "loaded";
            newInputObj.progress = 100;
            break;
        case "file":
            newInputObj.data = {
                fileBuffer: new ArrayBuffer(),
                name: fileData.name,
                size: fileData.size,
                type: fileData.type
            };
            newInputObj.status = "pending";
            newInputObj.progress = 0;
            break;
        default:
            log.error(`Invalid type '${type}'.`);
            return -1;
    }
    self.inputs[inputNum] = newInputObj;

    // Tell the inputWaiter we've added an input, so it can create a tab to display it
    self.postMessage({
        action: "inputAdded",
        data: {
            changeTab: changeTab,
            inputNum: inputNum
        }
    });

    return inputNum;
};

/**
 * Remove an input from the inputs dictionary
 *
 * @param {object} removeInputData
 * @param {number} removeInputData.inputNum - The number of the input to be removed
 * @param {boolean} removeInputData.refreshTabs - If true, refresh the tabs after removing the input
 * @param {boolean} removeInputData.removeChefWorker - If true, remove a chefWorker from the WorkerWaiter
 */
self.removeInput = function(removeInputData) {
    const inputNum = removeInputData.inputNum;
    const refreshTabs = removeInputData.refreshTabs;
    self.numInputs--;

    for (let i = 0; i < self.loaderWorkers.length; i++) {
        if (self.loaderWorkers[i].inputNum === inputNum) {
            // Terminate any loaderWorker that's loading the removed input
            self.loadingInputs--;
            self.terminateLoaderWorker(self.loaderWorkers[i].id);
            break;
        }
    }

    for (let i = 0; i < self.pendingFiles.length; i++) {
        // Remove the input from the pending files list
        if (self.pendingFiles[i].inputNum === inputNum) {
            self.pendingFiles.splice(i, 1);
            break;
        }
    }

    delete self.inputs[inputNum];

    if (refreshTabs) {
        self.refreshTabs(self.getPreviousInputNum(inputNum), "left");
    }

    if (self.numInputs < self.maxWorkers && removeInputData.removeChefWorker) {
        self.postMessage({
            action: "removeChefWorker"
        });
    }
};

/**
 * Change to the next tab.
 *
 * @param {number} inputNum - The inputNum of the tab to change to
 */
self.changeTabRight = function(inputNum) {
    const newInput = self.getNextInputNum(inputNum);
    self.postMessage({
        action: "changeTab",
        data: newInput
    });
};

/**
 * Change to the previous tab.
 *
 * @param {number} inputNum - The inputNum of the tab to change to
 */
self.changeTabLeft = function(inputNum) {
    const newInput = self.getPreviousInputNum(inputNum);
    self.postMessage({
        action: "changeTab",
        data: newInput
    });
};

/**
 * Updates the maximum number of tabs, and refreshes them if it changes
 *
 * @param {number} maxTabs - The new max number of tabs
 * @param {number} activeTab - The currently selected tab
 */
self.updateMaxTabs = function(maxTabs, activeTab) {
    if (self.maxTabs !== maxTabs) {
        self.maxTabs = maxTabs;
        self.refreshTabs(activeTab, "right");
    }
};

/**
 * Search the inputs for any that match the filters provided,
 * posting the results back to the inputWaiter
 *
 * @param {object} searchData - Object containing the search filters
 * @param {boolean} searchData.showPending - If true, include pending inputs in the results
 * @param {boolean} searchData.showLoading - If true, include loading inputs in the results
 * @param {boolean} searchData.showLoaded - If true, include loaded inputs in the results
 * @param {string} searchData.filter - A regular expression to match the inputs on
 * @param {string} searchData.filterType - Either "CONTENT" or "FILENAME". Determines what should be matched with filter
 * @param {number} searchData.numResults - The maximum number of results to be returned
 */
self.filterTabs = function(searchData) {
    const showPending = searchData.showPending,
        showLoading = searchData.showLoading,
        showLoaded = searchData.showLoaded,
        filterType = searchData.filterType;

    let filterExp;
    try {
        filterExp = new RegExp(searchData.filter, "i");
    } catch (error) {
        self.postMessage({
            action: "filterTabError",
            data: error.message
        });
        return;
    }
    const numResults = searchData.numResults;

    const inputs = [];
    const inputNums = Object.keys(self.inputs);
    for (let i = 0; i < inputNums.length; i++) {
        const iNum = inputNums[i];
        let textDisplay = "";
        let addInput = false;
        if (self.inputs[iNum].status === "pending" && showPending ||
            self.inputs[iNum].status === "loading" && showLoading ||
            self.inputs[iNum].status === "loaded" && showLoaded) {
            try {
                if (typeof self.inputs[iNum].data === "string") {
                    if (filterType.toLowerCase() === "content" &&
                        filterExp.test(self.inputs[iNum].data.slice(0, 4096))) {
                        textDisplay = self.inputs[iNum].data.slice(0, 4096);
                        addInput = true;
                    }
                } else {
                    if ((filterType.toLowerCase() === "filename" &&
                        filterExp.test(self.inputs[iNum].data.name)) ||
                        filterType.toLowerCase() === "content" &&
                        filterExp.test(Utils.arrayBufferToStr(self.inputs[iNum].data.fileBuffer.slice(0, 4096)))) {
                        textDisplay = self.inputs[iNum].data.name;
                        addInput = true;
                    }
                }
            } catch (error) {
                self.postMessage({
                    action: "filterTabError",
                    data: error.message
                });
                return;
            }
        }

        if (addInput) {
            if (textDisplay === "" || textDisplay === undefined) {
                textDisplay = "New Tab";
            }
            const inputItem = {
                inputNum: iNum,
                textDisplay: textDisplay
            };
            inputs.push(inputItem);
        }
        if (inputs.length >= numResults) {
            break;
        }
    }

    // Send the results back to the inputWaiter
    self.postMessage({
        action: "displayTabSearchResults",
        data: inputs
    });
};

/**
 * Swaps the input and outputs, and sends the old input back to the main thread.
 *
 * @param {object} switchData
 * @param {number} switchData.inputNum - The inputNum of the input to be switched to
 * @param {string | ArrayBuffer} switchData.outputData - The data to switch to
 */
self.inputSwitch = function(switchData) {
    const currentInput = self.getInputObj(switchData.inputNum);
    const currentData = currentInput.data;
    if (currentInput === undefined || currentInput === null) return;

    if (typeof switchData.outputData !== "string") {
        const output = new Uint8Array(switchData.outputData),
            types = detectFileType(output);
        let type = "unknown",
            ext = "dat";
        if (types.length) {
            type = types[0].mime;
            ext = types[0].extension.split(",", 1)[0];
        }

        // ArrayBuffer
        self.updateInputObj({
            inputNum: switchData.inputNum,
            data: {
                fileBuffer: switchData.outputData,
                name: `output.${ext}`,
                size: switchData.outputData.byteLength.toLocaleString(),
                type: type
            }
        });
    } else {
        // String
        self.updateInputValue({
            inputNum: switchData.inputNum,
            value: switchData.outputData,
            force: true
        });
    }

    self.postMessage({
        action: "inputSwitch",
        data: {
            data: currentData,
            inputNum: switchData.inputNum
        }
    });

    self.postMessage({
        action: "fileLoaded",
        data: {
            inputNum: switchData.inputNum
        }
    });

};
