"use strict";
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());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
const sodium = require('libsodium-wrappers');
const lodash_1 = require("lodash");
const sha256_1 = require("./sha256");
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const isNode = require('detect-node');
const debug = require('debug')('wallet-socket');
class SocketClient {
    constructor(requiredAuthentication, authMessageTypes, aes, zlib, logger, callbackManager, protocol = 'ws', host = '127.0.0.1', port = 13535, path = '/db') {
        this.requiredAuthentication = requiredAuthentication;
        this.authMessageTypes = authMessageTypes;
        this.aes = aes;
        this.zlib = zlib;
        this.logger = logger;
        this.callbackManager = callbackManager;
        this.protocol = protocol;
        this.host = host;
        this.port = port;
        this.path = path;
        this.authenticated = false;
        this.closed = false;
        this.initialised = false;
        this.initialising = false;
        this.messageQueue = [];
        this.connSubject = new rxjs_1.BehaviorSubject(false);
    }
    connect() {
        return __awaiter(this, void 0, void 0, function* () {
            yield sodium.ready;
            return new Promise((resolve, reject) => {
                if (this.initialising) {
                    return reject();
                }
                this.connected().pipe(operators_1.filter((r) => r === true), operators_1.first()).subscribe(resolve);
                this.closed = false;
                this.initMessageId = this.callbackManager.uniqueIdValue;
                this.token = { name: '', token: '' };
                this.initialising = true;
                if (isNode) {
                    const WebSocket = require('ws');
                    this.socket = new WebSocket(`${this.protocol}://${this.host}:${this.port}${this.path}`);
                }
                else {
                    this.socket = new WebSocket(`${this.protocol}://${this.host}:${this.port}${this.path}`);
                }
                this.socket.addEventListener('open', () => {
                    this.encryption = {
                        mySecret: Buffer.alloc(sodium.crypto_box_SECRETKEYBYTES),
                        myPublicKey: Buffer.alloc(sodium.crypto_box_PUBLICKEYBYTES),
                        serverPublicKey: Buffer.alloc(0),
                        shareKey: Buffer.alloc(sodium.crypto_scalarmult_BYTES),
                    };
                    if ('ping' in this.socket) {
                        this.pingInterval = global.setInterval(() => this.socket.ping(() => { }), 15000);
                    }
                    this.encryption.mySecret = Buffer.from(sodium.randombytes_buf(sodium.crypto_box_SECRETKEYBYTES));
                    this.encryption.myPublicKey = Buffer.from(sodium.crypto_scalarmult_base(this.encryption.mySecret));
                    this.callbackManager.addHandler(this.initMessageId, (id, message) => {
                        const data = lodash_1.get(message, 'data', false);
                        debug('-> init %s', data);
                        if (data !== false) {
                            this.encryption.serverPublicKey = Buffer.from(data, 'hex');
                            this.encryption.shareKey = Buffer.from(sodium.crypto_scalarmult(this.encryption.mySecret, this.encryption.serverPublicKey));
                            this.logger.debug('sharekey: %s', this.encryption.shareKey.toString('hex'));
                        }
                        this.callbackManager.handleEvent('OnOpen', {});
                        this.initialised = true;
                        this.initialising = false;
                        this.processMessageQueue();
                        //resolve();
                    });
                    const messageBody = {
                        pub: this.encryption.myPublicKey.toString('hex'),
                    };
                    const request = {
                        MessageType: 'Initialise',
                        MessageHeader: '',
                        RequestID: this.initMessageId,
                        MessageBody: messageBody,
                    };
                    this.socket.send(JSON.stringify(request));
                });
                this.socket.addEventListener('message', (message) => __awaiter(this, void 0, void 0, function* () {
                    if ('data' in message) {
                        message = message.data;
                    }
                    let payload;
                    let isEncrypted = false;
                    debug('-> message: %s', message);
                    if (this.encryption.serverPublicKey.length === 0) {
                        if (message.substr(0, 3) === 'LZ_') {
                            debug('inflating message');
                            payload = this.zlib.inflate(message.substr(3));
                        }
                        else {
                            payload = JSON.parse(message);
                        }
                    }
                    else {
                        isEncrypted = true;
                        // decode AES
                        const decrypted = this.aes.decrypt(message, sha256_1.Sha256.hash(this.encryption.shareKey.toString('hex')));
                        debug('decrypted: %s', decrypted);
                        if (decrypted.substr(0, 3) === 'LZ_') {
                            payload = this.zlib.inflate(decrypted.substr(3));
                            debug('inflated:', payload);
                            try {
                                payload = JSON.parse(payload);
                            }
                            catch (e) {
                                var regex = /"Hash":(([A-Za-z]|[0-9])*)/gi;
                                payload = payload.replace(regex, '"Hash":"$1"');
                                payload = JSON.parse(payload);
                            }
                        }
                        else {
                            payload = JSON.parse(decrypted);
                        }
                    }
                    const id = lodash_1.get(payload, 'requestid', 0) || lodash_1.get(payload, 'RequestID', 0);
                    if (isEncrypted || (id === this.initMessageId)) {
                        this.callbackManager.handleEvent(id, payload);
                    }
                }));
                this.socket.addEventListener('error', (e) => __awaiter(this, void 0, void 0, function* () {
                }));
                this.socket.addEventListener('close', (e) => __awaiter(this, void 0, void 0, function* () {
                    debug('socket disconnected');
                    global.clearInterval(this.pingInterval);
                    this.defaultOnClose(e);
                }));
                this.socket.addEventListener('error', (e) => __awaiter(this, void 0, void 0, function* () {
                    this.defaultOnError(e);
                }));
            });
        });
    }
    disconnect() {
        if (this.socket) {
            this.socket.close(1000, 'logout');
            this.closed = true;
        }
    }
    processMessageQueue() {
        while (this.messageQueue.length > 0) {
            this.sendRequest(this.messageQueue.shift());
        }
    }
    connected() {
        return this.connSubject.asObservable();
    }
    clearMessageQueue() {
        this.messageQueue = [];
    }
    isAuthRequest(req) {
        return lodash_1.includes(this.authMessageTypes, req.MessageType.toLowerCase());
    }
    sendRequest(req) {
        if (!this.initialised && !this.isAuthRequest(req)) {
            debug('queuing request');
            this.connect();
            this.messageQueue.push(req);
            return;
        }
        req['Compress'] = 'lz';
        if (this.requiredAuthentication && this.authenticated && this.token.name) {
            debug('Setting token', this.token.token);
            req[this.token.name] = this.token.token;
        }
        let message = JSON.stringify(req);
        this.logger.debug('<- message: %s' + message);
        if (this.encryption.shareKey.length) {
            debug('encrypting...');
            message = this.aes.encrypt(message, sha256_1.Sha256.hash(this.encryption.shareKey.toString('hex')));
        }
        this.logger.debug('<- message (wire): %s' + message);
        this.socket.send(message);
    }
    authenticate(name, token) {
        this.token = { name, token };
        debug('authenticated. token: %s', this.token);
        this.clearMessageQueue();
        this.authenticated = true;
        this.connSubject.next(true);
    }
    on(event, callback, userData) {
        this.callbackManager.addHandler(event, callback, userData);
    }
    defaultOnError(e) {
        this.logger.debug('error', 'walletnode: Error detail: ' + e);
    }
    defaultOnClose(e) {
        this.logger.debug('error', 'walletnode: Connection closed! No longer connected to web socket server');
        this.socket = false;
        this.initialised = false;
        this.authenticated = false;
        this.token = { name: '', token: '' };
        this.initialising = false;
        this.connSubject.next(false);
        if (this.closed) {
            return;
        }
        setTimeout(() => {
            this.connect();
        }, 2000);
    }
}
exports.SocketClient = SocketClient;
