/*
 * 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.async.client.gridfs.helpers;

import com.mongodb.MongoGridFSException;
import com.mongodb.async.SingleResultCallback;
import com.mongodb.async.client.gridfs.AsyncInputStream;
import com.mongodb.async.client.gridfs.AsyncOutputStream;

import java.io.InputStream;
import java.io.OutputStream;
import java.nio.Buffer;
import java.nio.ByteBuffer;

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

/**
 * A general helper class that creates {@link AsyncInputStream} or {@link AsyncOutputStream} instances.
 *
 * Provides support for:
 *

 *
{@code byte[]} - Converts byte arrays into Async Streams

 *
{@link ByteBuffer} - Converts ByteBuffers into Async Streams

 *
{@link InputStream} - Converts InputStreams into Async Streams (Note: InputStream implementations are blocking)

 *
{@link OutputStream} - Converts OutputStreams into Async Streams (Note: OutputStream implementations are blocking)

 *

 *
 * @since 3.3
 * @deprecated Prefer the Reactive Streams-based asynchronous driver (mongodb-driver-reactivestreams artifactId)
 */
@Deprecated
public final class AsyncStreamHelper {

    /**
     * Converts a {@code byte[]} into a {@link AsyncInputStream}
     *
     * @param srcBytes the data source
     * @return the AsyncInputStream
     */
    public static AsyncInputStream toAsyncInputStream(final byte[] srcBytes) {
        return toAsyncInputStream(ByteBuffer.wrap(srcBytes));
    }

    /**
     * Converts a {@code byte[]} into a {@link AsyncOutputStream}
     *
     * @param dstBytes the data destination
     * @return the AsyncOutputStream
     */
    public static AsyncOutputStream toAsyncOutputStream(final byte[] dstBytes) {
        return toAsyncOutputStream(ByteBuffer.wrap(dstBytes));
    }

    /**
     * Converts a {@link ByteBuffer} into a {@link AsyncInputStream}
     *
     * @param srcByteBuffer the data source
     * @return the AsyncInputStream
     */
    public static AsyncInputStream toAsyncInputStream(final ByteBuffer srcByteBuffer) {
        notNull("srcByteBuffer", srcByteBuffer);
        return new AsyncInputStream() {
            @Override
            public void read(final ByteBuffer dstByteBuffer, final SingleResultCallback<Integer> callback) {
                transferDataFromByteBuffers(srcByteBuffer, dstByteBuffer, callback);
            }

            @Override
            public void skip(final long bytesToSkip, final SingleResultCallback<Long> callback) {
                notNull("callback", callback);
                callback.onResult(0L, null);
            }

            @Override
            public void close(final SingleResultCallback<Void> callback) {
                callback.onResult(null, null);
            }
        };
    }

    /**
     * Converts a {@link ByteBuffer} into a {@link AsyncOutputStream}
     *
     * @param dstByteBuffer the data destination
     * @return the AsyncOutputStream
     */
    public static AsyncOutputStream toAsyncOutputStream(final ByteBuffer dstByteBuffer) {
        notNull("dstByteBuffer", dstByteBuffer);
        return new AsyncOutputStream() {
            @Override
            public void write(final ByteBuffer srcByteBuffer, final SingleResultCallback<Integer> callback) {
                transferDataFromByteBuffers(srcByteBuffer, dstByteBuffer, callback);
            }

            @Override
            public void close(final SingleResultCallback<Void> callback) {
                callback.onResult(null, null);
            }
        };
    }

    /**
     * Converts a {@link InputStream} into a {@link AsyncInputStream}
     *
     * @param inputStream the InputStream
     * @return the AsyncInputStream
     */
    public static AsyncInputStream toAsyncInputStream(final InputStream inputStream) {
        notNull("inputStream", inputStream);
        return new AsyncInputStream() {
            @Override
            public void read(final ByteBuffer dstByteBuffer, final SingleResultCallback<Integer> callback) {
                notNull("dst", dstByteBuffer);
                notNull("callback", callback);
                if (!dstByteBuffer.hasRemaining()) {
                    callback.onResult(-1, null);
                    return;
                }

                int maxAmount = dstByteBuffer.remaining();
                byte[] bytes = new byte[maxAmount];
                int amountRead;
                try {
                    amountRead = inputStream.read(bytes);
                } catch (Throwable t) {
                    callback.onResult(null, new MongoGridFSException("Error reading from input stream", t));
                    return;
                }

                if (amountRead > 0) {
                    if (amountRead < maxAmount) {
                        byte[] dataRead = new byte[amountRead];
                        System.arraycopy(bytes, 0, dataRead, 0, amountRead);
                        dstByteBuffer.put(dataRead);
                    } else {
                        dstByteBuffer.put(bytes);
                    }
                }
                callback.onResult(amountRead, null);
            }

            @Override
            public void skip(final long bytesToSkip, final SingleResultCallback<Long> callback) {
                notNull("callback", callback);
                try {
                    long skipped = inputStream.skip(bytesToSkip);
                    callback.onResult(skipped, null);
                } catch (Throwable t) {
                    callback.onResult(null, t);
                }
            }

            @Override
            public void close(final SingleResultCallback<Void> callback) {
                try {
                    inputStream.close();
                } catch (Throwable t) {
                    callback.onResult(null, new MongoGridFSException("Error closing input stream", t));
                    return;
                }
                callback.onResult(null, null);
            }
        };
    }

    /**
     * Converts a {@link OutputStream} into a {@link AsyncOutputStream}
     *
     * @param outputStream the OutputStream
     * @return the AsyncOutputStream
     */
    public static AsyncOutputStream toAsyncOutputStream(final OutputStream outputStream) {
        notNull("outputStream", outputStream);
        return new AsyncOutputStream() {
            @Override
            public void write(final ByteBuffer srcByteBuffer, final SingleResultCallback<Integer> callback) {
                notNull("src", srcByteBuffer);
                notNull("callback", callback);
                if (!srcByteBuffer.hasRemaining()) {
                    callback.onResult(-1, null);
                    return;
                }

                int amount = srcByteBuffer.remaining();
                byte[] bytes = new byte[amount];
                try {
                    srcByteBuffer.get(bytes);
                    outputStream.write(bytes);
                } catch (Throwable t) {
                    callback.onResult(null, new MongoGridFSException("Error reading from input stream", t));
                    return;
                }
                callback.onResult(amount, null);
            }

            @Override
            public void close(final SingleResultCallback<Void> callback) {
                try {
                    outputStream.close();
                } catch (Throwable t) {
                    callback.onResult(null, new MongoGridFSException("Error closing from output stream", t));
                    return;
                }
                callback.onResult(null, null);
            }
        };
    }

    private static void transferDataFromByteBuffers(final ByteBuffer srcByteBuffer, final ByteBuffer dstByteBuffer,
                                                    final SingleResultCallback<Integer> callback) {
        if (!srcByteBuffer.hasRemaining()) {
            callback.onResult(-1, null);
            return;
        }
        int transferAmount = Math.min(dstByteBuffer.remaining(), srcByteBuffer.remaining());
        if (transferAmount > 0) {
            ByteBuffer temp = srcByteBuffer.duplicate();
            ((Buffer) temp).limit(temp.position() + transferAmount);
            dstByteBuffer.put(temp);
            ((Buffer) srcByteBuffer).position(srcByteBuffer.position() + transferAmount);
        }
        callback.onResult(transferAmount, null);
    }

    private AsyncStreamHelper() {
    }
}


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

import com.mongodb.Block;
import com.mongodb.async.SingleResultCallback;
import com.mongodb.async.client.MongoClient;
import com.mongodb.async.client.MongoClients;
import com.mongodb.async.client.MongoCollection;
import com.mongodb.async.client.MongoDatabase;
import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.codecs.pojo.PojoCodecProvider;

import java.util.List;
import java.util.concurrent.CountDownLatch;

import static com.mongodb.client.model.Filters.eq;
import static com.mongodb.client.model.Filters.gt;
import static com.mongodb.client.model.Filters.not;
import static com.mongodb.client.model.Updates.combine;
import static com.mongodb.client.model.Updates.set;
import static java.util.Arrays.asList;
import static org.bson.codecs.configuration.CodecRegistries.fromProviders;
import static org.bson.codecs.configuration.CodecRegistries.fromRegistries;

/**
 * The POJO QuickTour code example
 */
public class PojoQuickTour {
    /**
     * Run this main method to see the output of this quick example.
     *
     * @param args takes an optional single argument for the connection string
     * @throws InterruptedException if a latch is interrupted
     */
    public static void main(final String[] args) throws InterruptedException {
        MongoClient mongoClient;

        if (args.length == 0) {
            // connect to the local database server
            mongoClient = MongoClients.create();
        } else {
            mongoClient = MongoClients.create(args[0]);
        }

        // create codec registry for POJOs
        CodecRegistry pojoCodecRegistry = fromRegistries(MongoClients.getDefaultCodecRegistry(),
                fromProviders(PojoCodecProvider.builder().automatic(true).build()));

        // get handle to "mydb" database
        MongoDatabase database = mongoClient.getDatabase("mydb").withCodecRegistry(pojoCodecRegistry);

        // get a handle to the "people" collection
        final MongoCollection<Person> collection = database.getCollection("people", Person.class);

        // drop all the data in it
        final CountDownLatch dropLatch = new CountDownLatch(1);
        collection.drop(new SingleResultCallback<Void>() {
            @Override
            public void onResult(final Void result, final Throwable t) {
                dropLatch.countDown();
            }
        });
        dropLatch.await();

        // make a document and insert it
        final Person ada = new Person("Ada Byron", 20, new Address("St James Square", "London", "W1"));
        System.out.println("Original Person Model: " + ada);

        collection.insertOne(ada, new SingleResultCallback<Void>() {
            @Override
            public void onResult(final Void result, final Throwable t) {
                // Person will now have an ObjectId
                System.out.println("Mutated Person Model: " + ada);
                System.out.println("Inserted!");
            }
        });

        // get it (since it's the only one in there since we dropped the rest earlier on)
        SingleResultCallback<Person> printCallback = new SingleResultCallback<Person>() {
            @Override
            public void onResult(final Person person, final Throwable t) {
                System.out.println(person);
            }
        };
        collection.find().first(printCallback);

        // now, lets add some more people so we can explore queries and cursors
        List<Person> people = asList(
                new Person("Charles Babbage", 45, new Address("5 Devonshire Street", "London", "W11")),
                new Person("Alan Turing", 28, new Address("Bletchley Hall", "Bletchley Park", "MK12")),
                new Person("Timothy Berners-Lee", 61, new Address("Colehill", "Wimborne", null))
        );

        final CountDownLatch countLatch = new CountDownLatch(1);
        collection.insertMany(people, new SingleResultCallback<Void>() {
            @Override
            public void onResult(final Void result, final Throwable t) {
                collection.countDocuments(new SingleResultCallback<Long>() {
                    @Override
                    public void onResult(final Long count, final Throwable t) {
                        System.out.println("total # of people " + count);
                        countLatch.countDown();
                    }
                });
            }
        });
        countLatch.await();

        System.out.println("");
        // lets get all the documents in the collection and print them out
        Block<Person> printBlock = new Block<Person>() {
            @Override
            public void apply(final Person person) {
                System.out.println(person);
            }
        };
        SingleResultCallback<Void> callbackWhenFinished = new SingleResultCallback<Void>() {
            @Override
            public void onResult(final Void result, final Throwable t) {
                System.out.println("Operation Finished!");
            }
        };
        collection.find().forEach(printBlock, callbackWhenFinished);

        System.out.println("");
        // now use a query to get 1 document out
        collection.find(eq("address.city", "Wimborne")).first(printCallback);

        System.out.println("");
        // now lets find every over 30
        collection.find(gt("age", 30)).forEach(printBlock, callbackWhenFinished);

        System.out.println("");
        // Update One
        SingleResultCallback<UpdateResult> printModifiedCount = new SingleResultCallback<UpdateResult>() {
            @Override
            public void onResult(final UpdateResult result, final Throwable t) {
                System.out.println(result.getModifiedCount());
            }
        };
        collection.updateOne(eq("name", "Ada Byron"), combine(set("age", 23), set("name", "Ada Lovelace")),
                printModifiedCount);

        System.out.println("");
        // Update Many
        collection.updateMany(not(eq("zip", null)), set("zip", null),
                printModifiedCount);

        System.out.println("");
        // Replace One
        collection.replaceOne(eq("name", "Ada Lovelace"), ada, printModifiedCount);

        // Delete One
        SingleResultCallback<DeleteResult> printDeletedCount = new SingleResultCallback<DeleteResult>() {
            @Override
            public void onResult(final DeleteResult result, final Throwable t) {
                System.out.println(result.getDeletedCount());
            }
        };
        collection.deleteOne(eq("address.city", "Wimborne"), printDeletedCount);

        // Delete Many
        collection.deleteMany(eq("address.city", "London"), printDeletedCount);

        // Clean up
        final CountDownLatch deleteLatch = new CountDownLatch(1);
        database.drop(new SingleResultCallback<Void>() {
            @Override
            public void onResult(final Void result, final Throwable t) {
                deleteLatch.countDown();
            }
        });
        deleteLatch.await();

        // release resources
        mongoClient.close();
    }
}


/*
 * 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.BsonDocument;
import org.bson.BsonDocumentWriter;
import org.bson.BsonInt32;
import org.bson.BsonInt64;
import org.bson.BsonString;
import org.bson.BsonValue;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.conversions.Bson;

import java.util.List;
import java.util.Map;

import static com.mongodb.assertions.Assertions.notNull;
import static com.mongodb.client.model.BuildersHelper.encodeValue;
import static java.util.Arrays.asList;

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

 *    collection.updateOne(eq("x", 1), set("x", 2));
 *

 *
 * @since 3.1
 * @mongodb.driver.manual reference/operator/update/ Update Operators
 */
public final class Updates {

    /**
     * Combine a list of updates into a single update.
     *
     * @param updates the list of updates
     * @return a combined update
     */
    public static Bson combine(final Bson... updates) {
        return combine(asList(updates));
    }

    /**
     * Combine a list of updates into a single update.
     *
     * @param updates the list of updates
     * @return a combined update
     */
    public static Bson combine(final List<? extends Bson> updates) {
        notNull("updates", updates);
        return new CompositeUpdate(updates);
    }

    /**
     * Creates an update that sets the value of the field with the given name to the given value.
     *
     * @param fieldName the non-null field name
     * @param value     the value, which may be null
     * @param <TItem>   the value type
     * @return the update
     * @mongodb.driver.manual reference/operator/update/set/ $set
     */
    public static <TItem> Bson set(final String fieldName, @Nullable final TItem value) {
        return new SimpleUpdate<TItem>(fieldName, value, "$set");
    }

    /**
     * Creates an update that deletes the field with the given name.
     *
     * @param fieldName the non-null field name
     * @return the update
     * @mongodb.driver.manual reference/operator/update/unset/ $unset
     */
    public static Bson unset(final String fieldName) {
        return new SimpleUpdate<String>(fieldName, "", "$unset");
    }

    /**
     * Creates an update that sets the values for the document, but only if the update is an upsert that results in an insert of a document.
     *
     * @param value     the value
     * @return the update
     * @mongodb.driver.manual reference/operator/update/setOnInsert/ $setOnInsert
     * @since 3.10.0
     * @see UpdateOptions#upsert(boolean)
     */
    public static Bson setOnInsert(final Bson value) {
        return new SimpleBsonKeyValue("$setOnInsert", value);
    }

    /**
     * Creates an update that sets the value of the field with the given name to the given value, but only if the update is an upsert that
     * results in an insert of a document.
     *
     * @param fieldName the non-null field name
     * @param value     the value, which may be null
     * @param <TItem>   the value type
     * @return the update
     * @mongodb.driver.manual reference/operator/update/setOnInsert/ $setOnInsert
     * @see UpdateOptions#upsert(boolean)
     */
    public static <TItem> Bson setOnInsert(final String fieldName, @Nullable final TItem value) {
        return new SimpleUpdate<TItem>(fieldName, value, "$setOnInsert");
    }

    /**
     * Creates an update that renames a field.
     *
     * @param fieldName    the non-null field name
     * @param newFieldName the non-null new field name
     * @return the update
     * @mongodb.driver.manual reference/operator/update/rename/ $rename
     */
    public static Bson rename(final String fieldName, final String newFieldName) {
        notNull("newFieldName", newFieldName);
        return new SimpleUpdate<String>(fieldName, newFieldName, "$rename");
    }

    /**
     * Creates an update that increments the value of the field with the given name by the given value.
     *
     * @param fieldName the non-null field name
     * @param number    the value
     * @return the update
     * @mongodb.driver.manual reference/operator/update/inc/ $inc
     */
    public static Bson inc(final String fieldName, final Number number) {
        notNull("number", number);
        return new SimpleUpdate<Number>(fieldName, number, "$inc");
    }

    /**
     * Creates an update that multiplies the value of the field with the given name by the given number.
     *
     * @param fieldName the non-null field name
     * @param number    the non-null number
     * @return the update
     * @mongodb.driver.manual reference/operator/update/mul/ $mul
     */
    public static Bson mul(final String fieldName, final Number number) {
        notNull("number", number);
        return new SimpleUpdate<Number>(fieldName, number, "$mul");
    }

    /**
     * Creates an update that sets the value of the field to the given value if the given value is less than the current value of the
     * field.
     *
     * @param fieldName the non-null field name
     * @param value     the value
     * @param <TItem>   the value type
     * @return the update
     * @mongodb.driver.manual reference/operator/update/min/ $min
     */
    public static <TItem> Bson min(final String fieldName, final TItem value) {
        return new SimpleUpdate<TItem>(fieldName, value, "$min");
    }

    /**
     * Creates an update that sets the value of the field to the given value if the given value is greater than the current value of the
     * field.
     *
     * @param fieldName the non-null field name
     * @param value     the value
     * @param <TItem>   the value type
     * @return the update
     * @mongodb.driver.manual reference/operator/update/min/ $min
     */
    public static <TItem> Bson max(final String fieldName, final TItem value) {
        return new SimpleUpdate<TItem>(fieldName, value, "$max");
    }

    /**
     * Creates an update that sets the value of the field to the current date as a BSON date.
     *
     * @param fieldName the non-null field name
     * @return the update
     * @mongodb.driver.manual reference/operator/update/currentDate/ $currentDate
     * @mongodb.driver.manual reference/bson-types/#date Date
     */
    public static Bson currentDate(final String fieldName) {
        return new SimpleUpdate<Boolean>(fieldName, true, "$currentDate");
    }

    /**
     * Creates an update that sets the value of the field to the current date as a BSON timestamp.
     *
     * @param fieldName the non-null field name
     * @return the update
     * @mongodb.driver.manual reference/operator/update/currentDate/ $currentDate
     * @mongodb.driver.manual reference/bson-types/#document-bson-type-timestamp Timestamp
     */
    public static Bson currentTimestamp(final String fieldName) {
        return new SimpleUpdate<BsonDocument>(fieldName, new BsonDocument("$type", new BsonString("timestamp")), "$currentDate");
    }

    /**
     * Creates an update that adds the given value to the array value of the field with the given name, unless the value is
     * already present, in which case it does nothing
     *
     * @param fieldName the non-null field name
     * @param value     the value, which may be null
     * @param <TItem>   the value type
     * @return the update
     * @mongodb.driver.manual reference/operator/update/addToSet/ $addToSet
     */
    public static <TItem> Bson addToSet(final String fieldName, @Nullable final TItem value) {
        return new SimpleUpdate<TItem>(fieldName, value, "$addToSet");
    }

    /**
     * Creates an update that adds each of the given values to the array value of the field with the given name, unless the value is
     * already present, in which case it does nothing
     *
     * @param fieldName the non-null field name
     * @param values    the values
     * @param <TItem>   the value type
     * @return the update
     * @mongodb.driver.manual reference/operator/update/addToSet/ $addToSet
     */
    public static <TItem> Bson addEachToSet(final String fieldName, final List<TItem> values) {
        return new WithEachUpdate<TItem>(fieldName, values, "$addToSet");
    }

    /**
     * Creates an update that adds the given value to the array value of the field with the given name.
     *
     * @param fieldName the non-null field name
     * @param value     the value, which may be null
     * @param <TItem>   the value type
     * @return the update
     * @mongodb.driver.manual reference/operator/update/push/ $push
     */
    public static <TItem> Bson push(final String fieldName, @Nullable final TItem value) {
        return new SimpleUpdate<TItem>(fieldName, value, "$push");
    }

    /**
     * Creates an update that adds each of the given values to the array value of the field with the given name.
     *
     * @param fieldName the non-null field name
     * @param values    the values
     * @param <TItem>   the value type
     * @return the update
     * @mongodb.driver.manual reference/operator/update/push/ $push
     */
    public static <TItem> Bson pushEach(final String fieldName, final List<TItem> values) {
        return new PushUpdate<TItem>(fieldName, values, new PushOptions());
    }

    /**
     * Creates an update that adds each of the given values to the array value of the field with the given name, applying the given
     * options for positioning the pushed values, and then slicing and/or sorting the array.
     *
     * @param fieldName the non-null field name
     * @param values    the values
     * @param options   the non-null push options
     * @param <TItem>   the value type
     * @return the update
     * @mongodb.driver.manual reference/operator/update/push/ $push
     */
    public static <TItem> Bson pushEach(final String fieldName, final List<TItem> values, final PushOptions options) {
        return new PushUpdate<TItem>(fieldName, values, options);
    }

    /**
     * Creates an update that removes all instances of the given value from the array value of the field with the given name.
     *
     * @param fieldName the non-null field name
     * @param value     the value, which may be null
     * @param <TItem>   the value type
     * @return the update
     * @mongodb.driver.manual reference/operator/update/pull/ $pull
     */
    public static <TItem> Bson pull(final String fieldName, @Nullable final TItem value) {
        return new SimpleUpdate<TItem>(fieldName, value, "$pull");
    }

    /**
     * Creates an update that removes from an array all elements that match the given filter.
     *
     * @param filter the query filter
     * @return the update
     * @mongodb.driver.manual reference/operator/update/pull/ $pull
     */
    public static Bson pullByFilter(final Bson filter) {
        return new Bson() {
            @Override
            public <TDocument> BsonDocument toBsonDocument(final Class<TDocument> tDocumentClass, final CodecRegistry codecRegistry) {
                BsonDocumentWriter writer = new BsonDocumentWriter(new BsonDocument());

                writer.writeStartDocument();
                writer.writeName("$pull");

                encodeValue(writer, filter, codecRegistry);

                writer.writeEndDocument();

                return writer.getDocument();
            }
        };
    }

    /**
     * Creates an update that removes all instances of the given values from the array value of the field with the given name.
     *
     * @param fieldName the non-null field name
     * @param values    the values
     * @param <TItem>   the value type
     * @return the update
     * @mongodb.driver.manual reference/operator/update/pull/ $pull
     */
    public static <TItem> Bson pullAll(final String fieldName, final List<TItem> values) {
        return new PullAllUpdate<TItem>(fieldName, values);
    }

    /**
     * Creates an update that pops the first element of an array that is the value of the field with the given name.
     *
     * @param fieldName the non-null field name
     * @return the update
     * @mongodb.driver.manual reference/operator/update/pop/ $pop
     */
    public static Bson popFirst(final String fieldName) {
        return new SimpleUpdate<Integer>(fieldName, -1, "$pop");
    }

    /**
     * Creates an update that pops the last element of an array that is the value of the field with the given name.
     *
     * @param fieldName the non-null field name
     * @return the update
     * @mongodb.driver.manual reference/operator/update/pop/ $pop
     */
    public static Bson popLast(final String fieldName) {
        return new SimpleUpdate<Integer>(fieldName, 1, "$pop");
    }

    /**
     * Creates an update that performs a bitwise and between the given integer value and the integral value of the field with the given
     * name.
     *
     * @param fieldName the field name
     * @param value     the value
     * @return the update
     */
    public static Bson bitwiseAnd(final String fieldName, final int value) {
        return createBitUpdateDocument(fieldName, "and", value);
    }

    /**
     * Creates an update that performs a bitwise and between the given long value and the integral value of the field with the given name.
     *
     * @param fieldName the field name
     * @param value     the value
     * @return the update
     * @mongodb.driver.manual reference/operator/update/bit/ $bit
     */
    public static Bson bitwiseAnd(final String fieldName, final long value) {
        return createBitUpdateDocument(fieldName, "and", value);
    }

    /**
     * Creates an update that performs a bitwise or between the given integer value and the integral value of the field with the given
     * name.
     *
     * @param fieldName the field name
     * @param value     the value
     * @return the update
     * @mongodb.driver.manual reference/operator/update/bit/ $bit
     */
    public static Bson bitwiseOr(final String fieldName, final int value) {
        return createBitUpdateDocument(fieldName, "or", value);
    }

    /**
     * Creates an update that performs a bitwise or between the given long value and the integral value of the field with the given name.
     *
     * @param fieldName the field name
     * @param value     the value
     * @return the update
     * @mongodb.driver.manual reference/operator/update/bit/ $bit
     */
    public static Bson bitwiseOr(final String fieldName, final long value) {
        return createBitUpdateDocument(fieldName, "or", value);
    }

    /**
     * Creates an update that performs a bitwise xor between the given integer value and the integral value of the field with the given
     * name.
     *
     * @param fieldName the field name
     * @param value     the value
     * @return the update
     */
    public static Bson bitwiseXor(final String fieldName, final int value) {
        return createBitUpdateDocument(fieldName, "xor", value);
    }

    /**
     * Creates an update that performs a bitwise xor between the given long value and the integral value of the field with the given name.
     *
     * @param fieldName the field name
     * @param value     the value
     * @return the update
     */
    public static Bson bitwiseXor(final String fieldName, final long value) {
        return createBitUpdateDocument(fieldName, "xor", value);
    }

    private static Bson createBitUpdateDocument(final String fieldName, final String bitwiseOperator, final int value) {
        return createBitUpdateDocument(fieldName, bitwiseOperator, new BsonInt32(value));
    }

    private static Bson createBitUpdateDocument(final String fieldName, final String bitwiseOperator, final long value) {
        return createBitUpdateDocument(fieldName, bitwiseOperator, new BsonInt64(value));
    }

    private static Bson createBitUpdateDocument(final String fieldName, final String bitwiseOperator, final BsonValue value) {
        return new BsonDocument("$bit", new BsonDocument(fieldName, new BsonDocument(bitwiseOperator, value)));
    }

    private static class SimpleBsonKeyValue implements Bson {
        private final String fieldName;
        private final Bson value;

        SimpleBsonKeyValue(final String fieldName, final Bson value) {
            this.fieldName = notNull("fieldName", fieldName);
            this.value = notNull("value", value);
        }

        @Override
        public <TDocument> BsonDocument toBsonDocument(final Class<TDocument> tDocumentClass, final CodecRegistry codecRegistry) {
            BsonDocumentWriter writer = new BsonDocumentWriter(new BsonDocument());
            writer.writeStartDocument();
            writer.writeName(fieldName);
            encodeValue(writer, value, codecRegistry);
            writer.writeEndDocument();

            return writer.getDocument();
        }

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

            SimpleBsonKeyValue that = (SimpleBsonKeyValue) o;

            if (!fieldName.equals(that.fieldName)) {
                return false;
            }
            return value.equals(that.value);
        }

        @Override
        public int hashCode() {
            int result = fieldName.hashCode();
            result = 31 * result + value.hashCode();
            return result;
        }

        @Override
        public String toString() {
            return "SimpleBsonKeyValue{"
                    + "fieldName='" + fieldName + '\''
                    + ", value=" + value
                    + '}';
        }
    }

    private static class SimpleUpdate<TItem> implements Bson {
        private final String fieldName;
        private final TItem value;
        private final String operator;

        SimpleUpdate(final String fieldName, final TItem value, final String operator) {
            this.fieldName = notNull("fieldName", fieldName);
            this.value = value;
            this.operator = operator;
        }

        @Override
        public <TDocument> BsonDocument toBsonDocument(final Class<TDocument> tDocumentClass, final CodecRegistry codecRegistry) {
            BsonDocumentWriter writer = new BsonDocumentWriter(new BsonDocument());

            writer.writeStartDocument();
            writer.writeName(operator);

            writer.writeStartDocument();
            writer.writeName(fieldName);
            encodeValue(writer, value, codecRegistry);
            writer.writeEndDocument();

            writer.writeEndDocument();

            return writer.getDocument();
        }

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

            SimpleUpdate<?> that = (SimpleUpdate<?>) o;

            if (!fieldName.equals(that.fieldName)) {
                return false;
            }
            if (value != null ? !value.equals(that.value) : that.value != null) {
                return false;
            }
            return operator != null ? operator.equals(that.operator) : that.operator == null;
        }

        @Override
        public int hashCode() {
            int result = fieldName.hashCode();
            result = 31 * result + (value != null ? value.hashCode() : 0);
            result = 31 * result + (operator != null ? operator.hashCode() : 0);
            return result;
        }

        @Override
        public String toString() {
            return "Update{"
                           + "fieldName='" + fieldName + '\''
                           + ", operator='" + operator + '\''
                           + ", value=" + value
                           + '}';
        }
    }

    private static class WithEachUpdate<TItem> implements Bson {
        private final String fieldName;
        private final List<TItem> values;
        private final String operator;

        WithEachUpdate(final String fieldName, final List<TItem> values, final String operator) {
            this.fieldName = notNull("fieldName", fieldName);
            this.values = notNull("values", values);
            this.operator = operator;
        }

        @Override
        public <TDocument> BsonDocument toBsonDocument(final Class<TDocument> tDocumentClass, final CodecRegistry codecRegistry) {
            BsonDocumentWriter writer = new BsonDocumentWriter(new BsonDocument());

            writer.writeStartDocument();
            writer.writeName(operator);

            writer.writeStartDocument();
            writer.writeName(fieldName);
            writer.writeStartDocument();

            writer.writeStartArray("$each");
            for (TItem value : values) {
                encodeValue(writer, value, codecRegistry);
            }
            writer.writeEndArray();

            writeAdditionalFields(writer, tDocumentClass, codecRegistry);

            writer.writeEndDocument();

            writer.writeEndDocument();

            writer.writeEndDocument();

            return writer.getDocument();
        }

        protected <TDocument> void writeAdditionalFields(final BsonDocumentWriter writer, final Class<TDocument> tDocumentClass,
                                                         final CodecRegistry codecRegistry) {
        }

        protected String additionalFieldsToString() {
            return "";
        }

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

            WithEachUpdate<?> that = (WithEachUpdate<?>) o;

            if (!fieldName.equals(that.fieldName)) {
                return false;
            }
            if (!values.equals(that.values)) {
                return false;
            }
            return operator != null ? operator.equals(that.operator) : that.operator == null;
        }

        @Override
        public int hashCode() {
            int result = fieldName.hashCode();
            result = 31 * result + values.hashCode();
            result = 31 * result + (operator != null ? operator.hashCode() : 0);
            return result;
        }

        @Override
        public String toString() {
            return "Each Update{"
                           + "fieldName='" + fieldName + '\''
                           + ", operator='" + operator + '\''
                           + ", values=" + values
                           + additionalFieldsToString()
                           + '}';
        }
    }

    private static class PushUpdate<TItem> extends WithEachUpdate<TItem> {

        private final PushOptions options;

        PushUpdate(final String fieldName, final List<TItem> values, final PushOptions options) {
            super(fieldName, values, "$push");
            this.options = notNull("options", options);
        }

        @Override
        protected <TDocument> void writeAdditionalFields(final BsonDocumentWriter writer, final Class<TDocument> tDocumentClass,
                                                         final CodecRegistry codecRegistry) {
            Integer position = options.getPosition();
            if (position != null) {
                writer.writeInt32("$position", position);
            }
            Integer slice = options.getSlice();
            if (slice != null) {
                writer.writeInt32("$slice", slice);
            }
            Integer sort = options.getSort();
            if (sort != null) {
                writer.writeInt32("$sort", sort);
            } else {
                Bson sortDocument = options.getSortDocument();
                if (sortDocument != null) {
                    writer.writeName("$sort");
                    encodeValue(writer, sortDocument, codecRegistry);
                }
            }
        }

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

            PushUpdate<?> that = (PushUpdate<?>) o;

            return options.equals(that.options);
        }

        @Override
        public int hashCode() {
            int result = super.hashCode();
            result = 31 * result + options.hashCode();
            return result;
        }

        @Override
        protected String additionalFieldsToString() {
            return ", options=" + options;
        }
    }

    private static class PullAllUpdate<TItem> implements Bson {
        private final String fieldName;
        private final List<TItem> values;

        PullAllUpdate(final String fieldName, final List<TItem> values) {
            this.fieldName = notNull("fieldName", fieldName);
            this.values = notNull("values", values);
        }

        @Override
        public <TDocument> BsonDocument toBsonDocument(final Class<TDocument> tDocumentClass, final CodecRegistry codecRegistry) {
            BsonDocumentWriter writer = new BsonDocumentWriter(new BsonDocument());

            writer.writeStartDocument();
            writer.writeName("$pullAll");

            writer.writeStartDocument();
            writer.writeName(fieldName);

            writer.writeStartArray();
            for (TItem value : values) {
                encodeValue(writer, value, codecRegistry);
            }
            writer.writeEndArray();

            writer.writeEndDocument();

            writer.writeEndDocument();

            return writer.getDocument();
        }

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

            PullAllUpdate<?> that = (PullAllUpdate<?>) o;

            if (!fieldName.equals(that.fieldName)) {
                return false;
            }
            return values.equals(that.values);
        }

        @Override
        public int hashCode() {
            int result = fieldName.hashCode();
            result = 31 * result + values.hashCode();
            return result;
        }

        @Override
        public String toString() {
            return "Update{"
                           + "fieldName='" + fieldName + '\''
                           + ", operator='$pullAll'"
                           + ", value=" + values
                           + '}';
        }
    }

    private static class CompositeUpdate implements Bson {
        private final List<? extends Bson> updates;

        CompositeUpdate(final List<? extends Bson> updates) {
            this.updates = updates;
        }

        @Override
        public <TDocument> BsonDocument toBsonDocument(final Class<TDocument> tDocumentClass, final CodecRegistry codecRegistry) {
            BsonDocument document = new BsonDocument();

            for (Bson update : updates) {
                BsonDocument rendered = update.toBsonDocument(tDocumentClass, codecRegistry);
                for (Map.Entry<String, BsonValue> element : rendered.entrySet()) {
                    if (document.containsKey(element.getKey())) {
                        BsonDocument currentOperatorDocument = (BsonDocument) element.getValue();
                        BsonDocument existingOperatorDocument = document.getDocument(element.getKey());
                        for (Map.Entry<String, BsonValue> currentOperationDocumentElements : currentOperatorDocument.entrySet()) {
                            existingOperatorDocument.append(currentOperationDocumentElements.getKey(),
                                    currentOperationDocumentElements.getValue());
                        }
                    } else {
                        document.append(element.getKey(), element.getValue());
                    }
                }
            }

            return document;
        }

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

            CompositeUpdate that = (CompositeUpdate) o;

            return updates != null ? updates.equals(that.updates) : that.updates == null;
        }

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

        @Override
        public String toString() {
            return "Updates{"
                           + "updates=" + updates
                           + '}';
        }
    }

    private Updates() {
    }
}


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

import org.bson.io.BsonOutput;

/**
 * Base class for OP_QUERY messages.
 *
 * @mongodb.driver.manual ../meta-driver/latest/legacy/mongodb-wire-protocol/#op-query OP_QUERY
 */
abstract class BaseQueryMessage extends LegacyMessage {
    private final int skip;
    private final int numberToReturn;
    private boolean tailableCursor;
    private boolean slaveOk;
    private boolean oplogReplay;
    private boolean noCursorTimeout;
    private boolean awaitData;
    private boolean partial;

    BaseQueryMessage(final String collectionName, final int skip, final int numberToReturn, final MessageSettings settings) {
        super(collectionName, OpCode.OP_QUERY, settings);
        this.skip = skip;
        this.numberToReturn = numberToReturn;
    }

    /**
     * Gets whether the cursor is configured to be a tailable cursor.
     *
     *
Tailable means the cursor is not closed when the last data is retrieved. Rather, the cursor marks the final object's position. You
     * can resume using the cursor later, from where it was located, if more data were received. Like any "latent cursor",
     * the cursor may become invalid at some point - for example if the final object it references were deleted.

     *
     * @return true if the cursor is configured to be a tailable cursor
     * @mongodb.driver.manual ../meta-driver/latest/legacy/mongodb-wire-protocol/#op-query OP_QUERY
     */
    public boolean isTailableCursor() {
        return tailableCursor;
    }

    /**
     * Sets whether the cursor should be a tailable cursor.
     *
     *
Tailable means the cursor is not closed when the last data is retrieved. Rather, the cursor marks the final object's position. You
     * can resume using the cursor later, from where it was located, if more data were received. Like any "latent cursor",
     * the cursor may become invalid at some point - for example if the final object it references were deleted.

     *
     * @param tailableCursor whether the cursor should be a tailable cursor.
     * @return this
     * @mongodb.driver.manual ../meta-driver/latest/legacy/mongodb-wire-protocol/#op-query OP_QUERY
     */
    public BaseQueryMessage tailableCursor(final boolean tailableCursor) {
        this.tailableCursor = tailableCursor;
        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 BaseQueryMessage 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 BaseQueryMessage 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 BaseQueryMessage noCursorTimeout(final boolean noCursorTimeout) {
        this.noCursorTimeout = noCursorTimeout;
        return this;
    }

    /**
     * Returns true if the cursor should await for data.
     *
     *
Use with {@link #tailableCursor}. If we are at the end of the data, block for a while rather than returning no data. After a
     * timeout period, we do return as normal.

     *
     * @return if the cursor should await for data
     * @mongodb.driver.manual ../meta-driver/latest/legacy/mongodb-wire-protocol/#op-query OP_QUERY
     */
    public boolean isAwaitData() {
        return awaitData;
    }

    /**
     * Sets if the cursor should await for data.
     *
     *
Use with {@link #tailableCursor}. If we are at the end of the data, block for a while rather than returning no data. After a
     * timeout period, we do return as normal.

     *
     * @param awaitData if we should await for data
     * @return this
     * @mongodb.driver.manual ../meta-driver/latest/legacy/mongodb-wire-protocol/#op-query OP_QUERY
     */
    public BaseQueryMessage awaitData(final boolean awaitData) {
        this.awaitData = awaitData;
        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 BaseQueryMessage partial(final boolean partial) {
        this.partial = partial;
        return this;
    }

    private int getCursorFlag() {
        int cursorFlag = 0;
        if (isTailableCursor()) {
            cursorFlag |= 1 << 1;
        }
        if (isSlaveOk()){
            cursorFlag |= 1 << 2;
        }
        if (isOplogReplay()){
            cursorFlag |= 1 << 3;
        }
        if (isNoCursorTimeout()){
            cursorFlag |= 1 << 4;
        }
        if (isAwaitData()){
            cursorFlag |= 1 << 5;
        }
        if (isPartial()) {
            cursorFlag |= 1 << 7;
        }
        return cursorFlag;
    }

    /**
     * Write the query prologue to the given BSON output.
     *
     * @param bsonOutput the BSON output
     */
    protected void writeQueryPrologue(final BsonOutput bsonOutput) {
        bsonOutput.writeInt32(getCursorFlag());
        bsonOutput.writeCString(getCollectionName());
        bsonOutput.writeInt32(skip);
        bsonOutput.writeInt32(numberToReturn);
    }
}


/*
 * 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.annotations.Immutable;
import org.bson.codecs.pojo.annotations.BsonCreator;
import org.bson.codecs.pojo.annotations.BsonIgnore;
import org.bson.codecs.pojo.annotations.BsonProperty;

import java.util.HashSet;
import java.util.Set;

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

/**
 * A MongoDB namespace, which includes a database name and collection name.
 *
 * @since 3.0
 */
@Immutable
public final class MongoNamespace {
    public static final String COMMAND_COLLECTION_NAME = "$cmd";

    private static final Set<Character> PROHIBITED_CHARACTERS_IN_DATABASE_NAME =
            new HashSet<Character>(asList('\0', '/', '\\', ' ', '"', '.'));

    private final String databaseName;
    private final String collectionName;
    @BsonIgnore
    private final String fullName;  // cache to avoid repeated string building

    /**
     * Check the validity of the given database name. A valid database name is non-null, non-empty, and does not contain any of the
     * following characters: {@code '\0', '/', '\\', ' ', '"', '.'}. The server may impose additional restrictions on database names.
     *
     * @param databaseName the database name
     * @throws IllegalArgumentException if the database name is invalid
     * @since 3.4
     * @mongodb.driver.manual reference/limits/#naming-restrictions Naming Restrictions
     */
    public static void checkDatabaseNameValidity(final String databaseName) {
        notNull("databaseName", databaseName);
        isTrueArgument("databaseName is not empty", !databaseName.isEmpty());
        for (int i = 0; i < databaseName.length(); i++) {
            isTrueArgument("databaseName does not contain '" + databaseName.charAt(i) + "'",
                    !PROHIBITED_CHARACTERS_IN_DATABASE_NAME.contains(databaseName.charAt(i)));
        }
    }

    /**
     * Check the validity of the given collection name.   A valid collection name is non-null and non-empty.  The server may impose
     * additional restrictions on collection names.
     *
     * @param collectionName the collection name
     * @throws IllegalArgumentException if the collection name is invalid
     * @since 3.4
     * @mongodb.driver.manual reference/limits/#naming-restrictions Naming Restrictions
     */
    public static void checkCollectionNameValidity(final String collectionName) {
        notNull("collectionName", collectionName);
        isTrueArgument("collectionName is not empty", !collectionName.isEmpty());
    }

    /**
     * Construct an instance for the given full name.  The database name is the string preceding the first {@code "."} character.
     *
     * @param fullName the non-null full namespace
     * @see #checkDatabaseNameValidity(String)
     * @see #checkCollectionNameValidity(String)
     */
    public MongoNamespace(final String fullName) {
        notNull("fullName", fullName);
        this.fullName = fullName;
        this.databaseName = getDatatabaseNameFromFullName(fullName);
        this.collectionName = getCollectionNameFullName(fullName);
        checkDatabaseNameValidity(databaseName);
        checkCollectionNameValidity(collectionName);
    }

    /**
     * Construct an instance from the given database name and collection name.
     *
     * @param databaseName   the valid database name
     * @param collectionName the valid collection name
     * @see #checkDatabaseNameValidity(String)
     * @see #checkCollectionNameValidity(String)
     */
    @BsonCreator
    public MongoNamespace(@BsonProperty("db") final String databaseName,
                          @BsonProperty("coll") final String collectionName) {
        checkDatabaseNameValidity(databaseName);
        checkCollectionNameValidity(collectionName);
        this.databaseName = databaseName;
        this.collectionName = collectionName;
        this.fullName = databaseName + '.' + collectionName;
    }

    /**
     * Gets the database name.
     *
     * @return the database name
     */
    @BsonProperty("db")
    public String getDatabaseName() {
        return databaseName;
    }

    /**
     * Gets the collection name.
     *
     * @return the collection name
     */
    @BsonProperty("coll")
    public String getCollectionName() {
        return collectionName;
    }

    /**
     * Gets the full name, which is the database name and the collection name, separated by a period.
     *
     * @return the full name
     */
    public String getFullName() {
        return fullName;
    }

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

        MongoNamespace that = (MongoNamespace) o;

        if (!collectionName.equals(that.collectionName)) {
            return false;
        }
        if (!databaseName.equals(that.databaseName)) {
            return false;
        }

        return true;
    }

    /**
     * Returns the standard MongoDB representation of a namespace, which is {@code <database>.<collection>}.
     *
     * @return string representation of the namespace.
     */
    @Override
    public String toString() {
        return fullName;
    }

    @Override
    public int hashCode() {
        int result = databaseName.hashCode();
        result = 31 * result + (collectionName.hashCode());
        return result;
    }

    private static String getCollectionNameFullName(final String namespace) {
        int firstDot = namespace.indexOf('.');
        if (firstDot == -1) {
            return namespace;
        }
        return namespace.substring(firstDot + 1);
    }

    private static String getDatatabaseNameFromFullName(final String namespace) {
        int firstDot = namespace.indexOf('.');
        if (firstDot == -1) {
            return "";
        }
        return namespace.substring(0, firstDot);
    }
}


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

import com.mongodb.MongoException;
import com.mongodb.MongoInternalException;
import com.mongodb.MongoInterruptedException;
import com.mongodb.MongoSocketException;
import com.mongodb.MongoSocketReadTimeoutException;
import com.mongodb.MongoTimeoutException;
import com.mongodb.MongoWaitQueueFullException;
import com.mongodb.async.SingleResultCallback;
import com.mongodb.connection.ConnectionDescription;
import com.mongodb.connection.ConnectionId;
import com.mongodb.connection.ConnectionPoolSettings;
import com.mongodb.connection.ServerId;
import com.mongodb.diagnostics.logging.Logger;
import com.mongodb.diagnostics.logging.Loggers;
import com.mongodb.event.ConnectionAddedEvent;
import com.mongodb.event.ConnectionCheckedInEvent;
import com.mongodb.event.ConnectionCheckedOutEvent;
import com.mongodb.event.ConnectionPoolClosedEvent;
import com.mongodb.event.ConnectionPoolListener;
import com.mongodb.event.ConnectionPoolOpenedEvent;
import com.mongodb.event.ConnectionPoolWaitQueueEnteredEvent;
import com.mongodb.event.ConnectionPoolWaitQueueExitedEvent;
import com.mongodb.event.ConnectionRemovedEvent;
import com.mongodb.internal.connection.ConcurrentPool.Prune;
import com.mongodb.internal.thread.DaemonThreadFactory;
import com.mongodb.session.SessionContext;
import org.bson.ByteBuf;
import org.bson.codecs.Decoder;

import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import static com.mongodb.assertions.Assertions.isTrue;
import static com.mongodb.assertions.Assertions.notNull;
import static com.mongodb.internal.async.ErrorHandlingResultCallback.errorHandlingCallback;
import static com.mongodb.internal.event.EventListenerHelper.getConnectionPoolListener;
import static java.lang.String.format;
import static java.util.concurrent.TimeUnit.MILLISECONDS;

class DefaultConnectionPool implements ConnectionPool {
    private static final Logger LOGGER = Loggers.getLogger("connection");

    private final ConcurrentPool<UsageTrackingInternalConnection> pool;
    private final ConnectionPoolSettings settings;
    private final AtomicInteger waitQueueSize = new AtomicInteger(0);
    private final AtomicInteger generation = new AtomicInteger(0);
    private final AtomicInteger lastPrunedGeneration = new AtomicInteger(0);
    private final ScheduledExecutorService sizeMaintenanceTimer;
    private ExecutorService asyncGetter;
    private final Runnable maintenanceTask;
    private final ConnectionPoolListener connectionPoolListener;
    private final ServerId serverId;
    private volatile boolean closed;

    DefaultConnectionPool(final ServerId serverId, final InternalConnectionFactory internalConnectionFactory,
                          final ConnectionPoolSettings settings) {
        this.serverId = notNull("serverId", serverId);
        this.settings = notNull("settings", settings);
        UsageTrackingInternalConnectionItemFactory connectionItemFactory =
                new UsageTrackingInternalConnectionItemFactory(internalConnectionFactory);
        pool = new ConcurrentPool<UsageTrackingInternalConnection>(settings.getMaxSize(), connectionItemFactory);
        this.connectionPoolListener = getConnectionPoolListener(settings);
        maintenanceTask = createMaintenanceTask();
        sizeMaintenanceTimer = createMaintenanceTimer();
        connectionPoolListener.connectionPoolOpened(new ConnectionPoolOpenedEvent(serverId, settings));
    }

    @Override
    public void start() {
        if (sizeMaintenanceTimer != null) {
            sizeMaintenanceTimer.scheduleAtFixedRate(maintenanceTask, settings.getMaintenanceInitialDelay(MILLISECONDS),
                    settings.getMaintenanceFrequency(MILLISECONDS), MILLISECONDS);
        }
    }

    @Override
    public InternalConnection get() {
        return get(settings.getMaxWaitTime(MILLISECONDS), MILLISECONDS);
    }

    @Override
    public InternalConnection get(final long timeout, final TimeUnit timeUnit) {
        try {
            if (waitQueueSize.incrementAndGet() > settings.getMaxWaitQueueSize()) {
                throw createWaitQueueFullException();
            }
            try {
                connectionPoolListener.waitQueueEntered(new ConnectionPoolWaitQueueEnteredEvent(serverId));
                PooledConnection pooledConnection = getPooledConnection(timeout, timeUnit);
                if (!pooledConnection.opened()) {
                    try {
                        pooledConnection.open();
                    } catch (Throwable t) {
                        pool.release(pooledConnection.wrapped, true);
                        if (t instanceof MongoException) {
                            throw (MongoException) t;
                        } else {
                            throw new MongoInternalException(t.toString(), t);
                        }
                    }
                }

                return pooledConnection;
            } finally {
                connectionPoolListener.waitQueueExited(new ConnectionPoolWaitQueueExitedEvent(serverId));
            }
        } finally {
            waitQueueSize.decrementAndGet();
        }
    }

    @Override
    public void getAsync(final SingleResultCallback<InternalConnection> callback) {
        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace(format("Asynchronously getting a connection from the pool for server %s", serverId));
        }

        final SingleResultCallback<InternalConnection> errHandlingCallback = errorHandlingCallback(callback, LOGGER);
        PooledConnection connection = null;

        try {
            connection = getPooledConnection(0, MILLISECONDS);
        } catch (MongoTimeoutException e) {
            // fall through
        } catch (Throwable t) {
            callback.onResult(null, t);
            return;
        }

        if (connection != null) {
            if (LOGGER.isTraceEnabled()) {
                LOGGER.trace(format("Asynchronously opening pooled connection %s to server %s",
                                           connection.getDescription().getConnectionId(), serverId));
            }
            openAsync(connection, errHandlingCallback);
        } else if (waitQueueSize.incrementAndGet() > settings.getMaxWaitQueueSize()) {
            waitQueueSize.decrementAndGet();
            if (LOGGER.isTraceEnabled()) {
                LOGGER.trace(format("Asynchronously failing to get a pooled connection to %s because the wait queue is full",
                                           serverId));
            }
            callback.onResult(null, createWaitQueueFullException());
        } else {
            final long startTimeMillis = System.currentTimeMillis();
            connectionPoolListener.waitQueueEntered(new ConnectionPoolWaitQueueEnteredEvent(serverId));
            getAsyncGetter().submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        if (getRemainingWaitTime() <= 0) {
                            errHandlingCallback.onResult(null, createTimeoutException());
                        } else {
                            PooledConnection connection = getPooledConnection(getRemainingWaitTime(), MILLISECONDS);
                            openAsync(connection, errHandlingCallback);
                        }
                    } catch (Throwable t) {
                        errHandlingCallback.onResult(null, t);
                    } finally {
                        waitQueueSize.decrementAndGet();
                        connectionPoolListener.waitQueueExited(new ConnectionPoolWaitQueueExitedEvent(serverId));
                    }
                }

                private long getRemainingWaitTime() {
                    return startTimeMillis + settings.getMaxWaitTime(MILLISECONDS) - System.currentTimeMillis();
                }
            });
        }
    }

    private void openAsync(final PooledConnection pooledConnection,
                           final SingleResultCallback<InternalConnection> callback) {
        if (pooledConnection.opened()) {
            if (LOGGER.isTraceEnabled()) {
                LOGGER.trace(format("Pooled connection %s to server %s is already open",
                                           pooledConnection.getDescription().getConnectionId(), serverId));
            }
            callback.onResult(pooledConnection, null);
        } else {
            if (LOGGER.isTraceEnabled()) {
                LOGGER.trace(format("Pooled connection %s to server %s is not yet open",
                                           pooledConnection.getDescription().getConnectionId(), serverId));
            }
            pooledConnection.openAsync(new SingleResultCallback<Void>() {
                @Override
                public void onResult(final Void result, final Throwable t) {
                    if (t != null) {
                        if (LOGGER.isTraceEnabled()) {
                            LOGGER.trace(format("Pooled connection %s to server %s failed to open",
                                                       pooledConnection.getDescription().getConnectionId(), serverId));
                        }
                        callback.onResult(null, t);
                        pool.release(pooledConnection.wrapped, true);
                    } else {
                        if (LOGGER.isTraceEnabled()) {
                            LOGGER.trace(format("Pooled connection %s to server %s is now open",
                                                       pooledConnection.getDescription().getConnectionId(), serverId));
                        }
                        callback.onResult(pooledConnection, null);
                    }
                }
            });
        }
    }

    private synchronized ExecutorService getAsyncGetter() {
        if (asyncGetter == null) {
            asyncGetter = Executors.newSingleThreadExecutor(new DaemonThreadFactory("AsyncGetter"));
        }
        return asyncGetter;
    }

    private synchronized void shutdownAsyncGetter() {
        if (asyncGetter != null) {
            asyncGetter.shutdownNow();
        }
    }

    @Override
    public void invalidate() {
        LOGGER.debug("Invalidating the connection pool");
        generation.incrementAndGet();
    }

    @Override
    public void close() {
        if (!closed) {
            pool.close();
            if (sizeMaintenanceTimer != null) {
                sizeMaintenanceTimer.shutdownNow();
            }
            shutdownAsyncGetter();
            closed = true;
            connectionPoolListener.connectionPoolClosed(new ConnectionPoolClosedEvent(serverId));
        }
    }

    /**
     * Synchronously prune idle connections and ensure the minimum pool size.
     */
    public void doMaintenance() {
        if (maintenanceTask != null) {
            maintenanceTask.run();
        }
    }

    private PooledConnection getPooledConnection(final long timeout, final TimeUnit timeUnit) {
        UsageTrackingInternalConnection internalConnection = pool.get(timeout, timeUnit);
        while (shouldPrune(internalConnection)) {
            pool.release(internalConnection, true);
            internalConnection = pool.get(timeout, timeUnit);
        }
        connectionPoolListener.connectionCheckedOut(new ConnectionCheckedOutEvent(internalConnection.getDescription().getConnectionId()));
        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace(format("Checked out connection [%s] to server %s", getId(internalConnection), serverId.getAddress()));
        }
        return new PooledConnection(internalConnection);
    }

    private MongoTimeoutException createTimeoutException() {
        return new MongoTimeoutException(format("Timed out after %d ms while waiting for a connection to server %s.",
                                                settings.getMaxWaitTime(MILLISECONDS), serverId.getAddress()));
    }

    private MongoWaitQueueFullException createWaitQueueFullException() {
        return new MongoWaitQueueFullException(format("Too many threads are already waiting for a connection. "
                                                      + "Max number of threads (maxWaitQueueSize) of %d has been exceeded.",
                                                      settings.getMaxWaitQueueSize()));
    }

    ConcurrentPool<UsageTrackingInternalConnection> getPool() {
        return pool;
    }

    private Runnable createMaintenanceTask() {
        Runnable newMaintenanceTask = null;
        if (shouldPrune() || shouldEnsureMinSize()) {
            newMaintenanceTask = new Runnable() {
                @Override
                public synchronized void run() {
                    try {
                        int curGeneration = generation.get();
                        if (shouldPrune() || curGeneration > lastPrunedGeneration.get()) {
                            if (LOGGER.isDebugEnabled()) {
                                LOGGER.debug(format("Pruning pooled connections to %s", serverId.getAddress()));
                            }
                            pool.prune();
                        }
                        lastPrunedGeneration.set(curGeneration);
                        if (shouldEnsureMinSize()) {
                            if (LOGGER.isDebugEnabled()) {
                                LOGGER.debug(format("Ensuring minimum pooled connections to %s", serverId.getAddress()));
                            }
                            pool.ensureMinSize(settings.getMinSize(), true);
                        }
                    } catch (MongoInterruptedException e) {
                        // don't log interruptions due to the shutdownNow call on the ExecutorService
                    } catch (Exception e) {
                        LOGGER.warn("Exception thrown during connection pool background maintenance task", e);
                    }
                }
            };
        }
        return newMaintenanceTask;
    }

    private ScheduledExecutorService createMaintenanceTimer() {
        if (maintenanceTask == null) {
            return null;
        } else {
            return Executors.newSingleThreadScheduledExecutor(new DaemonThreadFactory("MaintenanceTimer"));
        }
    }

    private boolean shouldEnsureMinSize() {
        return settings.getMinSize() > 0;
    }

    private boolean shouldPrune() {
        return settings.getMaxConnectionIdleTime(MILLISECONDS) > 0 || settings.getMaxConnectionLifeTime(MILLISECONDS) > 0;
    }

    private boolean shouldPrune(final UsageTrackingInternalConnection connection) {
        return fromPreviousGeneration(connection) || pastMaxLifeTime(connection) || pastMaxIdleTime(connection);
    }

    private boolean pastMaxIdleTime(final UsageTrackingInternalConnection connection) {
        return expired(connection.getLastUsedAt(), System.currentTimeMillis(), settings.getMaxConnectionIdleTime(MILLISECONDS));
    }

    private boolean pastMaxLifeTime(final UsageTrackingInternalConnection connection) {
        return expired(connection.getOpenedAt(), System.currentTimeMillis(), settings.getMaxConnectionLifeTime(MILLISECONDS));
    }

    private boolean fromPreviousGeneration(final UsageTrackingInternalConnection connection) {
        return generation.get() > connection.getGeneration();
    }

    private boolean expired(final long startTime, final long curTime, final long maxTime) {
        return maxTime != 0 && curTime - startTime > maxTime;
    }

    /**
     * If there was a socket exception that wasn't some form of interrupted read, increment the generation count so that any connections
     * created prior will be discarded.
     *
     * @param connection the connection that generated the exception
     * @param t          the exception
     */
    private void incrementGenerationOnSocketException(final InternalConnection connection, final Throwable t) {
        if (t instanceof MongoSocketException && !(t instanceof MongoSocketReadTimeoutException)) {
            if (LOGGER.isWarnEnabled()) {
                LOGGER.warn(format("Got socket exception on connection [%s] to %s. All connections to %s will be closed.",
                                   getId(connection), serverId.getAddress(), serverId.getAddress()));
            }
            invalidate();
        }
    }

    private ConnectionId getId(final InternalConnection internalConnection) {
        return internalConnection.getDescription().getConnectionId();
    }

    private class PooledConnection implements InternalConnection {
        private final UsageTrackingInternalConnection wrapped;
        private final AtomicBoolean isClosed = new AtomicBoolean();

        PooledConnection(final UsageTrackingInternalConnection wrapped) {
            this.wrapped = notNull("wrapped", wrapped);
        }

        @Override
        public void open() {
            isTrue("open", !isClosed.get());
            wrapped.open();
        }

        @Override
        public void openAsync(final SingleResultCallback<Void> callback) {
            isTrue("open", !isClosed.get());
            wrapped.openAsync(callback);
        }

        @Override
        public void close() {
            // All but the first call is a no-op
            if (!isClosed.getAndSet(true)) {
                connectionPoolListener.connectionCheckedIn(new ConnectionCheckedInEvent(getId(wrapped)));
                if (LOGGER.isTraceEnabled()) {
                    LOGGER.trace(format("Checked in connection [%s] to server %s", getId(wrapped), serverId.getAddress()));
                }
                pool.release(wrapped, wrapped.isClosed() || shouldPrune(wrapped));
            }
        }

        @Override
        public boolean opened() {
            isTrue("open", !isClosed.get());
            return wrapped.opened();
        }

        @Override
        public boolean isClosed() {
            return isClosed.get() || wrapped.isClosed();
        }

        @Override
        public ByteBuf getBuffer(final int capacity) {
            return wrapped.getBuffer(capacity);
        }

        @Override
        public void sendMessage(final List<ByteBuf> byteBuffers, final int lastRequestId) {
            isTrue("open", !isClosed.get());
            try {
                wrapped.sendMessage(byteBuffers, lastRequestId);
            } catch (MongoException e) {
                incrementGenerationOnSocketException(this, e);
                throw e;
            }
        }

        @Override
        public <T> T sendAndReceive(final CommandMessage message, final Decoder<T> decoder, final SessionContext sessionContext) {
            isTrue("open", !isClosed.get());
            try {
                return wrapped.sendAndReceive(message, decoder, sessionContext);
            } catch (MongoException e) {
                incrementGenerationOnSocketException(this, e);
                throw e;
            }
        }

        @Override
        public <T> void sendAndReceiveAsync(final CommandMessage message, final Decoder<T> decoder,
                                            final SessionContext sessionContext, final SingleResultCallback<T> callback) {
            isTrue("open", !isClosed.get());
            wrapped.sendAndReceiveAsync(message, decoder, sessionContext, new SingleResultCallback<T>() {
                @Override
                public void onResult(final T result, final Throwable t) {
                    if (t != null) {
                        incrementGenerationOnSocketException(PooledConnection.this, t);
                    }
                    callback.onResult(result, t);
                }
            });
        }

        @Override
        public ResponseBuffers receiveMessage(final int responseTo) {
            isTrue("open", !isClosed.get());
            try {
                return wrapped.receiveMessage(responseTo);
            } catch (MongoException e) {
                incrementGenerationOnSocketException(this, e);
                throw e;
            }
        }

        @Override
        public void sendMessageAsync(final List<ByteBuf> byteBuffers, final int lastRequestId, final SingleResultCallback<Void> callback) {
            isTrue("open", !isClosed.get());
            wrapped.sendMessageAsync(byteBuffers, lastRequestId, new SingleResultCallback<Void>() {
                @Override
                public void onResult(final Void result, final Throwable t) {
                    if (t != null) {
                        incrementGenerationOnSocketException(PooledConnection.this, t);
                    }
                    callback.onResult(null, t);
                }
            });
        }

        @Override
        public void receiveMessageAsync(final int responseTo, final SingleResultCallback<ResponseBuffers> callback) {
            isTrue("open", !isClosed.get());
            wrapped.receiveMessageAsync(responseTo, new SingleResultCallback<ResponseBuffers>() {
                @Override
                public void onResult(final ResponseBuffers result, final Throwable t) {
                    if (t != null) {
                        incrementGenerationOnSocketException(PooledConnection.this, t);
                    }
                    callback.onResult(result, t);
                }
            });
        }

        @Override
        public ConnectionDescription getDescription() {
            isTrue("open", !isClosed.get());
            return wrapped.getDescription();
        }
    }

    private class UsageTrackingInternalConnectionItemFactory implements ConcurrentPool.ItemFactory<UsageTrackingInternalConnection> {
        private final InternalConnectionFactory internalConnectionFactory;

        UsageTrackingInternalConnectionItemFactory(final InternalConnectionFactory internalConnectionFactory) {
            this.internalConnectionFactory = internalConnectionFactory;
        }

        @Override
        public UsageTrackingInternalConnection create(final boolean initialize) {
            UsageTrackingInternalConnection internalConnection =
            new UsageTrackingInternalConnection(internalConnectionFactory.create(serverId), generation.get());
            if (initialize) {
                internalConnection.open();
            }
            connectionPoolListener.connectionAdded(new ConnectionAddedEvent(getId(internalConnection)));
            return internalConnection;
        }

        @Override
        public void close(final UsageTrackingInternalConnection connection) {
            connectionPoolListener.connectionRemoved(new ConnectionRemovedEvent(getId(connection), getReasonForClosing(connection)));
            if (LOGGER.isInfoEnabled()) {
                LOGGER.info(format("Closed connection [%s] to %s because %s.", getId(connection), serverId.getAddress(),
                                  getReasonStringForClosing(connection)));
            }
            connection.close();
        }

        private String getReasonStringForClosing(final UsageTrackingInternalConnection connection) {
            String reason;
            if (connection.isClosed()) {
                reason = "there was a socket exception raised by this connection";
            } else if (fromPreviousGeneration(connection)) {
                reason = "there was a socket exception raised on another connection from this pool";
            } else if (pastMaxLifeTime(connection)) {
                reason = "it is past its maximum allowed life time";
            } else if (pastMaxIdleTime(connection)) {
                reason = "it is past its maximum allowed idle time";
            } else {
                reason = "the pool has been closed";
            }
            return reason;
        }

        private ConnectionRemovedEvent.Reason getReasonForClosing(final UsageTrackingInternalConnection connection) {
            ConnectionRemovedEvent.Reason reason;
            if (connection.isClosed()) {
                reason = ConnectionRemovedEvent.Reason.ERROR;
            } else if (fromPreviousGeneration(connection)) {
                reason = ConnectionRemovedEvent.Reason.STALE;
            } else if (pastMaxLifeTime(connection)) {
                reason = ConnectionRemovedEvent.Reason.MAX_LIFE_TIME_EXCEEDED;
            } else if (pastMaxIdleTime(connection)) {
                reason = ConnectionRemovedEvent.Reason.MAX_IDLE_TIME_EXCEEDED;
            } else {
                reason = ConnectionRemovedEvent.Reason.POOL_CLOSED;
            }
            return reason;
        }

        @Override
        public Prune shouldPrune(final UsageTrackingInternalConnection usageTrackingConnection) {
            return DefaultConnectionPool.this.shouldPrune(usageTrackingConnection) ? Prune.YES : Prune.NO;
        }
    }
}


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

import com.mongodb.BasicDBObject;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.DBObject;
import com.mongodb.MongoClient;
import com.mongodb.MongoClientURI;
import com.mongodb.QueryBuilder;
import org.bson.types.Decimal128;

import java.math.BigDecimal;

/**
 * Decimal128 Quick Tour for the Legacy API
 */
@SuppressWarnings("deprecation")
public class Decimal128LegacyAPIQuickTour {
    /**
     * Run this main method to see the output of this quick example.
     *
     * @param args takes an optional single argument for the connection string
     */
    public static void main(final String[] args) {
        MongoClient mongoClient;

        if (args.length == 0) {
            // connect to the local database server
            mongoClient = new MongoClient();
        } else {
            mongoClient = new MongoClient(new MongoClientURI(args[0]));
        }

        // get handle to "mydb" database
        DB database = mongoClient.getDB("mydb");

        // get a handle to the "test" collection
        DBCollection collection = database.getCollection("test");

        // drop all the data in it
        collection.drop();

        // make a document and insert it
        BasicDBObject doc = new BasicDBObject("name", "MongoDB")
                                    .append("amount1", Decimal128.parse(".10"))
                                    .append("amount2", new Decimal128(42L))
                                    .append("amount3", new Decimal128(new BigDecimal(".200")));

        collection.insert(doc);

        DBObject first = collection.findOne(QueryBuilder.start("amount1").is(new Decimal128(new BigDecimal(".10"))).get());

        Decimal128 amount3 = (Decimal128) first.get("amount3");
        BigDecimal amount2AsBigDecimal = amount3.bigDecimalValue();

        System.out.println(amount3.toString());
        System.out.println(amount2AsBigDecimal.toString());

    }
}