text: const VoiceWebSocket = require('./VoiceWebSocket');
  const VoiceUDP = require('./VoiceUDPClient');
  const Util = require('../../util/Util');
  const Constants = require('../../util/Constants');
  const AudioPlayer = require('./player/AudioPlayer');
  const VoiceReceiver = require('./receiver/VoiceReceiver');
  const EventEmitter = require('events').EventEmitter;
  const Prism = require('prism-media');
  
  /**
   * Represents a connection to a guild's voice server.
   * ```js
   * // Obtained using:
   * voiceChannel.join()
   *   .then(connection => {
   *
   *   });
   * ```
   * @extends {EventEmitter}
   */
  class VoiceConnection extends EventEmitter {
    constructor(voiceManager, channel) {
      super();
  
      /**
       * The voice manager that instantiated this connection
       * @type {ClientVoiceManager}
       */
      this.voiceManager = voiceManager;
  
      /**
       * The client that instantiated this connection
       * @type {Client}
       */
      this.client = voiceManager.client;
  
      /**
       * @external Prism
       * @see {@link https://github.com/hydrabolt/prism-media}
       */
  
      /**
       * The audio transcoder for this connection
       * @type {Prism}
       */
      this.prism = new Prism();
  
      /**
       * The voice channel this connection is currently serving
       * @type {VoiceChannel}
       */
      this.channel = channel;
  
      /**
       * The current status of the voice connection
       * @type {VoiceStatus}
       */
      this.status = Constants.VoiceStatus.AUTHENTICATING;
  
      /**
       * Whether we're currently transmitting audio
       * @type {boolean}
       */
      this.speaking = false;
  
      /**
       * An array of Voice Receivers that have been created for this connection
       * @type {VoiceReceiver[]}
       */
      this.receivers = [];
  
      /**
       * The authentication data needed to connect to the voice server
       * @type {Object}
       * @private
       */
      this.authentication = {};
  
      /**
       * The audio player for this voice connection
       * @type {AudioPlayer}
       */
      this.player = new AudioPlayer(this);
  
      this.player.on('debug', m => {
        /**
         * Debug info from the connection.
         * @event VoiceConnection#debug
         * @param {string} message The debug message
         */
        this.emit('debug', `audio player - ${m}`);
      });
  
      this.player.on('error', e => {
        /**
         * Warning info from the connection.
         * @event VoiceConnection#warn
         * @param {string|Error} warning The warning
         */
        this.emit('warn', e);
      });
  
      /**
       * Map SSRC to speaking values
       * @type {Map<number, boolean>}
       * @private
       */
      this.ssrcMap = new Map();
  
      /**
       * Object that wraps contains the `ws` and `udp` sockets of this voice connection
       * @type {Object}
       * @private
       */
      this.sockets = {};
  
      this.authenticate();
    }
  
    /**
     * The current stream dispatcher (if any)
     * @type {?StreamDispatcher}
     * @readonly
     */
    get dispatcher() {
      return this.player.dispatcher;
    }
  
    /**
     * Sets whether the voice connection should display as "speaking" or not.
     * @param {boolean} value Whether or not to speak
     * @private
     */
    setSpeaking(value) {
      if (this.speaking === value) return;
      if (this.status !== Constants.VoiceStatus.CONNECTED) return;
      this.speaking = value;
      this.sockets.ws.sendPacket({
        op: Constants.VoiceOPCodes.SPEAKING,
        d: {
          speaking: true,
          delay: 0,
        },
      }).catch(e => {
        this.emit('debug', e);
      });
    }
  
    /**
     * Sends a request to the main gateway to join a voice channel.
     * @param {Object} [options] The options to provide
     */
    sendVoiceStateUpdate(options = {}) {
      options = Util.mergeDefault({
        guild_id: this.channel.guild.id,
        channel_id: this.channel.id,
        self_mute: false,
        self_deaf: false,
      }, options);
  
      this.client.ws.send({
        op: Constants.OPCodes.VOICE_STATE_UPDATE,
        d: options,
      });
    }
  
    /**
     * Set the token and endpoint required to connect to the voice servers.
     * @param {string} token The voice token
     * @param {string} endpoint The voice endpoint
     * @returns {void}
     */
    setTokenAndEndpoint(token, endpoint) {
      if (!endpoint) {
        // Signifies awaiting endpoint stage
        return;
      }
  
      if (!token) {
        this.authenticateFailed('Token not provided from voice server packet.');
        return;
      }
  
      endpoint = endpoint.match(/([^:]*)/)[0];
  
      if (!endpoint) {
        this.authenticateFailed('Invalid endpoint received.');
        return;
      }
  
      if (this.status === Constants.VoiceStatus.AUTHENTICATING) {
        this.authentication.token = token;
        this.authentication.endpoint = endpoint;
        this.checkAuthenticated();
      } else if (token !== this.authentication.token || endpoint !== this.authentication.endpoint) {
        this.reconnect(token, endpoint);
      }
    }
  
    /**
     * Sets the Session ID for the connection.
     * @param {string} sessionID The voice session ID
     */
    setSessionID(sessionID) {
      if (!sessionID) {
        this.authenticateFailed('Session ID not supplied.');
        return;
      }
  
      if (this.status === Constants.VoiceStatus.AUTHENTICATING) {
        this.authentication.sessionID = sessionID;
        this.checkAuthenticated();
      } else if (sessionID !== this.authentication.sessionID) {
        this.authentication.sessionID = sessionID;
        /**
         * Emitted when a new session ID is received.
         * @event VoiceConnection#newSession
         * @private
         */
        this.emit('newSession', sessionID);
      }
    }
  
    /**
     * Checks whether the voice connection is authenticated.
     * @private
     */
    checkAuthenticated() {
      const { token, endpoint, sessionID } = this.authentication;
  
      if (token && endpoint && sessionID) {
        clearTimeout(this.connectTimeout);
        this.status = Constants.VoiceStatus.CONNECTING;
        /**
         * Emitted when we successfully initiate a voice connection.
         * @event VoiceConnection#authenticated
         */
        this.emit('authenticated');
        this.connect();
      }
    }
  
    /**
     * Invoked when we fail to initiate a voice connection.
     * @param {string} reason The reason for failure
     * @private
     */
    authenticateFailed(reason) {
      clearTimeout(this.connectTimeout);
      if (this.status === Constants.VoiceStatus.AUTHENTICATING) {
        /**
         * Emitted when we fail to initiate a voice connection.
         * @event VoiceConnection#failed
         * @param {Error} error The encountered error
         */
        this.emit('failed', new Error(reason));
      } else {
        /**
         * Emitted whenever the connection encounters an error.
         * @event VoiceConnection#error
         * @param {Error} error The encountered error
         */
        this.emit('error', new Error(reason));
      }
      this.status = Constants.VoiceStatus.DISCONNECTED;
    }
  
    /**
     * Move to a different voice channel in the same guild.
     * @param {VoiceChannel} channel The channel to move to
     * @private
     */
    updateChannel(channel) {
      this.channel = channel;
      this.sendVoiceStateUpdate();
    }
  
    /**
     * Attempts to authenticate to the voice server.
     * @private
     */
    authenticate() {
      this.sendVoiceStateUpdate();
      this.connectTimeout = this.client.setTimeout(
        () => this.authenticateFailed(new Error('Connection not established within 15 seconds.')), 15000);
    }
  
    /**
     * Attempts to reconnect to the voice server (typically after a region change).
     * @param {string} token The voice token
     * @param {string} endpoint The voice endpoint
     * @private
     */
    reconnect(token, endpoint) {
      this.authentication.token = token;
      this.authentication.endpoint = endpoint;
  
      this.status = Constants.VoiceStatus.RECONNECTING;
      /**
       * Emitted when the voice connection is reconnecting (typically after a region change).
       * @event VoiceConnection#reconnecting
       */
      this.emit('reconnecting');
      this.connect();
    }
  
    /**
     * Disconnect the voice connection, causing a disconnect and closing event to be emitted.
     */
    disconnect() {
      this.emit('closing');
      this.sendVoiceStateUpdate({
        channel_id: null,
      });
      this.player.destroy();
      this.cleanup();
      this.status = Constants.VoiceStatus.DISCONNECTED;
      /**
       * Emitted when the voice connection disconnects.
       * @event VoiceConnection#disconnect
       */
      this.emit('disconnect');
    }
  
    /**
     * Cleans up after disconnect.
     * @private
     */
    cleanup() {
      const { ws, udp } = this.sockets;
  
      if (ws) {
        ws.removeAllListeners('error');
        ws.removeAllListeners('ready');
        ws.removeAllListeners('sessionDescription');
        ws.removeAllListeners('speaking');
      }
  
      if (udp) udp.removeAllListeners('error');
  
      this.sockets.ws = null;
      this.sockets.udp = null;
    }
  
    /**
     * Connect the voice connection.
     * @private
     */
    connect() {
      if (this.status !== Constants.VoiceStatus.RECONNECTING) {
        if (this.sockets.ws) throw new Error('There is already an existing WebSocket connection.');
        if (this.sockets.udp) throw new Error('There is already an existing UDP connection.');
      }
  
      if (this.sockets.ws) this.sockets.ws.shutdown();
      if (this.sockets.udp) this.sockets.udp.shutdown();
  
      this.sockets.ws = new VoiceWebSocket(this);
      this.sockets.udp = new VoiceUDP(this);
  
      const { ws, udp } = this.sockets;
  
      ws.on('error', err => this.emit('error', err));
      udp.on('error', err => this.emit('error', err));
      ws.on('ready', this.onReady.bind(this));
      ws.on('sessionDescription', this.onSessionDescription.bind(this));
      ws.on('speaking', this.onSpeaking.bind(this));
    }
  
    /**
     * Invoked when the voice websocket is ready.
     * @param {Object} data The received data
     * @private
     */
    onReady({ port, ssrc, ip }) {
      this.authentication.port = port;
      this.authentication.ssrc = ssrc;
      this.sockets.udp.createUDPSocket(ip);
    }
  
    /**
     * Invoked when a session description is received.
     * @param {string} mode The encryption mode
     * @param {string} secret The secret key
     * @private
     */
    onSessionDescription(mode, secret) {
      this.authentication.encryptionMode = mode;
      this.authentication.secretKey = secret;
  
      this.status = Constants.VoiceStatus.CONNECTED;
      /**
       * Emitted once the connection is ready, when a promise to join a voice channel resolves,
       * the connection will already be ready.
       * @event VoiceConnection#ready
       */
      this.emit('ready');
    }
  
    /**
     * Invoked when a speaking event is received.
     * @param {Object} data The received data
     * @private
     */
    onSpeaking({ user_id, ssrc, speaking }) {
      const guild = this.channel.guild;
      const user = this.client.users.get(user_id);
      this.ssrcMap.set(+ssrc, user);
      if (!speaking) {
        for (const receiver of this.receivers) {
          receiver.stoppedSpeaking(user);
        }
      }
      /**
       * Emitted whenever a user starts/stops speaking.
       * @event VoiceConnection#speaking
       * @param {User} user The user that has started/stopped speaking
       * @param {boolean} speaking Whether or not the user is speaking
       */
      if (this.status === Constants.VoiceStatus.CONNECTED) this.emit('speaking', user, speaking);
      guild._memberSpeakUpdate(user_id, speaking);
    }
  
    /**
     * Options that can be passed to stream-playing methods:
     * @typedef {Object} StreamOptions
     * @property {number} [seek=0] The time to seek to
     * @property {number} [volume=1] The volume to play at
     * @property {number} [passes=1] How many times to send the voice packet to reduce packet loss
     * @property {number|string} [bitrate=48000] The bitrate (quality) of the audio.
     * If set to 'auto', the voice channel's bitrate will be used
     */
  
    /**
     * Play the given file in the voice connection.
     * @param {string} file The absolute path to the file
     * @param {StreamOptions} [options] Options for playing the stream
     * @returns {StreamDispatcher}
     * @example
     * // Play files natively
     * voiceChannel.join()
     *   .then(connection => {
     *     const dispatcher = connection.playFile('C:/Users/Discord/Desktop/music.mp3');
     *   })
     *   .catch(console.error);
     */
    playFile(file, options) {
      return this.player.playUnknownStream(`file:${file}`, options);
    }
  
    /**
     * Play an arbitrary input that can be [handled by ffmpeg](https://ffmpeg.org/ffmpeg-protocols.html#Description)
     * @param {string} input the arbitrary input
     * @param {StreamOptions} [options] Options for playing the stream
     * @returns {StreamDispatcher}
     */
    playArbitraryInput(input, options) {
      return this.player.playUnknownStream(input, options);
    }
  
    /**
     * Plays and converts an audio stream in the voice connection.
     * @param {ReadableStream} stream The audio stream to play
     * @param {StreamOptions} [options] Options for playing the stream
     * @returns {StreamDispatcher}
     * @example
     * // Play streams using ytdl-core
     * const ytdl = require('ytdl-core');
     * const streamOptions = { seek: 0, volume: 1 };
     * voiceChannel.join()
     *   .then(connection => {
     *     const stream = ytdl('https://www.youtube.com/watch?v=XAWgeLF9EVQ', { filter : 'audioonly' });
     *     const dispatcher = connection.playStream(stream, streamOptions);
     *   })
     *   .catch(console.error);
     */
    playStream(stream, options) {
      return this.player.playUnknownStream(stream, options);
    }
  
    /**
     * Plays a stream of 16-bit signed stereo PCM.
     * @param {ReadableStream} stream The audio stream to play
     * @param {StreamOptions} [options] Options for playing the stream
     * @returns {StreamDispatcher}
     */
    playConvertedStream(stream, options) {
      return this.player.playPCMStream(stream, options);
    }
  
    /**
     * Plays an Opus encoded stream.
     * <warn>Note that inline volume is not compatible with this method.</warn>
     * @param {ReadableStream} stream The Opus audio stream to play
     * @param {StreamOptions} [options] Options for playing the stream
     * @returns {StreamDispatcher}
     */
    playOpusStream(stream, options) {
      return this.player.playOpusStream(stream, options);
    }
  
    /**
     * Plays a voice broadcast.
     * @param {VoiceBroadcast} broadcast The broadcast to play
     * @param {StreamOptions} [options] Options for playing the stream
     * @returns {StreamDispatcher}
     * @example
     * // Play a broadcast
     * const broadcast = client
     *   .createVoiceBroadcast()
     *   .playFile('./test.mp3');
     * const dispatcher = voiceConnection.playBroadcast(broadcast);
     */
    playBroadcast(broadcast, options) {
      return this.player.playBroadcast(broadcast, options);
    }
  
    /**
     * Creates a VoiceReceiver so you can start listening to voice data.
     * It's recommended to only create one of these.
     * @returns {VoiceReceiver}
     */
    createReceiver() {
      const receiver = new VoiceReceiver(this);
      this.receivers.push(receiver);
      return receiver;
    }
  }
  
  module.exports = VoiceConnection;
  