"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const operation_tree_1 = require("../utils/operation_tree");
const utils_1 = require("#sfvc_src/utils/utils");
const uuid_1 = require("uuid");
/**
 * Main app class. TODO: this should be broken down into smaller, more testable units.
 */
class SFVC {
    /**
     * Creates a new instance of the core program.
     * @param configurationService Instance of a ConfigurationService
     * @param jobQueue Instance of a JobQueue
     * @param filesystem Instance of a FileSystemService
     * @param logger Instance of a ILogger
     * @param puppeteer Instance of a PuppeteerService
     * @param plugin_logger Instance of a ILogger, specifically logs errors thrown by plugins.
     */
    constructor(configurationService, jobQueue, filesystem, logger, puppeteer, plugin_logger) {
        this.configurationService = configurationService;
        this.jobQueue = jobQueue;
        this.filesystem = filesystem;
        this.logger = logger;
        this.puppeteer = puppeteer;
        this.plugin_logger = plugin_logger;
        this.operator_name = "";
        this.operator = undefined;
        this.controllers = new Map;
    }
    async processTraits(operator, traitSet) {
        traitSet.add("id"); //ensure video id is always present
        // Builds an operation tree here using traits passed from controller and dependencies from operators and traverse it in-order
        // TODO: See page 40 of SFV architecture documentation on the tablet for more in-depth explanation, put this on the wiki!
        const operations = new operation_tree_1.OperationTree(traitSet, operator.operations);
        for (const warning of operations.getWarnings()) {
            this.logger.warn(warning);
        }
        const results = await this.evaluateOperations(operations);
        return results;
    }
    /**
     * Evaluates the given OperationTree
     * @param operation_tree The operation tree to evaluate.
     * @returns The results extracted from the OperationTree
     */
    async evaluateOperations(operation_tree) {
        const results = new Map();
        const operations = operation_tree.getOrderedOperations();
        for (let i = 0; i < operations.length; i++) {
            const operation = operations[i];
            try {
                await operation[1](results);
                await (0, utils_1.shortWait)();
            }
            catch (error) {
                this.logger.warn(`Failed to gather trait ${operation[0]}: ${error}`);
                // Skip the dependencies of the failed trait.
                // All traits are guaranteed to have a cleanup function in the output array (even if it is empty) see Line 148 of `operation_tree.ts`
                for (let j = i; j < operations.length; j++) {
                    if (operations[j][0] === operations[i][0]) {
                        i = j;
                        break;
                    }
                    else {
                        this.logger.warn(`Skipped ${operations[j][0]} because it depends on failed trait ${operations[i][0]}`);
                    }
                }
            }
        }
        return results;
    }
    /**
     * Listener method which is called on job initialization
     */
    async onJob(job) {
        this.logger.info(`Controllers: ${Array.from(this.controllers.values())} Operator: ${JSON.stringify(this.operator)} Events: ${job.request.events}`);
        try {
            const puppeteerInstance = await this.puppeteer.getInstance();
            //At this point the user agent will be initialized
            const capture_basename = await this.filesystem.writeCapture(job, { "client_data": await this.puppeteer.getUserAgent() });
            for (const controller of job.request.controllers) {
                if (controller.includes("\n")) {
                    throw new Error(`Controller ${controller} failed to load. Newlines are not permitted in controller names.`);
                }
                if (!this.controllers.has(controller)) {
                    let activate;
                    try {
                        const plugin = await this.filesystem.getPlugin(controller, "controller");
                        activate = plugin.activate;
                        if (!activate) {
                            throw new Error(`Controller ${controller} failed to load. Controller is undefined.`);
                        }
                    }
                    catch (error) {
                        throw new Error(`Controller ${controller} failed to load: ${error.message}`);
                    }
                    try {
                        if (activate) {
                            const ctx = {
                                configurationServer: this.configurationService,
                                capture_id: job.capture_id,
                                logger: this.plugin_logger
                            };
                            this.controllers.set(controller, await activate(ctx));
                        }
                    }
                    catch (error) {
                        throw new Error(`Error activating controller ${controller}:\n${error}`);
                    }
                }
            }
            //TODO: this was supposed to cache, but instead there's a lot of setup code in "activate" that must run each time a new job is started.
            //In order for the caching here to work, the setup code needs to move somewhere else.
            // if (this.operator_name != job.request.operator) {
            try {
                const context = {
                    configurationServer: this.configurationService,
                    capture_id: job.capture_id,
                    logger: this.plugin_logger
                };
                const plugin = (await this.filesystem.getPlugin(job.request.operator, "operator"));
                const activate = plugin.activate;
                this.operator = await activate(context, puppeteerInstance.page);
                this.operator_name = job.request.operator;
            }
            catch (error) {
                throw new Error(`Error activating operator ${job.request.operator}:\n${error}`);
            }
            // }
            if (!this.operator) {
                job.error = `Operator ${job.request.operator} failed to load.`;
                throw new Error("failed to get plugin operator");
            }
            let index = 0;
            // This is the main loop that processes the jobs in the job queue.
            while (!job.halted && (index < job.request.events || (job.request.events == 0))) {
                const traitSet = new Set();
                // If no operator was present, return early.
                if (!this.operator) {
                    return;
                }
                const requested_traits = new Map();
                for (const [name, controller] of this.controllers.entries()) {
                    try {
                        controller.getTraits().forEach(trait => {
                            traitSet.add(trait);
                            if (requested_traits.has(trait)) {
                                requested_traits.get(trait)?.push(name);
                            }
                            else {
                                requested_traits.set(trait, [name]);
                            }
                        });
                    }
                    catch (error) {
                        throw new Error(`Error on getTraits() in controller ${name}:\n${error}`);
                    }
                }
                this.logger.info(`Processing traits ${Array.from(traitSet.keys())}`);
                const results = await this.processTraits(this.operator, traitSet).catch(err => {
                    throw new Error(`Error in operator:\n${err}`);
                });
                const manifest = {
                    video_id: results.get("id")?.processed ?? "unknown",
                    gathered_at: new Date(),
                    gathered_by: this.operator_name,
                    requested_traits: Array.from(requested_traits, ([trait, requested_by]) => ({ trait, requested_by })),
                    event_id: (0, uuid_1.v7)()
                };
                await this.filesystem.writeEvent(capture_basename, results, manifest);
                //If you want the full trait set to persist then remove this. TODO: maybe this should be a config that controllers can expose?
                traitSet.clear();
                if (results) {
                    for (const controller of this.controllers.values()) {
                        if (controller.onTrait) {
                            for (const trait of results.keys()) {
                                const requestedTraits = await controller.onTrait(trait, results).catch(err => {
                                    throw new Error(`Error in controller on Trait '${trait}':\n${err}`);
                                }); //controllers should be able to add more traits to the TraitSet here.
                                if (requestedTraits) {
                                    for (const trait of requestedTraits) {
                                        traitSet.add(trait);
                                    }
                                }
                            }
                        }
                    }
                }
                if (traitSet.size > 0) {
                    //NOTE: if you want to react to content in an SFV more than once this will have to be some kind of loop.
                    //For now this just runs if any additional traits were requested after the main collection event.
                    const results = await this.processTraits(this.operator, traitSet).catch(err => {
                        throw new Error(`Error in operator:\n${err}`);
                    });
                    const manifest = {
                        video_id: results.get("id")?.processed ?? "unknown",
                        gathered_at: new Date(),
                        gathered_by: this.operator_name,
                        requested_traits: Array.from(requested_traits, ([trait, requested_by]) => ({ trait, requested_by })),
                        event_id: (0, uuid_1.v7)()
                    };
                    await this.filesystem.writeEvent(capture_basename, results, manifest);
                }
                await this.operator.scroll().catch(err => {
                    this.logger.error(`Error in operator scroll function:\n${err}`);
                });
                //Wait briefly after each scroll for content to render
                await (0, utils_1.shortWait)();
                index++;
            }
        }
        catch (error) {
            job.error = error.message;
            this.logger.error(`Error on job: \n${error.stack}`);
        }
        this.logger.info("Job is complete");
        await this.puppeteer.dispose();
    }
}
exports.default = SFVC;
//# sourceMappingURL=sfvc.js.map