var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
import { v4 as uuidv4 } from 'uuid';
import logger from './util/logger';
import Connection from './services/Connection';
import { deRegister, register, trigger, deRegisterAll } from './services/Handler';
import { SwEvent, XcallMethod, SESSION_ID } from './util/constants';
import { NOTIFICATION_TYPE } from './base/constants';
import { isFunction, randomInt } from './util/helpers';
import { Ping, Connect } from './base/Uas';
const KEEPALIVE_INTERVAL = 10 * 1000;
const MAX_FAILED_PING = 2;
export default class BaseSession {
    constructor(options) {
        this.options = options;
        this.uuid = uuidv4();
        this.sessionid = '';
        this.subscriptions = {};
        this.expiresAt = 0;
        this.signature = null;
        this.relayProtocol = null;
        this.contexts = [];
        this.timeoutErrorCode = -32000;
        this.connection = null;
        this._jwtAuth = false;
        this._doKeepAlive = false;
        this._autoReconnect = true;
        this._idle = false;
        this.Ping = null;
        this._executeQueue = [];
        this._pong = null;
        this._failCount = 0;
        if (!this.validateOptions()) {
            throw new Error('Invalid init options');
        }
        this._onSocketOpen = this._onSocketOpen.bind(this);
        this._onSocketCloseOrError = this._onSocketCloseOrError.bind(this);
        this._onSocketMessage = this._onSocketMessage.bind(this);
        this._handleLoginError = this._handleLoginError.bind(this);
        this._checkTokenExpiration = this._checkTokenExpiration.bind(this);
        this._attachListeners();
        this.connection = new Connection(this);
    }
    get __logger() {
        return logger;
    }
    get connected() {
        return this.connection && this.connection.connected;
    }
    get expired() {
        return this.expiresAt && this.expiresAt <= (Date.now() / 1000);
    }
    get reconnectDelay() {
        return randomInt(6, 2) * 1000;
    }
    execute(msg) {
        if (this._idle) {
            return new Promise(resolve => this.filterExecuteQueue({ resolve, msg }));
        }
        if (!this.connected) {
            return new Promise(resolve => {
                this.filterExecuteQueue({ resolve, msg });
                this.connect();
            });
        }
        return this.connection.send(msg)
            .catch(error => {
            if (error.code && error.code === this.timeoutErrorCode) {
                this._failCount++;
                logger.error((new Date()) + ' websocket Uas.Ping error, failCount=' + this._failCount);
                if (this._failCount >= MAX_FAILED_PING) {
                    this._closeConnection();
                }
            }
            throw error;
        });
    }
    filterExecuteQueue(v) {
        if (['Uas.Unsubscribe', 'Uas.Subscribe'].includes(v.msg['method'])) {
            return;
        }
        try {
            let tempData = undefined;
            if (typeof (v.msg) == 'string') {
                tempData = this._executeQueue.find((item) => item.msg == v.msg);
            }
            else {
                tempData = this._executeQueue.find((item) => item.msg['method'] == v.msg['method']);
            }
            if (tempData) {
                this._executeQueue = this._executeQueue.map((item) => {
                    if (typeof (v.msg) == 'string' && item.msg == v.msg) {
                        return v;
                    }
                    if (item.msg['method'] == v.msg['method']) {
                        return v;
                    }
                    return item;
                });
            }
            else {
                this._executeQueue.push(v);
            }
        }
        catch (error) {
        }
    }
    executeRaw(text) {
        if (this._idle) {
            this.filterExecuteQueue({ msg: text });
            return;
        }
        this.connection.sendRawText(text);
    }
    validateOptions() {
        const { host = false } = this.options;
        return Boolean(host);
    }
    broadcast(params) { }
    subscribe(params) { }
    unsubscribe({ channels: eventChannel = [] }) { }
    disconnect() {
        return __awaiter(this, void 0, void 0, function* () {
            clearTimeout(this._reconnectTimeout);
            this.subscriptions = {};
            this._autoReconnect = false;
            this.relayProtocol = null;
            this._closeConnection();
            yield sessionStorage.removeItem(this.signature);
            this._executeQueue = [];
            this._detachListeners();
        });
    }
    on(eventName, callback) {
        register(eventName, callback, this.uuid);
        return this;
    }
    off(eventName, callback) {
        deRegister(eventName, callback, this.uuid);
        return this;
    }
    _refreshToken() {
        return __awaiter(this, void 0, void 0, function* () {
            const token = sessionStorage.getItem('XCALL_TOKEN');
            if (token == "") {
                return;
            }
            yield this.execute(new Connect(token, this.sessionid)).then((response) => {
                if (response) {
                    const { code, message } = response;
                    if (code == 200) {
                        this.Ping = new Ping();
                        this._autoReconnect = true;
                        this._doKeepAlive = true;
                        this._failCount = 0;
                        this.sessionid = response.sessionid;
                        sessionStorage.setItem(SESSION_ID, this.sessionid);
                        this._jwtAuth = true;
                    }
                    else {
                        logger.debug("automatic authentication failed:", message);
                        this._jwtAuth = false;
                        sessionStorage.setItem('XCALL_TOKEN', "");
                    }
                }
            }).catch((error) => {
                logger.debug("automatic authentication failed:", error);
                sessionStorage.setItem('XCALL_TOKEN', "");
                this._handleLoginError(error);
            });
        });
    }
    connect() {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.connection === null) {
                this.connection = new Connection(this);
            }
            this._attachListeners();
            if (this.connection.isDead) {
                this.connection.connect();
            }
        });
    }
    _handleLoginError(error) {
        trigger(SwEvent.Error, error, this.uuid);
    }
    _onSocketOpen(e) {
        return __awaiter(this, void 0, void 0, function* () {
            logger.info((new Date()) + ' websocket open:', e);
            this._idle = false;
            this._emptyExecuteQueues();
            this._pong = null;
            this._failCount = 0;
            yield this._refreshToken();
            this._keepAlive();
            trigger(SwEvent.Ready, this, this.uuid);
            logger.info('Session Ready!');
        });
    }
    _onSocketCloseOrError(event) {
        logger.error((new Date()) + ' websocket closed:', event);
        if (this.relayProtocol) {
            deRegisterAll(this.relayProtocol);
        }
        for (const sub in this.subscriptions) {
            deRegisterAll(sub);
        }
        this.subscriptions = {};
        this.contexts = [];
        if (this.expired) {
            this._idle = true;
            this._autoReconnect = false;
            this.expiresAt = 0;
            this._failCount = 0;
        }
        if (this._keepAliveTimeout) {
            clearTimeout(this._keepAliveTimeout);
        }
        if (this._autoReconnect) {
            this._reconnectTimeout = setTimeout(() => this.connect(), this.reconnectDelay);
        }
        trigger(SwEvent.Error, this, this.uuid);
    }
    _onSocketMessage(msg) {
        logger.info(new Date() + 'websocket message: ---->', msg);
        const { method, params } = msg;
        switch (method) {
            case XcallMethod.Broadcast:
                const session = this;
                const { topic } = params;
                if (!topic) {
                    logger.error('received an unknown event:', params);
                    return;
                }
                const protocol = this.relayProtocol;
                const eventChannel = topic;
                if (this._existsSubscription(protocol, eventChannel)) {
                    trigger(protocol, params, eventChannel);
                }
                else {
                    trigger(SwEvent.Notification, params, session.uuid);
                }
                params.type = NOTIFICATION_TYPE.generic;
                trigger(SwEvent.Notification, params, this.uuid);
                break;
            case XcallMethod.Disconnect:
                this._idle = true;
                break;
        }
    }
    _removeSubscription(protocol, channel) {
        if (!this._existsSubscription(protocol, channel)) {
            return;
        }
        if (channel) {
            delete this.subscriptions[protocol][channel];
            deRegister(protocol, null, channel);
        }
        else {
            delete this.subscriptions[protocol];
            deRegisterAll(protocol);
        }
    }
    _addSubscription(protocol, handler = null, channel) {
        if (this._existsSubscription(protocol, channel)) {
            return;
        }
        if (!this._existsSubscription(protocol)) {
            this.subscriptions[protocol] = {};
        }
        this.subscriptions[protocol][channel] = {};
        if (isFunction(handler)) {
            register(protocol, handler, channel);
        }
    }
    _existsSubscription(protocol, channel) {
        if (this.subscriptions.hasOwnProperty(protocol)) {
            if (!channel || (channel && this.subscriptions[protocol].hasOwnProperty(channel))) {
                return true;
            }
        }
        return false;
    }
    _attachListeners() {
        this._detachListeners();
        this.on(SwEvent.SocketOpen, this._onSocketOpen);
        this.on(SwEvent.SocketClose, this._onSocketCloseOrError);
        this.on(SwEvent.SocketError, this._onSocketCloseOrError);
        this.on(SwEvent.SocketMessage, this._onSocketMessage);
    }
    _detachListeners() {
        this.off(SwEvent.SocketOpen, this._onSocketOpen);
        this.off(SwEvent.SocketClose, this._onSocketCloseOrError);
        this.off(SwEvent.SocketError, this._onSocketCloseOrError);
        this.off(SwEvent.SocketMessage, this._onSocketMessage);
    }
    _emptyExecuteQueues() {
        this._executeQueue.forEach(({ resolve, msg }) => {
            if (typeof msg === 'string') {
                this.executeRaw(msg);
            }
            else {
                resolve(this.execute(msg));
            }
        });
    }
    _closeConnection() {
        logger.error((new Date()), '_closeConnection...');
        this._idle = true;
        this._executeQueue = [];
        clearTimeout(this._keepAliveTimeout);
        if (this.connection) {
            this.connection.close();
        }
        this.connection = null;
        this._failCount = 0;
    }
    _checkTokenExpiration() {
        if (!this.expiresAt) {
            return;
        }
        const diff = this.expiresAt - (Date.now() / 1000);
        if (diff <= 60) {
            logger.warn('Your JWT is going to expire. You should refresh it to keep the session live.');
            trigger(SwEvent.Notification, { type: NOTIFICATION_TYPE.refreshToken, session: this }, this.uuid, false);
        }
        if (!this.expired) {
            setTimeout(this._checkTokenExpiration, 30 * 1000);
        }
    }
    _keepAlive() {
        if (this._doKeepAlive !== true) {
            return;
        }
        if (this._pong === false) {
            logger.info((new Date()) + " websocket close connection due to failCount=", this._failCount);
            return this._closeConnection();
        }
        this.execute(this.Ping)
            .then(() => {
            this._failCount = 0;
            this._pong = true;
        })
            .catch((e) => {
            this._pong = (this._failCount < MAX_FAILED_PING);
        });
        let keepDelay = KEEPALIVE_INTERVAL;
        try {
            if (document.visibilityState == 'hidden' || document.hidden) {
                keepDelay = randomInt(40, 30) * 1000;
            }
        }
        catch (error) {
        }
        this._keepAliveTimeout = setTimeout(() => this._keepAlive(), keepDelay);
    }
    static on(eventName, callback) {
        register(eventName, callback);
    }
    static off(eventName) {
        deRegister(eventName);
    }
    static uuid() {
        return uuidv4();
    }
}
