const readline = require('readline');
const protobuf = require('protobufjs');
const ws = require('nodejs-websocket');

function prettyJsonEncode(obj) {
  return JSON.stringify(obj, "", "  ");
}

class ClientNet {

  constructor(url, protoPbFile, msgIdPbFile) {
    this.url = url;
    this.conn = null;
    this.protoPbFile = protoPbFile;
    this.msgIdPbFile = msgIdPbFile;
    this.protoPb = null;
    this.msgIdPb = null;
    this.cmMsgId = null;
    this.smMsgId = null;
    this.recvBuf = Buffer.alloc(0);
    this.uniqId = 1000;
    this.msgHandlerMap = new Map();
    this.rl = readline.createInterface({
      input: process.stdin,
      //output: process.stdout
    });
    this.rl.on("line", (line)=> {
      this.parseCmdLine(line);
    });
  }

  selfRegisterMsgHandle(msgName) {
    this.registerMsgHandle(
      msgName,
      (msg) => {
        if (this[msgName]) {
          this[msgName](msg);
        }
      });
  }

  async init() {
    {
      this.protoPb = await (new protobuf.Root()).load
      (this.protoPbFile,
       {
         'keepCase': true
       }
      );
    }
    {
      this.msgIdPb = await (new protobuf.Root()).load
      (this.msgIdPbFile,
       {
         'keepCase': true
       }
      );
      this.cmMsgId = this.msgIdPb.lookup('CMMessageId_e');
      this.smMsgId = this.msgIdPb.lookup('SMMessageId_e');
    }
    {
      for (let id in this.smMsgId.valuesById) {
        const msgName = this.smMsgId.valuesById[id];
        this.selfRegisterMsgHandle(msgName.slice(1));
      }
    }
  }

  async connect() {
    console.log(this.url);
    this.conn = await ws.connect(this.url);
    this.on('binary', this.onReceive.bind(this));
  }

  on(eventName, ...args) {
    this.conn.on(eventName, ...args);
  }

  async onReceive(inStream) {
    inStream.on('readable', async () => {
      //console.log('inStream.readable');
      const newData = inStream.read();
      if (newData) {
        this.recvBuf = Buffer.concat([this.recvBuf, newData]);
        await this.onParsePacket();
      }
    });
    inStream.on('end', () => {
      //console.log('inStream.end', this.recvBuf.length);
    });
    inStream.on('close', () => {
      //console.log('inStream.close');
    });
  }

  async onParsePacket() {
    let offset = 0;
    while (this.recvBuf.length >= offset + 12) {
      const msgSize = this.recvBuf.readUInt16LE(offset + 0);
      const msgId = this.recvBuf.readUInt16LE(offset + 2);
      const seqId = this.recvBuf.readUInt32LE(offset + 4);
      const magicCode = this.recvBuf.readUInt16LE(offset + 8);
      //console.log('onParsePacket', msgSize, msgId, seqId, magicCode);
      if (this.recvBuf.length >= offset + 12 + msgSize) {
        await this.processMsg(msgId,
                               this.recvBuf.slice
                               (
                                 offset + 12,
                                 offset + 12 + msgSize)
                              );
        offset += 12 + msgSize;
      } else {
        break;
      }
    }
    this.recvBuf = this.recvBuf.slice(offset);
  }

  async processMsg(msgId, buff) {
    const handlers = this.msgHandlerMap.get(msgId);
    if (handlers) {
      let msg = null;
      handlers.forEach((value, key) => {
        if (!msg) {
          msg = value.msgType.decode(buff);
        }
        console.log(value.msgType['name'], prettyJsonEncode(msg));
        value.cb(msg);
      });
    }
  }

  registerMsgHandle(msgName, cb) {
    try {
      const msgType = this.protoPb.lookupType(msgName);
      const msgId = this.smMsgId.values['_' + msgName];
      if (!msgId) {
        return null;
      }
      const handle = {
        msgId: msgId,
        msgName: msgName,
        msgType: msgType,
        cb: cb,
        uniqId: ++this.uniqId}
      ;
      if (!this.msgHandlerMap.has(msgId)) {
        this.msgHandlerMap.set(msgId, new Map([
          [handle.uniqId, handle]
        ]));
      } else {
        this.msgHandlerMap.get(msgId).set(handle.uniqId, handle);
      }
      return handle;
    } catch (err) {
      return null;
    }
  }

  unRegisterMsgHandle(handle) {
    if (this.msgHandlerMap.has(handle.msgId)) {
      if (this.msgHandlerMap[handle.msgId].has(handle.uniqId)) {
        this.msgHandlerMap[handle.msgId].delete(handle.uniqId);
      }
    }
  }

  async sendMsg(name, msg) {
    const msgType = this.protoPb.lookupType(name);
    const msgId = this.cmMsgId.values['_' + name];
    const msgPb = msgType.create(msg);

    const msgBuf = msgType.encode(msg).finish();
    let buf = Buffer.alloc(12);
    buf.writeUInt16LE(msgBuf.length, 0);
    buf.writeUInt16LE(msgId, 2);
    buf.writeUInt32LE(0, 4);
    buf.writeUInt8('K'.charCodeAt(0), 8);
    buf.writeUInt8('S'.charCodeAt(0), 9);
    buf.writeUInt16LE(0, 10);
    this.conn.sendBinary(Buffer.concat([buf, msgBuf]));
    console.log(name, msg, (Buffer.concat([buf, msgBuf])).length);
  }

  SMLogin(msg) {
    if (msg.errcode) {
      return;
    }
  }

  SMPing(msg) {

  }

  parseCmdLine(line) {
    const params = line.split(' ');
    if (params.length <= 0) {
      console.log('unknown command');
      return;
    }
    const msgName = params[0];
    const msgFields = params.slice(1);
    const msgType = this.protoPb.lookupType(msgName);
    if (!msgType) {
      console.log('unknown command');
      return;
    }
    const msgBody = line.slice(line.indexOf(msgName) + msgName.length);
    const msg = eval('({' + msgBody + '})');
    /*const msg = {};
    for (let i = 0; i < msgType.fieldsArray.length; ++i) {
      if (i < msgFields.length) {
        const name = msgType.fieldsArray[i].name;
        const type = msgType.fieldsArray[i].type;
        if (['int32', 'int', 'float', 'double'].indexOf(type) >= 0) {
          msg[name] = Number(msgFields[i]);
        } else {
          msg[name] = msgFields[i];
        }
      }
    }
    console.log(msg);*/
    this.sendMsg(msgName, msg);
  }

}

module.exports = ClientNet;
