#include uniqstr.js

const WorkerSkeletonObject = function(name, defineObjectMethod, deleteObjectMethod, deleteObject) {
    if(typeof name !== 'string')
        throw 'name must be a string';
    if(typeof defineObjectMethod !== 'function')
        throw 'defineObjectMethod must be a function';
    if(typeof deleteObjectMethod !== 'function')
        throw 'deleteObjectMethod must be a function';
    if(typeof deleteObject !== 'function')
        throw 'deleteObject must be a function';

    return {
        getObjectName: () => name,
        defineMethod: (...args) => defineObjectMethod(name, ...args),
        deleteMethod: (...args) => deleteObjectMethod(name, ...args),
        destroy: () => deleteObject(name),
    };
};

const WorkerSkeleton = function(globalScope) {
    if(globalScope === undefined)
        globalScope = self;

    const objects = new Map;
    const handlers = {};
    let initialised = false;

    const sendPayload = (type, detail) => {
        globalScope.postMessage({ type: type, detail: detail });
    };

    const sendObjectDefinePayload = (objName, objBody, eventPrefix) => {
        sendPayload('objdef', { object: objName, methods: Object.keys(objBody), eventPrefix: eventPrefix });
    };

    const defineObject = (objName, objBody, eventPrefix) => {
        if(typeof objName !== 'string')
            throw 'objName must be a string';
        if(typeof eventPrefix !== 'string' && eventPrefix !== undefined)
            throw 'eventPrefix must be string or undefined';
        if(objects.has(objName))
            throw 'objName is already defined';

        const object = {};
        for(const name in objBody) {
            const item = objBody[name];
            if(typeof item === 'function')
                object[name] = { body: item };
        }

        objects.set(objName, object);
        if(initialised)
            sendObjectDefinePayload(objName, object, eventPrefix);
    };

    const deleteObject = objName => {
        if(typeof objName !== 'string')
            throw 'objName must be a string';
        if(!objects.has(objName))
            throw 'objName is not defined';

        objects.delete(objName);
        if(initialised)
            sendPayload('objdel', { object: objName });
    };

    const defineObjectMethod = (objName, metName, metBody, returnsObject) => {
        if(typeof objName !== 'string')
            throw 'objName must be a string';
        if(typeof metName !== 'string')
            throw 'metName must be a string';
        if(typeof metBody !== 'function')
            throw 'metBody must be a function';

        const objBody = objects.get(objName);
        if(objBody === undefined)
            throw 'objName has not been defined';

        objBody[metName] = { body: metBody, returnsObject: returnsObject === true };
        if(initialised)
            sendPayload('metdef', { object: objName, method: metName });
    };

    const deleteObjectMethod = (objName, metName) => {
        if(typeof objName !== 'string')
            throw 'objName must be a string';
        if(typeof metName !== 'string')
            throw 'metName must be a string';

        const objBody = objects.get(objName);
        if(objBody === undefined)
            throw 'objName has not been defined';

        delete objBody[objName];
        if(initialised)
            sendPayload('metdel', { object: objName, method: metName });
    };

    const createDispatcher = prefix => {
        if(prefix === undefined)
            prefix = '';
        else if(typeof prefix !== 'string')
            throw 'prefix must be a string or undefined';

        if(prefix !== '' && !prefix.endsWith(':'))
            prefix += ':';

        return (name, detail) => sendPayload('evtdisp', { name: prefix + name, detail: detail });
    };

    defineObject('', {});

    const defineHandler = (name, handler) => {
        if(typeof name !== 'string')
            throw 'name must be a string';
        if(typeof handler !== 'function')
            throw 'handler must be a function';
        if(name in handlers)
            throw 'name is already defined';

        handlers[name] = handler;
    };

    defineHandler('init', () => {
        if(initialised)
            return;
        initialised = true;

        sendObjectDefinePayload('', objects.get(''));
    });

    defineHandler('metcall', req => {
        if(typeof req.id !== 'string')
            throw 'call id is not a string';

        const respond = (id, success, result, mightBeObject) => {
            let isObject = false;
            if(mightBeObject) {
                const resultType = typeof result;
                if(resultType === 'string' && objects.has(result))
                    isObject = true;
                else if(resultType === 'object' && result !== null && typeof result.getObjectName === 'function') {
                    const objectName = result.getObjectName();
                    if(objects.has(objectName)) {
                        isObject = true;
                        result = objectName;
                    }
                }
            }

            sendPayload('funcret', {
                id: id,
                success: success,
                result: result,
                object: isObject,
            });
        };

        try {
            if(typeof req.object !== 'string')
                throw 'object name is not a string';
            if(typeof req.method !== 'string')
                throw 'method name is not a string';

            const object = objects.get(req.object);
            if(object === undefined)
                throw 'object is not defined';
            if(!(req.method in object))
                throw 'method is not defined in object';

            const args = Array.isArray(req.args) ? req.args : [];
            const info = object[req.method];
            let result = info.body(...args);

            if(result instanceof Promise) {
                result.then(result => respond(req.id, true, result, info.returnsObject)).catch(ex => respond(req.id, false, ex));
            } else
                respond(req.id, true, result, info.returnsObject);
        } catch(ex) {
            respond(req.id, false, ex);
        }
    });

    globalScope.addEventListener('message', ev => {
        if(typeof ev.data === 'string') {
            if(ev.data.startsWith('ping:')) {
                globalScope.postMessage(`pong:${ev.data.substring(5)}`);
                return;
            }
        }

        if(typeof ev.data === 'object' && ev.data !== null && typeof ev.data.type === 'string') {
            if(ev.data.type in handlers)
                handlers[ev.data.type](ev.data.detail);
            return;
        }
    });

    return {
        sendPayload: sendPayload,
        createDispatcher: createDispatcher,
        defineObject: (object, eventPrefix) => {
            if(typeof object !== 'object' || object === null)
                return undefined;

            const name = MamiUniqueStr(8);
            defineObject(name, object, eventPrefix);
            return new WorkerSkeletonObject(name, defineObjectMethod, deleteObjectMethod, deleteObject);
        },
        defineMethod: (...args) => defineObjectMethod('', ...args),
        deleteMethod: (...args) => deleteObjectMethod('', ...args),
    };
};
