'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }

var d = _interopDefault(require('debug'));
var WebSocket = _interopDefault(require('isomorphic-ws'));
var request = _interopDefault(require('superagent'));
var EventEmitter = _interopDefault(require('eventemitter3'));

/**
 * 节点地区
 * @readonly
 * @enum {number}
 */
const Region = {
  /** 华北节点 */
  NorthChina: 0,
  /** 华东节点 */
  EastChina: 1,
  /** 美国节点 */
  NorthAmerica: 2,
};

/**
 * 事件
 * @readonly
 * @enum {string}
 */
const Event = {
  /**
   * 连接成功
   * @event Play#CONNECTED
   */
  CONNECTED: 'connected',
  /**
   * 连接失败
   *
   * @event Play#CONNECT_FAILED
   * @param {Object} payload
   * @param {Number} payload.code
   * @param {String} payload.detail
   */
  CONNECT_FAILED: 'connectFailed',
  /**
   * 断开连接
   * @event Play#DISCONNECTED
   */
  DISCONNECTED: 'disconnected',
  /**
   * 加入到大厅
   * @event Play#LOBBY_JOINED
   */
  LOBBY_JOINED: 'lobbyJoined',
  /**
   * 离开大厅
   * @event Play#LOBBY_LEFT
   */
  LOBBY_LEFT: 'lobbyLeft',
  /**
   * 大厅房间列表变化
   * @event Play#LOBBY_ROOM_LIST_UPDATED
   */
  LOBBY_ROOM_LIST_UPDATED: 'lobbyRoomListUpdate',
  /**
   * 创建房间成功
   * @event Play#ROOM_CREATED
   */
  ROOM_CREATED: 'roomCreated',
  /**
   * 创建房间失败
   * @event Play#ROOM_CREATE_FAILED
   * @param {Object} payload
   * @param {Number} payload.code
   * @param {String} payload.detail
   */
  ROOM_CREATE_FAILED: 'roomCreateFailed',
  /**
   * 加入房间成功
   * @event Play#ROOM_JOINED
   */
  ROOM_JOINED: 'roomJoined',
  /**
   * 加入房间失败
   * @event Play#ROOM_JOIN_FAILED
   */
  ROOM_JOIN_FAILED: 'roomJoinFailed',
  /**
   * 有新玩家加入房间
   * @event Play#PLAYER_ROOM_JOINED
   * @param {Object} payload
   * @param {Player} payload.newPlayer
   */
  PLAYER_ROOM_JOINED: 'newPlayerJoinedRoom',
  /**
   * 有玩家离开房间
   * @event Play#PLAYER_ROOM_LEFT
   * @param {Object} payload
   * @param {Player} payload.leftPlayer
   */
  PLAYER_ROOM_LEFT: 'playerLeftRoom',
  /**
   * 玩家活跃属性变化
   * @event Play#PLAYER_ACTIVITY_CHANGED
   * @param {Object} payload
   * @param {Player} payload.player
   */
  PLAYER_ACTIVITY_CHANGED: 'playerActivityChanged',
  /**
   * 主机变更
   * @event Play#MASTER_SWITCHED
   * @param {Object} payload
   * @param {Player} payload.newMaster
   */
  MASTER_SWITCHED: 'masterSwitched',
  /**
   * 离开房间
   * @event Play#ROOM_LEFT
   */
  ROOM_LEFT: 'roomLeft',
  /**
   * 房间自定义属性变化
   * @event Play#ROOM_CUSTOM_PROPERTIES_CHANGED
   * @param {Object} payload
   * @param {Object} payload.changedProps
   */
  ROOM_CUSTOM_PROPERTIES_CHANGED: 'roomCustomPropertiesChanged',
  /**
   * 玩家自定义属性变化
   * @event Play#PLAYER_CUSTOM_PROPERTIES_CHANGED
   * @param {Object} payload
   * @param {Player} payload.player
   * @param {Object} payload.changedProps
   */
  PLAYER_CUSTOM_PROPERTIES_CHANGED: 'playerCustomPropertiesChanged',
  /**
   * 自定义事件
   * @event Play#CUSTOM_EVENT
   * @param {Object} payload
   * @param {Number|String} payload.eventId
   * @param {Object} payload.eventData
   * @param {Number} payload.senderId
   */
  CUSTOM_EVENT: 'customEvent',
  /**
   * 错误事件
   * @event Play#ERROR
   * @param {Object} payload
   * @param {Number} payload.code
   * @param {String} payload.detail
   */
  ERROR: 'error',
};

/**
 * 玩家类
 */
class Player {
  constructor(play) {
    this._play = play;
    this._userId = '';
    this._actorId = -1;
  }

  static _newFromJSONObject(play, playerJSONObject) {
    const player = new Player(play);
    player._initWithJSONObject(playerJSONObject);
    return player;
  }

  _initWithJSONObject(playerJSONObject) {
    this._userId = playerJSONObject.pid;
    this._actorId = playerJSONObject.actorId;
    if (playerJSONObject.properties) {
      this.properties = playerJSONObject.properties;
    } else {
      this.properties = {};
    }
  }

  /**
   * 玩家 ID
   * @type {string}
   * @readonly
   */
  get userId() {
    return this._userId;
  }

  /**
   * 房间玩家 ID
   * @type {number}
   * @readonly
   */
  get actorId() {
    return this._actorId;
  }

  /**
   * 判断是不是当前客户端玩家
   * @return {Boolean}
   */
  isLocal() {
    return (
      this._actorId !== -1 && this._play._player._actorId === this._actorId
    );
  }

  /**
   * 判断是不是主机玩家
   * @return {Boolean}
   */
  isMaster() {
    return this._actorId !== -1 && this._play._room.masterId === this._actorId;
  }

  /**
   * 判断是不是活跃状态
   * @return {Boolean}
   */
  isActive() {
    return this.active;
  }

  /**
   * 设置玩家的自定义属性
   * @param {Object} properties 自定义属性
   * @param {Object} opts 设置选项
   * @param {Object} opts.expectedValues 期望属性，用于 CAS 检测
   */
  setCustomProperties(properties, { expectedValues = null } = {}) {
    this._play._setPlayerCustomProperties(
      this._actorId,
      properties,
      expectedValues
    );
  }

  /**
   * 获取自定义属性
   * @return {Object}
   */
  getCustomProperties() {
    return this.properties;
  }

  // 设置活跃状态
  _setActive(active) {
    this.active = active;
  }

  _mergeProperties(changedProperties) {
    this.properties = Object.assign(this.properties, changedProperties);
  }
}

/**
 * 大厅房间数据类
 */
class LobbyRoom {
  constructor(lobbyRoomDTO) {
    this._roomName = lobbyRoomDTO.cid;
    this._maxPlayerCount = lobbyRoomDTO.maxMembers;
    this._expectedUserIds = lobbyRoomDTO.expectMembers;
    this._emptyRoomTtl = lobbyRoomDTO.emptyRoomTtl;
    this._playerTtl = lobbyRoomDTO.playerTtl;
    this._playerCount = lobbyRoomDTO.playerCount;
    if (lobbyRoomDTO.attr) {
      this._customRoomProperties = lobbyRoomDTO.attr;
    }
  }

  /**
   * 房间名称
   * @type {string}
   * @readonly
   */
  get roomName() {
    return this._roomName;
  }

  /**
   * 房间最大玩家数
   * @type {number}
   * @readonly
   */
  get maxPlayerCount() {
    return this._maxPlayerCount;
  }

  /**
   * 邀请好友 ID 数组
   * @type {Array.<string>}
   * @readonly
   */
  get expectedUserIds() {
    return this._expectedUserIds;
  }

  /**
   * 房间置空后销毁时间（秒）
   * @type {number}
   * @readonly
   */
  get emptyRoomTtl() {
    return this._emptyRoomTtl;
  }

  /**
   * 玩家离线后踢出房间时间（秒）
   * @type {number}
   * @readonly
   */
  get playerTtl() {
    return this._playerTtl;
  }

  /**
   * 当前房间玩家数量
   * @type {number}
   * @readonly
   */
  get playerCount() {
    return this._playerCount;
  }

  /**
   * 房间属性
   * @type {Object}
   * @readonly
   */
  get customRoomProperties() {
    return this._customRoomProperties;
  }
}

function handleErrorMsg(play, msg) {
  console.error(`error: ${JSON.stringify(msg)}`);
  play.emit(Event.ERROR, {
    code: msg.reasonCode,
    detail: msg.detail,
  });
}

const debug = d('Play:MasterHandler');

// 连接建立
function handleMasterServerSessionOpen(play, msg) {
  play._sessionToken = msg.st;
  const player = new Player(play);
  player._userId = play.userId;
  play._player = player;
  if (play._autoJoinLobby) {
    play.joinLobby();
  }
  play.emit(Event.CONNECTED);
}

// 加入大厅
function handleJoinedLobby(play, msg) {
  if (msg.reasonCode) {
    const { reasonCode, detail } = msg;
    console.error(`join lobby failed: ${reasonCode} - ${detail}`);
  } else {
    play._inLobby = true;
    play.emit(Event.LOBBY_JOINED);
  }
}

// 离开大厅
function handleLeftLobby(play) {
  play._inLobby = false;
  play.emit(Event.LOBBY_LEFT);
}

// 房间列表更新
function handleRoomList(play, msg) {
  play._lobbyRoomList = [];
  for (let i = 0; i < msg.list.length; i += 1) {
    const lobbyRoomDTO = msg.list[i];
    play._lobbyRoomList[i] = new LobbyRoom(lobbyRoomDTO);
  }
  play.emit(Event.LOBBY_ROOM_LIST_UPDATED);
}

function handleGameServer(play, msg) {
  if (play._inLobby) {
    play._inLobby = false;
    play.emit(Event.LOBBY_LEFT);
  }
  play._gameServer = msg.secureAddr;
  if (msg.cid) {
    play._cachedRoomMsg.cid = msg.cid;
  }
  play._connectToGame();
}

// 创建房间
function handleCreateGameServer(play, msg) {
  if (msg.reasonCode) {
    play.emit(Event.ROOM_CREATE_FAILED, {
      code: msg.reasonCode,
      detail: msg.detail,
    });
  } else {
    play._cachedRoomMsg.op = 'start';
    handleGameServer(play, msg);
  }
}

// 加入房间
/* eslint no-param-reassign: ["error", { "props": false }] */
function handleJoinGameServer(play, msg) {
  if (msg.reasonCode) {
    play.emit(Event.ROOM_JOIN_FAILED, {
      code: msg.reasonCode,
      detail: msg.detail,
    });
  } else {
    play._cachedRoomMsg.op = 'add';
    handleGameServer(play, msg);
  }
}

// 大厅消息处理
function handleMasterMsg(play, message) {
  const msg = JSON.parse(message.data);
  debug(`${play.userId} Lobby msg: ${msg.op} <- ${message.data}`);
  switch (msg.cmd) {
    case 'session':
      switch (msg.op) {
        case 'opened':
          handleMasterServerSessionOpen(play, msg);
          break;
        default:
          console.error(`no handler for lobby msg: ${msg.op}`);
          break;
      }
      break;
    case 'lobby':
      switch (msg.op) {
        case 'added':
          handleJoinedLobby(play, msg);
          break;
        case 'room-list':
          handleRoomList(play, msg);
          break;
        case 'remove':
          handleLeftLobby(play);
          break;
        default:
          console.error(`no handler for lobby msg: ${msg.op}`);
          break;
      }
      break;
    case 'statistic':
      break;
    case 'conv':
      switch (msg.op) {
        case 'results':
          handleRoomList(play, msg);
          break;
        case 'started':
          handleCreateGameServer(play, msg);
          break;
        case 'added':
          handleJoinGameServer(play, msg);
          break;
        case 'random-added':
          handleJoinGameServer(play, msg);
          break;
        default:
          console.error(`no handler for lobby msg: ${msg.op}`);
          break;
      }
      break;
    case 'events':
      // TODO

      break;
    case 'error':
      handleErrorMsg(play, msg);
      break;
    default:
      if (msg.cmd) {
        console.error(`no handler for lobby msg: ${msg.cmd}`);
      }
      break;
  }
}

/**
 * 房间类
 */
class Room {
  constructor(play) {
    this._play = play;
  }

  /* eslint no-param-reassign: ["error", { "props": false }] */
  static _newFromJSONObject(play, roomJSONObject) {
    const room = new Room(play);
    room._name = roomJSONObject.cid;
    room._opened = roomJSONObject.open;
    room._visible = roomJSONObject.visible;
    room._maxPlayerCount = roomJSONObject.maxMembers;
    room._masterActorId = roomJSONObject.masterActorId;
    room._expectedUserIds = roomJSONObject.expectMembers;
    room._players = {};
    for (let i = 0; i < roomJSONObject.members.length; i += 1) {
      const playerDTO = roomJSONObject.members[i];
      const player = Player._newFromJSONObject(play, playerDTO);
      if (player.userId === play.userId) {
        play._player = player;
      }
      room._players[player.actorId] = player;
    }
    if (roomJSONObject.attr) {
      room._properties = roomJSONObject.attr;
    } else {
      room._properties = {};
    }
    return room;
  }

  /**
   * 房间名称
   * @type {string}
   * @readonly
   */
  get name() {
    return this._name;
  }

  /**
   * 房间是否开启
   * @type {boolean}
   * @readonly
   */
  get opened() {
    return this._opened;
  }

  /**
   * 房间是否可见
   * @type {boolean}
   * @readonly
   */
  get visible() {
    return this._visible;
  }

  /**
   * 房间允许的最大玩家数量
   * @type {number}
   * @readonly
   */
  get maxPlayerCount() {
    return this._maxPlayerCount;
  }

  /**
   * 房间主机玩家 ID
   * @type {number}
   * @readonly
   */
  get masterId() {
    return this._masterActorId;
  }

  /**
   * 邀请的好友 ID 列表
   * @type {Array.<string>}
   * @readonly
   */
  get expectedUserIds() {
    return this._expectedUserIds;
  }

  /**
   * 根据 actorId 获取 Player 对象
   * @param {number} actorId
   * @return {Player}
   */
  getPlayer(actorId) {
    if (!(typeof actorId === 'number')) {
      throw new TypeError(`${actorId} is not a number`);
    }
    const player = this._players[actorId];
    if (player === null) {
      throw new TypeError(`player with id:${actorId} not found`);
    }
    return player;
  }

  /**
   * 获取房间内的玩家列表
   * @return {Array.<Player>}
   * @readonly
   */
  get playerList() {
    return Object.values(this._players);
  }

  /**
   * 设置房间的自定义属性
   * @param {Object} properties 自定义属性
   * @param {Object} opts 设置选项
   * @param {Object} opts.expectedValues 期望属性，用于 CAS 检测
   */
  setCustomProperties(properties, { expectedValues = null } = {}) {
    this._play._setRoomCustomProperties(properties, expectedValues);
  }

  /**
   * 获取自定义属性
   * @return {Object}
   */
  getCustomProperties() {
    return this._properties;
  }

  _addPlayer(newPlayer) {
    if (!(newPlayer instanceof Player)) {
      throw new TypeError(`${newPlayer} is not a Player`);
    }
    this._players[newPlayer.actorId] = newPlayer;
  }

  _removePlayer(actorId) {
    delete this._players[actorId];
  }

  _mergeProperties(changedProperties) {
    this._properties = Object.assign(this._properties, changedProperties);
  }
}

const debug$1 = d('Play:GameHandler');

// 连接建立后创建 / 加入房间
function handleGameServerSessionOpen(play) {
  // 根据缓存加入房间的规则
  play._cachedRoomMsg.i = play._getMsgId();
  play._send(play._cachedRoomMsg);
}

// 创建房间
function handleCreatedRoom(play, msg) {
  if (msg.reasonCode) {
    play.emit(Event.ROOM_CREATE_FAILED, {
      code: msg.reasonCode,
      detail: msg.detail,
    });
  } else {
    play._room = Room._newFromJSONObject(play, msg);
    play.emit(Event.ROOM_CREATED);
    play.emit(Event.ROOM_JOINED);
  }
}

// 加入房间
function handleJoinedRoom(play, msg) {
  if (msg.reasonCode) {
    play.emit(Event.ROOM_JOIN_FAILED, {
      code: msg.reasonCode,
      detail: msg.detail,
    });
  } else {
    play._room = Room._newFromJSONObject(play, msg);
    play.emit(Event.ROOM_JOINED);
  }
}

// 有新玩家加入房间
function handleNewPlayerJoinedRoom(play, msg) {
  const newPlayer = Player._newFromJSONObject(play, msg.member);
  play._room._addPlayer(newPlayer);
  play.emit(Event.PLAYER_ROOM_JOINED, {
    newPlayer,
  });
}

// 有玩家离开房间
function handlePlayerLeftRoom(play, msg) {
  const actorId = msg.initByActor;
  const leftPlayer = play._room.getPlayer(actorId);
  play._room._removePlayer(actorId);
  play.emit(Event.PLAYER_ROOM_LEFT, {
    leftPlayer,
  });
}

// 主机切换应答
function handleMasterUpdated(msg) {
  if (msg.reasonCode) {
    console.error(`set master error: ${msg.reasonCode}, ${msg.detail}`);
  }
}

// 主机切换
function handleMasterChanged(play, msg) {
  play._room._masterActorId = msg.masterActorId;
  const newMaster = play._room.getPlayer(msg.masterActorId);
  play.emit(Event.MASTER_SWITCHED, {
    newMaster,
  });
}

// 房间开启 / 关闭
function handleRoomOpenedChanged(play, msg) {
  play._room._opened = msg.toggle;
}

// 房间是否可见
function handleRoomVisibleChanged(play, msg) {
  play._room._visible = msg.toggle;
}

// 房间属性变更应答
function handleRoomCustomPropertiesChangedResponse(msg) {
  if (msg.reasonCode) {
    console.error(
      `set room properties error: ${msg.reasonCode}, ${msg.detail}`
    );
  }
}

// 房间属性变更
function handleRoomCustomPropertiesChanged(play, msg) {
  const changedProps = msg.attr;
  play._room._mergeProperties(changedProps);
  play.emit(Event.ROOM_CUSTOM_PROPERTIES_CHANGED, {
    changedProps,
  });
}

// 玩家属性变更
function handlePlayerCustomPropertiesChanged(play, msg) {
  const player = play._room.getPlayer(msg.actorId);
  player._mergeProperties(msg.attr);
  play.emit(Event.PLAYER_CUSTOM_PROPERTIES_CHANGED, {
    player,
    changedProps: msg.attr,
  });
}

// 玩家下线
function handlePlayerOffline(play, msg) {
  const player = play._room.getPlayer(msg.initByActor);
  player._setActive(false);
  play.emit(Event.PLAYER_ACTIVITY_CHANGED, {
    player,
  });
}

// 玩家上线
function handlePlayerOnline(play, msg) {
  const player = play._room.getPlayer(msg.member.actorId);
  player._initWithJSONObject(msg.member);
  player._setActive(true);
  play.emit(Event.PLAYER_ACTIVITY_CHANGED, {
    player,
  });
}

// 离开房间
/* eslint no-param-reassign: ["error", { "props": false }] */
function handleLeaveRoom(play) {
  // 清理工作
  play._room = null;
  play._player = null;
  play.emit(Event.ROOM_LEFT);
  play._connectToMaster();
}

// 自定义事件
function handleEvent(play, msg) {
  play.emit(Event.CUSTOM_EVENT, {
    eventId: msg.eventId,
    eventData: msg.msg,
    senderId: msg.fromActorId,
  });
}

function handleGameMsg(play, message) {
  const msg = JSON.parse(message.data);
  debug$1(`${play.userId} Game msg: ${msg.op} <- ${message.data}`);
  switch (msg.cmd) {
    case 'session':
      switch (msg.op) {
        case 'opened':
          handleGameServerSessionOpen(play);
          break;
        default:
          console.error(`no handler for op: ${msg.op}`);
          break;
      }
      break;
    case 'conv':
      switch (msg.op) {
        case 'started':
          handleCreatedRoom(play, msg);
          break;
        case 'added':
          handleJoinedRoom(play, msg);
          break;
        case 'members-joined':
          handleNewPlayerJoinedRoom(play, msg);
          break;
        case 'members-left':
          handlePlayerLeftRoom(play, msg);
          break;
        case 'master-client-updated':
          handleMasterUpdated(msg);
          break;
        case 'master-client-changed':
          handleMasterChanged(play, msg);
          break;
        case 'open':
          handleRoomOpenedChanged(play, msg);
          break;
        case 'visible':
          handleRoomVisibleChanged(play, msg);
          break;
        case 'updated':
          handleRoomCustomPropertiesChangedResponse(msg);
          break;
        case 'updated-notify':
          handleRoomCustomPropertiesChanged(play, msg);
          break;
        case 'player-prop-updated':
          break;
        case 'player-props':
          handlePlayerCustomPropertiesChanged(play, msg);
          break;
        case 'members-offline':
          handlePlayerOffline(play, msg);
          break;
        case 'members-online':
          handlePlayerOnline(play, msg);
          break;
        case 'removed':
          handleLeaveRoom(play);
          break;
        case 'direct':
          handleEvent(play, msg);
          break;
        default:
          console.error(`no handler for game msg: ${msg.op}`);
          break;
      }
      break;
    case 'direct':
      handleEvent(play, msg);
      break;
    case 'ack':
      // ignore
      break;
    case 'events':
      // TODO

      break;
    case 'error':
      handleErrorMsg(play, msg);
      break;
    default:
      if (msg.cmd) {
        console.error(`no handler for cmd: ${message.data}`);
      }
      break;
  }
}

var version = "0.11.4";

// SDK 版本号
const NorthCNServerURL =
  'https://game-router-cn-n1.leancloud.cn/v1/router';
const EastCNServerURL =
  'https://game-router-cn-e1.leancloud.cn/v1/router';
const USServerURL = 'https://game-router-us-w1.leancloud.cn/v1/router';

const debug$2 = d('Play:Play');

const MAX_PLAYER_COUNT = 10;

function convertRoomOptions(roomOptions) {
  const options = {};
  if (!roomOptions.opened) options.open = roomOptions.opened;
  if (!roomOptions.visible) options.visible = roomOptions.visible;
  if (roomOptions.emptyRoomTtl > 0)
    options.emptyRoomTtl = roomOptions.emptyRoomTtl;
  if (roomOptions.playerTtl > 0) options.playerTtl = roomOptions.playerTtl;
  if (
    roomOptions.maxPlayerCount > 0 &&
    roomOptions.maxPlayerCount < MAX_PLAYER_COUNT
  )
    options.maxMembers = roomOptions.maxPlayerCount;
  if (roomOptions.customRoomProperties)
    options.attr = roomOptions.customRoomProperties;
  if (roomOptions.customRoomPropertyKeysForLobby)
    options.lobbyAttrKeys = roomOptions.customRoomPropertyKeysForLobby;
  if (roomOptions.flag) options.flag = roomOptions.flag;
  return options;
}

/**
 * Play 客户端类
 */
class Play extends EventEmitter {
  constructor() {
    super();
    /**
     * 玩家 ID
     * @type {string}
     */
    this.userId = null;
    this._room = null;
    this._player = null;
  }

  /**
   * 初始化客户端
   * @param {Object} opts
   * @param {string} opts.appId APP ID
   * @param {string} opts.appKey APP KEY
   * @param {number} opts.region 节点地区
   */
  init(opts) {
    if (!(typeof opts.appId === 'string')) {
      throw new TypeError(`${opts.appId} is not a string`);
    }
    if (!(typeof opts.appKey === 'string')) {
      throw new TypeError(`${opts.appKey} is not a string`);
    }
    if (!(typeof opts.region === 'number')) {
      throw new TypeError(`${opts.region} is not a number`);
    }
    this._appId = opts.appId;
    this._appKey = opts.appKey;
    this._region = opts.region;
    if (opts.autoJoinLobby === undefined) {
      this._autoJoinLobby = true;
    } else {
      this._autoJoinLobby = opts.autoJoinLobby;
    }
    this._masterServer = null;
    this._gameServer = null;
    this._msgId = 0;
    this._requestMsg = {};
    // 切换服务器状态
    this._switchingServer = false;
    // 是否处于大厅
    this._inLobby = false;
    // 大厅房间列表
    this._lobbyRoomList = null;
    // 连接失败次数
    this._connectFailedCount = 0;
    // 下次允许的连接时间戳
    this._nextConnectTimestamp = 0;
    // 连接计时器
    this._connectTimer = null;
  }

  /**
   * 建立连接
   * @param {Object} opts （可选）连接选项
   * @param {string} opts.gameVersion （可选）游戏版本号，不同的游戏版本号将路由到不同的服务端，默认值为 0.0.1
   */
  connect({ gameVersion = '0.0.1' } = {}) {
    // 判断是否有 userId
    if (this.userId === null) {
      throw new Error('userId is null');
    }
    // 判断是否已经在等待连接
    if (this._connectTimer) {
      console.warn('waiting for connect');
      return;
    }

    // 判断连接时间
    const now = new Date().getTime();
    if (now < this._nextConnectTimestamp) {
      const waitTime = this._nextConnectTimestamp - now;
      debug$2(`wait time: ${waitTime}`);
      this._connectTimer = setTimeout(() => {
        this._connect(gameVersion);
        clearTimeout(this._connectTimer);
        this._connectTimer = null;
      }, waitTime);
    } else {
      this._connect(gameVersion);
    }
  }

  _connect(gameVersion) {
    if (gameVersion && !(typeof gameVersion === 'string')) {
      throw new TypeError(`${gameVersion} is not a string`);
    }
    this._gameVersion = gameVersion;
    let masterURL = EastCNServerURL;
    if (this._region === Region.NorthChina) {
      masterURL = NorthCNServerURL;
    } else if (this._region === Region.EastChina) {
      masterURL = EastCNServerURL;
    } else if (this._region === Region.NorthAmerica) {
      masterURL = USServerURL;
    }

    request
      .get(masterURL)
      .query({ appId: this._appId, secure: true, ua: this._getUA() })
      .end((error, response) => {
        if (error) {
          console.error(error);
          // 连接失败，则增加下次连接时间间隔
          this._connectFailedCount += 1;
          this._nextConnectTimestamp =
            Date.now() + 2 ** this._connectFailedCount * 1000;
          this.emit(Event.CONNECT_FAILED, {
            code: -1,
            detail: 'Game router connect failed',
          });
        } else {
          const body = JSON.parse(response.text);
          debug$2(body);
          // 重置下次允许的连接时间
          this._connectFailedCount = 0;
          this._nextConnectTimestamp = 0;
          clearTimeout(this._connectTimer);
          this._connectTimer = null;
          // 主大厅服务器
          this._primaryServer = body.server;
          // 备用大厅服务器
          this._secondaryServer = body.secondary;
          // 默认服务器是 master server
          this._masterServer = this._primaryServer;
          // ttl
          this._serverValidTimeStamp = Date.now() + body.ttl * 1000;
          this._connectToMaster();
        }
      });
  }

  /**
   * 重新连接
   */
  reconnect() {
    const now = Date.now();
    if (now > this._serverValidTimeStamp) {
      // 超出 ttl 后将重新请求 router 连接
      this.connect(this._gameVersion);
    } else {
      this._connectToMaster();
    }
  }

  /**
   * 重新连接并自动加入房间
   */
  reconnectAndRejoin() {
    this._cachedRoomMsg = {
      cmd: 'conv',
      op: 'add',
      i: this._getMsgId(),
      cid: this._cachedRoomMsg.cid,
      rejoin: true,
    };
    this._connectToGame();
  }

  /**
   * 断开连接
   */
  disconnect() {
    this._stopKeepAlive();
    if (this._websocket) {
      this._websocket.close();
      this._websocket = null;
    }
    debug$2(`${this.userId} disconnect.`);
  }

  /**
   * 加入大厅，只有在 autoJoinLobby = false 时才需要调用
   */
  joinLobby() {
    const msg = {
      cmd: 'lobby',
      op: 'add',
      i: this._getMsgId(),
    };
    this._send(msg);
  }

  /**
   * 离开大厅
   */
  leaveLobby() {
    const msg = {
      cmd: 'lobby',
      op: 'remove',
      i: this._getMsgId(),
    };
    this._send(msg);
  }

  /**
   * 创建房间
   * @param {Object} opts （可选）创建房间选项
   * @param {string} opts.roomName 房间名称，在整个游戏中唯一，默认值为 null，则由服务端分配一个唯一 Id
   * @param {Object} opts.roomOptions （可选）创建房间选项，默认值为 null
   * @param {Array.<string>} opts.expectedUserIds （可选）邀请好友 ID 数组，默认值为 null
   */
  createRoom({
    roomName = null,
    roomOptions = null,
    expectedUserIds = null,
  } = {}) {
    if (roomName !== null && !(typeof roomName === 'string')) {
      throw new TypeError(`${roomName} is not a string`);
    }
    if (roomOptions !== null && !(roomOptions instanceof Object)) {
      throw new TypeError(`${roomOptions} is not a Object`);
    }
    if (expectedUserIds !== null && !Array.isArray(expectedUserIds)) {
      throw new TypeError(`${expectedUserIds} is not an Array with string`);
    }
    // 缓存 GameServer 创建房间的消息体
    this._cachedRoomMsg = {
      cmd: 'conv',
      op: 'start',
      i: this._getMsgId(),
    };
    if (roomName) {
      this._cachedRoomMsg.cid = roomName;
    }
    // 拷贝房间属性（包括 系统属性和玩家定义属性）
    if (roomOptions) {
      const opts = convertRoomOptions(roomOptions);
      this._cachedRoomMsg = Object.assign(this._cachedRoomMsg, opts);
    }
    if (expectedUserIds) {
      this._cachedRoomMsg.expectMembers = expectedUserIds;
    }
    // Router 创建房间的消息体
    const msg = this._cachedRoomMsg;
    this._send(msg);
  }

  /**
   * 加入房间
   * @param {string} roomName 房间名称
   * @param {*} expectedUserIds （可选）邀请好友 ID 数组，默认值为 null
   */
  joinRoom(roomName, { expectedUserIds = null } = {}) {
    if (!(typeof roomName === 'string')) {
      throw new TypeError(`${roomName} is not a string`);
    }
    if (expectedUserIds !== null && !Array.isArray(expectedUserIds)) {
      throw new TypeError(`${expectedUserIds} is not an array with string`);
    }
    // 加入房间的消息体
    this._cachedRoomMsg = {
      cmd: 'conv',
      op: 'add',
      i: this._getMsgId(),
      cid: roomName,
    };
    if (expectedUserIds) {
      this._cachedRoomMsg.expectMembers = expectedUserIds;
    }
    const msg = this._cachedRoomMsg;
    this._send(msg);
  }

  /**
   * 重新加入房间
   * @param {string} roomName 房间名称
   */
  rejoinRoom(roomName) {
    if (!(typeof roomName === 'string')) {
      throw new TypeError(`${roomName} is not a string`);
    }
    this._cachedRoomMsg = {
      cmd: 'conv',
      op: 'add',
      i: this._getMsgId(),
      cid: roomName,
      rejoin: true,
    };
    const msg = this._cachedRoomMsg;
    this._send(msg);
  }

  /**
   * 随机加入或创建房间
   * @param {string} roomName 房间名称
   * @param {Object} opts （可选）创建房间选项
   * @param {Object} opts.roomOptions （可选）创建房间选项，默认值为 null
   * @param {Array.<string>} opts.expectedUserIds （可选）邀请好友 ID 数组，默认值为 null
   */
  joinOrCreateRoom(
    roomName,
    { roomOptions = null, expectedUserIds = null } = {}
  ) {
    if (!(typeof roomName === 'string')) {
      throw new TypeError(`${roomName} is not a string`);
    }
    if (roomOptions !== null && !(roomOptions instanceof Object)) {
      throw new TypeError(`${roomOptions} is not a Object`);
    }
    if (expectedUserIds !== null && !Array.isArray(expectedUserIds)) {
      throw new TypeError(`${expectedUserIds} is not an array with string`);
    }
    this._cachedRoomMsg = {
      cmd: 'conv',
      op: 'add',
      i: this._getMsgId(),
      cid: roomName,
    };
    // 拷贝房间参数
    if (roomOptions != null) {
      const opts = convertRoomOptions(roomOptions);
      this._cachedRoomMsg = Object.assign(this._cachedRoomMsg, opts);
    }
    if (expectedUserIds) {
      this._cachedRoomMsg.expectMembers = expectedUserIds;
    }
    const msg = {
      cmd: 'conv',
      op: 'add',
      i: this._getMsgId(),
      cid: roomName,
      createOnNotFound: true,
    };
    if (expectedUserIds) {
      msg.expectMembers = expectedUserIds;
    }
    this._send(msg);
  }

  /**
   * 随机加入房间
   * @param {Object} opts （可选）随机加入房间选项
   * @param {Object} opts.matchProperties （可选）匹配属性，默认值为 null
   * @param {Array.<string>} opts.expectedUserIds （可选）邀请好友 ID 数组，默认值为 null
   */
  joinRandomRoom({ matchProperties = null, expectedUserIds = null } = {}) {
    if (matchProperties !== null && !(typeof matchProperties === 'object')) {
      throw new TypeError(`${matchProperties} is not an object`);
    }
    if (expectedUserIds !== null && !Array.isArray(expectedUserIds)) {
      throw new TypeError(`${expectedUserIds} is not an array with string`);
    }
    this._cachedRoomMsg = {
      cmd: 'conv',
      op: 'add',
      i: this._getMsgId(),
    };
    if (matchProperties) {
      this._cachedRoomMsg.expectAttr = matchProperties;
    }
    if (expectedUserIds) {
      this._cachedRoomMsg.expectMembers = expectedUserIds;
    }

    const msg = {
      cmd: 'conv',
      op: 'add-random',
    };
    if (matchProperties) {
      msg.expectAttr = matchProperties;
    }
    if (expectedUserIds) {
      msg.expectMembers = expectedUserIds;
    }
    this._send(msg);
  }

  /**
   * 设置房间开启 / 关闭
   * @param {Boolean} opened 是否开启
   */
  setRoomOpened(opened) {
    if (!(typeof opened === 'boolean')) {
      throw new TypeError(`${opened} is not a boolean value`);
    }
    if (this._room === null) {
      throw new Error('room is null');
    }
    const msg = {
      cmd: 'conv',
      op: 'open',
      i: this._getMsgId(),
      toggle: opened,
    };
    this.this._send(msg);
  }

  /**
   * 设置房间可见 / 不可见
   * @param {Boolean} visible 是否可见
   */
  setRoomVisible(visible) {
    if (!(typeof visible === 'boolean')) {
      throw new TypeError(`${visible} is not a boolean value`);
    }
    if (this._room === null) {
      throw new Error('room is null');
    }
    const msg = {
      cmd: 'conv',
      op: 'visible',
      i: this._getMsgId(),
      toggle: visible,
    };
    this._send(msg);
  }

  /**
   * 设置房主
   * @param {number} newMasterId 新房主 ID
   */
  setMaster(newMasterId) {
    if (!(typeof newMasterId === 'number')) {
      throw new TypeError(`${newMasterId} is not a number`);
    }
    if (this._room === null) {
      throw new Error('room is null');
    }
    const msg = {
      cmd: 'conv',
      op: 'update-master-client',
      i: this._getMsgId(),
      masterActorId: newMasterId,
    };
    this._send(msg);
  }

  /**
   * 发送自定义消息
   * @param {number|string} eventId 事件 ID
   * @param {Object} eventData 事件参数
   * @param {Object} options 发送事件选项
   * @param {ReceiverGroup} options.receiverGroup 接收组
   * @param {Array.<number>} options.targetActorIds 接收者 Id。如果设置，将会覆盖 receiverGroup
   */
  sendEvent(eventId, eventData, options) {
    if (!(typeof eventId === 'string') && !(typeof eventId === 'number')) {
      throw new TypeError(`${eventId} is not a string or number`);
    }
    if (!(typeof eventData === 'object')) {
      throw new TypeError(`${eventData} is not an object`);
    }
    if (!(options instanceof Object)) {
      throw new TypeError(`${options} is not a Object`);
    }
    if (this._room === null) {
      throw new Error('room is null');
    }
    if (this._player === null) {
      throw new Error('player is null');
    }
    const msg = {
      cmd: 'direct',
      i: this._getMsgId(),
      eventId,
      msg: eventData,
      receiverGroup: options.receiverGroup,
      toActorIds: options.targetActorIds,
    };
    this._send(msg);
  }

  /**
   * 离开房间
   */
  leaveRoom() {
    const msg = {
      cmd: 'conv',
      op: 'remove',
      i: this._getMsgId(),
      cid: this.room.name,
    };
    this._send(msg);
  }

  /**
   * 获取当前所在房间
   * @return {Room}
   * @readonly
   */
  get room() {
    return this._room;
  }

  /**
   * 获取当前玩家
   * @return {Player}
   * @readonly
   */
  get player() {
    return this._player;
  }

  /**
   * 获取房间列表
   * @return {Array.<LobbyRoom>}
   * @readonly
   */
  get lobbyRoomList() {
    return this._lobbyRoomList;
  }

  // 设置房间属性
  _setRoomCustomProperties(properties, expectedValues) {
    if (!(typeof properties === 'object')) {
      throw new TypeError(`${properties} is not an object`);
    }
    if (expectedValues && !(typeof expectedValues === 'object')) {
      throw new TypeError(`${expectedValues} is not an object`);
    }
    const msg = {
      cmd: 'conv',
      op: 'update',
      i: this._getMsgId(),
      attr: properties,
    };
    if (expectedValues) {
      msg.expectAttr = expectedValues;
    }
    this._send(msg);
  }

  // 设置玩家属性
  _setPlayerCustomProperties(actorId, properties, expectedValues) {
    if (!(typeof actorId === 'number')) {
      throw new TypeError(`${actorId} is not a number`);
    }
    if (!(typeof properties === 'object')) {
      throw new TypeError(`${properties} is not an object`);
    }
    if (expectedValues && !(typeof expectedValues === 'object')) {
      throw new TypeError(`${expectedValues} is not an object`);
    }
    const msg = {
      cmd: 'conv',
      op: 'update-player-prop',
      i: this._getMsgId(),
      targetActorId: actorId,
      playerProperty: properties,
    };
    if (expectedValues) {
      msg.expectAttr = expectedValues;
    }
    this._send(msg);
  }

  // 开始会话，建立连接后第一条消息
  _sessionOpen() {
    const msg = {
      cmd: 'session',
      op: 'open',
      i: this._getMsgId(),
      appId: this._appId,
      peerId: this.userId,
      ua: this._getUA(),
    };
    this._send(msg);
  }

  // 发送消息
  _send(msg) {
    if (!(typeof msg === 'object')) {
      throw new TypeError(`${msg} is not an object`);
    }
    const msgData = JSON.stringify(msg);
    debug$2(`${this.userId} msg: ${msg.op} -> ${msgData}`);
    this._websocket.send(msgData);
    // 心跳包
    this._stopKeepAlive();
    this._keepAlive = setTimeout(() => {
      const keepAliveMsg = {};
      this._send(keepAliveMsg);
    }, 10000);
  }

  // 连接至大厅服务器
  _connectToMaster() {
    this._cleanup();
    this._switchingServer = true;
    this._websocket = new WebSocket(this._masterServer);
    this._websocket.onopen = () => {
      debug$2('Lobby websocket opened');
      this._switchingServer = false;
      this._sessionOpen();
    };
    this._websocket.onmessage = msg => {
      handleMasterMsg(this, msg);
    };
    this._websocket.onclose = evt => {
      debug$2(`Lobby websocket closed: ${evt.code}`);
      if (evt.code === 1006) {
        // 连接失败
        if (this._masterServer === this._secondaryServer) {
          this.emit(Event.CONNECT_FAILED, {
            code: -2,
            detail: 'Websocket connect failed',
          });
        } else {
          // 内部重连
          this._masterServer = this._secondaryServer;
          this._connectToMaster();
        }
      } else if (this._switchingServer) {
        debug$2('swiching server');
      } else {
        // 断开连接
        this.emit(Event.DISCONNECTED);
      }
    };
    this._websocket.onerror = error => {
      console.error(error);
    };
  }

  // 连接至游戏服务器
  _connectToGame() {
    this._cleanup();
    this._switchingServer = true;
    this._websocket = new WebSocket(this._gameServer);
    this._websocket.onopen = () => {
      debug$2('Game websocket opened');
      this._switchingServer = false;
      this._sessionOpen();
    };
    this._websocket.onmessage = msg => {
      handleGameMsg(this, msg);
    };
    this._websocket.onclose = evt => {
      debug$2('Game websocket closed');
      if (evt.code === 1006) {
        // 连接失败
        this.emit(Event.CONNECT_FAILED, {
          code: -2,
          detail: 'Websocket connect failed',
        });
      } else if (this._switchingServer) {
        debug$2('swiching server');
      } else {
        // 断开连接
        this.emit(Event.DISCONNECTED);
      }
      this._stopKeepAlive();
    };
    this._websocket.onerror = error => {
      console.error(error);
    };
  }

  _getMsgId() {
    this._msgId += 1;
    return this._msgId;
  }

  _stopKeepAlive() {
    if (this._keepAlive) {
      clearTimeout(this._keepAlive);
      this._keepAlive = null;
    }
  }

  _cleanup() {
    if (this._websocket) {
      this._websocket.onopen = null;
      this._websocket.onconnect = null;
      this._websocket.onmessage = null;
      this._websocket.onclose = null;
      this._websocket.close();
      this._websocket = null;
    }
  }

  _getUA() {
    return `${version}_${this._gameVersion}`;
  }
}

/**
 * 接收组枚举
 * @readonly
 * @enum {number}
 */
const ReceiverGroup = {
  /**
   * 其他人（除了自己之外的所有人）
   */
  Others: 0,
  /**
   * 所有人（包括自己）
   */
  All: 1,
  /**
   * 主机客户端
   */
  MasterClient: 2,
};

/**
 * 创建房间标识
 * @readonly
 * @enum {number}
 */
const CreateRoomFlag = {
  /**
   * Master 掉线后自动切换 Master
   */
  MasterAutoSwitch: 1,
  /**
   * 只允许 Master 设置房间属性
   */
  MasterUpdateRoomProperties: 2,
  /**
   * 只允许 Master 设置 Master
   */
  MasterSetMaster: 4,
};

const play = new Play();

exports.play = play;
exports.Play = Play;
exports.Region = Region;
exports.Room = Room;
exports.Player = Player;
exports.Event = Event;
exports.ReceiverGroup = ReceiverGroup;
exports.CreateRoomFlag = CreateRoomFlag;
//# sourceMappingURL=play-node.js.map
