/**
 * An implementation of the insert wire protocol. This class also takes care of applying the write
 * concern.
 *
 * @mongodb.driver.manual ../meta-driver/latest/legacy/mongodb-wire-protocol/#op-insert OP_INSERT
 */
class InsertProtocol extends WriteProtocol {

  private static final Logger LOGGER = Loggers.getLogger("protocol.insert");

  private final List<InsertRequest> insertRequestList;

  /**
   * Construct a new instance.
   *
   * @param namespace the namespace
   * @param ordered whether the inserts are ordered
   * @param writeConcern the write concern
   * @param insertRequestList the list of documents to insert
   */
  public InsertProtocol(
      final MongoNamespace namespace,
      final boolean ordered,
      final WriteConcern writeConcern,
      final List<InsertRequest> insertRequestList) {
    super(namespace, ordered, writeConcern);
    this.insertRequestList = insertRequestList;
  }

  @Override
  public WriteConcernResult execute(final InternalConnection connection) {
    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug(
          format(
              "Inserting %d documents into namespace %s on connection [%s] to server %s",
              insertRequestList.size(),
              getNamespace(),
              connection.getDescription().getConnectionId(),
              connection.getDescription().getServerAddress()));
    }
    WriteConcernResult writeConcernResult = super.execute(connection);
    LOGGER.debug("Insert completed");
    return writeConcernResult;
  }

  @Override
  public void executeAsync(
      final InternalConnection connection,
      final SingleResultCallback<WriteConcernResult> callback) {
    try {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug(
            format(
                "Asynchronously inserting %d documents into namespace %s on connection [%s] to server %s",
                insertRequestList.size(),
                getNamespace(),
                connection.getDescription().getConnectionId(),
                connection.getDescription().getServerAddress()));
      }
      super.executeAsync(
          connection,
          new SingleResultCallback<WriteConcernResult>() {
            @Override
            public void onResult(final WriteConcernResult result, final Throwable t) {
              if (t != null) {
                callback.onResult(null, t);
              } else {
                LOGGER.debug("Asynchronous insert completed");
                callback.onResult(result, null);
              }
            }
          });
    } catch (Throwable t) {
      callback.onResult(null, t);
    }
  }

  @Override
  protected BsonDocument getAsWriteCommand(
      final ByteBufferBsonOutput bsonOutput, final int firstDocumentPosition) {
    return getBaseCommandDocument("insert")
        .append(
            "documents",
            new BsonArray(ByteBufBsonDocument.create(bsonOutput, firstDocumentPosition)));
  }

  protected RequestMessage createRequestMessage(final MessageSettings settings) {
    return new InsertMessage(
        getNamespace().getFullName(), isOrdered(), getWriteConcern(), insertRequestList, settings);
  }

  @Override
  protected void appendToWriteCommandResponseDocument(
      final RequestMessage curMessage,
      final RequestMessage nextMessage,
      final WriteConcernResult writeConcernResult,
      final BsonDocument response) {
    response.append(
        "n",
        new BsonInt32(
            nextMessage == null
                ? ((InsertMessage) curMessage).getInsertRequestList().size()
                : ((InsertMessage) curMessage).getInsertRequestList().size()
                    - ((InsertMessage) nextMessage).getInsertRequestList().size()));
  }

  @Override
  protected com.mongodb.diagnostics.logging.Logger getLogger() {
    return LOGGER;
  }
}
final class OperationHelper {
  public static final Logger LOGGER = Loggers.getLogger("operation");

  interface CallableWithConnection<T> {
    T call(Connection connection);
  }

  interface CallableWithConnectionAndSource<T> {
    T call(ConnectionSource source, Connection connection);
  }

  interface AsyncCallableWithConnection {
    void call(AsyncConnection connection, Throwable t);
  }

  interface AsyncCallableWithConnectionAndSource {
    void call(AsyncConnectionSource source, AsyncConnection connection, Throwable t);
  }

  static void checkValidReadConcern(final Connection connection, final ReadConcern readConcern) {
    if (!serverIsAtLeastVersionThreeDotTwo(connection.getDescription())
        && !readConcern.isServerDefault()) {
      throw new IllegalArgumentException(
          format(
              "ReadConcern not supported by server version: %s",
              connection.getDescription().getServerVersion()));
    }
  }

  static void checkValidReadConcern(
      final AsyncConnection connection,
      final ReadConcern readConcern,
      final AsyncCallableWithConnection callable) {
    Throwable throwable = null;
    if (!serverIsAtLeastVersionThreeDotTwo(connection.getDescription())
        && !readConcern.isServerDefault()) {
      throwable =
          new IllegalArgumentException(
              format(
                  "ReadConcern not supported by server version: %s",
                  connection.getDescription().getServerVersion()));
    }
    callable.call(connection, throwable);
  }

  static void checkValidReadConcern(
      final AsyncConnectionSource source,
      final AsyncConnection connection,
      final ReadConcern readConcern,
      final AsyncCallableWithConnectionAndSource callable) {
    checkValidReadConcern(
        connection,
        readConcern,
        new AsyncCallableWithConnection() {
          @Override
          public void call(final AsyncConnection connection, final Throwable t) {
            callable.call(source, connection, t);
          }
        });
  }

  static void checkValidCollation(final Connection connection, final Collation collation) {
    if (!serverIsAtLeastVersionThreeDotFour(connection.getDescription()) && collation != null) {
      throw new IllegalArgumentException(
          format(
              "Collation not supported by server version: %s",
              connection.getDescription().getServerVersion()));
    }
  }

  static void checkValidCollation(
      final AsyncConnection connection,
      final Collation collation,
      final AsyncCallableWithConnection callable) {
    Throwable throwable = null;
    if (!serverIsAtLeastVersionThreeDotFour(connection.getDescription()) && collation != null) {
      throwable =
          new IllegalArgumentException(
              format(
                  "Collation not supported by server version: %s",
                  connection.getDescription().getServerVersion()));
    }
    callable.call(connection, throwable);
  }

  static void checkValidCollation(
      final AsyncConnectionSource source,
      final AsyncConnection connection,
      final Collation collation,
      final AsyncCallableWithConnectionAndSource callable) {
    checkValidCollation(
        connection,
        collation,
        new AsyncCallableWithConnection() {
          @Override
          public void call(final AsyncConnection connection, final Throwable t) {
            callable.call(source, connection, t);
          }
        });
  }

  static void checkValidWriteRequestCollations(
      final Connection connection, final List<? extends WriteRequest> requests) {
    Collation collation = null;
    for (WriteRequest request : requests) {
      if (request instanceof UpdateRequest) {
        collation = ((UpdateRequest) request).getCollation();
      } else if (request instanceof DeleteRequest) {
        collation = ((DeleteRequest) request).getCollation();
      }
      if (collation != null) {
        break;
      }
    }
    checkValidCollation(connection, collation);
  }

  static void checkValidWriteRequestCollations(
      final AsyncConnection connection,
      final List<? extends WriteRequest> requests,
      final AsyncCallableWithConnection callable) {
    Collation collation = null;
    for (WriteRequest request : requests) {
      if (request instanceof UpdateRequest) {
        collation = ((UpdateRequest) request).getCollation();
      } else if (request instanceof DeleteRequest) {
        collation = ((DeleteRequest) request).getCollation();
      }
      if (collation != null) {
        break;
      }
    }
    checkValidCollation(
        connection,
        collation,
        new AsyncCallableWithConnection() {
          @Override
          public void call(final AsyncConnection connection, final Throwable t) {
            callable.call(connection, t);
          }
        });
  }

  static void checkValidIndexRequestCollations(
      final Connection connection, final List<IndexRequest> requests) {
    for (IndexRequest request : requests) {
      if (request.getCollation() != null) {
        checkValidCollation(connection, request.getCollation());
        break;
      }
    }
  }

  static void checkValidIndexRequestCollations(
      final AsyncConnection connection,
      final List<IndexRequest> requests,
      final AsyncCallableWithConnection callable) {
    boolean calledTheCallable = false;
    for (IndexRequest request : requests) {
      if (request.getCollation() != null) {
        calledTheCallable = true;
        checkValidCollation(
            connection,
            request.getCollation(),
            new AsyncCallableWithConnection() {
              @Override
              public void call(final AsyncConnection connection, final Throwable t) {
                callable.call(connection, t);
              }
            });
        break;
      }
    }
    if (!calledTheCallable) {
      callable.call(connection, null);
    }
  }

  static void checkValidReadConcernAndCollation(
      final Connection connection, final ReadConcern readConcern, final Collation collation) {
    checkValidReadConcern(connection, readConcern);
    checkValidCollation(connection, collation);
  }

  static void checkValidReadConcernAndCollation(
      final AsyncConnection connection,
      final ReadConcern readConcern,
      final Collation collation,
      final AsyncCallableWithConnection callable) {
    checkValidReadConcern(
        connection,
        readConcern,
        new AsyncCallableWithConnection() {
          @Override
          public void call(final AsyncConnection connection, final Throwable t) {
            if (t != null) {
              callable.call(connection, t);
            } else {
              checkValidCollation(connection, collation, callable);
            }
          }
        });
  }

  static void checkValidReadConcernAndCollation(
      final AsyncConnectionSource source,
      final AsyncConnection connection,
      final ReadConcern readConcern,
      final Collation collation,
      final AsyncCallableWithConnectionAndSource callable) {
    checkValidReadConcernAndCollation(
        connection,
        readConcern,
        collation,
        new AsyncCallableWithConnection() {
          @Override
          public void call(final AsyncConnection connection, final Throwable t) {
            callable.call(source, connection, t);
          }
        });
  }

  static boolean bypassDocumentValidationNotSupported(
      final Boolean bypassDocumentValidation,
      final WriteConcern writeConcern,
      final ConnectionDescription description) {
    return bypassDocumentValidation != null
        && serverIsAtLeastVersionThreeDotTwo(description)
        && !writeConcern.isAcknowledged();
  }

  static MongoClientException getBypassDocumentValidationException() {
    return new MongoClientException(
        "Specifying bypassDocumentValidation with an unacknowledged WriteConcern "
            + "is not supported");
  }

  static <T> QueryBatchCursor<T> createEmptyBatchCursor(
      final MongoNamespace namespace,
      final Decoder<T> decoder,
      final ServerAddress serverAddress,
      final int batchSize) {
    return new QueryBatchCursor<T>(
        new QueryResult<T>(namespace, Collections.<T>emptyList(), 0L, serverAddress),
        0,
        batchSize,
        decoder);
  }

  static <T> AsyncBatchCursor<T> createEmptyAsyncBatchCursor(
      final MongoNamespace namespace,
      final Decoder<T> decoder,
      final ServerAddress serverAddress,
      final int batchSize) {
    return new AsyncQueryBatchCursor<T>(
        new QueryResult<T>(namespace, Collections.<T>emptyList(), 0L, serverAddress),
        0,
        batchSize,
        decoder);
  }

  static <T> BatchCursor<T> cursorDocumentToBatchCursor(
      final BsonDocument cursorDocument,
      final Decoder<T> decoder,
      final ConnectionSource source,
      final int batchSize) {
    return new QueryBatchCursor<T>(
        OperationHelper.<T>cursorDocumentToQueryResult(
            cursorDocument, source.getServerDescription().getAddress()),
        0,
        batchSize,
        decoder,
        source);
  }

  static <T> AsyncBatchCursor<T> cursorDocumentToAsyncBatchCursor(
      final BsonDocument cursorDocument,
      final Decoder<T> decoder,
      final AsyncConnectionSource source,
      final AsyncConnection connection,
      final int batchSize) {
    return new AsyncQueryBatchCursor<T>(
        OperationHelper.<T>cursorDocumentToQueryResult(
            cursorDocument, source.getServerDescription().getAddress()),
        0,
        batchSize,
        0,
        decoder,
        source,
        connection);
  }

  static <T> QueryResult<T> cursorDocumentToQueryResult(
      final BsonDocument cursorDocument, final ServerAddress serverAddress) {
    return cursorDocumentToQueryResult(cursorDocument, serverAddress, "firstBatch");
  }

  static <T> QueryResult<T> getMoreCursorDocumentToQueryResult(
      final BsonDocument cursorDocument, final ServerAddress serverAddress) {
    return cursorDocumentToQueryResult(cursorDocument, serverAddress, "nextBatch");
  }

  private static <T> QueryResult<T> cursorDocumentToQueryResult(
      final BsonDocument cursorDocument,
      final ServerAddress serverAddress,
      final String fieldNameContainingBatch) {
    long cursorId = ((BsonInt64) cursorDocument.get("id")).getValue();
    MongoNamespace queryResultNamespace =
        new MongoNamespace(cursorDocument.getString("ns").getValue());
    return new QueryResult<T>(
        queryResultNamespace,
        BsonDocumentWrapperHelper.<T>toList(cursorDocument, fieldNameContainingBatch),
        cursorId,
        serverAddress);
  }

  static <T> SingleResultCallback<T> releasingCallback(
      final SingleResultCallback<T> wrapped, final AsyncConnection connection) {
    return new ReferenceCountedReleasingWrappedCallback<T>(wrapped, singletonList(connection));
  }

  static <T> SingleResultCallback<T> releasingCallback(
      final SingleResultCallback<T> wrapped,
      final AsyncConnectionSource source,
      final AsyncConnection connection) {
    return new ReferenceCountedReleasingWrappedCallback<T>(wrapped, asList(connection, source));
  }

  static <T> SingleResultCallback<T> releasingCallback(
      final SingleResultCallback<T> wrapped,
      final AsyncReadBinding readBinding,
      final AsyncConnectionSource source,
      final AsyncConnection connection) {
    return new ReferenceCountedReleasingWrappedCallback<T>(
        wrapped, asList(readBinding, connection, source));
  }

  private static class ReferenceCountedReleasingWrappedCallback<T>
      implements SingleResultCallback<T> {
    private final SingleResultCallback<T> wrapped;
    private final List<? extends ReferenceCounted> referenceCounted;

    ReferenceCountedReleasingWrappedCallback(
        final SingleResultCallback<T> wrapped,
        final List<? extends ReferenceCounted> referenceCounted) {
      this.wrapped = wrapped;
      this.referenceCounted = notNull("referenceCounted", referenceCounted);
    }

    @Override
    public void onResult(final T result, final Throwable t) {
      for (ReferenceCounted cur : referenceCounted) {
        cur.release();
      }
      wrapped.onResult(result, t);
    }
  }

  static boolean serverIsAtLeastVersionTwoDotSix(final ConnectionDescription description) {
    return serverIsAtLeastVersion(description, new ServerVersion(2, 6));
  }

  static boolean serverIsAtLeastVersionThreeDotZero(final ConnectionDescription description) {
    return serverIsAtLeastVersion(description, new ServerVersion(asList(3, 0, 0)));
  }

  static boolean serverIsAtLeastVersionThreeDotTwo(final ConnectionDescription description) {
    return serverIsAtLeastVersion(description, new ServerVersion(asList(3, 1, 9)));
  }

  static boolean serverIsAtLeastVersionThreeDotFour(final ConnectionDescription description) {
    return serverIsAtLeastVersion(description, new ServerVersion(asList(3, 3, 10)));
  }

  static boolean serverIsAtLeastVersion(
      final ConnectionDescription description, final ServerVersion serverVersion) {
    return description.getServerVersion().compareTo(serverVersion) >= 0;
  }

  static <T> T withConnection(final ReadBinding binding, final CallableWithConnection<T> callable) {
    ConnectionSource source = binding.getReadConnectionSource();
    try {
      return withConnectionSource(source, callable);
    } finally {
      source.release();
    }
  }

  static <T> T withConnection(
      final ReadBinding binding, final CallableWithConnectionAndSource<T> callable) {
    ConnectionSource source = binding.getReadConnectionSource();
    try {
      return withConnectionSource(source, callable);
    } finally {
      source.release();
    }
  }

  static <T> T withConnection(
      final WriteBinding binding, final CallableWithConnection<T> callable) {
    ConnectionSource source = binding.getWriteConnectionSource();
    try {
      return withConnectionSource(source, callable);
    } finally {
      source.release();
    }
  }

  static <T> T withConnectionSource(
      final ConnectionSource source, final CallableWithConnection<T> callable) {
    Connection connection = source.getConnection();
    try {
      return callable.call(connection);
    } finally {
      connection.release();
    }
  }

  static <T> T withConnectionSource(
      final ConnectionSource source, final CallableWithConnectionAndSource<T> callable) {
    Connection connection = source.getConnection();
    try {
      return callable.call(source, connection);
    } finally {
      connection.release();
    }
  }

  static void withConnection(
      final AsyncWriteBinding binding, final AsyncCallableWithConnection callable) {
    binding.getWriteConnectionSource(
        errorHandlingCallback(new AsyncCallableWithConnectionCallback(callable), LOGGER));
  }

  static void withConnection(
      final AsyncReadBinding binding, final AsyncCallableWithConnection callable) {
    binding.getReadConnectionSource(
        errorHandlingCallback(new AsyncCallableWithConnectionCallback(callable), LOGGER));
  }

  static void withConnection(
      final AsyncReadBinding binding, final AsyncCallableWithConnectionAndSource callable) {
    binding.getReadConnectionSource(
        errorHandlingCallback(new AsyncCallableWithConnectionAndSourceCallback(callable), LOGGER));
  }

  private static class AsyncCallableWithConnectionCallback
      implements SingleResultCallback<AsyncConnectionSource> {
    private final AsyncCallableWithConnection callable;

    public AsyncCallableWithConnectionCallback(final AsyncCallableWithConnection callable) {
      this.callable = callable;
    }

    @Override
    public void onResult(final AsyncConnectionSource source, final Throwable t) {
      if (t != null) {
        callable.call(null, t);
      } else {
        withConnectionSource(source, callable);
      }
    }
  }

  private static void withConnectionSource(
      final AsyncConnectionSource source, final AsyncCallableWithConnection callable) {
    source.getConnection(
        new SingleResultCallback<AsyncConnection>() {
          @Override
          public void onResult(final AsyncConnection connection, final Throwable t) {
            source.release();
            if (t != null) {
              callable.call(null, t);
            } else {
              callable.call(connection, null);
            }
          }
        });
  }

  private static void withConnectionSource(
      final AsyncConnectionSource source, final AsyncCallableWithConnectionAndSource callable) {
    source.getConnection(
        new SingleResultCallback<AsyncConnection>() {
          @Override
          public void onResult(final AsyncConnection result, final Throwable t) {
            callable.call(source, result, t);
          }
        });
  }

  private static class AsyncCallableWithConnectionAndSourceCallback
      implements SingleResultCallback<AsyncConnectionSource> {
    private final AsyncCallableWithConnectionAndSource callable;

    public AsyncCallableWithConnectionAndSourceCallback(
        final AsyncCallableWithConnectionAndSource callable) {
      this.callable = callable;
    }

    @Override
    public void onResult(final AsyncConnectionSource source, final Throwable t) {
      if (t != null) {
        callable.call(null, null, t);
      } else {
        withConnectionSource(source, callable);
      }
    }
  }

  private OperationHelper() {}
}
/**
 * A protocol for executing a command against a MongoDB server using the OP_QUERY wire protocol
 * message.
 *
 * @param <T> the type returned from execution
 * @mongodb.driver.manual ../meta-driver/latest/legacy/mongodb-wire-protocol/#op-query OP_QUERY
 */
class CommandProtocol<T> implements Protocol<T> {

  public static final Logger LOGGER = Loggers.getLogger("protocol.command");

  private static final Set<String> SECURITY_SENSITIVE_COMMANDS =
      new HashSet<String>(
          asList(
              "authenticate",
              "saslStart",
              "saslContinue",
              "getnonce",
              "createUser",
              "updateUser",
              "copydbgetnonce",
              "copydbsaslstart",
              "copydb"));
  private final MongoNamespace namespace;
  private final BsonDocument command;
  private final Decoder<T> commandResultDecoder;
  private final FieldNameValidator fieldNameValidator;
  private boolean slaveOk;
  private CommandListener commandListener;
  private volatile String commandName;

  /**
   * Construct an instance.
   *
   * @param database the database
   * @param command the command
   * @param fieldNameValidator the field name validator to apply tot the command
   * @param commandResultDecoder the decoder to use to decode the command result
   */
  public CommandProtocol(
      final String database,
      final BsonDocument command,
      final FieldNameValidator fieldNameValidator,
      final Decoder<T> commandResultDecoder) {
    notNull("database", database);
    this.namespace = new MongoNamespace(database, MongoNamespace.COMMAND_COLLECTION_NAME);
    this.command = notNull("command", command);
    this.commandResultDecoder = notNull("commandResultDecoder", commandResultDecoder);
    this.fieldNameValidator = notNull("fieldNameValidator", fieldNameValidator);
  }

  public boolean isSlaveOk() {
    return slaveOk;
  }

  public CommandProtocol<T> slaveOk(final boolean slaveOk) {
    this.slaveOk = slaveOk;
    return this;
  }

  @Override
  public T execute(final InternalConnection connection) {
    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug(
          format(
              "Sending command {%s : %s} to database %s on connection [%s] to server %s",
              getCommandName(),
              command.values().iterator().next(),
              namespace.getDatabaseName(),
              connection.getDescription().getConnectionId(),
              connection.getDescription().getServerAddress()));
    }
    long startTimeNanos = System.nanoTime();
    CommandMessage commandMessage =
        new CommandMessage(
            namespace.getFullName(),
            command,
            slaveOk,
            fieldNameValidator,
            ProtocolHelper.getMessageSettings(connection.getDescription()));
    ResponseBuffers responseBuffers = null;
    try {
      sendMessage(commandMessage, connection);
      responseBuffers = connection.receiveMessage(commandMessage.getId());
      if (!ProtocolHelper.isCommandOk(
          new BsonBinaryReader(new ByteBufferBsonInput(responseBuffers.getBodyByteBuffer())))) {
        throw getCommandFailureException(
            getResponseDocument(responseBuffers, commandMessage, new BsonDocumentCodec()),
            connection.getDescription().getServerAddress());
      }

      T retval = getResponseDocument(responseBuffers, commandMessage, commandResultDecoder);

      if (commandListener != null) {
        sendSucceededEvent(
            connection.getDescription(),
            startTimeNanos,
            commandMessage,
            getResponseDocument(responseBuffers, commandMessage, new RawBsonDocumentCodec()));
      }
      LOGGER.debug("Command execution completed");
      return retval;
    } catch (RuntimeException e) {
      sendFailedEvent(connection.getDescription(), startTimeNanos, commandMessage, e);
      throw e;
    } finally {
      if (responseBuffers != null) {
        responseBuffers.close();
      }
    }
  }

  private static <D> D getResponseDocument(
      final ResponseBuffers responseBuffers,
      final CommandMessage commandMessage,
      final Decoder<D> decoder) {
    responseBuffers.reset();
    ReplyMessage<D> replyMessage =
        new ReplyMessage<D>(responseBuffers, decoder, commandMessage.getId());

    return replyMessage.getDocuments().get(0);
  }

  @Override
  public void executeAsync(
      final InternalConnection connection, final SingleResultCallback<T> callback) {
    long startTimeNanos = System.nanoTime();
    CommandMessage message =
        new CommandMessage(
            namespace.getFullName(),
            command,
            slaveOk,
            fieldNameValidator,
            ProtocolHelper.getMessageSettings(connection.getDescription()));
    boolean sentStartedEvent = false;
    try {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug(
            format(
                "Asynchronously sending command {%s : %s} to database %s on connection [%s] to server %s",
                getCommandName(),
                command.values().iterator().next(),
                namespace.getDatabaseName(),
                connection.getDescription().getConnectionId(),
                connection.getDescription().getServerAddress()));
      }
      ByteBufferBsonOutput bsonOutput = new ByteBufferBsonOutput(connection);
      int documentPosition =
          ProtocolHelper.encodeMessageWithMetadata(message, bsonOutput).getFirstDocumentPosition();
      sendStartedEvent(connection, bsonOutput, message, documentPosition);
      sentStartedEvent = true;
      SingleResultCallback<ResponseBuffers> receiveCallback =
          new CommandResultCallback(callback, message, connection.getDescription(), startTimeNanos);
      connection.sendMessageAsync(
          bsonOutput.getByteBuffers(),
          message.getId(),
          new SendMessageCallback<T>(
              connection,
              bsonOutput,
              message,
              getCommandName(),
              startTimeNanos,
              commandListener,
              callback,
              receiveCallback));
    } catch (Throwable t) {
      if (sentStartedEvent) {
        sendFailedEvent(connection.getDescription(), startTimeNanos, message, t);
      }
      callback.onResult(null, t);
    }
  }

  @Override
  public void setCommandListener(final CommandListener commandListener) {
    this.commandListener = commandListener;
  }

  private String getCommandName() {
    return commandName != null ? commandName : command.keySet().iterator().next();
  }

  private void sendMessage(final CommandMessage message, final InternalConnection connection) {
    ByteBufferBsonOutput bsonOutput = new ByteBufferBsonOutput(connection);
    try {
      int documentPosition = message.encodeWithMetadata(bsonOutput).getFirstDocumentPosition();
      sendStartedEvent(connection, bsonOutput, message, documentPosition);

      connection.sendMessage(bsonOutput.getByteBuffers(), message.getId());
    } finally {
      bsonOutput.close();
    }
  }

  private void sendStartedEvent(
      final InternalConnection connection,
      final ByteBufferBsonOutput bsonOutput,
      final CommandMessage message,
      final int documentPosition) {
    if (commandListener != null) {
      ByteBufBsonDocument byteBufBsonDocument = createOne(bsonOutput, documentPosition);
      BsonDocument commandDocument;
      if (byteBufBsonDocument.containsKey("$query")) {
        commandDocument = byteBufBsonDocument.getDocument("$query");
        commandName = commandDocument.keySet().iterator().next();
      } else {
        commandDocument = byteBufBsonDocument;
        commandName = byteBufBsonDocument.getFirstKey();
      }
      BsonDocument commandDocumentForEvent =
          (SECURITY_SENSITIVE_COMMANDS.contains(commandName))
              ? new BsonDocument()
              : commandDocument;
      sendCommandStartedEvent(
          message,
          namespace.getDatabaseName(),
          commandName,
          commandDocumentForEvent,
          connection.getDescription(),
          commandListener);
    }
  }

  private void sendSucceededEvent(
      final ConnectionDescription connectionDescription,
      final long startTimeNanos,
      final CommandMessage commandMessage,
      final BsonDocument response) {
    if (commandListener != null) {
      BsonDocument responseDocumentForEvent =
          (SECURITY_SENSITIVE_COMMANDS.contains(getCommandName())) ? new BsonDocument() : response;
      sendCommandSucceededEvent(
          commandMessage,
          getCommandName(),
          responseDocumentForEvent,
          connectionDescription,
          startTimeNanos,
          commandListener);
    }
  }

  private void sendFailedEvent(
      final ConnectionDescription connectionDescription,
      final long startTimeNanos,
      final CommandMessage commandMessage,
      final Throwable t) {
    if (commandListener != null) {
      Throwable commandEventException = t;
      if (t instanceof MongoCommandException
          && (SECURITY_SENSITIVE_COMMANDS.contains(getCommandName()))) {
        commandEventException =
            new MongoCommandException(new BsonDocument(), connectionDescription.getServerAddress());
      }
      sendCommandFailedEvent(
          commandMessage,
          getCommandName(),
          connectionDescription,
          startTimeNanos,
          commandEventException,
          commandListener);
    }
  }

  class CommandResultCallback extends ResponseCallback {
    private final SingleResultCallback<T> callback;
    private final CommandMessage message;
    private final ConnectionDescription connectionDescription;
    private final long startTimeNanos;

    CommandResultCallback(
        final SingleResultCallback<T> callback,
        final CommandMessage message,
        final ConnectionDescription connectionDescription,
        final long startTimeNanos) {
      super(message.getId(), connectionDescription.getServerAddress());
      this.callback = callback;
      this.message = message;
      this.connectionDescription = connectionDescription;
      this.startTimeNanos = startTimeNanos;
    }

    @Override
    protected void callCallback(
        final ResponseBuffers responseBuffers, final Throwable throwableFromCallback) {
      try {
        if (throwableFromCallback != null) {
          throw throwableFromCallback;
        }

        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Command execution completed");
        }

        if (!ProtocolHelper.isCommandOk(
            new BsonBinaryReader(new ByteBufferBsonInput(responseBuffers.getBodyByteBuffer())))) {
          throw getCommandFailureException(
              getResponseDocument(responseBuffers, message, new BsonDocumentCodec()),
              connectionDescription.getServerAddress());
        }

        if (commandListener != null) {
          sendSucceededEvent(
              connectionDescription,
              startTimeNanos,
              message,
              getResponseDocument(responseBuffers, message, new RawBsonDocumentCodec()));
        }
        callback.onResult(
            getResponseDocument(responseBuffers, message, commandResultDecoder), null);

      } catch (Throwable t) {
        sendFailedEvent(connectionDescription, startTimeNanos, message, t);
        callback.onResult(null, t);
      } finally {
        if (responseBuffers != null) {
          responseBuffers.close();
        }
      }
    }
  }
}