import * as WebSocket from 'ws';
import {Package, Message, Protocol} from 'omelox-protocol';
import {Protobuf} from 'omelox-protobuf';
import {EventEmitter} from 'events';
import {cacheClass} from "./cacheClass";
import { MyLogger } from "./my.logger";

const JS_WS_CLIENT_TYPE = 'js-ws';
const JS_WS_CLIENT_VERSION = '0.0.1';

const RES_OK = 200;
const RES_OLD_CLIENT = 501;
const CODE_DICT_ERROR = 502;
const CODE_PROTOS_ERROR = 503;

export interface IPomeloInterface {
    on(event: 'close', cb): any;

    on(event: 'io-error', cb): any;

    on(event: 'error', cb): any;

    on(event: 'heartbeat timeout', cb): any;

    on(event: 'onKick', cb): any;

    initAsync(params): Promise<any>;

    init(params, cb);

    disconnect();

    request(route, msg): Promise<any>;

    notify(route, msg);
}

export class Pomelo extends EventEmitter implements IPomeloInterface {
    socket = null;
    reqId = 0;
    callbacks = {};
    handlers = {};
    routeMap = {};
    protobuf: Protobuf = null;
    heartbeatInterval = 5000;
    heartbeatTimeout = this.heartbeatInterval * 2;
    nextHeartbeatTimeout = 0;
    gapThreshold = 100; // heartbeat gap threshold
    heartbeatId = null;
    heartbeatTimeoutId = null;

    handshakeCallback = null;
    logger: MyLogger;

    handshakeBuffer = {
        'sys': {
            type: JS_WS_CLIENT_TYPE,
            version: JS_WS_CLIENT_VERSION,
            dictVersion: '' as any,
            protoVersion: '' as any
        },
        'user': {}
    };

    initCallback = null;

    params = null;


    data: { dict: any, abbrs: any, protos: any } = {} as any;

    sysCache: { dictVersion: string, protoVersion: string, dict: any, protos: any } = null;
    static ClientId = 0;

    constructor(useNestLogger = true, private readonly showPackageLog: boolean = true) {
        super();
        this.handlers[Package.TYPE_HANDSHAKE] = this.handshake.bind(this);
        this.handlers[Package.TYPE_HEARTBEAT] = this.heartbeat.bind(this);
        this.handlers[Package.TYPE_DATA] = this.onData.bind(this);
        this.handlers[Package.TYPE_KICK] = this.onKick.bind(this);
        if (useNestLogger) {
            this.logger = new MyLogger('wsclient-' + ++Pomelo.ClientId);
        } else {
            this.logger = console as any;
        }

    }

    initAsync(params): Promise<any> {
        return new Promise(resolve => {
            this.params = params;
            params.debug = true;
            this.initCallback = resolve;
            const host = params.host;
            const port = params.port;

            let url = host;
            if (port) {
                url += ':' + port;
            }
            this.sysCache = cacheClass.getCache() || {} as any;
            this.handshakeBuffer.sys.dictVersion = this.sysCache.dictVersion || 0;
            this.handshakeBuffer.sys.protoVersion = this.sysCache.protoVersion || 0;

            if (!params.type) {
                this.logger.log('init websocket');
                this.handshakeBuffer.user = params.user;
                this.handshakeCallback = params.handshakeCallback;
                this.initWebSocket(url, resolve);
            }
        })
    }

    init(params, cb) {
        this.params = params;
        params.debug = true;
        this.initCallback = cb;
        const host = params.host;
        const port = params.port;

        let url = 'ws://' + host;
        if (port) {
            url += ':' + port;
        }
        this.sysCache = cacheClass.getCache() || {} as any;
        this.handshakeBuffer.sys.dictVersion = this.sysCache.dictVersion || 0;
        this.handshakeBuffer.sys.protoVersion = this.sysCache.protoVersion || 0;

        if (!params.type) {
            this.logger.log('init websocket');
            this.handshakeBuffer.user = params.user;
            this.handshakeCallback = params.handshakeCallback;
            this.initWebSocket(url, cb);
        }

    };

    private initWebSocket(url, cb) {
        this.logger.log(url);
        const onopen = (event) => {
            this.logger.log('[pomeloclient.init] websocket connected!');
            const obj = Package.encode(Package.TYPE_HANDSHAKE, Protocol.strencode(JSON.stringify(this.handshakeBuffer)));
            this.send(obj);
        };
        const onmessage = (event) => {
            if (this.showPackageLog && event.data.byteLength != 4) {
                this.logger.log('recv orgdata', event.data.byteLength, Buffer.from(event.data).toString('hex'));
            }
            this.processPackage(Package.decode(event.data));//, cb);
            // new package arrived, update the heartbeat timeout
            if (this.heartbeatTimeout) {
                this.nextHeartbeatTimeout = Date.now() + this.heartbeatTimeout;
            }
        };
        const onerror = (event) => {
            this.emit('io-error', event);
            this.logger.log('socket error %j ', event);
        };
        const onclose = (event) => {
            this.emit('close', event);
            this.logger.log('socket close %j ', event);
        };
        this.socket = new WebSocket(url);
        this.socket.binaryType = 'arraybuffer';
        this.socket.onopen = onopen;
        this.socket.onmessage = onmessage;
        this.socket.onerror = onerror;
        this.socket.onclose = onclose;
    };

    disconnect() {
        if (this.socket) {
            if (this.socket.disconnect) this.socket.disconnect();
            if (this.socket.close) this.socket.close();
            this.logger.log('disconnect');
            this.socket = null;
        }

        if (this.heartbeatId) {
            clearTimeout(this.heartbeatId);
            this.heartbeatId = null;
        }
        if (this.heartbeatTimeoutId) {
            clearTimeout(this.heartbeatTimeoutId);
            this.heartbeatTimeoutId = null;
        }
    };

    request(route, msg): Promise<any> {
        return new Promise((resolve, reject) => {
            msg = msg || {};
            route = route || msg.route;
            if (!route) {
                this.logger.log('fail to send request without route.');
                return;
            }

            this.reqId++;
            this.sendMessage(this.reqId, route, msg);

            this.callbacks[this.reqId] = resolve;
            this.routeMap[this.reqId] = route;
        })
    };

    notify(route, msg) {
        msg = msg || {};
        this.sendMessage(0, route, msg);
    };

    private sendMessage(reqId, route, msg) {
        const type = reqId ? Message.TYPE_REQUEST : Message.TYPE_NOTIFY;
        if (this.showPackageLog) {
            this.logger.log('send', reqId, route, msg);
        }
        //compress message by protobuf
        const protos = !!this.data.protos ? this.data.protos.client : {};
        if (!!protos[route]) {
            msg = this.protobuf.encode(route, msg);
        } else {
            msg = Protocol.strencode(JSON.stringify(msg));
        }

        let compressRoute = false;
        if (this.data.dict && this.data.dict[route]) {
            route = this.data.dict[route];
            compressRoute = true;
        }

        msg = Message.encode(reqId, type, compressRoute, route, msg);
        const packet = Package.encode(Package.TYPE_DATA, msg);
        if (this.showPackageLog) {
            this.logger.log('send', "packet", packet.length, packet.toString('hex'));
        }
        this.send(packet);
    };

    private send(packet) {
        if (!!this.socket) {
            this.socket.send(packet.buffer || packet, {binary: true, mask: true});
        }
    };

    private heartbeat(data) {
        const obj = Package.encode(Package.TYPE_HEARTBEAT);
        if (this.heartbeatTimeoutId) {
            clearTimeout(this.heartbeatTimeoutId);
            this.heartbeatTimeoutId = null;
        }

        if (this.heartbeatId) {
            // already in a heartbeat interval
            return;
        }

        this.heartbeatId = setTimeout(() => {
            this.heartbeatId = null;
            this.send(obj);

            this.nextHeartbeatTimeout = Date.now() + this.heartbeatTimeout;
            this.heartbeatTimeoutId = setTimeout(this.heartbeatTimeoutCb.bind(this), this.heartbeatTimeout);
        }, this.heartbeatInterval);
    };

    private heartbeatTimeoutCb() {
        const gap = this.nextHeartbeatTimeout - Date.now();
        if (gap > this.gapThreshold) {
            this.heartbeatTimeoutId = setTimeout(this.heartbeatTimeoutCb.bind(this), gap);
        } else {
            this.logger.error('server heartbeat timeout');
            this.emit('heartbeat timeout');
            this.disconnect();
        }
    };

    private handshake(data) {
        data = JSON.parse(Protocol.strdecode(data));
        if (data.code === RES_OLD_CLIENT) {
            this.emit('error', 'client version not fullfill');
            return;
        }

        if (data.code !== RES_OK) {
            this.emit('error', 'handshake fail');
            return;
        }

        this.handshakeInit(data);

        const obj = Package.encode(Package.TYPE_HANDSHAKE_ACK);
        this.send(obj);
        if (this.initCallback) {
            this.initCallback(this.socket);
            this.initCallback = null;
        }
    };

    private onData(data) {
        //probuff decode
        const msg = Message.decode(data);

        if (msg.id > 0) {
            msg.route = this.routeMap[msg.id];
            delete this.routeMap[msg.id];
            if (!msg.route) {
                return;
            }
        }

        msg.body = this.deCompose(msg);
        if (this.showPackageLog) {
            this.logger.log('recv', JSON.stringify(msg), "\n\tpacket", data.length, data.toString('hex'));
        }
        this.processMessage(msg);
    };


    private onKick(data) {
        this.emit('onKick', data.toString());
    };

    private processPackage(msg) {
        if (Array.isArray(msg)) {
            for (let m of msg) {
                this.handlers[m.type](m.body);
            }
        } else {
            this.handlers[msg.type](msg.body);
        }

    };

    private processMessage(msg) {
        if (!msg || !msg.id) {
            // server push message
            // this.logger.error('processMessage error!!!');
            this.emit(msg.route, msg.body);
            return;
        }

        //if have a id then find the callback function with the request
        const cb = this.callbacks[msg.id];

        delete this.callbacks[msg.id];
        if (typeof cb !== 'function') {
            return;
        }

        cb(msg.body);
        return;
    };

    private processMessageBatch(pomelo, msgs) {
        for (let i = 0, l = msgs.length; i < l; i++) {
            this.processMessage(msgs[i]);
        }
    };

    private deCompose(msg) {
        const protos = !!this.data.protos ? this.data.protos.server : {};
        const abbrs = this.data.abbrs;
        let route = msg.route;

        try {
            //Decompose route from dict
            if (msg.compressRoute) {
                if (!abbrs[route]) {
                    this.logger.error('illegal msg!');
                    return {};
                }

                route = msg.route = abbrs[route];
            }
            if (!!protos[route]) {
                return this.protobuf.decode(route, msg.body);
            } else {
                return JSON.parse(Protocol.strdecode(msg.body));
            }
        } catch (ex) {
            this.logger.error('route, body = ' + route + ", " + msg.body);
        }

        return msg;
    };

    private handshakeInit(data) {
        if (data.sys && data.sys.heartbeat) {
            this.heartbeatInterval = data.sys.heartbeat * 1000;   // heartbeat interval
            this.heartbeatTimeout = this.heartbeatInterval * 2;        // max heartbeat timeout
        } else {
            this.heartbeatInterval = 0;
            this.heartbeatTimeout = 0;
        }

        this.initData(data);

        if (typeof this.handshakeCallback === 'function') {
            this.handshakeCallback(data.user);
        }
    };

    //Initilize data used in pomelo client
    private initData(data) {
        if (!data || !data.sys) {
            return;
        }

        const dictVersion = data.sys.dictVersion;
        const protoVersion = data.sys.protos ? data.sys.protos.version : null;

        let changed = false;
        const dict = data.sys.dict || this.sysCache.dict;
        const protos = data.sys.protos || this.sysCache.protos;

        if (dictVersion) {
            this.sysCache.dict = dict;
            this.sysCache.dictVersion = dictVersion;
            changed = true;
        }

        if (protoVersion) {
            this.sysCache.protos = protos;
            this.sysCache.protoVersion = protoVersion;
            changed = true;
        }
        if (changed) {
            cacheClass.saveCache(this.sysCache);
        }
        //Init compress dict
        if (!!dict) {
            this.data.dict = dict;
            this.data.abbrs = {};

            for (const route in dict) {
                this.data.abbrs[dict[route]] = route;
            }
        }

        //Init protobuf protos
        if (!!protos) {
            this.data.protos = {
                server: protos.server || {},
                client: protos.client || {}
            };
            if (!this.protobuf) {
                // 要改WEB JS客户端的话 这里可能需要改一下。
                this.protobuf = new Protobuf({encoderProtos: protos.client, decoderProtos: protos.server});
            }
        }
    };
}
























