text: var util = require('util');
  var uuid = require('uuid');
  var EventEmitter = require('events').EventEmitter;
  
  var SimpleQueuedHandler = require('./simpleQueuedHandler');
  var TcpPackageConnection = require('../transport/tcp/tcpPackageConnection');
  var OperationsManager = require('./operationsManager');
  var SubscriptionsManager = require('./subscriptionsManager');
  var VolatileSubscriptionOperation = require('../clientOperations/volatileSubscriptionOperation');
  var ConnectToPersistentSubscriptionOperation = require('../clientOperations/connectToPersistentSubscriptionOperation');
  var messages = require('./messages');
  var ClientMessage = require('../messages/clientMessage');
  var createBufferSegment = require('../common/bufferSegment');
  
  var TcpPackage = require('../systemData/tcpPackage');
  var TcpCommand = require('../systemData/tcpCommand');
  var TcpFlags = require('../systemData/tcpFlags');
  var InspectionDecision = require('../systemData/inspectionDecision');
  
  const ConnectionState = Object.freeze({
    Init: 'init',
    Connecting: 'connecting',
    Connected: 'connected',
    Closed: 'closed'
  });
  
  const ConnectingPhase = Object.freeze({
    Invalid: 'invalid',
    Reconnecting: 'reconnecting',
    EndPointDiscovery: 'endpointDiscovery',
    ConnectionEstablishing: 'connectionEstablishing',
    Authentication: 'authentication',
    Identification: 'identification',
    Connected: 'connected'
  });
  
  const TimerPeriod = 200;
  const TimerTickMessage = new messages.TimerTickMessage();
  const EmptyGuid = '00000000-0000-0000-0000-000000000000';
  const ClientVersion = 1;
  
  /**
   * @private
   * @param {EventStoreNodeConnection} esConnection
   * @param {Object} settings
   * @constructor
   * @property {Number} totalOperationCount
   */
  function EventStoreConnectionLogicHandler(esConnection, settings) {
    EventEmitter.call(this);
    this._esConnection = esConnection;
    this._settings = settings;
    this._queue = new SimpleQueuedHandler();
    this._state = ConnectionState.Init;
    this._connectingPhase = ConnectingPhase.Invalid;
    this._endpointDiscoverer = null;
    this._connection = null;
    this._wasConnected = false;
    this._packageNumber = 0;
    this._authInfo = null;
    this._lastTimeoutsTimeStamp = 0;
  
    this._operations = new OperationsManager(esConnection.connectionName, settings);
    this._subscriptions = new SubscriptionsManager(esConnection.connectionName, settings);
  
    var self = this;
    this._queue.registerHandler(messages.StartConnectionMessage, function(msg) {
      self._startConnection(msg.cb, msg.endpointDiscoverer);
    });
    this._queue.registerHandler(messages.CloseConnectionMessage, function(msg) {
      self._closeConnection(msg.reason, msg.error);
    });
  
    this._queue.registerHandler(messages.StartOperationMessage, function(msg) {
      self._startOperation(msg.operation, msg.maxRetries, msg.timeout);
    });
    this._queue.registerHandler(messages.StartSubscriptionMessage, function(msg) {
      self._startSubscription(msg);
    });
    this._queue.registerHandler(messages.StartPersistentSubscriptionMessage, function(msg) {
      self._startPersistentSubscription(msg);
    });
  
    this._queue.registerHandler(messages.EstablishTcpConnectionMessage, function(msg) {
      self._establishTcpConnection(msg.endPoints);
    });
    this._queue.registerHandler(messages.TcpConnectionEstablishedMessage, function(msg) {
      self._tcpConnectionEstablished(msg.connection);
    });
    this._queue.registerHandler(messages.TcpConnectionErrorMessage, function(msg) {
      self._tcpConnectionError(msg.connection, msg.error);
    });
    this._queue.registerHandler(messages.TcpConnectionClosedMessage, function(msg) {
      self._tcpConnectionClosed(msg.connection, msg.error);
    });
    this._queue.registerHandler(messages.HandleTcpPackageMessage, function(msg) {
      self._handleTcpPackage(msg.connection, msg.package);
    });
  
    this._queue.registerHandler(messages.TimerTickMessage, function(msg) {
      self._timerTick();
    });
  
    this._timer = setInterval(function() {
      self.enqueueMessage(TimerTickMessage);
    }, TimerPeriod);
  }
  util.inherits(EventStoreConnectionLogicHandler, EventEmitter);
  
  Object.defineProperty(EventStoreConnectionLogicHandler.prototype, 'totalOperationCount', {
    get: function() {
      return this._operations.totalOperationCount;
    }
  });
  
  EventStoreConnectionLogicHandler.prototype.enqueueMessage = function(msg) {
    if (this._settings.verboseLogging && msg !== TimerTickMessage) this._logDebug("enqueuing message %s.", msg);
    this._queue.enqueueMessage(msg);
  };
  
  EventStoreConnectionLogicHandler.prototype._discoverEndpoint = function(cb) {
    this._logDebug('DiscoverEndpoint');
  
    if (this._state !== ConnectionState.Connecting) return;
    if (this._connectingPhase !== ConnectingPhase.Reconnecting) return;
  
    this._connectingPhase = ConnectingPhase.EndPointDiscovery;
  
    cb = cb || function() {};
  
    var self = this;
    this._endpointDiscoverer.discover(this._connection !== null ? this._connection.remoteEndPoint : null)
        .then(function(nodeEndpoints){
          self.enqueueMessage(new messages.EstablishTcpConnectionMessage(nodeEndpoints));
          cb();
        })
        .catch(function(err) {
          self.enqueueMessage(new messages.CloseConnectionMessage("Failed to resolve TCP end point to which to connect.", err));
          cb(new Error("Couldn't resolve target end point: " + err.message));
        });
  };
  
  /**
   * @param {Function} cb
   * @param {StaticEndpointDiscoverer} endpointDiscoverer
   * @private
   */
  EventStoreConnectionLogicHandler.prototype._startConnection = function(cb, endpointDiscoverer) {
    this._logDebug('StartConnection');
  
    switch(this._state) {
      case ConnectionState.Init:
        this._endpointDiscoverer = endpointDiscoverer;
        this._state = ConnectionState.Connecting;
        this._connectingPhase = ConnectingPhase.Reconnecting;
        this._discoverEndpoint(cb);
        break;
      case ConnectionState.Connecting:
      case ConnectionState.Connected:
        return cb(new Error(['EventStoreConnection', this._esConnection.connectionName, 'is already active.'].join(' ')));
      case ConnectionState.Closed:
        return cb(new Error(['EventStoreConnection', this._esConnection.connectionName, 'is closed.'].join(' ')));
      default:
        return cb(new Error(['Unknown state:', this._state].join(' ')));
    }
  };
  
  /**
   * @param {string} reason
   * @param {Error} [error]
   * @private
   */
  EventStoreConnectionLogicHandler.prototype._closeConnection = function(reason, error) {
    if (this._state === ConnectionState.Closed) {
      this._logDebug("CloseConnection IGNORED because is ESConnection is CLOSED, reason %s, error %s.", reason, error ? error.stack : '');
      return;
    }
  
    this._logDebug("CloseConnection, reason %s, error %s.", reason, error ? error.stack : '');
  
    this._state = ConnectionState.Closed;
  
    clearInterval(this._timer);
    this._operations.cleanUp();
    this._subscriptions.cleanUp();
    this._closeTcpConnection(reason);
  
    this._logInfo("Closed. Reason: %s", reason);
  
    if (error) this.emit('error', error);
  
    this.emit('closed', reason);
  };
  
  EventStoreConnectionLogicHandler.prototype._closeTcpConnection = function(reason) {
    if (!this._connection) {
      this._logDebug("CloseTcpConnection IGNORED because _connection === null");
      return;
    }
  
    this._logDebug("CloseTcpConnection");
    this._connection.close(reason);
    this._tcpConnectionClosed(this._connection);
    this._connection = null;
  };
  
  var _nextSeqNo = 0;
  function createOperationItem(operation, maxRetries, timeout) {
    var operationItem = {
      seqNo: _nextSeqNo++,
      operation: operation,
      maxRetries: maxRetries,
      timeout: timeout,
      createdTime: Date.now(),
      correlationId: uuid.v4(),
      retryCount: 0,
      lastUpdated: Date.now()
    };
    operationItem.toString = (function() {
      return util.format("Operation %s (%s): %s, retry count: %d, created: %s, last updated: %s",
                         this.operation.constructor.name, this.correlationId, this.operation, this.retryCount,
                         new Date(this.createdTime).toISOString().substr(11,12),
                         new Date(this.lastUpdated).toISOString().substr(11,12));
    }).bind(operationItem);
    return operationItem;
  }
  
  EventStoreConnectionLogicHandler.prototype._startOperation = function(operation, maxRetries, timeout) {
    switch(this._state) {
      case ConnectionState.Init:
        operation.fail(new Error("EventStoreConnection '" + this._esConnection.connectionName + "' is not active."));
        break;
      case ConnectionState.Connecting:
        this._logDebug("StartOperation enqueue %s, %s, %d, %d.", operation.constructor.name, operation, maxRetries, timeout);
        this._operations.enqueueOperation(createOperationItem(operation, maxRetries, timeout));
        break;
      case ConnectionState.Connected:
        this._logDebug("StartOperation schedule %s, %s, %d, %d.", operation.constructor.name, operation, maxRetries, timeout);
        this._operations.scheduleOperation(createOperationItem(operation, maxRetries, timeout), this._connection);
        break;
      case ConnectionState.Closed:
        operation.fail(new Error("EventStoreConnection '" + this._esConnection.connectionName + "' is closed."));
        break;
      default:
        throw new Error("Unknown state: " + this._state + '.');
    }
  };
  
  function createSubscriptionItem(operation, maxRetries, timeout) {
    var subscriptionItem = {
      operation: operation,
      maxRetries: maxRetries,
      timeout: timeout,
      createdTime: Date.now(),
      correlationId: uuid.v4(),
      retryCount: 0,
      lastUpdated: Date.now(),
      isSubscribed: false
    };
    subscriptionItem.toString = (function(){
      return util.format("Subscription %s (%s): %s, is subscribed: %s, retry count: %d, created: %s, last updated: %s",
          this.operation.constructor.name, this.correlationId, this.operation, this.isSubscribed, this.retryCount,
          new Date(this.createdTime).toISOString().substr(11,12),
          new Date(this.lastUpdated).toISOString().substr(11,12));
    }).bind(subscriptionItem);
    return subscriptionItem;
  }
  
  EventStoreConnectionLogicHandler.prototype._startSubscription = function(msg) {
    switch (this._state)
    {
      case ConnectionState.Init:
        msg.cb(new Error(util.format("EventStoreConnection '%s' is not active.", this._esConnection.connectionName)));
        break;
      case ConnectionState.Connecting:
      case ConnectionState.Connected:
        var self = this;
        var operation = new VolatileSubscriptionOperation(this._settings.log, msg.cb, msg.streamId, msg.resolveLinkTos,
                msg.userCredentials, msg.eventAppeared, msg.subscriptionDropped,
                this._settings.verboseLogging, function() { return self._connection });
        this._logDebug("StartSubscription %s %s, %s, %d, %d.",
            this._state === ConnectionState.Connected ? "fire" : "enqueue",
            operation.constructor.name, operation, msg.maxRetries, msg.timeout);
        var subscription = createSubscriptionItem(operation, msg.maxRetries, msg.timeout);
        if (this._state === ConnectionState.Connecting) {
          this._subscriptions.enqueueSubscription(subscription);
        } else {
          this._subscriptions.startSubscription(subscription, this._connection);
        }
        break;
      case ConnectionState.Closed:
        msg.cb(new Error("Connection closed. Connection: " + this._esConnection.connectionName));
        break;
      default:
        throw new Error(util.format("Unknown state: %s.", this._state));
    }
  };
  
  EventStoreConnectionLogicHandler.prototype._startPersistentSubscription = function(msg) {
    var self = this;
    switch (this._state)
    {
      case ConnectionState.Init:
        msg.cb(new Error(util.format("EventStoreConnection '%s' is not active.", this._esConnection.connectionName)));
        break;
      case ConnectionState.Connecting:
      case ConnectionState.Connected:
        var operation = new ConnectToPersistentSubscriptionOperation(this._settings.log, msg.cb, msg.subscriptionId,
                msg.bufferSize, msg.streamId, msg.userCredentials, msg.eventAppeared, msg.subscriptionDropped,
                this._settings.verboseLogging, function() { return self._connection });
        this._logDebug("StartSubscription %s %s, %s, %d, %d.",
            this._state === ConnectionState.Connected ? "fire" : "enqueue",
            operation.constructor.name, operation, msg.maxRetries, msg.timeout);
        var subscription = createSubscriptionItem(operation, msg.maxRetries, msg.timeout);
        if (this._state === ConnectionState.Connecting) {
          this._subscriptions.enqueueSubscription(subscription);
        } else {
          this._subscriptions.startSubscription(subscription, this._connection);
        }
        break;
      case ConnectionState.Closed:
        msg.cb(new Error("Connection closed. " + this._esConnection.connectionName));
        break;
      default: throw new Error(util.format("Unknown state: %s.", this._state));
    }
  };
  
  EventStoreConnectionLogicHandler.prototype._establishTcpConnection = function(endPoints) {
    var endPoint = this._settings.useSslConnection ? endPoints.secureTcpEndPoint : endPoints.tcpEndPoint;
    if (endPoint === null)
    {
      this._closeConnection("No end point to node specified.");
      return;
    }
  
    this._logDebug("EstablishTcpConnection to [%j]", endPoint);
  
    if (this._state !== ConnectionState.Connecting) return;
    if (this._connectingPhase !== ConnectingPhase.EndPointDiscovery) return;
  
    var self = this;
    this._connectingPhase = ConnectingPhase.ConnectionEstablishing;
    this._connection = new TcpPackageConnection(
            this._settings.log,
            endPoint,
            uuid.v4(),
            this._settings.useSslConnection,
            this._settings.targetHost,
            this._settings.validateServer,
            this._settings.clientConnectionTimeout,
            function(connection, pkg) {
              self.enqueueMessage(new messages.HandleTcpPackageMessage(connection, pkg));
            },
            function(connection, error) {
              self.enqueueMessage(new messages.TcpConnectionErrorMessage(connection, error));
            },
            function(connection) {
              connection.startReceiving();
              self.enqueueMessage(new messages.TcpConnectionEstablishedMessage(connection));
            },
            function(connection, error) {
              self.enqueueMessage(new messages.TcpConnectionClosedMessage(connection, error));
            }
        );
  };
  
  EventStoreConnectionLogicHandler.prototype._tcpConnectionEstablished = function(connection) {
    if (this._state !== ConnectionState.Connecting || !this._connection || !this._connection.equals(connection) || connection.isClosed)
    {
      this._logDebug("IGNORED (_state %s, _conn.Id %s, conn.Id %s, conn.closed %s): TCP connection to [%j, L%j] established.",
          this._state, this._connection === null ? EmptyGuid : this._connection.connectionId, connection.connectionId,
          connection.isClosed, connection.remoteEndPoint, connection.localEndPoint);
      return;
    }
  
    this._logDebug("TCP connection to [%j, L%j, %s] established.", connection.remoteEndPoint, connection.localEndPoint, connection.connectionId);
    this._heartbeatInfo = {
      lastPackageNumber: this._packageNumber,
      isIntervalStage: true,
      timeStamp: Date.now()
    };
  
    if (this._settings.defaultUserCredentials !== null)
    {
      this._connectingPhase = ConnectingPhase.Authentication;
  
      this._authInfo = {
        correlationId: uuid.v4(),
        timeStamp: Date.now()
      };
      this._connection.enqueueSend(new TcpPackage(
        TcpCommand.Authenticate,
        TcpFlags.Authenticated,
        this._authInfo.correlationId,
        this._settings.defaultUserCredentials.username,
        this._settings.defaultUserCredentials.password));
    }
    else
    {
      this._goToIdentifiedState();
    }
  };
  
  EventStoreConnectionLogicHandler.prototype._goToIdentifiedState = function() {
    this._connectingPhase = ConnectingPhase.Identification;
    this._identityInfo = {
      correlationId: uuid.v4(),
      timeStamp: Date.now()
    };
    var dto = new ClientMessage.IdentifyClient({version: ClientVersion, connectionName: this._esConnection.connectionName});
    var buf = dto.constructor.encode(dto).finish();
    this._connection.enqueueSend(new TcpPackage(TcpCommand.IdentifyClient, TcpFlags.None, this._identityInfo.correlationId, null, null, createBufferSegment(buf)))
  };
  
  EventStoreConnectionLogicHandler.prototype._goToConnectedState = function() {
    this._state = ConnectionState.Connected;
    this._connectingPhase = ConnectingPhase.Connected;
  
    this._wasConnected = true;
  
    this.emit('connected', this._connection.remoteEndPoint);
  
    if ((Date.now() - this._lastTimeoutsTimeStamp) >= this._settings.operationTimeoutCheckPeriod)
    {
      this._operations.checkTimeoutsAndRetry(this._connection);
      this._subscriptions.checkTimeoutsAndRetry(this._connection);
      this._lastTimeoutsTimeStamp = Date.now();
    }
  };
  
  EventStoreConnectionLogicHandler.prototype._tcpConnectionError = function(connection, error) {
    if (!this._connection || !this._connection.equals(connection)) return;
    if (this._state === ConnectionState.Closed) return;
  
    this._logDebug("TcpConnectionError connId %s, exc %s.", connection.connectionId, error);
    this._closeConnection("TCP connection error occurred.", error);
  };
  
  EventStoreConnectionLogicHandler.prototype._tcpConnectionClosed = function(connection, error) {
    if (this._state === ConnectionState.Init) throw new Error();
    if (this._state === ConnectionState.Closed || !this._connection || !this._connection.equals(connection))
    {
      this._logDebug("IGNORED (_state: %s, _conn.ID: %s, conn.ID: %s): TCP connection to [%j, L%j] closed.",
          this._state, this._connection === null ? EmptyGuid : this._connection.connectionId,  connection.connectionId,
          connection.remoteEndPoint, connection.localEndPoint);
      return;
    }
  
    this._state = ConnectionState.Connecting;
    this._connectingPhase = ConnectingPhase.Reconnecting;
  
    this._logDebug("TCP connection to [%j, L%j, %s] closed. %s", connection.remoteEndPoint, connection.localEndPoint, connection.connectionId, error);
  
    this._subscriptions.purgeSubscribedAndDroppedSubscriptions(this._connection.connectionId);
    this._reconnInfo = {
      reconnectionAttempt: this._reconnInfo ? this._reconnInfo.reconnectionAttempt : 0,
      timeStamp: Date.now()
    };
  
    if (this._wasConnected)
    {
      this._wasConnected = false;
      this.emit('disconnected', connection.remoteEndPoint);
    }
  };
  
  EventStoreConnectionLogicHandler.prototype._handleTcpPackage = function(connection, pkg) {
    if (!connection.equals(this._connection) || this._state === ConnectionState.Closed || this._state === ConnectionState.Init)
    {
      this._logDebug("IGNORED: HandleTcpPackage connId %s, package %s, %s.",
                     connection.connectionId, TcpCommand.getName(pkg.command), pkg.correlationId);
      return;
    }
  
    this._logDebug("HandleTcpPackage connId %s, package %s, %s.",
                   this._connection.connectionId, TcpCommand.getName(pkg.command), pkg.correlationId);
    this._packageNumber += 1;
  
    if (pkg.command === TcpCommand.HeartbeatResponseCommand)
    {
      if (pkg.correlationId === this._heartbeatInfo.correlationId)
      {
        var now = Date.now();
        var heartbeatEvent = {
          connectionId: this._connection.connectionId,
          remoteEndPoint: this._connection.remoteEndPoint,
          requestSentAt: this._heartbeatInfo.timeStamp,
          requestPkgNumber: this._heartbeatInfo.lastPackageNumber,
          responseReceivedAt: now,
          responsePkgNumber: this._packageNumber
        };
        try {
          this.emit('heartbeatInfo', heartbeatEvent);
        } catch(e) {
          this._logDebug("IGNORED: emit heartbeat event failed.\n%s", e.stack);
        }
      }
      return;
    }
    if (pkg.command === TcpCommand.HeartbeatRequestCommand)
    {
      this._connection.enqueueSend(new TcpPackage(
        TcpCommand.HeartbeatResponseCommand,
        TcpFlags.None,
        pkg.correlationId));
      return;
    }
  
    if (pkg.command === TcpCommand.Authenticated || pkg.command === TcpCommand.NotAuthenticated)
    {
      if (this._state === ConnectionState.Connecting
          && this._connectingPhase === ConnectingPhase.Authentication
          && this._authInfo.correlationId === pkg.correlationId)
      {
        if (pkg.command === TcpCommand.NotAuthenticated)
          this.emit('authenticationFailed', "Not authenticated");
  
        this._goToIdentifiedState();
        return;
      }
    }
  
    if (pkg.command === TcpCommand.ClientIdentified)
    {
      if (this._state === ConnectionState.Connecting
        && this._identityInfo.correlationId === pkg.correlationId)
      {
        this._goToConnectedState();
        return;
      }
    }
  
    if (pkg.command === TcpCommand.BadRequest && pkg.correlationId === EmptyGuid)
    {
      var message = "<no message>";
      try {
        message = pkg.data.toString();
      } catch(e) {}
      var err = new Error("Bad request received from server. Error: " + message);
      this._closeConnection("Connection-wide BadRequest received. Too dangerous to continue.", err);
      return;
    }
  
    var operation = this._operations.getActiveOperation(pkg.correlationId);
    if (operation)
    {
      var result = operation.operation.inspectPackage(pkg);
      this._logDebug("HandleTcpPackage OPERATION DECISION %s (%s), %s", result.decision, result.description, operation.operation);
      switch (result.decision)
      {
        case InspectionDecision.DoNothing: break;
        case InspectionDecision.EndOperation:
          this._operations.removeOperation(operation);
          break;
        case InspectionDecision.Retry:
          this._operations.scheduleOperationRetry(operation);
          break;
        case InspectionDecision.Reconnect:
          this._reconnectTo({tcpEndPoint: result.tcpEndPoint, secureTcpEndPoint: result.secureTcpEndPoint});
          this._operations.scheduleOperationRetry(operation);
          break;
        default:
          throw new Error("Unknown InspectionDecision: " + result.decision);
      }
      if (this._state === ConnectionState.Connected) {
        this._operations.scheduleWaitingOperations(connection);
      }
  
      return;
    }
  
    var subscription = this._subscriptions.getActiveSubscription(pkg.correlationId);
    if (subscription)
    {
      var result = subscription.operation.inspectPackage(pkg);
      this._logDebug("HandleTcpPackage SUBSCRIPTION DECISION %s (%s), %s", result.decision, result.description, subscription);
      switch (result.decision)
      {
        case InspectionDecision.DoNothing: break;
        case InspectionDecision.EndOperation:
          this._subscriptions.removeSubscription(subscription);
          break;
        case InspectionDecision.Retry:
          this._subscriptions.scheduleSubscriptionRetry(subscription);
          break;
        case InspectionDecision.Reconnect:
          this._reconnectTo({tcpEndPoint: result.tcpEndPoint, secureTcpEndPoint: result.secureTcpEndPoint});
          this._subscriptions.scheduleSubscriptionRetry(subscription);
          break;
        case InspectionDecision.Subscribed:
          subscription.isSubscribed = true;
          break;
        default:
          throw new Error("Unknown InspectionDecision: " + result.decision);
      }
  
      return;
    }
  
    this._logDebug("HandleTcpPackage UNMAPPED PACKAGE with CorrelationId %s, Command: %s",
                   pkg.correlationId, TcpCommand.getName(pkg.command));
  };
  
  EventStoreConnectionLogicHandler.prototype._reconnectTo = function(endPoints) {
    var endPoint = this._settings.useSslConnection
        ? endPoints.secureTcpEndPoint
        : endPoints.tcpEndPoint;
    if (endPoint === null)
    {
      this._closeConnection("No end point is specified while trying to reconnect.");
      return;
    }
  
    if (this._state !== ConnectionState.Connected || this._connection.remoteEndPoint === endPoint) return;
  
    var msg = util.format("EventStoreConnection '%s': going to reconnect to [%j]. Current endpoint: [%j, L%j].",
        this._esConnection.connectionName, endPoint, this._connection.remoteEndPoint, this._connection.localEndPoint);
    if (this._settings.verboseLogging) this._settings.log.info(msg);
    this._closeTcpConnection(msg);
  
    this._state = ConnectionState.Connecting;
    this._connectingPhase = ConnectingPhase.EndPointDiscovery;
    this._establishTcpConnection(endPoints);
  };
  
  EventStoreConnectionLogicHandler.prototype._timerTick = function() {
    switch (this._state)
    {
      case ConnectionState.Init: break;
      case ConnectionState.Connecting:
        if (this._connectingPhase === ConnectingPhase.Reconnecting && (Date.now() - this._reconnInfo.timeStamp) >= this._settings.reconnectionDelay) {
          this._logDebug("TimerTick checking reconnection...");
  
          this._reconnInfo = {reconnectionAttempt: this._reconnInfo.reconnectionAttempt + 1, timeStamp: Date.now()};
          if (this._settings.maxReconnections >= 0 && this._reconnInfo.reconnectionAttempt > this._settings.maxReconnections) {
            this._closeConnection("Reconnection limit reached.");
          } else {
            this.emit('reconnecting', {});
            this._discoverEndpoint(null);
          }
        } else if (this._connectingPhase === ConnectingPhase.Authentication && (Date.now() - this._authInfo.timeStamp) >= this._settings.operationTimeout) {
          this.emit('authenticationFailed', "Authentication timed out.");
          if (this._clientVersion === 1) {
            this._goToIdentifiedState();
          } else {
            this._goToConnectedState();
          }
        } else if (this._connectingPhase === ConnectingPhase.Authentication || this._connectingPhase === ConnectingPhase.Connected) {
          this._manageHeartbeats();
        }
        break;
      case ConnectionState.Connected:
        // operations timeouts are checked only if connection is established and check period time passed
        if ((Date.now() - this._lastTimeoutsTimeStamp) >= this._settings.operationTimeoutCheckPeriod)
        {
          // On mono even impossible connection first says that it is established
          // so clearing of reconnection count on ConnectionEstablished event causes infinite reconnections.
          // So we reset reconnection count to zero on each timeout check period when connection is established
          this._reconnInfo = {reconnectionAttempt: 0, timeStamp: Date.now()};
          this._operations.checkTimeoutsAndRetry(this._connection);
          this._subscriptions.checkTimeoutsAndRetry(this._connection);
          this._lastTimeoutsTimeStamp = Date.now();
        }
        this._manageHeartbeats();
        break;
      case ConnectionState.Closed:
        break;
      default:
        throw new Error("Unknown state: " + this._state + ".");
    }
  };
  
  EventStoreConnectionLogicHandler.prototype._manageHeartbeats = function() {
    if (this._connection === null) return;
  
    var timeout = this._heartbeatInfo.isIntervalStage ? this._settings.heartbeatInterval : this._settings.heartbeatTimeout;
    if ((Date.now() - this._heartbeatInfo.timeStamp) < timeout) return;
  
    var packageNumber = this._packageNumber;
    if (this._heartbeatInfo.lastPackageNumber !== packageNumber)
    {
      this._heartbeatInfo = {lastPackageNumber: packageNumber, isIntervalStage: true, timeStamp: Date.now()};
      return;
    }
  
    if (this._heartbeatInfo.isIntervalStage)
    {
      var correlationId = uuid.v4();
      // TcpMessage.Heartbeat analog
      this._connection.enqueueSend(new TcpPackage(
        TcpCommand.HeartbeatRequestCommand,
        TcpFlags.None,
        correlationId));
      this._heartbeatInfo = {correlationId: correlationId, lastPackageNumber: this._heartbeatInfo.lastPackageNumber, isIntervalStage: false, timeStamp: Date.now()};
    }
    else
    {
      // TcpMessage.HeartbeatTimeout analog
      var msg = util.format("EventStoreConnection '%s': closing TCP connection [%j, L%j, %s] due to HEARTBEAT TIMEOUT at pkgNum %d.",
          this._esConnection.connectionName, this._connection.remoteEndPoint, this._connection.localEndPoint,
          this._connection.connectionId, packageNumber);
      this._settings.log.info(msg);
      this._closeTcpConnection(msg);
    }
  };
  
  EventStoreConnectionLogicHandler.prototype._logDebug = function(message) {
    if (!this._settings.verboseLogging) return;
  
    if (arguments.length > 1) {
      message = util.format.apply(util, Array.prototype.slice.call(arguments));
    }
  
    this._settings.log.debug("EventStoreConnection '%s': %s", this._esConnection.connectionName, message);
  };
  
  EventStoreConnectionLogicHandler.prototype._logInfo = function(message){
    if (arguments.length > 1) {
      message = util.format.apply(util, Array.prototype.slice.call(arguments));
    }
  
    this._settings.log.info("EventStoreConnection '%s': %s", this._esConnection.connectionName, message);
  };
  
  module.exports = EventStoreConnectionLogicHandler;