'use strict';

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

const WebSocket = require('ws');
const URL = require('url');
const events = require('events');
const Debug = require('debug');
const zlib = require('zlib');
const axios = require('axios');
const util = require('util');
const fs = require('fs');
const YAML = require('yamljs');
const path = require('path');

function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }

const WebSocket__default = /*#__PURE__*/_interopDefaultLegacy(WebSocket);
const URL__default = /*#__PURE__*/_interopDefaultLegacy(URL);
const Debug__default = /*#__PURE__*/_interopDefaultLegacy(Debug);
const zlib__default = /*#__PURE__*/_interopDefaultLegacy(zlib);
const axios__default = /*#__PURE__*/_interopDefaultLegacy(axios);
const util__default = /*#__PURE__*/_interopDefaultLegacy(util);
const fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
const YAML__default = /*#__PURE__*/_interopDefaultLegacy(YAML);
const path__default = /*#__PURE__*/_interopDefaultLegacy(path);

const debug = Debug__default['default']('screepsapi:socket');

const DEFAULTS$1 = {
  reconnect: true,
  resubscribe: true,
  keepAlive: true,
  maxRetries: 10,
  maxRetryDelay: 60 * 1000 // in milli-seconds
};

class Socket extends events.EventEmitter {
  constructor (ScreepsAPI) {
    super();
    this.api = ScreepsAPI;
    this.opts = Object.assign({}, DEFAULTS$1);
    this.on('error', () => {}); // catch to prevent unhandled-exception errors
    this.reset();
    this.on('auth', ev => {
      if (ev.data.status === 'ok') {
        while (this.__queue.length) {
          this.emit(this.__queue.shift());
        }
        clearInterval(this.keepAliveInter);
        if (this.opts.keepAlive) {
          this.keepAliveInter = setInterval(() => this.ws && this.ws.ping(1), 10000);
        }
      }
    });
  }

  reset () {
    this.authed = false;
    this.connected = false;
    this.reconnecting = false;
    clearInterval(this.keepAliveInter);
    this.keepAliveInter = 0;
    this.__queue = []; // pending messages  (to send once authenticated)
    this.__subQueue = []; // pending subscriptions (to request once authenticated)
    this.__subs = {}; // number of callbacks for each subscription
  }

  async connect (opts = {}) {
    Object.assign(this.opts, opts);
    if (!this.api.token) {
      throw new Error('No token! Call api.auth() before connecting the socket!')
    }
    return new Promise((resolve, reject) => {
      const baseURL = this.api.opts.url.replace('http', 'ws');
      const wsurl = new URL.URL('socket/websocket', baseURL);
      this.ws = new WebSocket__default['default'](wsurl);
      this.ws.on('open', () => {
        this.connected = true;
        this.reconnecting = false;
        if (this.opts.resubscribe) {
          this.__subQueue.push(...Object.keys(this.__subs));
        }
        debug('connected');
        this.emit('connected');
        resolve(this.auth(this.api.token));
      });
      this.ws.on('close', () => {
        clearInterval(this.keepAliveInter);
        this.authed = false;
        this.connected = false;
        debug('disconnected');
        this.emit('disconnected');
        if (this.opts.reconnect) {
          this.reconnect().catch(() => { /* error emitted in reconnect() */ });
        }
      });
      this.ws.on('error', (err) => {
        this.ws.terminate();
        this.emit('error', err);
        debug(`error ${err}`);
        if (!this.connected) {
          reject(err);
        }
      });
      this.ws.on('unexpected-response', (req, res) => {
        const err = new Error(`WS Unexpected Response: ${res.statusCode} ${res.statusMessage}`);
        this.emit('error', err);
        reject(err);
      });
      this.ws.on('message', (data) => this.handleMessage(data));
    })
  }

  async reconnect () {
    if (this.reconnecting) {
      return
    }
    this.reconnecting = true;
    let retries = 0;
    let retry;
    do {
      let time = Math.pow(2, retries) * 100;
      if (time > this.opts.maxRetryDelay) time = this.opts.maxRetryDelay;
      await this.sleep(time);
      if (!this.reconnecting) return // reset() called in-between
      try {
        await this.connect();
        retry = false;
      } catch (err) {
        retry = true;
      }
      retries++;
      debug(`reconnect ${retries}/${this.opts.maxRetries}`);
    } while (retry && retries < this.opts.maxRetries)
    if (retry) {
      const err = new Error(`Reconnection failed after ${this.opts.maxRetries} retries`);
      this.reconnecting = false;
      debug('reconnect failed');
      this.emit('error', err);
      throw err
    } else {
      // Resume existing subscriptions on the new socket
      Object.keys(this.__subs).forEach(sub => this.subscribe(sub));
    }
  }

  disconnect () {
    debug('disconnect');
    clearInterval(this.keepAliveInter);
    this.ws.removeAllListeners(); // remove listeners first or we may trigger reconnection & Co.
    this.ws.terminate();
    this.reset();
    this.emit('disconnected');
  }

  sleep (time) {
    return new Promise((resolve, reject) => {
      setTimeout(resolve, time);
    })
  }

  handleMessage (msg) {
    msg = msg.data || msg; // Handle ws/browser difference
    if (msg.slice(0, 3) === 'gz:') { msg = this.api.inflate(msg); }
    debug(`message ${msg}`);
    if (msg[0] === '[') {
      msg = JSON.parse(msg);
      let [, type, id, channel] = msg[0].match(/^(.+):(.+?)(?:\/(.+))?$/);
      channel = channel || type;
      const event = { channel, id, type, data: msg[1] };
      this.emit(msg[0], event);
      this.emit(event.channel, event);
      this.emit('message', event);
    } else {
      const [channel, ...data] = msg.split(' ');
      const event = { type: 'server', channel, data };
      if (channel === 'auth') { event.data = { status: data[0], token: data[1] }; }
      if (['protocol', 'time', 'package'].includes(channel)) { event.data = { [channel]: data[0] }; }
      this.emit(channel, event);
      this.emit('message', event);
    }
  }

  async gzip (bool) {
    this.send(`gzip ${bool ? 'on' : 'off'}`);
  }

  async send (data) {
    if (!this.connected) {
      this.__queue.push(data);
    } else {
      this.ws.send(data);
    }
  }

  auth (token) {
    return new Promise((resolve, reject) => {
      this.send(`auth ${token}`);
      this.once('auth', (ev) => {
        const { data } = ev;
        if (data.status === 'ok') {
          this.authed = true;
          this.emit('token', data.token);
          this.emit('authed');
          while (this.__subQueue.length) {
            this.send(this.__subQueue.shift());
          }
          resolve();
        } else {
          reject(new Error('socket auth failed'));
        }
      });
    })
  }

  async subscribe (path, cb) {
    if (!path) return
    const userID = await this.api.userID();
    if (!path.match(/^(\w+):(.+?)$/)) { path = `user:${userID}/${path}`; }
    if (this.authed) {
      this.send(`subscribe ${path}`);
    } else {
      this.__subQueue.push(`subscribe ${path}`);
    }
    this.emit('subscribe', path);
    this.__subs[path] = this.__subs[path] || 0;
    this.__subs[path]++;
    if (cb) this.on(path, cb);
  }

  async unsubscribe (path) {
    if (!path) return
    const userID = await this.api.userID();
    if (!path.match(/^(\w+):(.+?)$/)) { path = `user:${userID}/${path}`; }
    this.send(`unsubscribe ${path}`);
    this.emit('unsubscribe', path);
    if (this.__subs[path]) this.__subs[path]--;
  }
}

const debugHttp = Debug__default['default']('screepsapi:http');
const debugRateLimit = Debug__default['default']('screepsapi:ratelimit');

const { format } = URL__default['default'];

const gunzipAsync = util__default['default'].promisify(zlib__default['default'].gunzip);
const inflateAsync = util__default['default'].promisify(zlib__default['default'].inflate);

const DEFAULT_SHARD = 'shard0';
const OFFICIAL_HISTORY_INTERVAL = 100;
const PRIVATE_HISTORY_INTERVAL = 20;

const sleep = ms => new Promise(resolve => setInterval(resolve, ms));

class RawAPI extends events.EventEmitter {
  constructor (opts = {}) {
    super();
    this.setServer(opts);
    const self = this;
    this.raw = {

      /**
       * GET /api/version
       * @returns {{
       *  ok:1, package:number, protocol: number,
       *  serverData: {
       *    customObjectTypes,
       *    historyChunkSize:number,
       *    features,
       *    shards: string[]
       *  },
       *  users:number
       * }}
       */
      version () {
        return self.req('GET', '/api/version')
      },
      /**
       * GET /api/authmod
       * @returns {Object}
       */
      authmod () {
        if (self.isOfficialServer()) {
          return Promise.resolve({ name: 'official' })
        }
        return self.req('GET', '/api/authmod')
      },
      /**
       * Official:
       * GET /room-history/${shard}/${room}/${tick}.json
       * Private:
       * GET /room-history
       * @param {string} room
       * @param {number} tick
       * @param {string} shard
       * @returns {Object} A json file with history data
       */
      history (room, tick, shard = DEFAULT_SHARD) {
        if (self.isOfficialServer()) {
          tick -= tick % OFFICIAL_HISTORY_INTERVAL;
          return self.req('GET', `/room-history/${shard}/${room}/${tick}.json`)
        } else {
          tick -= tick % PRIVATE_HISTORY_INTERVAL;
          return self.req('GET', '/room-history', { room, time: tick })
        }
      },
      servers: {
        /**
         * POST /api/servers/list
         * A list of community servers
         * @returns {{
         *  ok:number,
         *  servers:{
         *   _id:string,
         *   settings:{
         *     host:string,
         *     port:string,
         *     pass:string
         *   },
         *   name:string,
         *   status:"active"|string
         *   likeCount:number
         *  }[]
         * }}
         */
        list () {
          return self.req('POST', '/api/servers/list', {})
        }
      },
      auth: {
        /**
         * POST /api/auth/signin
         * @param {string} email
         * @param {string} password
         * @returns {{ok:number, token:string}}
         */
        signin (email, password) {
          return self.req('POST', '/api/auth/signin', { email, password })
        },
        /**
         * POST /api/auth/steam-ticket
         * @param {*} ticket
         * @param {*} useNativeAuth
         * @returns {Object}
         */
        steamTicket (ticket, useNativeAuth = false) {
          return self.req('POST', '/api/auth/steam-ticket', { ticket, useNativeAuth })
        },
        /**
         * GET /api/auth/me
         * @returns {{
         *  ok: number;
         *  _id: string;
         *  email: string;
         *  username: string;
         *  cpu: number;
         *  badge: Badge;
         *  password: string;
         *  notifyPrefs: { sendOnline: any; errorsInterval: any; disabledOnMessages: any; disabled: any; interval: any };
         *  gcl: number;
         *  credits: number;
         *  lastChargeTime: any;
         *  lastTweetTime: any;
         *  github: { id: any; username: any };
         *  twitter: { username: string; followers_count: number };
         *}}
         */
        me () {
          return self.req('GET', '/api/auth/me')
        },
        /**
         * GET /api/auth/query-token
         * @param {string} token
         * @returns {Object}
         */
        queryToken (token) {
          return self.req('GET', '/api/auth/query-token', { token })
        }
      },
      register: {
        /**
         * GET /api/register/check-email
         * @param {string} email
         * @returns {Object}
         */
        checkEmail (email) {
          return self.req('GET', '/api/register/check-email', { email })
        },
        /**
         * GET /api/register/check-username
         * @param {string} username
         * @returns  {Object}
         */
        checkUsername (username) {
          return self.req('GET', '/api/register/check-username', { username })
        },
        /**
         * POST /api/register/set-username
         * @param {string} username
         * @returns {Object}
         */
        setUsername (username) {
          return self.req('POST', '/api/register/set-username', { username })
        },
        /**
         * POST /api/register/submit
         * @param {string} username
         * @param {string} email
         * @param {string} password
         * @param {*} modules
         * @returns {Object}
         */
        submit (username, email, password, modules) {
          return self.req('POST', '/api/register/submit', { username, email, password, modules })
        }
      },
      userMessages: {
        /**
         * GET /api/user/messages/list?respondent={userId}
         * @param {string} respondent the long `_id` of the user, not the username
         * @returns {{ ok, messages: [ { _id, date, type, text, unread } ] }}
         */
        list (respondent) {
          return self.req('GET', '/api/user/messages/list', { respondent })
        },
        /**
         * GET /api/user/messages/index
         * @returns {{ ok, messages: [ { _id, message: { _id, user, respondent, date, type, text, unread } } ], users: { <user's _id>: { _id, username, badge: Badge } } }}
         */
        index () {
          return self.req('GET', '/api/user/messages/index')
        },
        /**
         * GET /api/user/messages/unread-count
         * @returns {{ ok, count:number }}
         */
        unreadCount () {
          return self.req('GET', '/api/user/messages/unread-count')
        },
        /**
         * POST /api/user/messages/send
         * @param {string} respondent the long `_id` of the user, not the username
         * @param {string} text
         * @returns {{ ok }}
         */
        send (respondent, text) {
          return self.req('POST', '/api/user/messages/send', { respondent, text })
        },
        /**
         * POST /api/user/messages/mark-read
         * @param {string} id
         * @returns {Object}
         */
        markRead (id) {
          return self.req('POST', '/api/user/messages/mark-read', { id })
        }
      },
      game: {
        /**
         * @typedef {"creepsLost"|"creepsProduced"|"energyConstruction"|"energyControl"|"energyCreeps"|"energyHarvested"} stat
         * @param {string[]} rooms An array of room names
         * @param {"owner0"|"claim0"|stat} statName
         * @param {string} shard
         * @returns {{
         *  ok:number,
         *  stats: {
         *    [roomName:string]: {
         *      status,
         *      novice,
         *      own: { user, level },
         *      <stat>: [ { user, value }]
         *    }
         *  }
         * , users: { [userId:string]: { _id, username, badge: Badge } } }}
         * The return type is not mapped correctly
         */
        mapStats (rooms, statName, shard = DEFAULT_SHARD) {
          return self.req('POST', '/api/game/map-stats', { rooms, statName, shard })
        },
        /**
         * POST /api/game/gen-unique-object-name
         * @param {"flag"|"spawn"|string} type can be at least "flag" or "spawn"
         * @param {string} shard
         * @returns { ok, name:string }
         */
        genUniqueObjectName (type, shard = DEFAULT_SHARD) {
          return self.req('POST', '/api/game/gen-unique-object-name', { type, shard })
        },
        /**
         * POST /api/game/check-unique-object-name
         * @param {string} type
         * @param {string} name
         * @param {string} shard
         * @returns {Object}
         */
        checkUniqueObjectName (type, name, shard = DEFAULT_SHARD) {
          return self.req('POST', '/api/game/check-unique-object-name', { type, name, shard })
        },
        /**
         * @param {string} room
         * @param {number} x
         * @param {number} y
         * @param {string} name
         * @param {string?} shard
         */
        placeSpawn (room, x, y, name, shard = DEFAULT_SHARD) {
          return self.req('POST', '/api/game/place-spawn', { name, room, x, y, shard })
        },
        /**
         * POST /api/game/create-flag
         * @param {string} room
         * @param {number} x
         * @param {number} y
         * @param {string} name
         * @param {FlagColor} color
         * @param {FlagColor} secondaryColor
         * @param {string} shard
         * @returns {{ ok, result: { nModified, ok, upserted: [ { index, _id } ], n }, connection: { host, id, port } }}
         * - if the name is new, result.upserted[0]._id is the game id of the created flag
         * - if not, this moves the flag and the response does not contain the id (but the id doesn't change)
         * - `connection` looks like some internal MongoDB thing that is irrelevant to us
         */
        createFlag (room, x, y, name, color = 1, secondaryColor = 1, shard = DEFAULT_SHARD) {
          return self.req('POST', '/api/game/create-flag', { name, room, x, y, color, secondaryColor, shard })
        },
        /**
         * POST/api/game/gen-unique-flag-name
         * @param {string} shard
         * @returns {Object}
         */
        genUniqueFlagName (shard = DEFAULT_SHARD) {
          return self.req('POST', '/api/game/gen-unique-flag-name', { shard })
        },
        /**
         * POST /api/game/check-unique-flag-name
         * @param {string} name
         * @param {string} shard
         * @returns {Object}
         */
        checkUniqueFlagName (name, shard = DEFAULT_SHARD) {
          return self.req('POST', '/api/game/check-unique-flag-name', { name, shard })
        },
        /**
         * POST /api/game/change-flag-color
         * @param {FlagColor} color
         * @param {FlagColor} secondaryColor
         * @param {string} shard
         * @returns {{ ok, result: { nModified, ok, n }, connection: { host, id, port } }}
         */
        changeFlagColor (color = 1, secondaryColor = 1, shard = DEFAULT_SHARD) {
          return self.req('POST', '/api/game/change-flag-color', { color, secondaryColor, shard })
        },
        /**
         * POST /api/game/remove-flag
         * @param {string} room
         * @param {string} name
         * @param {string} shard
         * @returns {Object}
         */
        removeFlag (room, name, shard = DEFAULT_SHARD) {
          return self.req('POST', '/api/game/remove-flag', { name, room, shard })
        },
        /**
         * POST /api/game/add-object-intent
         * [Missing parameter] _id is the game id of the object to affect (except for destroying structures), room is the name of the room it's in
         * this method is used for a variety of actions, depending on the `name` and `intent` parameters
         * @example remove flag: name = "remove", intent = {}
         * @example destroy structure: _id = "room", name = "destroyStructure", intent = [ {id: <structure id>, roomName, <room name>, user: <user id>} ]
can destroy multiple structures at once
         * @example suicide creep: name = "suicide", intent = {id: <creep id>}
         * @example unclaim controller: name = "unclaim", intent = {id: <controller id>}
intent can be an empty object for suicide and unclaim, but the web interface sends the id in it, as described
         * @example remove construction site: name = "remove", intent = {}
         * @param {string} room
         * @param {string} name
         * @param {string} intent
         * @param {string} shard
         * @returns {{ ok, result: { nModified, ok, upserted: [ { index, _id } ], n }, connection: { host, id, port } }}
         */
        addObjectIntent (room, name, intent, shard = DEFAULT_SHARD) {
          return self.req('POST', '/api/game/add-object-intent', { room, name, intent, shard })
        },
        /**
         * POST /api/game/create-construction
         * @param {string} room
         * @param {number} x
         * @param {number} y
         * @param {string} structureType the same value as one of the in-game STRUCTURE_* constants ('road', 'spawn', etc.)
         * @param {string} name
         * @param {string} shard
         * @returns {{ ok, result: { ok, n }, ops: [ { type, room, x, y, structureType, user, progress, progressTotal, _id } ], insertedCount, insertedIds }}
         */
        createConstruction (room, x, y, structureType, name, shard = DEFAULT_SHARD) {
          return self.req('POST', '/api/game/create-construction', { room, x, y, structureType, name, shard })
        },
        /**
         * POST /api/game/set-notify-when-attacked
         * @param {string} _id
         * @param {bool} enabled is either true or false (literal values, not strings)
         * @param {string} shard
         * @returns {{ ok, result: { ok, nModified, n }, connection: { id, host, port } }}
         */
        setNotifyWhenAttacked (_id, enabled = true, shard = DEFAULT_SHARD) {
          return self.req('POST', '/api/game/set-notify-when-attacked', { _id, enabled, shard })
        },
        /**
         * POST /api/game/create-invader
         * @param {string} room
         * @param {number} x
         * @param {number} y
         * @param {*} size
         * @param {*} type
         * @param {boolean} boosted
         * @param {string} shard
         * @returns {Object}
         */
        createInvader (room, x, y, size, type, boosted = false, shard = DEFAULT_SHARD) {
          return self.req('POST', '/api/game/create-invader', { room, x, y, size, type, boosted, shard })
        },
        /**
         * POST /api/game/remove-invader
         * @param {string} _id
         * @param {string} shard
         * @returns {Object}
         */
        removeInvader (_id, shard = DEFAULT_SHARD) {
          return self.req('POST', '/api/game/remove-invader', { _id, shard })
        },
        /**
         * GET /api/game/time
         * @param {string} shard
         * @returns {{ ok:number, time:number }}
         */
        time (shard = DEFAULT_SHARD) {
          return self.req('GET', '/api/game/time', { shard })
        },
        /**
         * GET /api/game/world-size
         * @param {string} shard
         * @returns {Object}
         */
        worldSize (shard = DEFAULT_SHARD) {
          return self.req('GET', '/api/game/world-size', { shard })
        },
        /**
         * GET /api/game/room-decorations
         * @param {string} room
         * @param {string} shard
         * @returns {Object}
         */
        roomDecorations (room, shard = DEFAULT_SHARD) {
          return self.req('GET', '/api/game/room-decorations', { room, shard })
        },
        /**
         * GET /api/game/room-objects
         * @param {string} room
         * @param {string} shard
         * @returns {Object}
         */
        roomObjects (room, shard = DEFAULT_SHARD) {
          return self.req('GET', '/api/game/room-objects', { room, shard })
        },
        /**
         * @param {string} room
         * @param {*} encoded can be anything non-empty
         * @param {string} shard
         * @returns {{ ok, terrain: [ { room:string, x:number, y:number, type:"wall"|"swamp" } ] }
         * | { ok, terrain: [ { _id,room:string, terrain:string, type:"wall"|"swamp" } ] }}
         * terrain is a string of digits, giving the terrain left-to-right and top-to-bottom
         * 0: plain, 1: wall, 2: swamp, 3: also wall
         */
        roomTerrain (room, encoded = 1, shard = DEFAULT_SHARD) {
          return self.req('GET', '/api/game/room-terrain', { room, encoded, shard })
        },
        /**
         * @param {string} room
         * @param {string} shard
         * @returns {{ _id, status:"normal"|"out of borders"|string, novice:string }}
         * `status` can at least be "normal" or "out of borders"
         * if the room is in a novice area, novice will contain the Unix timestamp of the end of the protection (otherwise it is absent)
         */
        roomStatus (room, shard = DEFAULT_SHARD) {
          return self.req('GET', '/api/game/room-status', { room, shard })
        },
        /**
         * GET /api/game/room-overview
         * @param {string} room
         * @param {number} interval
         * @param {string} shard
         * @returns {Object}
         */
        roomOverview (room, interval = 8, shard = DEFAULT_SHARD) {
          return self.req('GET', '/api/game/room-overview', { room, interval, shard })
        },
        market: {
          /**
           * GET /api/game/market/orders-index
           * @param {string} shard
           * @returns {{ok:1,list:[{_id:string,count:number}]}}
           * - _id is the resource type, and there will only be one of each type.
           * - `count` is the number of orders.
           */
          ordersIndex (shard = DEFAULT_SHARD) {
            return self.req('GET', '/api/game/market/orders-index', { shard })
          },
          /**
           * GET /api/game/market/my-orders
           * @returns {{ ok:number, list: [ { _id, created, user, active, type, amount, remainingAmount, resourceType, price, totalAmount, roomName } ] }}
           * `resourceType` is one of the RESOURCE_* constants.
           */
          myOrders () {
            return self.req('GET', '/api/game/market/my-orders').then(self.mapToShard)
          },
          /**
           * GET /api/game/market/orders
           * @param {string} resourceType one of the RESOURCE_* constants.
           * @param {string} shard
           * @returns {{ ok:number, list: [ { _id, created, user, active, type, amount, remainingAmount, resourceType, price, totalAmount, roomName } ] }}
           * `resourceType` is one of the RESOURCE_* constants.
           */
          orders (resourceType, shard = DEFAULT_SHARD) {
            return self.req('GET', '/api/game/market/orders', { resourceType, shard })
          },
          /**
           * GET /api/game/market/stats
           * @param {*} resourceType
           * @param {string} shard
           * @returns {Object}
           */
          stats (resourceType, shard = DEFAULT_SHARD) {
            return self.req('GET', '/api/game/market/stats', { resourceType, shard })
          }
        },
        shards: {
          /**
           * GET /api/game/shards/info
           * @returns {{ok:number, shards:[{name:string,lastTicks:number[],cpuLimimt:number,rooms:number,users:number,tick:number}]}}
           */
          info () {
            return self.req('GET', '/api/game/shards/info')
          }
        }
      },
      leaderboard: {
        /**
         * GET /api/leaderboard/list
         * @param {number} limit
         * @param {"world"|"power"} mode
         * @param {number?} offset
         * @param {string?} season
         * @returns {{ ok, list: [ { _id, season, user, score, rank } ], count, users: { <user's _id>: { _id, username, badge: { type, color1, color2, color3, param, flip }, gcl } } }}
         */
        list (limit = 10, mode = 'world', offset = 0, season) {
          if (mode !== 'world' && mode !== 'power') throw new Error('incorrect mode parameter')
          if (!season) season = self.currentSeason();
          return self.req('GET', '/api/leaderboard/list', { limit, mode, offset, season })
        },
        /**
         * GET /api/leaderboard/find
         * @param {string} username
         * @param {"world"|string} mode
         * @param {string?} season An optional date in the format YYYY-MM, if not supplied all ranks in all seasons is returned.
         * @returns {{ ok, _id, season, user, score, rank }}
         * - `user` (not `_id`) is the user's _id, as returned by `me` and `user/find`
         * - `rank` is 0-based
         */
        find (username, mode = 'world', season = '') {
          return self.req('GET', '/api/leaderboard/find', { season, mode, username })
        },
        /**
         * GET /api/leaderboard/seasons
         * @returns {{ ok, seasons: [ { _id, name, date } ] }}
         * The _id returned here is used for the season name in the other leaderboard calls
         */
        seasons () {
          return self.req('GET', '/api/leaderboard/seasons')
        }
      },
      user: {
        /**
         * @param {Badge} badge
         * @returns {{ ok?:number,error?:string}}
         */
        badge (badge) {
          return self.req('POST', '/api/user/badge', { badge })
        },
        /**
         * POST /api/user/respawn
         * @returns {Object}
         */
        respawn () {
          return self.req('POST', '/api/user/respawn')
        },
        /**
         * POST /api/user/set-active-branch
         * @param {string} branch
         * @param {string} activeName
         * @returns {Object}
         */
        setActiveBranch (branch, activeName) {
          return self.req('POST', '/api/user/set-active-branch', { branch, activeName })
        },
        /**
         * POST /api/user/clone-branch
         * @param {string} branch
         * @param {string} newName
         * @param {*} defaultModules
         * @returns {Object}
         */
        cloneBranch (branch, newName, defaultModules) {
          return self.req('POST', '/api/user/clone-branch', { branch, newName, defaultModules })
        },
        /**
         * POST /api/user/delete-branch
         * @param {string} branch
         * @returns {Object}
         */
        deleteBranch (branch) {
          return self.req('POST', '/api/user/delete-branch', { branch })
        },
        /**
         * POST /api/user/notify-prefs
         * @param {*} prefs
         * @returns {Object}
         */
        notifyPrefs (prefs) {
          // disabled,disabledOnMessages,sendOnline,interval,errorsInterval
          return self.req('POST', '/api/user/notify-prefs', prefs)
        },
        /**
         * POST /api/user/tutorial-done
         * @returns {Object}
         */
        tutorialDone () {
          return self.req('POST', '/api/user/tutorial-done')
        },
        /**
         * POST /api/user/email
         * @param {string} email
         * @returns {Object}
         */
        email (email) {
          return self.req('POST', '/api/user/email', { email })
        },
        /**
         * GET /api/user/world-start-room
         * @param {string} shard
         * @returns {Object}
         */
        worldStartRoom (shard) {
          return self.req('GET', '/api/user/world-start-room', { shard })
        },
        /**
         * returns a world status
         * - 'normal'
         * - 'lost' when you loose all your spawns
         * - 'empty' when you have respawned and not placed your spawn yet
         * @returns {{ ok: number; status: "normal" | "lost" | "empty" }} */
        worldStatus () {
          return self.req('GET', '/api/user/world-status')
        },
        /**
         * GET /api/user/branches
         * @returns {{ ok:number, list: [{
         *   _id: string;
         *   branch: string;
         *   activeWorld: boolean;
         *   activeSim: boolean;
         * }]}
         * }
         */
        branches () {
          return self.req('GET', '/api/user/branches')
        },
        code: {
          /**
           * GET /api/user/code
           * for pushing or pulling code, as documented at http://support.screeps.com/hc/en-us/articles/203022612
           * @param {string} branch
           * @returns code
           */
          get (branch) {
            return self.req('GET', '/api/user/code', { branch })
          },
          /**
           * POST /api/user/code
           * for pushing or pulling code, as documented at http://support.screeps.com/hc/en-us/articles/203022612
           * @param {string} branch
           * @param {*} modules
           * @param {*} _hash
           * @returns {Object}
           */
          set (branch, modules, _hash) {
            if (!_hash) _hash = Date.now();
            return self.req('POST', '/api/user/code', { branch, modules, _hash })
          }
        },
        decorations: {
          /**
           * GET /api/user/decorations/inventory
           * @returns {Object}
           */
          inventory () {
            return self.req('GET', '/api/user/decorations/inventory')
          },
          /**
           * GET /api/user/decorations/themes
           * @returns {Object}
           */
          themes () {
            return self.req('GET', '/api/user/decorations/themes')
          },
          /**
           * POST /api/user/decorations/convert
           * @param {*} decorations decorations is a string array of ids
           * @returns {Object}
           */
          convert (decorations) {
            return self.req('POST', '/api/user/decorations/convert', { decorations })
          },
          /**
           * POST /api/user/decorations/pixelize
           * @param {number} count
           * @param {string} theme
           * @returns {Object}
           */
          pixelize (count, theme = '') {
            return self.req('POST', '/api/user/decorations/pixelize', { count, theme })
          },
          /**
           * POST /api/user/decorations/activate
           * @param {string} _id
           * @param {*} active
           * @returns {Object}
           */
          activate (_id, active) {
            return self.req('POST', '/api/user/decorations/activate', { _id, active })
          },
          /**
           * POST /api/user/decorations/deactivate
           * @param {*} decorations decorations is a string array of ids
           * @returns {Object}
           */
          deactivate (decorations) {
            return self.req('POST', '/api/user/decorations/deactivate', { decorations })
          }
        },
        /**
         * GET /api/user/respawn-prohibited-rooms
         * @returns {{ ok, rooms: [  ] }}
         * - `room` is an array, but seems to always contain only one element
         */
        respawnProhibitedRooms () {
          return self.req('GET', '/api/user/respawn-prohibited-rooms')
        },
        memory: {
          /**
           * GET /api/user/memory?path={path}
           * @param {string} path the path may be empty or absent to retrieve all of Memory, Example: flags.Flag1
           * @param {string} shard
           * @returns {string} gz: followed by base64-encoded gzipped JSON encoding of the requested memory path
           */
          get (path, shard = DEFAULT_SHARD) {
            return self.req('GET', '/api/user/memory', { path, shard })
          },
          /**
           * POST /api/user/memory
           * @param {string} path the path may be empty or absent to retrieve all of Memory, Example: flags.Flag1
           * @param {*} value
           * @param {string} shard
           * @returns {{ ok, result: { ok, n }, ops: [ { user, expression, hidden } ], data, insertedCount, insertedIds }}
           */
          set (path, value, shard = DEFAULT_SHARD) {
            return self.req('POST', '/api/user/memory', { path, value, shard })
          },
          segment: {
            /**
             * GET /api/user/memory-segment?segment=[0-99]
             * @param {number} segment A number from 0-99
             * @param {string} shard
             * @returns {{ ok, data: string }}
             */
            get (segment, shard = DEFAULT_SHARD) {
              return self.req('GET', '/api/user/memory-segment', { segment, shard })
            },
            /**
             * POST /api/user/memory-segment
             * @param {number} segment A number from 0-99
             * @param {*} data
             * @param {string} shard
             * @returns {Object}
             */
            set (segment, data, shard = DEFAULT_SHARD) {
              return self.req('POST', '/api/user/memory-segment', { segment, data, shard })
            }
          }
        },
        /**
         * GET /api/user/find?username={username}
         * @param {string} username
         * @returns {{ ok, user: { _id, username, badge: Badge, gcl } }}
         */
        find (username) {
          return self.req('GET', '/api/user/find', { username })
        },
        /**
         * GET /api/user/find?id={userId}
         * @param {string} id
         * @returns {{ ok, user: { _id, username, badge: Badge, gcl } }}
         */
        findById (id) {
          return self.req('GET', '/api/user/find', { id })
        },
        /**
         * GET /api/user/stats
         * @param {number} interval
         * @returns {Object}
         */
        stats (interval) {
          return self.req('GET', '/api/user/stats', { interval })
        },
        /**
         * GET /api/user/rooms
         * @param {string} id
         * @returns {Object}
         */
        rooms (id) {
          return self.req('GET', '/api/user/rooms', { id }).then(self.mapToShard)
        },
        /**
         * GET /api/user/overview?interval={interval}&statName={statName}
         * @param {number} interval
         * @param {string} statName energyControl
         * @returns {{{ ok, rooms: [ <room name> ], stats: { <room name>: [ { value, endTime } ] }, statsMax }}}
         */
        overview (interval, statName) {
          return self.req('GET', '/api/user/overview', { interval, statName })
        },
        /**
         * GET /api/user/money-history
         * @param {number} page Used for pagination
         * @returns {{"ok":1,"page":0,"list":[ { _id, date, tick, user, type, balance, change, market: {} } ] }}
         * - page used for pagination.
         * - hasMore is true if there are more pages to view.
         * - market
         *   - New Order- { order: { type, resourceType, price, totalAmount, roomName } }
         *   - Extended Order- { extendOrder: { orderId, addAmount } }
         *   - Fulfilled Order- { resourceType, roomName, targetRoomName, price, npc, amount }
         *   - Price Change - { changeOrderPrice: { orderId, oldPrice, newPrice } }
         */
        moneyHistory (page = 0) {
          return self.req('GET', '/api/user/money-history', { page })
        },
        /**
         * POST /api/user/console
         * @param {*} expression
         * @param {string} shard
         * @returns {{ ok, result: { ok, n }, ops: [ { user, expression, _id } ], insertedCount, insertedIds: [ <mongodb id> ] }}
         */
        console (expression, shard = DEFAULT_SHARD) {
          return self.req('POST', '/api/user/console', { expression, shard })
        },
        /**
         * GET /api/user/name
         * @returns {Object}
         */
        name () {
          return self.req('GET', '/api/user/name')
        }
      },
      experimental: {
        // https://screeps.com/api/experimental/pvp?start=14787157 seems to not be implemented in the api
        /**
         * @param {number} interval
         * @returns {{ ok, time, rooms: [ { _id, lastPvpTime } ] }}
         * time is the current server tick
         * _id contains the room name for each room, and lastPvpTime contains the last tick pvp occurred
         * if neither a valid interval nor a valid start argument is provided, the result of the call is still ok, but with an empty rooms array.
         */
        pvp (interval = 100) {
          return self.req('GET', '/api/experimental/pvp', { interval }).then(self.mapToShard)
        },
        /**
         * GET /api/experimental/nukes
         * @returns {Object}
         */
        nukes () {
          return self.req('GET', '/api/experimental/nukes').then(self.mapToShard)
        }
      },
      warpath: {
        /**
         * GET /api/warpath/battles
         * @param {number} interval
         * @returns {Object}
         */
        battles (interval = 100) {
          return self.req('GET', '/api/warpath/battles', { interval })
        }
      },
      scoreboard: {
        /**
         * GET /api/scoreboard/list
         * @param {number} limit
         * @param {number} offset
         * @returns {Object}
         */
        list (limit = 20, offset = 0) {
          return self.req('GET', '/api/scoreboard/list', { limit, offset })
        }
      }
    };
  }

  currentSeason () {
    const now = new Date();
    const year = now.getFullYear();
    let month = (now.getUTCMonth() + 1).toString();
    if (month.length === 1) month = `0${month}`;
    return `${year}-${month}`
  }

  isOfficialServer () {
    return this.opts.url.match(/screeps\.com/) !== null
  }

  mapToShard (res) {
    if (!res.shards) {
      res.shards = {
        privSrv: res.list || res.rooms
      };
    }
    return res
  }

  setServer (opts) {
    if (!this.opts) {
      this.opts = {};
    }
    Object.assign(this.opts, opts);
    if (opts.path && !opts.pathname) {
      this.opts.pathname = this.opts.path;
    }
    if (!opts.url) {
      this.opts.url = format(this.opts);
      if (!this.opts.url.endsWith('/')) this.opts.url += '/';
    }
    if (opts.token) {
      this.token = opts.token;
    }
    this.http = axios__default['default'].create({
      baseURL: this.opts.url
    });
  }

  async auth (email, password, opts = {}) {
    this.setServer(opts);
    if (email && password) {
      Object.assign(this.opts, { email, password });
    }
    const res = await this.raw.auth.signin(this.opts.email, this.opts.password);
    this.emit('token', res.token);
    this.emit('auth');
    this.__authed = true;
    return res
  }

  async req (method, path, body = {}) {
    const opts = {
      method,
      url: path,
      headers: {}
    };
    debugHttp(`${method} ${path} ${JSON.stringify(body)}`);
    if (this.token) {
      Object.assign(opts.headers, {
        'X-Token': this.token,
        'X-Username': this.token
      });
    }
    if (method === 'GET') {
      opts.params = body;
    } else {
      opts.data = body;
    }
    try {
      const res = await this.http(opts);
      const token = res.headers['x-token'];
      if (token) {
        this.emit('token', token);
      }
      const rateLimit = this.buildRateLimit(method, path, res);
      this.emit('rateLimit', rateLimit);
      debugRateLimit(`${method} ${path} ${rateLimit.remaining}/${rateLimit.limit} ${rateLimit.toReset}s`);
      if (typeof res.data.data === 'string' && res.data.data.slice(0, 3) === 'gz:') {
        res.data.data = await this.gz(res.data.data);
      }
      this.emit('response', res);
      return res.data
    } catch (err) {
      const res = err.response || {};
      const rateLimit = this.buildRateLimit(method, path, res);
      this.emit('rateLimit', rateLimit);
      debugRateLimit(`${method} ${path} ${rateLimit.remaining}/${rateLimit.limit} ${rateLimit.toReset}s`);
      if (res.status === 401) {
        if (this.__authed && this.opts.email && this.opts.password) {
          this.__authed = false;
          await this.auth(this.opts.email, this.opts.password);
          return this.req(method, path, body)
        } else {
          throw new Error('Not Authorized')
        }
      }
      if (res.status === 429 && !res.headers['x-ratelimit-limit'] && this.opts.experimentalRetry429) {
        await sleep(Math.floor(Math.random() * 500) + 200);
        return this.req(method, path, body)
      }
      if (err.response) {
        throw new Error(res.data)
      }
      throw new Error(err.message)
    }
  }

  async gz (data) {
    const buf = Buffer.from(data.slice(3), 'base64');
    const ret = await gunzipAsync(buf);
    return JSON.parse(ret.toString())
  }

  async inflate (data) { // es
    const buf = Buffer.from(data.slice(3), 'base64');
    const ret = await inflateAsync(buf);
    return JSON.parse(ret.toString())
  }

  buildRateLimit (method, path, res) {
    const {
      headers: {
        'x-ratelimit-limit': limit,
        'x-ratelimit-remaining': remaining,
        'x-ratelimit-reset': reset
      } = {}
    } = res;
    return {
      method,
      path,
      limit: +limit,
      remaining: +remaining,
      reset: +reset,
      toReset: reset - Math.floor(Date.now() / 1000)
    }
  }
}

/**
 * @typedef {{
 *   "color1": string;
 *   "color2": string;
 *   "color3": string;
 *   "flip": boolean;
 *   "param": number;
 *   "type": number|{ path1:string, path2:string};
 *}} Badge
 */

/**
 * @typedef {1|2|3|4|5|6|7|8|9|10} FlagColor
 * - Red = 1,
 * - Purple = 2,
 * - Blue = 3,
 * - Cyan = 4,
 * - Green = 5,
 * - Yellow = 6,
 * - Orange = 7,
 * - Brown = 8,
 * - Grey = 9,
 * - White = 10
 */

const readFileAsync = util__default['default'].promisify(fs__default['default'].readFile);

class ConfigManager {
  async refresh () {
    this._config = null;
    await this.getConfig();
  }

  async getServers () {
    const conf = await this.getConfig();
    return Object.keys(conf.servers)
  }

  async getConfig () {
    if (this._config) {
      return this._config
    }
    const paths = [];
    if (process.env.SCREEPS_CONFIG) {
      paths.push(process.env.SCREEPS_CONFIG);
    }
    const dirs = [__dirname, ''];
    for (const dir of dirs) {
      paths.push(path__default['default'].join(dir, '.screeps.yaml'));
      paths.push(path__default['default'].join(dir, '.screeps.yml'));
    }
    if (process.platform === 'win32') {
      paths.push(path__default['default'].join(process.env.APPDATA, 'screeps/config.yaml'));
      paths.push(path__default['default'].join(process.env.APPDATA, 'screeps/config.yml'));
    } else {
      if (process.env.XDG_CONFIG_PATH) {
        paths.push(
          path__default['default'].join(process.env.XDG_CONFIG_HOME, 'screeps/config.yaml')
        );
        paths.push(
          path__default['default'].join(process.env.XDG_CONFIG_HOME, 'screeps/config.yml')
        );
      }
      if (process.env.HOME) {
        paths.push(path__default['default'].join(process.env.HOME, '.config/screeps/config.yaml'));
        paths.push(path__default['default'].join(process.env.HOME, '.config/screeps/config.yml'));
        paths.push(path__default['default'].join(process.env.HOME, '.screeps.yaml'));
        paths.push(path__default['default'].join(process.env.HOME, '.screeps.yml'));
      }
    }
    for (const path of paths) {
      const data = await this.loadConfig(path);
      if (data) {
        if (!data.servers) {
          throw new Error(
            `Invalid config: 'servers' object does not exist in '${path}'`
          )
        }
        this._config = data;
        this.path = path;
        return data
      }
    }
    return null
  }

  async loadConfig (file) {
    try {
      const contents = await readFileAsync(file, 'utf8');
      return YAML__default['default'].parse(contents)
    } catch (e) {
      if (e.code === 'ENOENT') {
        return false
      } else {
        throw e
      }
    }
  }
}

const DEFAULTS = {
  protocol: 'https',
  hostname: 'screeps.com',
  port: 443,
  path: '/'
};

const configManager = new ConfigManager();

class ScreepsAPI extends RawAPI {
  static async fromConfig (server = 'main', config = false, opts = {}) {
    const data = await configManager.getConfig();

    if (data) {
      if (!data.servers[server]) {
        throw new Error(`Server '${server}' does not exist in '${configManager.path}'`)
      }

      const conf = data.servers[server];
      if (conf.ptr) conf.path = '/ptr';
      if (conf.season) conf.path = '/season';
      const api = new ScreepsAPI(
        Object.assign(
          {
            hostname: conf.host,
            port: conf.port,
            protocol: conf.secure ? 'https' : 'http',
            token: conf.token,
            path: conf.path || '/'
          },
          opts
        )
      );

      api.appConfig = (data.configs && data.configs[config]) || {};

      if (!conf.token && conf.username && conf.password) {
        await api.auth(conf.username, conf.password);
      }

      return api
    }

    throw new Error('No valid config found')
  }

  constructor (opts) {
    opts = Object.assign({}, DEFAULTS, opts);
    super(opts);
    this.on('token', token => {
      this.token = token;
      this.raw.token = token;
    });
    const defaultLimit = (limit, period) => ({
      limit,
      period,
      remaining: limit,
      reset: 0,
      toReset: 0
    });
    this.rateLimits = {
      global: defaultLimit(120, 'minute'),
      GET: {
        '/api/game/room-terrain': defaultLimit(360, 'hour'),
        '/api/user/code': defaultLimit(60, 'hour'),
        '/api/user/memory': defaultLimit(1440, 'day'),
        '/api/user/memory-segment': defaultLimit(360, 'hour'),
        '/api/game/market/orders-index': defaultLimit(60, 'hour'),
        '/api/game/market/orders': defaultLimit(60, 'hour'),
        '/api/game/market/my-orders': defaultLimit(60, 'hour'),
        '/api/game/market/stats': defaultLimit(60, 'hour'),
        '/api/game/user/money-history': defaultLimit(60, 'hour')
      },
      POST: {
        '/api/user/console': defaultLimit(360, 'hour'),
        '/api/game/map-stats': defaultLimit(60, 'hour'),
        '/api/user/code': defaultLimit(240, 'day'),
        '/api/user/set-active-branch': defaultLimit(240, 'day'),
        '/api/user/memory': defaultLimit(240, 'day'),
        '/api/user/memory-segment': defaultLimit(60, 'hour')
      }
    };
    this.on('rateLimit', limits => {
      const rate =
        this.rateLimits[limits.method][limits.path] || this.rateLimits.global;
      const copy = Object.assign({}, limits);
      delete copy.path;
      delete copy.method;
      Object.assign(rate, copy);
    });
    this.socket = new Socket(this);
  }

  getRateLimit (method, path) {
    return this.rateLimits[method][path] || this.rateLimits.global
  }

  get rateLimitResetUrl () {
    return `https://screeps.com/a/#!/account/auth-tokens/noratelimit?token=${this.token.slice(
      0,
      8
    )}`
  }

  async me () {
    if (this._user) return this._user
    const tokenInfo = await this.tokenInfo();
    if (tokenInfo.full) {
      this._user = await this.raw.auth.me();
    } else {
      const { username } = await this.raw.user.name();
      const { user } = await this.raw.user.find(username);
      this._user = user;
    }
    return this._user
  }

  async tokenInfo () {
    if (this._tokenInfo) {
      return this._tokenInfo
    }
    if (this.opts.token) {
      const { token } = await this.raw.auth.queryToken(this.token);
      this._tokenInfo = token;
    } else {
      this._tokenInfo = { full: true };
    }
    return this._tokenInfo
  }

  async userID () {
    const user = await this.me();
    return user._id
  }

  get history () {
    return this.raw.history
  }

  get authmod () {
    return this.raw.authmod
  }

  get version () {
    return this.raw.version
  }

  get time () {
    return this.raw.game.time
  }

  get leaderboard () {
    return this.raw.leaderboard
  }

  get market () {
    return this.raw.game.market
  }

  get registerUser () {
    return this.raw.register.submit
  }

  get code () {
    return this.raw.user.code
  }

  get memory () {
    return this.raw.user.memory
  }

  get segment () {
    return this.raw.user.memory.segment
  }

  get console () {
    return this.raw.user.console
  }
}

exports.ScreepsAPI = ScreepsAPI;
