declare let Buffer: any;

let PKG_HEAD_BYTES = 4;
let MSG_FLAG_BYTES = 1;
let MSG_ROUTE_CODE_BYTES = 2;
let MSG_ID_MAX_BYTES = 5;
let MSG_ROUTE_LEN_BYTES = 1;

let MSG_ROUTE_CODE_MAX = 0xffff;

let MSG_COMPRESS_ROUTE_MASK = 0x1;
let MSG_COMPRESS_GZIP_MASK = 0x1;
let MSG_COMPRESS_GZIP_ENCODE_MASK = 1 << 4;
let MSG_TYPE_MASK = 0x7;

export namespace Protocol {
  /**
   * pomele client encode
   * id message id;
   * route message route
   * msg message body
   * socketio current support string
   */
  export function strencode(str: string) {
      // encoding defaults to 'utf8'
      return Buffer.from(str);
  }

  /**
   * client decode
   * msg String data
   * return Message Object
   */
  export function strdecode(buffer: object) {
      // encoding defaults to 'utf8'
      return buffer.toString();
  }
}


export namespace Package {

  export let TYPE_HANDSHAKE = 1;
  export let TYPE_HANDSHAKE_ACK = 2;
  export let TYPE_HEARTBEAT = 3;
  export let TYPE_DATA = 4;
  export let TYPE_KICK = 5;

  function isValidType(type: number): boolean {
    return type >= TYPE_HANDSHAKE && type <= TYPE_KICK;
  }

  /**
   * Package protocol encode.
   *
   * Omelox package format:
   * +------+-------------+------------------+
   * | type | body length |       body       |
   * +------+-------------+------------------+
   *
   * Head: 4bytes
   *   0: package type,
   *      1 - handshake,
   *      2 - handshake ack,
   *      3 - heartbeat,
   *      4 - data
   *      5 - kick
   *   1 - 3: big-endian body length
   * Body: body length bytes
   *
   * @param  {Number}    type   package type
   * @param  {Buffer} body   body content in bytes
   * @return {Buffer}        new byte array that contains encode result
   */
  export function encode(type: number, body?: Buffer) {
    let length = body ? body.length : 0;
    let buffer = Buffer.alloc(PKG_HEAD_BYTES + length);
    let index = 0;
    buffer[index++] = type & 0xff;
    buffer[index++] = (length >> 16) & 0xff;
    buffer[index++] = (length >> 8) & 0xff;
    buffer[index++] = length & 0xff;
    if (body) {
      copyArray(buffer, index, body, 0, length);
    }
    return buffer;
  }

  /**
   * Package protocol decode.
   * See encode for package format.
   *
   * @param  {Buffer} buffer byte array containing package content
   * @return {Object}           {type: package type, buffer: body byte array}
   */
  export function decode(buffer: Buffer) {
    let offset = 0;
    let bytes = Buffer.from(buffer);
    let length = 0;
    let rs = [];
    while (offset < bytes.length) {
      let type = bytes[offset++];
      length = ((bytes[offset++]) << 16 | (bytes[offset++]) << 8 | bytes[offset++]) >>> 0;
      if (!isValidType(type) || length > bytes.length) {
        return { 'type': type }; // return invalid type, then disconnect!
      }
      let body = length ? Buffer.alloc(length) : null;
      if (body) {
        copyArray(body, 0, bytes, offset, length);
      }
      offset += length;
      rs.push({ 'type': type, 'body': body });
    }
    return rs.length === 1 ? rs[0] : rs;
  }
}

export namespace Message {

  export let TYPE_REQUEST = 0;
  export let TYPE_NOTIFY = 1;
  export let TYPE_RESPONSE = 2;
  export let TYPE_PUSH = 3;
  /**
   * Message protocol encode.
   *
   * @param  {Number} id            message id
   * @param  {Number} type          message type
   * @param  {Number} compressRoute whether compress route
   * @param  {Number|String} route  route code or route string
   * @param  {Buffer} msg           message body bytes
   * @return {Buffer}               encode result
   */
  export function encode(id: number, type: number, compressRoute: boolean, route: number | string | Buffer, msg: Buffer, compressGzip?: boolean) {
    // caculate message max length
    let idBytes = msgHasId(type) ? caculateMsgIdBytes(id) : 0;
    let msgLen = MSG_FLAG_BYTES + idBytes;

    if (msgHasRoute(type)) {
      if (compressRoute) {
        if (typeof route !== 'number') {
          throw new Error('error flag for number route!');
        }
        msgLen += MSG_ROUTE_CODE_BYTES;
      } else {
        msgLen += MSG_ROUTE_LEN_BYTES;
        if (route) {
          route = Protocol.strencode(route as string);
          if ((route as string).length > 255) {
            throw new Error('route maxlength is overflow');
          }
          msgLen += (route as string).length;
        }
      }
    }

    if (msg) {
      msgLen += msg.length;
    }

    let buffer = Buffer.alloc(msgLen);
    let offset = 0;

    // add flag
    offset = encodeMsgFlag(type, compressRoute, buffer, offset, compressGzip);

    // add message id
    if (msgHasId(type)) {
      offset = encodeMsgId(id, buffer, offset);
    }

    // add route
    if (msgHasRoute(type)) {
      offset = encodeMsgRoute(compressRoute, route, buffer, offset);
    }

    // add body
    if (msg) {
      offset = encodeMsgBody(msg, buffer, offset);
    }

    return buffer;
  }

  /**
   * Message protocol decode.
   *
   * @param  {Buffer|Uint8Array} buffer message bytes
   * @return {Object}            message object
   */
  export function decode(buffer: Buffer) {
    let bytes = Buffer.from(buffer);
    let bytesLen = bytes.length || bytes.byteLength;
    let offset = 0;
    let id = 0;
    let route = null;

    // parse flag
    let flag = bytes[offset++];
    let compressRoute = flag & MSG_COMPRESS_ROUTE_MASK;
    let type = (flag >> 1) & MSG_TYPE_MASK;
    let compressGzip = (flag >> 4) & MSG_COMPRESS_GZIP_MASK;

    // parse id
    if (msgHasId(type)) {
      let m = 0;
      let i = 0;
      do {
        m = parseInt(bytes[offset]);
        id += (m & 0x7f) << (7 * i);
        offset++;
        i++;
      } while (m >= 128);
    }

    // parse route
    if (msgHasRoute(type)) {
      if (compressRoute) {
        route = (bytes[offset++]) << 8 | bytes[offset++];
      } else {
        let routeLen = bytes[offset++];
        if (routeLen) {
          route = Buffer.alloc(routeLen);
          copyArray(route, 0, bytes, offset, routeLen);
          route = Protocol.strdecode(route);
        } else {
          route = '';
        }
        offset += routeLen;
      }
    }

    // parse body
    let bodyLen = bytesLen - offset;
    let body = Buffer.alloc(bodyLen);

    copyArray(body, 0, bytes, offset, bodyLen);

    return {
      'id': id, 'type': type, 'compressRoute': compressRoute,
      'route': route, 'body': body, 'compressGzip': compressGzip
    };
  }
}
let copyArray = function (dest: Buffer, doffset: number, src: Buffer, soffset: number, length: number) {
  if ('function' === typeof src.copy) {
    // Buffer
    src.copy(dest, doffset, soffset, soffset + length);
  } else {
    // Uint8Array
    for (let index = 0; index < length; index++) {
      dest[doffset++] = src[soffset++];
    }
  }
};

let msgHasId = function (type: number) {
  return type === Message.TYPE_REQUEST || type === Message.TYPE_RESPONSE;
};

let msgHasRoute = function (type: number) {
  return type === Message.TYPE_REQUEST || type === Message.TYPE_NOTIFY ||
    type === Message.TYPE_PUSH;
};

let caculateMsgIdBytes = function (id: number) {
  let len = 0;
  do {
    len += 1;
    id >>= 7;
  } while (id > 0);
  return len;
};

let encodeMsgFlag = function (type: number, compressRoute: boolean, buffer: Buffer, offset: number, compressGzip: boolean) {
  if (type !== Message.TYPE_REQUEST && type !== Message.TYPE_NOTIFY &&
    type !== Message.TYPE_RESPONSE && type !== Message.TYPE_PUSH) {
    throw new Error('unkonw message type: ' + type);
  }

  buffer[offset] = (type << 1) | (compressRoute ? 1 : 0);

  if (compressGzip) {
    buffer[offset] = buffer[offset] | MSG_COMPRESS_GZIP_ENCODE_MASK;
  }

  return offset + MSG_FLAG_BYTES;
};

let encodeMsgId = function (id: number, buffer: Buffer, offset: number) {
  do {
    let tmp = id % 128;
    let next = Math.floor(id / 128);

    if (next !== 0) {
      tmp = tmp + 128;
    }
    buffer[offset++] = tmp;

    id = next;
  } while (id !== 0);

  return offset;
};

let encodeMsgRoute = function (compressRoute: boolean, _route: number | string | Buffer, buffer: Buffer, offset: number) {
  if (compressRoute) {
    let route = _route as number;
    if (route > MSG_ROUTE_CODE_MAX) {
      throw new Error('route number is overflow');
    }

    buffer[offset++] = (route >> 8) & 0xff;
    buffer[offset++] = route & 0xff;
  } else {
    let route = _route as Buffer;
    if (route) {
      buffer[offset++] = route.length & 0xff;
      copyArray(buffer, offset, route as Buffer, 0, route.length);
      offset += route.length;
    } else {
      buffer[offset++] = 0;
    }
  }

  return offset;
};

let encodeMsgBody = function (msg: Buffer, buffer: Buffer, offset: number) {
  copyArray(buffer, offset, msg, 0, msg.length);
  return offset + msg.length;
};
