text: 'use strict';
  
  const EventEmitter = require('events');
  const { setTimeout, clearTimeout } = require('timers');
  const fetch = require('node-fetch');
  const transports = require('./transports');
  const { RPCCommands, RPCEvents } = require('./Constants');
  const { pid: getPid, uuid } = require('./Util');
  
  function subKey(event, args) {
    return `${event}${JSON.stringify(args)}`;
  }
  
  /**
   * @typedef {RPCClientOptions}
   * @extends {ClientOptions}
   * @prop {string} transport RPC transport. one of `ipc` or `websocket`
   */
  
  /**
   * The main hub for interacting with Discord RPC
   * @extends {BaseClient}
   */
  class RPCClient extends EventEmitter {
    /**
     * @param {RPCClientOptions} [options] Options for the client
     * You must provide a transport
     */
    constructor(options = {}) {
      super();
  
      this.options = options;
  
      this.accessToken = null;
      this.clientId = null;
  
      /**
       * Application used in this client
       * @type {?ClientApplication}
       */
      this.application = null;
  
      /**
       * User used in this application
       * @type {?User}
       */
      this.user = null;
  
      const Transport = transports[options.transport];
      if (!Transport) {
        throw new TypeError('RPC_INVALID_TRANSPORT', options.transport);
      }
  
      this.fetch = (method, path, { data, query } = {}) =>
        fetch(`${this.fetch.endpoint}${path}${query ? new URLSearchParams(query) : ''}`, {
          method,
          body: data,
          headers: {
            Authorzation: `Bearer ${this.accessToken}`,
          },
        }).then((r) => r.json());
  
      this.fetch.endpoint = 'https://discordapp.com/api';
  
      /**
       * Raw transport userd
       * @type {RPCTransport}
       * @private
       */
      this.transport = new Transport(this);
      this.transport.on('message', this._onRpcMessage.bind(this));
  
      /**
       * Map of nonces being expected from the transport
       * @type {Map}
       * @private
       */
      this._expecting = new Map();
  
      /**
       * Map of current subscriptions
       * @type {Map}
       * @private
       */
      this._subscriptions = new Map();
  
      this._connectPromise = undefined;
    }
  
    /**
     * Search and connect to RPC
     */
    connect(clientId) {
      if (this._connectPromise) {
        return this._connectPromise;
      }
      this._connectPromise = new Promise((resolve, reject) => {
        this.clientId = clientId;
        const timeout = setTimeout(() => reject(new Error('RPC_CONNECTION_TIMEOUT')), 10e3);
        timeout.unref();
        this.once('connected', () => {
          clearTimeout(timeout);
          resolve(this);
        });
        this.transport.once('close', reject);
        this.transport.connect().catch(reject);
      });
      return this._connectPromise;
    }
  
    /**
     * @typedef {RPCLoginOptions}
     * @param {string} clientId Client ID
     * @param {string} [clientSecret] Client secret
     * @param {string} [accessToken] Access token
     * @param {string} [rpcToken] RPC token
     * @param {string} [tokenEndpoint] Token endpoint
     * @param {string[]} [scopes] Scopes to authorize with
     */
  
    /**
     * Performs authentication flow. Automatically calls Client#connect if needed.
     * @param {RPCLoginOptions} options Options for authentication.
     * At least one property must be provided to perform login.
     * @example client.login({ clientId: '1234567', clientSecret: 'abcdef123' });
     * @returns {Promise<RPCClient>}
     */
    async login(options = {}) {
      let { clientId, accessToken } = options;
      await this.connect(clientId);
      if (!options.scopes) {
        this.emit('ready');
        return this;
      }
      if (!accessToken) {
        accessToken = await this.authorize(options);
      }
      return this.authenticate(accessToken);
    }
  
    /**
     * Request
     * @param {string} cmd Command
     * @param {Object} [args={}] Arguments
     * @param {string} [evt] Event
     * @returns {Promise}
     * @private
     */
    request(cmd, args, evt) {
      return new Promise((resolve, reject) => {
        const nonce = uuid();
        this.transport.send({ cmd, args, evt, nonce });
        this._expecting.set(nonce, { resolve, reject });
      });
    }
  
    /**
     * Message handler
     * @param {Object} message message
     * @private
     */
    _onRpcMessage(message) {
      if (message.cmd === RPCCommands.DISPATCH && message.evt === RPCEvents.READY) {
        if (message.data.user) {
          this.user = message.data.user;
        }
        this.emit('connected');
      } else if (this._expecting.has(message.nonce)) {
        const { resolve, reject } = this._expecting.get(message.nonce);
        if (message.evt === 'ERROR') {
          const e = new Error(message.data.message);
          e.code = message.data.code;
          e.data = message.data;
          reject(e);
        } else {
          resolve(message.data);
        }
        this._expecting.delete(message.nonce);
      } else {
        const subid = subKey(message.evt, message.args);
        if (!this._subscriptions.has(subid)) {
          return;
        }
        this._subscriptions.get(subid)(message.data);
      }
    }
  
    /**
     * Authorize
     * @param {Object} options options
     * @returns {Promise}
     * @private
     */
    async authorize({ scopes, clientSecret, rpcToken, redirectUri } = {}) {
      if (clientSecret && rpcToken === true) {
        const body = await this.fetch('POST', '/oauth2/token/rpc', {
          data: new URLSearchParams({
            client_id: this.clientId,
            client_secret: clientSecret,
          }),
        });
        rpcToken = body.rpc_token;
      }
  
      const { code } = await this.request('AUTHORIZE', {
        scopes,
        client_id: this.clientId,
        rpc_token: rpcToken,
        redirect_uri: redirectUri,
      });
  
      const response = await this.fetch('POST', '/oauth2/token', {
        data: new URLSearchParams({
          client_id: this.clientId,
          code,
          grant_type: 'authorization_code',
          redirect_uri: redirectUri,
        }),
      });
  
      return response.access_token;
    }
  
    /**
     * Authenticate
     * @param {string} accessToken access token
     * @returns {Promise}
     * @private
     */
    authenticate(accessToken) {
      return this.request('AUTHENTICATE', { access_token: accessToken })
        .then(({ application, user }) => {
          this.accessToken = accessToken;
          this.application = application;
          this.user = user;
          this.emit('ready');
          return this;
        });
    }
  
  
    /**
     * Fetch a guild
     * @param {Snowflake} id Guild ID
     * @param {number} [timeout] Timeout request
     * @returns {Promise<Guild>}
     */
    getGuild(id, timeout) {
      return this.request(RPCCommands.GET_GUILD, { guild_id: id, timeout });
    }
  
    /**
     * Fetch all guilds
     * @param {number} [timeout] Timeout request
     * @returns {Promise<Collection<Snowflake, Guild>>}
     */
    getGuilds(timeout) {
      return this.request(RPCCommands.GET_GUILDS, { timeout });
    }
  
    /**
     * Get a channel
     * @param {Snowflake} id Channel ID
     * @param {number} [timeout] Timeout request
     * @returns {Promise<Channel>}
     */
    getChannel(id, timeout) {
      return this.request(RPCCommands.GET_CHANNEL, { channel_id: id, timeout });
    }
  
    /**
     * Get all channels
     * @param {Snowflake} [id] Guild ID
     * @param {number} [timeout] Timeout request
     * @returns {Promise<Collection<Snowflake, Channel>>}
     */
    async getChannels(id, timeout) {
      const { channels } = await this.request(RPCCommands.GET_CHANNELS, {
        timeout,
        guild_id: id,
      });
      return channels;
    }
  
    /**
     * @typedef {CertifiedDevice}
     * @prop {string} type One of `AUDIO_INPUT`, `AUDIO_OUTPUT`, `VIDEO_INPUT`
     * @prop {string} uuid This device's Windows UUID
     * @prop {object} vendor Vendor information
     * @prop {string} vendor.name Vendor's name
     * @prop {string} vendor.url Vendor's url
     * @prop {object} model Model information
     * @prop {string} model.name Model's name
     * @prop {string} model.url Model's url
     * @prop {string[]} related Array of related product's Windows UUIDs
     * @prop {boolean} echoCancellation If the device has echo cancellation
     * @prop {boolean} noiseSuppression If the device has noise suppression
     * @prop {boolean} automaticGainControl If the device has automatic gain control
     * @prop {boolean} hardwareMute If the device has a hardware mute
     */
  
    /**
     * Tell discord which devices are certified
     * @param {CertifiedDevice[]} devices Certified devices to send to discord
     * @returns {Promise}
     */
    setCertifiedDevices(devices) {
      return this.request(RPCCommands.SET_CERTIFIED_DEVICES, {
        devices: devices.map((d) => ({
          type: d.type,
          id: d.uuid,
          vendor: d.vendor,
          model: d.model,
          related: d.related,
          echo_cancellation: d.echoCancellation,
          noise_suppression: d.noiseSuppression,
          automatic_gain_control: d.automaticGainControl,
          hardware_mute: d.hardwareMute,
        })),
      });
    }
  
    /**
     * @typedef {UserVoiceSettings}
     * @prop {Snowflake} id ID of the user these settings apply to
     * @prop {?Object} [pan] Pan settings, an object with `left` and `right` set between
     * 0.0 and 1.0, inclusive
     * @prop {?number} [volume=100] The volume
     * @prop {bool} [mute] If the user is muted
     */
  
    /**
     * Set the voice settings for a uer, by id
     * @param {Snowflake} id ID of the user to set
     * @param {UserVoiceSettings} settings Settings
     * @returns {Promise}
     */
    setUserVoiceSettings(id, settings) {
      return this.request(RPCCommands.SET_USER_VOICE_SETTINGS, {
        user_id: id,
        pan: settings.pan,
        mute: settings.mute,
        volume: settings.volume,
      });
    }
  
    /**
     * Move the user to a voice channel
     * @param {Snowflake} id ID of the voice channel
     * @param {Object} [options] Options
     * @param {number} [options.timeout] Timeout for the command
     * @param {boolean} [options.force] Force this move. This should only be done if you
     * have explicit permission from the user.
     * @returns {Promise}
     */
    selectVoiceChannel(id, { timeout, force = false } = {}) {
      return this.request(RPCCommands.SELECT_VOICE_CHANNEL, { channel_id: id, timeout, force });
    }
  
    /**
     * Move the user to a text channel
     * @param {Snowflake} id ID of the voice channel
     * @param {Object} [options] Options
     * @param {number} [options.timeout] Timeout for the command
     * @param {boolean} [options.force] Force this move. This should only be done if you
     * have explicit permission from the user.
     * @returns {Promise}
     */
    selectTextChannel(id, { timeout, force = false } = {}) {
      return this.request(RPCCommands.SELECT_TEXT_CHANNEL, { channel_id: id, timeout, force });
    }
  
    /**
     * Get current voice settings
     * @returns {Promise}
     */
    getVoiceSettings() {
      return this.request(RPCCommands.GET_VOICE_SETTINGS)
        .then((s) => ({
          automaticGainControl: s.automatic_gain_control,
          echoCancellation: s.echo_cancellation,
          noiseSuppression: s.noise_suppression,
          qos: s.qos,
          silenceWarning: s.silence_warning,
          deaf: s.deaf,
          mute: s.mute,
          input: {
            availableDevices: s.input.available_devices,
            device: s.input.device_id,
            volume: s.input.volume,
          },
          output: {
            availableDevices: s.output.available_devices,
            device: s.output.device_id,
            volume: s.output.volume,
          },
          mode: {
            type: s.mode.type,
            autoThreshold: s.mode.auto_threshold,
            threshold: s.mode.threshold,
            shortcut: s.mode.shortcut,
            delay: s.mode.delay,
          },
        }));
    }
  
    /**
     * Set current voice settings, overriding the current settings until this session disconnects.
     * This also locks the settings for any other rpc sessions which may be connected.
     * @param {Object} args Settings
     * @returns {Promise}
     */
    setVoiceSettings(args) {
      return this.request(RPCCommands.SET_VOICE_SETTINGS, {
        automatic_gain_control: args.automaticGainControl,
        echo_cancellation: args.echoCancellation,
        noise_suppression: args.noiseSuppression,
        qos: args.qos,
        silence_warning: args.silenceWarning,
        deaf: args.deaf,
        mute: args.mute,
        input: args.input ? {
          device_id: args.input.device,
          volume: args.input.volume,
        } : undefined,
        output: args.output ? {
          device_id: args.output.device,
          volume: args.output.volume,
        } : undefined,
        mode: args.mode ? {
          mode: args.mode.type,
          auto_threshold: args.mode.autoThreshold,
          threshold: args.mode.threshold,
          shortcut: args.mode.shortcut,
          delay: args.mode.delay,
        } : undefined,
      });
    }
  
    /**
     * Capture a shortcut using the client
     * The callback takes (key, stop) where `stop` is a function that will stop capturing.
     * This `stop` function must be called before disconnecting or else the user will have
     * to restart their client.
     * @param {Function} callback Callback handling keys
     * @returns {Promise<Function>}
     */
    captureShortcut(callback) {
      const subid = subKey(RPCEvents.CAPTURE_SHORTCUT_CHANGE);
      const stop = () => {
        this._subscriptions.delete(subid);
        return this.request(RPCCommands.CAPTURE_SHORTCUT, { action: 'STOP' });
      };
      this._subscriptions.set(subid, ({ shortcut }) => {
        callback(shortcut, stop);
      });
      return this.request(RPCCommands.CAPTURE_SHORTCUT, { action: 'START' })
        .then(() => stop);
    }
  
    setActivity(args = {}, pid = getPid()) {
      let timestamps;
      let assets;
      let party;
      let secrets;
      if (args.startTimestamp || args.endTimestamp) {
        timestamps = {
          start: args.startTimestamp,
          end: args.endTimestamp,
        };
        if (timestamps.start instanceof Date) {
          timestamps.start = Math.round(timestamps.start.getTime() / 1000);
        }
        if (timestamps.end instanceof Date) {
          timestamps.end = Math.round(timestamps.end.getTime() / 1000);
        }
        if (timestamps.start > 2147483647) {
          throw new RangeError('timestamps.start must fit into a unix timestamp');
        }
        if (timestamps.end > 2147483647) {
          throw new RangeError('timestamps.end must fit into a unix timestamp');
        }
      }
      if (
        args.largeImageKey || args.largeImageText ||
        args.smallImageKey || args.smallImageText
      ) {
        assets = {
          large_image: args.largeImageKey,
          large_text: args.largeImageText,
          small_image: args.smallImageKey,
          small_text: args.smallImageText,
        };
      }
      if (args.partySize || args.partyId || args.partyMax) {
        party = { id: args.partyId };
        if (args.partySize || args.partyMax) {
          party.size = [args.partySize, args.partyMax];
        }
      }
      if (args.matchSecret || args.joinSecret || args.spectateSecret) {
        secrets = {
          match: args.matchSecret,
          join: args.joinSecret,
          spectate: args.spectateSecret,
        };
      }
  
      return this.request(RPCCommands.SET_ACTIVITY, {
        pid,
        activity: {
          state: args.state,
          details: args.details,
          timestamps,
          assets,
          party,
          secrets,
          instance: !!args.instance,
        },
      });
    }
  
    /**
     * Clears the currently set presence, if any. This will hide the "Playing X" message
     * displayed below the user's name.
     * @param {number} [pid] The application's process ID. Defaults to the executing process' PID.
     * @returns {Promise}
     */
    clearActivity(pid = getPid()) {
      return this.request(RPCCommands.SET_ACTIVITY, {
        pid,
      });
    }
  
    /**
     * Invite a user to join the game the RPC user is currently playing
     * @param {User} user The user to invite
     * @returns {Promise}
     */
    sendJoinInvite(user) {
      return this.request(RPCCommands.SEND_ACTIVITY_JOIN_INVITE, {
        user_id: user.id || user,
      });
    }
  
    /**
     * Request to join the game the user is playing
     * @param {User} user The user whose game you want to request to join
     * @returns {Promise}
     */
    sendJoinRequest(user) {
      return this.request(RPCCommands.SEND_ACTIVITY_JOIN_REQUEST, {
        user_id: user.id || user,
      });
    }
  
    /**
     * Reject a join request from a user
     * @param {User} user The user whose request you wish to reject
     * @returns {Promise}
     */
    closeJoinRequest(user) {
      return this.request(RPCCommands.CLOSE_ACTIVITY_JOIN_REQUEST, {
        user_id: user.id || user,
      });
    }
  
    async createLobby(type, capacity, metadata) {
      return this.request(RPCCommands.CREATE_LOBBY, {
        type,
        capacity,
        metadata,
      });
    }
  
    async updateLobby(lobby, { type, owner, capacity, metadata } = {}) {
      return this.request(RPCCommands.UPDATE_LOBBY, {
        id: lobby.id || lobby,
        type,
        owner_id: (owner && owner.id) || owner,
        capacity,
        metadata,
      });
    }
  
    deleteLobby(lobby) {
      return this.request(RPCCommands.DELETE_LOBBY, {
        id: lobby.id || lobby,
      });
    }
  
    async connectToLobby(id, secret) {
      return this.request(RPCCommands.CONNECT_TO_LOBBY, {
        id,
        secret,
      });
    }
  
    sendToLobby(lobby, data) {
      return this.request(RPCCommands.SEND_TO_LOBBY, {
        id: lobby.id || lobby,
        data,
      });
    }
  
    disconnectFromLobby(lobby) {
      return this.request(RPCCommands.DISCONNECT_FROM_LOBBY, {
        id: lobby.id || lobby,
      });
    }
  
    updateLobbyMember(lobby, user, metadata) {
      return this.request(RPCCommands.UPDATE_LOBBY_MEMBER, {
        lobby_id: lobby.id || lobby,
        user_id: user.id || user,
        metadata,
      });
    }
  
    /**
     * Subscribe to an event
     * @param {string} event Name of event e.g. `MESSAGE_CREATE`
     * @param {Object} [args] Args for event e.g. `{ channel_id: '1234' }`
     * @param {Function} callback Callback when an event for the subscription is triggered
     * @returns {Promise<Object>}
     */
    subscribe(event, args, callback) {
      if (!callback && typeof args === 'function') {
        callback = args;
        args = undefined;
      }
      return this.request(RPCCommands.SUBSCRIBE, args, event).then(() => {
        const subid = subKey(event, args);
        this._subscriptions.set(subid, callback);
        return {
          unsubscribe: () => this.request(RPCCommands.UNSUBSCRIBE, args, event)
            .then(() => this._subscriptions.delete(subid)),
        };
      });
    }
  
    /**
     * Destroy the client
     */
    async destroy() {
      this.transport.close();
    }
  }
  
  module.exports = RPCClient;
  