/*
 * 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 static java.lang.String.format;

/**
 * Base class for any BSON type.
 *
 * @since 3.0
 */
public abstract class BsonValue {
    /**
     * Construct a new instance.  This is package-protected so that the BSON type system is closed.
     */
    BsonValue() {
    }

    /**
     * Gets the BSON type of this value.
     *
     * @return the BSON type, which may not be null (but may be BSONType.NULL)
     */
    public abstract BsonType getBsonType();

    /**
     * Gets this value as a BsonDocument if it is one, otherwise throws exception
     *
     * @return a BsonDocument
     * @throws org.bson.BsonInvalidOperationException if this value is not of the expected type
     */
    public BsonDocument asDocument() {
        throwIfInvalidType(BsonType.DOCUMENT);
        return (BsonDocument) this;
    }

    /**
     * Gets this value as a BsonArray if it is one, otherwise throws exception
     *
     * @return a BsonArray
     * @throws org.bson.BsonInvalidOperationException if this value is not of the expected type
     */
    public BsonArray asArray() {
        throwIfInvalidType(BsonType.ARRAY);
        return (BsonArray) this;
    }

    /**
     * Gets this value as a BsonString if it is one, otherwise throws exception
     *
     * @return a BsonString
     * @throws org.bson.BsonInvalidOperationException if this value is not of the expected type
     */
    public BsonString asString() {
        throwIfInvalidType(BsonType.STRING);
        return (BsonString) this;
    }

    /**
     * Gets this value as a BsonNumber if it is one, otherwise throws exception
     *
     * @return a BsonNumber
     * @throws org.bson.BsonInvalidOperationException if this value is not of the expected type
     */
    public BsonNumber asNumber() {
        if (getBsonType() != BsonType.INT32 && getBsonType() != BsonType.INT64 && getBsonType() != BsonType.DOUBLE) {
            throw new BsonInvalidOperationException(format("Value expected to be of a numerical BSON type is of unexpected type %s",
                                                           getBsonType()));
        }
        return (BsonNumber) this;
    }

    /**
     * Gets this value as a BsonInt32 if it is one, otherwise throws exception
     *
     * @return a BsonInt32
     * @throws org.bson.BsonInvalidOperationException if this value is not of the expected type
     */
    public BsonInt32 asInt32() {
        throwIfInvalidType(BsonType.INT32);
        return (BsonInt32) this;
    }

    /**
     * Gets this value as a BsonInt64 if it is one, otherwise throws exception
     *
     * @return a BsonInt64
     * @throws org.bson.BsonInvalidOperationException if this value is not of the expected type
     */
    public BsonInt64 asInt64() {
        throwIfInvalidType(BsonType.INT64);
        return (BsonInt64) this;
    }

    /**
     * Gets this value as a BsonDecimal128 if it is one, otherwise throws exception
     *
     * @return a BsonDecimal128
     * @throws org.bson.BsonInvalidOperationException if this value is not of the expected type
     * @since 3.4
     */
    public BsonDecimal128 asDecimal128() {
        throwIfInvalidType(BsonType.DECIMAL128);
        return (BsonDecimal128) this;
    }

    /**
     * Gets this value as a BsonDouble if it is one, otherwise throws exception
     *
     * @return a BsonDouble
     * @throws org.bson.BsonInvalidOperationException if this value is not of the expected type
     */
    public BsonDouble asDouble() {
        throwIfInvalidType(BsonType.DOUBLE);
        return (BsonDouble) this;
    }

    /**
     * Gets this value as a BsonBoolean if it is one, otherwise throws exception
     *
     * @return a BsonBoolean
     * @throws org.bson.BsonInvalidOperationException if this value is not of the expected type
     */
    public BsonBoolean asBoolean() {
        throwIfInvalidType(BsonType.BOOLEAN);
        return (BsonBoolean) this;
    }

    /**
     * Gets this value as an BsonObjectId if it is one, otherwise throws exception
     *
     * @return an BsonObjectId
     * @throws org.bson.BsonInvalidOperationException if this value is not of the expected type
     */
    public BsonObjectId asObjectId() {
        throwIfInvalidType(BsonType.OBJECT_ID);
        return (BsonObjectId) this;
    }

    /**
     * Gets this value as a BsonDbPointer if it is one, otherwise throws exception
     *
     * @return an BsonDbPointer
     * @throws org.bson.BsonInvalidOperationException if this value is not of the expected type
     */
    public BsonDbPointer asDBPointer() {
        throwIfInvalidType(BsonType.DB_POINTER);
        return (BsonDbPointer) this;
    }

    /**
     * Gets this value as a BsonTimestamp if it is one, otherwise throws exception
     *
     * @return an BsonTimestamp
     * @throws org.bson.BsonInvalidOperationException if this value is not of the expected type
     */
    public BsonTimestamp asTimestamp() {
        throwIfInvalidType(BsonType.TIMESTAMP);
        return (BsonTimestamp) this;
    }

    /**
     * Gets this value as a BsonBinary if it is one, otherwise throws exception
     *
     * @return an BsonBinary
     * @throws org.bson.BsonInvalidOperationException if this value is not of the expected type
     */
    public BsonBinary asBinary() {
        throwIfInvalidType(BsonType.BINARY);
        return (BsonBinary) this;
    }

    /**
     * Gets this value as a BsonDateTime if it is one, otherwise throws exception
     *
     * @return an BsonDateTime
     * @throws org.bson.BsonInvalidOperationException if this value is not of the expected type
     */
    public BsonDateTime asDateTime() {
        throwIfInvalidType(BsonType.DATE_TIME);
        return (BsonDateTime) this;
    }

    /**
     * Gets this value as a BsonSymbol if it is one, otherwise throws exception
     *
     * @return an BsonSymbol
     * @throws org.bson.BsonInvalidOperationException if this value is not of the expected type
     */
    public BsonSymbol asSymbol() {
        throwIfInvalidType(BsonType.SYMBOL);
        return (BsonSymbol) this;
    }

    /**
     * Gets this value as a BsonRegularExpression if it is one, otherwise throws exception
     *
     * @return an BsonRegularExpression
     * @throws org.bson.BsonInvalidOperationException if this value is not of the expected type
     */
    public BsonRegularExpression asRegularExpression() {
        throwIfInvalidType(BsonType.REGULAR_EXPRESSION);
        return (BsonRegularExpression) this;
    }

    /**
     * Gets this value as a {@code BsonJavaScript} if it is one, otherwise throws exception
     *
     * @return a BsonJavaScript
     * @throws org.bson.BsonInvalidOperationException if this value is not of the expected type
     */
    public BsonJavaScript asJavaScript() {
        throwIfInvalidType(BsonType.JAVASCRIPT);
        return (BsonJavaScript) this;
    }

    /**
     * Gets this value as a BsonJavaScriptWithScope if it is one, otherwise throws exception
     *
     * @return a BsonJavaScriptWithScope
     * @throws org.bson.BsonInvalidOperationException if this value is not of the expected type
     */
    public BsonJavaScriptWithScope asJavaScriptWithScope() {
        throwIfInvalidType(BsonType.JAVASCRIPT_WITH_SCOPE);
        return (BsonJavaScriptWithScope) this;
    }

    /**
     * Returns true if this is a BsonNull, false otherwise.
     *
     * @return true if this is a BsonNull, false otherwise
     */
    public boolean isNull() {
        return this instanceof BsonNull;
    }

    /**
     * Returns true if this is a BsonDocument, false otherwise.
     *
     * @return true if this is a BsonDocument, false otherwise
     */
    public boolean isDocument() {
        return this instanceof BsonDocument;
    }

    /**
     * Returns true if this is a BsonArray, false otherwise.
     *
     * @return true if this is a BsonArray, false otherwise
     */
    public boolean isArray() {
        return this instanceof BsonArray;
    }

    /**
     * Returns true if this is a BsonString, false otherwise.
     *
     * @return true if this is a BsonString, false otherwise
     */
    public boolean isString() {
        return this instanceof BsonString;
    }

    /**
     * Returns true if this is a BsonNumber, false otherwise.
     *
     * @return true if this is a BsonNumber, false otherwise
     */
    public boolean isNumber() {
        return isInt32() || isInt64() || isDouble();
    }

    /**
     * Returns true if this is a BsonInt32, false otherwise.
     *
     * @return true if this is a BsonInt32, false otherwise
     */
    public boolean isInt32() {
        return this instanceof BsonInt32;
    }

    /**
     * Returns true if this is a BsonInt64, false otherwise.
     *
     * @return true if this is a BsonInt64, false otherwise
     */
    public boolean isInt64() {
        return this instanceof BsonInt64;
    }

    /**
     * Returns true if this is a BsonDecimal128, false otherwise.
     *
     * @return true if this is a BsonDecimal128, false otherwise
     * @since 3.4
     */
    public boolean isDecimal128() {
        return this instanceof BsonDecimal128;
    }

    /**
     * Returns true if this is a BsonDouble, false otherwise.
     *
     * @return true if this is a BsonDouble, false otherwise
     */
    public boolean isDouble() {
        return this instanceof BsonDouble;

    }

    /**
     * Returns true if this is a BsonBoolean, false otherwise.
     *
     * @return true if this is a BsonBoolean, false otherwise
     */
    public boolean isBoolean() {
        return this instanceof BsonBoolean;

    }

    /**
     * Returns true if this is an BsonObjectId, false otherwise.
     *
     * @return true if this is an BsonObjectId, false otherwise
     */
    public boolean isObjectId() {
        return this instanceof BsonObjectId;
    }

    /**
     * Returns true if this is a BsonDbPointer, false otherwise.
     *
     * @return true if this is a BsonDbPointer, false otherwise
     */
    public boolean isDBPointer() {
        return this instanceof BsonDbPointer;
    }

    /**
     * Returns true if this is a BsonTimestamp, false otherwise.
     *
     * @return true if this is a BsonTimestamp, false otherwise
     */
    public boolean isTimestamp() {
        return this instanceof BsonTimestamp;
    }

    /**
     * Returns true if this is a BsonBinary, false otherwise.
     *
     * @return true if this is a BsonBinary, false otherwise
     */
    public boolean isBinary() {
        return this instanceof BsonBinary;
    }

    /**
     * Returns true if this is a BsonDateTime, false otherwise.
     *
     * @return true if this is a BsonDateTime, false otherwise
     */
    public boolean isDateTime() {
        return this instanceof BsonDateTime;
    }

    /**
     * Returns true if this is a BsonSymbol, false otherwise.
     *
     * @return true if this is a BsonSymbol, false otherwise
     */
    public boolean isSymbol() {
        return this instanceof BsonSymbol;
    }

    /**
     * Returns true if this is a BsonRegularExpression, false otherwise.
     *
     * @return true if this is a BsonRegularExpression, false otherwise
     */
    public boolean isRegularExpression() {
        return this instanceof BsonRegularExpression;
    }

    /**
     * Returns true if this is a BsonJavaScript, false otherwise.
     *
     * @return true if this is a BsonJavaScript, false otherwise
     */
    public boolean isJavaScript() {
        return this instanceof BsonJavaScript;
    }

    /**
     * Returns true if this is a BsonJavaScriptWithScope, false otherwise.
     *
     * @return true if this is a BsonJavaScriptWithScope, false otherwise
     */
    public boolean isJavaScriptWithScope() {
        return this instanceof BsonJavaScriptWithScope;
    }

    private void throwIfInvalidType(final BsonType expectedType) {
        if (getBsonType() != expectedType) {
            throw new BsonInvalidOperationException(format("Value expected to be of type %s is of unexpected type %s",
                                                           expectedType, getBsonType()));
        }
    }
}


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

import com.mongodb.annotations.Immutable;

import java.util.concurrent.atomic.AtomicInteger;

import static com.mongodb.assertions.Assertions.isTrue;
import static com.mongodb.assertions.Assertions.notNull;
import static java.lang.String.format;

/**
 * An immutable connection identifier of a connection to a MongoDB server.
 *
 *
Contains a locally created id and if available the MongoDB server created connection id

 *
 * @since 3.0
 */
@Immutable
public final class ConnectionId {
    private static final AtomicInteger INCREMENTING_ID = new AtomicInteger();

    private final ServerId serverId;
    private final int localValue;
    private final Integer serverValue;
    private final String stringValue;

    /**
     * Construct an instance with the given server id.
     *
     * @param serverId the server id
     * @since 3.8
     */
    public ConnectionId(final ServerId serverId) {
        this(serverId, INCREMENTING_ID.incrementAndGet(), null);
    }

    /**
     * Construct an instance with the given serverId, localValue, and serverValue.
     *
     *

     *     Useful for testing, but generally prefer {@link #withServerValue(int)}
     *

     *
     * @param serverId the server id
     * @param localValue the local value
     * @param serverValue the server value, which may be null
     * @see #withServerValue(int)
     * @since 3.11
     */
    public ConnectionId(final ServerId serverId, final int localValue, final Integer serverValue) {
        this.serverId = notNull("serverId", serverId);
        this.localValue = localValue;
        this.serverValue = serverValue;
        if (serverValue == null) {
            stringValue = format("connectionId{localValue:%s}", localValue);
        } else {
            stringValue = format("connectionId{localValue:%s, serverValue:%s}", localValue, serverValue);
        }
    }

    /**
     * Creates a new connectionId with the set server value
     *
     * @param serverValue the server value
     * @return the new connection id
     * @since 3.8
     */
    public ConnectionId withServerValue(final int serverValue) {
        isTrue("server value is null", this.serverValue == null);
        return new ConnectionId(serverId, localValue, serverValue);
    }

    /**
     * Gets the server id.
     *
     * @return the server id
     */
    public ServerId getServerId() {
        return serverId;
    }

    /**
     * Gets the locally created id value for the connection
     *
     * @return the locally created id value for the connection
     */
    public int getLocalValue() {
        return localValue;
    }

    /**
     * Gets the server generated id value for the connection or null if not set.
     *
     * @return the server generated id value for the connection or null if not set.
     */
    public Integer getServerValue() {
        return serverValue;
    }

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

        ConnectionId that = (ConnectionId) o;

        if (localValue != that.localValue) {
            return false;
        }
        if (!serverId.equals(that.serverId)) {
            return false;
        }
        if (serverValue != null ? !serverValue.equals(that.serverValue) : that.serverValue != null) {
            return false;
        }

        return true;
    }

    @Override
    public int hashCode() {
        int result = serverId.hashCode();
        result = 31 * result + localValue;
        result = 31 * result + (serverValue != null ? serverValue.hashCode() : 0);
        return result;
    }

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


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

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ConcurrentMap;

import static java.util.Collections.unmodifiableList;

@SuppressWarnings("deprecation")
class ClassAncestry {

    /**
     *
Walks superclass and interface graph, superclasses first, then interfaces, to compute an ancestry list. Supertypes are visited
     * left
     * to right. Duplicates are removed such that no Class will appear in the list before one of its subtypes.

     *
     *
Does not need to be synchronized, races are harmless as the Class graph does not change at runtime.

     */
    public static <T> List<Class<?>> getAncestry(final Class<T> c) {
        ConcurrentMap<Class<?>, List<Class<?>>> cache = getClassAncestryCache();
        while (true) {
            List<Class<?>> cachedResult = cache.get(c);
            if (cachedResult != null) {
                return cachedResult;
            }
            cache.putIfAbsent(c, computeAncestry(c));
        }
    }

    /**
     * Starting with children and going back to parents
     */
    private static List<Class<?>> computeAncestry(final Class<?> c) {
        List<Class<?>> result = new ArrayList<Class<?>>();
        result.add(Object.class);
        computeAncestry(c, result);
        Collections.reverse(result);
        return unmodifiableList(new ArrayList<Class<?>>(result));
    }

    private static <T> void computeAncestry(final Class<T> c, final List<Class<?>> result) {
        if ((c == null) || (c == Object.class)) {
            return;
        }

        // first interfaces (looks backwards but is not)
        Class<?>[] interfaces = c.getInterfaces();
        for (int i = interfaces.length - 1; i >= 0; i--) {
            computeAncestry(interfaces[i], result);
        }

        // next superclass
        computeAncestry(c.getSuperclass(), result);

        if (!result.contains(c)) {
            result.add(c);
        }
    }

    /**
     * classAncestryCache
     */
    private static ConcurrentMap<Class<?>, List<Class<?>>> getClassAncestryCache() {
        return (_ancestryCache);
    }

    private static final ConcurrentMap<Class<?>, List<Class<?>>> _ancestryCache = CopyOnWriteMap.newHashMap();
}


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

import com.mongodb.client.model.geojson.codecs.GeoJsonCodecProvider;
import com.mongodb.lang.Nullable;
import org.bson.codecs.Codec;
import org.bson.codecs.EncoderContext;
import org.bson.codecs.configuration.CodecRegistries;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.json.JsonWriter;
import org.bson.json.JsonWriterSettings;

import java.io.StringWriter;

/**
 * An abstract class for representations of GeoJSON geometry objects.
 *
 * @since 3.1
 */
public abstract class Geometry {

    private static final CodecRegistry REGISTRY = CodecRegistries.fromProviders(new GeoJsonCodecProvider());

    private final CoordinateReferenceSystem coordinateReferenceSystem;

    /**
     * Construct an instance with no specified coordinate reference system.
     *
     */
    protected Geometry() {
        this(null);
    }

    /**
     * Construct an instance with the specified coordinate reference system.
     *
     * @param coordinateReferenceSystem the coordinate reference system
     */
    protected Geometry(@Nullable final CoordinateReferenceSystem coordinateReferenceSystem) {
        this.coordinateReferenceSystem = coordinateReferenceSystem;
    }

    /**
     * Gets the GeoJSON object type.
     *
     * @return the type
     */
    public abstract GeoJsonObjectType getType();

    /**
     * Converts to GeoJSON representation
     *
     * @return the GeoJSON representation
     */
    @SuppressWarnings({"unchecked", "rawtypes", "deprecation"})
    public String toJson() {
        StringWriter stringWriter = new StringWriter();
        JsonWriter writer = new JsonWriter(stringWriter, new JsonWriterSettings());
        Codec codec = getRegistry().get(getClass());
        codec.encode(writer, this, EncoderContext.builder().build());
        return stringWriter.toString();
    }

    static CodecRegistry getRegistry() {
        return REGISTRY;
    }

    /**
     * Gets the coordinate reference system, which may be null
     *
     * @return the possibly-null coordinate reference system
     */
    @Nullable
    public CoordinateReferenceSystem getCoordinateReferenceSystem() {
        return coordinateReferenceSystem;
    }

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

        Geometry geometry = (Geometry) o;

        if (coordinateReferenceSystem != null ? !coordinateReferenceSystem.equals(geometry.coordinateReferenceSystem)
                                              : geometry.coordinateReferenceSystem != null) {
            return false;
        }

        return true;
    }

    @Override
    public int hashCode() {
        return coordinateReferenceSystem != null ? coordinateReferenceSystem.hashCode() : 0;
    }
}


/*
 * 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.ExplainVerbosity;
import com.mongodb.MongoNamespace;
import com.mongodb.async.SingleResultCallback;
import com.mongodb.binding.AsyncConnectionSource;
import com.mongodb.binding.AsyncReadBinding;
import com.mongodb.binding.ConnectionSource;
import com.mongodb.binding.ReadBinding;
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.internal.connection.NoOpSessionContext;
import com.mongodb.operation.CommandOperationHelper.CommandReadTransformer;
import com.mongodb.operation.CommandOperationHelper.CommandReadTransformerAsync;
import com.mongodb.session.SessionContext;
import org.bson.BsonBoolean;
import org.bson.BsonDocument;
import org.bson.BsonInt32;
import org.bson.BsonJavaScript;
import org.bson.BsonNull;
import org.bson.BsonString;
import org.bson.BsonValue;
import org.bson.codecs.BsonDocumentCodec;
import org.bson.codecs.Decoder;

import java.util.concurrent.TimeUnit;

import static com.mongodb.assertions.Assertions.notNull;
import static com.mongodb.internal.async.ErrorHandlingResultCallback.errorHandlingCallback;
import static com.mongodb.operation.CommandOperationHelper.CommandCreator;
import static com.mongodb.operation.CommandOperationHelper.executeCommand;
import static com.mongodb.operation.CommandOperationHelper.executeCommandAsync;
import static com.mongodb.operation.DocumentHelper.putIfNotZero;
import static com.mongodb.operation.DocumentHelper.putIfTrue;
import static com.mongodb.operation.ExplainHelper.asExplainCommand;
import static com.mongodb.operation.OperationHelper.LOGGER;
import static com.mongodb.operation.OperationHelper.validateReadConcernAndCollation;
import static com.mongodb.operation.OperationReadConcernHelper.appendReadConcernToCommand;
import static java.util.concurrent.TimeUnit.MILLISECONDS;

/**
 *
Operation that runs a Map Reduce against a MongoDB instance.  This operation only supports "inline" results, i.e. the results will be
 * returned as a result of running this operation.

 *
 *
To run a map reduce operation into a given collection, use {@code MapReduceToCollectionOperation}.

 *
 * @param <T> the operations result type.
 * @mongodb.driver.manual reference/command/mapReduce/ mapReduce
 * @mongodb.driver.manual core/map-reduce Map-Reduce
 * @since 3.0
 */
@Deprecated
public class MapReduceWithInlineResultsOperation<T> implements AsyncReadOperation<MapReduceAsyncBatchCursor<T>>,
                                                               ReadOperation<MapReduceBatchCursor<T>> {
    private final MongoNamespace namespace;
    private final BsonJavaScript mapFunction;
    private final BsonJavaScript reduceFunction;
    private final Decoder<T> decoder;
    private BsonJavaScript finalizeFunction;
    private BsonDocument scope;
    private BsonDocument filter;
    private BsonDocument sort;
    private int limit;
    private boolean jsMode;
    private boolean verbose;
    private long maxTimeMS;
    private Collation collation;

    /**
     * Construct a MapReduceOperation with all the criteria it needs to execute.
     *
     * @param namespace the database and collection namespace for the operation.
     * @param mapFunction a JavaScript function that associates or "maps" a value with a key and emits the key and value pair.
     * @param reduceFunction a JavaScript function that "reduces" to a single object all the values associated with a particular key.
     * @param decoder the decoder for the result documents.
     * @mongodb.driver.manual reference/command/mapReduce/ mapReduce
     */
    public MapReduceWithInlineResultsOperation(final MongoNamespace namespace, final BsonJavaScript mapFunction,
                                               final BsonJavaScript reduceFunction, final Decoder<T> decoder) {
        this.namespace = notNull("namespace", namespace);
        this.mapFunction = notNull("mapFunction", mapFunction);
        this.reduceFunction = notNull("reduceFunction", reduceFunction);
        this.decoder = notNull("decoder", decoder);
    }

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

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

    /**
     * Gets the JavaScript function that associates or "maps" a value with a key and emits the key and value pair.
     *
     * @return the JavaScript function that associates or "maps" a value with a key and emits the key and value pair.
     */
    public BsonJavaScript getMapFunction() {
        return mapFunction;
    }

    /**
     * Gets the JavaScript function that "reduces" to a single object all the values associated with a particular key.
     *
     * @return the JavaScript function that "reduces" to a single object all the values associated with a particular key.
     */
    public BsonJavaScript getReduceFunction() {
        return reduceFunction;
    }

    /**
     * Gets the JavaScript function that follows the reduce method and modifies the output. Default is null
     *
     * @return the JavaScript function that follows the reduce method and modifies the output.
     * @mongodb.driver.manual reference/command/mapReduce/#mapreduce-finalize-cmd Requirements for the finalize Function
     */
    public BsonJavaScript getFinalizeFunction() {
        return finalizeFunction;
    }

    /**
     * Sets the JavaScript function that follows the reduce method and modifies the output.
     *
     * @param finalizeFunction the JavaScript function that follows the reduce method and modifies the output.
     * @return this
     * @mongodb.driver.manual reference/command/mapReduce/#mapreduce-finalize-cmd Requirements for the finalize Function
     */
    public MapReduceWithInlineResultsOperation<T> finalizeFunction(final BsonJavaScript finalizeFunction) {
        this.finalizeFunction = finalizeFunction;
        return this;
    }

    /**
     * Gets the global variables that are accessible in the map, reduce and finalize functions.
     *
     * @return the global variables that are accessible in the map, reduce and finalize functions.
     * @mongodb.driver.manual reference/command/mapReduce Scope
     */
    public BsonDocument getScope() {
        return scope;
    }

    /**
     * Sets the global variables that are accessible in the map, reduce and finalize functions.
     *
     * @param scope the global variables that are accessible in the map, reduce and finalize functions.
     * @return this
     * @mongodb.driver.manual reference/command/mapReduce mapReduce
     */
    public MapReduceWithInlineResultsOperation<T> scope(final BsonDocument scope) {
        this.scope = scope;
        return this;
    }

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

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

    /**
     * Gets the flag that specifies whether to convert intermediate data into BSON format between the execution of the map and reduce
     * functions. Defaults to false.
     *
     * @return jsMode
     * @mongodb.driver.manual reference/command/mapReduce mapReduce
     */
    public boolean isJsMode() {
        return jsMode;
    }

    /**
     * Sets the flag that specifies whether to convert intermediate data into BSON format between the execution of the map and reduce
     * functions. Defaults to false.
     *
     * @param jsMode the flag that specifies whether to convert intermediate data into BSON format between the execution of the map and
     *               reduce functions
     * @return jsMode
     * @mongodb.driver.manual reference/command/mapReduce mapReduce
     */
    public MapReduceWithInlineResultsOperation<T> jsMode(final boolean jsMode) {
        this.jsMode = jsMode;
        return this;
    }

    /**
     * Gets whether to include the timing information in the result information. Defaults to true.
     *
     * @return whether to include the timing information in the result information
     */
    public boolean isVerbose() {
        return verbose;
    }

    /**
     * Sets whether to include the timing information in the result information.
     *
     * @param verbose whether to include the timing information in the result information.
     * @return this
     */
    public MapReduceWithInlineResultsOperation<T> verbose(final boolean verbose) {
        this.verbose = verbose;
        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 MapReduceWithInlineResultsOperation<T> collation(final Collation collation) {
        this.collation = collation;
        return this;
    }

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

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

    /**
     * Executing this will return a cursor with your results and the statistics in.
     *
     * @param binding the binding
     * @return a MapReduceCursor that can be iterated over to find all the results of the Map Reduce operation.
     */
    @Override
    @SuppressWarnings("unchecked")
    public MapReduceBatchCursor<T> execute(final ReadBinding binding) {
        return executeCommand(binding, namespace.getDatabaseName(), getCommandCreator(binding.getSessionContext()),
                CommandResultDocumentCodec.create(decoder, "results"), transformer(), false);
    }

    @Override
    public void executeAsync(final AsyncReadBinding binding, final SingleResultCallback<MapReduceAsyncBatchCursor<T>> callback) {
        SingleResultCallback<MapReduceAsyncBatchCursor<T>> errHandlingCallback = errorHandlingCallback(callback, LOGGER);
        executeCommandAsync(binding, namespace.getDatabaseName(), getCommandCreator(binding.getSessionContext()),
                CommandResultDocumentCodec.create(decoder, "results"),
                asyncTransformer(), false, errHandlingCallback);
    }

    /**
     * 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) {
        return createExplainableOperation(explainVerbosity);
    }

    /**
     * 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) {
        return createExplainableOperation(explainVerbosity);
    }

    private CommandReadOperation<BsonDocument> createExplainableOperation(final ExplainVerbosity explainVerbosity) {
        return new CommandReadOperation<BsonDocument>(namespace.getDatabaseName(),
                                                      asExplainCommand(getCommand(NoOpSessionContext.INSTANCE), explainVerbosity),
                                                      new BsonDocumentCodec());
    }

    private CommandReadTransformer<BsonDocument, MapReduceBatchCursor<T>> transformer() {
        return new CommandReadTransformer<BsonDocument, MapReduceBatchCursor<T>>() {
            @Override
            public MapReduceBatchCursor<T> apply(final BsonDocument result, final ConnectionSource source, final Connection connection) {
                return new MapReduceInlineResultsCursor<T>(createQueryResult(result, connection.getDescription()), decoder, source,
                                                           MapReduceHelper.createStatistics(result));
            }
        };
    }

    private CommandReadTransformerAsync<BsonDocument, MapReduceAsyncBatchCursor<T>> asyncTransformer() {
        return new CommandReadTransformerAsync<BsonDocument, MapReduceAsyncBatchCursor<T>>() {
            @Override
            public MapReduceAsyncBatchCursor<T> apply(final BsonDocument result, final AsyncConnectionSource source,
                                                      final AsyncConnection connection) {
                return new MapReduceInlineResultsAsyncCursor<T>(createQueryResult(result, connection.getDescription()),
                                                                MapReduceHelper.createStatistics(result));
            }
        };
    }

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

    private BsonDocument getCommand(final SessionContext sessionContext) {
        BsonDocument commandDocument = new BsonDocument("mapreduce", new BsonString(namespace.getCollectionName()))
                                           .append("map", getMapFunction())
                                           .append("reduce", getReduceFunction())
                                           .append("out", new BsonDocument("inline", new BsonInt32(1)))
                                           .append("query", asValueOrNull(getFilter()))
                                           .append("sort", asValueOrNull(getSort()))
                                           .append("finalize", asValueOrNull(getFinalizeFunction()))
                                           .append("scope", asValueOrNull(getScope()))
                                           .append("verbose", BsonBoolean.valueOf(isVerbose()));

        appendReadConcernToCommand(sessionContext, commandDocument);
        putIfNotZero(commandDocument, "limit", getLimit());
        putIfNotZero(commandDocument, "maxTimeMS", getMaxTime(MILLISECONDS));
        putIfTrue(commandDocument, "jsMode", isJsMode());
        if (collation != null) {
            commandDocument.put("collation", collation.asDocument());
        }
        return commandDocument;
    }

    @SuppressWarnings("unchecked")
    private QueryResult<T> createQueryResult(final BsonDocument result, final ConnectionDescription description) {
        return new QueryResult<T>(namespace, BsonDocumentWrapperHelper.<T>toList(result, "results"), 0,
                                  description.getServerAddress());
    }

    private static BsonValue asValueOrNull(final BsonValue value) {
        return value == null ? BsonNull.VALUE : value;
    }
}


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

/**
 * The options to apply to the creation of an index.
 *
 * @mongodb.driver.manual reference/command/createIndexes Index options
 * @since 3.0
 */
public class IndexOptions {
    private boolean background;
    private boolean unique;
    private String name;
    private boolean sparse;
    private Long expireAfterSeconds;
    private Integer version;
    private Bson weights;
    private String defaultLanguage;
    private String languageOverride;
    private Integer textVersion;
    private Integer sphereVersion;
    private Integer bits;
    private Double min;
    private Double max;
    private Double bucketSize;
    private Bson storageEngine;
    private Bson partialFilterExpression;
    private Collation collation;
    private Bson wildcardProjection;

    /**
     * Create the index in the background
     *
     * @return true if should create the index in the background
     */
    public boolean isBackground() {
        return background;
    }

    /**
     * Should the index should be created in the background
     *
     * @param background true if should create the index in the background
     * @return this
     */
    public IndexOptions background(final boolean background) {
        this.background = background;
        return this;
    }

    /**
     * Gets if the index should be unique.
     *
     * @return true if the index should be unique
     */
    public boolean isUnique() {
        return unique;
    }

    /**
     * Should the index should be unique.
     *
     * @param unique if the index should be unique
     * @return this
     */
    public IndexOptions unique(final boolean unique) {
        this.unique = unique;
        return this;
    }

    /**
     * Gets the name of the index.
     *
     * @return the name of the index
     */
    @Nullable
    public String getName() {
        return name;
    }

    /**
     * Sets the name of the index.
     *
     * @param name of the index
     * @return this
     */
    public IndexOptions name(@Nullable final String name) {
        this.name = name;
        return this;
    }

    /**
     * If true, the index only references documents with the specified field
     *
     * @return if the index should only reference documents with the specified field
     */
    public boolean isSparse() {
        return sparse;
    }

    /**
     * Should the index only references documents with the specified field
     *
     * @param sparse if true, the index only references documents with the specified field
     * @return this
     */
    public IndexOptions sparse(final boolean sparse) {
        this.sparse = sparse;
        return this;
    }

    /**
     * Gets the time to live for documents in the collection
     *
     * @return the time to live for documents in the collection
     * @param timeUnit the time unit
     * @mongodb.driver.manual tutorial/expire-data TTL
     */
    @Nullable
    public Long getExpireAfter(final TimeUnit timeUnit) {
        if (expireAfterSeconds == null) {
            return null;
        }
        return timeUnit.convert(expireAfterSeconds, TimeUnit.SECONDS);
    }

    /**
     * Sets the time to live for documents in the collection
     *
     * @param expireAfter the time to live for documents in the collection
     * @param timeUnit the time unit for expireAfter
     * @return this
     * @mongodb.driver.manual tutorial/expire-data TTL
     */
    public IndexOptions expireAfter(@Nullable final Long expireAfter, final TimeUnit timeUnit) {
        if (expireAfter == null) {
            this.expireAfterSeconds = null;
        } else {
            this.expireAfterSeconds = TimeUnit.SECONDS.convert(expireAfter, timeUnit);
        }
        return this;
    }

    /**
     * Gets the index version number.
     *
     * @return the index version number
     */
    @Nullable
    public Integer getVersion() {
        return this.version;
    }

    /**
     * Sets the index version number.
     *
     * @param version the index version number
     * @return this
     */
    public IndexOptions version(@Nullable final Integer version) {
        this.version = version;
        return this;
    }

    /**
     * Gets the weighting object for use with a text index
     *
     *
A document that represents field and weight pairs. The weight is an integer ranging from 1 to 99,999 and denotes the significance
     * of the field relative to the other indexed fields in terms of the score.

     *
     * @return the weighting object
     * @mongodb.driver.manual tutorial/control-results-of-text-search Control Search Results with Weights
     */
    @Nullable
    public Bson getWeights() {
        return weights;
    }

    /**
     * Sets the weighting object for use with a text index.
     *
     *
An document that represents field and weight pairs. The weight is an integer ranging from 1 to 99,999 and denotes the significance
     * of the field relative to the other indexed fields in terms of the score.

     *
     * @param weights the weighting object
     * @return this
     * @mongodb.driver.manual tutorial/control-results-of-text-search Control Search Results with Weights
     */
    public IndexOptions weights(@Nullable final Bson weights) {
        this.weights = weights;
        return this;
    }

    /**
     * Gets the language for a text index.
     *
     *
The language that determines the list of stop words and the rules for the stemmer and tokenizer.

     *
     * @return the language for a text index.
     * @mongodb.driver.manual reference/text-search-languages Text Search languages
     */
    @Nullable
    public String getDefaultLanguage() {
        return defaultLanguage;
    }

    /**
     * Sets the language for the text index.
     *
     *
The language that determines the list of stop words and the rules for the stemmer and tokenizer.

     *
     * @param defaultLanguage the language for the text index.
     * @return this
     * @mongodb.driver.manual reference/text-search-languages Text Search languages
     */
    public IndexOptions defaultLanguage(@Nullable final String defaultLanguage) {
        this.defaultLanguage = defaultLanguage;
        return this;
    }

    /**
     * Gets the name of the field that contains the language string.
     *
     *
For text indexes, the name of the field, in the collection's documents, that contains the override language for the document.

     *
     * @return the name of the field that contains the language string.
     * @mongodb.driver.manual tutorial/specify-language-for-text-index/#specify-language-field-text-index-example Language override
     */
    @Nullable
    public String getLanguageOverride() {
        return languageOverride;
    }

    /**
     * Sets the name of the field that contains the language string.
     *
     *
For text indexes, the name of the field, in the collection's documents, that contains the override language for the document.

     *
     * @param languageOverride the name of the field that contains the language string.
     * @return this
     * @mongodb.driver.manual tutorial/specify-language-for-text-index/#specify-language-field-text-index-example Language override
     */
    public IndexOptions languageOverride(@Nullable final String languageOverride) {
        this.languageOverride = languageOverride;
        return this;
    }

    /**
     * The text index version number.
     *
     * @return the text index version number.
     */
    @Nullable
    public Integer getTextVersion() {
        return textVersion;
    }

    /**
     * Set the text index version number.
     *
     * @param textVersion the text index version number.
     * @return this
     */
    public IndexOptions textVersion(@Nullable final Integer textVersion) {
        this.textVersion = textVersion;
        return this;
    }

    /**
     * Gets the 2dsphere index version number.
     *
     * @return the 2dsphere index version number
     */
    @Nullable
    public Integer getSphereVersion() {
        return sphereVersion;
    }

    /**
     * Sets the 2dsphere index version number.
     *
     * @param sphereVersion the 2dsphere index version number.
     * @return this
     */
    public IndexOptions sphereVersion(@Nullable final Integer sphereVersion) {
        this.sphereVersion = sphereVersion;
        return this;
    }

    /**
     * Gets the number of precision of the stored geohash value of the location data in 2d indexes.
     *
     * @return the number of precision of the stored geohash value
     */
    @Nullable
    public Integer getBits() {
        return bits;
    }

    /**
     * Sets the number of precision of the stored geohash value of the location data in 2d indexes.
     *
     * @param bits the number of precision of the stored geohash value
     * @return this
     */
    public IndexOptions bits(@Nullable final Integer bits) {
        this.bits = bits;
        return this;
    }

    /**
     * Gets the lower inclusive boundary for the longitude and latitude values for 2d indexes..
     *
     * @return the lower inclusive boundary for the longitude and latitude values.
     */
    @Nullable
    public Double getMin() {
        return min;
    }

    /**
     * Sets the lower inclusive boundary for the longitude and latitude values for 2d indexes..
     *
     * @param min the lower inclusive boundary for the longitude and latitude values
     * @return this
     */
    public IndexOptions min(@Nullable final Double min) {
        this.min = min;
        return this;
    }

    /**
     * Gets the upper inclusive boundary for the longitude and latitude values for 2d indexes..
     *
     * @return the upper inclusive boundary for the longitude and latitude values.
     */
    @Nullable
    public Double getMax() {
        return max;
    }

    /**
     * Sets the upper inclusive boundary for the longitude and latitude values for 2d indexes..
     *
     * @param max the upper inclusive boundary for the longitude and latitude values
     * @return this
     */
    public IndexOptions max(@Nullable final Double max) {
        this.max = max;
        return this;
    }

    /**
     * Gets the specified the number of units within which to group the location values for geoHaystack Indexes
     *
     * @return the specified the number of units within which to group the location values for geoHaystack Indexes
     * @mongodb.driver.manual core/geohaystack/ geoHaystack Indexes
     */
    @Nullable
    public Double getBucketSize() {
        return bucketSize;
    }

    /**
     * Sets the specified the number of units within which to group the location values for geoHaystack Indexes
     *
     * @param bucketSize the specified the number of units within which to group the location values for geoHaystack Indexes
     * @return this
     * @mongodb.driver.manual core/geohaystack/ geoHaystack Indexes
     */
    public IndexOptions bucketSize(@Nullable final Double bucketSize) {
        this.bucketSize = bucketSize;
        return this;
    }

    /**
     * Gets the storage engine options document for this index.
     *
     * @return the storage engine options
     * @mongodb.server.release 3.0
     */
    @Nullable
    public Bson getStorageEngine() {
        return storageEngine;
    }

    /**
     * Sets the storage engine options document for this index.
     *
     * @param storageEngine the storage engine options
     * @return this
     * @mongodb.server.release 3.0
     */
    public IndexOptions storageEngine(@Nullable final Bson storageEngine) {
        this.storageEngine = storageEngine;
        return this;
    }

    /**
     * Get the filter expression for the documents to be included in the index or null if not set
     *
     * @return the filter expression for the documents to be included in the index or null if not set
     * @mongodb.server.release 3.2
     * @since 3.2
     */
    @Nullable
    public Bson getPartialFilterExpression() {
        return partialFilterExpression;
    }

    /**
     * Sets the filter expression for the documents to be included in the index
     *
     * @param partialFilterExpression the filter expression for the documents to be included in the index
     * @return this
     * @mongodb.server.release 3.2
     * @since 3.2
     */
    public IndexOptions partialFilterExpression(@Nullable final Bson partialFilterExpression) {
        this.partialFilterExpression = partialFilterExpression;
        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 IndexOptions collation(@Nullable final Collation collation) {
        this.collation = collation;
        return this;
    }

    /**
     * Gets the wildcard projection of a wildcard index
     *
     * @return the wildcard projection
     * @mongodb.server.release 4.2
     * @since 3.10
     */
    public Bson getWildcardProjection() {
        return wildcardProjection;
    }

    /**
     * Sets the wildcard projection of a wildcard index
     *
     * @param wildcardProjection the wildcard projection
     * @return this
     * @mongodb.server.release 4.2
     * @since 3.10
     */
    public IndexOptions wildcardProjection(final Bson wildcardProjection) {
        this.wildcardProjection = wildcardProjection;
        return this;
    }

    @Override
    public String toString() {
        return "IndexOptions{"
                + "background=" + background
                + ", unique=" + unique
                + ", name='" + name + '\''
                + ", sparse=" + sparse
                + ", expireAfterSeconds=" + expireAfterSeconds
                + ", version=" + version
                + ", weights=" + weights
                + ", defaultLanguage='" + defaultLanguage + '\''
                + ", languageOverride='" + languageOverride + '\''
                + ", textVersion=" + textVersion
                + ", sphereVersion=" + sphereVersion
                + ", bits=" + bits
                + ", min=" + min
                + ", max=" + max
                + ", bucketSize=" + bucketSize
                + ", storageEngine=" + storageEngine
                + ", partialFilterExpression=" + partialFilterExpression
                + ", collation=" + collation
                + ", wildcardProjection=" + wildcardProjection
                + '}';
    }
}


/*
 * 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.client.model.Collation;
import com.mongodb.lang.Nullable;
import com.mongodb.operation.GroupOperation;
import org.bson.BsonDocumentWrapper;
import org.bson.BsonJavaScript;

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

/**
 * This class groups the argument for a group operation and can build the underlying command object
 *
 * @mongodb.driver.manual reference/command/group/ Group
 * @deprecated the group command was deprecated in MongoDB 3.4
 */
@Deprecated
public class GroupCommand {
    private final String collectionName;
    private final DBObject keys;
    private final String keyf;
    private final DBObject condition;
    private final DBObject initial;
    private final String reduce;
    private final String finalize;
    private final Collation collation;

    /**
     * Creates a new group command.
     *
     * @param collection the collection from which to perform the group by operation.
     * @param keys       the field or fields to group.
     * @param condition  optional - a filter to determine which documents in the collection to process.
     * @param initial    the initial state of the aggregation result document.
     * @param reduce     a JavaScript aggregation function that operates on the documents during the grouping operation.
     * @param finalize   optional - a JavaScript function that runs each item in the result set before group returns the final value.
     */
    public GroupCommand(final DBCollection collection, final DBObject keys, final DBObject condition,
                        final DBObject initial, final String reduce, @Nullable final String finalize) {
        this(collection, keys, condition, initial, reduce, finalize, null);
    }

    /**
     * Creates a new group command.
     *
     * @param collection the collection from which to perform the group by operation.
     * @param keys       the field or fields to group.
     * @param condition  optional - a filter to determine which documents in the collection to process.
     * @param initial    the initial state of the aggregation result document.
     * @param reduce     a JavaScript aggregation function that operates on the documents during the grouping operation.
     * @param finalize   optional - a JavaScript function that runs each item in the result set before group returns the final value.
     * @param collation  optional - the collation options
     *
     * @since 3.4
     */
    public GroupCommand(final DBCollection collection, final DBObject keys, final DBObject condition,
                        final DBObject initial, final String reduce, @Nullable final String finalize, @Nullable final Collation collation) {
        notNull("collection", collection);
        this.collectionName = collection.getName();
        this.keys = keys;
        this.condition = condition;
        this.initial = initial;
        this.reduce = reduce;
        this.finalize = finalize;
        this.keyf = null;
        this.collation = collation;
    }

    /**
     * Creates a new group command.
     *
     * @param collection the collection from which to perform the group by operation.
     * @param keyf       the function that creates a "key object" for use as the grouping key
     * @param condition  optional - a filter to determine which documents in the collection to process.
     * @param initial    the initial state of the aggregation result document.
     * @param reduce     a JavaScript aggregation function that operates on the documents during the grouping operation.
     * @param finalize   optional - a JavaScript function that runs each item in the result set before group returns the final value.
     *
     * @since 3.1
     */
    public GroupCommand(final DBCollection collection, final String keyf, final DBObject condition,
                        final DBObject initial, final String reduce, final String finalize) {
        this(collection, keyf, condition, initial, reduce, finalize, null);
    }

    /**
     * Creates a new group command.
     *
     * @param collection the collection from which to perform the group by operation.
     * @param keyf       the function that creates a "key object" for use as the grouping key
     * @param condition  optional - a filter to determine which documents in the collection to process.
     * @param initial    the initial state of the aggregation result document.
     * @param reduce     a JavaScript aggregation function that operates on the documents during the grouping operation.
     * @param finalize   optional - a JavaScript function that runs each item in the result set before group returns the final value.
     * @param collation  optional - the collation options
     *
     * @since 3.4
     */
    public GroupCommand(final DBCollection collection, final String keyf, final DBObject condition,
                        final DBObject initial, final String reduce, @Nullable final String finalize, @Nullable final Collation collation) {
        notNull("collection", collection);
        this.collectionName = collection.getName();
        this.keyf = notNull("keyf", keyf);
        this.condition = condition;
        this.initial = initial;
        this.reduce = reduce;
        this.finalize = finalize;
        this.keys = null;
        this.collation = collation;
    }

    /**
     * Turns this group command into the DBObject format of the command.
     *
     * @return a DBObject containing the group command as a MongoDB document
     */
    public DBObject toDBObject() {
        DBObject args = new BasicDBObject("ns", collectionName).append("cond", condition)
                                                               .append("$reduce", reduce)
                                                               .append("initial", initial);

        if (keys != null) {
            args.put("key", keys);
        }

        if (keyf != null) {
            args.put("$keyf", keyf);
        }

        if (finalize != null) {
            args.put("finalize", finalize);
        }
        return new BasicDBObject("group", args);
    }

    GroupOperation<DBObject> toOperation(final MongoNamespace namespace, final DBObjectCodec codec, final boolean retryReads) {
        if (initial == null) {
            throw new IllegalArgumentException("Group command requires an initial document for the aggregate result");
        }

        if (reduce == null) {
            throw new IllegalArgumentException("Group command requires a reduce function for the aggregate result");
        }

        GroupOperation<DBObject> operation = new GroupOperation<DBObject>(namespace, new BsonJavaScript(reduce),
                new BsonDocumentWrapper<DBObject>(initial, codec), codec).retryReads(retryReads);

        if (keys != null) {
            operation.key(new BsonDocumentWrapper<DBObject>(keys, codec));
        }

        if (keyf != null) {
            operation.keyFunction(new BsonJavaScript(keyf));
        }

        if (condition != null) {
            operation.filter(new BsonDocumentWrapper<DBObject>(condition, codec));
        }

        if (finalize != null) {
            operation.finalizeFunction(new BsonJavaScript(finalize));
        }
        operation.collation(collation);
        return operation;
    }

}


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

import com.mongodb.MongoException;
import com.mongodb.ReadPreference;
import com.mongodb.ServerAddress;
import com.mongodb.TagSet;
import com.mongodb.annotations.Immutable;
import com.mongodb.selector.ReadPreferenceServerSelector;
import com.mongodb.selector.WritableServerSelector;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import static com.mongodb.assertions.Assertions.notNull;
import static java.lang.String.format;

/**
 * Immutable snapshot state of a cluster.
 *
 * @since 3.0
 */
@Immutable
public class ClusterDescription {
    private final ClusterConnectionMode connectionMode;
    private final ClusterType type;
    private final List<ServerDescription> serverDescriptions;
    private final ClusterSettings clusterSettings;
    private final ServerSettings serverSettings;
    private final MongoException srvResolutionException;

    /**
     * Creates a new ClusterDescription.
     *
     * @param connectionMode     whether to connect directly to a single server or to multiple servers
     * @param type               what sort of cluster this is
     * @param serverDescriptions the descriptions of all the servers currently in this cluster
     */
    public ClusterDescription(final ClusterConnectionMode connectionMode, final ClusterType type,
                              final List<ServerDescription> serverDescriptions) {
        this(connectionMode, type, serverDescriptions, null, null);
    }

    /**
     * Creates a new ClusterDescription.
     *
     * @param connectionMode     whether to connect directly to a single server or to multiple servers
     * @param type               what sort of cluster this is
     * @param serverDescriptions the descriptions of all the servers currently in this cluster
     * @param clusterSettings    the cluster settings
     * @param serverSettings     the server settings
     * @since 3.4
     */
    public ClusterDescription(final ClusterConnectionMode connectionMode, final ClusterType type,
                              final List<ServerDescription> serverDescriptions,
                              final ClusterSettings clusterSettings,
                              final ServerSettings serverSettings) {
        this(connectionMode, type, null, serverDescriptions, clusterSettings, serverSettings);
    }

    /**
     * Creates a new ClusterDescription.
     *
     * @param connectionMode     whether to connect directly to a single server or to multiple servers
     * @param type               what sort of cluster this is
     * @param srvResolutionException an exception resolving the SRV record
     * @param serverDescriptions the descriptions of all the servers currently in this cluster
     * @param clusterSettings    the cluster settings
     * @param serverSettings     the server settings
     * @since 3.10
     */
    public ClusterDescription(final ClusterConnectionMode connectionMode, final ClusterType type,
                              final MongoException srvResolutionException,
                              final List<ServerDescription> serverDescriptions,
                              final ClusterSettings clusterSettings,
                              final ServerSettings serverSettings) {
        notNull("all", serverDescriptions);
        this.connectionMode = notNull("connectionMode", connectionMode);
        this.type = notNull("type", type);
        this.srvResolutionException = srvResolutionException;
        this.serverDescriptions = new ArrayList<ServerDescription>(serverDescriptions);
        this.clusterSettings = clusterSettings;
        this.serverSettings = serverSettings;
    }

    /**
     * Gets the cluster settings, which may be null if not provided.
     *
     * @return the cluster settings
     * @since 3.4
     */
    public ClusterSettings getClusterSettings() {
        return clusterSettings;
    }

    /**
     * Gets the server settings, which may be null if not provided.
     *
     * @return the server settings
     * @since 3.4
     */
    public ServerSettings getServerSettings() {
        return serverSettings;
    }

    /**
     * Return whether all servers in the cluster are compatible with the driver.
     *
     * @return true if all servers in the cluster are compatible with the driver
     */
    public boolean isCompatibleWithDriver() {
        for (ServerDescription cur : serverDescriptions) {
            if (!cur.isCompatibleWithDriver()) {
                return false;
            }
        }
        return true;
    }

    /**
     * Return a server in the cluster that is incompatibly older than the driver.
     *
     * @return a server in the cluster that is incompatibly older than the driver, or null if there are none
     * @since 3.6
     */
    public ServerDescription findServerIncompatiblyOlderThanDriver() {
        for (ServerDescription cur : serverDescriptions) {
            if (cur.isIncompatiblyOlderThanDriver()) {
                return cur;
            }
        }
        return null;
    }

    /**
     * Return a server in the cluster that is incompatibly newer than the driver.
     *
     * @return a server in the cluster that is incompatibly newer than the driver, or null if there are none
     * @since 3.6
     */
    public ServerDescription findServerIncompatiblyNewerThanDriver() {
        for (ServerDescription cur : serverDescriptions) {
            if (cur.isIncompatiblyNewerThanDriver()) {
                return cur;
            }
        }
        return null;
    }

    /**
     * Returns true if this cluster has at least one server that satisfies the given read preference.
     *
     * @param readPreference the non-null read preference
     * @return whether this cluster has at least one server that satisfies the given read preference
     * @since 3.3
     */
    public boolean hasReadableServer(final ReadPreference readPreference) {
        notNull("readPreference", readPreference);
        return !new ReadPreferenceServerSelector(readPreference).select(this).isEmpty();
    }

    /**
     * Returns true if this cluster has at least one server that can be used for write operations.
     *
     * @return true if this cluster has at least one server that can be used for write operations
     * @since 3.3
     */
    public boolean hasWritableServer() {
        return !new WritableServerSelector().select(this).isEmpty();
    }

    /**
     * Gets whether this cluster is connecting to a single server or multiple servers.
     *
     * @return the ClusterConnectionMode for this cluster
     */
    public ClusterConnectionMode getConnectionMode() {
        return connectionMode;
    }

    /**
     * Gets the specific type of this cluster
     *
     * @return a ClusterType enum representing the type of this cluster
     */
    public ClusterType getType() {
        return type;
    }

    /**
     * Gets any exception encountered while resolving the SRV record for the initial host.
     *
     * @return any exception encountered while resolving the SRV record for the initial host, or null if none
     * @since 3.10
     */
    public MongoException getSrvResolutionException() {
        return srvResolutionException;
    }

    /**
     * Returns an unmodifiable list of the server descriptions in this cluster description.
     *
     * @return an unmodifiable list of the server descriptions in this cluster description
     * @since 3.3
     */
    public List<ServerDescription> getServerDescriptions() {
        return Collections.unmodifiableList(serverDescriptions);
    }

    /**
     * Gets the logical session timeout in minutes, or null if at least one of the known servers does not support logical sessions.
     *
     * @return the logical session timeout in minutes, which may be null
     * @mongodb.server.release 3.6
     * @since 3.6
     */
    public Integer getLogicalSessionTimeoutMinutes() {
        Integer retVal = null;
        for (ServerDescription cur : getServersByPredicate(new Predicate() {
            @Override
            public boolean apply(final ServerDescription serverDescription) {
                return serverDescription.isPrimary() || serverDescription.isSecondary();
            }
        })) {
            if (cur.getLogicalSessionTimeoutMinutes() == null) {
                return null;
            }
            if (retVal == null) {
                retVal = cur.getLogicalSessionTimeoutMinutes();
            } else {
                retVal = Math.min(retVal, cur.getLogicalSessionTimeoutMinutes());
            }
        }
        return retVal;
    }

    /**
     * Returns the Set of all server descriptions in this cluster, sorted by the String value of the ServerAddress of each one.
     *
     * @return the set of server descriptions
     * @deprecated Use {@link #getServerDescriptions()} instead
     */
    @Deprecated
    public Set<ServerDescription> getAll() {
        Set<ServerDescription> serverDescriptionSet = new TreeSet<ServerDescription>(new Comparator<ServerDescription>() {
            @Override
            public int compare(final ServerDescription o1, final ServerDescription o2) {
                int val = o1.getAddress().getHost().compareTo(o2.getAddress().getHost());
                if (val != 0) {
                    return val;
                }
                return integerCompare(o1.getAddress().getPort(), o2.getAddress().getPort());
            }

            private int integerCompare(final int p1, final int p2) {
                return (p1 < p2) ? -1 : ((p1 == p2) ? 0 : 1);
            }
        });
        serverDescriptionSet.addAll(serverDescriptions);
        return Collections.unmodifiableSet(serverDescriptionSet);
    }

    /**
     * Returns the ServerDescription for the server at the given address
     *
     * @param serverAddress the ServerAddress for a server in this cluster
     * @return the ServerDescription for this server
     * @deprecated Replace with a filter on ServerDescription in the caller
     */
    @Deprecated
    public ServerDescription getByServerAddress(final ServerAddress serverAddress) {
        for (final ServerDescription cur : serverDescriptions) {
            if (cur.isOk() && cur.getAddress().equals(serverAddress)) {
                return cur;
            }
        }
        return null;
    }

    /**
     * While it may seem counter-intuitive that a MongoDB cluster can have more than one primary, it can in the case where the client's view
     * of the cluster is a set of mongos servers, any of which can serve as the primary.
     *
     * @return a list of servers that can act as primaries
     * @deprecated Replace with a filter on ServerDescription in the caller
     */
    @Deprecated
    public List<ServerDescription> getPrimaries() {
        return getServersByPredicate(new Predicate() {
            public boolean apply(final ServerDescription serverDescription) {
                return serverDescription.isPrimary();
            }
        });
    }

    /**
     * Get a list of all the secondaries in this cluster
     *
     * @return a List of ServerDescriptions of all the secondaries this cluster is currently aware of
     * @deprecated Replace with a filter on ServerDescription in the caller
     */
    @Deprecated
    public List<ServerDescription> getSecondaries() {
        return getServersByPredicate(new Predicate() {
            public boolean apply(final ServerDescription serverDescription) {
                return serverDescription.isSecondary();
            }
        });
    }

    /**
     * Get a list of all the secondaries in this cluster that match a given TagSet
     *
     * @param tagSet a Set of replica set tags
     * @return a List of ServerDescriptions of all the secondaries this cluster that match all of the given tags
     * @deprecated Replace with a filter on ServerDescription in the caller
     */
    @Deprecated
    public List<ServerDescription> getSecondaries(final TagSet tagSet) {
        return getServersByPredicate(new Predicate() {
            public boolean apply(final ServerDescription serverDescription) {
                return serverDescription.isSecondary() && serverDescription.hasTags(tagSet);
            }
        });
    }

    /**
     * Gets a list of ServerDescriptions for all the servers in this cluster which are currently accessible.
     *
     * @return a List of ServerDescriptions for all servers that have a status of OK
     * @deprecated Replace with a filter on ServerDescription in the caller
     */
    @Deprecated
    public List<ServerDescription> getAny() {
        return getServersByPredicate(new Predicate() {
            public boolean apply(final ServerDescription serverDescription) {
                return serverDescription.isOk();
            }
        });
    }

    /**
     * Gets a list of all the primaries and secondaries in this cluster.
     *
     * @return a list of ServerDescriptions for all primary and secondary servers
     * @deprecated Replace with a filter on ServerDescription in the caller
     */
    @Deprecated
    public List<ServerDescription> getAnyPrimaryOrSecondary() {
        return getServersByPredicate(new Predicate() {
            public boolean apply(final ServerDescription serverDescription) {
                return serverDescription.isPrimary() || serverDescription.isSecondary();
            }
        });
    }

    /**
     * Gets a list of all the primaries and secondaries in this cluster that match the given replica set tags.
     *
     * @param tagSet a Set of replica set tags
     * @return a list of ServerDescriptions for all primary and secondary servers that contain all of the given tags
     * @deprecated Replace with a filter on ServerDescription in the caller
     */
    @Deprecated
    public List<ServerDescription> getAnyPrimaryOrSecondary(final TagSet tagSet) {
        return getServersByPredicate(new Predicate() {
            public boolean apply(final ServerDescription serverDescription) {
                return (serverDescription.isPrimary() || serverDescription.isSecondary()) && serverDescription.hasTags(tagSet);
            }
        });
    }

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

        ClusterDescription that = (ClusterDescription) o;

        if (connectionMode != that.connectionMode) {
            return false;
        }
        if (type != that.type) {
            return false;
        }
        if (serverDescriptions.size() != that.serverDescriptions.size()) {
            return false;
        }

        if (!serverDescriptions.containsAll(that.serverDescriptions)) {
            return false;
        }

        // Compare class equality and message as exceptions rarely override equals
        Class<?> thisExceptionClass = srvResolutionException != null ? srvResolutionException.getClass() : null;
        Class<?> thatExceptionClass = that.srvResolutionException != null ? that.srvResolutionException.getClass() : null;
        if (thisExceptionClass != null ? !thisExceptionClass.equals(thatExceptionClass) : thatExceptionClass != null) {
            return false;
        }

        String thisExceptionMessage = srvResolutionException != null ? srvResolutionException.getMessage() : null;
        String thatExceptionMessage = that.srvResolutionException != null ? that.srvResolutionException.getMessage() : null;
        if (thisExceptionMessage != null ? !thisExceptionMessage.equals(thatExceptionMessage) : thatExceptionMessage != null) {
            return false;
        }

        return true;
    }

    @Override
    public int hashCode() {
        int result = connectionMode.hashCode();
        result = 31 * result + type.hashCode();
        result = 31 * result + (srvResolutionException == null ? 0 : srvResolutionException.hashCode());
        result = 31 * result + serverDescriptions.hashCode();
        return result;
    }

    @Override
    public String toString() {
        return "ClusterDescription{"
               + "type=" + getType()
               + (srvResolutionException == null ? "" : ", srvResolutionException=" + srvResolutionException)
               + ", connectionMode=" + connectionMode
               + ", serverDescriptions=" + serverDescriptions
               + '}';
    }

    /**
     * Returns a short, pretty description for this ClusterDescription.
     *
     * @return a String describing this cluster.
     */
    public String getShortDescription() {
        StringBuilder serverDescriptions = new StringBuilder();
        String delimiter = "";
        for (final ServerDescription cur : this.serverDescriptions) {
            serverDescriptions.append(delimiter).append(cur.getShortDescription());
            delimiter = ", ";
        }
        if (srvResolutionException == null) {
            return format("{type=%s, servers=[%s]", type, serverDescriptions);
        }  else {
            return format("{type=%s, srvResolutionException=%s, servers=[%s]", type, srvResolutionException, serverDescriptions);
        }
    }

    private interface Predicate {
        boolean apply(ServerDescription serverDescription);
    }

    private List<ServerDescription> getServersByPredicate(final Predicate predicate) {
        List<ServerDescription> membersByTag = new ArrayList<ServerDescription>();

        for (final ServerDescription cur : serverDescriptions) {
            if (predicate.apply(cur)) {
                membersByTag.add(cur);
            }
        }

        return membersByTag;
    }
}