"use strict";
/**
 * todo
 * 1. Include compression
 * 2. Handle JWT when talking to walletnode, if we are going to communicate to walletnode directly.
 */
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());
    });
};
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
    result["default"] = mod;
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const get = require('lodash/get');
const shajs = __importStar(require("sha.js"));
const sodium = require('libsodium-wrappers');
const isNode = require('detect-node');
const debug = require('debug')('member-socket');
let aes;
let crypto;
let base64;
if (isNode) {
    aes = require('./aes');
    crypto = require('./crypto');
    base64 = require('./base64');
}
else {
    aes = require('./aes.web');
    crypto = require('./crypto.web');
    base64 = require('./base64.web');
}
const { encrypt, decrypt } = aes;
const { toHex, randomBytes } = crypto;
const { bytesToBase64, base64ToBytes } = base64;
const rxjs_1 = require("rxjs");
const events_1 = require("events");
const initialRequestTemplate = {
    EndPoint: 'member',
    MessageType: 'Initialise',
    MessageHeader: '',
    MessageBody: {},
};
class SocketClient extends events_1.EventEmitter {
    constructor(stateService) {
        super();
        this.stateService = stateService;
        this.encryption = {};
        this.messageQueue = [];
        this.initialising = false;
        this.hasConnected = false;
        this.closed = false;
        this.connectTries = 0;
        this.channels = [];
        this.callbackMap = {};
        this.open$ = new rxjs_1.Subject();
    }
    ready() {
        return this.socket && !this.initialising;
    }
    connect() {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.socket || this.initialising) {
                return;
            }
            this.closed = false;
            yield sodium.ready;
            return new Promise((resolve, reject) => {
                this.initialising = true;
                try {
                    try {
                        this.connectTries += 1;
                        const mn = this.stateService.detectResults.mnSocket;
                        if (isNode) {
                            const WebSocket = require('ws');
                            this.socket = new WebSocket(`${mn.scheme}://${this.stateService.endpoint}:${mn.port}${mn.path}`);
                        }
                        else {
                            this.socket = new WebSocket(`${mn.scheme}://${this.stateService.endpoint}:${mn.port}${mn.path}`);
                        }
                    }
                    catch (error) {
                        this.initialising = false;
                        this.socket = undefined;
                        return false;
                    }
                    this.socket.addEventListener('message', (message) => __awaiter(this, void 0, void 0, function* () {
                        debug('message -> %o', message.data);
                        const data = JSON.parse(message.data);
                        if (!this.encryption.shareKey) {
                            this.encryption.serverPublicKey = sodium.from_hex(data.data.Data);
                            const shared = sodium.crypto_scalarmult(this.encryption.mySecret, this.encryption.serverPublicKey);
                            this.encryption.shareKey = new shajs.sha256().update(shared).digest();
                            debug('sharekey: %s', this.encryption.shareKey);
                            this.hasConnected = true;
                            this.initialising = false;
                            while (this.messageQueue.length) {
                                this.sendRequest(this.messageQueue.pop());
                            }
                            // On connection:
                            this.emit('initialised');
                            return resolve();
                        }
                        debug('raw data %s', data.data);
                        const encoded = base64ToBytes(data.data);
                        const decrypted = yield decrypt(encoded, this.encryption.shareKey);
                        const decoder = new TextDecoder();
                        const decodedMessage = decoder.decode(decrypted);
                        debug('decrypted message: %s', decodedMessage);
                        switch (data.event) {
                            case 'message':
                                const response = JSON.parse(decodedMessage);
                                if (!(response.RequestID in this.callbackMap)) {
                                    console.error(`Could not find callback for request ${response.RequestID}`);
                                    return;
                                }
                                this.callbackMap[response.RequestID](null, response);
                                delete this.callbackMap[response.RequestID];
                                break;
                            case 'broadcast':
                                debug('-> broadcast %s', decodedMessage);
                                this.subscriptionCallback(decodedMessage);
                                break;
                        }
                    }));
                    this.socket.addEventListener('open', () => {
                        this.emit('open');
                        // Generate my public-private key pair.
                        this.encryption.mySecret = sodium.randombytes_buf(sodium.crypto_box_SECRETKEYBYTES);
                        this.encryption.myPublicKey = sodium.crypto_scalarmult_base(this.encryption.mySecret, 'hex');
                        // Send my public key to server.
                        const requestBody = {
                            pub: this.encryption.myPublicKey,
                        };
                        // Initial handshake request.
                        const initialRequest = Object.assign({}, initialRequestTemplate, { MessageBody: requestBody });
                        const initialRequestText = JSON.stringify(initialRequest);
                        this.socket.send(JSON.stringify({ event: 'message', data: { MessageBody: requestBody } }));
                    });
                    this.socket.addEventListener('error', () => {
                    });
                    this.socket.addEventListener('close', () => {
                        /**
                         * Reconnect when error. Hopefully, the make the connection more stable.
                         */
                        debug('socket disconnected');
                        this.socket = null;
                        this.encryption = {};
                        this.initialising = false;
                        this.emit('close');
                        if (!this.closed) {
                            setTimeout(() => this.connect(), 2000);
                        }
                    });
                    this.defaultOnOpen();
                }
                catch (error) {
                    this.showTryCatchError(error);
                }
            });
        });
    }
    sendRequest([request, callback]) {
        return __awaiter(this, void 0, void 0, function* () {
            /**
             * Cache the requests from the browser when:
             * 1. Initialising
             * 2. Required authentication and has not authenticated and the request message type is not
             *      this.authMessageTypes: ['authentication']
             */
            if (this.initialising || !this.encryption.shareKey) {
                this.messageQueue.push([request, callback]);
            }
            else {
                // Log the request we sending out.
                try {
                    debug('<- message [%s] %o', get(request, 'MessageBody.RequestName'), request);
                }
                catch (error) {
                    this.showTryCatchError(error);
                }
                // Send request
                request.RequestID = toHex(randomBytes(16));
                let requestText = JSON.stringify(request);
                if (this.encryption.shareKey) {
                    const decoder = new TextDecoder();
                    const cipherText = yield encrypt(requestText, this.encryption.shareKey);
                    debug('encrypted: %s', cipherText);
                    requestText = bytesToBase64(cipherText);
                }
                this.callbackMap[request.RequestID.toString()] = callback;
                this.socket.send(JSON.stringify({ event: 'message', data: requestText }));
            }
        });
    }
    subscribeToChannel(channelName, callback) {
        this.subscriptionCallback = callback;
    }
    defaultOnOpen() {
        debug('Connection established to web socket server!');
    }
    disconnect() {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.socket) {
                this.socket.close(1000, 'logout');
                this.resetConnectionData();
                this.closed = true;
            }
        });
    }
    resetConnectionData() {
        this.initialising = false;
        this.encryption = {};
        this.messageQueue = [];
        this.hasConnected = false;
        this.channels = [];
        this.connectTries = 0;
    }
    showTryCatchError(error) {
        console.log('Error: ' + error.message + ', file: ' + error.fileName + ', line: ' + error.lineNumber);
    }
}
exports.SocketClient = SocketClient;
