const isBrowser = (typeof window !== 'undefined');

const utils = {
    sleep: (ms) => { return new Promise(resolve => setTimeout(resolve, ms)); }
};

const cleanPeriod = 5*1000;//5 секунд

class WebSocketConnection {
    //messageLifeTime в секундах (проверка каждый cleanPeriod интервал)
    constructor(url, openTimeoutSecs = 10, messageLifeTimeSecs = 30) {
        this.WebSocket = (isBrowser ? WebSocket : require('ws'));
        this.url = url;
        this.ws = null;
        this.listeners = [];
        this.messageQueue = [];
        this.messageLifeTime = messageLifeTimeSecs*1000;
        this.openTimeout = openTimeoutSecs*1000;
        this.requestId = 0;

        this.wsErrored = false;
        this.closed = false;

        this.connecting = false;
        this.periodicClean();//no await
    }

    //рассылаем сообщение и удаляем те обработчики, которые его получили
    emit(mes, isError) {
        const len = this.listeners.length;
        if (len > 0) {
            let newListeners = [];
            for (const listener of this.listeners) {
                let emitted = false;
                if (isError) {
                    listener.onError(mes);
                    emitted = true;
                } else {
                    if ( (listener.requestId && mes.requestId && listener.requestId === mes.requestId) ||
                        (!listener.requestId && !mes.requestId) ) {
                        listener.onMessage(mes);
                        emitted = true;
                    }
                }

                if (!emitted)
                    newListeners.push(listener);
            }
            this.listeners = newListeners;
        }
        
        return this.listeners.length != len;
    }

    get isOpen() {
        return (this.ws && this.ws.readyState == this.WebSocket.OPEN);
    }

    processMessageQueue() {
        let newMessageQueue = [];
        for (const message of this.messageQueue) {
            if (!this.emit(message.mes)) {
                newMessageQueue.push(message);
            }
        }

        this.messageQueue = newMessageQueue;
    }

    _open() {
        return new Promise((resolve, reject) => { (async() => {
            if (this.closed)
                reject(new Error('Этот экземпляр класса уничтожен. Пожалуйста, создайте новый.'));

            if (this.connecting) {
                let i = this.openTimeout/100;
                while (i-- > 0 && this.connecting) {
                    await utils.sleep(100);
                }
            }

            //проверим подключение, и если нет, то подключимся заново
            if (this.isOpen) {
                resolve(this.ws);
            } else {
                this.connecting = true;
                this.terminate();

                if (isBrowser) {
                    const protocol = (window.location.protocol == 'https:' ? 'wss:' : 'ws:');
                    const url = this.url || `${protocol}//${window.location.host}/ws`;
                    this.ws = new this.WebSocket(url);
                } else {
                    this.ws = new this.WebSocket(this.url);
                }

                const onopen = (e) => {
                    this.connecting = false;
                    resolve(this.ws);
                };

                const onmessage = (data) => {
                    try {
                        if (isBrowser)
                            data = data.data;
                        const mes = JSON.parse(data);
                        this.messageQueue.push({regTime: Date.now(), mes});

                        this.processMessageQueue();
                    } catch (e) {
                        this.emit(e.message, true);
                    }
                };

                const onerror = (e) => {
                    this.emit(e.message, true);
                    reject(new Error(e.message));
                };

                const onclose = (e) => {
                    this.emit(e.message, true);
                    reject(new Error(e.message));
                };

                if (isBrowser) {
                    this.ws.onopen = onopen;
                    this.ws.onmessage = onmessage;
                    this.ws.onerror = onerror;
                    this.ws.onclose = onclose;
                } else {
                    this.ws.on('open', onopen);
                    this.ws.on('message', onmessage);
                    this.ws.on('error', onerror);
                    this.ws.on('close', onclose);
                }

                await utils.sleep(this.openTimeout);
                reject(new Error('Соединение не удалось'));
            }
        })() });
    }

    //timeout в секундах (проверка каждый cleanPeriod интервал)
    message(requestId, timeoutSecs = 4) {
        return new Promise((resolve, reject) => {
            this.listeners.push({
                regTime: Date.now(),
                requestId,
                timeout: timeoutSecs*1000,
                onMessage: (mes) => {
                    resolve(mes);
                },
                onError: (mes) => {
                    reject(new Error(mes));
                }
            });
            
            this.processMessageQueue();
        });
    }

    async send(req, timeoutSecs = 4) {
        await this._open();
        if (this.isOpen) {
            this.requestId = (this.requestId < 1000000 ? this.requestId + 1 : 1);
            const requestId = this.requestId;//реентерабельность!!!

            this.ws.send(JSON.stringify(Object.assign({requestId, _rpo: 1}, req)));//_rpo: 1 - ждем в ответ _rok: 1

            let resp = {};
            try {
                resp = await this.message(requestId, timeoutSecs);
            } catch(e) {
                this.terminate();
                throw new Error('WebSocket не отвечает');
            }

            if (resp._rok) {                
                return requestId;
            } else {
                throw new Error('Запрос не принят сервером');
            }
        } else {
            throw new Error('WebSocket коннект закрыт');
        }
    }

    terminate() {
        if (this.ws) {
            if (isBrowser) {
                this.ws.close();
            } else {
                this.ws.terminate();
            }
        }
        this.ws = null;
    }

    close() {
        this.terminate();
        this.closed = true;
    }

    async periodicClean() {
        while (!this.closed) {
            try {
                const now = Date.now();
                //чистка listeners
                let newListeners = [];
                for (const listener of this.listeners) {
                    if (now - listener.regTime < listener.timeout) {
                        newListeners.push(listener);
                    } else {
                        if (listener.onError)
                            listener.onError('Время ожидания ответа истекло');
                    }
                }
                this.listeners = newListeners;

                //чистка messageQueue
                let newMessageQueue = [];
                for (const message of this.messageQueue) {
                    if (now - message.regTime < this.messageLifeTime) {
                        newMessageQueue.push(message);
                    }
                }
                this.messageQueue = newMessageQueue;
            } catch(e) {
                //
            }

            await utils.sleep(cleanPeriod);
        }
    }
}

module.exports = WebSocketConnection;