"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.threadedClass = void 0;
const path = require("path");
const callsites = require("callsites");
const lib_1 = require("../shared/lib");
const sharedApi_1 = require("../shared/sharedApi");
const manager_1 = require("./manager");
/**
 * Returns an asynchronous version of the provided class
 * @param orgModule Path to imported module (this is what is in the require('XX') function, or import {class} from 'XX'} )
 * @param orgExport Name of export in module
 * @param constructorArgs An array of arguments to be fed into the class constructor
 */
function threadedClass(orgModule, orgExport, constructorArgs, configOrg = {}) {
    let exportName = orgExport;
    /** Used to  extrack the original stack */
    const errStack = new Error();
    if (typeof orgModule !== 'string')
        throw new Error('threadedClass parameter orgModule must be a string!');
    if (typeof orgExport !== 'string')
        throw new Error('threadedClass parameter orgExport must be a string!');
    const config = Object.assign(Object.assign({}, configOrg), { instanceName: configOrg.instanceName || orgExport // Default to the export class name
     });
    if ((0, lib_1.isBrowser)()) {
        if (!config.pathToWorker) {
            throw Error('config.pathToWorker is required in browser');
        }
        if (!(0, lib_1.browserSupportsWebWorkers)()) {
            console.log('Web-workers not supported, disabling multi-threading');
            config.disableMultithreading = true;
        }
    }
    let parentCallPath = callsites()[1].getFileName();
    let thisCallPath = callsites()[0].getFileName();
    return new Promise((resolve, reject) => {
        function sendFcn(instance, fcn, args, cb) {
            let msg = {
                cmd: sharedApi_1.Message.To.Instance.CommandType.FUNCTION,
                fcn: fcn,
                args: args
            };
            manager_1.ThreadedClassManagerInternal.sendMessageToInstance(instance, msg, cb);
        }
        function sendSet(instance, property, value, cb) {
            let msg = {
                cmd: sharedApi_1.Message.To.Instance.CommandType.SET,
                property: property,
                value: value
            };
            manager_1.ThreadedClassManagerInternal.sendMessageToInstance(instance, msg, cb);
        }
        function sendReplyToInstance(instance, replyTo, error, reply, cb) {
            let msg = {
                cmd: sharedApi_1.Message.To.Instance.CommandType.REPLY,
                replyTo: replyTo,
                reply: reply,
                error: error ? (error.stack || error).toString() : error
            };
            manager_1.ThreadedClassManagerInternal.sendMessageToInstance(instance, msg, cb);
        }
        function replyError(instance, msg, error) {
            sendReplyToInstance(instance, msg.cmdId, error);
        }
        function sendCallback(instance, callbackId, args, cb) {
            let msg = {
                cmd: sharedApi_1.Message.To.Instance.CommandType.CALLBACK,
                callbackId: callbackId,
                args: args
            };
            manager_1.ThreadedClassManagerInternal.sendMessageToInstance(instance, msg, cb);
        }
        function decodeResultFromWorker(instance, encodedResult) {
            return (0, sharedApi_1.decodeArguments)(() => instance.proxy, [encodedResult], (a) => {
                return (...args) => {
                    return new Promise((resolve, reject) => {
                        // Function result function is called from parent
                        sendCallback(instance, a.value, args, (_instance, err, encodedResult) => {
                            // Function result is returned from worker
                            if (err) {
                                reject(err);
                            }
                            else {
                                let result = decodeResultFromWorker(_instance, encodedResult);
                                resolve(result);
                            }
                        });
                    });
                };
            })[0];
        }
        function onMessageFromInstance(instance, m) {
            if (m.cmd === sharedApi_1.Message.From.Instance.CommandType.REPLY) {
                let msg = m;
                const child = instance.child;
                let cb = child.instanceMessageQueue[msg.replyTo + ''];
                if (!cb)
                    return;
                if (msg.error) {
                    cb(instance, msg.error);
                }
                else {
                    cb(instance, null, msg.reply);
                }
                delete child.instanceMessageQueue[msg.replyTo + ''];
            }
            else if (m.cmd === sharedApi_1.Message.From.Instance.CommandType.CALLBACK) {
                // Callback function is called by worker
                let msg = m;
                let callback = instance.child.callbacks[msg.callbackId];
                if (callback) {
                    try {
                        Promise.resolve(callback(...msg.args))
                            .then((result) => {
                            let encodedResult = (0, sharedApi_1.encodeArguments)(instance, instance.child.callbacks, [result], !!config.disableMultithreading);
                            sendReplyToInstance(instance, msg.cmdId, undefined, encodedResult[0]);
                        })
                            .catch((err) => {
                            try {
                                replyError(instance, msg, err);
                            }
                            catch (e) {
                                manager_1.ThreadedClassManagerInternal.debugLogError(new Error(`ThreadedClass: Thrown error in callback "${msg.callbackId}": Unable to forward error to child due to: ${e}`));
                            }
                        });
                    }
                    catch (err) {
                        try {
                            replyError(instance, msg, err);
                        }
                        catch (e) {
                            manager_1.ThreadedClassManagerInternal.debugLogError(new Error(`ThreadedClass: Thrown error in callback "${msg.callbackId}": Unable to forward error to child due to: ${e}`));
                        }
                    }
                }
                else
                    throw Error(`callback "${msg.callbackId}" not found in instance ${m.instanceId}`);
            }
        }
        try {
            let pathToModule = '';
            let pathToWorker = '';
            if ((0, lib_1.isBrowser)()) {
                pathToWorker = config.pathToWorker;
                pathToModule = orgModule;
            }
            else {
                if (!parentCallPath)
                    throw new Error('Unable to resolve parent file path');
                if (!thisCallPath)
                    throw new Error('Unable to resolve own file path');
                let absPathToModule = (orgModule.match(/^\./) ?
                    path.resolve(parentCallPath, '../', orgModule) :
                    orgModule);
                pathToModule = require.resolve(absPathToModule);
                pathToWorker = thisCallPath
                    .replace(/parent-process/, 'child-process')
                    .replace(/threadedClass(\.[tj]s)$/, 'threadedclass-worker.js')
                    .replace(/src([\\\/])child-process([\\\/])threadedclass-worker/, 'dist$1child-process$2threadedclass-worker');
            }
            const child = manager_1.ThreadedClassManagerInternal.findNextAvailableChild(config, pathToWorker);
            const proxy = {};
            let instanceInChild = manager_1.ThreadedClassManagerInternal.attachInstanceToChild(config, child, proxy, pathToModule, exportName, constructorArgs, onMessageFromInstance);
            manager_1.ThreadedClassManagerInternal.sendInit(child, instanceInChild, config, (instance, err, props) => {
                // This callback is called from the child process, with a list of supported properties of the instance
                if (err) {
                    reject(err);
                    return false;
                }
                else {
                    props.forEach((p) => {
                        if (!instance.child.alive)
                            throw Error(`Child process of instance ${instance.id} has been closed`);
                        if (proxy.hasOwnProperty(p.key)) {
                            return;
                        }
                        if (p.type === sharedApi_1.InitPropType.FUNCTION) {
                            const callMethod = (...args) => {
                                // An instance method is called by parent
                                const originalError = new Error();
                                if (!instance.child)
                                    return Promise.reject(new Error(`Instance ${instance.id} has been detached from child process`));
                                return manager_1.ThreadedClassManagerInternal.doMethod(instance.child, p.key, (resolve, reject) => {
                                    if (!instance.child)
                                        throw new Error(`Instance ${instance.id} has been detached from child process`);
                                    // Go through arguments and serialize them:
                                    let encodedArgs = (0, sharedApi_1.encodeArguments)(instance, instance.child.callbacks, args, !!config.disableMultithreading);
                                    sendFcn(instance, p.key, encodedArgs, (_instance, err, encodedResult) => {
                                        // Function result is returned from child instance
                                        if (err) {
                                            err = (0, lib_1.combineErrorStacks)(err, 'Original stack (on parent):', originalError.stack || '');
                                            reject(err);
                                        }
                                        else {
                                            let result = decodeResultFromWorker(_instance, encodedResult);
                                            resolve(result);
                                        }
                                    });
                                });
                            };
                            // @ts-ignore
                            proxy[p.key] = callMethod;
                        }
                        else if (p.type === sharedApi_1.InitPropType.VALUE) {
                            let m = {
                                configurable: false,
                                enumerable: p.descriptor.enumerable
                                // writable: // We handle everything through getters & setters instead
                            };
                            if (p.descriptor.get ||
                                p.descriptor.readable) {
                                m.get = function () {
                                    return new Promise((resolve, reject) => {
                                        sendFcn(instance, p.key, [], (_instance, err, encodedResult) => {
                                            if (err) {
                                                reject(err);
                                            }
                                            else {
                                                let result = decodeResultFromWorker(_instance, encodedResult);
                                                resolve(result);
                                            }
                                        });
                                    });
                                };
                            }
                            if (p.descriptor.set ||
                                p.descriptor.writable) {
                                m.set = function (newVal) {
                                    let fixedArgs = (0, sharedApi_1.encodeArguments)(instance, instance.child.callbacks, [newVal], !!config.disableMultithreading);
                                    // in the strictest of worlds, we should block the main thread here,
                                    // until the remote acknowledges the write.
                                    // Instead we're going to pretend that everything is okay. *whistling*
                                    sendSet(instance, p.key, fixedArgs[0], (_instance, err, _result) => {
                                        if (err) {
                                            console.log('Error in setter', err);
                                            proxy.__uncaughtError = err;
                                        }
                                    });
                                };
                            }
                            Object.defineProperty(proxy, p.key, m);
                        }
                    });
                    manager_1.ThreadedClassManagerInternal.startMonitoringChild(instanceInChild);
                    resolve(proxy);
                    manager_1.ThreadedClassManagerInternal.checkInstance(instanceInChild, errStack);
                    return true;
                }
            });
        }
        catch (e) {
            reject(e);
        }
    });
}
exports.threadedClass = threadedClass;
//# sourceMappingURL=threadedClass.js.map