(function(global, factory) {
    /*console.log(typeof exports === "object" && typeof module !== 'undefined');
    console.log(typeof define === 'function' && define.amd);
    console.log(this);*/
    typeof exports === "object" && typeof module !== 'undefined' ? factory(module.exports) : 
    typeof define === 'function' && define.amd ? define(['exports'], factory) : 
    (function(global) {
        const factory_returns = factory({});
        for(const exp in factory_returns) {
            if(!global[exp]) {
                global[exp] = factory_returns[exp];
                //delete factory_returns[exp];
            } else {
                console.error("thread.js - Name " + exp + " already present in global scope. NOT IMPORTED!");
                //delete factory_returns[exp];
            };
        };
    })(global);
})(this, function(exports) {


    class JsonRequest {
        JsonPostEncpsulatePayload(data) {
            return {
                jsonrpc: '2.0', 
                method: 'call', 
                params: data, 
                id: Math.floor(Math.random() * 1000 * 1000 * 1000),
            };
        };
        makeRequest(url, data=undefined) {
            let self = this;
            if(!data) {
                data = {};
            };
            let encapsulatedData = self.JsonPostEncpsulatePayload(data);
            let opts = {
                method: 'POST',
                mode: 'same-origin',
                headers: {
                    'Accept': 'application/json',
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify(encapsulatedData),
            }
            const FetchPromise = fetch(url, opts);
            FetchPromise.request_id = encapsulatedData.id;
            return FetchPromise;
        };
    };

    class ThreadClosedError extends Error {
        constructor(message) {
            super(message);
            this.name = "ThreadClosedError";
        };
    };

    class ThreadWorkingError extends Error {
        constructor(message) {
            super(message);
            this.name = "ThreadWorkingError";
        };
    };

    class ThreadAliveError extends Error {
        constructor(message) {
            super(message);
            this.name = "ThreadAliveError";
        };
    };
    /**
     * wrapper per i workers
     * threadName sarà il nome del worker
     */
    class Thread {
        #currentState = -1;
        #worker = undefined;
        /**
         * 
         * @param {string} threadName 
         * @param {object} params object o function returning an object
         * params = {
         *  source: url del file js da richiedere al server (obbligatorio),
         *  customEventToDispatch: object che descrive un evento custom {name: "nome dell' evento", params: {bubbles: true|false, cancelable: true|false}},
         *  customEventTarget: DOM Element, il target da cui sarà emesso l'evento di message posted da un thread
         *  onMessage: function(event) (obbligatorio), la funzione che sarà chiamata quando il worker nel thread posta un message di risposta. L'evento viene passato come argomento
         *  onError: function(event), funzione che viene chiamata in caso di errore del worker
         *  onMessageError: function(event), funzione che viene chiamata quando accade un errore all' interno del thread stesso
         * }
         */
        constructor(threadName, params, paramsArgs, paramsThis) {
            //console.log(" ==== costruttore Thread ==== ");
            //console.log(threadName);
            //console.log(params);
            let self = this;
            this.threadName = threadName;
            this.params = params;
            if(!params) {
                let msg = "No params passed to construct worker " + threadName;
                throw new Error(msg);
            } else if(!params.source) {
                let msg = "No source in params for worker " + threadName;
                throw new Error(msg);
            };
            // questa funzione è obbligatoria
            if(!this.params.onMessage) {
                let messg = "No valid onMessage parameter for worker " + threadName;
                throw new Error(messg);
            } else if(typeof this.params.onMessage !== "function") {
                let messg = "No valid onMessage parameter for worker " + threadName + ". Function expected, got " + typeof this.params.onMessage;
                throw new Error(messg);
            };
            if(typeof params === "function") {
                paramsArgs = paramsArgs ? paramsArgs : [];
                paramsThis = paramsThis ? paramsThis : window;
                params = params.apply(paramsThis, paramsArgs);
            };
            this.sourceUrl = this.params.source;
            this.customEventToDispatch = this.params && this.params.customEventToDispatch ? this.params.customEventToDispatch : undefined;
            this.customEventTarget = this.params && this.params.customEventTarget ? this.params.customEventTarget : document.body;
            this.messagePostedEvent = {name: "thread-message-posted", params:{bubbles: true, cancelable: true}};
            this.workerErrorEvent = {name: "thread-error", params:{bubbles: true, cancelable: true}};
            this.workerMessageErrorEvent = {name: "thread-message-error", params:{bubbles: true, cancelable: true}};
            this.workerKilledEvent = {name: "thread-killed", params:{bubbles: true, cancelable: true}};
            /**
             * set di oggetti, ognuno deve avere il metodo messageCallback che deve accettare l'evento come primo parametro e il thread come secondo
             */
            this.messageListeners = new Set();
            /**
             * stato corrente, 0 = worker impegnato, 1 = worker in attesa, -1 = worker killed, -2 = worker killedWithErrors
             */
            self.#setAwaiting();
            this.murdererError = undefined;
            /**
             * una coda di lavori da processare: (FIFO)
             */
            this.toProcessQueue = [];
            this.threadId = this.params.threadId ? this.params.threadId : this.generate_id();
            this.#worker = new Worker(this.sourceUrl);
            this.#worker.onmessage = this.onMessage.bind(this);
            this.#worker.onerror = this.onError.bind(this);
            this.#worker.onmessageerror = this.onMessageError.bind(this);
        };
        getRandomnumber(max) {
            return Math.floor(Math.random() * max);;
        }
        /**
         * 
         * @param {*} length lunghezza dell' id, default 25
         * @returns 
         */
        generate_id(length) {
            let self = this;
            const res = {
                0: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
                1: "1234567890",
            };
            if(!length) {
                length = 25;
            };
            let newId = "";
            for(let i = 0; i < length; i++) {
                let resChoice = self.getRandomnumber(2);
                let resource = res[resChoice];
                let char = resource[self.getRandomnumber(resource.length)];
                newId += char;
            };
            //console.log("NEW THREAD ID: " + newId);
            return newId;
        };
        onMessage(evnt) {
            //console.log("MESSAGGIO RICEVUTO DAL WORKER!");
            //console.log(evnt);
            let self = this;
            self.#setAwaiting();
            self.params.onMessage(evnt, self);
            if(self.customEventToDispatch) {
                let customEvent = new Event(self.customEventToDispatch.name, self.customEventToDispatch.params);
                customEvent.data = evnt.data;
                self.customEventTarget.dispatchEvent(customEvent);
            };
            let defaultEvent = new Event(self.messagePostedEvent.name, self.messagePostedEvent.params);
            defaultEvent.data = evnt.data; // replichiamo i dati postati dal worker
            document.body.dispatchEvent(defaultEvent);
            if(self.messageListeners.size > 0) {
                self.messageListeners.forEach((l) => {
                    if(typeof l === "function") {
                        l(evnt.data, self)
                    } else if(typeof l.messageCallback === "function") {
                        l.messageCallback(evnt.data, self);
                    };
                });
            };
            if(self.toProcessQueue.length > 0) {
                let newJob = self.toProcessQueue.splice(0, 1)[0];
                self.postMessage(newJob);
            };
        };
        onError(evnt) {
            let self = this;
            self.murdererError = evnt.data;
            if(self.params.onError) {
                self.params.onError(evnt, self);
            };
            let defaultErrorEvent = new Event(self.workerErrorEvent.name, self.workerErrorEvent.params);
            defaultErrorEvent.data = evnt.data; // replichiamo i dati postati dal worker
            defaultErrorEvent.lineno = evnt.lineno;
            defaultErrorEvent.message = evnt.message;
            defaultErrorEvent.filename = evnt.filename;
            document.body.dispatchEvent(defaultErrorEvent);
            if(self.messageListeners.size > 0) {
                self.messageListeners.forEach((l) => {
                    if(typeof l === "function") {
                        l(evnt, self)
                    } else if(typeof l.errorCallback === "function") {
                        l.errorCallback(evnt, self);
                    };
                });
            };
            self.kill(true);
        };
        /**
         * in questo caso è capitato un errore nel message, il worker può rimanere alive e può andare al lavoro successivo
         * informa i subscribers eventuali
         * @param {*} event 
         */
        onMessageError(evnt) {
            let self = this;
            self.#setAwaiting();
            if(self.params.onMessageError) {
                self.params.onMessageError(evnt, self);
            };
            let defaultMessageErrorEvent = new Event(self.workerMessageErrorEvent.name, self.workerMessageErrorEvent.params);
            defaultMessageErrorEvent.data = evnt.data; // replichiamo i dati postati dal worker
            document.body.dispatchEvent(defaultMessageErrorEvent);
            if(self.messageListeners.size > 0) {
                self.messageListeners.forEach((l) => {
                    if(typeof l === "function") {
                        l(evnt, self)
                    } else if(typeof l.messageErrorCallback === "function") {
                        l.messageErrorCallback(evnt, self);
                    };
                });
            };
            if(self.toProcessQueue.length > 0) {
                let newJob = self.toProcessQueue.splice(0, 1)[0];
                self.postMessage(newJob);
            };
        };
        /**
         * 
         * @param  {...object} listeners oggetti in successione che saranno considerati ascoltatori e che saranno informati alla postmessage da parte del thread
         * verrà chiamata la messageCallback (unico metodo obbligatorio al loro interno) con l'evento del message e l'istanza corrente del thread come parametri.
         * Nel caso debbano essere informati anche di un eventuale errore o messageError dovranno avere anche rispettivamente:
         * errorCallback function(event, istanzaThread)
         * messageErrorCallback function(event, istanzaThread)
         */
        subscribeMessageListener(...listeners) {
            //console.log("subscribeMessageListener");
            if(!this.isAlive()) {
                throw new ThreadClosedError("Cannot subscribe to a closed thread. Thread " + this.threadId + " is closed.");
            }
            listeners.forEach((list, id) => {
                if(!this.messageListeners.has(list)) {
                    this.messageListeners.add(list);
                };
            });
            //console.log(this);
        };
        /**
         * 
         * @param {*} listener 
         */
        unsubscribeMessageListener(listener) {
            /*console.log("unsubscribeMessageListener chiamata di " + this.threadName + " - " + this.threadId);
            console.log(listener);
            console.log(listener.name);
            console.log(this.messageListeners.has(listener));*/
            if(this.messageListeners.has(listener)) {
                if(typeof listener.unsubscribeCallback === "function") {
                    listener.unsubscribeCallback.apply(listener, self);
                }
                this.messageListeners.delete(listener);
            };
        };
        unsubscribeAll() {
            let self = this;
            /*console.log("unsubscribeAll chiamata di " + this.threadName + " - " + this.threadId);
            console.log(this.messageListeners);*/
            this.messageListeners.forEach((listener) => {
                //console.log("Trying to delete -> " + listener?.name);
                if(typeof listener.unsubscribeCallback === "function") {
                    listener.unsubscribeCallback.apply(listener, self);
                }
                self.messageListeners.delete(listener);
            });
        };
        /**
         * posta i dati specificati al thread se il thread è in attesa, altrimenti o lo mette in una coda FIFO
         * o caccia un errore nel caso il thread sia chiuso
         * @param {object} data 
         */
        postMessage(data) {
            let self = this;
            if(self.isAwaiting()) {
                self.#setWorking();
                self.#worker.postMessage(data);
            } else if(self.isWorking()) {
                self.toProcessQueue.push(data);
            } else if(self.isKilled()) {
                throw new Error("Cannot post messages to a closed thread. Thread " + self.threadName + " (" + self.threadId + ") is closed.");
            } else {
                throw new Error("Cannot post messages to a closed thread. Thread " + self.threadName + " (" + self.threadId + ") exited with errors.");
            }
        };
        isAlive() {
            return this.isWorking() || this.isAwaiting();
        }
        isWorking() {
            return this.#currentState === 0;
        };
        isAwaiting() {
            return this.#currentState === 1;
        };
        isKilled() {
            return this.#currentState === -1;
        };
        isErrorKilled() {
            return this.#currentState === -2;
        };
        #setWorking() {
            this.#currentState = 0;
        };
        #setAwaiting() {
            this.#currentState = 1;
        };
        #setKilled() {
            this.#currentState = -1;
        };
        #setErrorKilled() {
            this.#currentState = -2;
        };
        kill(errorKilled=false) {
            let self = this;
            if(self.isWorking()) {
                throw new ThreadWorkingError("Cannot kill a working Thread. Thread " + this.threadName + " (" + this.threadId + ") is working.");
            } else {
                if(this.#worker) {
                    this.#worker.terminate();
                    if(self.isAwaiting() && !errorKilled) {
                        self.#setKilled();
                    } else if(errorKilled) {
                        self.#setErrorKilled();
                    };
                    let workerKilledEvent = new Event(this.workerKilledEvent.name, this.workerKilledEvent.params);
                    workerKilledEvent.data = {
                        threadId: this.threadId,
                        threadName: this.threadName,
                    };
                    this.unsubscribeAll();
                    document.body.dispatchEvent(workerKilledEvent);
                } else {
                    if(self.isKilled()) {
                        throw new ThreadClosedError("Cannot kill closed thread. Thread " + this.threadName + " (" + this.threadId + ") is already closed.");
                    };
                    if(self.isErrorKilled()) {
                        throw new Error("Cannot kill closed thread. Thread " + this.threadName + " (" + this.threadId + ") exited with errors.");
                    };
                };
            };
        };
        /**
         * 
         */
        destroy() {
            if(this.#worker && (this.isAwaiting() || this.isWorking)) {
                throw new ThreadAliveError("Destroy on an alive Thread is forbidden. Kill this thread before ( " + this.threadName + " - " + this.threadId + ").")
            };
            for(let prop in this) {
                this[prop] = null;
            };
        };
    };

    /**
     * NON ANCORA FINITA!!!!
     */
    class TasksQueue {
        constructor() {
            this.tasks = [];
        };
    };
    /**
     * Una classe per gestire in modo comodo i thread
     */
    class ThreadManager {
        constructor() {
            this.threads = {}; // key=threadId, value=Thread
            this.boundedFunctions = {};
            this.listenForKilledThreads();
        };
        generateThread(threadName, params, paramsArgs, paramsThis) {
            let newThread = new Thread(threadName, params, paramsArgs, paramsThis);
            this.registerThread(newThread);
            return newThread;
        };
        /**
         * 
         * @param {Thread} thread 
         */
        registerThread(thread) {
            if(this.threads[thread.threadId]) {
                console.error("Thread " + thread.threadId + " already registered.");
                console.error("Generating new id");
                let newId = thread.generate_id();
                thread.threadId = newId;
                this.registerThread(thread);
            } else {
                this.threads[thread.threadId] = thread;
            };
        };
        unregisterThread(threadId) {
            if(threadId && this.threads[threadId]) {
                delete this.threads[threadId];
            };
        };
        manageThreadKilled(event) {
            let threadId = event.data.threadId;
            this.unregisterThread(threadId)
        };
        listenForKilledThreads() {
            if(!this.boundedFunctions.manageThreadKilled) {
                this.boundedFunctions.manageThreadKilled = this.manageThreadKilled.bind(this);
            }
            document.addEventListener("thread-killed", this.boundedFunctions.manageThreadKilled);
        };
        unlistenForKilledThreads() {
            document.removeEventListener("thread-killed", this.boundedFunctions.manageThreadKilled);
        };
        killAll() {
            for(const t in this.threads) {
                this.threads[t].kill();
            };
            //console.log(this);
            //this.threads = {};
        };
    };
    const currentScript = document.currentScript;
    let currentThreadManager = currentScript.getAttribute('intantiateThreadManager') ? new ThreadManager() : undefined;
    const ToExport = {
        Thread: Thread,
        ThreadManager: ThreadManager,
        JsonRequest: JsonRequest,
        //TasksQueue: TasksQueue, //not finished!!!
    };
    if(currentThreadManager) {
        ToExport.currentThreadManager = currentThreadManager;
    }
    /*exports = {...ToExport, ...exports};
    console.log(exports);*/
    exports.Thread = Thread;
    exports.ThreadManager = ThreadManager;
    exports.currentThreadManager = currentThreadManager;
    exports.JsonRequest = JsonRequest;
    //exports.TasksQueue = TasksQueue;
    return ToExport;
});