text: const _ = require('../utils/underscore.js');
  const Q = require('bluebird');
  const EventEmitter = require('events').EventEmitter;
  
  const log = require('../utils/logger').create('Sockets');
  
  const CONNECT_INTERVAL_MS = 1000;
  const CONNECT_TIMEOUT_MS = 3000;
  
  /**
   * Socket connecting to Ethereum Node.
   */
  class Socket extends EventEmitter {
    constructor(socketMgr, id) {
      super();
  
      this._mgr = socketMgr;
      this._id = id;
  
      this._log = log.create(this._id);
  
      this._state = null;
    }
  
    get id() {
      return this._id;
    }
  
    get isConnected() {
      return STATE.CONNECTED === this._state;
    }
  
    /**
     * Connect to host.
     * @param  {Object} connectConfig
     * @param  {Object} [options]
     * @param  {Number} [options.timeout] Milliseconds to wait before timeout (default is 5000).
     * @return {Promise}
     */
    connect(connectConfig, options) {
      this._log.info(`Connect to ${JSON.stringify(connectConfig)}`);
  
      options = _.extend(
        {
          timeout: CONNECT_TIMEOUT_MS
        },
        options
      );
  
      return this._resetSocket().then(() => {
        let connectTimerId = null;
        let timeoutTimerId = null;
  
        this._log.debug('Connecting...');
  
        this._log.debug(
          `Will wait ${options.timeout}ms for connection to happen.`
        );
  
        this._state = STATE.CONNECTING;
  
        return new Q((resolve, reject) => {
          this._socket.once('connect', () => {
            if (STATE.CONNECTING === this._state) {
              this._log.info('Connected!');
  
              this._state = STATE.CONNECTED;
  
              clearTimeout(connectTimerId);
              clearTimeout(timeoutTimerId);
  
              this.emit('connect');
  
              resolve();
            }
          });
  
          this._socket.on('error', err => {
            if (STATE.CONNECTING === this._state) {
              this._log.warn(
                `Connection failed, retrying after ${CONNECT_INTERVAL_MS}ms...`
              );
  
              connectTimerId = setTimeout(() => {
                this._socket.connect(connectConfig);
              }, CONNECT_INTERVAL_MS);
            }
          });
  
          timeoutTimerId = setTimeout(() => {
            if (STATE.CONNECTING === this._state) {
              this._log.error(`Connection failed (${options.timeout}ms elapsed)`);
  
              this._state = STATE.CONNECTION_TIMEOUT;
  
              clearTimeout(connectTimerId);
  
              return reject(new Error('Unable to connect to socket: timeout'));
            }
          }, options.timeout);
  
          // initial kick-off
          this._socket.connect(connectConfig);
        });
      });
    }
  
    resume() {
      this._socket.resume.apply(this, arguments);
    }
  
    pause() {
      this._socket.pause.apply(this, arguments);
    }
  
    pipe() {
      this._socket.pipe.apply(this, arguments);
    }
  
    /**
     * Disconnect from socket.
     * @return {Promise}
     */
    disconnect(options) {
      if (!this._disconnectPromise) {
        this._disconnectPromise = new Q((resolve, reject) => {
          this._log.info('Disconnecting...');
  
          this._state = STATE.DISCONNECTING;
  
          // remove all existing listeners
          this._socket.removeAllListeners();
  
          const timer = setTimeout(() => {
            log.warn('Disconnection timed out, closing socket anyway...');
  
            this._state = STATE.DISCONNECTION_TIMEOUT;
  
            resolve();
          }, 5000 /* wait 5 seconds for disconnection */);
  
          this._socket.once('close', () => {
            // if we manually killed it then all good
            if (STATE.DISCONNECTING === this._state) {
              this._log.debug('Disconnected as expected');
            } else {
              this._log.warn('Unexpectedly disconnected');
            }
  
            this._state = STATE.DISCONNECTED;
  
            clearTimeout(timer);
  
            resolve();
          });
  
          this._socket.destroy();
        }).finally(() => {
          this._disconnectPromise = null;
        });
      }
  
      return this._disconnectPromise;
    }
  
    /**
     * An alias to `disconnect()`.
     * @return {Promise}
     */
    destroy() {
      this.removeAllListeners();
      return this.disconnect();
    }
  
    /**
     * Write data to socket.
     * @param  {String}   data
     */
    write(data) {
      if (STATE.CONNECTED !== this._state) {
        throw new Error('Socket not connected');
      }
  
      this._log.trace('Write data', data);
  
      this._socket.write(data);
    }
  
    /**
     * Reset socket.
     *
     * Upon completion `this._socket` will be set to a valid socket object, but
     * not yet connected.
     *
     * To be implemented by subclasses.
     */
    _resetSocket() {
      return Q.reject(new Error('Not yet implemented'));
    }
  }
  
  exports.Socket = Socket;
  
  const STATE = (exports.STATE = Socket.STATE = {
    CREATED: 0,
    CONNECTING: 1,
    CONNECTED: 2,
    DISCONNECTING: 3,
    DISCONNECTED: 4,
    ERROR: -1,
    DISCONNECTION_TIMEOUT: -2,
    CONNECTION_TIMEOUT: -3
  });
  