/*
 * 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.CursorType;
import com.mongodb.ExplainVerbosity;
import com.mongodb.MongoCommandException;
import com.mongodb.MongoInternalException;
import com.mongodb.MongoNamespace;
import com.mongodb.MongoQueryException;
import com.mongodb.ReadPreference;
import com.mongodb.ServerAddress;
import com.mongodb.async.AsyncBatchCursor;
import com.mongodb.async.SingleResultCallback;
import com.mongodb.binding.AsyncConnectionSource;
import com.mongodb.binding.AsyncReadBinding;
import com.mongodb.binding.AsyncSingleConnectionReadBinding;
import com.mongodb.binding.ConnectionSource;
import com.mongodb.binding.ReadBinding;
import com.mongodb.binding.SingleConnectionReadBinding;
import com.mongodb.client.model.Collation;
import com.mongodb.connection.AsyncConnection;
import com.mongodb.connection.Connection;
import com.mongodb.connection.ConnectionDescription;
import com.mongodb.connection.QueryResult;
import com.mongodb.connection.ServerDescription;
import com.mongodb.operation.OperationHelper.CallableWithConnectionAndSource;
import com.mongodb.operation.OperationHelper.CallableWithSource;
import com.mongodb.session.SessionContext;
import org.bson.BsonBoolean;
import org.bson.BsonDocument;
import org.bson.BsonDocumentReader;
import org.bson.BsonInt32;
import org.bson.BsonInt64;
import org.bson.BsonString;
import org.bson.BsonValue;
import org.bson.codecs.BsonDocumentCodec;
import org.bson.codecs.Decoder;
import org.bson.codecs.DecoderContext;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import static com.mongodb.ReadPreference.primary;
import static com.mongodb.assertions.Assertions.isTrueArgument;
import static com.mongodb.assertions.Assertions.notNull;
import static com.mongodb.connection.ServerType.SHARD_ROUTER;
import static com.mongodb.internal.async.ErrorHandlingResultCallback.errorHandlingCallback;
import static com.mongodb.internal.operation.ServerVersionHelper.serverIsAtLeastVersionThreeDotTwo;
import static com.mongodb.operation.CommandOperationHelper.CommandCreator;
import static com.mongodb.operation.CommandOperationHelper.CommandReadTransformer;
import static com.mongodb.operation.CommandOperationHelper.CommandReadTransformerAsync;
import static com.mongodb.operation.CommandOperationHelper.executeCommandWithConnection;
import static com.mongodb.operation.CommandOperationHelper.executeCommandAsyncWithConnection;
import static com.mongodb.operation.DocumentHelper.putIfNotNullOrEmpty;
import static com.mongodb.operation.OperationHelper.AsyncCallableWithConnectionAndSource;
import static com.mongodb.operation.OperationHelper.LOGGER;
import static com.mongodb.operation.OperationHelper.cursorDocumentToQueryResult;
import static com.mongodb.operation.OperationHelper.releasingCallback;
import static com.mongodb.operation.OperationHelper.validateReadConcernAndCollation;
import static com.mongodb.operation.OperationHelper.withConnection;
import static com.mongodb.operation.OperationHelper.withConnectionSource;
import static com.mongodb.operation.OperationReadConcernHelper.appendReadConcernToCommand;

/**
 * An operation that queries a collection using the provided criteria.
 *
 * @param <T> the operations result type.
 * @since 3.0
 */
@Deprecated
public class FindOperation<T> implements AsyncReadOperation<AsyncBatchCursor<T>>, ReadOperation<BatchCursor<T>> {
    private static final String FIRST_BATCH = "firstBatch";

    private final MongoNamespace namespace;
    private final Decoder<T> decoder;
    private boolean retryReads;
    private BsonDocument filter;
    private int batchSize;
    private int limit;
    private BsonDocument modifiers;
    private BsonDocument projection;
    private long maxTimeMS;
    private long maxAwaitTimeMS;
    private int skip;
    private BsonDocument sort;
    private CursorType cursorType = CursorType.NonTailable;
    private boolean slaveOk;
    private boolean oplogReplay;
    private boolean noCursorTimeout;
    private boolean partial;
    private Collation collation;
    private String comment;
    private BsonDocument hint;
    private BsonDocument max;
    private BsonDocument min;
    private long maxScan;
    private boolean returnKey;
    private boolean showRecordId;
    private boolean snapshot;

    /**
     * Construct a new instance.
     *
     * @param namespace the database and collection namespace for the operation.
     * @param decoder the decoder for the result documents.
     */
    public FindOperation(final MongoNamespace namespace, final Decoder<T> decoder) {
        this.namespace = notNull("namespace", namespace);
        this.decoder = notNull("decoder", decoder);
    }

    /**
     * Gets the namespace.
     *
     * @return the namespace
     */
    public MongoNamespace getNamespace() {
        return namespace;
    }

    /**
     * Gets the decoder used to decode the result documents.
     *
     * @return the decoder
     */
    public Decoder<T> getDecoder() {
        return decoder;
    }

    /**
     * Gets the query filter.
     *
     * @return the query filter
     * @mongodb.driver.manual reference/method/db.collection.find/ Filter
     */
    public BsonDocument getFilter() {
        return filter;
    }

    /**
     * Sets the query filter to apply to the query.
     *
     * @param filter the filter, which may be null.
     * @return this
     * @mongodb.driver.manual reference/method/db.collection.find/ Filter
     */
    public FindOperation<T> filter(final BsonDocument filter) {
        this.filter = filter;
        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, which may be null
     * @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 FindOperation<T> batchSize(final int batchSize) {
        this.batchSize = batchSize;
        return this;
    }

    /**
     * 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 FindOperation<T> limit(final int limit) {
        this.limit = limit;
        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
     */
    public BsonDocument 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 setters instead
     */
    @Deprecated
    public FindOperation<T> modifiers(final BsonDocument 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
     */
    public BsonDocument 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 FindOperation<T> projection(final BsonDocument projection) {
        this.projection = projection;
        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
     */
    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
     */
    public FindOperation<T> 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 FindOperation<T> 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 skip.  The default is 0.
     *
     * @return the number of documents to skip, which may be null
     * @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 FindOperation<T> skip(final int skip) {
        this.skip = skip;
        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
     */
    public BsonDocument 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 FindOperation<T> sort(final BsonDocument sort) {
        this.sort = sort;
        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 FindOperation<T> cursorType(final CursorType cursorType) {
        this.cursorType = notNull("cursorType", cursorType);
        return this;
    }

    /**
     * Returns true if set to allowed to query non-primary replica set members.
     *
     * @return true if set to allowed to query non-primary replica set members.
     * @mongodb.driver.manual ../meta-driver/latest/legacy/mongodb-wire-protocol/#op-query OP_QUERY
     */
    public boolean isSlaveOk() {
        return slaveOk;
    }

    /**
     * Sets if allowed to query non-primary replica set members.
     *
     * @param slaveOk true if allowed to query non-primary replica set members.
     * @return this
     * @mongodb.driver.manual ../meta-driver/latest/legacy/mongodb-wire-protocol/#op-query OP_QUERY
     */
    public FindOperation<T> slaveOk(final boolean slaveOk) {
        this.slaveOk = slaveOk;
        return this;
    }

    /**
     * Internal replication use only.  Driver users should ordinarily not use this.
     *
     * @return oplogReplay
     * @mongodb.driver.manual ../meta-driver/latest/legacy/mongodb-wire-protocol/#op-query OP_QUERY
     */
    public boolean isOplogReplay() {
        return oplogReplay;
    }

    /**
     * Internal replication use only.  Driver users should ordinarily not use this.
     *
     * @param oplogReplay the oplogReplay value
     * @return this
     * @mongodb.driver.manual ../meta-driver/latest/legacy/mongodb-wire-protocol/#op-query OP_QUERY
     */
    public FindOperation<T> oplogReplay(final boolean oplogReplay) {
        this.oplogReplay = oplogReplay;
        return this;
    }

    /**
     * Returns true if cursor timeout has been turned off.
     *
     *
The server normally times out idle cursors after an inactivity period (10 minutes) to prevent excess memory use.

     *
     * @return if cursor timeout has been turned off
     * @mongodb.driver.manual ../meta-driver/latest/legacy/mongodb-wire-protocol/#op-query OP_QUERY
     */
    public boolean isNoCursorTimeout() {
        return noCursorTimeout;
    }

    /**
     * Sets if the cursor timeout should be turned off.
     *
     * @param noCursorTimeout true if the cursor timeout should be turned off.
     * @return this
     * @mongodb.driver.manual ../meta-driver/latest/legacy/mongodb-wire-protocol/#op-query OP_QUERY
     */
    public FindOperation<T> noCursorTimeout(final boolean noCursorTimeout) {
        this.noCursorTimeout = noCursorTimeout;
        return this;
    }

    /**
     * Returns true if can get partial results from a mongos if some shards are down.
     *
     * @return if can get partial results from a mongos if some shards are down
     * @mongodb.driver.manual ../meta-driver/latest/legacy/mongodb-wire-protocol/#op-query OP_QUERY
     */
    public boolean isPartial() {
        return partial;
    }

    /**
     * Sets if partial results from a mongos if some shards are down are allowed
     *
     * @param partial allow partial results from a mongos if some shards are down
     * @return this
     * @mongodb.driver.manual ../meta-driver/latest/legacy/mongodb-wire-protocol/#op-query OP_QUERY
     */
    public FindOperation<T> partial(final boolean partial) {
        this.partial = partial;
        return this;
    }

    /**
     * Returns the collation options
     *
     * @return the collation options
     * @since 3.4
     * @mongodb.server.release 3.4
     */
    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 FindOperation<T> collation(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
     */
    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 FindOperation<T> comment(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
     */
    public BsonDocument 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 FindOperation<T> hint(final BsonDocument 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
     */
    public BsonDocument 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 FindOperation<T> max(final BsonDocument 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
     */
    public BsonDocument 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 FindOperation<T> min(final BsonDocument 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 FindOperation<T> 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 FindOperation<T> 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 FindOperation<T> 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 FindOperation<T> snapshot(final boolean snapshot) {
        this.snapshot = snapshot;
        return this;
    }

    /**
     * Enables retryable reads if a read fails due to a network error.
     *
     * @param retryReads true if reads should be retried
     * @return this
     * @since 3.11
     */
    public FindOperation<T> retryReads(final boolean retryReads) {
        this.retryReads = retryReads;
        return this;
    }

    /**
     * Gets the value for retryable reads. The default is true.
     *
     * @return the retryable reads value
     * @since 3.11
     */
    public boolean getRetryReads() {
        return retryReads;
    }

    @Override
    public BatchCursor<T> execute(final ReadBinding binding) {
        return withConnectionSource(binding, new CallableWithSource<BatchCursor<T>>() {
            @Override
            public BatchCursor<T> call(final ConnectionSource source) {
                Connection connection = source.getConnection();
                if (serverIsAtLeastVersionThreeDotTwo(connection.getDescription())) {
                    try {
                        return executeCommandWithConnection(binding, source, namespace.getDatabaseName(),
                                getCommandCreator(binding.getSessionContext()),
                                CommandResultDocumentCodec.create(decoder, FIRST_BATCH), transformer(), retryReads, connection);
                    } catch (MongoCommandException e) {
                        throw new MongoQueryException(e);
                    }
                } else {
                    try {
                        validateReadConcernAndCollation(connection, binding.getSessionContext().getReadConcern(), collation);
                        QueryResult<T> queryResult = connection.query(namespace,
                                asDocument(connection.getDescription(), binding.getReadPreference()),
                                projection,
                                skip,
                                limit,
                                batchSize,
                                isSlaveOk() || binding.getReadPreference().isSlaveOk(),
                                isTailableCursor(),
                                isAwaitData(),
                                isNoCursorTimeout(),
                                isPartial(),
                                isOplogReplay(),
                                decoder);
                        return new QueryBatchCursor<T>(queryResult, limit, batchSize, getMaxTimeForCursor(), decoder, source, connection);
                    } finally {
                        connection.release();
                    }
                }
            }
        });
    }

    @Override
    public void executeAsync(final AsyncReadBinding binding, final SingleResultCallback<AsyncBatchCursor<T>> callback) {
        withConnection(binding, new AsyncCallableWithConnectionAndSource() {
            @Override
            public void call(final AsyncConnectionSource source, final AsyncConnection connection, final Throwable t) {
                SingleResultCallback<AsyncBatchCursor<T>> errHandlingCallback = errorHandlingCallback(callback, LOGGER);
                if (t != null) {
                    errHandlingCallback.onResult(null, t);
                } else {
                    if (serverIsAtLeastVersionThreeDotTwo(connection.getDescription())) {
                        final SingleResultCallback<AsyncBatchCursor<T>> wrappedCallback =
                                exceptionTransformingCallback(errHandlingCallback);
                        executeCommandAsyncWithConnection(binding, source, namespace.getDatabaseName(),
                                getCommandCreator(binding.getSessionContext()), CommandResultDocumentCodec.create(decoder, FIRST_BATCH),
                                asyncTransformer(), retryReads, connection, wrappedCallback);
                    } else {
                        final SingleResultCallback<AsyncBatchCursor<T>> wrappedCallback =
                                releasingCallback(errHandlingCallback, source, connection);
                        validateReadConcernAndCollation(source, connection, binding.getSessionContext().getReadConcern(), collation,
                                new AsyncCallableWithConnectionAndSource() {
                                    @Override
                                    public void call(final AsyncConnectionSource source, final AsyncConnection connection, final
                                    Throwable t) {
                                        if (t != null) {
                                            wrappedCallback.onResult(null, t);
                                        } else {
                                            connection.queryAsync(namespace, asDocument(connection.getDescription(),
                                                    binding.getReadPreference()), projection, skip, limit, batchSize,
                                                    isSlaveOk() || binding.getReadPreference().isSlaveOk(),
                                                    isTailableCursor(), isAwaitData(), isNoCursorTimeout(), isPartial(), isOplogReplay(),
                                                    decoder, new SingleResultCallback<QueryResult<T>>() {
                                                        @Override
                                                        public void onResult(final QueryResult<T> result, final Throwable t) {
                                                            if (t != null) {
                                                                wrappedCallback.onResult(null, t);
                                                            } else {
                                                                wrappedCallback.onResult(new AsyncQueryBatchCursor<T>(result, limit,
                                                                        batchSize, getMaxTimeForCursor(), decoder, source, connection),
                                                                        null);
                                                            }
                                                        }
                                                    });
                                        }
                                    }
                        });
                    }
                }
            }
        });
    }

    private static <T> SingleResultCallback<T> exceptionTransformingCallback(final SingleResultCallback<T> callback) {
        return new SingleResultCallback<T>() {
            @Override
            public void onResult(final T result, final Throwable t) {
                if (t != null) {
                    if (t instanceof MongoCommandException) {
                        MongoCommandException commandException = (MongoCommandException) t;
                        callback.onResult(result, new MongoQueryException(commandException.getServerAddress(),
                                                                          commandException.getErrorCode(),
                                commandException.getErrorMessage()));
                    } else {
                        callback.onResult(result, t);
                    }
                } else {
                    callback.onResult(result, null);
                }
            }
        };
    }

    /**
     * Gets an operation whose execution explains this operation.
     *
     * @param explainVerbosity the explain verbosity
     * @return a read operation that when executed will explain this operation
     */
    public ReadOperation<BsonDocument> asExplainableOperation(final ExplainVerbosity explainVerbosity) {
        notNull("explainVerbosity", explainVerbosity);
        return new ReadOperation<BsonDocument>() {
            @Override
            public BsonDocument execute(final ReadBinding binding) {
                return withConnection(binding, new CallableWithConnectionAndSource<BsonDocument>() {
                    @Override
                    public BsonDocument call(final ConnectionSource connectionSource, final Connection connection) {
                        ReadBinding singleConnectionBinding = new SingleConnectionReadBinding(binding.getReadPreference(),
                                                                                connectionSource.getServerDescription(),
                                                                                connection);
                        try {
                            if (serverIsAtLeastVersionThreeDotTwo(connection.getDescription())) {
                                try {
                                    return new CommandReadOperation<BsonDocument>(getNamespace().getDatabaseName(),
                                                                                new BsonDocument("explain",
                                                                                getCommand(binding.getSessionContext())),
                                                                                new BsonDocumentCodec()).execute(singleConnectionBinding);
                                } catch (MongoCommandException e) {
                                    throw new MongoQueryException(e);
                                }
                            } else {
                                BatchCursor<BsonDocument> cursor = createExplainableQueryOperation().execute(singleConnectionBinding);
                                try {
                                    return cursor.next().iterator().next();
                                } finally {
                                    cursor.close();
                                }
                            }
                        } finally {
                            singleConnectionBinding.release();
                        }
                    }
                });
            }
        };
    }

    /**
     * Gets an operation whose execution explains this operation.
     *
     * @param explainVerbosity the explain verbosity
     * @return a read operation that when executed will explain this operation
     */
    public AsyncReadOperation<BsonDocument> asExplainableOperationAsync(final ExplainVerbosity explainVerbosity) {
        notNull("explainVerbosity", explainVerbosity);
        return new AsyncReadOperation<BsonDocument>() {
            @Override
            public void executeAsync(final AsyncReadBinding binding, final SingleResultCallback<BsonDocument> callback) {
                withConnection(binding, new AsyncCallableWithConnectionAndSource() {
                    @Override
                    public void call(final AsyncConnectionSource connectionSource, final AsyncConnection connection, final Throwable t) {
                        SingleResultCallback<BsonDocument> errHandlingCallback = errorHandlingCallback(callback, LOGGER);
                        if (t != null) {
                            errHandlingCallback.onResult(null, t);
                        } else {
                            AsyncReadBinding singleConnectionReadBinding =
                            new AsyncSingleConnectionReadBinding(binding.getReadPreference(), connectionSource.getServerDescription(),
                                                                 connection);

                            if (serverIsAtLeastVersionThreeDotTwo(connection.getDescription())) {
                                new CommandReadOperation<BsonDocument>(namespace.getDatabaseName(),
                                                                       new BsonDocument("explain", getCommand(binding.getSessionContext())),
                                                                       new BsonDocumentCodec())
                                .executeAsync(singleConnectionReadBinding,
                                              releasingCallback(exceptionTransformingCallback(errHandlingCallback),
                                                                singleConnectionReadBinding, connectionSource, connection));
                            } else {
                                createExplainableQueryOperation()
                                .executeAsync(singleConnectionReadBinding,
                                              releasingCallback(new ExplainResultCallback(errHandlingCallback),
                                                      singleConnectionReadBinding, connectionSource, connection));
                            }
                        }
                    }
                });
            }
        };
    }

    private FindOperation<BsonDocument> createExplainableQueryOperation() {
        FindOperation<BsonDocument> explainFindOperation = new FindOperation<BsonDocument>(namespace, new BsonDocumentCodec());

        BsonDocument explainModifiers = new BsonDocument();
        if (modifiers != null) {
            explainModifiers.putAll(modifiers);
        }
        explainModifiers.append("$explain", BsonBoolean.TRUE);

        return explainFindOperation.filter(filter)
               .projection(projection)
               .sort(sort)
               .skip(skip)
               .limit(Math.abs(limit) * -1)
               .hint(hint)
               .min(min)
               .max(max)
               .modifiers(explainModifiers);

    }

    private BsonDocument asDocument(final ConnectionDescription connectionDescription, final ReadPreference readPreference) {
        BsonDocument document = new BsonDocument();

        if (modifiers != null) {
            document.putAll(modifiers);
        }
        if (sort != null) {
            document.put("$orderby", sort);
        }
        if (maxTimeMS > 0) {
            document.put("$maxTimeMS", new BsonInt64(maxTimeMS));
        }
        if (connectionDescription.getServerType() == SHARD_ROUTER && !readPreference.equals(primary())) {
            document.put("$readPreference", readPreference.toDocument());
        }
        if (comment != null) {
            document.put("$comment", new BsonString(comment));
        }
        if (hint != null) {
            document.put("$hint", hint);
        }
        if (max != null) {
            document.put("$max", max);
        }
        if (min != null) {
            document.put("$min", min);
        }
        if (maxScan > 0) {
            document.put("$maxScan", new BsonInt64(maxScan));
        }
        if (returnKey) {
            document.put("$returnKey", BsonBoolean.TRUE);
        }
        if (showRecordId) {
            document.put("$showDiskLoc", BsonBoolean.TRUE);
        }
        if (snapshot) {
            document.put("$snapshot", BsonBoolean.TRUE);
        }

        if (document.isEmpty()) {
            document = filter != null ? filter : new BsonDocument();
        } else if (filter != null) {
            document.put("$query", filter);
        } else if (!document.containsKey("$query")) {
            document.put("$query", new BsonDocument());
        }

        return document;
    }

    private static final Map<String, String> META_OPERATOR_TO_COMMAND_FIELD_MAP = new HashMap<String, String>();

    static {
        META_OPERATOR_TO_COMMAND_FIELD_MAP.put("$query", "filter");
        META_OPERATOR_TO_COMMAND_FIELD_MAP.put("$orderby", "sort");
        META_OPERATOR_TO_COMMAND_FIELD_MAP.put("$hint", "hint");
        META_OPERATOR_TO_COMMAND_FIELD_MAP.put("$comment", "comment");
        META_OPERATOR_TO_COMMAND_FIELD_MAP.put("$maxScan", "maxScan");
        META_OPERATOR_TO_COMMAND_FIELD_MAP.put("$maxTimeMS", "maxTimeMS");
        META_OPERATOR_TO_COMMAND_FIELD_MAP.put("$max", "max");
        META_OPERATOR_TO_COMMAND_FIELD_MAP.put("$min", "min");
        META_OPERATOR_TO_COMMAND_FIELD_MAP.put("$returnKey", "returnKey");
        META_OPERATOR_TO_COMMAND_FIELD_MAP.put("$showDiskLoc", "showRecordId");
        META_OPERATOR_TO_COMMAND_FIELD_MAP.put("$snapshot", "snapshot");
    }

    private BsonDocument getCommand(final SessionContext sessionContext) {
        BsonDocument commandDocument = new BsonDocument("find", new BsonString(namespace.getCollectionName()));

        appendReadConcernToCommand(sessionContext, commandDocument);

        if (modifiers != null) {
            for (Map.Entry<String, BsonValue> cur : modifiers.entrySet()) {
                String commandFieldName = META_OPERATOR_TO_COMMAND_FIELD_MAP.get(cur.getKey());
                if (commandFieldName != null) {
                    commandDocument.append(commandFieldName, cur.getValue());
                }
            }
        }
        putIfNotNullOrEmpty(commandDocument, "filter", filter);
        putIfNotNullOrEmpty(commandDocument, "sort", sort);
        putIfNotNullOrEmpty(commandDocument, "projection", projection);
        if (skip > 0) {
            commandDocument.put("skip", new BsonInt32(skip));
        }
        if (limit != 0) {
            commandDocument.put("limit", new BsonInt32(Math.abs(limit)));
        }
        if (limit >= 0) {
            if (batchSize < 0 && Math.abs(batchSize) < limit) {
                commandDocument.put("limit", new BsonInt32(Math.abs(batchSize)));
            } else if (batchSize != 0) {
                commandDocument.put("batchSize", new BsonInt32(Math.abs(batchSize)));
            }
        }
        if (limit < 0 || batchSize < 0) {
            commandDocument.put("singleBatch", BsonBoolean.TRUE);
        }
        if (maxTimeMS > 0) {
            commandDocument.put("maxTimeMS", new BsonInt64(maxTimeMS));
        }
        if (isTailableCursor()) {
            commandDocument.put("tailable", BsonBoolean.TRUE);
        }
        if (isAwaitData()) {
            commandDocument.put("awaitData", BsonBoolean.TRUE);
        }
        if (oplogReplay) {
            commandDocument.put("oplogReplay", BsonBoolean.TRUE);
        }
        if (noCursorTimeout) {
            commandDocument.put("noCursorTimeout", BsonBoolean.TRUE);
        }
        if (partial) {
            commandDocument.put("allowPartialResults", BsonBoolean.TRUE);
        }
        if (collation != null) {
            commandDocument.put("collation", collation.asDocument());
        }
        if (comment != null) {
            commandDocument.put("comment", new BsonString(comment));
        }
        if (hint != null) {
            commandDocument.put("hint", hint);
        }
        if (max != null) {
            commandDocument.put("max", max);
        }
        if (min != null) {
            commandDocument.put("min", min);
        }
        if (maxScan > 0) {
            commandDocument.put("maxScan", new BsonInt64(maxScan));
        }
        if (returnKey) {
            commandDocument.put("returnKey", BsonBoolean.TRUE);
        }
        if (showRecordId) {
            commandDocument.put("showRecordId", BsonBoolean.TRUE);
        }
        if (snapshot) {
            commandDocument.put("snapshot", BsonBoolean.TRUE);
        }
        return commandDocument;
    }

    private BsonDocument wrapInExplainIfNecessary(final BsonDocument commandDocument) {
        if (isExplain()) {
            return new BsonDocument("explain", commandDocument);
        } else {
            return commandDocument;
        }
    }

    private CommandCreator getCommandCreator(final SessionContext sessionContext) {
        return new CommandOperationHelper.CommandCreator() {
            @Override
            public BsonDocument create(final ServerDescription serverDescription, final ConnectionDescription connectionDescription) {
                validateReadConcernAndCollation(connectionDescription, sessionContext.getReadConcern(), collation);
                return wrapInExplainIfNecessary(getCommand(sessionContext));
            }
        };
    }

    private boolean isExplain() {
        return modifiers != null && modifiers.get("$explain", BsonBoolean.FALSE).equals(BsonBoolean.TRUE);
    }

    private boolean isTailableCursor() {
        return cursorType.isTailable();
    }

    private boolean isAwaitData() {
        return cursorType == CursorType.TailableAwait;
    }

    private CommandReadTransformer<BsonDocument, BatchCursor<T>> transformer() {
        return new CommandReadTransformer<BsonDocument, BatchCursor<T>>() {
            @Override
            public BatchCursor<T> apply(final BsonDocument result, final ConnectionSource source, final Connection connection) {
                QueryResult<T> queryResult = documentToQueryResult(result, connection.getDescription().getServerAddress());
                return new QueryBatchCursor<T>(queryResult, limit, batchSize, getMaxTimeForCursor(), decoder, source, connection);
            }
        };
    }

    private long getMaxTimeForCursor() {
        return cursorType == CursorType.TailableAwait ? maxAwaitTimeMS : 0;
    }

    private CommandReadTransformerAsync<BsonDocument, AsyncBatchCursor<T>> asyncTransformer() {
        return new CommandReadTransformerAsync<BsonDocument, AsyncBatchCursor<T>>() {
            @Override
            public AsyncBatchCursor<T> apply(final BsonDocument result, final AsyncConnectionSource source,
                                             final AsyncConnection connection) {
                QueryResult<T> queryResult = documentToQueryResult(result, connection.getDescription().getServerAddress());
                return new AsyncQueryBatchCursor<T>(queryResult, limit, batchSize, getMaxTimeForCursor(), decoder, source, connection);
            }
        };
    }

    private QueryResult<T> documentToQueryResult(final BsonDocument result, final ServerAddress serverAddress) {
        QueryResult<T> queryResult;
        if (isExplain()) {
            T decodedDocument = decoder.decode(new BsonDocumentReader(result), DecoderContext.builder().build());
            queryResult = new QueryResult<T>(getNamespace(), Collections.singletonList(decodedDocument), 0, serverAddress);
        } else {
            queryResult = cursorDocumentToQueryResult(result.getDocument("cursor"), serverAddress);
        }
        return queryResult;
    }

    private static class ExplainResultCallback implements SingleResultCallback<AsyncBatchCursor<BsonDocument>> {
        private final SingleResultCallback<BsonDocument> callback;

        ExplainResultCallback(final SingleResultCallback<BsonDocument> callback) {
            this.callback = callback;
        }

        @Override
        public void onResult(final AsyncBatchCursor<BsonDocument> cursor, final Throwable t) {
            if (t != null) {
                callback.onResult(null, t);
            } else {
                cursor.next(new SingleResultCallback<List<BsonDocument>>() {
                    @Override
                    public void onResult(final List<BsonDocument> result, final Throwable t) {
                        cursor.close();
                        if (t != null) {
                            callback.onResult(null, t);
                        } else if (result == null || result.size() == 0) {
                            callback.onResult(null, new MongoInternalException("Expected explain result"));
                        } else {
                            callback.onResult(result.get(0), null);
                        }
                    }
                });
            }
        }
    }
}


/*
 * 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;

import com.mongodb.lang.Nullable;
import org.bson.BSONObject;

import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import static java.util.Collections.synchronizedMap;

/**
 * This class enables to map simple Class fields to a BSON object fields
 * @deprecated Replaced by {@link org.bson.codecs.pojo.PojoCodecProvider}
 */
@SuppressWarnings({"unchecked", "rawtypes"})
@Deprecated
public abstract class ReflectionDBObject implements DBObject {

    @Override
    @Nullable
    public Object get(final String key) {
        return getWrapper().get(this, key);
    }

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

    @SuppressWarnings("deprecation")
    @Override
    public boolean containsKey(final String key) {
        return containsField(key);
    }

    @Override
    public boolean containsField(final String fieldName) {
        return getWrapper().containsKey(fieldName);
    }

    @Override
    public Object put(final String key, final Object v) {
        return getWrapper().set(this, key, v);
    }

    @SuppressWarnings("unchecked")
    @Override
    public void putAll(final Map m) {
        for (final Map.Entry entry : (Set<Map.Entry>) m.entrySet()) {
            put(entry.getKey().toString(), entry.getValue());
        }
    }

    @Override
    public void putAll(final BSONObject o) {
        for (final String k : o.keySet()) {
            put(k, o.get(k));
        }
    }

    /**
     * Gets the _id
     *
     * @return the _id of this document
     */
    public Object get_id() {
        return _id;
    }

    /**
     * Sets the _id
     *
     * @param id the unique identifier for this DBObject
     */
    public void set_id(final Object id) {
        _id = id;
    }

    @Override
    public boolean isPartialObject() {
        return false;
    }

    @SuppressWarnings("unchecked")
    @Override
    public Map toMap() {
        Map m = new HashMap();
        Iterator i = this.keySet().iterator();
        while (i.hasNext()) {
            Object s = i.next();
            m.put(s, this.get(s + ""));
        }
        return m;
    }

    /**
     * ReflectionDBObjects can't be partial. This operation is not supported.
     *
     * @throws RuntimeException ReflectionDBObjects can't be partial
     */
    @Override
    public void markAsPartialObject() {
        throw new RuntimeException("ReflectionDBObjects can't be partial");
    }

    /**
     * This operation is not supported.
     *
     * @param key The name of the field to remove
     * @return The value removed from this object
     * @throws UnsupportedOperationException can't remove from a ReflectionDBObject
     */
    @Override
    public Object removeField(final String key) {
        throw new UnsupportedOperationException("can't remove from a ReflectionDBObject");
    }

    JavaWrapper getWrapper() {
        if (_wrapper != null) {
            return _wrapper;
        }

        _wrapper = getWrapper(this.getClass());
        return _wrapper;
    }

    //CHECKSTYLE:OFF
    JavaWrapper _wrapper;
    Object _id;
    //CHECKSTYLE:ON

    /**
     * Represents a wrapper around the DBObject to interface with the Class fields
     */
    public static class JavaWrapper {
        JavaWrapper(final Class c) {
            clazz = c;
            name = c.getName();

            fields = new TreeMap<String, FieldInfo>();
            for (final Method m : c.getMethods()) {
                if (!(m.getName().startsWith("get") || m.getName().startsWith("set"))) {
                    continue;
                }

                String name = m.getName().substring(3);
                if (name.length() == 0 || IGNORE_FIELDS.contains(name)) {
                    continue;
                }

                Class type = m.getName().startsWith("get") ? m.getReturnType() : m.getParameterTypes()[0];

                FieldInfo fi = fields.get(name);
                if (fi == null) {
                    fi = new FieldInfo(name, type);
                    fields.put(name, fi);
                }

                if (m.getName().startsWith("get")) {
                    fi.getter = m;
                } else {
                    fi.setter = m;
                }
            }

            Set<String> names = new HashSet<String>(fields.keySet());
            for (final String name : names) {
                if (!fields.get(name).ok()) {
                    fields.remove(name);
                }
            }

            keys = Collections.unmodifiableSet(fields.keySet());
        }

        /**
         * Gets all the fields on this object.
         *
         * @return a Set of all the field names.
         */
        public Set<String> keySet() {
            return keys;
        }

        /**
         * Whether the document this represents contains the given field.
         *
         * @param key a field name
         * @return true if the key exists
         * @deprecated
         */
        @Deprecated
        public boolean containsKey(final String key) {
            return keys.contains(key);
        }

        /**
         * Gets the value for the given field from the given document.
         *
         * @param document  a ReflectionDBObject representing a MongoDB document
         * @param fieldName the name of the field to get the value for
         * @return the value for the given field name
         */
        @Nullable
        public Object get(final ReflectionDBObject document, final String fieldName) {
            FieldInfo i = fields.get(fieldName);
            if (i == null) {
                return null;
            }
            try {
                return i.getter.invoke(document);
            } catch (Exception e) {
                throw new RuntimeException("could not invoke getter for [" + fieldName + "] on [" + this.name + "]", e);
            }
        }

        /**
         * Adds or sets the given field to the given value on the document.
         *
         * @param document  a ReflectionDBObject representing a MongoDB document
         * @param fieldName the name of the field to get the value for
         * @param value     the value to set the field to
         * @return the result of setting this value
         */
        public Object set(final ReflectionDBObject document, final String fieldName, final Object value) {
            FieldInfo i = fields.get(fieldName);
            if (i == null) {
                throw new IllegalArgumentException("no field [" + fieldName + "] on [" + this.name + "]");
            }
            try {
                return i.setter.invoke(document, value);
            } catch (Exception e) {
                throw new RuntimeException("could not invoke setter for [" + fieldName + "] on [" + this.name + "]", e);
            }
        }

        @Nullable
        Class<? extends DBObject> getInternalClass(final List<String> path) {
            String cur = path.get(0);

            FieldInfo fi = fields.get(cur);
            if (fi == null) {
                return null;
            }

            if (path.size() == 1) {
                return fi.clazz;
            }

            JavaWrapper w = getWrapperIfReflectionObject(fi.clazz);
            if (w == null) {
                return null;
            }
            return w.getInternalClass(path.subList(1, path.size()));
        }

        //CHECKSTYLE:OFF
        final Class clazz;
        final String name;
        final Map<String, FieldInfo> fields;
        final Set<String> keys;
        //CHECKSTYLE:ON
    }

    static class FieldInfo {
        FieldInfo(final String name, final Class<? extends DBObject> clazz) {
            this.name = name;
            this.clazz = clazz;
        }

        boolean ok() {
            return getter != null && setter != null;
        }

        //CHECKSTYLE:OFF
        final String name;
        final Class<? extends DBObject> clazz;
        Method getter;
        Method setter;
        //CHECKSTYLE:ON
    }

    /**
     * Returns the wrapper if this object can be assigned from this class.
     *
     * @param c the class to be wrapped
     * @return the wrapper
     */
    @Nullable
    public static JavaWrapper getWrapperIfReflectionObject(final Class c) {
        if (ReflectionDBObject.class.isAssignableFrom(c)) {
            return getWrapper(c);
        }
        return null;
    }

    /**
     * Returns an existing Wrapper instance associated with a class, or creates a new one.
     *
     * @param c the class to be wrapped
     * @return the wrapped
     */
    public static JavaWrapper getWrapper(final Class c) {
        JavaWrapper w = _wrappers.get(c);
        if (w == null) {
            w = new JavaWrapper(c);
            _wrappers.put(c, w);
        }
        return w;
    }

    private static final Map<Class, JavaWrapper> _wrappers = synchronizedMap(new HashMap<Class, JavaWrapper>());
    private static final Set<String> IGNORE_FIELDS = new HashSet<String>();

    static {
        IGNORE_FIELDS.add("Int");
    }

}


/*
 * 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.lang.Nullable;
import org.bson.conversions.Bson;

import java.util.concurrent.TimeUnit;

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

/**
 * The options for a count operation.
 *
 * @since 3.0
 * @mongodb.driver.manual reference/command/count/ Count
 */
public class CountOptions {
    private Bson hint;
    private String hintString;
    private int limit;
    private int skip;
    private long maxTimeMS;
    private Collation collation;

    /**
     * Gets the hint to apply.
     *
     * @return the hint, which should describe an existing
     */
    @Nullable
    public Bson getHint() {
        return hint;
    }

    /**
     * Gets the hint string to apply.
     *
     * @return the hint string, which should be the name of an existing index
     * @deprecated Prefer {@link #getHint()}
     */
    @Deprecated
    @Nullable
    public String getHintString() {
        return hintString;
    }

    /**
     * Sets the hint to apply.
     *
     * @param hint a document describing the index which should be used for this operation.
     * @return this
     */
    public CountOptions hint(@Nullable final Bson hint) {
        this.hint = hint;
        return this;
    }

    /**
     * Sets the hint to apply.
     *
     * @param hint the name of the index which should be used for the operation
     * @return this
     * @deprecated Prefer {@link #hint(Bson)}
     */
    @Deprecated
    public CountOptions hintString(@Nullable final String hint) {
        this.hintString = hint;
        return this;
    }

    /**
     * Gets the limit to apply.  The default is 0, which means there is no limit.
     *
     * @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
     * @return this
     * @mongodb.driver.manual reference/method/cursor.limit/#cursor.limit Limit
     */
    public CountOptions 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 CountOptions 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
     */
    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
     */
    public CountOptions maxTime(final long maxTime, final TimeUnit timeUnit) {
        notNull("timeUnit", timeUnit);
        this.maxTimeMS = TimeUnit.MILLISECONDS.convert(maxTime, timeUnit);
        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 CountOptions collation(@Nullable final Collation collation) {
        this.collation = collation;
        return this;
    }

    @Override
    public String toString() {
        return "CountOptions{"
                + "hint=" + hint
                + ", hintString='" + hintString + '\''
                + ", limit=" + limit
                + ", skip=" + skip
                + ", maxTimeMS=" + maxTimeMS
                + ", collation=" + collation
                + '}';
    }
}


/*
 * 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.assertions.Assertions;
import org.bson.internal.UuidHelper;

import java.util.Arrays;
import java.util.UUID;

/**
 * A representation of the BSON Binary type.  Note that for performance reasons instances of this class are not immutable,
 * so care should be taken to only modify the underlying byte array if you know what you're doing, or else make a defensive copy.
 *
 * @since 3.0
 */
public class BsonBinary extends BsonValue {

    private final byte type;
    private final byte[] data;

    /**
     * Construct a new instance with the given data and the default sub-type
     *
     * @param data the data
     *
     * @see org.bson.BsonBinarySubType#BINARY
     */
    public BsonBinary(final byte[] data) {
        this(BsonBinarySubType.BINARY, data);
    }

    /**
     * Construct a new instance with the given data and binary sub type.
     *
     * @param data the data
     * @param type the binary sub type
     *
     * @see org.bson.BsonBinarySubType#BINARY
     */
    public BsonBinary(final BsonBinarySubType type, final byte[] data) {
        if (type == null) {
            throw new IllegalArgumentException("type may not be null");
        }
        if (data == null) {
            throw new IllegalArgumentException("data may not be null");
        }
        this.type = type.getValue();
        this.data = data;
    }

    /**
     * Construct a new instance with the given data and binary sub type.
     *
     * @param data the data
     * @param type the binary sub type
     *
     * @see org.bson.BsonBinarySubType#BINARY
     */
    public BsonBinary(final byte type, final byte[] data) {
        if (data == null) {
            throw new IllegalArgumentException("data may not be null");
        }
        this.type = type;
        this.data = data;
    }

    /**
     * Construct a Type 4 BsonBinary from the given UUID.
     *
     * @param uuid the UUID
     * @since 3.9
     */
    public BsonBinary(final UUID uuid) {
        this(uuid, UuidRepresentation.STANDARD);
    }

    /**
     * Construct a new instance from the given UUID and UuidRepresentation
     *
     * @param uuid the UUID
     * @param uuidRepresentation the UUID representation
     * @since 3.9
     */
    public BsonBinary(final UUID uuid, final UuidRepresentation uuidRepresentation) {
        if (uuid == null) {
            throw new IllegalArgumentException("uuid may not be null");
        }
        if (uuidRepresentation == null) {
            throw new IllegalArgumentException("uuidRepresentation may not be null");
        }
        this.data = UuidHelper.encodeUuidToBinary(uuid, uuidRepresentation);
        this.type = uuidRepresentation == UuidRepresentation.STANDARD
                ? BsonBinarySubType.UUID_STANDARD.getValue()
                : BsonBinarySubType.UUID_LEGACY.getValue();
    }

    /**
     * Returns the binary as a UUID. The binary type must be 4.
     *
     * @return the uuid
     * @since 3.9
     */
    public UUID asUuid() {
        if (!BsonBinarySubType.isUuid(type)) {
            throw new BsonInvalidOperationException("type must be a UUID subtype.");
        }

        if (type != BsonBinarySubType.UUID_STANDARD.getValue()) {
            throw new BsonInvalidOperationException("uuidRepresentation must be set to return the correct UUID.");
        }

        return UuidHelper.decodeBinaryToUuid(this.data.clone(), this.type, UuidRepresentation.STANDARD);
    }

    /**
     * Returns the binary as a UUID.
     *
     * @param uuidRepresentation the UUID representation
     * @return the uuid
     * @since 3.9
     */
    public UUID asUuid(final UuidRepresentation uuidRepresentation) {
        Assertions.notNull("uuidRepresentation", uuidRepresentation);

        final byte uuidType = uuidRepresentation == UuidRepresentation.STANDARD
                ? BsonBinarySubType.UUID_STANDARD.getValue()
                : BsonBinarySubType.UUID_LEGACY.getValue();

        if (type != uuidType) {
            throw new BsonInvalidOperationException("uuidRepresentation does not match current uuidRepresentation.");
        }

        return UuidHelper.decodeBinaryToUuid(data.clone(), type, uuidRepresentation);
    }

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

    /**
     * Gets the type of this Binary.
     *
     * @return the type
     */
    public byte getType() {
        return type;
    }

    /**
     * Gets the data of this Binary.
     *
     * @return the data
     */
    public byte[] getData() {
        return data;
    }

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

        BsonBinary that = (BsonBinary) o;

        if (!Arrays.equals(data, that.data)) {
            return false;
        }
        if (type != that.type) {
            return false;
        }

        return true;
    }

    @Override
    public int hashCode() {
        int result = (int) type;
        result = 31 * result + Arrays.hashCode(data);
        return result;
    }

    @Override
    public String toString() {
        return "BsonBinary{"
               + "type=" + type
               + ", data=" + Arrays.toString(data)
               + '}';
    }

    static BsonBinary clone(final BsonBinary from) {
        return new BsonBinary(from.type, from.data.clone());
    }
}


/*
 * 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.Codec;
import org.bson.codecs.Decoder;
import org.bson.codecs.DecoderContext;
import org.bson.codecs.EncoderContext;
import org.bson.codecs.RawBsonDocumentCodec;
import org.bson.io.BasicOutputBuffer;
import org.bson.io.ByteBufferBsonInput;
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.Map;
import java.util.NoSuchElementException;
import java.util.Set;

import static org.bson.assertions.Assertions.isTrueArgument;
import static org.bson.assertions.Assertions.notNull;

/**
 * An immutable BSON document that is represented using only the raw bytes.
 *
 * @since 3.0
 */
public final class RawBsonDocument extends BsonDocument {
    private static final long serialVersionUID = 1L;
    private static final int MIN_BSON_DOCUMENT_SIZE = 5;

    private final byte[] bytes;
    private final int offset;
    private final int length;

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

    /**
     * Constructs a new instance with the given byte array.  Note that it does not make a copy of the array, so do not modify it after
     * passing it to this constructor.
     *
     * @param bytes the bytes representing a BSON document.  Note that the byte array is NOT copied, so care must be taken not to modify it
     *              after passing it to this construction, unless of course that is your intention.
     */
    public RawBsonDocument(final byte[] bytes) {
        this(notNull("bytes", bytes), 0, bytes.length);
    }

    /**
     * Constructs a new instance with the given byte array, offset, and length. Note that it does not make a copy of the array, so do not
     * modify it after passing it to this constructor.
     *
     * @param bytes the bytes representing a BSON document.  Note that the byte array is NOT copied, so care must be taken not to modify it
     *              after passing it to this construction, unless of course that is your intention.
     * @param offset the offset into the byte array
     * @param length the length of the subarray to use
     * @since 3.3
     */
    public RawBsonDocument(final byte[] bytes, final int offset, final int length) {
        notNull("bytes", bytes);
        isTrueArgument("offset >= 0", offset >= 0);
        isTrueArgument("offset < bytes.length", offset < bytes.length);
        isTrueArgument("length <= bytes.length - offset", length <= bytes.length - offset);
        isTrueArgument("length >= 5", length >= MIN_BSON_DOCUMENT_SIZE);
        this.bytes = bytes;
        this.offset = offset;
        this.length = length;
    }

    /**
     * Construct a new instance from the given document and codec for the document type.
     *
     * @param document the document to transform
     * @param codec    the codec to facilitate the transformation
     * @param <T>      the BSON type that the codec encodes/decodes
     */
    public <T> RawBsonDocument(final T document, final Codec<T> codec) {
        notNull("document", document);
        notNull("codec", codec);
        BasicOutputBuffer buffer = new BasicOutputBuffer();
        BsonBinaryWriter writer = new BsonBinaryWriter(buffer);
        try {
            codec.encode(writer, document, EncoderContext.builder().build());
            this.bytes = buffer.getInternalBuffer();
            this.offset = 0;
            this.length = buffer.getPosition();
        } finally {
            writer.close();
        }
    }

    /**
     * Returns a {@code ByteBuf} that wraps the byte array, with the proper byte order.  Any changes made to the returned will be reflected
     * in the underlying byte array owned by this instance.
     *
     * @return a byte buffer that wraps the byte array owned by this instance.
     */
    public ByteBuf getByteBuffer() {
        ByteBuffer buffer = ByteBuffer.wrap(bytes, offset, length);
        buffer.order(ByteOrder.LITTLE_ENDIAN);
        return new ByteBufNIO(buffer);
    }

    /**
     * Decode this into a document.
     *
     * @param codec the codec to facilitate the transformation
     * @param <T>   the BSON type that the codec encodes/decodes
     * @return the decoded document
     */
    public <T> T decode(final Codec<T> codec) {
        return decode((Decoder<T>) codec);
    }

    /**
     * Decode this into a document.
     *
     * @param decoder the decoder to facilitate the transformation
     * @param <T>   the BSON type that the codec encodes/decodes
     * @return the decoded document
     * @since 3.6
     */
    public <T> T decode(final Decoder<T> decoder) {
        BsonBinaryReader reader = createReader();
        try {
            return decoder.decode(reader, DecoderContext.builder().build());
        } finally {
            reader.close();
        }
    }

    @Override
    public void clear() {
        throw new UnsupportedOperationException("RawBsonDocument instances are immutable");
    }

    @Override
    public BsonValue put(final String key, final BsonValue value) {
        throw new UnsupportedOperationException("RawBsonDocument instances are immutable");
    }

    @Override
    public BsonDocument append(final String key, final BsonValue value) {
        throw new UnsupportedOperationException("RawBsonDocument instances are immutable");
    }

    @Override
    public void putAll(final Map<? extends String, ? extends BsonValue> m) {
        throw new UnsupportedOperationException("RawBsonDocument instances are immutable");
    }

    @Override
    public BsonValue remove(final Object key) {
        throw new UnsupportedOperationException("RawBsonDocument instances are immutable");
    }

    @Override
    public boolean isEmpty() {
        BsonBinaryReader bsonReader = createReader();
        try {
            bsonReader.readStartDocument();
            if (bsonReader.readBsonType() != BsonType.END_OF_DOCUMENT) {
                return false;
            }
            bsonReader.readEndDocument();
        } finally {
            bsonReader.close();
        }

        return true;
    }

    @Override
    public int size() {
        int size = 0;
        BsonBinaryReader bsonReader = createReader();
        try {
            bsonReader.readStartDocument();
            while (bsonReader.readBsonType() != BsonType.END_OF_DOCUMENT) {
                size++;
                bsonReader.readName();
                bsonReader.skipValue();
            }
            bsonReader.readEndDocument();
        } finally {
            bsonReader.close();
        }

        return size;
    }

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

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

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

    @Override
    public String getFirstKey() {
        BsonBinaryReader bsonReader = createReader();
        try {
            bsonReader.readStartDocument();
            try {
                return bsonReader.readName();
            } catch (BsonInvalidOperationException e) {
                throw new NoSuchElementException();
            }
        } finally {
            bsonReader.close();
        }
    }

    @Override
    public boolean containsKey(final Object key) {
        if (key == null) {
            throw new IllegalArgumentException("key can not be null");
        }

        BsonBinaryReader bsonReader = createReader();
        try {
            bsonReader.readStartDocument();
            while (bsonReader.readBsonType() != BsonType.END_OF_DOCUMENT) {
                if (bsonReader.readName().equals(key)) {
                    return true;
                }
                bsonReader.skipValue();
            }
            bsonReader.readEndDocument();
        } finally {
            bsonReader.close();
        }

        return false;
    }

    @Override
    public boolean containsValue(final Object value) {
        BsonBinaryReader bsonReader = createReader();
        try {
            bsonReader.readStartDocument();
            while (bsonReader.readBsonType() != BsonType.END_OF_DOCUMENT) {
                bsonReader.skipName();
                if (RawBsonValueHelper.decode(bytes, bsonReader).equals(value)) {
                    return true;
                }
            }
            bsonReader.readEndDocument();
        } finally {
            bsonReader.close();
        }

        return false;
    }

    @Override
    public BsonValue get(final Object key) {
        notNull("key", key);

        BsonBinaryReader bsonReader = createReader();
        try {
            bsonReader.readStartDocument();
            while (bsonReader.readBsonType() != BsonType.END_OF_DOCUMENT) {
                if (bsonReader.readName().equals(key)) {
                    return RawBsonValueHelper.decode(bytes, bsonReader);
                }
                bsonReader.skipValue();
            }
            bsonReader.readEndDocument();
        } finally {
            bsonReader.close();
        }

        return null;
    }

    @Override
    @SuppressWarnings("deprecation")
    public String toJson() {
        return toJson(new JsonWriterSettings());
    }

    @Override
    public String toJson(final JsonWriterSettings settings) {
        StringWriter writer = new StringWriter();
        new RawBsonDocumentCodec().encode(new JsonWriter(writer, settings), this, EncoderContext.builder().build());
        return writer.toString();
    }

    @Override
    public boolean equals(final Object o) {
        return toBsonDocument().equals(o);
    }

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

    @Override
    public BsonDocument clone() {
        return new RawBsonDocument(bytes.clone(), offset, length);
    }

    private BsonBinaryReader createReader() {
        return new BsonBinaryReader(new ByteBufferBsonInput(getByteBuffer()));
    }

    private BsonDocument toBsonDocument() {
        BsonBinaryReader bsonReader = createReader();
        try {
            return new BsonDocumentCodec().decode(bsonReader, DecoderContext.builder().build());
        } finally {
            bsonReader.close();
        }
    }

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

    // 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 byte[] bytes, final int offset, final int length) {
            if (bytes.length == length) {
                this.bytes = bytes;
            } else {
                this.bytes = new byte[length];
                System.arraycopy(bytes, offset, this.bytes, 0, length);
            }
        }

        private Object readResolve() {
            return new RawBsonDocument(bytes);
        }
    }
}


/*
 * 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.util;

import org.bson.BSONCallback;

/**
 * Helper methods for JSON serialization and de-serialization
 *
 * @see org.bson.json.JsonReader
 * @see org.bson.json.JsonWriter
 * @see com.mongodb.BasicDBObject#toJson()
 * @see com.mongodb.BasicDBObject#parse(String)
 *
 * @deprecated This class has been superseded by to toJson and parse methods on BasicDBObject
 */
@Deprecated
@SuppressWarnings("deprecation")
public class JSON {

    /**
     *
Serializes an object into its JSON form.

     *
     *
This method delegates serialization to {@code JSONSerializers.getLegacy}

     *
     * @param object object to serialize
     * @return String containing JSON form of the object
     * @see JSONSerializers#getLegacy()
     */
    public static String serialize(final Object object) {
        StringBuilder buf = new StringBuilder();
        serialize(object, buf);
        return buf.toString();
    }

    /**
     *
Serializes an object into its JSON form.

     *
     *
This method delegates serialization to {@code JSONSerializers.getLegacy}

     *
     * @param object   object to serialize
     * @param buf StringBuilder containing the JSON representation under construction
     * @see JSONSerializers#getLegacy()
     */
    public static void serialize(final Object object, final StringBuilder buf) {
        JSONSerializers.getLegacy().serialize(object, buf);
    }

    /**
     *
Parses a JSON string and returns a corresponding Java object. The returned value is either a {@link com.mongodb.DBObject DBObject}
     * (if the string is a JSON object or array), or a boxed primitive value according to the following mapping:

     *

     *
{@code java.lang.Boolean} for {@code true} or {@code false}

     *
{@code java.lang.Integer} for integers between Integer.MIN_VALUE and Integer.MAX_VALUE

     *
{@code java.lang.Long} for integers outside of this range

     *
{@code java.lang.Double} for floating point numbers

     *

     * If the parameter is a string that contains a single-quoted or double-quoted string, it is returned as an unquoted {@code
     * java.lang.String}. Parses a JSON string representing a JSON value
     *
     * @param jsonString the string to parse
     * @return a Java object representing the JSON data
     * @throws JSONParseException if jsonString is not valid JSON
     */
    public static Object parse(final String jsonString) {
        return parse(jsonString, null);
    }

    /**
     * Parses a JSON string and constructs a corresponding Java object by calling the methods of a {@link org.bson.BSONCallback
     * BSONCallback} during parsing. If the callback {@code c} is null, this method is equivalent to {@link JSON#parse(String)
     * parse(String)}.
     *
     * @param s the string to parse
     * @param c the BSONCallback to call during parsing
     * @return a Java object representing the JSON data
     * @throws JSONParseException if s is not valid JSON
     */
    public static Object parse(final String s, final BSONCallback c) {
        if (s == null || (s.trim()).equals("")) {
            return null;
        }

        JSONParser p = new JSONParser(s, c);
        return p.parse();
    }

    static void string(final StringBuilder a, final String s) {
        a.append("\"");
        for (int i = 0; i < s.length(); ++i) {
            char c = s.charAt(i);
            if (c == '\\') {
                a.append("\\\\");
            } else if (c == '"') {
                a.append("\\\"");
            } else if (c == '\n') {
                a.append("\\n");
            } else if (c == '\r') {
                a.append("\\r");
            } else if (c == '\t') {
                a.append("\\t");
            } else if (c == '\b') {
                a.append("\\b");
            } else if (c < 32) {
                continue;
            } else {
                a.append(c);
            }
        }
        a.append("\"");
    }
}

/**
 *
Parser for JSON objects.

 *
 *
Supports all types described at www.json.org, except for numbers with "e" or "E" in them.

 */
class JSONParser {

    final String s;
    int pos = 0;
    final BSONCallback _callback;

    JSONParser(final String s) {
        this(s, null);
    }

    @SuppressWarnings("deprecation")
    JSONParser(final String s, final BSONCallback callback) {
        this.s = s;
        _callback = (callback == null) ? new JSONCallback() : callback;
    }

    /**
     * Parse an unknown type.
     *
     * @return Object the next item
     * @throws JSONParseException if invalid JSON is found
     */
    public Object parse() {
        return parse(null);
    }

    /**
     * Parse an unknown type.
     *
     * @return Object the next item
     * @throws JSONParseException if invalid JSON is found
     */
    protected Object parse(final String name) {
        Object value = null;
        char current = get();

        switch (current) {
            // null
            case 'n':
                read('n');
                read('u');
                read('l');
                read('l');
                value = null;
                break;
            // NaN
            case 'N':
                read('N');
                read('a');
                read('N');
                value = Double.NaN;
                break;
            // true
            case 't':
                read('t');
                read('r');
                read('u');
                read('e');
                value = true;
                break;
            // false
            case 'f':
                read('f');
                read('a');
                read('l');
                read('s');
                read('e');
                value = false;
                break;
            // string
            case '\'':
            case '\"':
                value = parseString(true);
                break;
            // number
            case '0':
            case '1':
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7':
            case '8':
            case '9':
            case '+':
            case '-':
                value = parseNumber();
                break;
            // array
            case '[':
                value = parseArray(name);
                break;
            // object
            case '{':
                value = parseObject(name);
                break;
            default:
                throw new JSONParseException(s, pos);
        }
        return value;
    }

    /**
     * Parses an object for the form {} and { members }.
     *
     * @return DBObject the next object
     * @throws JSONParseException if invalid JSON is found
     */
    public Object parseObject() {
        return parseObject(null);
    }

    /**
     * Parses an object for the form {} and { members }.
     *
     * @return DBObject the next object
     * @throws JSONParseException if invalid JSON is found
     */
    protected Object parseObject(final String name) {
        if (name != null) {
            _callback.objectStart(name);
        } else {
            _callback.objectStart();
        }

        read('{');
        char current = get();
        while (get() != '}') {
            String key = parseString(false);
            read(':');
            Object value = parse(key);
            doCallback(key, value);

            if ((current = get()) == ',') {
                read(',');
            } else {
                break;
            }
        }
        read('}');

        return _callback.objectDone();
    }

    protected void doCallback(final String name, final Object value) {
        if (value == null) {
            _callback.gotNull(name);
        } else if (value instanceof String) {
            _callback.gotString(name, (String) value);
        } else if (value instanceof Boolean) {
            _callback.gotBoolean(name, (Boolean) value);
        } else if (value instanceof Integer) {
            _callback.gotInt(name, (Integer) value);
        } else if (value instanceof Long) {
            _callback.gotLong(name, (Long) value);
        } else if (value instanceof Double) {
            _callback.gotDouble(name, (Double) value);
        }
    }

    /**
     * Read the current character, making sure that it is the expected character. Advances the pointer to the next character.
     *
     * @param ch the character expected
     * @throws JSONParseException if the current character does not match the given character
     */
    public void read(final char ch) {
        if (!check(ch)) {
            throw new JSONParseException(s, pos);
        }
        pos++;
    }

    public char read() {
        if (pos >= s.length()) {
            throw new IllegalStateException("string done");
        }
        return s.charAt(pos++);
    }

    /**
     * Read the current character, making sure that it is a hexidecimal character.
     *
     * @throws JSONParseException if the current character is not a hexidecimal character
     */
    public void readHex() {
        if (pos < s.length()
            && ((s.charAt(pos) >= '0' && s.charAt(pos) <= '9')
                || (s.charAt(pos) >= 'A' && s.charAt(pos) <= 'F')
                || (s.charAt(pos) >= 'a' && s.charAt(pos) <= 'f'))) {
            pos++;
        } else {
            throw new JSONParseException(s, pos);
        }
    }

    /**
     * Checks the current character, making sure that it is the expected character.
     *
     * @param ch the character expected
     * @throws JSONParseException if the current character does not match the given character
     */
    public boolean check(final char ch) {
        return get() == ch;
    }

    /**
     * Advances the position in the string past any whitespace.
     */
    public void skipWS() {
        while (pos < s.length() && Character.isWhitespace(s.charAt(pos))) {
            pos++;
        }
    }

    /**
     * Returns the current character. Returns -1 if there are no more characters.
     *
     * @return the next character
     */
    public char get() {
        skipWS();
        if (pos < s.length()) {
            return s.charAt(pos);
        }
        return (char) -1;
    }

    /**
     * Parses a string.
     *
     * @return the next string.
     * @throws JSONParseException if invalid JSON is found
     */
    public String parseString(final boolean needQuote) {
        char quot = 0;
        if (check('\'')) {
            quot = '\'';
        } else if (check('\"')) {
            quot = '\"';
        } else if (needQuote) {
            throw new JSONParseException(s, pos);
        }

        char current;

        if (quot > 0) {
            read(quot);
        }
        StringBuilder buf = new StringBuilder();
        int start = pos;
        while (pos < s.length()) {
            current = s.charAt(pos);
            if (quot > 0) {
                if (current == quot) {
                    break;
                }
            } else {
                if (current == ':' || current == ' ') {
                    break;
                }
            }

            if (current == '\\') {
                pos++;

                char x = get();

                char special = 0;

                //CHECKSTYLE:OFF
                switch (x) {
                    case 'u':  // decode unicode
                        buf.append(s.substring(start, pos - 1));
                        pos++;
                        int tempPos = pos;

                        readHex();
                        readHex();
                        readHex();
                        readHex();

                        int codePoint = Integer.parseInt(s.substring(tempPos, tempPos + 4), 16);
                        buf.append((char) codePoint);

                        start = pos;
                        continue;
                    case 'n':
                        special = '\n';
                        break;
                    case 'r':
                        special = '\r';
                        break;
                    case 't':
                        special = '\t';
                        break;
                    case 'b':
                        special = '\b';
                        break;
                    case '"':
                        special = '\"';
                        break;
                    case '\\':
                        special = '\\';
                        break;
                    default:
                        break;
                }
                //CHECKSTYLE:ON

                buf.append(s.substring(start, pos - 1));
                if (special != 0) {
                    pos++;
                    buf.append(special);
                }
                start = pos;
                continue;
            }
            pos++;
        }
        buf.append(s.substring(start, pos));
        if (quot > 0) {
            read(quot);
        }
        return buf.toString();
    }

    /**
     * Parses a number.
     *
     * @return the next number (int or double).
     * @throws JSONParseException if invalid JSON is found
     */
    public Number parseNumber() {

        get();
        int start = this.pos;
        boolean isDouble = false;

        if (check('-') || check('+')) {
            pos++;
        }

        outer:
        while (pos < s.length()) {
            switch (s.charAt(pos)) {
                case '0':
                case '1':
                case '2':
                case '3':
                case '4':
                case '5':
                case '6':
                case '7':
                case '8':
                case '9':
                    pos++;
                    break;
                case '.':
                    isDouble = true;
                    parseFraction();
                    break;
                case 'e':
                case 'E':
                    isDouble = true;
                    parseExponent();
                    break;
                default:
                    break outer;
            }
        }

        try {
            if (isDouble) {
                return Double.valueOf(s.substring(start, pos));
            }

            Long val = Long.valueOf(s.substring(start, pos));
            if (val <= Integer.MAX_VALUE && val >= Integer.MIN_VALUE) {
                return val.intValue();
            }
            return val;
        } catch (NumberFormatException e) {
            throw new JSONParseException(s, start, e);
        }
    }

    /**
     * Advances the pointed through .digits.
     */
    public void parseFraction() {
        // get past .
        pos++;

        outer:
        while (pos < s.length()) {
            switch (s.charAt(pos)) {
                case '0':
                case '1':
                case '2':
                case '3':
                case '4':
                case '5':
                case '6':
                case '7':
                case '8':
                case '9':
                    pos++;
                    break;
                case 'e':
                case 'E':
                    parseExponent();
                    break;
                default:
                    break outer;
            }
        }
    }

    /**
     * Advances the pointer through the exponent.
     */
    public void parseExponent() {
        // get past E
        pos++;

        if (check('-') || check('+')) {
            pos++;
        }

        outer:
        while (pos < s.length()) {
            switch (s.charAt(pos)) {
                case '0':
                case '1':
                case '2':
                case '3':
                case '4':
                case '5':
                case '6':
                case '7':
                case '8':
                case '9':
                    pos++;
                    break;
                default:
                    break outer;
            }
        }
    }

    /**
     * Parses the next array.
     *
     * @return the array
     * @throws JSONParseException if invalid JSON is found
     */
    public Object parseArray() {
        return parseArray(null);
    }

    /**
     * Parses the next array.
     *
     * @return the array
     * @throws JSONParseException if invalid JSON is found
     */
    protected Object parseArray(final String name) {
        if (name != null) {
            _callback.arrayStart(name);
        } else {
            _callback.arrayStart();
        }

        read('[');

        int i = 0;
        char current = get();
        while (current != ']') {
            String elemName = String.valueOf(i++);
            Object elem = parse(elemName);
            doCallback(elemName, elem);

            if ((current = get()) == ',') {
                read(',');
            } else if (current == ']') {
                break;
            } else {
                throw new JSONParseException(s, pos);
            }
        }

        read(']');

        return _callback.arrayDone();
    }

}