text: 'use strict';
  
  const EventEmitter = require('events');
  const util = require('util');
  const Queue = require('denque');
  const Net = require('net');
  const PacketInputStream = require('./io/packet-input-stream');
  const PacketOutputStream = require('./io/packet-output-stream');
  const CompressionInputStream = require('./io/compression-input-stream');
  const CompressionOutputStream = require('./io/compression-output-stream');
  const ServerStatus = require('./const/server-status');
  const ConnectionInformation = require('./misc/connection-information');
  const tls = require('tls');
  const Errors = require('./misc/errors');
  const Utils = require('./misc/utils');
  const Capabilities = require('./const/capabilities');
  const moment = require('moment-timezone');
  
  /*commands*/
  const Handshake = require('./cmd/handshake/handshake');
  const Quit = require('./cmd/quit');
  const Ping = require('./cmd/ping');
  const Reset = require('./cmd/reset');
  const Query = require('./cmd/query');
  const BatchRewrite = require('./cmd/batch-rewrite');
  const BatchBulk = require('./cmd/batch-bulk');
  const Stream = require('./cmd/stream');
  const ChangeUser = require('./cmd/change-user');
  const { Status } = require('./const/connection_status');
  
  /**
   * New Connection instance.
   *
   * @param options    connection options
   * @returns Connection instance
   * @constructor
   * @fires Connection#connect
   * @fires Connection#end
   * @fires Connection#error
   *
   */
  function Connection(options) {
    //*****************************************************************
    // public API functions
    //*****************************************************************
  
    /**
     * Connect event
     *
     * @returns {Promise} promise
     */
    this.connect = () => {
      switch (_status) {
        case Status.NOT_CONNECTED:
          _status = Status.CONNECTING;
          return new Promise(function(resolve, reject) {
            _registerHandshakeCmd(resolve, reject);
          });
  
        case Status.CLOSING:
        case Status.CLOSED:
          return Promise.reject(
            Errors.createError(
              'Connection closed',
              true,
              info,
              '08S01',
              Errors.ER_CONNECTION_ALREADY_CLOSED
            )
          );
  
        case Status.CONNECTING:
        case Status.AUTHENTICATING:
          return Promise.reject(
            Errors.createError(
              'Connection is already connecting',
              true,
              info,
              '08S01',
              Errors.ER_ALREADY_CONNECTING
            )
          );
      }
      //status Connected
      return Promise.resolve(this);
    };
  
    /**
     * Permit to change user during connection.
     * All user variables will be reset, Prepare commands will be released.
     * !!! mysql has a bug when CONNECT_ATTRS capability is set, that is default !!!!
     *
     * @param options   connection options
     * @returns {Promise} promise
     */
    this.changeUser = options => {
      if (!info.isMariaDB()) {
        return Promise.reject(
          Errors.createError(
            'method changeUser not available for MySQL server due to Bug #83472',
            false,
            info,
            '0A000',
            Errors.ER_MYSQL_CHANGE_USER_BUG
          )
        );
      }
  
      return new Promise(function(resolve, reject) {
        _addCommand(
          new ChangeUser(
            options,
            res => {
              if (options && options.collation) opts.collation = options.collation;
              resolve(res);
            },
            _authFailHandler.bind(this, reject),
            _addCommand.bind(this)
          )
        );
      });
    };
  
    /**
     * Start transaction
     *
     * @returns {Promise} promise
     */
    this.beginTransaction = () => {
      return this.query('START TRANSACTION');
    };
  
    /**
     * Commit a transaction.
     *
     * @returns {Promise} command if commit was needed only
     */
    this.commit = () => {
      return _changeTransaction('COMMIT');
    };
  
    /**
     * Roll back a transaction.
     *
     * @returns {Promise} promise
     */
    this.rollback = () => {
      return _changeTransaction('ROLLBACK');
    };
  
    /**
     * Execute query using text protocol.
     *
     * @param sql     sql parameter Object can be used to supersede default option.
     *                Object must then have sql property.
     * @param values  object / array of placeholder values (not mandatory)
     * @returns {Promise} promise
     */
    this.query = (sql, values) => {
      let _cmdOpt,
        _sql,
        _values = values;
      if (typeof sql === 'object') {
        _cmdOpt = sql;
        _sql = _cmdOpt.sql;
        if (_cmdOpt.values) _values = _cmdOpt.values;
      } else {
        _sql = sql;
      }
  
      return new Promise(function(resolve, reject) {
        const cmd = new Query(resolve, reject, _cmdOpt, opts, _sql, _values);
        if (opts.trace) Error.captureStackTrace(cmd);
        _addCommand(cmd);
      });
    };
  
    /**
     * Execute batch using text protocol.
     *
     * @param sql     sql parameter Object can be used to supersede default option.
     *                Object must then have sql property.
     * @param initialValues  object / array of placeholder values (not mandatory)
     * @returns {Promise} promise
     */
    this.batch = (sql, initialValues) => {
      let _options,
        _sql,
        _values = initialValues;
      if (typeof sql === 'object') {
        _options = sql;
        _sql = _options.sql;
        if (_options.values) _values = _options.values;
      } else {
        _sql = sql;
      }
  
      if (!_values) {
        return Promise.reject(
          Errors.createError(
            'Batch must have values set\nsql: ' + _sql + ' - parameters:[]',
            false,
            info,
            'HY000',
            Errors.ER_BATCH_WITH_NO_VALUES
          )
        );
      }
  
      const vals = Array.isArray(_values) ? _values : [_values];
  
      return new Promise(function(resolve, reject) {
        let useBulk = canUseBulk(vals);
  
        const cmd = useBulk
          ? new BatchBulk(resolve, reject, _options, opts, _sql, vals)
          : new BatchRewrite(resolve, reject, _options, opts, _sql, vals);
        if (opts.trace) Error.captureStackTrace(cmd);
        _addCommand(cmd);
      });
    };
  
    /**
     * Execute query returning a Readable Object that will emit columns/data/end/error events
     * to permit streaming big result-set
     *
     * @param sql     sql parameter Object can be used to supersede default option.
     *                Object must then have sql property.
     * @param values  object / array of placeholder values (not mandatory)
     * @returns {Readable}
     */
    this.queryStream = (sql, values) => {
      let _cmdOpt,
        _sql,
        _values = values;
      if (typeof sql === 'object') {
        _cmdOpt = sql;
        _sql = _cmdOpt.sql;
        if (sql.values) _values = sql.values;
      } else {
        _sql = sql;
      }
  
      const cmd = new Stream(_cmdOpt, opts, _sql, _values, _socket);
      if (opts.trace) Error.captureStackTrace(cmd);
      _addCommand(cmd);
      return cmd.inStream;
    };
  
    /**
     * Send an empty MySQL packet to ensure connection is active, and reset @@wait_timeout
     *
     * @returns {Promise} promise
     */
    this.ping = () => {
      return new Promise(function(resolve, reject) {
        return _addCommand(new Ping(resolve, reject));
      });
    };
  
    /**
     * Send a reset command that will
     * - rollback any open transaction
     * - reset transaction isolation level
     * - reset session variables
     * - delete user variables
     * - remove temporary tables
     * - remove all PREPARE statement
     *
     * @returns {Promise} promise
     */
    this.reset = () => {
      if (
        (info.isMariaDB() && info.hasMinVersion(10, 2, 4)) ||
        (!info.isMariaDB() && info.hasMinVersion(5, 7, 3))
      ) {
        return new Promise(function(resolve, reject) {
          return _addCommand(new Reset(resolve, reject));
        });
      }
      return Promise.reject(
        new Error(
          'Reset command not permitted for server ' +
            this.info.serverVersion +
            ' (requires server MariaDB version 10.2.4+ or MySQL 5.7.3+)'
        )
      );
    };
  
    /**
     * Indicates the state of the connection as the driver knows it
     * @returns {boolean}
     */
    this.isValid = () => {
      return _status === Status.CONNECTED;
    };
  
    /**
     * Terminate connection gracefully.
     *
     * @returns {Promise} promise
     */
    this.end = () => {
      _addCommand = _addCommandDisabled;
      clearTimeout(_timeout);
  
      if (
        _status !== Status.CLOSING &&
        _status !== Status.CLOSED &&
        _status !== Status.NOT_CONNECTED
      ) {
        _status = Status.CLOSING;
        return new Promise(function(resolve, reject) {
          const ended = () => {
            _status = Status.CLOSED;
            _socket.destroy();
            _socket.unref();
            _clear();
            _receiveQueue.clear();
            resolve();
          };
          const quitCmd = new Quit(ended, ended);
          _sendQueue.push(quitCmd);
          _receiveQueue.push(quitCmd);
          if (_sendQueue.length === 1) {
            process.nextTick(_nextSendCmd.bind(this));
          }
        });
      }
      return Promise.resolve();
    };
  
    /**
     * Alias for destroy.
     */
    this.close = function() {
      this.destroy();
    };
  
    /**
     * Force connection termination by closing the underlying socket and killing server process if any.
     */
    this.destroy = () => {
      _addCommand = _addCommandDisabled;
      clearTimeout(_timeout);
      if (_status !== Status.CLOSING && _status !== Status.CLOSED) {
        _status = Status.CLOSING;
        _sendQueue.clear();
        if (_receiveQueue.length > 0) {
          //socket is closed, but server may still be processing a huge select
          //only possibility is to kill process by another thread
          //TODO reuse a pool connection to avoid connection creation
          const self = this;
          const killCon = new Connection(opts);
          killCon
            .connect()
            .then(() => {
              //*************************************************
              //kill connection
              //*************************************************
              const killResHandler = () => {
                const destroyError = Errors.createError(
                  'Connection destroyed, command was killed',
                  true,
                  info,
                  '08S01',
                  Errors.ER_CMD_NOT_EXECUTED_DESTROYED
                );
                socketErrorDispatchToQueries(destroyError);
                process.nextTick(() => {
                  if (_socket) _socket.destroy();
                });
                _status = Status.CLOSED;
                killCon.end().catch(() => {});
              };
  
              killCon
                .query('KILL ' + info.threadId)
                .then(killResHandler)
                .catch(killResHandler);
            })
            .catch(err => {
              //*************************************************
              //failing to create a kill connection, end normally
              //*************************************************
              const ended = () => {
                let sock = _socket;
                _clear();
                _status = Status.CLOSED;
                setImmediate(resolve);
                sock.destroy();
                _receiveQueue.clear();
              };
              const quitCmd = new Quit(ended, ended);
              _sendQueue.push(quitCmd);
              _receiveQueue.push(quitCmd);
              if (_sendQueue.length === 1) {
                process.nextTick(_nextSendCmd.bind(self));
              }
            });
        } else {
          _status = Status.CLOSED;
          _socket.destroy();
        }
      }
      _clear();
    };
  
    this.pause = () => {
      _socket.pause();
    };
  
    this.resume = () => {
      _socket.resume();
    };
  
    this.format = (sql, values) => {
      throw Errors.createError(
        '"Connection.format intentionally not implemented. please use Connection.query(sql, values), it will be more secure and faster',
        false,
        info,
        '0A000',
        Errors.ER_NOT_IMPLEMENTED_FORMAT
      );
    };
  
    //*****************************************************************
    // additional public methods
    //*****************************************************************
  
    /**
     * return current connected server version information.
     *
     * @returns {*}
     */
    this.serverVersion = () => {
      if (!info.serverVersion)
        throw new Error('cannot know if server information until connection is established');
      return info.serverVersion.raw;
    };
  
    /**
     * Change option "debug" during connection.
     * @param val   debug value
     */
    this.debug = val => {
      opts.debug = val;
      opts.emit('debug', opts.logPackets, opts.debug);
    };
  
    this.debugCompress = val => {
      opts.debugCompress = val;
    };
  
    //*****************************************************************
    // internal public testing methods
    //*****************************************************************
  
    function TestMethods() {}
  
    TestMethods.prototype.getCollation = () => {
      return opts.collation;
    };
  
    TestMethods.prototype.getSocket = () => {
      return _socket;
    };
  
    this.__tests = new TestMethods();
  
    //*****************************************************************
    // internal methods
    //*****************************************************************
  
    this._status = () => {
      return _status;
    };
  
    /**
     * Execute query using text protocol with callback emit columns/data/end/error
     * events to permit streaming big result-set
     *
     * @param sql     sql parameter Object can be used to supersede default option.
     *                Object must then have sql property.
     * @param values  object / array of placeholder values (not mandatory)
     * @param cb      callback
     * @returns {Query} query
     */
    this._queryCallback = (sql, values, cb) => {
      let _cmdOpts,
        _sql,
        _values = values,
        _cb = cb;
  
      if (typeof values === 'function') {
        _cb = values;
        _values = undefined;
      }
  
      if (typeof sql === 'object') {
        _cmdOpts = sql;
        _sql = _cmdOpts.sql;
        if (sql.values) _values = sql.values;
      } else {
        _sql = sql;
      }
  
      let cmd;
      if (_cb) {
        const resolve = rows => {
          const meta = rows.meta;
          delete rows.meta;
          _cb(null, rows, meta);
        };
        cmd = new Query(resolve, _cb, _cmdOpts, opts, _sql, _values);
      } else {
        cmd = new Query(
          () => {},
          () => {},
          _cmdOpts,
          opts,
          _sql,
          _values
        );
      }
      cmd.handleNewRows = row => {
        cmd._rows[cmd._responseIndex].push(row);
        cmd.emit('data', row);
      };
  
      if (opts.trace) Error.captureStackTrace(cmd);
      _addCommand(cmd);
      return cmd;
    };
  
    /**
     * Execute a batch using text protocol with callback emit columns/data/end/error
     * events to permit streaming big result-set
     *
     * @param sql     sql parameter Object can be used to supersede default option.
     *                Object must then have sql property.
     * @param values  object / array of placeholder values (not mandatory)
     * @param cb      callback
     * @returns {Query} query
     */
    this._batchCallback = (sql, values, cb) => {
      let _cmdOpts,
        _sql,
        _values = values,
        _cb = cb;
  
      if (typeof values === 'function') {
        _cb = values;
        _values = undefined;
      }
  
      if (typeof sql === 'object') {
        _cmdOpts = sql;
        _sql = _cmdOpts.sql;
        if (sql.values) _values = sql.values;
      } else {
        _sql = sql;
      }
  
      if (_values !== undefined) {
        _values = Array.isArray(_values) ? _values : [_values];
      }
  
      let cmd;
  
      if (!_values) {
        if (_cb) {
          _cb(
            Errors.createError(
              'Batch must have values set\nsql: ' + _sql + ' - parameters:[]',
              false,
              info,
              'HY000',
              Errors.ER_BATCH_WITH_NO_VALUES
            )
          );
        }
        return null;
      }
  
      let useBulk = canUseBulk(_values);
  
      const fct = useBulk ? BatchBulk : BatchRewrite;
  
      if (_cb) {
        const resolve = rows => {
          const meta = rows.meta;
          delete rows.meta;
          _cb(null, rows, meta);
        };
        cmd = new fct(resolve, _cb, _cmdOpts, opts, _sql, _values);
      } else {
        cmd = new fct(
          () => {},
          () => {},
          _cmdOpts,
          opts,
          _sql,
          _values
        );
      }
      cmd.handleNewRows = row => {
        cmd._rows[cmd._responseIndex].push(row);
        cmd.emit('data', row);
      };
  
      if (opts.trace) Error.captureStackTrace(cmd);
      _addCommand(cmd);
      return cmd;
    };
  
    /**
     * Use Batch rewrite or MariaDB bulk protocol.
     *
     * @param values current batch values
     * @return {boolean} indicating if must use rewrite or bulk
     */
    const canUseBulk = values => {
      let useBulk = info.isMariaDB() && info.hasMinVersion(10, 2, 7) && opts.bulk;
      if (useBulk) {
        //ensure that there is no stream object
        if (values !== undefined) {
          if (!opts.namedPlaceholders) {
            //ensure that all parameters have same length
            //single array is considered as an array of single element.
            const paramLen = Array.isArray(values[0]) ? values[0].length : values[0] ? 1 : 0;
            if (paramLen == 0) return false;
            for (let r = 0; r < values.length; r++) {
              let row = values[r];
              if (!Array.isArray(row)) row = [row];
              if (paramLen !== row.length) {
                return false;
              }
              for (let j = 0; j < paramLen; j++) {
                const val = row[j];
                if (
                  val !== null &&
                  typeof val === 'object' &&
                  typeof val.pipe === 'function' &&
                  typeof val.read === 'function'
                ) {
                  return false;
                }
              }
            }
          } else {
            for (let r = 0; r < values.length; r++) {
              let row = values[r];
              const keys = Object.keys(row);
              for (let j = 0; j < keys.length; j++) {
                const val = row[keys[j]];
                if (
                  val !== null &&
                  typeof val === 'object' &&
                  typeof val.pipe === 'function' &&
                  typeof val.read === 'function'
                ) {
                  return false;
                }
              }
            }
          }
        }
      }
      return useBulk;
    };
  
    /**
     * Add handshake command to queue.
     *
     * @private
     */
    const _registerHandshakeCmd = (resolve, rejected) => {
      const _authFail = _authFailHandler.bind(this, rejected);
      const _authSucceed = _authSucceedHandler.bind(this, resolve, _authFail);
  
      const handshake = new Handshake(
        _authSucceed,
        _authFail,
        _createSecureContext.bind(this, _authFail),
        _addCommand.bind(this),
        _getSocket
      );
      Error.captureStackTrace(handshake);
  
      handshake.once('end', () => {
        process.nextTick(_nextSendCmd);
      });
  
      _receiveQueue.push(handshake);
      _initSocket(_authFail);
    };
  
    const _executeSessionVariableQuery = () => {
      if (opts.sessionVariables) {
        const values = [];
        let sessionQuery = 'set ';
        let keys = Object.keys(opts.sessionVariables);
        if (keys.length > 0) {
          return new Promise(function(resolve, reject) {
            for (let k = 0; k < keys.length; ++k) {
              sessionQuery +=
                (k !== 0 ? ',' : '') + '@@' + keys[k].replace(/[^a-z0-9_]/gi, '') + '=?';
              values.push(opts.sessionVariables[keys[k]]);
            }
            const errorHandling = initialErr => {
              reject(
                Errors.createError(
                  'Error setting session variable (value ' +
                    JSON.stringify(opts.sessionVariables) +
                    '). Error: ' +
                    initialErr.message,
                  true,
                  info,
                  '08S01',
                  Errors.ER_SETTING_SESSION_ERROR,
                  null
                )
              );
            };
            const cmd = new Query(resolve, errorHandling, null, opts, sessionQuery, values);
            if (opts.trace) Error.captureStackTrace(cmd);
            _addCommand(cmd);
          });
        }
      }
      return Promise.resolve();
    };
  
    /**
     * Asking server timezone if not set in case of 'auto'
     * @returns {Promise<void>}
     * @private
     */
    const _checkServerTimezone = () => {
      if (opts.timezone !== 'auto') {
        return Promise.resolve();
      }
      return this.query('SELECT @@system_time_zone stz, @@time_zone tz').then(res => {
        const serverTimezone = res[0].tz === 'SYSTEM' ? res[0].stz : res[0].tz;
        const serverZone = moment.tz.zone(serverTimezone);
        if (serverZone) {
          if (serverTimezone === moment.tz.guess()) {
            //db server and client use same timezone, avoid any conversion
            opts.tz = null;
          } else {
            opts.tz = moment.tz.setDefault(serverTimezone);
          }
        } else {
          return Promise.reject(
            Errors.createError(
              "Automatic timezone setting fails. Server timezone '" +
                serverTimezone +
                "' does't have a corresponding IANA timezone. Option timezone must be set according to server timezone",
              true,
              info,
              '08S01',
              Errors.ER_WRONG_AUTO_TIMEZONE
            )
          );
        }
        return Promise.resolve();
      });
    };
  
    const _checkServerVersion = () => {
      if (!opts.forceVersionCheck) {
        return Promise.resolve();
      }
      return this.query('SELECT @@VERSION AS v').then(res => {
        info.serverVersion.raw = res[0].v;
        info.serverVersion.mariaDb = info.serverVersion.raw.includes('MariaDB');
        ConnectionInformation.parseVersionString(info);
        return Promise.resolve();
      });
    };
  
    const _executeInitQuery = () => {
      if (opts.initSql) {
        const initialArr = Array.isArray(opts.initSql) ? opts.initSql : [opts.initSql];
        const initialPromises = [];
        initialArr.forEach(sql => {
          initialPromises.push(
            new Promise(function(resolve, reject) {
              const errorHandling = initialErr => {
                reject(
                  Errors.createError(
                    'Error executing initial sql command: ' + initialErr.message,
                    true,
                    info,
                    '08S01',
                    Errors.ER_INITIAL_SQL_ERROR,
                    null
                  )
                );
              };
              const cmd = new Query(resolve, errorHandling, null, opts, sql, null);
              if (opts.trace) Error.captureStackTrace(cmd);
              _addCommand(cmd);
            })
          );
        });
        return Promise.all(initialPromises);
      }
      return Promise.resolve();
    };
  
    const _executeSessionTimeout = () => {
      if (opts.queryTimeout) {
        if (info.isMariaDB() && info.hasMinVersion(10, 1, 2)) {
          return new Promise(function(resolve, reject) {
            const errorHandling = initialErr => {
              reject(
                Errors.createError(
                  'Error setting session queryTimeout: ' + initialErr.message,
                  true,
                  info,
                  '08S01',
                  Errors.ER_INITIAL_TIMEOUT_ERROR,
                  null
                )
              );
            };
            const cmd = new Query(
              resolve,
              errorHandling,
              null,
              opts,
              'SET max_statement_time=' + opts.queryTimeout / 1000,
              null
            );
            if (opts.trace) Error.captureStackTrace(cmd);
            _addCommand(cmd);
          });
        } else {
          return Promise.reject(
            Errors.createError(
              'Can only use queryTimeout for MariaDB server after 10.1.1. queryTimeout value: ' +
                opts.queryTimeout,
              false,
              info,
              'HY000',
              Errors.ER_TIMEOUT_NOT_SUPPORTED
            )
          );
        }
      }
      return Promise.resolve();
    };
  
    const _getSocket = () => {
      return _socket;
    };
  
    /**
     * Initialize socket and associate events.
     * @private
     */
    const _initSocket = authFailHandler => {
      if (opts.socketPath) {
        _socket = Net.connect(opts.socketPath);
      } else {
        _socket = Net.connect(opts.port, opts.host);
      }
  
      if (opts.connectTimeout) {
        _timeout = setTimeout(
          _connectTimeoutReached,
          opts.connectTimeout,
          authFailHandler,
          Date.now()
        );
      }
  
      const _socketError = _socketErrorHandler.bind(this, authFailHandler);
  
      _socket.on('data', _in.onData.bind(_in));
      _socket.on('error', _socketError);
      _socket.on('end', _socketError);
      _socket.on(
        'connect',
        function() {
          clearTimeout(_timeout);
          if (_status === Status.CONNECTING) {
            _status = Status.AUTHENTICATING;
            _socketConnected = true;
            _socket.setTimeout(opts.socketTimeout, _socketTimeoutReached.bind(this, authFailHandler));
            _socket.setNoDelay(true);
          }
        }.bind(this)
      );
  
      _socket.writeBuf = _socket.write;
      _socket.flush = () => {};
      _out.setStream(_socket);
    };
  
    /**
     * Authentication success result handler.
     *
     * @private
     */
    const _authSucceedHandler = (resolve, rejected) => {
      //enable packet compression according to option
      if (opts.logPackets) info.enableLogPacket();
      if (opts.compress) {
        if (info.serverCapabilities & Capabilities.COMPRESS) {
          _out.setStream(new CompressionOutputStream(_socket, opts, info));
          _in = new CompressionInputStream(_in, _receiveQueue, opts, info);
          _socket.removeAllListeners('data');
          _socket.on('data', _in.onData.bind(_in));
        } else {
          console.error(
            "connection is configured to use packet compression, but the server doesn't have this capability"
          );
        }
      }
  
      if (opts.pipelining) {
        _addCommand = _addCommandEnablePipeline;
        const commands = _receiveQueue.toArray();
        commands.forEach(cmd => {
          const listeners = cmd.listeners('end');
          cmd.removeListener('end', listeners[0]);
          cmd.once('send_end', () => {
            setImmediate(_nextSendCmd);
          });
        });
      }
  
      const errorInitialQueries = err => {
        if (!err.fatal) this.end().catch(err => {});
        process.nextTick(rejected, err);
      };
  
      _executeSessionVariableQuery()
        .then(() => {
          return _checkServerTimezone();
        })
        .then(() => {
          return _checkServerVersion();
        })
        .then(() => {
          return _executeInitQuery();
        })
        .then(() => {
          return _executeSessionTimeout();
        })
        .then(() => {
          _status = Status.CONNECTED;
          process.nextTick(resolve, this);
        })
        .catch(errorInitialQueries);
    };
  
    /**
     * Authentication failed result handler.
     *
     * @private
     */
    const _authFailHandler = (reject, err) => {
      process.nextTick(reject, err);
      //remove handshake command
      _receiveQueue.shift();
  
      _fatalError(err, true);
    };
  
    /**
     * Create TLS socket and associate events.
     *
     * @param rejected  rejected function when error
     * @param callback  callback function when done
     * @private
     */
    const _createSecureContext = (rejected, callback) => {
      const _socketError = _socketErrorHandler.bind(this, rejected);
      const sslOption = Object.assign({}, opts.ssl, {
        servername: opts.host,
        socket: _socket
      });
  
      try {
        const secureSocket = tls.connect(sslOption, callback);
  
        secureSocket.on('data', _in.onData.bind(_in));
        secureSocket.on('error', _socketError);
        secureSocket.on('end', _socketError);
        secureSocket.writeBuf = secureSocket.write;
        secureSocket.flush = () => {};
  
        _socket.removeAllListeners('data');
        _socket = secureSocket;
  
        _out.setStream(secureSocket);
      } catch (err) {
        _socketError(err);
      }
    };
  
    /**
     * Handle packet when no packet is expected.
     * (there can be an ERROR packet send by server/proxy to inform that connection is ending).
     *
     * @param packet  packet
     * @private
     */
    const _unexpectedPacket = function(packet) {
      if (packet && packet.peek() === 0xff) {
        //can receive unexpected error packet from server/proxy
        //to inform that connection is closed (usually by timeout)
        let err = packet.readError(info);
        if (err.fatal && _status !== Status.CLOSING && _status !== Status.CLOSED) {
          this.emit('error', err);
          this.end();
        }
      } else if (_status !== Status.CLOSING && _status !== Status.CLOSED) {
        this.emit(
          'error',
          Errors.createError(
            'receiving packet from server without active commands\n' +
              'conn:' +
              (info.threadId ? info.threadId : -1) +
              '(' +
              packet.pos +
              ',' +
              packet.end +
              ')\n' +
              Utils.log(opts, packet.buf, packet.pos, packet.end),
            true,
            info,
            '08S01',
            Errors.ER_UNEXPECTED_PACKET
          )
        );
        this.destroy();
      }
    };
  
    /**
     * Change transaction state.
     *
     * @param sql sql
     * @returns {Promise} promise
     * @private
     */
    const _changeTransaction = sql => {
      //if command in progress, driver cannot rely on status and must execute query
      if (_status === Status.CLOSING || _status === Status.CLOSED) {
        return Promise.reject(
          Errors.createError(
            'Cannot execute new commands: connection closed\nsql: ' + sql,
            true,
            info,
            '08S01',
            Errors.ER_CMD_CONNECTION_CLOSED
          )
        );
      }
  
      //Command in progress => must execute query
      //or if no command in progress, can rely on status to know if query is needed
      if (_receiveQueue.peekFront() || info.status & ServerStatus.STATUS_IN_TRANS) {
        return new Promise(function(resolve, reject) {
          const cmd = new Query(resolve, reject, null, opts, sql, null);
          if (opts.trace) Error.captureStackTrace(cmd);
          _addCommand(cmd);
        });
      }
      return Promise.resolve();
    };
  
    /**
     * Handle connection timeout.
     *
     * @private
     */
    const _connectTimeoutReached = function(authFailHandler, initialConnectionTime) {
      _timeout = null;
      const handshake = _receiveQueue.peekFront();
      authFailHandler(
        Errors.createError(
          'Connection timeout: failed to create socket after ' +
            (Date.now() - initialConnectionTime) +
            'ms',
          true,
          info,
          '08S01',
          Errors.ER_CONNECTION_TIMEOUT,
          handshake ? handshake.stack : null
        )
      );
    };
  
    /**
     * Handle socket timeout.
     *
     * @private
     */
    const _socketTimeoutReached = function() {
      const err = Errors.createError('socket timeout', true, info, '08S01', Errors.ER_SOCKET_TIMEOUT);
      const packetMsgs = info.getLastPackets();
      if (packetMsgs !== '') {
        err.message = err.message + '\nlast received packets:\n' + packetMsgs;
      }
      _fatalError(err, true);
    };
  
    /**
     * Add command to command sending and receiving queue.
     *
     * @param cmd         command
     * @returns {*}       current command
     * @private
     */
    const _addCommandEnable = cmd => {
      cmd.once('end', () => {
        setImmediate(_nextSendCmd);
      });
  
      //send immediately only if no current active receiver
      if (_sendQueue.isEmpty() && _status === Status.CONNECTED) {
        if (_receiveQueue.peekFront()) {
          _receiveQueue.push(cmd);
          _sendQueue.push(cmd);
          return cmd;
        }
  
        _receiveQueue.push(cmd);
        cmd.start(_out, opts, info);
      } else {
        _receiveQueue.push(cmd);
        _sendQueue.push(cmd);
      }
      return cmd;
    };
  
    /**
     * Add command to command sending and receiving queue using pipelining
     *
     * @param cmd         command
     * @returns {*}       current command
     * @private
     */
    const _addCommandEnablePipeline = cmd => {
      cmd.once('send_end', () => {
        setImmediate(_nextSendCmd);
      });
  
      _receiveQueue.push(cmd);
      if (_sendQueue.isEmpty()) {
        cmd.start(_out, opts, info);
        if (cmd.sending) {
          _sendQueue.push(cmd);
          cmd.prependOnceListener('send_end', () => {
            _sendQueue.shift();
          });
        }
      } else {
        _sendQueue.push(cmd);
      }
      return cmd;
    };
  
    /**
     * Replacing command when connection is closing or closed to send a proper error message.
     *
     * @param cmd         command
     * @private
     */
    const _addCommandDisabled = cmd => {
      cmd.throwNewError(
        'Cannot execute new commands: connection closed\n' + cmd.displaySql(),
        true,
        info,
        '08S01',
        Errors.ER_CMD_CONNECTION_CLOSED
      );
    };
  
    /**
     * Handle socket error.
     *
     * @param authFailHandler   authentication handler
     * @param err               socket error
     * @private
     */
    const _socketErrorHandler = function(authFailHandler, err) {
      if (_status === Status.CLOSING || _status === Status.CLOSED) return;
  
      _socket.writeBuf = () => {};
      _socket.flush = () => {};
  
      //socket has been ended without error
      if (!err) {
        err = Errors.createError(
          'socket has unexpectedly been closed',
          true,
          info,
          '08S01',
          Errors.ER_SOCKET_UNEXPECTED_CLOSE
        );
      } else {
        err.fatal = true;
        this.sqlState = 'HY000';
      }
  
      const packetMsgs = info.getLastPackets();
      if (packetMsgs !== '') {
        err.message += '\nlast received packets:\n' + packetMsgs;
      }
  
      switch (_status) {
        case Status.CONNECTING:
        case Status.AUTHENTICATING:
          const currentCmd = _receiveQueue.peekFront();
          if (currentCmd && currentCmd.stack && err) {
            err.stack +=
              '\n From event:\n' + currentCmd.stack.substring(currentCmd.stack.indexOf('\n') + 1);
          }
          authFailHandler(err);
          break;
  
        default:
          _fatalError(err, false);
      }
    };
  
    /**
     * Fatal unexpected error : closing connection, and throw exception.
     *
     * @param self    current connection
     * @private
     */
    const _fatalErrorHandler = function(self) {
      return function(err, avoidThrowError) {
        if (_status === Status.CLOSING || _status === Status.CLOSED) {
          socketErrorDispatchToQueries(err);
          return;
        }
        const mustThrowError = _status !== Status.CONNECTING;
        _status = Status.CLOSING;
  
        //prevent executing new commands
        _addCommand = _addCommandDisabled;
  
        if (_socket) {
          _socket.removeAllListeners('error');
          _socket.removeAllListeners('timeout');
          _socket.removeAllListeners('close');
          _socket.removeAllListeners('data');
          if (!_socket.destroyed) _socket.destroy();
          _socket = undefined;
        }
        _status = Status.CLOSED;
  
        const errorThrownByCmd = socketErrorDispatchToQueries(err);
        if (mustThrowError) {
          if (self.listenerCount('error') > 0) {
            self.emit('error', err);
            self.emit('end');
            _clear();
          } else {
            self.emit('end');
            _clear();
            //error will be thrown if no error listener and no command did throw the exception
            if (!avoidThrowError && !errorThrownByCmd) throw err;
          }
        } else {
          _clear();
        }
      };
    };
  
    /**
     * Dispatch fatal error to current running queries.
     *
     * @param err        the fatal error
     * @return {boolean} return if error has been relayed to queries
     */
    const socketErrorDispatchToQueries = err => {
      let receiveCmd;
      let errorThrownByCmd = false;
      while ((receiveCmd = _receiveQueue.shift())) {
        if (receiveCmd && receiveCmd.onPacketReceive) {
          errorThrownByCmd = true;
          setImmediate(receiveCmd.throwError.bind(receiveCmd), err, info);
        }
      }
      return errorThrownByCmd;
    };
  
    /**
     * Will send next command in queue if any.
     *
     * @private
     */
    const _nextSendCmd = () => {
      let sendCmd;
      if ((sendCmd = _sendQueue.shift())) {
        if (sendCmd.sending) {
          _sendQueue.unshift(sendCmd);
        } else {
          sendCmd.start(_out, opts, info);
          if (sendCmd.sending) {
            sendCmd.prependOnceListener('send_end', () => {
              _sendQueue.shift();
            });
            _sendQueue.unshift(sendCmd);
          }
        }
      }
    };
  
    /**
     * Clearing connection variables when ending.
     *
     * @private
     */
    const _clear = () => {
      _sendQueue.clear();
      opts.removeAllListeners();
      _out = undefined;
      _socket = undefined;
    };
  
    //*****************************************************************
    // internal variables
    //*****************************************************************
  
    EventEmitter.call(this);
    const opts = Object.assign(new EventEmitter(), options);
    const info = new ConnectionInformation();
    const _sendQueue = new Queue();
    const _receiveQueue = new Queue();
    let _status = Status.NOT_CONNECTED;
    let _socketConnected = false;
    let _socket = null;
    let _timeout = null;
    let _addCommand = _addCommandEnable;
    const _fatalError = _fatalErrorHandler(this);
    let _out = new PacketOutputStream(opts, info);
    let _in = new PacketInputStream(_unexpectedPacket.bind(this), _receiveQueue, _out, opts, info);
  
    this.escape = Utils.escape.bind(this, opts, info);
    this.escapeId = Utils.escapeId.bind(this, opts, info);
  
    //add alias threadId for mysql/mysql2 compatibility
    Object.defineProperty(this, 'threadId', {
      get() {
        return info ? info.threadId : undefined;
      }
    });
    Object.defineProperty(this, 'info', {
      get() {
        return info;
      }
    });
  }
  
  util.inherits(Connection, EventEmitter);
  
  module.exports = Connection;
  