/*
 * Copyright (c) 2008-2015 MongoDB, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.mongodb;

import com.mongodb.annotations.ThreadSafe;
import com.mongodb.binding.ClusterBinding;
import com.mongodb.binding.ConnectionSource;
import com.mongodb.binding.ReadBinding;
import com.mongodb.binding.ReadWriteBinding;
import com.mongodb.binding.SingleServerBinding;
import com.mongodb.binding.WriteBinding;
import com.mongodb.connection.BufferProvider;
import com.mongodb.connection.Cluster;
import com.mongodb.connection.ClusterConnectionMode;
import com.mongodb.connection.ClusterDescription;
import com.mongodb.connection.ClusterSettings;
import com.mongodb.connection.Connection;
import com.mongodb.connection.DefaultClusterFactory;
import com.mongodb.connection.ServerDescription;
import com.mongodb.connection.SocketStreamFactory;
import com.mongodb.event.ClusterListener;
import com.mongodb.event.CommandEventMulticaster;
import com.mongodb.event.CommandListener;
import com.mongodb.internal.connection.PowerOfTwoBufferPool;
import com.mongodb.internal.thread.DaemonThreadFactory;
import com.mongodb.management.JMXConnectionPoolListener;
import com.mongodb.operation.CurrentOpOperation;
import com.mongodb.operation.FsyncUnlockOperation;
import com.mongodb.operation.ListDatabasesOperation;
import com.mongodb.operation.OperationExecutor;
import com.mongodb.operation.ReadOperation;
import com.mongodb.operation.WriteOperation;
import com.mongodb.selector.LatencyMinimizingServerSelector;
import com.mongodb.selector.ServerSelector;
import org.bson.BsonBoolean;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;

import static com.mongodb.ReadPreference.primary;
import static com.mongodb.connection.ClusterConnectionMode.MULTIPLE;
import static com.mongodb.connection.ClusterType.REPLICA_SET;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static java.util.Collections.unmodifiableList;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;

/**
 * <p>A database connection with internal connection pooling. For most applications, you should have one Mongo instance for the entire
 * JVM.</p>
 *
 * <p>Note: This class has been superseded by {@code MongoClient}, and may be deprecated in a future release.</p>
 *
 * @see MongoClient
 * @see ReadPreference
 * @see WriteConcern
 */
@ThreadSafe
public class Mongo {
    static final String ADMIN_DATABASE_NAME = "admin";

    private final ConcurrentMap<String, DB> dbCache = new ConcurrentHashMap<String, DB>();

    private volatile WriteConcern writeConcern;
    private volatile ReadPreference readPreference;
    private final ReadConcern readConcern;

    private final MongoClientOptions options;
    private final List<MongoCredential> credentialsList;

    private final Bytes.OptionHolder optionHolder;

    private final Cluster cluster;
    private final BufferProvider bufferProvider = new PowerOfTwoBufferPool();

    private final ConcurrentLinkedQueue<ServerCursorAndNamespace> orphanedCursors = new ConcurrentLinkedQueue<ServerCursorAndNamespace>();
    private final ExecutorService cursorCleaningService;

    /**
     * Creates a Mongo instance based on a (single) mongodb node (localhost, default port)
     *
     * @throws MongoException if there's a failure
     * @deprecated Replaced by {@link MongoClient#MongoClient()})
     */
    @Deprecated
    public Mongo() {
        this(new ServerAddress(), createLegacyOptions());
    }

    /**
     * Creates a Mongo instance based on a (single) mongodb node (default port)
     *
     * @param host server to connect to
     * @deprecated Replaced by {@link MongoClient#MongoClient(String)}
     */
    @Deprecated
    public Mongo(final String host) {
        this(new ServerAddress(host), createLegacyOptions());
    }


    /**
     * Creates a Mongo instance based on a (single) mongodb node (default port)
     *
     * @param host    server to connect to
     * @param options default query options
     * @deprecated Replaced by {@link MongoClient#MongoClient(String, MongoClientOptions)}
     */
    @Deprecated
    public Mongo(final String host,
                 @SuppressWarnings("deprecation")
                 final MongoOptions options) {
        this(new ServerAddress(host), options.toClientOptions());
    }

    /**
     * Creates a Mongo instance based on a (single) mongodb node
     *
     * @param host the host address of the database
     * @param port the port on which the database is running
     * @deprecated Replaced by {@link MongoClient#MongoClient(String, int)}
     */
    @Deprecated
    public Mongo(final String host, final int port) {
        this(new ServerAddress(host, port), createLegacyOptions());
    }

    /**
     * Creates a Mongo instance based on a (single) mongodb node
     *
     * @param address the database address
     * @see com.mongodb.ServerAddress
     * @deprecated Replaced by {@link MongoClient#MongoClient(ServerAddress)}
     */
    @Deprecated
    public Mongo(final ServerAddress address) {
        this(address, createLegacyOptions());
    }

    /**
     * Creates a Mongo instance based on a (single) mongo node using a given ServerAddress
     *
     * @param address the database address
     * @param options default query options
     * @see com.mongodb.ServerAddress
     * @deprecated Replaced by {@link MongoClient#MongoClient(ServerAddress, MongoClientOptions)}
     */
    @Deprecated
    public Mongo(final ServerAddress address,
                 @SuppressWarnings("deprecation")
                 final MongoOptions options) {
        this(address, options.toClientOptions());
    }

    /**
     * <p>Creates a Mongo in paired mode. </p>
     *
     * <p>This will also work for a replica set and will find all members (the master will be used by default).</p>
     *
     * @param left  left side of the pair
     * @param right right side of the pair
     * @see com.mongodb.ServerAddress
     * @deprecated Please use {@link MongoClient#MongoClient(java.util.List)} instead.
     */
    @Deprecated
    public Mongo(final ServerAddress left, final ServerAddress right) {
        this(asList(left, right), createLegacyOptions());
    }

    /**
     * <p>Creates a Mongo connection in paired mode. </p>
     *
     * <p>This will also work for a replica set and will find all members (the master will be used by default).</p>
     *
     * @param left    left side of the pair
     * @param right   right side of the pair
     * @param options the optional settings for the Mongo instance
     * @see com.mongodb.ServerAddress
     * @deprecated Please use {@link MongoClient#MongoClient(java.util.List, MongoClientOptions)} instead.
     */
    @Deprecated
    public Mongo(final ServerAddress left, final ServerAddress right,
                 @SuppressWarnings("deprecation")
                 final MongoOptions options) {
        this(asList(left, right), options.toClientOptions());
    }

    /**
     * <p>Creates an instance based on a list of replica set members or mongos servers. For a replica set it will discover all members.
     * For a list with a single seed, the driver will still discover all members of the replica set.  For a direct
     * connection to a replica set member, with no discovery, use the {@link #Mongo(ServerAddress)} constructor instead.</p>
     *
     * <p>When there is more than one server to choose from based on the type of request (read or write) and the read preference (if it's a
     * read request), the driver will randomly select a server to send a request. This applies to both replica sets and sharded clusters.
     * The servers to randomly select from are further limited by the local threshold.  See
     * {@link MongoClientOptions#getLocalThreshold()}</p>
     *
     * @param seeds Put as many servers as you can in the list and the system will figure out the rest.  This can either be a list of mongod
     *              servers in the same replica set or a list of mongos servers in the same sharded cluster.
     * @see MongoClientOptions#getLocalThreshold()
     * @deprecated Replaced by {@link MongoClient#MongoClient(java.util.List)}
     */
    @Deprecated
    public Mongo(final List<ServerAddress> seeds) {
        this(seeds, createLegacyOptions());
    }

    /**
     * <p>Creates an instance based on a list of replica set members or mongos servers. For a replica set it will discover all members.
     * For a list with a single seed, the driver will still discover all members of the replica set.  For a direct
     * connection to a replica set member, with no discovery, use the {@link #Mongo(ServerAddress, MongoClientOptions)} constructor
     * instead.</p>
     *
     * <p>When there is more than one server to choose from based on the type of request (read or write) and the read preference (if it's a
     * read request), the driver will randomly select a server to send a request. This applies to both replica sets and sharded clusters.
     * The servers to randomly select from are further limited by the local threshold.  See
     * {@link MongoClientOptions#getLocalThreshold()}</p>
     *
     * @param seeds Put as many servers as you can in the list and the system will figure out the rest.  This can either be a list of mongod
     *              servers in the same replica set or a list of mongos servers in the same sharded cluster.
     * @param options the options
     * @see MongoClientOptions#getLocalThreshold()
     * @deprecated Replaced by {@link MongoClient#MongoClient(java.util.List, MongoClientOptions)}
     */
    @Deprecated
    public Mongo(final List<ServerAddress> seeds,
                 @SuppressWarnings("deprecation")
                 final MongoOptions options) {
        this(seeds, options.toClientOptions());
    }

    /**
     * <p>Creates a Mongo described by a URI. If only one address is used it will only connect to that node, otherwise it will discover all
     * nodes. If the URI contains database credentials, the database will be authenticated lazily on first use with those credentials.</p>
     *
     * <p>Examples:</p>
     * <ul>
     *     <li>mongodb://localhost</li>
     *     <li>mongodb://fred:foobar@localhost/</li>
     * </ul>
     *
     * @param uri URI to connect to, optionally containing additional information like credentials
     * @throws MongoException if there's a failure
     * @mongodb.driver.manual reference/connection-string Connection String URI Format
     * @see MongoURI
     * @deprecated Replaced by {@link MongoClient#MongoClient(MongoClientURI)}
     */
    @Deprecated
    public Mongo(
                @SuppressWarnings("deprecation")
                final MongoURI uri) {
        this(uri.toClientURI());
    }

    Mongo(final List<ServerAddress> seedList, final MongoClientOptions options) {
        this(seedList, Collections.<MongoCredential>emptyList(), options);
    }

    Mongo(final ServerAddress serverAddress, final MongoClientOptions options) {
        this(serverAddress, Collections.<MongoCredential>emptyList(), options);
    }

    Mongo(final ServerAddress serverAddress, final List<MongoCredential> credentialsList, final MongoClientOptions options) {
        this(createCluster(serverAddress, credentialsList, options), options, credentialsList);
    }

    Mongo(final List<ServerAddress> seedList, final List<MongoCredential> credentialsList, final MongoClientOptions options) {
        this(createCluster(seedList, credentialsList, options), options, credentialsList);
    }

    Mongo(final MongoClientURI mongoURI) {
        this(createCluster(mongoURI),
             mongoURI.getOptions(),
             mongoURI.getCredentials() != null ? asList(mongoURI.getCredentials()) : Collections.<MongoCredential>emptyList());
    }

    Mongo(final Cluster cluster, final MongoClientOptions options, final List<MongoCredential> credentialsList) {
        this.cluster = cluster;
        this.options = options;
        this.readPreference = options.getReadPreference() != null ? options.getReadPreference() : primary();
        this.writeConcern = options.getWriteConcern() != null ? options.getWriteConcern() : WriteConcern.UNACKNOWLEDGED;
        this.readConcern = options.getReadConcern() != null ? options.getReadConcern() : ReadConcern.DEFAULT;
        this.optionHolder = new Bytes.OptionHolder(null);
        this.credentialsList = unmodifiableList(credentialsList);
        cursorCleaningService = options.isCursorFinalizerEnabled() ? createCursorCleaningService() : null;
    }

    /**
     * Sets the default write concern to use for write operations executed on any {@link DBCollection} created indirectly from this
     * instance, via a {@link DB} instance created from {@link #getDB(String)}.
     *
     * <p>
     *     Note that changes to the default write concern made via this method will NOT affect the write concern of
     *     {@link com.mongodb.client.MongoDatabase} instances created via {@link MongoClient#getDatabase(String)}
     * </p>
     *
     * @param writeConcern write concern to use
     * @deprecated Set the default write concern with either {@link MongoClientURI} or {@link MongoClientOptions}
     */
    @Deprecated
    public void setWriteConcern(final WriteConcern writeConcern) {
        this.writeConcern = writeConcern;
    }

    /**
     * Gets the write concern
     *
     * @return the write concern
     */
    public WriteConcern getWriteConcern() {
        return writeConcern;
    }

    /**
     * Gets the read concern
     *
     * @return the read concern
     */
    public ReadConcern getReadConcern() {
        return readConcern;
    }

    /**
     * Sets the default read preference to use for reads operations executed on any {@link DBCollection} created indirectly from this
     * instance, via a {@link DB} instance created from {@link #getDB(String)}.
     *
     * <p>
     *     Note that changes to the default read preference made via this method will NOT affect the read preference of
     *     {@link com.mongodb.client.MongoDatabase} instances created via {@link MongoClient#getDatabase(String)}
     * </p>

     * @param readPreference Read Preference to use
     * @deprecated Set the default read preference with either {@link MongoClientURI} or {@link MongoClientOptions}
     */
    @Deprecated
    public void setReadPreference(final ReadPreference readPreference) {
        this.readPreference = readPreference;
    }

    /**
     * Gets the default read preference
     *
     * @return the default read preference
     */
    public ReadPreference getReadPreference() {
        return readPreference;
    }

    /**
     * Gets a list of all server addresses used when this Mongo was created
     *
     * @return list of server addresses
     * @throws MongoException if there's a failure
     */
    public List<ServerAddress> getAllAddress() {
        //TODO It should return the address list without auto-discovered nodes. Not sure if it's required. Maybe users confused with name.
        return getServerAddressList();
    }

    /**
     * Gets the list of server addresses currently seen by this client. This includes addresses auto-discovered from a replica set.
     *
     * @return list of server addresses
     * @throws MongoException if there's a failure
     */
    public List<ServerAddress> getServerAddressList() {
        List<ServerAddress> serverAddresses = new ArrayList<ServerAddress>();
        for (final ServerDescription cur : getClusterDescription().getServerDescriptions()) {
            serverAddresses.add(cur.getAddress());
        }
        return serverAddresses;
    }

    private ClusterDescription getClusterDescription() {
        return cluster.getDescription();
    }

    /**
     * Gets the address of the current master
     *
     * @return the address
     */
    @SuppressWarnings("deprecation")
    public ServerAddress getAddress() {
        ClusterDescription description = getClusterDescription();
        if (description.getPrimaries().isEmpty()) {
            return null;
        }
        return description.getPrimaries().get(0).getAddress();
    }

    /**
     * <p>Returns the mongo options.</p>
     *
     * <p>Changes to {@code MongoOptions} that are done after connection are not reflected.</p>
     *
     * @return the mongo options
     * @deprecated Please use {@link MongoClient} class to connect to server and corresponding {@link
     * com.mongodb.MongoClient#getMongoClientOptions()}
     */
    @Deprecated
    public MongoOptions getMongoOptions() {
        return new MongoOptions(getMongoClientOptions());
    }

    /**
     * Get the status of the replica set cluster.
     *
     * @return replica set status information
     */
    public ReplicaSetStatus getReplicaSetStatus() {
        ClusterDescription clusterDescription = getClusterDescription();
        return clusterDescription.getType() == REPLICA_SET && clusterDescription.getConnectionMode() == MULTIPLE
               ? new ReplicaSetStatus(cluster) : null; // this is intended behavior in 2.x
    }


    /**
     * Gets a list of the names of all databases on the connected server.
     *
     * @return list of database names
     * @throws MongoException  if the operation fails
     * @deprecated Replaced with {@link com.mongodb.MongoClient#listDatabaseNames()}
     */
    @Deprecated
    public List<String> getDatabaseNames() {
      return new OperationIterable<DBObject>(new ListDatabasesOperation<DBObject>(MongoClient.getCommandCodec()),
          primary(), createOperationExecutor())
          .map(new Function<DBObject, String>() {
              @Override
              public String apply(final DBObject result) {
                  return (String) result.get("name");
              }
          }).into(new ArrayList<String>());
    }

    /**
     * Gets a database object. Users should use {@link com.mongodb.MongoClient#getDatabase(String)} instead.
     *
     * <p>
     * The {@link DB} class has been superseded by {@link com.mongodb.client.MongoDatabase}.  The deprecation of this method effectively
     * deprecates the {@link DB}, {@link DBCollection}, and {@link DBCursor} classes, among others; but in order to give users time to
     * migrate to the new API without experiencing a huge number of compiler warnings, those classes have not yet been formally
     * deprecated.
     * </p>
     *
     * @param dbName the name of the database to retrieve
     * @return a DB representing the specified database
     * @deprecated use {@link com.mongodb.MongoClient#getDatabase(String)}
     */
    @Deprecated
    public DB getDB(final String dbName) {
        DB db = dbCache.get(dbName);
        if (db != null) {
            return db;
        }

        db = new DB(this, dbName, createOperationExecutor());
        DB temp = dbCache.putIfAbsent(dbName, db);
        if (temp != null) {
            return temp;
        }
        return db;
    }

    /**
     * Returns the list of databases used by the driver since this Mongo instance was created. This may include DBs that exist in the client
     * but not yet on the server.
     *
     * @return a collection of database objects
     */
    public Collection<DB> getUsedDatabases() {
        return dbCache.values();
    }


    /**
     * Drops the database if it exists.
     *
     * @param dbName name of database to drop
     * @throws MongoException if the operation fails
     */
    public void dropDatabase(final String dbName) {
        getDB(dbName).dropDatabase();
    }

    /**
     * Closes all resources associated with this instance, in particular any open network connections. Once called, this instance and any
     * databases obtained from it can no longer be used.
     */
    public void close() {
        cluster.close();
        if (cursorCleaningService != null) {
            cursorCleaningService.shutdownNow();
        }
    }

    /**
     * Makes it possible to run read queries on secondary nodes
     *
     * @see ReadPreference#secondaryPreferred()
     * @deprecated Replaced with {@code ReadPreference.secondaryPreferred()}
     */
    @Deprecated
    public void slaveOk() {
        addOption(Bytes.QUERYOPTION_SLAVEOK);
    }

    /**
     * Set the default query options for reads operations executed on any {@link DBCollection} created indirectly from this
     * instance, via a {@link DB} instance created from {@link #getDB(String)}.
     *
     * <p>
     *     Note that changes to query options made via this method will NOT affect
     *     {@link com.mongodb.client.MongoDatabase} instances created via {@link MongoClient#getDatabase(String)}
     * </p>
     *
     * @param options value to be set
     * @deprecated Set options on instances of {@link DBCursor}
     */
    @Deprecated
    public void setOptions(final int options) {
        optionHolder.set(options);
    }

    /**
     * Reset the default query options for reads operations executed on any {@link DBCollection} created indirectly from this
     * instance, via a {@link DB} instance created from {@link #getDB(String)}.
     *
     * @deprecated Reset options instead on instances of {@link DBCursor}
     */
    @Deprecated
    public void resetOptions() {
        optionHolder.reset();
    }

    /**
     * Add the default query option for reads operations executed on any {@link DBCollection} created indirectly from this
     * instance, via a {@link DB} instance created from {@link #getDB(String)}.
     *
     * <p>
     *     Note that changes to query options made via this method will NOT affect
     *     {@link com.mongodb.client.MongoDatabase} instances created via {@link MongoClient#getDatabase(String)}
     * </p>
     *
     * @param option value to be added to current options
     * @deprecated Add options instead on instances of {@link DBCursor}
     */
    @Deprecated
    public void addOption(final int option) {
        optionHolder.add(option);
    }

    /**
     * Gets the default query options for reads operations executed on any {@link DBCollection} created indirectly from this
     * instance, via a {@link DB} instance created from {@link #getDB(String)}.
     *
     * @return an int representing the options to be used by queries
     * @deprecated Get options instead from instances of {@link DBCursor}
     */
    @Deprecated
    public int getOptions() {
        return optionHolder.get();
    }

    /**
     * Forces the master server to fsync the RAM data to disk This is done automatically by the server at intervals, but can be forced for
     * better reliability.
     *
     * @param async if true, the fsync will be done asynchronously on the server.
     * @return result of the command execution
     * @throws MongoException if there's a failure
     * @mongodb.driver.manual reference/command/fsync/ fsync command
     */
    public CommandResult fsync(final boolean async) {
        DBObject command = new BasicDBObject("fsync", 1);
        if (async) {
            command.put("async", 1);
        }
        return getDB(ADMIN_DATABASE_NAME).command(command);
    }

    /**
     * Forces the master server to fsync the RAM data to disk, then lock all writes. The database will be read-only after this command
     * returns.
     *
     * @return result of the command execution
     * @throws MongoException if there's a failure
     * @mongodb.driver.manual reference/command/fsync/ fsync command
     */
    public CommandResult fsyncAndLock() {
        DBObject command = new BasicDBObject("fsync", 1);
        command.put("lock", 1);
        return getDB(ADMIN_DATABASE_NAME).command(command);
    }

    /**
     * Unlocks the database, allowing the write operations to go through. This command may be asynchronous on the server, which means there
     * may be a small delay before the database becomes writable.
     *
     * @return {@code DBObject} in the following form {@code {"ok": 1,"info": "unlock completed"}}
     * @throws MongoException if there's a failure
     * @mongodb.driver.manual reference/command/fsync/ fsync command
     */
    public DBObject unlock() {
        return DBObjects.toDBObject(execute(new FsyncUnlockOperation()));
    }

    /**
     * Returns true if the database is locked (read-only), false otherwise.
     *
     * @return result of the command execution
     * @throws MongoException if the operation fails
     * @mongodb.driver.manual reference/command/fsync/ fsync command
     */
    public boolean isLocked() {
        return execute(new CurrentOpOperation(), ReadPreference.primary())
               .getBoolean("fsyncLock", BsonBoolean.FALSE).getValue();
    }

    @Override
    public String toString() {
        return "Mongo{"
               + "options=" + getMongoClientOptions()
               + '}';
    }

    /**
     * Gets the maximum size for a BSON object supported by the current master server. Note that this value may change over time depending
     * on which server is master.
     *
     * @return the maximum size, or 0 if not obtained from servers yet.
     * @throws MongoException if there's a failure
     */
    @SuppressWarnings("deprecation")
    public int getMaxBsonObjectSize() {
        List<ServerDescription> primaries = getClusterDescription().getPrimaries();
        return primaries.isEmpty() ? ServerDescription.getDefaultMaxDocumentSize() : primaries.get(0).getMaxDocumentSize();
    }

    /**
     * Gets a {@code String} representation of current connection point, i.e. master.
     *
     * @return server address in a host:port form
     */
    public String getConnectPoint() {
        ServerAddress master = getAddress();
        return master != null ? String.format("%s:%d", master.getHost(), master.getPort()) : null;
    }

    private static MongoClientOptions createLegacyOptions() {
        return MongoClientOptions.builder()
                                 .legacyDefaults()
                                 .build();
    }

    private static Cluster createCluster(final MongoClientURI mongoURI) {

        List<MongoCredential> credentialList = mongoURI.getCredentials() != null
                                               ? asList(mongoURI.getCredentials())
                                               : Collections.<MongoCredential>emptyList();

        if (mongoURI.getHosts().size() == 1) {
            return createCluster(new ServerAddress(mongoURI.getHosts().get(0)),
                                 credentialList,
                                 mongoURI.getOptions());
        } else {
            List<ServerAddress> seedList = new ArrayList<ServerAddress>(mongoURI.getHosts().size());
            for (final String host : mongoURI.getHosts()) {
                seedList.add(new ServerAddress(host));
            }
            return createCluster(seedList, credentialList, mongoURI.getOptions());
        }
    }

    private static Cluster createCluster(final List<ServerAddress> seedList,
                                         final List<MongoCredential> credentialsList, final MongoClientOptions options) {
        return createCluster(ClusterSettings.builder().hosts(createNewSeedList(seedList))
                                            .requiredReplicaSetName(options.getRequiredReplicaSetName())
                                            .serverSelectionTimeout(options.getServerSelectionTimeout(), MILLISECONDS)
                                            .serverSelector(createServerSelector(options))
                                            .description(options.getDescription())
                                            .maxWaitQueueSize(options.getConnectionPoolSettings().getMaxWaitQueueSize()),
                             credentialsList, options);
    }

    private static Cluster createCluster(final ServerAddress serverAddress, final List<MongoCredential> credentialsList,
                                         final MongoClientOptions options) {
        return createCluster(ClusterSettings.builder()
                                            .mode(getSingleServerClusterMode(options))
                                            .hosts(asList(serverAddress))
                                            .requiredReplicaSetName(options.getRequiredReplicaSetName())
                                            .serverSelectionTimeout(options.getServerSelectionTimeout(), MILLISECONDS)
                                            .serverSelector(createServerSelector(options))
                                            .description(options.getDescription())
                                            .maxWaitQueueSize(options.getConnectionPoolSettings().getMaxWaitQueueSize()),
                             credentialsList, options);
    }

    private static Cluster createCluster(final ClusterSettings.Builder settingsBuilder, final List<MongoCredential> credentialsList,
                                         final MongoClientOptions options) {
        for (ClusterListener cur : options.getClusterListeners()) {
            settingsBuilder.addClusterListener(cur);
        }
        return new DefaultClusterFactory().create(settingsBuilder.build(),
                                                  options.getServerSettings(),
                                                  options.getConnectionPoolSettings(),
                                                  new SocketStreamFactory(options.getSocketSettings(),
                                                                          options.getSslSettings(),
                                                                          options.getSocketFactory()),
                                                  new SocketStreamFactory(options.getHeartbeatSocketSettings(),
                                                                          options.getSslSettings(),
                                                                          options.getSocketFactory()),
                                                  credentialsList, null,
                                                  new JMXConnectionPoolListener(), null,
                                                  createCommandListener(options.getCommandListeners()));
    }

    private static CommandListener createCommandListener(final List<CommandListener> commandListeners) {
        switch (commandListeners.size()) {
            case 0:
                return null;
            case 1:
                return commandListeners.get(0);
            default:
                return new CommandEventMulticaster(commandListeners);
        }
    }

    private static List<ServerAddress> createNewSeedList(final List<ServerAddress> seedList) {
        List<ServerAddress> retVal = new ArrayList<ServerAddress>(seedList.size());
        for (final ServerAddress cur : seedList) {
            retVal.add(cur);
        }
        return retVal;
    }

    private static ServerSelector createServerSelector(final MongoClientOptions options) {
        return new LatencyMinimizingServerSelector(options.getLocalThreshold(), MILLISECONDS);
    }

    Cluster getCluster() {
        return cluster;
    }

    Bytes.OptionHolder getOptionHolder() {
        return optionHolder;
    }

    BufferProvider getBufferProvider() {
        return bufferProvider;
    }

    MongoClientOptions getMongoClientOptions() {
        return options;
    }

    List<MongoCredential> getCredentialsList() {
        return credentialsList;
    }

    WriteBinding getWriteBinding() {
        return getReadWriteBinding(primary());
    }

    ReadBinding getReadBinding(final ReadPreference readPreference) {
        return getReadWriteBinding(readPreference);
    }

    private ReadWriteBinding getReadWriteBinding(final ReadPreference readPreference) {
        return new ClusterBinding(getCluster(), readPreference);
    }

    void addOrphanedCursor(final ServerCursor serverCursor, final MongoNamespace namespace) {
        orphanedCursors.add(new ServerCursorAndNamespace(serverCursor, namespace));
    }

    OperationExecutor createOperationExecutor() {
        return new OperationExecutor() {
            @Override
            public <T> T execute(final ReadOperation<T> operation, final ReadPreference readPreference) {
                return Mongo.this.execute(operation, readPreference);
            }

            @Override
            public <T> T execute(final WriteOperation<T> operation) {
                return Mongo.this.execute(operation);
            }
        };
    }

    <T> T execute(final ReadOperation<T> operation, final ReadPreference readPreference) {
        ReadBinding binding = getReadBinding(readPreference);
        try {
            return operation.execute(binding);
        } finally {
            binding.release();
        }
    }

    <T> T execute(final WriteOperation<T> operation) {
        WriteBinding binding = getWriteBinding();
        try {
            return operation.execute(binding);
        } finally {
            binding.release();
        }
    }

    private ExecutorService createCursorCleaningService() {
        ScheduledExecutorService newTimer = Executors.newSingleThreadScheduledExecutor(new DaemonThreadFactory());
        newTimer.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                cleanCursors();
            }
        }, 1, 1, SECONDS);
        return newTimer;
    }

    private void cleanCursors() {
        ServerCursorAndNamespace cur;
        while ((cur = orphanedCursors.poll()) != null) {
            ReadWriteBinding binding = new SingleServerBinding(cluster, cur.serverCursor.getAddress());
            try {
                ConnectionSource source = binding.getReadConnectionSource();
                try {
                    Connection connection = source.getConnection();
                    try {
                        connection.killCursor(cur.namespace, singletonList(cur.serverCursor.getId()));
                    } finally {
                        connection.release();
                    }
                } finally {
                    source.release();
                }
            } finally {
                binding.release();
            }
        }
    }

    private static ClusterConnectionMode getSingleServerClusterMode(final MongoClientOptions options) {
        if (options.getRequiredReplicaSetName() == null) {
            return ClusterConnectionMode.SINGLE;
        } else {
            return ClusterConnectionMode.MULTIPLE;
        }
    }

    private static class ServerCursorAndNamespace {
        private final ServerCursor serverCursor;
        private final MongoNamespace namespace;

        public ServerCursorAndNamespace(final ServerCursor serverCursor, final MongoNamespace namespace) {
            this.serverCursor = serverCursor;
            this.namespace = namespace;
        }
    }

    /**
     * Mongo.Holder can be used as a static place to hold several instances of Mongo. Security is not enforced at this level, and needs to
     * be done on the application side.
     */
    public static class Holder {

        private static final Holder INSTANCE = new Holder();
        private final ConcurrentMap<String, Mongo> clients = new ConcurrentHashMap<String, Mongo>();

        /**
         * Get the only instance of {@code Holder}.
         *
         * @return the singleton instance of {@code Holder}
         */
        public static Holder singleton() {
            return INSTANCE;
        }

        /**
         * Attempts to find an existing MongoClient instance matching that URI in the holder, and returns it if exists. Otherwise creates a
         * new Mongo instance based on this URI and adds it to the holder.
         *
         * @param uri the Mongo URI
         * @return the client
         * @throws MongoException if there's a failure
         * @deprecated Please use {@link #connect(MongoClientURI)} instead.
         */
        @Deprecated
        public Mongo connect(final MongoURI uri) {
            return connect(uri.toClientURI());
        }

        /**
         * Attempts to find an existing MongoClient instance matching that URI in the holder, and returns it if exists. Otherwise creates a
         * new Mongo instance based on this URI and adds it to the holder.
         *
         * @param uri the Mongo URI
         * @return the client
         * @throws MongoException if there's a failure
         */
        public Mongo connect(final MongoClientURI uri) {
            String key = toKey(uri);
            Mongo client = clients.get(key);

            if (client == null) {
                Mongo newbie = new MongoClient(uri);
                client = clients.putIfAbsent(key, newbie);
                if (client == null) {
                    client = newbie;
                } else {
                    newbie.close();
                }
            }

            return client;
        }

        private String toKey(final MongoClientURI uri) {
            return uri.toString();
        }
    }
}
