"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Factory = void 0;
/*
* Parameters to startup the factory
* */
const Logging_1 = require("./Logging");
const winston_1 = __importDefault(require("winston"));
const path_1 = __importDefault(require("path"));
const fs_extra_1 = __importDefault(require("fs-extra"));
const os = __importStar(require("os"));
const FactorioApi_1 = require("./FactorioApi");
const decompress_1 = __importDefault(require("decompress"));
const child_process_1 = require("child_process");
const Trial_1 = require("./Trial");
const process = __importStar(require("process"));
const Dataset_1 = require("./Dataset");
const lodash_1 = __importDefault(require("lodash"));
const FactoryDatabase_1 = require("./FactoryDatabase");
const DatasetAnalysis_1 = require("./DatasetAnalysis");
/*
* The main 'runner' - this class is used for accessing all executable functionality, along with preping and setting
* the data needed in filesystem. It should include as little references out as possible!
* */
class Factory {
    static get initStatus() {
        return Factory._initStatus;
    }
    static set initStatus(val) {
        Factory._initStatus = val;
        if (Factory.onStatusChange)
            Factory.onStatusChange(val);
    }
    static initialize(params) {
        return __awaiter(this, void 0, void 0, function* () {
            if (Factory.initStatus !== 'not-started')
                return;
            // set defaults first and throw errors if needed
            if (params && !(params === null || params === void 0 ? void 0 : params.installDir))
                params.installDir = path_1.default.join(process.cwd(), 'factorio');
            if (params && !params.updateInstall)
                params.updateInstall = true;
            if (!(params === null || params === void 0 ? void 0 : params.installDir))
                throw new Error(`Cannot start factory without installPath ||| ${params.installDir}`);
            if (params && !(params === null || params === void 0 ? void 0 : params.build))
                params.build = 'headless';
            // If no data path specified, we assume same as install path. Should be the case in alllllll situations.... except Steam
            if (params && !(params === null || params === void 0 ? void 0 : params.dataDir))
                params.dataDir = params.installDir;
            this.initStatus = 'logging';
            yield Factory.setupLogger(params.hideConsole);
            Logging_1.Logging.log('info', `Beginning factory startup`);
            // Phase 2 - link to the data path folder
            // ensure core folders are made - if any of these fail, the rest fails
            this.initStatus = 'folder-setup';
            yield Promise.all([
                fs_extra_1.default.ensureDir(path_1.default.join(params.dataDir, 'mods')),
                fs_extra_1.default.ensureDir(path_1.default.join(params.dataDir, 'scenarios')),
                fs_extra_1.default.ensureDir(path_1.default.join(params.dataDir, 'saves')),
                fs_extra_1.default.ensureDir(path_1.default.join(params.dataDir, 'saves-upload')),
                fs_extra_1.default.ensureDir(path_1.default.join(params.dataDir, 'script-output')),
                fs_extra_1.default.ensureDir(path_1.default.join(params.dataDir, 'downloads')),
                fs_extra_1.default.ensureDir(path_1.default.join(params.dataDir, 'mods-cache'))
            ]);
            // Will need to have a check for script-output being populated after a run or not, to ID a bad Data Path
            Factory.factoryDataPath = params.dataDir;
            Factory.factoryInstallPath = params.installDir;
            // clear the script-output folder as well
            //await fs.emptyDir(path.join(params.dataDir, 'script-output'))
            Logging_1.Logging.log('info', `Core folders exist in ${params.dataDir}`);
            // Clear the 'compiled' save that is specified, if it exists
            // this will force a recompile, which is only done once on init anyways.
            // ensures correct version with executable. Happens in 'verify' function
            //await fs.remove(Factory.activeSavePath)
            this.initStatus = 'factorio-api-init';
            // Phase 3 - Setup API access
            // Setup factorio api access, to be able to download/update needed mods and game
            yield FactorioApi_1.FactorioApi.initialize({
                username: params.username ? params.username : process.env.FACTORIO_USER,
                token: params.token ? params.token : process.env.FACTORIO_TOKEN,
                dataPath: Factory.factoryDataPath
            });
            // Phase 4 - Verify our install or reinstall as needed
            // First off, lets load our cached mods
            yield Factory.refreshModCache();
            let err;
            try {
                this.initStatus = 'validating-install';
                // Validate our executable exists and info file exists
                yield Factory.verifyInstall(params.updateInstall);
            }
            catch (e) {
                // if this gets hit, means that something had an error in the 'link' process above
                // In this case, this catch allows code to continue past in this case, where future code handles the re-install if possible
                // or re-throws an error
                err = e;
                Logging_1.Logging.log('error', `Error linking factory install path - ${e.message}`);
            }
            // Max number of times can be downloaded while process is running - if this occurs, restart process. Safety check for paranoia of constant re-downloads for now
            if (Factory.factoryExecutable != true) {
                if (Factory.clientDownloadCount < 2) {
                    // re-install!
                    try {
                        this.initStatus = 'installing';
                        yield Factory.installGame(process.env.FACTORIO_VERSION, params.build);
                    }
                    catch (e) {
                        Logging_1.Logging.log('error', `Error installing Factorio - ${e.message}`);
                        // delete the cached download file, as it may have issues. Next loop will retry!
                    }
                }
                else {
                    throw new Error(`Cannot link factory install path, too many download attempts - ${err.message}. Manual install required, then restart process!`);
                }
            }
            this.initStatus = 'ready';
        });
    }
    static prepareTrial(t) {
        var _a;
        return __awaiter(this, void 0, void 0, function* () {
            Logging_1.Logging.log('info', `Preparing trial ${t.id}`);
            if (!(t === null || t === void 0 ? void 0 : t.id))
                throw new Error('Cannot prepare trial! ID is missing, or trial is null');
            // IF BLUEPRINT SOURCE!!
            // copy template-scenario to trial ID scenario folder
            // modify <trialfolder>/sandbox.lua with your parameters provided
            // download and move mods into the correct folder
            // mark as prepared
            t.setStage('preparing');
            try {
                if (t.source.variant == 'blueprint') {
                    // COpy our scenario source to scenarios, as we need it for blueprint running
                    let trialFolder = yield Factory.copyScenarioSourceToFolder(t.id);
                    // now, we need to modify the sandbox.lua file with our parameters
                    yield Factory.writeKeyValuesToLuaFile(path_1.default.join(trialFolder, 'sandbox.lua'), {
                        UID: t.id,
                        BLUEPRINT: t.source.text,
                        ITEM_TICKS: t.recordItems ? t.tickInterval : null,
                        ELEC_TICKS: t.recordElectric ? t.tickInterval : null,
                        CIRC_TICKS: t.recordCircuits ? t.tickInterval : null,
                        POLL_TICKS: t.recordPollution ? t.tickInterval : null,
                        BOTS: t.initialBots,
                    });
                }
                else if (t.source.variant == 'savegame') {
                    // 1. copy the save to the saves folder renaming it as the Trial ID
                    // 2. delete trial ID folder in 'saves' if it exists (NOT .zip)
                    // 3. Extract the save file to a folder of the same name
                    // copy savegame.lua
                    // modify control.lua
                    // copy the original save to the saves folder, renaming it as the trial ID
                    Logging_1.Logging.log('info', `Copying save ${t.source.text} to ${t.id}.zip`);
                    yield fs_extra_1.default.copyFile(t.source.text, path_1.default.join(Factory.savesPath, t.id + '.zip'));
                    // delete trial ID folder in 'saves' if it exists
                    Logging_1.Logging.log('info', `Deleting existing save folder ${t.id} if present`);
                    yield fs_extra_1.default.remove(path_1.default.join(Factory.savesPath, t.id));
                    // extract save file
                    Logging_1.Logging.log('info', `Extracting save file ${t.id}.zip to ${path_1.default.join(Factory.savesPath, t.id)}`);
                    let results = yield (0, decompress_1.default)(path_1.default.join(Factory.savesPath, t.id + '.zip'), Factory.savesPath);
                    // rename extracted folder to trial ID
                    let fn = results[0].path.substring(0, results[0].path.indexOf('/'));
                    fs_extra_1.default.rename(path_1.default.join(Factory.savesPath, fn), path_1.default.join(Factory.savesPath, t.id));
                    // copy savegame.lua
                    Logging_1.Logging.log('info', `Copying savegame.lua to ${path_1.default.join(Factory.savesPath, t.id)}`);
                    yield fs_extra_1.default.copyFile(path_1.default.join(__dirname, '../', '../', 'factory', 'savegame.lua'), path_1.default.join(Factory.savesPath, t.id, 'savegame.lua'));
                    // MODIFY savegame.lua with our trial changes
                    // now, we need to modify the sandbox.lua file with our parameters
                    yield Factory.writeKeyValuesToLuaFile(path_1.default.join(Factory.savesPath, t.id, 'savegame.lua'), {
                        UID: t.id,
                        ITEM_TICKS: t.recordItems ? t.tickInterval : null,
                        ELEC_TICKS: t.recordElectric ? t.tickInterval : null,
                        CIRC_TICKS: t.recordCircuits ? t.tickInterval : null,
                        POLL_TICKS: t.recordPollution ? t.tickInterval : null,
                        BOTS: t.initialBots,
                    });
                    // modify control.lua
                    Logging_1.Logging.log('info', `Modifying control.lua to include savegame.lua`);
                    let controlLua = yield fs_extra_1.default.readFile(path_1.default.join(Factory.savesPath, t.id, 'control.lua'), { encoding: 'utf8' });
                    controlLua += 'handler.add_lib(require("savegame"))';
                    yield fs_extra_1.default.writeFile(path_1.default.join(Factory.savesPath, t.id, 'control.lua'), controlLua, { encoding: 'utf8' });
                }
                else
                    throw new Error('Cannot prepare trial! Unknown source type provided');
                // download the mods specified in 'source' if we can. do NOT move them into the 'mods' folder yet
                if ((_a = t.source) === null || _a === void 0 ? void 0 : _a.modList)
                    yield Factory.cacheModList(t.source.modList);
            }
            catch (e) {
                Logging_1.Logging.log('error', { message: `Failed to prepare trial ${t.id}`, error: e });
                throw e;
            }
            t.setStage('prepared');
        });
    }
    static compileTrial(t) {
        return __awaiter(this, void 0, void 0, function* () {
            Logging_1.Logging.log('info', `Compiling trial ${t.id}`);
            if (!(t === null || t === void 0 ? void 0 : t.id))
                throw new Error('Cannot prepare trial! ID is missing, or trial is null');
            // trial must be prepared before we can compile it - if not, prepare it
            if (t.stage !== 'prepared')
                yield Factory.prepareTrial(t);
            // we do the executable check inside IF so that savegames don't throw an error
            // savegames as a whole dont need to be compiled, so we just skip this step
            if (t.source.variant == 'blueprint') {
                if (Factory.isTrialRunning)
                    throw new Error('Cannot compile trial! Factorio executable is already running');
                Factory.isTrialRunning = true;
                // compiling step for 'save game' source does nothing - compiling turns a scenario into a save, no point there
                t.setStage('compiling');
                try {
                    // First off, we need to move our mod configuration files into the mods folder if they exist
                    yield Factory.applyModsOfSource(t.source);
                    // remove existing compile folder and zip file if exists
                    yield fs_extra_1.default.remove(path_1.default.join(Factory.savesPath, t.id + '.zip'));
                    yield fs_extra_1.default.remove(path_1.default.join(Factory.savesPath, t.id));
                    // run factorio executable to convert scenario -> save
                    Logging_1.Logging.log('info', `Compiling scenario ${t.id} into save file`);
                    let results = yield Factory.runFactorio(['-m', t.id]);
                    if (!fs_extra_1.default.existsSync(path_1.default.join(Factory.savesPath, `${t.id}.zip`))) {
                        yield fs_extra_1.default.writeFile(path_1.default.join(Factory.factoryDataPath, 'compile.log'), results.execResults, { encoding: 'utf8' });
                        throw new Error(`Scenario ${t.id} could not be compiled! Zip save file does not exist - check 'compile.log' for more details`);
                    }
                    else
                        Logging_1.Logging.log('info', `Scenario ${t.id} compiled in ${results.end.getTime() - results.start.getTime()}ms`);
                    // extract the save file to a folder of the same name
                    Logging_1.Logging.log('info', `Extracting save file ${t.id}.zip to ${path_1.default.join(Factory.savesPath, t.id)}`);
                    yield (0, decompress_1.default)(path_1.default.join(Factory.savesPath, `${t.id}.zip`), path_1.default.join(Factory.savesPath));
                    if (!fs_extra_1.default.existsSync(path_1.default.join(Factory.savesPath, t.id, 'sandbox.lua')))
                        throw new Error(`Scenario ${t.id} was not extracted! Save Folder does not exist - extract ${t.id}.zip manually if needed`);
                    Factory.isTrialRunning = false;
                }
                catch (e) {
                    Factory.isTrialRunning = false;
                    Logging_1.Logging.log('error', { message: `Failed to compile scenario ${t.id}`, error: e });
                    throw e;
                }
            }
            t.setStage('compiled');
        });
    }
    static runTrial(t) {
        return __awaiter(this, void 0, void 0, function* () {
            Logging_1.Logging.log('info', `Running trial ${t.id}`);
            // should just run the trial, nothing more. dont do data analysis here
            if (!(t === null || t === void 0 ? void 0 : t.id))
                throw new Error('Cannot run trial! ID is missing, or trial is null');
            // trial must be compiled before we can run it - if not, compile it
            if (t.stage !== 'compiled')
                yield Factory.compileTrial(t);
            if (Factory.isTrialRunning)
                throw new Error('Cannot run trial! Factorio executable is already running');
            Factory.isTrialRunning = true;
            t.setStage('running');
            try {
                yield Factory.applyModsOfSource(t.source);
                t.startedRunAt = new Date();
                Logging_1.Logging.log('info', { message: `Running trial ${t.id}` });
                const execParams = ['--benchmark', `${t.id}`,
                    '--benchmark-ticks', `${t.length}`];
                if (t.recordSystem) {
                    execParams.push('--benchmark-verbose');
                    execParams.push('all');
                }
                let r = yield Factory.runFactorio(execParams);
                t.startedAt = r.start;
                t.endedAt = r.end;
                // parse out some info from text results - this is fast and just text parsing, when we get to sys data we proceed
                let gameTimeStart = r.execResults.indexOf('Performed ');
                // If there is a trial - we read up until the data. Else, we read up until the end
                let idxA = r.execResults.indexOf('run 1:');
                let gameTimeSubstr = r.execResults.substring(gameTimeStart, idxA >= 0 ? idxA : r.execResults.length - 1);
                //Read very last row of execResults for perfTime - all we need there. Then do perf stats
                let lastIdx = r.execResults.substring(0, r.execResults.length - 1).lastIndexOf('\n');
                let lastLine = r.execResults.substring(lastIdx);
                let perfTime = lastLine.substring(3, lastLine.length - 8);
                let execTime = (Number.parseFloat(perfTime)) * 1000;
                // Grab the first newline after the 'run' declaration, then substring from there until end of file
                let perfStart = r.execResults.indexOf('\n', r.execResults.indexOf('run 1:'));
                let perfSubstr = r.execResults.substring(perfStart + 1, lastIdx);
                // Record our end parsing time.
                let nd = (new Date()).getTime();
                // Grab the text logs that prefix our raw system data
                if (idxA != -1)
                    t.textLogs = r.execResults.substring(0, idxA).split('\n');
                else
                    t.textLogs = r.execResults.substring(0, r.execResults.indexOf('Running update') - 1).split('\n');
                // We parse this way later in the analysis stage. We just do enough to split out textlogs and metadata
                t.rawSystemText = perfSubstr;
                t.metadata = {
                    updates: Number.parseInt(gameTimeSubstr.substring(10, gameTimeSubstr.indexOf(' ', 10))),
                    avg: Number.parseFloat(gameTimeSubstr.substring(gameTimeSubstr.indexOf('avg:') + 4, gameTimeSubstr.indexOf('ms', gameTimeSubstr.indexOf('avg:') + 4))),
                    min: Number.parseFloat(gameTimeSubstr.substring(gameTimeSubstr.indexOf('min:') + 4, gameTimeSubstr.indexOf('ms', gameTimeSubstr.indexOf('min:') + 4))),
                    max: Number.parseFloat(gameTimeSubstr.substring(gameTimeSubstr.indexOf('max:') + 4, gameTimeSubstr.indexOf('ms', gameTimeSubstr.indexOf('max:') + 4))),
                    trialTime: Number.parseFloat(gameTimeSubstr.substring(gameTimeSubstr.indexOf('in ') + 3, gameTimeSubstr.indexOf('ms'))),
                    //points: gdRaw.split('\n').length - 1,
                    totalTime: nd - t.startedAt.getTime(),
                    execTime: execTime
                };
                Logging_1.Logging.log('info', { message: `Finished trial ${t.id} - parsing console outputs` });
                Factory.isTrialRunning = false;
                // #todo check if script-output files were made... can do this later
            }
            catch (e) {
                Factory.isTrialRunning = false;
                Logging_1.Logging.log('error', { message: `Failed to run trial ${t.id}`, error: e });
                throw e;
            }
            t.setStage('ran');
        });
    }
    static analyzeTrial(t, createSummaries = true, saveToDB = false) {
        var _a, _b, _c, _d, _e, _f;
        return __awaiter(this, void 0, void 0, function* () {
            Logging_1.Logging.log('info', `Analyzing trial ${t.id}`);
            if (!(t === null || t === void 0 ? void 0 : t.id))
                throw new Error('Cannot analyze trial! ID is missing, or trial is null');
            yield FactoryDatabase_1.FactoryDatabase.deleteTrialData(t.id);
            // trial must be compiled before we can run it - if not, compile it
            if (t.stage !== 'ran')
                yield Factory.runTrial(t);
            t.setStage('analyzing');
            Logging_1.Logging.log('info', `Trial ${t.id} analyzed - parsing data files next`);
            try {
                let rObj = {};
                // we should also try to parse out any recipelister files that were created - these are used to build better item data info
                if (t.recordItems) {
                    Logging_1.Logging.log('info', { message: `Analyzing item data for trial ${t.id}` });
                    rObj.items = {
                        data: yield Factory.parseItemDataFile(path_1.default.join(Factory.scriptOutputPath, 'data', t.dataFiles.items), t),
                        trial: t
                    };
                    if (saveToDB && t instanceof Trial_1.Trial)
                        yield FactoryDatabase_1.FactoryDatabase.insertItemRecords(rObj.items.data);
                    if (createSummaries && t instanceof Trial_1.Trial)
                        rObj.items = DatasetAnalysis_1.DatasetAnalysis.createSummaryOfItemDataset(rObj.items);
                }
                /*if (t.recordElectric) {
                    Logging.log('info', {message: `Analyzing electric data for trial ${t.id}`});
                    let electricDataset = new ElectricDataset(t, path.join(Factory.scriptOutputPath, 'data', t.dataFiles.electric))
                    await electricDataset.parseData()
                    rObj.electric = electricDataset;
                }*/
                if (t.recordElectric) {
                    Logging_1.Logging.log('info', { message: `Analyzing electric data for trial ${t.id}` });
                    rObj.electric = {
                        data: yield Factory.parseElectricDataFile(path_1.default.join(Factory.scriptOutputPath, 'data', t.dataFiles.electric), t),
                        trial: t
                    };
                    if (saveToDB && t instanceof Trial_1.Trial)
                        yield FactoryDatabase_1.FactoryDatabase.insertElectricRecords(rObj.electric.data);
                    if (createSummaries && t instanceof Trial_1.Trial)
                        rObj.electric = DatasetAnalysis_1.DatasetAnalysis.createSummaryOfElectricDataset(rObj.electric);
                }
                if (t.recordCircuits) {
                    Logging_1.Logging.log('info', { message: `Analyzing circuit data for trial ${t.id}` });
                    rObj.circuits = {
                        data: yield Factory.parseCircuitDataFile(path_1.default.join(Factory.scriptOutputPath, 'data', t.dataFiles.circuits), t),
                        trial: t
                    };
                    if (saveToDB && t instanceof Trial_1.Trial)
                        yield FactoryDatabase_1.FactoryDatabase.insertCircuitRecords(((_b = (_a = rObj.circuits) === null || _a === void 0 ? void 0 : _a.data) !== null && _b !== void 0 ? _b : []));
                    if (createSummaries && t instanceof Trial_1.Trial)
                        rObj.circuits = DatasetAnalysis_1.DatasetAnalysis.createSummaryOfCircuitDataset(rObj.circuits);
                }
                if (t.recordPollution) {
                    Logging_1.Logging.log('info', { message: `Analyzing pollution data for trial ${t.id}` });
                    rObj.pollution = {
                        data: yield Factory.parsePollutionDataFile(path_1.default.join(Factory.scriptOutputPath, 'data', t.dataFiles.pollution), t),
                        trial: t
                    };
                    if (saveToDB && t instanceof Trial_1.Trial)
                        yield FactoryDatabase_1.FactoryDatabase.insertPollutionRecords(((_d = (_c = rObj.pollution) === null || _c === void 0 ? void 0 : _c.data) !== null && _d !== void 0 ? _d : []));
                    if (createSummaries && t instanceof Trial_1.Trial)
                        rObj.pollution = DatasetAnalysis_1.DatasetAnalysis.createSummaryOfPollutionDataset(rObj.pollution);
                }
                if (t.recordSystem) {
                    Logging_1.Logging.log('info', { message: `Analyzing system data for trial ${t.id}` });
                    rObj.system = {
                        data: Factory.parseSystemData(t.rawSystemText, t),
                        trial: t
                    };
                    if (saveToDB && t instanceof Trial_1.Trial)
                        yield FactoryDatabase_1.FactoryDatabase.insertSystemRecords(((_f = (_e = rObj.system) === null || _e === void 0 ? void 0 : _e.data) !== null && _f !== void 0 ? _f : []));
                    if (createSummaries && t instanceof Trial_1.Trial)
                        rObj.system = DatasetAnalysis_1.DatasetAnalysis.createSummaryOfSystemDataset(rObj.system);
                }
                yield Factory.deleteScriptOutputFiles(t);
                yield Factory.deleteSaveFileClutter(t);
                t.setStage('analyzed');
                if (saveToDB && t instanceof Trial_1.Trial) {
                    yield FactoryDatabase_1.FactoryDatabase.saveTrial(t, true);
                }
                return rObj;
            }
            catch (e) {
                Factory.isTrialRunning = false;
                Logging_1.Logging.log('error', { message: `Failed to analyze trial ${t.id}`, error: e });
                throw e;
            }
        });
    }
    static parseItemDataFile(filepath, trial) {
        return __awaiter(this, void 0, void 0, function* () {
            let raw = yield fs_extra_1.default.readFile(filepath, 'utf-8');
            let results = [];
            let lines = raw.split('\n');
            for (let i = 0; i < lines.length - 1; i++) {
                let l = lines[i];
                // keep an active map of all items in this tick, adding information as needed
                let itemMap = {};
                // Parse line information into temp object
                let lineParsed = JSON.parse(l);
                // for each key in cons, upsert data to itemMap
                let co = Object.keys(lineParsed.cons);
                for (let c of co) {
                    // the item record if it exists
                    let ir = itemMap[c];
                    if (ir) {
                        // Update the cons value, as there is nothing else it can be.
                        ir.cons = lineParsed.cons[c];
                    }
                    else {
                        // No object record exists yet - create and set it
                        itemMap[c] = {
                            label: c,
                            tick: (i + 1) * trial.tickInterval,
                            cons: lineParsed.cons[c],
                            prod: 0,
                        };
                    }
                }
                let po = Object.keys(lineParsed.prod);
                for (let p of po) {
                    // the item record if it exists
                    let ir = itemMap[p];
                    if (ir) {
                        // Update the cons value, as there is nothing else it can be.
                        ir.prod = lineParsed.prod[p];
                    }
                    else {
                        // No object record exists yet - create and set it
                        itemMap[p] = {
                            label: p,
                            tick: (i + 1) * trial.tickInterval,
                            cons: 0,
                            prod: lineParsed.prod[p],
                        };
                    }
                }
                // Now that the itemMap is populated, push all items to the results array for this tick. Convert all keys to an array, grabbing values from the map in a loop
                let itemKeys = Object.keys(itemMap);
                for (let ik of itemKeys) {
                    results.push(itemMap[ik]);
                }
            }
            // for each item, calculate the average and total for  each item
            let g = lodash_1.default.groupBy(results, 'label');
            let kList = Object.keys(g);
            let metadata = {
                avg: {},
                total: {}
            };
            for (let i = 0; i < kList.length; i++) {
                // sum / length (in minutes)
                let itemData = g[kList[i]];
                let secondsLength = trial.length / (60);
                let totalCons = lodash_1.default.sumBy(itemData, 'cons');
                let totalProd = lodash_1.default.sumBy(itemData, 'prod');
                metadata.avg[kList[i]] = {
                    cons: totalCons / secondsLength,
                    prod: totalProd / secondsLength
                };
                metadata.total[kList[i]] = {
                    cons: totalCons,
                    prod: totalProd
                };
            }
            // lastly, if  our trial is a 'SavedTrial' type, we will make sure we convert all these records into SavedFlowRecords
            if (trial instanceof Trial_1.Trial && (Dataset_1.GameFlowItemRecord === null || Dataset_1.GameFlowItemRecord === void 0 ? void 0 : Dataset_1.GameFlowItemRecord.fromRecords)) {
                results = Dataset_1.GameFlowItemRecord.fromRecords(results, trial.id);
            }
            trial.itemMetadata = metadata;
            return results;
        });
    }
    static parseElectricDataFile(filepath, trial) {
        var _a, _b;
        return __awaiter(this, void 0, void 0, function* () {
            if (filepath == null)
                throw new Error('Cannot parse electric data file! Filepath is null');
            let raw = yield fs_extra_1.default.readFile(filepath, 'utf-8');
            let results = [];
            let lines = raw.split('\n');
            for (let i = 0; i < lines.length - 1; i++) {
                // each line is an array with 1 object inside, containing cons and prod
                // cons and prod are both objects with double values for each field
                // keep an active map of all items in this tick, adding information as needed
                let elecMap = {};
                let l = JSON.parse(lines[i]);
                // for each key in cons, upsert data to itemMap
                if (l && ((_a = l[0]) === null || _a === void 0 ? void 0 : _a.cons)) {
                    let co = Object.keys(l[0].cons);
                    for (let c of co) {
                        // the item record if it exists
                        let ir = elecMap[c];
                        if (ir) {
                            // Update the cons value, as there is nothing else it can be.
                            ir.cons = l[0].cons[c];
                        }
                        else {
                            // No object record exists yet - create and set it
                            elecMap[c] = {
                                label: c,
                                tick: (i + 1) * trial.tickInterval,
                                cons: l[0].cons[c],
                                prod: 0,
                            };
                        }
                    }
                }
                if (l && ((_b = l[0]) === null || _b === void 0 ? void 0 : _b.prod)) {
                    for (let p of Object.keys(l[0].prod)) {
                        // the item record if it exists
                        let ir = elecMap[p];
                        if (ir) {
                            // Update the cons value, as there is nothing else it can be.
                            ir.prod = l[0].prod[p];
                        }
                        else {
                            // No object record exists yet - create and set it
                            elecMap[p] = {
                                label: p,
                                tick: (i + 1) * trial.tickInterval,
                                cons: 0,
                                prod: l[0].prod[p],
                            };
                        }
                    }
                }
                // Now that the itemMap is populated, push all items to the results array for this tick. Convert all keys to an array, grabbing values from the map in a loop
                let itemKeys = Object.keys(elecMap);
                for (let ik of itemKeys) {
                    results.push(elecMap[ik]);
                }
            }
            // for each item, calculate the average and total for  each item
            let g = lodash_1.default.groupBy(results, 'label');
            let kList = Object.keys(g);
            let metadata = {
                avg: {},
                min: {},
                max: {}
            };
            for (let i = 0; i < kList.length; i++) {
                // sum / length (in minutes)
                let elecData = g[kList[i]];
                let secondsLength = trial.length / (60);
                let totalCons = lodash_1.default.sumBy(elecData, 'cons');
                let totalProd = lodash_1.default.sumBy(elecData, 'prod');
                let minCons = lodash_1.default.minBy(elecData, 'cons').cons;
                let maxCons = lodash_1.default.maxBy(elecData, 'cons').cons;
                let minProd = lodash_1.default.minBy(elecData, 'prod').prod;
                let maxProd = lodash_1.default.maxBy(elecData, 'prod').prod;
                metadata.avg[kList[i]] = {
                    cons: totalCons / secondsLength,
                    prod: totalProd / secondsLength
                };
                metadata.min[kList[i]] = {
                    cons: minCons,
                    prod: minProd
                };
                metadata.max[kList[i]] = {
                    cons: maxCons,
                    prod: maxProd
                };
            }
            // lastly, if  our trial is a 'SavedTrial' type, we will make sure we convert all these records into SavedFlowRecords
            if (trial instanceof Trial_1.Trial && (Dataset_1.GameFlowElectricRecord === null || Dataset_1.GameFlowElectricRecord === void 0 ? void 0 : Dataset_1.GameFlowElectricRecord.fromRecords)) {
                results = Dataset_1.GameFlowElectricRecord.fromRecords(results, trial.id);
            }
            trial.electricMetadata = metadata;
            return results;
        });
    }
    static parseCircuitDataFile(filepath, trial) {
        var _a;
        return __awaiter(this, void 0, void 0, function* () {
            if (filepath == null)
                throw new Error('Cannot parse circuit data file! Filepath is null');
            let raw = yield fs_extra_1.default.readFile(filepath, 'utf-8');
            let results = [];
            let lines = raw.split('\n');
            for (let i = 0; i < lines.length - 1; i++) {
                let networks = JSON.parse(lines[i]);
                // l is an array, each object in it has network_id color  and signals (signals is complex)
                for (let j = 0; j < networks.length; j++) {
                    // find signals - cycle through for each signal, adding to the results array
                    let subnet = networks[j];
                    let network_id = subnet.network_id;
                    let color = subnet.color;
                    if (subnet.signals && subnet.signals.length > 0) {
                        for (let k = 0; k < subnet.signals.length; k++) {
                            let signal = subnet.signals[k];
                            results.push({
                                label: signal.signal.name,
                                signalType: signal.signal.type,
                                tick: (i + 1) * trial.tickInterval,
                                networkId: network_id,
                                color: color,
                                count: signal.count
                            });
                        }
                    }
                }
            }
            if (trial instanceof Trial_1.Trial && (Dataset_1.GameFlowCircuitRecord === null || Dataset_1.GameFlowCircuitRecord === void 0 ? void 0 : Dataset_1.GameFlowCircuitRecord.fromRecords)) {
                results = Dataset_1.GameFlowCircuitRecord.fromRecords(results, trial.id);
            }
            // not going to bother doing math related things - circuit values could mean a number of things, but its not a 'resource' like items
            // instead, list other facts about the dataset, such as....
            /// list of signals used (label only)
            /// list of different networks, and how many different labels are on each (number)
            // final metadata will be a object that has 'signalList' and 'networkList'
            // signalList just includes label (string only), networkList includes network_id (number) and count (number)
            let networkMap = lodash_1.default.groupBy(results, 'networkId');
            let networkList = [];
            for (let k of Object.keys(networkMap)) {
                // push to networkList an object that has the 'network' and the 'count' of unique labels inside that network
                networkList.push({
                    network: Number.parseInt(k),
                    uniqueSignalCount: lodash_1.default.uniqBy(networkMap[k], 'label').length
                });
            }
            let signals = (_a = (lodash_1.default.uniqBy(results, 'label')).map((v) => v.label)) !== null && _a !== void 0 ? _a : [];
            trial.circuitMetadata = {
                signals: signals,
                networks: networkList
            };
            return results;
        });
    }
    static parsePollutionDataFile(filepath, trial) {
        return __awaiter(this, void 0, void 0, function* () {
            let raw = yield fs_extra_1.default.readFile(filepath, 'utf-8');
            let results = [];
            let lines = raw.split('\n');
            // Now, need to sort through the list of lines and parse out the data
            // last line isn't 'real' - there's always a newline in the banana stand
            for (let i = 0; i < lines.length - 1; i++) {
                let l = lines[i];
                // each line is a object with 1 field - pollution. This is a float
                let lData = JSON.parse(l);
                results.push({
                    label: 'pollution',
                    tick: (i + 1) * trial.tickInterval,
                    count: lData.pollution
                });
            }
            if (trial instanceof Trial_1.Trial && (Dataset_1.GameFlowPollutionRecord === null || Dataset_1.GameFlowPollutionRecord === void 0 ? void 0 : Dataset_1.GameFlowPollutionRecord.fromRecords)) {
                results = Dataset_1.GameFlowPollutionRecord.fromRecords(results, trial.id);
            }
            // for each item, calculate the average and total for  each item
            let g = lodash_1.default.groupBy(results, 'label');
            let kList = Object.keys(g);
            let metadata = {
                avg: 0,
                max: 0,
                min: 0,
            };
            for (let i = 0; i < kList.length; i++) {
                // sum / length (in minutes)
                let pollutionData = g[kList[i]];
                metadata.avg = lodash_1.default.meanBy(pollutionData, 'count');
                metadata.max = lodash_1.default.maxBy(pollutionData, 'count').count;
                metadata.min = lodash_1.default.minBy(pollutionData, 'count').count;
            }
            trial.pollutionMetadata = metadata;
            return results;
        });
    }
    static parseSystemData(raw, trial) {
        let lines = raw.split('\n');
        // First line is headers, so we remove it
        lines.shift();
        // Now we have an array of strings, each string being a line of csv data
        let results = [];
        let currentTickItem = null;
        let currentSetCount = 0;
        let runningTimestamp = 0;
        for (let l of lines) {
            // Split by commas
            let data = l.split(',');
            if (!currentTickItem) {
                currentSetCount = 1;
                currentTickItem = {
                    label: 'system',
                    tick: Number.parseInt(data[0].substring(1)),
                    timestamp: Number.parseInt(data[1]),
                    wholeUpdate: Number.parseInt(data[2]),
                    latencyUpdate: Number.parseInt(data[3]),
                    gameUpdate: Number.parseInt(data[4]),
                    circuitNetworkUpdate: Number.parseInt(data[5]),
                    transportLinesUpdate: Number.parseInt(data[6]),
                    fluidsUpdate: Number.parseInt(data[7]),
                    heatManagerUpdate: Number.parseInt(data[8]),
                    entityUpdate: Number.parseInt(data[9]),
                    particleUpdate: Number.parseInt(data[10]),
                    mapGenerator: Number.parseInt(data[11]),
                    mapGeneratorBasicTilesSupportCompute: Number.parseInt(data[12]),
                    mapGeneratorBasicTilesSupportApply: Number.parseInt(data[13]),
                    mapGeneratorCorrectedTilesPrepare: Number.parseInt(data[14]),
                    mapGeneratorCorrectedTilesCompute: Number.parseInt(data[15]),
                    mapGeneratorCorrectedTilesApply: Number.parseInt(data[16]),
                    mapGeneratorVariations: Number.parseInt(data[17]),
                    mapGeneratorEntitiesPrepare: Number.parseInt(data[18]),
                    mapGeneratorEntitiesCompute: Number.parseInt(data[19]),
                    mapGeneratorEntitiesApply: Number.parseInt(data[20]),
                    crcComputation: Number.parseInt(data[21]),
                    electricNetworkUpdate: Number.parseInt(data[22]),
                    logisticManagerUpdate: Number.parseInt(data[23]),
                    constructionManagerUpdate: Number.parseInt(data[24]),
                    pathFinder: Number.parseInt(data[25]),
                    trains: Number.parseInt(data[26]),
                    trainPathFinder: Number.parseInt(data[27]),
                    commander: Number.parseInt(data[28]),
                    chartRefresh: Number.parseInt(data[29]),
                    luaGarbageIncremental: Number.parseInt(data[30]),
                    chartUpdate: Number.parseInt(data[31]),
                    scriptUpdate: Number.parseInt(data[32])
                };
            }
            else {
                // add to the tick item, then evaluate if we need to push and reset it (if we are at tickInterval count)
                // We add 1 to the tick because it is representative of the 'last 300 ticks'. So if we state 300, we are
                // NOT including tick 300. therefore, it can show as 300 for data organization and matching, but tick 300
                // goes towards the next interval mark, at 600.
                currentTickItem.tick = Number.parseInt(data[0].substring(1)) + 1;
                currentTickItem.timestamp = Number.parseInt(data[1]);
                currentTickItem.wholeUpdate += Number.parseInt(data[2]);
                currentTickItem.latencyUpdate += Number.parseInt(data[3]);
                currentTickItem.gameUpdate += Number.parseInt(data[4]);
                currentTickItem.circuitNetworkUpdate += Number.parseInt(data[5]);
                currentTickItem.transportLinesUpdate += Number.parseInt(data[6]);
                currentTickItem.fluidsUpdate += Number.parseInt(data[7]);
                currentTickItem.heatManagerUpdate += Number.parseInt(data[8]);
                currentTickItem.entityUpdate += Number.parseInt(data[9]);
                currentTickItem.particleUpdate += Number.parseInt(data[10]);
                currentTickItem.mapGenerator += Number.parseInt(data[11]);
                currentTickItem.mapGeneratorBasicTilesSupportCompute += Number.parseInt(data[12]);
                currentTickItem.mapGeneratorBasicTilesSupportApply += Number.parseInt(data[13]);
                currentTickItem.mapGeneratorCorrectedTilesPrepare += Number.parseInt(data[14]);
                currentTickItem.mapGeneratorCorrectedTilesCompute += Number.parseInt(data[15]);
                currentTickItem.mapGeneratorCorrectedTilesApply += Number.parseInt(data[16]);
                currentTickItem.mapGeneratorVariations += Number.parseInt(data[17]);
                currentTickItem.mapGeneratorEntitiesPrepare += Number.parseInt(data[18]);
                currentTickItem.mapGeneratorEntitiesCompute += Number.parseInt(data[19]);
                currentTickItem.mapGeneratorEntitiesApply += Number.parseInt(data[20]);
                currentTickItem.crcComputation += Number.parseInt(data[21]);
                currentTickItem.electricNetworkUpdate += Number.parseInt(data[22]);
                currentTickItem.logisticManagerUpdate += Number.parseInt(data[23]);
                currentTickItem.constructionManagerUpdate += Number.parseInt(data[24]);
                currentTickItem.pathFinder += Number.parseInt(data[25]);
                currentTickItem.trains += Number.parseInt(data[26]);
                currentTickItem.trainPathFinder += Number.parseInt(data[27]);
                currentTickItem.commander += Number.parseInt(data[28]);
                currentTickItem.chartRefresh += Number.parseInt(data[29]);
                currentTickItem.luaGarbageIncremental += Number.parseInt(data[30]);
                currentTickItem.chartUpdate += Number.parseInt(data[31]);
                currentTickItem.scriptUpdate += Number.parseInt(data[32]);
                currentSetCount += 1;
            }
            if (currentSetCount >= trial.tickInterval) {
                // Cut nanoseconds down to milliseconds so its more comprehensive
                // Timestamp is a 'always increasing' field, so instead we just want to record the differences between points.
                // This gives much more useful information
                let tb = currentTickItem.timestamp;
                currentTickItem.timestamp = (currentTickItem.timestamp - runningTimestamp) / 1000000;
                runningTimestamp = tb;
                currentTickItem.wholeUpdate = currentTickItem.wholeUpdate / 1000000;
                currentTickItem.latencyUpdate = currentTickItem.latencyUpdate / 1000000;
                currentTickItem.gameUpdate = currentTickItem.gameUpdate / 1000000;
                currentTickItem.circuitNetworkUpdate = currentTickItem.circuitNetworkUpdate / 1000000;
                currentTickItem.transportLinesUpdate = currentTickItem.transportLinesUpdate / 1000000;
                currentTickItem.fluidsUpdate = currentTickItem.fluidsUpdate / 1000000;
                currentTickItem.heatManagerUpdate = currentTickItem.heatManagerUpdate / 1000000;
                currentTickItem.entityUpdate = currentTickItem.entityUpdate / 1000000;
                currentTickItem.particleUpdate = currentTickItem.particleUpdate / 1000000;
                currentTickItem.mapGenerator = currentTickItem.mapGenerator / 1000000;
                currentTickItem.mapGeneratorBasicTilesSupportCompute = currentTickItem.mapGeneratorBasicTilesSupportCompute / 1000000;
                currentTickItem.mapGeneratorBasicTilesSupportApply = currentTickItem.mapGeneratorBasicTilesSupportApply / 1000000;
                currentTickItem.mapGeneratorCorrectedTilesPrepare = currentTickItem.mapGeneratorCorrectedTilesPrepare / 1000000;
                currentTickItem.mapGeneratorCorrectedTilesCompute = currentTickItem.mapGeneratorCorrectedTilesCompute / 1000000;
                currentTickItem.mapGeneratorCorrectedTilesApply = currentTickItem.mapGeneratorCorrectedTilesApply / 1000000;
                currentTickItem.mapGeneratorVariations = currentTickItem.mapGeneratorVariations / 1000000;
                currentTickItem.mapGeneratorEntitiesPrepare = currentTickItem.mapGeneratorEntitiesPrepare / 1000000;
                currentTickItem.mapGeneratorEntitiesCompute = currentTickItem.mapGeneratorEntitiesCompute / 1000000;
                currentTickItem.mapGeneratorEntitiesApply = currentTickItem.mapGeneratorEntitiesApply / 1000000;
                currentTickItem.crcComputation = currentTickItem.crcComputation / 1000000;
                currentTickItem.electricNetworkUpdate = currentTickItem.electricNetworkUpdate / 1000000;
                currentTickItem.logisticManagerUpdate = currentTickItem.logisticManagerUpdate / 1000000;
                currentTickItem.constructionManagerUpdate = currentTickItem.constructionManagerUpdate / 1000000;
                currentTickItem.pathFinder = currentTickItem.pathFinder / 1000000;
                currentTickItem.trains = currentTickItem.trains / 1000000;
                currentTickItem.trainPathFinder = currentTickItem.trainPathFinder / 1000000;
                currentTickItem.commander = currentTickItem.commander / 1000000;
                currentTickItem.chartRefresh = currentTickItem.chartRefresh / 1000000;
                currentTickItem.luaGarbageIncremental = currentTickItem.luaGarbageIncremental / 1000000;
                currentTickItem.chartUpdate = currentTickItem.chartUpdate / 1000000;
                currentTickItem.scriptUpdate = currentTickItem.scriptUpdate / 1000000;
                // Push the current tick item, then reset it.
                results.push(currentTickItem);
                currentTickItem = null;
                currentSetCount = 0;
            }
        }
        let metadata = {
            avg: {
                timestamp: lodash_1.default.meanBy(results, 'timestamp'),
                wholeUpdate: lodash_1.default.meanBy(results, 'wholeUpdate'),
                latencyUpdate: lodash_1.default.meanBy(results, 'latencyUpdate'),
                gameUpdate: lodash_1.default.meanBy(results, 'gameUpdate'),
                circuitNetworkUpdate: lodash_1.default.meanBy(results, 'circuitNetworkUpdate'),
                transportLinesUpdate: lodash_1.default.meanBy(results, 'transportLinesUpdate'),
                fluidsUpdate: lodash_1.default.meanBy(results, 'fluidsUpdate'),
                heatManagerUpdate: lodash_1.default.meanBy(results, 'heatManagerUpdate'),
                entityUpdate: lodash_1.default.meanBy(results, 'entityUpdate'),
                particleUpdate: lodash_1.default.meanBy(results, 'particleUpdate'),
                mapGenerator: lodash_1.default.meanBy(results, 'mapGenerator'),
                mapGeneratorBasicTilesSupportCompute: lodash_1.default.meanBy(results, 'mapGeneratorBasicTilesSupportCompute'),
                mapGeneratorBasicTilesSupportApply: lodash_1.default.meanBy(results, 'mapGeneratorBasicTilesSupportApply'),
                mapGeneratorCorrectedTilesPrepare: lodash_1.default.meanBy(results, 'mapGeneratorCorrectedTilesPrepare'),
                mapGeneratorCorrectedTilesCompute: lodash_1.default.meanBy(results, 'mapGeneratorCorrectedTilesCompute'),
                mapGeneratorCorrectedTilesApply: lodash_1.default.meanBy(results, 'mapGeneratorCorrectedTilesApply'),
                mapGeneratorVariations: lodash_1.default.meanBy(results, 'mapGeneratorVariations'),
                mapGeneratorEntitiesPrepare: lodash_1.default.meanBy(results, 'mapGeneratorEntitiesPrepare'),
                mapGeneratorEntitiesCompute: lodash_1.default.meanBy(results, 'mapGeneratorEntitiesCompute'),
                mapGeneratorEntitiesApply: lodash_1.default.meanBy(results, 'mapGeneratorEntitiesApply'),
                crcComputation: lodash_1.default.meanBy(results, 'crcComputation'),
                electricNetworkUpdate: lodash_1.default.meanBy(results, 'electricNetworkUpdate'),
                logisticManagerUpdate: lodash_1.default.meanBy(results, 'logisticManagerUpdate'),
                constructionManagerUpdate: lodash_1.default.meanBy(results, 'constructionManagerUpdate'),
                pathFinder: lodash_1.default.meanBy(results, 'pathFinder'),
                trains: lodash_1.default.meanBy(results, 'trains'),
                trainPathFinder: lodash_1.default.meanBy(results, 'trainPathFinder'),
                commander: lodash_1.default.meanBy(results, 'commander'),
                chartRefresh: lodash_1.default.meanBy(results, 'chartRefresh'),
                luaGarbageIncremental: lodash_1.default.meanBy(results, 'luaGarbageIncremental'),
                chartUpdate: lodash_1.default.meanBy(results, 'chartUpdate'),
                scriptUpdate: lodash_1.default.meanBy(results, 'scriptUpdate')
            },
            max: {
                timestamp: lodash_1.default.maxBy(results, 'timestamp').timestamp,
                wholeUpdate: lodash_1.default.maxBy(results, 'wholeUpdate').wholeUpdate,
                latencyUpdate: lodash_1.default.maxBy(results, 'latencyUpdate').latencyUpdate,
                gameUpdate: lodash_1.default.maxBy(results, 'gameUpdate').gameUpdate,
                circuitNetworkUpdate: lodash_1.default.maxBy(results, 'circuitNetworkUpdate').circuitNetworkUpdate,
                transportLinesUpdate: lodash_1.default.maxBy(results, 'transportLinesUpdate').transportLinesUpdate,
                fluidsUpdate: lodash_1.default.maxBy(results, 'fluidsUpdate').fluidsUpdate,
                heatManagerUpdate: lodash_1.default.maxBy(results, 'heatManagerUpdate').heatManagerUpdate,
                entityUpdate: lodash_1.default.maxBy(results, 'entityUpdate').entityUpdate,
                particleUpdate: lodash_1.default.maxBy(results, 'particleUpdate').particleUpdate,
                mapGenerator: lodash_1.default.maxBy(results, 'mapGenerator').mapGenerator,
                mapGeneratorBasicTilesSupportCompute: lodash_1.default.maxBy(results, 'mapGeneratorBasicTilesSupportCompute').mapGeneratorBasicTilesSupportCompute,
                mapGeneratorBasicTilesSupportApply: lodash_1.default.maxBy(results, 'mapGeneratorBasicTilesSupportApply').mapGeneratorBasicTilesSupportApply,
                mapGeneratorCorrectedTilesPrepare: lodash_1.default.maxBy(results, 'mapGeneratorCorrectedTilesPrepare').mapGeneratorCorrectedTilesPrepare,
                mapGeneratorCorrectedTilesCompute: lodash_1.default.maxBy(results, 'mapGeneratorCorrectedTilesCompute').mapGeneratorCorrectedTilesCompute,
                mapGeneratorCorrectedTilesApply: lodash_1.default.maxBy(results, 'mapGeneratorCorrectedTilesApply').mapGeneratorCorrectedTilesApply,
                mapGeneratorVariations: lodash_1.default.maxBy(results, 'mapGeneratorVariations').mapGeneratorVariations,
                mapGeneratorEntitiesPrepare: lodash_1.default.maxBy(results, 'mapGeneratorEntitiesPrepare').mapGeneratorEntitiesPrepare,
                mapGeneratorEntitiesCompute: lodash_1.default.maxBy(results, 'mapGeneratorEntitiesCompute').mapGeneratorEntitiesCompute,
                mapGeneratorEntitiesApply: lodash_1.default.maxBy(results, 'mapGeneratorEntitiesApply').mapGeneratorEntitiesApply,
                crcComputation: lodash_1.default.maxBy(results, 'crcComputation').crcComputation,
                electricNetworkUpdate: lodash_1.default.maxBy(results, 'electricNetworkUpdate').electricNetworkUpdate,
                logisticManagerUpdate: lodash_1.default.maxBy(results, 'logisticManagerUpdate').logisticManagerUpdate,
                constructionManagerUpdate: lodash_1.default.maxBy(results, 'constructionManagerUpdate').constructionManagerUpdate,
                pathFinder: lodash_1.default.maxBy(results, 'pathFinder').pathFinder,
                trains: lodash_1.default.maxBy(results, 'trains').trains,
                trainPathFinder: lodash_1.default.maxBy(results, 'trainPathFinder').trainPathFinder,
                commander: lodash_1.default.maxBy(results, 'commander').commander,
                chartRefresh: lodash_1.default.maxBy(results, 'chartRefresh').chartRefresh,
                luaGarbageIncremental: lodash_1.default.maxBy(results, 'luaGarbageIncremental').luaGarbageIncremental,
                chartUpdate: lodash_1.default.maxBy(results, 'chartUpdate').chartUpdate,
                scriptUpdate: lodash_1.default.maxBy(results, 'scriptUpdate').scriptUpdate
            },
            min: {
                timestamp: lodash_1.default.minBy(results, 'timestamp').timestamp,
                wholeUpdate: lodash_1.default.minBy(results, 'wholeUpdate').wholeUpdate,
                latencyUpdate: lodash_1.default.minBy(results, 'latencyUpdate').latencyUpdate,
                gameUpdate: lodash_1.default.minBy(results, 'gameUpdate').gameUpdate,
                circuitNetworkUpdate: lodash_1.default.minBy(results, 'circuitNetworkUpdate').circuitNetworkUpdate,
                transportLinesUpdate: lodash_1.default.minBy(results, 'transportLinesUpdate').transportLinesUpdate,
                fluidsUpdate: lodash_1.default.minBy(results, 'fluidsUpdate').fluidsUpdate,
                heatManagerUpdate: lodash_1.default.minBy(results, 'heatManagerUpdate').heatManagerUpdate,
                entityUpdate: lodash_1.default.minBy(results, 'entityUpdate').entityUpdate,
                particleUpdate: lodash_1.default.minBy(results, 'particleUpdate').particleUpdate,
                mapGenerator: lodash_1.default.minBy(results, 'mapGenerator').mapGenerator,
                mapGeneratorBasicTilesSupportCompute: lodash_1.default.minBy(results, 'mapGeneratorBasicTilesSupportCompute').mapGeneratorBasicTilesSupportCompute,
                mapGeneratorBasicTilesSupportApply: lodash_1.default.minBy(results, 'mapGeneratorBasicTilesSupportApply').mapGeneratorBasicTilesSupportApply,
                mapGeneratorCorrectedTilesPrepare: lodash_1.default.minBy(results, 'mapGeneratorCorrectedTilesPrepare').mapGeneratorCorrectedTilesPrepare,
                mapGeneratorCorrectedTilesCompute: lodash_1.default.minBy(results, 'mapGeneratorCorrectedTilesCompute').mapGeneratorCorrectedTilesCompute,
                mapGeneratorCorrectedTilesApply: lodash_1.default.minBy(results, 'mapGeneratorCorrectedTilesApply').mapGeneratorCorrectedTilesApply,
                mapGeneratorVariations: lodash_1.default.minBy(results, 'mapGeneratorVariations').mapGeneratorVariations,
                mapGeneratorEntitiesPrepare: lodash_1.default.minBy(results, 'mapGeneratorEntitiesPrepare').mapGeneratorEntitiesPrepare,
                mapGeneratorEntitiesCompute: lodash_1.default.minBy(results, 'mapGeneratorEntitiesCompute').mapGeneratorEntitiesCompute,
                mapGeneratorEntitiesApply: lodash_1.default.minBy(results, 'mapGeneratorEntitiesApply').mapGeneratorEntitiesApply,
                crcComputation: lodash_1.default.minBy(results, 'crcComputation').crcComputation,
                electricNetworkUpdate: lodash_1.default.minBy(results, 'electricNetworkUpdate').electricNetworkUpdate,
                logisticManagerUpdate: lodash_1.default.minBy(results, 'logisticManagerUpdate').logisticManagerUpdate,
                constructionManagerUpdate: lodash_1.default.minBy(results, 'constructionManagerUpdate').constructionManagerUpdate,
                pathFinder: lodash_1.default.minBy(results, 'pathFinder').pathFinder,
                trains: lodash_1.default.minBy(results, 'trains').trains,
                trainPathFinder: lodash_1.default.minBy(results, 'trainPathFinder').trainPathFinder,
                commander: lodash_1.default.minBy(results, 'commander').commander,
                chartRefresh: lodash_1.default.minBy(results, 'chartRefresh').chartRefresh,
                luaGarbageIncremental: lodash_1.default.minBy(results, 'luaGarbageIncremental').luaGarbageIncremental,
                chartUpdate: lodash_1.default.minBy(results, 'chartUpdate').chartUpdate,
                scriptUpdate: lodash_1.default.minBy(results, 'scriptUpdate').scriptUpdate
            }
        };
        // lastly, if  our trial is a 'SavedTrial' type, we will make sure we convert all these records into SavedFlowRecords
        if (trial instanceof Trial_1.Trial && (Dataset_1.GameFlowSystemRecord === null || Dataset_1.GameFlowSystemRecord === void 0 ? void 0 : Dataset_1.GameFlowSystemRecord.fromRecords)) {
            results = Dataset_1.GameFlowSystemRecord.fromRecords(results, trial.id);
        }
        trial.systemMetadata = metadata;
        return results;
    }
    static get modsPath() {
        return path_1.default.join(this.factoryDataPath, 'mods');
    }
    static get modsCachePath() {
        return path_1.default.join(this.factoryDataPath, 'mods-cache');
    }
    static get savesPath() {
        return path_1.default.join(this.factoryDataPath, 'saves');
    }
    static get scenariosPath() {
        return path_1.default.join(this.factoryDataPath, 'scenarios');
    }
    static get scriptOutputPath() {
        return path_1.default.join(this.factoryDataPath, 'script-output');
    }
    // in the future, add a parameter to this function to specify a version to verify we have, else redownload
    static verifyInstall(forceLatestVersion = false) {
        return __awaiter(this, void 0, void 0, function* () {
            // Phase 3 - link to the install path folder
            let p;
            Logging_1.Logging.log('info', { message: `Verifying factory install path at ${this.factoryInstallPath}` });
            // Validate our executable exists, regardless of platform.
            if (os.platform() === 'win32') {
                p = path_1.default.join(this.factoryInstallPath, 'bin', 'x64', 'factorio.exe');
            }
            else {
                p = path_1.default.join(this.factoryInstallPath, 'bin', 'x64', 'factorio');
            }
            if (!(yield fs_extra_1.default.pathExists(p))) {
                Logging_1.Logging.log('error', { message: `Cannot link factory install path - no executable found at ${p}` });
                throw new Error(`Cannot link factory install path - no executable found at ${p}`);
            }
            // read the info.json file to ensure we have a valid base game, and load the version
            const infoJson = yield fs_extra_1.default.readJson(path_1.default.join(this.factoryInstallPath, 'data', 'base', 'info.json'));
            Factory.factoryVersion = infoJson.version;
            Factory.factoryExecutable = true;
            Logging_1.Logging.log('info', { message: `Successfully verified factory install path at ${this.factoryInstallPath} - version ${Factory.factoryVersion}` });
            if (forceLatestVersion && (FactorioApi_1.FactorioApi.latestVersion.stable.headless !== Factory.factoryVersion)) {
                Logging_1.Logging.log('info', { message: `Version mismatch - latest version of Factorio is ${FactorioApi_1.FactorioApi.latestVersion.stable.headless}, but we have ${Factory.factoryVersion}.` });
                throw new Error(`Version mismatch - latest version of Factorio is ${FactorioApi_1.FactorioApi.latestVersion.stable.headless}, but we have ${Factory.factoryVersion}.`);
            }
            return infoJson;
        });
    }
    static setupLogger(hideConsole = false) {
        return __awaiter(this, void 0, void 0, function* () {
            // Phase 1 - start the logger and other random system packages
            const logFormat = winston_1.default.format.printf(function (info) {
                try {
                    return `${info.timestamp}-${info.level}: ${Object.keys(info).length <= 3 ? info.message : JSON.stringify(info, null, 4)}`;
                }
                catch (e) {
                    return "LOGERROR";
                }
            });
            if (hideConsole === true) {
                Logging_1.Logging.startLogger([
                    new winston_1.default.transports.File({
                        filename: path_1.default.join(process.cwd(), 'factory.log'),
                        level: 'info'
                    })
                ]);
            }
            else {
                Logging_1.Logging.startLogger([
                    new winston_1.default.transports.Console({
                        level: 'info',
                        format: winston_1.default.format.combine(winston_1.default.format.colorize(), logFormat)
                    }),
                    new winston_1.default.transports.File({
                        filename: path_1.default.join(process.cwd(), 'factory.log'),
                        level: 'info'
                    })
                ]);
            }
        });
    }
    static deleteScriptOutputFiles(trial) {
        return __awaiter(this, void 0, void 0, function* () {
            // deletes all raw files associated with this trial in the script-output directory
            let trialId = typeof trial === 'string' ? trial : trial.id;
            try {
                if (trial instanceof Trial_1.Trial)
                    trial.rawSystemText = undefined;
                yield Promise.allSettled([
                    fs_extra_1.default.rm(path_1.default.join(Factory.scriptOutputPath, 'data', `${trialId}_item.jsonl`)),
                    fs_extra_1.default.rm(path_1.default.join(Factory.scriptOutputPath, 'data', `${trialId}_elec.jsonl`)),
                    fs_extra_1.default.rm(path_1.default.join(Factory.scriptOutputPath, 'data', `${trialId}_circ.jsonl`)),
                    fs_extra_1.default.rm(path_1.default.join(Factory.scriptOutputPath, 'data', `${trialId}_poll.jsonl`))
                ]);
            }
            catch (e) {
                Logging_1.Logging.log('error', { message: `Failed to delete raw files for trial ${trialId}`, error: e });
            }
        });
    }
    static deleteSaveFileClutter(trial) {
        return __awaiter(this, void 0, void 0, function* () {
            // deletes all raw files associated with this trial in the script-output directory
            let trialId = typeof trial === 'string' ? trial : trial.id;
            try {
                yield Promise.allSettled([
                    fs_extra_1.default.remove(path_1.default.join(Factory.savesPath, `${trialId}.zip`)),
                    fs_extra_1.default.remove(path_1.default.join(Factory.savesPath, `${trialId}`)),
                    fs_extra_1.default.remove(path_1.default.join(Factory.factoryDataPath, 'scenarios', `${trialId}`)),
                ]);
            }
            catch (e) {
                Logging_1.Logging.log('error', { message: `Failed to delete save files for trial ${trialId}`, error: e });
            }
        });
    }
    static applyModsOfSource(source) {
        var _a;
        return __awaiter(this, void 0, void 0, function* () {
            /*
            * We assume that all mods listed exist in the mods-cache - if we try to symlink one and it doesn't exist, throw error
            * */
            // first off, clear mods!
            yield Factory.clearActiveMods();
            if (source.modList && source.modList.mods.length > 0) {
                // we now need to symlink all the mods first
                yield Factory.symlinkModFiles(source.modList.modFileNames);
                // if it exists, symlink the mod-settings.dat file
                // BUT WAIT!! We are changing this - just check if there's a <mod-list-id>.dat file in the mod-list folder
                // if there is, symlink it to the mod-settings.dat file
                if (((_a = source === null || source === void 0 ? void 0 : source.modList) === null || _a === void 0 ? void 0 : _a.id) && (yield fs_extra_1.default.exists(path_1.default.join(Factory.modsCachePath, `${source.modList.id}.dat`))))
                    yield fs_extra_1.default.symlink(path_1.default.join(Factory.modsCachePath, `${source.modList.id}.dat`), path_1.default.join(Factory.modsPath, 'mod-settings.dat'));
                // then, we need to write the mod-list.json file. Factorio uses this to load the actual list of mods
                yield source.modList.writeModListFile(path_1.default.join(Factory.modsPath, 'mod-list.json'));
            }
        });
    }
    static symlinkModFiles(modFiles = []) {
        return __awaiter(this, void 0, void 0, function* () {
            for (let i = 0; i < modFiles.length; i++) {
                if (!Factory.modCache.has(modFiles[i]))
                    throw new Error(`Cannot symlink mod ${modFiles[i]} - not present in mods-cache!`);
                yield fs_extra_1.default.symlink(path_1.default.join(Factory.modsCachePath, modFiles[i]), path_1.default.join(Factory.modsPath, modFiles[i]));
            }
        });
    }
    static refreshModCache() {
        return __awaiter(this, void 0, void 0, function* () {
            if (Factory.modCache)
                Factory.modCache.clear();
            // first off, read the entire list of files from our mods-cache folder
            let files = yield fs_extra_1.default.readdir(Factory.modsCachePath);
            // For each one, remove the '.zip' at the end and add to our cache
            for (let i = 0; i < files.length; i++) {
                if (files[i].endsWith('.zip'))
                    Factory.modCache.add(files[i].substring(0, files[i].length - 4));
                else
                    Factory.modCache.add(files[i]);
            }
        });
    }
    static stop(includeLogger = true) {
        return __awaiter(this, void 0, void 0, function* () {
            if (Factory.isTrialRunning)
                throw new Error(`Cannot stop factory while factorio executable is running!`);
            Logging_1.Logging.log('info', `Stopping factory - clearing all variables`);
            Factory.factoryDataPath = undefined;
            Factory.factoryInstallPath = undefined;
            Factory.factoryVersion = undefined;
            Factory.factoryExecutable = undefined;
            Factory.clientDownloadCount = 0;
            Factory.initStatus = 'not-started';
            // clear API vars
            FactorioApi_1.FactorioApi.clear();
            // stop the logger before exiting
            if (includeLogger === true)
                Logging_1.Logging.stopLogger();
        });
    }
    static installGame(version = undefined, build = undefined) {
        return __awaiter(this, void 0, void 0, function* () {
            // First, increment our download count here, so that even if errors occur we know that it was attempted
            Factory.clientDownloadCount++;
            const localOsDistro = os.platform();
            let localBuild = build;
            if (!localBuild)
                localBuild = process.env.FACTORIO_BUILD ? process.env.FACTORIO_BUILD : 'headless';
            // If this is WINDOWS, we need to ensure we download that distro (manual zip file). Otherwise, tar.xz is used
            if (localOsDistro === 'win32' && localBuild === 'headless')
                throw new Error('Factorio does not support windows for headless builds. Set environmental variable FACTORIO_BUILD to \'alpha\' to enable the full game download.');
            if (localOsDistro === 'win32' && !FactorioApi_1.FactorioApi.isAuthenticated)
                throw new Error('To download the Windows version of Factorio, you must be authenticated and own the game! Only linux headless can run without license');
            if (process.env.FACTORIO_BUILD === 'alpha' && !FactorioApi_1.FactorioApi.isAuthenticated)
                throw new Error('To download the Graphical version of Factorio, you must be authenticated and own the game! Only linux headless can run without license');
            // even if win32 is chosen for distro in the API, MAKE SURE win32-manual is used for download
            let downloadDistro;
            switch (localOsDistro) {
                case 'win32':
                    downloadDistro = 'win32-manual';
                    break;
                case 'freebsd':
                case 'openbsd':
                case 'linux':
                    downloadDistro = 'linux64';
                    break;
            }
            // Download game to the downloads folder
            Logging_1.Logging.log('info', `Downloading Factorio ${version ? version : FactorioApi_1.FactorioApi.latestVersion.stable[localBuild]} ${localBuild} ${downloadDistro} to ${Factory.factoryDataPath}/downloads`);
            let filepath = yield FactorioApi_1.FactorioApi.downloadGame(version ? version : FactorioApi_1.FactorioApi.latestVersion.stable[localBuild], localBuild, downloadDistro);
            if (Factory.clientDownloadCount > 1) {
                // this is our 2nd time around... try re-downloading first
                yield fs_extra_1.default.remove(filepath);
                filepath = yield FactorioApi_1.FactorioApi.downloadGame(version ? version : FactorioApi_1.FactorioApi.latestVersion.stable[localBuild], localBuild, downloadDistro);
            }
            // Delete needed folders in the install location
            Logging_1.Logging.log('info', `Deleting old install directories and files in ${Factory.factoryInstallPath}`);
            yield Promise.all([
                fs_extra_1.default.remove(path_1.default.join(Factory.factoryInstallPath, 'data')),
                fs_extra_1.default.remove(path_1.default.join(Factory.factoryInstallPath, 'bin')),
                fs_extra_1.default.remove(path_1.default.join(Factory.factoryInstallPath, 'doc-html')),
                fs_extra_1.default.remove(path_1.default.join(Factory.factoryInstallPath, 'temp')),
                fs_extra_1.default.remove(path_1.default.join(Factory.factoryInstallPath, 'config'))
            ]);
            // Remove the last files in the install folder
            let installFolderContents = yield fs_extra_1.default.readdir(Factory.factoryInstallPath, { withFileTypes: true });
            for (let i = 0; i < installFolderContents.length; i++) {
                if (installFolderContents[i].isFile()) {
                    yield fs_extra_1.default.remove(path_1.default.join(Factory.factoryInstallPath, installFolderContents[i].name));
                }
            }
            // Now, we can extract to this folder to 'install'. method is slightly different depending on OS
            Logging_1.Logging.log('info', `Extracting ${filepath} to ${Factory.factoryInstallPath} - This can take some time depending on build chosen to download.`);
            if (localOsDistro === 'win32') {
                yield (0, decompress_1.default)(filepath, Factory.factoryInstallPath);
            }
            else {
                // decompress using TAR
                yield new Promise((resolve, reject) => {
                    (0, child_process_1.exec)(`tar -xf ${filepath} -C ${Factory.factoryInstallPath} --strip-components=1`, (error, stdout, stderr) => {
                        if (error)
                            reject(error);
                        else
                            resolve(stdout);
                    });
                });
                /*await decompress(filepath, Factory.factoryInstallPath, {
                    plugins: [decompressTarxz()],
                    map: file => {
                        file.path = file.path.substring(9)
                        return file;
                    }
                });*/
            }
            yield Factory.verifyInstall();
        });
    }
    /*
    * Copies the 'scenario-source' that we have as a template to the <datadir>/scenarios folder, and renames it to the folder name specified
    * */
    static copyScenarioSourceToFolder(folder) {
        return __awaiter(this, void 0, void 0, function* () {
            const srcPath = path_1.default.join(__dirname, '../', '../', 'factory', 'scenarios', 'scenario-source');
            const srcPathTest2 = path_1.default.join(__dirname, '../', 'factory', 'scenarios', 'scenario-source');
            const destPath = path_1.default.join(Factory.factoryDataPath, 'scenarios', folder);
            Logging_1.Logging.log('info', { message: `Copying scenario-source from ${srcPath} to ${destPath}` });
            try {
                yield fs_extra_1.default.copy(srcPath, destPath, { overwrite: true });
                return destPath;
            }
            catch (e) {
                try {
                    Logging_1.Logging.log('info', { message: `Failed to copy scenario-source! Trying again from ${srcPathTest2} to ${destPath}` });
                    yield fs_extra_1.default.copy(srcPathTest2, destPath, { overwrite: true });
                    return destPath;
                }
                catch (e2) {
                    Logging_1.Logging.log('error', { message: `Failed to copy scenario-source! Second attempt, error`, error: e2 });
                    throw new Error('Failed to copy scenario-source - ' + (e2 === null || e2 === void 0 ? void 0 : e2.message) ? e2.message : e2);
                }
            }
        });
    }
    /*
    * Mod Management - from modlist through to individual mod downloads
    *
    * SO - REMOVE ANY KIND OF FILE REQUIREMENTS FOR MODS
    * just define mods by array - this is a npm package, not a CLI-first repository
    * Defining custom scenarios will stay the same, but is based entirely on the 'scenarios' folder of the install
    * Once this is done, I can do an update
    *
    * */
    /*
    * Clears the files mod-list.json and mod-settings.dat from the mods folder. Lets Factorio start over fresh with vanilla modlist
    * If mods are desired or a particular modlist is chosen, this is the first step to implementing that.
    * */
    static clearActiveMods() {
        return __awaiter(this, void 0, void 0, function* () {
            // delete the mod-list.json file (and the modsettings.dat) from mods folder - this allows us to start fresh, with no mods by default
            yield fs_extra_1.default.emptyDir(Factory.modsPath);
        });
    }
    // returns cached version of given mod
    static cacheMod(mod) {
        return __awaiter(this, void 0, void 0, function* () {
            let name = mod.substring(0, mod.lastIndexOf('_'));
            let v;
            if (mod.endsWith('latest.zip') || mod.endsWith('latest')) {
                // get most recent version first
                v = yield FactorioApi_1.FactorioApi.getLatestModVersion(mod.substring(0, mod.lastIndexOf('_')));
            }
            else {
                v = mod.substring(mod.lastIndexOf('_') + 1, mod.indexOf('.zip') == -1 ? undefined : mod.indexOf('.zip'));
            }
            // first, check if this mod is already cached
            if (Factory.modCache.has(`${name}_${v}.zip`))
                return v;
            yield FactorioApi_1.FactorioApi.downloadMod(name, v);
            Factory.modCache.add(`${name}_${v}.zip`);
            return v;
        });
    }
    /*
    * We download all mods into the 'mods-cache' folder on 'preparing' a trial. This allows us to then quickly and easily
    * move files to the 'mods' directory later on, just before the trial itself is run that requires them.
    * */
    static cacheModList(modList) {
        return __awaiter(this, void 0, void 0, function* () {
            // We need to make sure all listed mods are present in the mods-cache
            // if a version does not exist in the name, we will assume latest version and append it to the name
            for (let i = 0; i < modList.mods.length; i++) {
                let v = yield Factory.cacheMod(modList.mods[i]);
                if (modList.mods[i].endsWith('_latest'))
                    modList.mods[i] = modList.mods[i].substring(0, modList.mods[i].lastIndexOf('_latest')) + '_' + v;
            }
            return yield FactoryDatabase_1.FactoryDatabase.saveModList(modList);
            //await fs.ensureSymlink('','', 'file')
        });
    }
    // Lists all scenarios
    static listScenarios() {
        return __awaiter(this, void 0, void 0, function* () {
            return (yield fs_extra_1.default.readdir(path_1.default.join(Factory.factoryDataPath, 'scenarios')));
        });
    }
    // lists both save files and folders - identified by the '.zip' extension
    static listSaves() {
        return __awaiter(this, void 0, void 0, function* () {
            return yield fs_extra_1.default.readdir(path_1.default.join(Factory.savesPath));
        });
    }
    /*
    * Running factorio executable
    * */
    static get factorioExecutablePath() {
        return path_1.default.join(Factory.factoryInstallPath, 'bin', 'x64', 'factorio');
    }
    static runFactorio(params) {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.factorioRunning)
                throw new Error('Factorio is already running! Can only run one instance at a time');
            let execResults = '';
            this.factorioRunning = true;
            let start = new Date();
            try {
                yield new Promise((resolve, reject) => {
                    let s = (0, child_process_1.spawn)(Factory.factorioExecutablePath, params, {});
                    let er;
                    s.stdout.on('data', (data) => {
                        execResults += data;
                    });
                    s.on('close', (code) => {
                        // test has finished - proceed!
                        if (er)
                            reject(er);
                        else if (code === 1)
                            reject('Could not start executable - is the game currently running?');
                        else
                            resolve(code);
                    });
                    s.on('error', (err) => {
                        er = err;
                    });
                });
            }
            catch (e) {
                Logging_1.Logging.log('error', `Error running factorio executable - ${execResults}}`);
                throw new Error(`Error running factorio executable - ${e}`);
            }
            this.factorioRunning = false;
            let end = new Date();
            return {
                execResults,
                start,
                end
            };
        });
    }
    static writeKeyValuesToLuaFile(path, vars) {
        return __awaiter(this, void 0, void 0, function* () {
            let k = Object.keys(vars);
            let luaContent = yield fs_extra_1.default.readFile(path, 'utf8');
            for (let i = 0; i < k.length; i++) {
                luaContent = Factory._replaceLuaKeyValue(luaContent, k[i], vars[k[i]]);
            }
            // need to write back to the file path...
            yield fs_extra_1.default.writeFile(path, luaContent, 'utf8');
            return luaContent;
        });
    }
    static _replaceLuaKeyValue(luaContent, key, val) {
        // find <key> first
        // find </key> next
        // we want to remove allll lines (including and between), then instead insert...
        // --<key>
        // local key = val
        // --</key> -> to newline
        // this should be inserted wherever the first index was found for start
        let start = luaContent.indexOf(`--<${key}>`);
        let end = luaContent.indexOf('\n', luaContent.indexOf(`--</${key}>`));
        // If we can't find our key - just return the lua content as-is
        if (start === -1 || end === -1)
            return luaContent;
        // we now have our region to 'remove' - replace it with our new string created below
        let newVal;
        if (val == 'null' || val == null || val === undefined)
            newVal = `--<${key}>\nlocal ${key} = nil\n--</${key}>\n`;
        else if (typeof val === "number")
            newVal = `--<${key}>\nlocal ${key} = ${val}\n--</${key}>\n`;
        else
            newVal = `--<${key}>\nlocal ${key} = '${val}'\n--</${key}>\n`;
        // write in!
        return luaContent.replace(luaContent.substring(start, end + 1), newVal);
    }
}
exports.Factory = Factory;
Factory._initStatus = 'not-started';
Factory.isTrialRunning = false;
Factory.clientDownloadCount = 0;
Factory.modCache = new Set();
Factory.factorioRunning = false;
//# sourceMappingURL=Factory.js.map