/*
 * Copyright 2008-present 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.client.model;

import org.bson.BsonDocument;
import org.bson.BsonInt32;
import org.bson.BsonString;
import org.bson.BsonValue;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.conversions.Bson;

import java.util.List;

import static com.mongodb.assertions.Assertions.notNull;
import static java.util.Arrays.asList;

/**
 * A factory for defining index keys. A convenient way to use this class is to statically import all of its methods, which allows usage
 * like:
 *

 *    collection.createIndex(compoundIndex(ascending("x"), descending("y")));
 *

 * @since 3.1
 */
public final class Indexes {

    private Indexes() {
    }

    /**
     * Create an index key for an ascending index on the given fields.
     *
     * @param fieldNames the field names, which must contain at least one
     * @return the index specification
     * @mongodb.driver.manual core/indexes indexes
     */
    public static Bson ascending(final String... fieldNames) {
        return ascending(asList(fieldNames));
    }

    /**
     * Create an index key for an ascending index on the given fields.
     *
     * @param fieldNames the field names, which must contain at least one
     * @return the index specification
     * @mongodb.driver.manual core/indexes indexes
     */
    public static Bson ascending(final List<String> fieldNames) {
        notNull("fieldNames", fieldNames);
        return compoundIndex(fieldNames, new BsonInt32(1));
    }

    /**
     * Create an index key for an ascending index on the given fields.
     *
     * @param fieldNames the field names, which must contain at least one
     * @return the index specification
     * @mongodb.driver.manual core/indexes indexes
     */
    public static Bson descending(final String... fieldNames) {
        return descending(asList(fieldNames));
    }

    /**
     * Create an index key for an ascending index on the given fields.
     *
     * @param fieldNames the field names, which must contain at least one
     * @return the index specification
     * @mongodb.driver.manual core/indexes indexes
     */
    public static Bson descending(final List<String> fieldNames) {
        notNull("fieldNames", fieldNames);
        return compoundIndex(fieldNames, new BsonInt32(-1));
    }

    /**
     * Create an index key for an 2dsphere index on the given fields.
     *
     * @param fieldNames the field names, which must contain at least one
     * @return the index specification
     * @mongodb.driver.manual core/2dsphere 2dsphere Index
     */
    public static Bson geo2dsphere(final String... fieldNames) {
        return geo2dsphere(asList(fieldNames));
    }

    /**
     * Create an index key for an ascending index on the given fields.
     *
     * @param fieldNames the field names, which must contain at least one
     * @return the index specification
     * @mongodb.driver.manual core/indexes indexes
     */
    public static Bson geo2dsphere(final List<String> fieldNames) {
        notNull("fieldNames", fieldNames);
        return compoundIndex(fieldNames, new BsonString("2dsphere"));
    }

    /**
     * Create an index key for a 2d index on the given field.
     *
     *

     * Note: A 2d index is for data stored as points on a two-dimensional plane.
     * The 2d index is intended for legacy coordinate pairs used in MongoDB 2.2 and earlier.
     *

     *
     * @param fieldName the field to create a 2d index on
     * @return the index specification
     * @mongodb.driver.manual core/2d 2d index
     */
    public static Bson geo2d(final String fieldName) {
        notNull("fieldName", fieldName);
        return new BsonDocument(fieldName, new BsonString("2d"));
    }

    /**
     * Create an index key for a geohaystack index on the given field.
     *
     *

     * Note: For queries that use spherical geometry, a 2dsphere index is a better option than a haystack index.
     * 2dsphere indexes allow field reordering; geoHaystack indexes require the first field to be the location field. Also, geoHaystack
     * indexes are only usable via commands and so always return all results at once..
     *

     *
     * @param fieldName the field to create a geoHaystack index on
     * @param additional the additional field that forms the geoHaystack index key
     * @return the index specification
     * @mongodb.driver.manual core/geohaystack geoHaystack index
     */
    public static Bson geoHaystack(final String fieldName, final Bson additional) {
        notNull("fieldName", fieldName);
        return compoundIndex(new BsonDocument(fieldName, new BsonString("geoHaystack")), additional);
    }

    /**
     * Create an index key for a text index on the given field.
     *
     * @param fieldName the field to create a text index on
     * @return the index specification
     * @mongodb.driver.manual core/text text index
     */
    public static Bson text(final String fieldName) {
        notNull("fieldName", fieldName);
        return new BsonDocument(fieldName, new BsonString("text"));
    }

    /**
     * Create an index key for a text index on every field that contains string data.
     *
     * @return the index specification
     * @mongodb.driver.manual core/text text index
     */
    public static Bson text() {
        return text("$**");
    }

    /**
     * Create an index key for a hashed index on the given field.
     *
     * @param fieldName the field to create a hashed index on
     * @return the index specification
     * @mongodb.driver.manual core/hashed hashed index
     */
    public static Bson hashed(final String fieldName) {
        notNull("fieldName", fieldName);
        return new BsonDocument(fieldName, new BsonString("hashed"));
    }

    /**
     * create a compound index specifications.  If any field names are repeated, the last one takes precedence.
     *
     * @param indexes the index specifications
     * @return the compound index specification
     * @mongodb.driver.manual core/index-compound compoundIndex
     */
    public static Bson compoundIndex(final Bson... indexes) {
        return compoundIndex(asList(indexes));
    }

    /**
     * compound multiple index specifications.  If any field names are repeated, the last one takes precedence.
     *
     * @param indexes the index specifications
     * @return the compound index specification
     * @mongodb.driver.manual core/index-compound compoundIndex
     */
    public static Bson compoundIndex(final List<? extends Bson> indexes) {
        return new CompoundIndex(indexes);
    }

    private static Bson compoundIndex(final List<String> fieldNames, final BsonValue value) {
        BsonDocument document = new BsonDocument();
        for (String fieldName : fieldNames) {
            document.append(fieldName, value);
        }
        return document;
    }

    private static class CompoundIndex implements Bson {
        private final List<? extends Bson> indexes;

        CompoundIndex(final List<? extends Bson> indexes) {
            notNull("indexes", indexes);
            this.indexes = indexes;
        }

        @Override
        public <TDocument> BsonDocument toBsonDocument(final Class<TDocument> documentClass, final CodecRegistry codecRegistry) {
            BsonDocument compoundIndex = new BsonDocument();
            for (Bson index : indexes) {
                BsonDocument indexDocument = index.toBsonDocument(documentClass, codecRegistry);
                for (String key : indexDocument.keySet()) {
                    compoundIndex.append(key, indexDocument.get(key));
                }
            }
            return compoundIndex;
        }

        @Override
        public boolean equals(final Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            CompoundIndex that = (CompoundIndex) o;

            return indexes.equals(that.indexes);
        }

        @Override
        public int hashCode() {
            return indexes.hashCode();
        }
    }
}


/*
 * Copyright 2008-present 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.operation;

import com.mongodb.DuplicateKeyException;
import com.mongodb.ErrorCategory;
import com.mongodb.MongoCommandException;
import com.mongodb.MongoException;
import com.mongodb.MongoNamespace;
import com.mongodb.WriteConcern;
import com.mongodb.WriteConcernResult;
import com.mongodb.async.SingleResultCallback;
import com.mongodb.binding.AsyncWriteBinding;
import com.mongodb.binding.WriteBinding;
import com.mongodb.bulk.IndexRequest;
import com.mongodb.connection.AsyncConnection;
import com.mongodb.connection.Connection;
import com.mongodb.connection.ConnectionDescription;
import com.mongodb.internal.operation.IndexHelper;
import org.bson.BsonArray;
import org.bson.BsonBoolean;
import org.bson.BsonDocument;
import org.bson.BsonDouble;
import org.bson.BsonInt32;
import org.bson.BsonInt64;
import org.bson.BsonString;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import static com.mongodb.assertions.Assertions.isTrueArgument;
import static com.mongodb.assertions.Assertions.notNull;
import static com.mongodb.internal.async.ErrorHandlingResultCallback.errorHandlingCallback;
import static com.mongodb.operation.CommandOperationHelper.executeCommand;
import static com.mongodb.operation.CommandOperationHelper.executeCommandAsync;
import static com.mongodb.operation.CommandOperationHelper.writeConcernErrorWriteTransformer;
import static com.mongodb.operation.DocumentHelper.putIfNotZero;
import static com.mongodb.internal.operation.IndexHelper.generateIndexName;
import static com.mongodb.operation.OperationHelper.AsyncCallableWithConnection;
import static com.mongodb.operation.OperationHelper.CallableWithConnection;
import static com.mongodb.operation.OperationHelper.LOGGER;
import static com.mongodb.operation.OperationHelper.releasingCallback;
import static com.mongodb.operation.OperationHelper.validateIndexRequestCollations;
import static com.mongodb.operation.OperationHelper.withConnection;
import static com.mongodb.internal.operation.WriteConcernHelper.appendWriteConcernToCommand;
import static com.mongodb.operation.CommandOperationHelper.writeConcernErrorTransformer;

/**
 * An operation that creates one or more indexes.
 *
 *
Multiple index creation is supported starting with MongoDB server version 2.6

 *
 * @mongodb.driver.manual reference/command/createIndexes/ Create indexes
 * @since 3.0
 */
@Deprecated
public class CreateIndexesOperation implements AsyncWriteOperation<Void>, WriteOperation<Void> {
    private final MongoNamespace namespace;
    private final List<IndexRequest> requests;
    private final WriteConcern writeConcern;
    private long maxTimeMS;

    /**
     * Construct a new instance.
     *
     * @param namespace     the database and collection namespace for the operation.
     * @param requests the index request
     * @deprecated Prefer {@link #CreateIndexesOperation(MongoNamespace, List, WriteConcern)}
     */
    @Deprecated
    public CreateIndexesOperation(final MongoNamespace namespace, final List<IndexRequest> requests) {
        this(namespace, requests, null);
    }

    /**
     * Construct a new instance.
     *
     * @param namespace     the database and collection namespace for the operation.
     * @param requests the index request
     * @param writeConcern the write concern
     *
     * @since 3.4
     */
    public CreateIndexesOperation(final MongoNamespace namespace, final List<IndexRequest> requests, final WriteConcern writeConcern) {
        this.namespace = notNull("namespace", namespace);
        this.requests = notNull("indexRequests", requests);
        this.writeConcern = writeConcern;
    }

    /**
     * Gets the write concern.
     *
     * @return the write concern, which may be null
     *
     * @since 3.4
     */
    public WriteConcern getWriteConcern() {
        return writeConcern;
    }

    /**
     * Gets the index requests.
     *
     * @return the index requests
     */
    public List<IndexRequest> getRequests() {
        return requests;
    }

    /**
     * Gets the index names.
     *
     * @return a list of index names
     */
    public List<String> getIndexNames() {
        List<String> indexNames = new ArrayList<String>(requests.size());
        for (IndexRequest request : requests) {
            if (request.getName() != null) {
                indexNames.add(request.getName());
            } else {
                indexNames.add(IndexHelper.generateIndexName(request.getKeys()));
            }
        }
        return indexNames;
    }

    /**
     * Gets the maximum execution time on the server for this operation.  The default is 0, which places no limit on the execution time.
     *
     * @param timeUnit the time unit to return the result in
     * @return the maximum execution time in the given time unit
     * @since 3.6
     */
    public long getMaxTime(final TimeUnit timeUnit) {
        notNull("timeUnit", timeUnit);
        return timeUnit.convert(maxTimeMS, TimeUnit.MILLISECONDS);
    }

    /**
     * Sets the maximum execution time on the server for this operation.
     *
     * @param maxTime  the max time
     * @param timeUnit the time unit, which may not be null
     * @return this
     * @since 3.6
     */
    public CreateIndexesOperation maxTime(final long maxTime, final TimeUnit timeUnit) {
        notNull("timeUnit", timeUnit);
        isTrueArgument("maxTime >= 0", maxTime >= 0);
        this.maxTimeMS = TimeUnit.MILLISECONDS.convert(maxTime, timeUnit);
        return this;
    }

    @Override
    public Void execute(final WriteBinding binding) {
        return withConnection(binding, new CallableWithConnection<Void>() {
            @Override
            public Void call(final Connection connection) {
                try {
                    validateIndexRequestCollations(connection, requests);
                    executeCommand(binding, namespace.getDatabaseName(), getCommand(connection.getDescription()),
                            connection, writeConcernErrorTransformer());
                } catch (MongoCommandException e) {
                    throw checkForDuplicateKeyError(e);
                }
                return null;
            }
        });
    }

    @Override
    public void executeAsync(final AsyncWriteBinding binding, final SingleResultCallback<Void> callback) {
        withConnection(binding, new AsyncCallableWithConnection() {
            @Override
            public void call(final AsyncConnection connection, final Throwable t) {
                SingleResultCallback<Void> errHandlingCallback = errorHandlingCallback(callback, LOGGER);
                if (t != null) {
                    errHandlingCallback.onResult(null, t);
                } else {
                    final SingleResultCallback<Void> wrappedCallback = releasingCallback(errHandlingCallback, connection);
                    validateIndexRequestCollations(connection, requests, new AsyncCallableWithConnection() {
                        @Override
                        public void call(final AsyncConnection connection, final Throwable t) {
                            if (t != null) {
                                wrappedCallback.onResult(null, t);
                            } else {
                                executeCommandAsync(binding, namespace.getDatabaseName(),
                                        getCommand(connection.getDescription()), connection, writeConcernErrorWriteTransformer(),
                                        new SingleResultCallback<Void>() {
                                            @Override
                                            public void onResult(final Void result, final Throwable t) {
                                                wrappedCallback.onResult(null, translateException(t));
                                            }
                                        });
                            }
                        }
                    });
                }
            }
        });
    }

    private BsonDocument getIndex(final IndexRequest request) {
        BsonDocument index = new BsonDocument();
        index.append("key", request.getKeys());
        index.append("name", new BsonString(request.getName() != null ? request.getName() : generateIndexName(request.getKeys())));
        index.append("ns", new BsonString(namespace.getFullName()));
        if (request.isBackground()) {
            index.append("background", BsonBoolean.TRUE);
        }
        if (request.isUnique()) {
            index.append("unique", BsonBoolean.TRUE);
        }
        if (request.isSparse()) {
            index.append("sparse", BsonBoolean.TRUE);
        }
        if (request.getExpireAfter(TimeUnit.SECONDS) != null) {
            index.append("expireAfterSeconds", new BsonInt64(request.getExpireAfter(TimeUnit.SECONDS)));
        }
        if (request.getVersion() != null) {
            index.append("v", new BsonInt32(request.getVersion()));
        }
        if (request.getWeights() != null) {
            index.append("weights", request.getWeights());
        }
        if (request.getDefaultLanguage() != null) {
            index.append("default_language", new BsonString(request.getDefaultLanguage()));
        }
        if (request.getLanguageOverride() != null) {
            index.append("language_override", new BsonString(request.getLanguageOverride()));
        }
        if (request.getTextVersion() != null) {
            index.append("textIndexVersion", new BsonInt32(request.getTextVersion()));
        }
        if (request.getSphereVersion() != null) {
            index.append("2dsphereIndexVersion", new BsonInt32(request.getSphereVersion()));
        }
        if (request.getBits() != null) {
            index.append("bits", new BsonInt32(request.getBits()));
        }
        if (request.getMin() != null) {
            index.append("min", new BsonDouble(request.getMin()));
        }
        if (request.getMax() != null) {
            index.append("max", new BsonDouble(request.getMax()));
        }
        if (request.getBucketSize() != null) {
            index.append("bucketSize", new BsonDouble(request.getBucketSize()));
        }
        if (request.getDropDups()) {
            index.append("dropDups", BsonBoolean.TRUE);
        }
        if (request.getStorageEngine() != null) {
            index.append("storageEngine", request.getStorageEngine());
        }
        if (request.getPartialFilterExpression() != null) {
            index.append("partialFilterExpression", request.getPartialFilterExpression());
        }
        if (request.getCollation() != null) {
            index.append("collation", request.getCollation().asDocument());
        }
        if (request.getWildcardProjection() != null) {
            index.append("wildcardProjection", request.getWildcardProjection());
        }
        return index;
    }

    private BsonDocument getCommand(final ConnectionDescription description) {
        BsonDocument command = new BsonDocument("createIndexes", new BsonString(namespace.getCollectionName()));
        List<BsonDocument> values = new ArrayList<BsonDocument>();
        for (IndexRequest request : requests) {
            values.add(getIndex(request));
        }
        command.put("indexes", new BsonArray(values));
        putIfNotZero(command, "maxTimeMS", maxTimeMS);
        appendWriteConcernToCommand(writeConcern, command, description);
        return command;
    }

    private MongoException translateException(final Throwable t) {
        return (t instanceof MongoCommandException) ? checkForDuplicateKeyError((MongoCommandException) t)
                                                      : MongoException.fromThrowable(t);
    }

    @SuppressWarnings("deprecation")
    private MongoException checkForDuplicateKeyError(final MongoCommandException e) {
        if (ErrorCategory.fromErrorCode(e.getCode()) == ErrorCategory.DUPLICATE_KEY) {
            return new DuplicateKeyException(e.getResponse(), e.getServerAddress(), WriteConcernResult.acknowledged(0, false, null));
        } else {
            return e;
        }
    }
}


/*
 * Copyright 2008-present 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.client.model;

import com.mongodb.CursorType;
import com.mongodb.lang.Nullable;
import org.bson.conversions.Bson;

import java.util.concurrent.TimeUnit;

import static com.mongodb.assertions.Assertions.isTrueArgument;
import static com.mongodb.assertions.Assertions.notNull;

/**
 * The options to apply to a find operation (also commonly referred to as a query).
 *
 * @since 3.0
 * @mongodb.driver.manual tutorial/query-documents/ Find
 * @mongodb.driver.manual ../meta-driver/latest/legacy/mongodb-wire-protocol/#op-query OP_QUERY
 * @deprecated there is no replacement for this class
 */
@Deprecated
public final class FindOptions {
    private int batchSize;
    private int limit;
    private Bson modifiers;
    private Bson projection;
    private long maxTimeMS;
    private long maxAwaitTimeMS;
    private int skip;
    private Bson sort;
    private CursorType cursorType = CursorType.NonTailable;
    private boolean noCursorTimeout;
    private boolean oplogReplay;
    private boolean partial;
    private Collation collation;
    private String comment;
    private Bson hint;
    private Bson max;
    private Bson min;
    private long maxScan;
    private boolean returnKey;
    private boolean showRecordId;
    private boolean snapshot;

    /**
     * Construct a new instance.
     */
    public FindOptions() {
    }

    /**
     * Construct a new instance by making a shallow copy of the given model.
     * @param from model to copy
     * @deprecated this constructor is unused
     */
    @Deprecated
    public FindOptions(final FindOptions from) {
        batchSize = from.batchSize;
        limit = from.limit;
        modifiers = from.modifiers;
        projection = from.projection;
        maxTimeMS = from.maxTimeMS;
        maxAwaitTimeMS = from.maxAwaitTimeMS;
        skip = from.skip;
        sort = from.sort;
        cursorType = from.cursorType;
        noCursorTimeout = from.noCursorTimeout;
        oplogReplay = from.oplogReplay;
        partial = from.partial;
        comment = from.comment;
        hint = from.hint;
        max = from.max;
        min = from.min;
        maxScan = from.maxScan;
        returnKey = from.returnKey;
        showRecordId = from.showRecordId;
        snapshot = from.snapshot;
    }

    /**
     * Gets the limit to apply.  The default is null.
     *
     * @return the limit
     * @mongodb.driver.manual reference/method/cursor.limit/#cursor.limit Limit
     */
    public int getLimit() {
        return limit;
    }

    /**
     * Sets the limit to apply.
     *
     * @param limit the limit, which may be null
     * @return this
     * @mongodb.driver.manual reference/method/cursor.limit/#cursor.limit Limit
     */
    public FindOptions limit(final int limit) {
        this.limit = limit;
        return this;
    }

    /**
     * Gets the number of documents to skip. The default is 0.
     *
     * @return the number of documents to skip
     * @mongodb.driver.manual reference/method/cursor.skip/#cursor.skip Skip
     */
    public int getSkip() {
        return skip;
    }

    /**
     * Sets the number of documents to skip.
     *
     * @param skip the number of documents to skip
     * @return this
     * @mongodb.driver.manual reference/method/cursor.skip/#cursor.skip Skip
     */
    public FindOptions skip(final int skip) {
        this.skip = skip;
        return this;
    }

    /**
     * Gets the maximum execution time on the server for this operation.  The default is 0, which places no limit on the execution time.
     *
     * @param timeUnit the time unit to return the result in
     * @return the maximum execution time in the given time unit
     * @mongodb.driver.manual reference/method/cursor.maxTimeMS/#cursor.maxTimeMS Max Time
     */
    public long getMaxTime(final TimeUnit timeUnit) {
        notNull("timeUnit", timeUnit);
        return timeUnit.convert(maxTimeMS, TimeUnit.MILLISECONDS);
    }

    /**
     * Sets the maximum execution time on the server for this operation.
     *
     * @param maxTime  the max time
     * @param timeUnit the time unit, which may not be null
     * @return this
     * @mongodb.driver.manual reference/method/cursor.maxTimeMS/#cursor.maxTimeMS Max Time
     */
    public FindOptions maxTime(final long maxTime, final TimeUnit timeUnit) {
        notNull("timeUnit", timeUnit);
        isTrueArgument("maxTime > = 0", maxTime >= 0);
        this.maxTimeMS = TimeUnit.MILLISECONDS.convert(maxTime, timeUnit);
        return this;
    }

    /**
     * The maximum amount of time for the server to wait on new documents to satisfy a tailable cursor
     * query. This only applies to a TAILABLE_AWAIT cursor. When the cursor is not a TAILABLE_AWAIT cursor,
     * this option is ignored.
     *
     * On servers >= 3.2, this option will be specified on the getMore command as "maxTimeMS". The default
     * is no value: no "maxTimeMS" is sent to the server with the getMore command.
     *
     * On servers < 3.2, this option is ignored, and indicates that the driver should respect the server's default value
     *
     * A zero value will be ignored.
     *
     * @param timeUnit the time unit to return the result in
     * @return the maximum await execution time in the given time unit
     * @since 3.2
     * @mongodb.driver.manual reference/method/cursor.maxTimeMS/#cursor.maxTimeMS Max Time
     */
    public long getMaxAwaitTime(final TimeUnit timeUnit) {
        notNull("timeUnit", timeUnit);
        return timeUnit.convert(maxAwaitTimeMS, TimeUnit.MILLISECONDS);
    }

    /**
     * Sets the maximum await execution time on the server for this operation.
     *
     * @param maxAwaitTime  the max await time.  A zero value will be ignored, and indicates that the driver should respect the server's
     *                      default value
     * @param timeUnit the time unit, which may not be null
     * @return this
     * @since 3.2
     * @mongodb.driver.manual reference/method/cursor.maxTimeMS/#cursor.maxTimeMS Max Time
     */
    public FindOptions maxAwaitTime(final long maxAwaitTime, final TimeUnit timeUnit) {
        notNull("timeUnit", timeUnit);
        isTrueArgument("maxAwaitTime > = 0", maxAwaitTime >= 0);
        this.maxAwaitTimeMS = TimeUnit.MILLISECONDS.convert(maxAwaitTime, timeUnit);
        return this;
    }

    /**
     * Gets the number of documents to return per batch.  Default to 0, which indicates that the server chooses an appropriate batch
     * size.
     *
     * @return the batch size
     * @mongodb.driver.manual reference/method/cursor.batchSize/#cursor.batchSize Batch Size
     */
    public int getBatchSize() {
        return batchSize;
    }

    /**
     * Sets the number of documents to return per batch.
     *
     * @param batchSize the batch size
     * @return this
     * @mongodb.driver.manual reference/method/cursor.batchSize/#cursor.batchSize Batch Size
     */
    public FindOptions batchSize(final int batchSize) {
        this.batchSize = batchSize;
        return this;
    }

    /**
     * Gets the query modifiers to apply to this operation.  The default is not to apply any modifiers.
     *
     * @return the query modifiers, which may be null
     * @mongodb.driver.manual reference/operator/query-modifier/ Query Modifiers
     * @deprecated use the individual modifier methods instead.
     */
    @Deprecated
    @Nullable
    public Bson getModifiers() {
        return modifiers;
    }

    /**
     * Sets the query modifiers to apply to this operation.
     *
     * @param modifiers the query modifiers to apply, which may be null.
     * @return this
     * @mongodb.driver.manual reference/operator/query-modifier/ Query Modifiers
     * @deprecated use the individual modifier methods instead.
     */
    @Deprecated
    public FindOptions modifiers(@Nullable final Bson modifiers) {
        this.modifiers = modifiers;
        return this;
    }

    /**
     * Gets a document describing the fields to return for all matching documents.
     *
     * @return the project document, which may be null
     * @mongodb.driver.manual reference/method/db.collection.find/ Projection
     */
    @Nullable
    public Bson getProjection() {
        return projection;
    }

    /**
     * Sets a document describing the fields to return for all matching documents.
     *
     * @param projection the project document, which may be null.
     * @return this
     * @mongodb.driver.manual reference/method/db.collection.find/ Projection
     */
    public FindOptions projection(@Nullable final Bson projection) {
        this.projection = projection;
        return this;
    }

    /**
     * Gets the sort criteria to apply to the query. The default is null, which means that the documents will be returned in an undefined
     * order.
     *
     * @return a document describing the sort criteria
     * @mongodb.driver.manual reference/method/cursor.sort/ Sort
     */
    @Nullable
    public Bson getSort() {
        return sort;
    }

    /**
     * Sets the sort criteria to apply to the query.
     *
     * @param sort the sort criteria, which may be null.
     * @return this
     * @mongodb.driver.manual reference/method/cursor.sort/ Sort
     */
    public FindOptions sort(@Nullable final Bson sort) {
        this.sort = sort;
        return this;
    }

    /**
     * The server normally times out idle cursors after an inactivity period (10 minutes)
     * to prevent excess memory use.  If true, that timeout is disabled.
     *
     * @return true if cursor timeout is disabled
     */
    public boolean isNoCursorTimeout() {
        return noCursorTimeout;
    }

    /**
     * The server normally times out idle cursors after an inactivity period (10 minutes)
     * to prevent excess memory use. Set this option to prevent that.
     *
     * @param noCursorTimeout true if cursor timeout is disabled
     * @return this
     */
    public FindOptions noCursorTimeout(final boolean noCursorTimeout) {
        this.noCursorTimeout = noCursorTimeout;
        return this;
    }

    /**
     * Users should not set this under normal circumstances.
     *
     * @return if oplog replay is enabled
     */
    public boolean isOplogReplay() {
        return oplogReplay;
    }

    /**
     * Users should not set this under normal circumstances.
     *
     * @param oplogReplay if oplog replay is enabled
     * @return this
     */
    public FindOptions oplogReplay(final boolean oplogReplay) {
        this.oplogReplay = oplogReplay;
        return this;
    }

    /**
     * Get partial results from a sharded cluster if one or more shards are unreachable (instead of throwing an error).
     *
     * @return if partial results for sharded clusters is enabled
     */
    public boolean isPartial() {
        return partial;
    }

    /**
     * Get partial results from a sharded cluster if one or more shards are unreachable (instead of throwing an error).
     *
     * @param partial if partial results for sharded clusters is enabled
     * @return this
     */
    public FindOptions partial(final boolean partial) {
        this.partial = partial;
        return this;
    }

    /**
     * Get the cursor type.
     *
     * @return the cursor type
     */
    public CursorType getCursorType() {
        return cursorType;
    }

    /**
     * Sets the cursor type.
     *
     * @param cursorType the cursor type
     * @return this
     */
    public FindOptions cursorType(final CursorType cursorType) {
        this.cursorType = notNull("cursorType", cursorType);
        return this;
    }

    /**
     * Returns the collation options
     *
     * @return the collation options
     * @since 3.4
     * @mongodb.server.release 3.4
     */
    @Nullable
    public Collation getCollation() {
        return collation;
    }

    /**
     * Sets the collation options
     *
     *
A null value represents the server default.

     * @param collation the collation options to use
     * @return this
     * @since 3.4
     * @mongodb.server.release 3.4
     */
    public FindOptions collation(@Nullable final Collation collation) {
        this.collation = collation;
        return this;
    }

    /**
     * Returns the comment to send with the query. The default is not to include a comment with the query.
     *
     * @return the comment
     * @since 3.5
     */
    @Nullable
    public String getComment() {
        return comment;
    }

    /**
     * Sets the comment to the query. A null value means no comment is set.
     *
     * @param comment the comment
     * @return this
     * @since 3.5
     */
    public FindOptions comment(@Nullable final String comment) {
        this.comment = comment;
        return this;
    }

    /**
     * Returns the hint for which index to use. The default is not to set a hint.
     *
     * @return the hint
     * @since 3.5
     */
    @Nullable
    public Bson getHint() {
        return hint;
    }

    /**
     * Sets the hint for which index to use. A null value means no hint is set.
     *
     * @param hint the hint
     * @return this
     * @since 3.5
     */
    public FindOptions hint(@Nullable final Bson hint) {
        this.hint = hint;
        return this;
    }

    /**
     * Returns the exclusive upper bound for a specific index. By default there is no max bound.
     *
     * @return the max
     * @since 3.5
     */
    @Nullable
    public Bson getMax() {
        return max;
    }

    /**
     * Sets the exclusive upper bound for a specific index. A null value means no max is set.
     *
     * @param max the max
     * @return this
     * @since 3.5
     */
    public FindOptions max(@Nullable final Bson max) {
        this.max = max;
        return this;
    }

    /**
     * Returns the minimum inclusive lower bound for a specific index. By default there is no min bound.
     *
     * @return the min
     * @since 3.5
     */
    @Nullable
    public Bson getMin() {
        return min;
    }

    /**
     * Sets the minimum inclusive lower bound for a specific index. A null value means no max is set.
     *
     * @param min the min
     * @return this
     * @since 3.5
     */
    public FindOptions min(@Nullable final Bson min) {
        this.min = min;
        return this;
    }

    /**
     * Returns the maximum number of documents or index keys to scan when executing the query.
     *
     * A zero value or less will be ignored, and indicates that the driver should respect the server's default value.
     *
     * @return the maxScan
     * @since 3.5
     * @deprecated Deprecated as of MongoDB 4.0 release
     */
    @Deprecated
    public long getMaxScan() {
        return maxScan;
    }

    /**
     * Sets the maximum number of documents or index keys to scan when executing the query.
     *
     * A zero value or less will be ignored, and indicates that the driver should respect the server's default value.
     *
     * @param maxScan the maxScan
     * @return this
     * @since 3.5
     * @deprecated Deprecated as of MongoDB 4.0 release
     */
    @Deprecated
    public FindOptions maxScan(final long maxScan) {
        this.maxScan = maxScan;
        return this;
    }

    /**
     * Returns the returnKey. If true the find operation will return only the index keys in the resulting documents.
     *
     * Default value is false. If returnKey is true and the find command does not use an index, the returned documents will be empty.
     *
     * @return the returnKey
     * @since 3.5
     */
    public boolean isReturnKey() {
        return returnKey;
    }

    /**
     * Sets the returnKey. If true the find operation will return only the index keys in the resulting documents.
     *
     * @param returnKey the returnKey
     * @return this
     * @since 3.5
     */
    public FindOptions returnKey(final boolean returnKey) {
        this.returnKey = returnKey;
        return this;
    }

    /**
     * Returns the showRecordId.
     *
     * Determines whether to return the record identifier for each document. If true, adds a field $recordId to the returned documents.
     * The default is false.
     *
     * @return the showRecordId
     * @since 3.5
     */
    public boolean isShowRecordId() {
        return showRecordId;
    }

    /**
     * Sets the showRecordId. Set to true to add a field {@code $recordId} to the returned documents.
     *
     * @param showRecordId the showRecordId
     * @return this
     * @since 3.5
     */
    public FindOptions showRecordId(final boolean showRecordId) {
        this.showRecordId = showRecordId;
        return this;
    }

    /**
     * Returns the snapshot.
     *
     * Prevents the cursor from returning a document more than once because of an intervening write operation. The default is false.
     *
     * @return the snapshot
     * @since 3.5
     * @deprecated Deprecated in MongoDB 3.6 release and removed in MongoDB 4.0 release
     */
    @Deprecated
    public boolean isSnapshot() {
        return snapshot;
    }

    /**
     * Sets the snapshot.
     *
     * If true it prevents the cursor from returning a document more than once because of an intervening write operation.
     *
     * @param snapshot the snapshot
     * @return this
     * @since 3.5
     * @deprecated Deprecated in MongoDB 3.6 release and removed in MongoDB 4.0 release
     */
    @Deprecated
    public FindOptions snapshot(final boolean snapshot) {
        this.snapshot = snapshot;
        return this;
    }

    @Override
    public String toString() {
        return "FindOptions{"
                + "batchSize=" + batchSize
                + ", limit=" + limit
                + ", modifiers=" + modifiers
                + ", projection=" + projection
                + ", maxTimeMS=" + maxTimeMS
                + ", maxAwaitTimeMS=" + maxAwaitTimeMS
                + ", skip=" + skip
                + ", sort=" + sort
                + ", cursorType=" + cursorType
                + ", noCursorTimeout=" + noCursorTimeout
                + ", oplogReplay=" + oplogReplay
                + ", partial=" + partial
                + ", collation=" + collation
                + ", comment='" + comment + "'"
                + ", hint=" + hint
                + ", max=" + max
                + ", min=" + min
                + ", maxScan=" + maxScan
                + ", returnKey=" + returnKey
                + ", showRecordId=" + showRecordId
                + ", snapshot=" + snapshot
                + "}";
    }
}


/*
 * Copyright 2008-present 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 org.bson;

import org.bson.codecs.BsonDocumentCodec;
import org.bson.codecs.DecoderContext;
import org.bson.codecs.EncoderContext;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.conversions.Bson;
import org.bson.io.BasicOutputBuffer;
import org.bson.json.JsonReader;
import org.bson.json.JsonWriter;
import org.bson.json.JsonWriterSettings;

import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.io.StringWriter;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static java.lang.String.format;

/**
 * A type-safe container for a BSON document.  This class should NOT be sub-classed by third parties.
 *
 * @since 3.0
 */
public class BsonDocument extends BsonValue implements Map<String, BsonValue>, Cloneable, Bson, Serializable {
    private static final long serialVersionUID = 1L;

    private final Map<String, BsonValue> map = new LinkedHashMap<String, BsonValue>();

    /**
     * Parses a string in MongoDB Extended JSON format to a {@code BsonDocument}
     *
     * @param json the JSON string
     * @return a corresponding {@code BsonDocument} object
     * @see org.bson.json.JsonReader
     * @mongodb.driver.manual reference/mongodb-extended-json/ MongoDB Extended JSON
     */
    public static BsonDocument parse(final String json) {
        return new BsonDocumentCodec().decode(new JsonReader(json), DecoderContext.builder().build());
    }

    /**
     * Construct a new instance with the given list {@code BsonElement}, none of which may be null.
     *
     * @param bsonElements a list of {@code BsonElement}
     */
    public BsonDocument(final List<BsonElement> bsonElements) {
        for (BsonElement cur : bsonElements) {
            put(cur.getName(), cur.getValue());
        }
    }

    /**
     * Construct a new instance with a single key value pair
     *
     * @param key   the key
     * @param value the value
     */
    public BsonDocument(final String key, final BsonValue value) {
        put(key, value);
    }

    /**
     * Construct an empty document.
     */
    public BsonDocument() {
    }

    @Override
    public <C> BsonDocument toBsonDocument(final Class<C> documentClass, final CodecRegistry codecRegistry) {
        return this;
    }

    @Override
    public BsonType getBsonType() {
        return BsonType.DOCUMENT;
    }

    @Override
    public int size() {
        return map.size();
    }

    @Override
    public boolean isEmpty() {
        return map.isEmpty();
    }

    @Override
    public boolean containsKey(final Object key) {
        return map.containsKey(key);
    }

    @Override
    public boolean containsValue(final Object value) {
        return map.containsValue(value);
    }

    @Override
    public BsonValue get(final Object key) {
        return map.get(key);
    }

    /**
     * Gets the value of the key if it is a BsonDocument, or throws if not.
     *
     * @param key the key
     * @return the value of the key as a BsonDocument
     * @throws org.bson.BsonInvalidOperationException if the document does not contain the key or the value is not a BsonDocument
     */
    public BsonDocument getDocument(final Object key) {
        throwIfKeyAbsent(key);
        return get(key).asDocument();
    }

    /**
     * Gets the value of the key if it is a BsonArray, or throws if not.
     *
     * @param key the key
     * @return the value of the key as a BsonArray
     * @throws org.bson.BsonInvalidOperationException if the document does not contain the key or the value is not of the expected type
     */
    public BsonArray getArray(final Object key) {
        throwIfKeyAbsent(key);

        return get(key).asArray();
    }

    /**
     * Gets the value of the key if it is a BsonNumber, or throws if not.
     *
     * @param key the key
     * @return the value of the key as a BsonNumber
     * @throws org.bson.BsonInvalidOperationException if the document does not contain the key or the value is not of the expected type
     */
    public BsonNumber getNumber(final Object key) {
        throwIfKeyAbsent(key);
        return get(key).asNumber();
    }

    /**
     * Gets the value of the key if it is a BsonInt32, or throws if not.
     *
     * @param key the key
     * @return the value of the key as a BsonInt32
     * @throws org.bson.BsonInvalidOperationException if the document does not contain the key or the value is not of the expected type
     */
    public BsonInt32 getInt32(final Object key) {
        throwIfKeyAbsent(key);
        return get(key).asInt32();
    }

    /**
     * Gets the value of the key if it is a BsonInt64, or throws if not.
     *
     * @param key the key
     * @return the value of the key as a BsonInt64
     * @throws org.bson.BsonInvalidOperationException if the document does not contain the key or the value is not of the expected type
     */
    public BsonInt64 getInt64(final Object key) {
        throwIfKeyAbsent(key);
        return get(key).asInt64();
    }

    /**
     * Gets the value of the key if it is a BsonDecimal128, or throws if not.
     *
     * @param key the key
     * @return the value of the key as a BsonDecimal128
     * @throws org.bson.BsonInvalidOperationException if the document does not contain the key or the value is not of the expected type
     * @since 3.4
     */
    public BsonDecimal128 getDecimal128(final Object key) {
        throwIfKeyAbsent(key);
        return get(key).asDecimal128();
    }

    /**
     * Gets the value of the key if it is a BsonDouble, or throws if not.
     *
     * @param key the key
     * @return the value of the key as a BsonDouble
     * @throws org.bson.BsonInvalidOperationException if the document does not contain the key or the value is not of the expected type
     */
    public BsonDouble getDouble(final Object key) {
        throwIfKeyAbsent(key);
        return get(key).asDouble();
    }

    /**
     * Gets the value of the key if it is a BsonBoolean, or throws if not.
     *
     * @param key the key
     * @return the value of the key as a BsonBoolean
     * @throws org.bson.BsonInvalidOperationException if the document does not contain the key or the value is not of the expected type
     */
    public BsonBoolean getBoolean(final Object key) {
        throwIfKeyAbsent(key);
        return get(key).asBoolean();
    }

    /**
     * Gets the value of the key if it is a BsonString, or throws if not.
     *
     * @param key the key
     * @return the value of the key as a BsonString
     * @throws org.bson.BsonInvalidOperationException if the document does not contain the key or the value is not of the expected type
     */
    public BsonString getString(final Object key) {
        throwIfKeyAbsent(key);
        return get(key).asString();
    }

    /**
     * Gets the value of the key if it is a BsonDateTime, or throws if not.
     *
     * @param key the key
     * @return the value of the key as a BsonDateTime
     * @throws org.bson.BsonInvalidOperationException if the document does not contain the key or the value is not of the expected type
     */
    public BsonDateTime getDateTime(final Object key) {
        throwIfKeyAbsent(key);
        return get(key).asDateTime();
    }

    /**
     * Gets the value of the key if it is a BsonTimestamp, or throws if not.
     *
     * @param key the key
     * @return the value of the key as a BsonTimestamp
     * @throws org.bson.BsonInvalidOperationException if the document does not contain the key or the value is not of the expected type
     */
    public BsonTimestamp getTimestamp(final Object key) {
        throwIfKeyAbsent(key);
        return get(key).asTimestamp();
    }

    /**
     * Gets the value of the key if it is a BsonObjectId, or throws if not.
     *
     * @param key the key
     * @return the value of the key as a BsonObjectId
     * @throws org.bson.BsonInvalidOperationException if the document does not contain the key or the value is not of the expected type
     */
    public BsonObjectId getObjectId(final Object key) {
        throwIfKeyAbsent(key);
        return get(key).asObjectId();
    }

    /**
     * Gets the value of the key if it is a BsonRegularExpression, or throws if not.
     *
     * @param key the key
     * @return the value of the key as a BsonRegularExpression
     * @throws org.bson.BsonInvalidOperationException if the document does not contain the key or the value is not of the expected type
     */
    public BsonRegularExpression getRegularExpression(final Object key) {
        throwIfKeyAbsent(key);
        return get(key).asRegularExpression();
    }

    /**
     * Gets the value of the key if it is a BsonBinary, or throws if not.
     *
     * @param key the key
     * @return the value of the key as a BsonBinary
     * @throws org.bson.BsonInvalidOperationException if the document does not contain the key or the value is not of the expected type
     */
    public BsonBinary getBinary(final Object key) {
        throwIfKeyAbsent(key);
        return get(key).asBinary();
    }

    /**
     * Returns true if the value of the key is a BsonNull, returns false if the document does not contain the key.
     *
     * @param key the key
     * @return true if the value of the key is a BsonNull, returns false if the document does not contain the key.
     */
    public boolean isNull(final Object key) {
        if (!containsKey(key)) {
            return false;
        }
        return get(key).isNull();
    }

    /**
     * Returns true if the value of the key is a BsonDocument, returns false if the document does not contain the key.
     *
     * @param key the key
     * @return true if the value of the key is a BsonDocument, returns false if the document does not contain the key.
     */
    public boolean isDocument(final Object key) {
        if (!containsKey(key)) {
            return false;
        }
        return get(key).isDocument();
    }

    /**
     * Returns true if the value of the key is a BsonArray, returns false if the document does not contain the key.
     *
     * @param key the key
     * @return true if the value of the key is a BsonArray, returns false if the document does not contain the key.
     */
    public boolean isArray(final Object key) {
        if (!containsKey(key)) {
            return false;
        }
        return get(key).isArray();
    }

    /**
     * Returns true if the value of the key is a BsonNumber, returns false if the document does not contain the key.
     *
     * @param key the key
     * @return true if the value of the key is a BsonNumber, returns false if the document does not contain the key.
     */
    public boolean isNumber(final Object key) {
        if (!containsKey(key)) {
            return false;
        }
        return get(key).isNumber();
    }

    /**
     * Returns true if the value of the key is a BsonInt32, returns false if the document does not contain the key.
     *
     * @param key the key
     * @return true if the value of the key is a BsonInt32, returns false if the document does not contain the key.
     */
    public boolean isInt32(final Object key) {
        if (!containsKey(key)) {
            return false;
        }
        return get(key).isInt32();
    }

    /**
     * Returns true if the value of the key is a BsonInt64, returns false if the document does not contain the key.
     *
     * @param key the key
     * @return true if the value of the key is a BsonInt64, returns false if the document does not contain the key.
     */
    public boolean isInt64(final Object key) {
        if (!containsKey(key)) {
            return false;
        }
        return get(key).isInt64();
    }

    /**
     * Returns true if the value of the key is a BsonDecimal128, returns false if the document does not contain the key.
     *
     * @param key the key
     * @return true if the value of the key is a BsonDecimal128, returns false if the document does not contain the key.
     * @since 3.4
     */
    public boolean isDecimal128(final Object key) {
        if (!containsKey(key)) {
            return false;
        }
        return get(key).isDecimal128();
    }

    /**
     * Returns true if the value of the key is a BsonDouble, returns false if the document does not contain the key.
     *
     * @param key the key
     * @return true if the value of the key is a BsonDouble, returns false if the document does not contain the key.
     */
    public boolean isDouble(final Object key) {
        if (!containsKey(key)) {
            return false;
        }
        return get(key).isDouble();
    }

    /**
     * Returns true if the value of the key is a BsonBoolean, returns false if the document does not contain the key.
     *
     * @param key the key
     * @return true if the value of the key is a BsonBoolean, returns false if the document does not contain the key.
     */
    public boolean isBoolean(final Object key) {
        if (!containsKey(key)) {
            return false;
        }
        return get(key).isBoolean();
    }

    /**
     * Returns true if the value of the key is a BsonString, returns false if the document does not contain the key.
     *
     * @param key the key
     * @return true if the value of the key is a BsonString, returns false if the document does not contain the key.
     */
    public boolean isString(final Object key) {
        if (!containsKey(key)) {
            return false;
        }
        return get(key).isString();
    }

    /**
     * Returns true if the value of the key is a BsonDateTime, returns false if the document does not contain the key.
     *
     * @param key the key
     * @return true if the value of the key is a BsonDateTime, returns false if the document does not contain the key.
     */
    public boolean isDateTime(final Object key) {
        if (!containsKey(key)) {
            return false;
        }
        return get(key).isDateTime();
    }

    /**
     * Returns true if the value of the key is a BsonTimestamp, returns false if the document does not contain the key.
     *
     * @param key the key
     * @return true if the value of the key is a BsonTimestamp, returns false if the document does not contain the key.
     */
    public boolean isTimestamp(final Object key) {
        if (!containsKey(key)) {
            return false;
        }
        return get(key).isTimestamp();
    }

    /**
     * Returns true if the value of the key is a BsonObjectId, returns false if the document does not contain the key.
     *
     * @param key the key
     * @return true if the value of the key is a BsonObjectId, returns false if the document does not contain the key.
     */
    public boolean isObjectId(final Object key) {
        if (!containsKey(key)) {
            return false;
        }
        return get(key).isObjectId();
    }

    /**
     * Returns true if the value of the key is a BsonBinary, returns false if the document does not contain the key.
     *
     * @param key the key
     * @return true if the value of the key is a BsonBinary, returns false if the document does not contain the key.
     */
    public boolean isBinary(final Object key) {
        if (!containsKey(key)) {
            return false;
        }
        return get(key).isBinary();
    }

    /**
     * If the document does not contain the given key, return the given default value.  Otherwise, gets the value of the key.
     *
     * @param key the key
     * @param defaultValue the default value
     * @return the value of the key as a BsonValue
     */
    public BsonValue get(final Object key, final BsonValue defaultValue) {
        BsonValue value = get(key);
        return value != null ? value : defaultValue;
    }

    /**
     * If the document does not contain the given key, return the given default value.  Otherwise, gets the value of the key as a
     * BsonDocument.
     *
     * @param key the key
     * @param defaultValue the default value
     * @return the value of the key as a BsonDocument
     * @throws org.bson.BsonInvalidOperationException if the document contains the key but the value is not of the expected type
     */
    public BsonDocument getDocument(final Object key, final BsonDocument defaultValue) {
        if (!containsKey(key)) {
            return defaultValue;
        }
        return get(key).asDocument();
    }

    /**
     * If the document does not contain the given key, return the given default value.  Otherwise, gets the value of the key as a
     * BsonArray.
     *
     * @param key the key
     * @param defaultValue the default value
     * @return the value of the key as a BsonArray
     * @throws org.bson.BsonInvalidOperationException if the document contains the key but the value is not of the expected type
     */
    public BsonArray getArray(final Object key, final BsonArray defaultValue) {
        if (!containsKey(key)) {
            return defaultValue;
        }
        return get(key).asArray();
    }

    /**
     * If the document does not contain the given key, return the given default value.  Otherwise, gets the value of the key as a
     * BsonNumber.
     *
     * @param key the key
     * @param defaultValue the default value
     * @return the value of the key as a BsonNumber
     * @throws org.bson.BsonInvalidOperationException if the document contains the key but the value is not of the expected type
     */
    public BsonNumber getNumber(final Object key, final BsonNumber defaultValue) {
        if (!containsKey(key)) {
            return defaultValue;
        }
        return get(key).asNumber();
    }

    /**
     * If the document does not contain the given key, return the given default value.  Otherwise, gets the value of the key as a
     * BsonInt32.
     *
     * @param key the key
     * @param defaultValue the default value
     * @return the value of the key as a BsonInt32
     * @throws org.bson.BsonInvalidOperationException if the document contains the key but the value is not of the expected type
     */
    public BsonInt32 getInt32(final Object key, final BsonInt32 defaultValue) {
        if (!containsKey(key)) {
            return defaultValue;
        }
        return get(key).asInt32();
    }

    /**
     * If the document does not contain the given key, return the given default value.  Otherwise, gets the value of the key as a
     * BsonInt64.
     *
     * @param key the key
     * @param defaultValue the default value
     * @return the value of the key as a BsonInt64
     * @throws org.bson.BsonInvalidOperationException if the document contains the key but the value is not of the expected type
     */
    public BsonInt64 getInt64(final Object key, final BsonInt64 defaultValue) {
        if (!containsKey(key)) {
            return defaultValue;
        }
        return get(key).asInt64();
    }

    /**
     * If the document does not contain the given key, return the given default value.  Otherwise, gets the value of the key as a
     * BsonDecimal128.
     *
     * @param key the key
     * @param defaultValue the default value
     * @return the value of the key as a BsonDecimal128
     * @throws org.bson.BsonInvalidOperationException if the document contains the key but the value is not of the expected type
     * @since 3.4
     */
    public BsonDecimal128 getDecimal128(final Object key, final BsonDecimal128 defaultValue) {
        if (!containsKey(key)) {
            return defaultValue;
        }
        return get(key).asDecimal128();
    }

    /**
     * If the document does not contain the given key, return the given default value.  Otherwise, gets the value of the key as a
     * BsonDouble.
     *
     * @param key the key
     * @param defaultValue the default value
     * @return the value of the key as a BsonDouble
     * @throws org.bson.BsonInvalidOperationException if the document contains the key but the value is not of the expected type
     */
    public BsonDouble getDouble(final Object key, final BsonDouble defaultValue) {
        if (!containsKey(key)) {
            return defaultValue;
        }
        return get(key).asDouble();
    }

    /**
     * If the document does not contain the given key, return the given default value.  Otherwise, gets the value of the key as a
     * BsonBoolean.
     *
     * @param key the key
     * @param defaultValue the default value
     * @return the value of the key as a BsonBoolean
     * @throws org.bson.BsonInvalidOperationException if the document contains the key but the value is not of the expected type
     */
    public BsonBoolean getBoolean(final Object key, final BsonBoolean defaultValue) {
        if (!containsKey(key)) {
            return defaultValue;
        }
        return get(key).asBoolean();
    }

    /**
     * If the document does not contain the given key, return the given default value.  Otherwise, gets the value of the key as a
     * BsonString.
     *
     * @param key the key
     * @param defaultValue the default value
     * @return the value of the key as a BsonString
     * @throws org.bson.BsonInvalidOperationException if the document contains the key but the value is not of the expected type
     */
    public BsonString getString(final Object key, final BsonString defaultValue) {
        if (!containsKey(key)) {
            return defaultValue;
        }
        return get(key).asString();
    }

    /**
     * If the document does not contain the given key, return the given default value.  Otherwise, gets the value of the key as a
     * BsonDateTime.
     *
     * @param key the key
     * @param defaultValue the default value
     * @return the value of the key as a BsonDateTime
     * @throws org.bson.BsonInvalidOperationException if the document contains the key but the value is not of the expected type
     */
    public BsonDateTime getDateTime(final Object key, final BsonDateTime defaultValue) {
        if (!containsKey(key)) {
            return defaultValue;
        }
        return get(key).asDateTime();
    }

    /**
     * If the document does not contain the given key, return the given default value.  Otherwise, gets the value of the key as a
     * BsonTimestamp.
     *
     * @param key the key
     * @param defaultValue the default value
     * @return the value of the key as a BsonTimestamp
     * @throws org.bson.BsonInvalidOperationException if the document contains the key but the value is not of the expected type
     */
    public BsonTimestamp getTimestamp(final Object key, final BsonTimestamp defaultValue) {
        if (!containsKey(key)) {
            return defaultValue;
        }
        return get(key).asTimestamp();
    }

    /**
     * If the document does not contain the given key, return the given default value.  Otherwise, gets the value of the key as a
     * BsonObjectId.
     *
     * @param key the key
     * @param defaultValue the default value
     * @return the value of the key as a BsonObjectId
     * @throws org.bson.BsonInvalidOperationException if the document contains the key but the value is not of the expected type
     */
    public BsonObjectId getObjectId(final Object key, final BsonObjectId defaultValue) {
        if (!containsKey(key)) {
            return defaultValue;
        }
        return get(key).asObjectId();
    }

    /**
     * If the document does not contain the given key, return the given default value.  Otherwise, gets the value of the key as a
     * BsonBinary.
     *
     * @param key the key
     * @param defaultValue the default value
     * @return the value of the key as a BsonBinary
     * @throws org.bson.BsonInvalidOperationException if the document contains the key but the value is not of the expected type
     */
    public BsonBinary getBinary(final Object key, final BsonBinary defaultValue) {
        if (!containsKey(key)) {
            return defaultValue;
        }
        return get(key).asBinary();
    }

    /**
     * If the document does not contain the given key, return the given default value.  Otherwise, gets the value of the key as a
     * BsonRegularExpression.
     *
     * @param key the key
     * @param defaultValue the default value
     * @return the value of the key as a BsonRegularExpression
     * @throws org.bson.BsonInvalidOperationException if the document contains the key but the value is not of the expected type
     */
    public BsonRegularExpression getRegularExpression(final Object key, final BsonRegularExpression defaultValue) {
        if (!containsKey(key)) {
            return defaultValue;
        }
        return get(key).asRegularExpression();
    }

    @Override
    public BsonValue put(final String key, final BsonValue value) {
        if (value == null) {
            throw new IllegalArgumentException(format("The value for key %s can not be null", key));
        }
        if (key.contains("\0")) {
            throw new BSONException(format("BSON cstring '%s' is not valid because it contains a null character at index %d", key,
                                           key.indexOf('\0')));
        }
        return map.put(key, value);
    }

    @Override
    public BsonValue remove(final Object key) {
        return map.remove(key);
    }

    @Override
    public void putAll(final Map<? extends String, ? extends BsonValue> m) {
        for (Map.Entry<? extends String, ? extends BsonValue> cur : m.entrySet()) {
            put(cur.getKey(), cur.getValue());
        }
    }

    @Override
    public void clear() {
        map.clear();
    }

    @Override
    public Set<String> keySet() {
        return map.keySet();
    }

    @Override
    public Collection<BsonValue> values() {
        return map.values();
    }

    @Override
    public Set<Entry<String, BsonValue>> entrySet() {
        return map.entrySet();
    }

    /**
     * Put the given key and value into this document, and return the document.
     *
     * @param key   the key
     * @param value the value
     * @return this
     */
    public BsonDocument append(final String key, final BsonValue value) {
        put(key, value);
        return this;
    }

    /**
     * Gets the first key in the document.
     *
     * @return the first key in the document
     * @throws java.util.NoSuchElementException if the document is empty
     * @since 3.6
     */
    public String getFirstKey() {
        return keySet().iterator().next();
    }

    /**
     * Gets the first value in the document
     *
     * @return the first value in the document
     * @throws java.util.NoSuchElementException if the document is empty
     * @since 3.7
     */
    public BsonReader asBsonReader() {
        return new BsonDocumentReader(this);
    }

    @Override
    public boolean equals(final Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof BsonDocument)) {
            return false;
        }

        BsonDocument that = (BsonDocument) o;

        return entrySet().equals(that.entrySet());
    }

    @Override
    public int hashCode() {
        return entrySet().hashCode();
    }

    /**
     * Gets a JSON representation of this document using the {@link org.bson.json.JsonMode#STRICT} output mode, and otherwise the default
     * settings of {@link JsonWriterSettings.Builder}.
     *
     * @return a JSON representation of this document
     * @see #toJson(JsonWriterSettings)
     * @see JsonWriterSettings
     */
    @SuppressWarnings("deprecation")
    public String toJson() {
        return toJson(new JsonWriterSettings());
    }

    /**
     * Gets a JSON representation of this document using the given {@code JsonWriterSettings}.
     * @param settings the JSON writer settings
     * @return a JSON representation of this document
     */
    public String toJson(final JsonWriterSettings settings) {
        StringWriter writer = new StringWriter();
        new BsonDocumentCodec().encode(new JsonWriter(writer, settings), this, EncoderContext.builder().build());
        return writer.toString();
    }

    @Override
    public String toString() {
        return toJson();
    }

    @Override
    public BsonDocument clone() {
        BsonDocument to = new BsonDocument();
        for (Entry<String, BsonValue> cur : entrySet()) {
            switch (cur.getValue().getBsonType()) {
                case DOCUMENT:
                    to.put(cur.getKey(), cur.getValue().asDocument().clone());
                    break;
                case ARRAY:
                    to.put(cur.getKey(), cur.getValue().asArray().clone());
                    break;
                case BINARY:
                    to.put(cur.getKey(), BsonBinary.clone(cur.getValue().asBinary()));
                    break;
                case JAVASCRIPT_WITH_SCOPE:
                    to.put(cur.getKey(), BsonJavaScriptWithScope.clone(cur.getValue().asJavaScriptWithScope()));
                    break;
                default:
                    to.put(cur.getKey(), cur.getValue());
            }
        }
        return to;
    }

    private void throwIfKeyAbsent(final Object key) {
        if (!containsKey(key)) {
            throw new BsonInvalidOperationException("Document does not contain key " + key);
        }
    }

    // see https://docs.oracle.com/javase/6/docs/platform/serialization/spec/output.html
    private Object writeReplace() {
        return new SerializationProxy(this);
    }

    // see https://docs.oracle.com/javase/6/docs/platform/serialization/spec/input.html
    private void readObject(final ObjectInputStream stream) throws InvalidObjectException {
        throw new InvalidObjectException("Proxy required");
    }

    private static class SerializationProxy implements Serializable {
        private static final long serialVersionUID = 1L;

        private final byte[] bytes;

        SerializationProxy(final BsonDocument document) {
            BasicOutputBuffer buffer = new BasicOutputBuffer();
            new BsonDocumentCodec().encode(new BsonBinaryWriter(buffer), document, EncoderContext.builder().build());
            this.bytes = new byte[buffer.size()];
            int curPos = 0;
            for (ByteBuf cur : buffer.getByteBuffers()) {
                System.arraycopy(cur.array(), cur.position(), bytes, curPos, cur.limit());
                curPos += cur.position();
            }
        }

        private Object readResolve() {
            return new BsonDocumentCodec().decode(new BsonBinaryReader(ByteBuffer.wrap(bytes)
                                                                                .order(ByteOrder.LITTLE_ENDIAN)),
                                                  DecoderContext.builder().build());
        }
    }
}