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

import java.util.List;

import static com.mongodb.client.model.AggregateOutStageOptions.Mode.REPLACE_COLLECTION;
import static java.util.Arrays.asList;
import static org.bson.assertions.Assertions.notNull;

/**
 * Builders for aggregation pipeline stages.
 *
 * @mongodb.driver.manual core/aggregation-pipeline/ Aggregation pipeline
 * @mongodb.server.release 2.2
 * @since 3.1
 */
public final class Aggregates {

    /**
     * Creates an $addFields pipeline stage
     *
     * @param fields        the fields to add
     * @return the $addFields pipeline stage
     * @mongodb.driver.manual reference/operator/aggregation/addFields/ $addFields
     * @mongodb.server.release 3.4
     * @since 3.4
     */
    public static Bson addFields(final Field<?>... fields) {
        return addFields(asList(fields));
    }

    /**
     * Creates an $addFields pipeline stage
     *
     * @param fields        the fields to add
     * @return the $addFields pipeline stage
     * @mongodb.driver.manual reference/operator/aggregation/addFields/ $addFields
     * @mongodb.server.release 3.4
     * @since 3.4
     */
    public static Bson addFields(final List<Field<?>> fields) {
        return new AddFieldsStage(fields);
    }

    /**
     * Creates a $bucket pipeline stage
     *
     * @param <TExpression> the groupBy expression type
     * @param <Boundary>    the boundary type
     * @param groupBy       the criteria to group By
     * @param boundaries    the boundaries of the buckets
     * @return the $bucket pipeline stage
     * @mongodb.driver.manual reference/operator/aggregation/bucket/ $bucket
     * @mongodb.server.release 3.4
     * @since 3.4
     */
    public static <TExpression, Boundary> Bson bucket(final TExpression groupBy, final List<Boundary> boundaries) {
        return bucket(groupBy, boundaries, new BucketOptions());
    }

    /**
     * Creates a $bucket pipeline stage
     *
     * @param <TExpression> the groupBy expression type
     * @param <TBoundary>    the boundary type
     * @param groupBy       the criteria to group By
     * @param boundaries    the boundaries of the buckets
     * @param options       the optional values for the $bucket stage
     * @return the $bucket pipeline stage
     * @mongodb.driver.manual reference/operator/aggregation/bucket/ $bucket
     * @mongodb.server.release 3.4
     * @since 3.4
     */
    public static <TExpression, TBoundary> Bson bucket(final TExpression groupBy, final List<TBoundary> boundaries,
                                                       final BucketOptions options) {
        return new BucketStage<TExpression, TBoundary>(groupBy, boundaries, options);
    }

    /**
     * Creates a $bucketAuto pipeline stage
     *
     * @param <TExpression> the groupBy expression type
     * @param groupBy       the criteria to group By
     * @param buckets       the number of the buckets
     * @return the $bucketAuto pipeline stage
     * @mongodb.driver.manual reference/operator/aggregation/bucketAuto/ $bucketAuto
     * @mongodb.server.release 3.4
     * @since 3.4
     */
    public static <TExpression> Bson bucketAuto(final TExpression groupBy, final int buckets) {
        return bucketAuto(groupBy, buckets, new BucketAutoOptions());
    }

    /**
     * Creates a $bucketAuto pipeline stage
     *
     * @param <TExpression> the groupBy expression type
     * @param groupBy       the criteria to group By
     * @param buckets       the number of the buckets
     * @param options       the optional values for the $bucketAuto stage
     * @return the $bucketAuto pipeline stage
     * @mongodb.driver.manual reference/operator/aggregation/bucketAuto/ $bucketAuto
     * @mongodb.server.release 3.4
     * @since 3.4
     */
    public static <TExpression> Bson bucketAuto(final TExpression groupBy, final int buckets, final BucketAutoOptions options) {
        return new BucketAutoStage<TExpression>(groupBy, buckets, options);
    }

    /**
     * Creates a $count pipeline stage using the field name "count" to store the result
     *
     * @return the $count pipeline stage
     * @mongodb.driver.manual reference/operator/aggregation/count/ $count
     * @mongodb.server.release 3.4
     * @since 3.4
     */
    public static Bson count() {
        return count("count");
    }

    /**
     * Creates a $count pipeline stage using the named field to store the result
     *
     * @param field the field in which to store the count
     * @return the $count pipeline stage
     * @mongodb.driver.manual reference/operator/aggregation/count/ $count
     * @mongodb.server.release 3.4
     * @since 3.4
     */
    public static Bson count(final String field) {
        return new BsonDocument("$count", new BsonString(field));
    }

    /**
     * Creates a $match pipeline stage for the specified filter
     *
     * @param filter the filter to match
     * @return the $match pipeline stage
     * @see Filters
     * @mongodb.driver.manual reference/operator/aggregation/match/ $match
     */
    public static Bson match(final Bson filter) {
        return new SimplePipelineStage("$match", filter);
    }

    /**
     * Creates a $project pipeline stage for the specified projection
     *
     * @param projection the projection
     * @return the $project pipeline stage
     * @see Projections
     * @mongodb.driver.manual reference/operator/aggregation/project/ $project
     */
    public static Bson project(final Bson projection) {
        return new SimplePipelineStage("$project", projection);
    }

    /**
     * Creates a $sort pipeline stage for the specified sort specification
     *
     * @param sort the sort specification
     * @return the $sort pipeline stage
     * @see Sorts
     * @mongodb.driver.manual reference/operator/aggregation/sort/#sort-aggregation $sort
     */
    public static Bson sort(final Bson sort) {
        return new SimplePipelineStage("$sort", sort);
    }

    /**
     * Creates a $sortByCount pipeline stage for the specified filter
     *
     * @param <TExpression> the expression type
     * @param filter        the filter specification
     * @return the $sortByCount pipeline stage
     * @mongodb.driver.manual reference/operator/aggregation/sortByCount/ $sortByCount
     * @mongodb.server.release 3.4
     * @since 3.4
     */
    public static <TExpression> Bson sortByCount(final TExpression filter) {
        return new SortByCountStage<TExpression>(filter);
    }

    /**
     * Creates a $skip pipeline stage
     *
     * @param skip the number of documents to skip
     * @return the $skip pipeline stage
     * @mongodb.driver.manual reference/operator/aggregation/skip/ $skip
     */
    public static Bson skip(final int skip) {
        return new BsonDocument("$skip", new BsonInt32(skip));
    }

    /**
     * Creates a $limit pipeline stage for the specified filter
     *
     * @param limit the limit
     * @return the $limit pipeline stage
     * @mongodb.driver.manual reference/operator/aggregation/limit/  $limit
     */
    public static Bson limit(final int limit) {
        return new BsonDocument("$limit", new BsonInt32(limit));
    }

    /**
     * Creates a $lookup pipeline stage, joining the current collection with the one specified in from
     * using equality match between the local field and the foreign field
     *
     * @param from         the name of the collection in the same database to perform the join with.
     * @param localField   the field from the local collection to match values against.
     * @param foreignField the field in the from collection to match values against.
     * @param as           the name of the new array field to add to the input documents.
     * @return the $lookup pipeline stage
     * @mongodb.driver.manual reference/operator/aggregation/lookup/ $lookup
     * @mongodb.server.release 3.2
     * @since 3.2
     */
    public static Bson lookup(final String from, final String localField, final String foreignField, final String as) {
        return new BsonDocument("$lookup", new BsonDocument("from", new BsonString(from))
                                                   .append("localField", new BsonString(localField))
                                                   .append("foreignField", new BsonString(foreignField))
                                                   .append("as", new BsonString(as)));
    }

    /**
     * Creates a $lookup pipeline stage, joining the current collection with the one specified in from using the given pipeline
     *
     * @param from          the name of the collection in the same database to perform the join with.
     * @param pipeline      the pipeline to run on the joined collection.
     * @param as            the name of the new array field to add to the input documents.
     * @return the $lookup pipeline stage
     * @mongodb.driver.manual reference/operator/aggregation/lookup/ $lookup
     * @mongodb.server.release 3.6
     * @since 3.7
     *
     */
    public static Bson lookup(final String from, final List<? extends Bson> pipeline, final String as) {
        return lookup(from, null, pipeline, as);
    }

    /**
     * Creates a $lookup pipeline stage, joining the current collection with the one specified in from using the given pipeline
     *
     * @param <TExpression> the Variable value expression type
     * @param from          the name of the collection in the same database to perform the join with.
     * @param let           the variables to use in the pipeline field stages.
     * @param pipeline      the pipeline to run on the joined collection.
     * @param as            the name of the new array field to add to the input documents.
     * @return the $lookup pipeline stage
     * @mongodb.driver.manual reference/operator/aggregation/lookup/ $lookup
     * @mongodb.server.release 3.6
     * @since 3.7
     */
    public static <TExpression> Bson lookup(final String from, @Nullable final List<Variable<TExpression>> let,
                                            final List<? extends Bson> pipeline, final String as) {
       return new LookupStage<TExpression>(from, let, pipeline, as);
    }

    /**
     * Creates a facet pipeline stage
     *
     * @param facets the facets to use
     * @return the new pipeline stage
     * @mongodb.driver.manual reference/operator/aggregation/facet/ $facet
     * @mongodb.server.release 3.4
     * @since 3.4
     */
    public static Bson facet(final List<Facet> facets) {
        return new FacetStage(facets);
    }

    /**
     * Creates a facet pipeline stage
     *
     * @param facets the facets to use
     * @return the new pipeline stage
     * @mongodb.driver.manual reference/operator/aggregation/facet/ $facet
     * @mongodb.server.release 3.4
     * @since 3.4
     */
    public static Bson facet(final Facet... facets) {
        return new FacetStage(asList(facets));
    }

    /**
     * Creates a graphLookup pipeline stage for the specified filter
     *
     * @param <TExpression>     the expression type
     * @param from             the collection to query
     * @param startWith        the expression to start the graph lookup with
     * @param connectFromField the from field
     * @param connectToField   the to field
     * @param as               name of field in output document
     * @return the $graphLookup pipeline stage
     * @mongodb.driver.manual reference/operator/aggregation/graphLookup/ $graphLookup
     * @mongodb.server.release 3.4
     * @since 3.4
     */
    public static <TExpression> Bson graphLookup(final String from, final TExpression startWith, final String connectFromField,
                                                 final String connectToField, final String as) {
        return graphLookup(from, startWith, connectFromField, connectToField, as, new GraphLookupOptions());
    }

    /**
     * Creates a graphLookup pipeline stage for the specified filter
     *
     * @param <TExpression>    the expression type
     * @param from             the collection to query
     * @param startWith        the expression to start the graph lookup with
     * @param connectFromField the from field
     * @param connectToField   the to field
     * @param as               name of field in output document
     * @param options          optional values for the graphLookup
     * @return the $graphLookup pipeline stage
     * @mongodb.driver.manual reference/operator/aggregation/graphLookup/ $graphLookup
     * @mongodb.server.release 3.4
     * @since 3.4
     */
    public static <TExpression> Bson graphLookup(final String from, final TExpression startWith, final String connectFromField,
                                                 final String connectToField, final String as, final GraphLookupOptions options) {
        notNull("options", options);
        return new GraphLookupStage<TExpression>(from, startWith, connectFromField, connectToField, as, options);
    }

    /**
     * Creates a $group pipeline stage for the specified filter
     *
     * @param <TExpression>     the expression type
     * @param id                the id expression for the group, which may be null
     * @param fieldAccumulators zero or more field accumulator pairs
     * @return the $group pipeline stage
     * @mongodb.driver.manual reference/operator/aggregation/group/ $group
     * @mongodb.driver.manual meta/aggregation-quick-reference/#aggregation-expressions Expressions
     */
    public static <TExpression> Bson group(@Nullable final TExpression id, final BsonField... fieldAccumulators) {
        return group(id, asList(fieldAccumulators));
    }

    /**
     * Creates a $group pipeline stage for the specified filter
     *
     * @param <TExpression>     the expression type
     * @param id                the id expression for the group, which may be null
     * @param fieldAccumulators zero or more field accumulator pairs
     * @return the $group pipeline stage
     * @mongodb.driver.manual reference/operator/aggregation/group/ $group
     * @mongodb.driver.manual meta/aggregation-quick-reference/#aggregation-expressions Expressions
     */
    public static <TExpression> Bson group(@Nullable final TExpression id, final List<BsonField> fieldAccumulators) {
        return new GroupStage<TExpression>(id, fieldAccumulators);
    }

    /**
     * Creates a $unwind pipeline stage for the specified field name, which must be prefixed by a {@code '$'} sign.
     *
     * @param fieldName the field name, prefixed by a {@code '$' sign}
     * @return the $unwind pipeline stage
     * @mongodb.driver.manual reference/operator/aggregation/unwind/ $unwind
     */
    public static Bson unwind(final String fieldName) {
        return new BsonDocument("$unwind", new BsonString(fieldName));
    }

    /**
     * Creates a $unwind pipeline stage for the specified field name, which must be prefixed by a {@code '$'} sign.
     *
     * @param fieldName     the field name, prefixed by a {@code '$' sign}
     * @param unwindOptions options for the unwind pipeline stage
     * @return the $unwind pipeline stage
     * @mongodb.driver.manual reference/operator/aggregation/unwind/ $unwind
     * @mongodb.server.release 3.2
     * @since 3.2
     */
    public static Bson unwind(final String fieldName, final UnwindOptions unwindOptions) {
        notNull("unwindOptions", unwindOptions);
        BsonDocument options = new BsonDocument("path", new BsonString(fieldName));
        Boolean preserveNullAndEmptyArrays = unwindOptions.isPreserveNullAndEmptyArrays();
        if (preserveNullAndEmptyArrays != null) {
            options.append("preserveNullAndEmptyArrays", BsonBoolean.valueOf(preserveNullAndEmptyArrays));
        }
        String includeArrayIndex = unwindOptions.getIncludeArrayIndex();
        if (includeArrayIndex != null) {
            options.append("includeArrayIndex", new BsonString(includeArrayIndex));
        }
        return new BsonDocument("$unwind", options);
    }

    /**
     * Creates a $out pipeline stage using the specified collection name
     *
     * @param collectionName the collection name
     * @return the $out pipeline stage
     * @mongodb.driver.manual reference/operator/aggregation/out/  $out
     * @mongodb.server.release 2.6
     */
    public static Bson out(final String collectionName) {
        return out(collectionName, new AggregateOutStageOptions());
    }

    /**
     * Creates a $out pipeline stage using the specified collection name and options
     *
     * @param collectionName the collection name
     * @param options the $out stage options
     * @return the $out pipeline stage
     * @mongodb.driver.manual reference/operator/aggregation/out/  $out
     * @mongodb.server.release 2.6
     */
    public static Bson out(final String collectionName, final AggregateOutStageOptions options) {
        notNull("collectionName", collectionName);
        notNull("options", options);
        return new OutStage(collectionName, options);
    }

    /**
     * Creates a $replaceRoot pipeline stage
     *
     * @param <TExpression> the new root type
     * @param value         the new root value
     * @return the $replaceRoot pipeline stage
     * @mongodb.driver.manual reference/operator/aggregation/replaceRoot/ $replaceRoot
     * @mongodb.server.release 3.4
     * @since 3.4
     */
    public static <TExpression> Bson replaceRoot(final TExpression value) {
        return new ReplaceRootStage<TExpression>(value);
    }

    /**
     * Creates a $sample pipeline stage with the specified sample size
     *
     * @param size the sample size
     * @return the $sample pipeline stage
     * @mongodb.driver.manual reference/operator/aggregation/sample/  $sample
     * @mongodb.server.release 3.2
     * @since 3.2
     */
    public static Bson sample(final int size) {
        return new BsonDocument("$sample", new BsonDocument("size", new BsonInt32(size)));
    }

    static void writeBucketOutput(final CodecRegistry codecRegistry, final BsonDocumentWriter writer,
                                  @Nullable final List<BsonField> output) {
        if (output != null) {
            writer.writeName("output");
            writer.writeStartDocument();
            for (BsonField field : output) {
                writer.writeName(field.getName());
                BuildersHelper.encodeValue(writer, field.getValue(), codecRegistry);
            }
            writer.writeEndDocument();
        }
    }

    private static class SimplePipelineStage implements Bson {
        private final String name;
        private final Bson value;

        SimplePipelineStage(final String name, final Bson value) {
            this.name = name;
            this.value = value;
        }

        @Override
        public <TDocument> BsonDocument toBsonDocument(final Class<TDocument> documentClass, final CodecRegistry codecRegistry) {
            return new BsonDocument(name, value.toBsonDocument(documentClass, codecRegistry));
        }

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

            SimplePipelineStage that = (SimplePipelineStage) o;

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

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

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

    private static final class BucketStage<TExpression, TBoundary> implements Bson {

        private final TExpression groupBy;
        private final List<TBoundary> boundaries;
        private final BucketOptions options;

        BucketStage(final TExpression groupBy, final List<TBoundary> boundaries, final BucketOptions options) {
            notNull("options", options);
            this.groupBy = groupBy;
            this.boundaries = boundaries;
            this.options = options;
        }

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

            writer.writeStartDocument();

            writer.writeStartDocument("$bucket");

            writer.writeName("groupBy");
            BuildersHelper.encodeValue(writer, groupBy, codecRegistry);

            writer.writeStartArray("boundaries");
            for (TBoundary boundary : boundaries) {
                BuildersHelper.encodeValue(writer, boundary, codecRegistry);
            }
            writer.writeEndArray();
            Object defaultBucket = options.getDefaultBucket();
            if (defaultBucket != null) {
                writer.writeName("default");
                BuildersHelper.encodeValue(writer, defaultBucket, codecRegistry);
            }
            writeBucketOutput(codecRegistry, writer, options.getOutput());
            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;
            }

            BucketStage<?, ?> that = (BucketStage<?, ?>) o;

            if (groupBy != null ? !groupBy.equals(that.groupBy) : that.groupBy != null) {
                return false;
            }
            if (boundaries != null ? !boundaries.equals(that.boundaries) : that.boundaries != null) {
                return false;
            }
            return options.equals(that.options);
        }

        @Override
        public int hashCode() {
            int result = groupBy != null ? groupBy.hashCode() : 0;
            result = 31 * result + (boundaries != null ? boundaries.hashCode() : 0);
            result = 31 * result + options.hashCode();
            return result;
        }

        @Override
        public String toString() {
            return "Stage{"
                + "name='$bucket'"
                + ", boundaries=" + boundaries
                + ", groupBy=" + groupBy
                + ", options=" + options
                + '}';
        }
    }

    private static final class BucketAutoStage<TExpression> implements Bson {

        private final TExpression groupBy;
        private final int buckets;
        private final BucketAutoOptions options;

        BucketAutoStage(final TExpression groupBy, final int buckets, final BucketAutoOptions options) {
            notNull("options", options);
            this.groupBy = groupBy;
            this.buckets = buckets;
            this.options = options;
        }

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

            writer.writeStartDocument();

            writer.writeStartDocument("$bucketAuto");

            writer.writeName("groupBy");
            BuildersHelper.encodeValue(writer, groupBy, codecRegistry);

            writer.writeInt32("buckets", buckets);

            writeBucketOutput(codecRegistry, writer, options.getOutput());

            BucketGranularity granularity = options.getGranularity();
            if (granularity != null) {
                writer.writeString("granularity", granularity.getValue());
            }
            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;
            }

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

            if (buckets != that.buckets) {
                return false;
            }
            if (groupBy != null ? !groupBy.equals(that.groupBy) : that.groupBy != null) {
                return false;
            }
            return options.equals(that.options);
        }

        @Override
        public int hashCode() {
            int result = groupBy != null ? groupBy.hashCode() : 0;
            result = 31 * result + buckets;
            result = 31 * result + options.hashCode();
            return result;
        }

        @Override
        public String toString() {
            return "Stage{"
                + "name='$bucketAuto'"
                + ", buckets=" + buckets
                + ", groupBy=" + groupBy
                + ", options=" + options
                + '}';
        }
    }

    private static final class LookupStage<TExpression> implements Bson {
        private final String from;
        private final List<Variable<TExpression>> let;
        private final List<? extends Bson> pipeline;
        private final String as;

        private LookupStage(final String from, @Nullable final List<Variable<TExpression>> let, final List<? extends Bson> pipeline,
                            final String as) {
            this.from = from;
            this.let = let;
            this.pipeline = pipeline;
            this.as = as;
        }

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

            writer.writeStartDocument();

            writer.writeStartDocument("$lookup");

            writer.writeString("from", from);

            if (let != null) {
                writer.writeStartDocument("let");

                for (Variable<?> variable : let) {
                    writer.writeName(variable.getName());
                    BuildersHelper.encodeValue(writer, variable.getValue(), codecRegistry);
                }

                writer.writeEndDocument();
            }

            writer.writeName("pipeline");
            writer.writeStartArray();
            for (Bson stage : pipeline) {
                BuildersHelper.encodeValue(writer, stage, codecRegistry);
            }
            writer.writeEndArray();

            writer.writeString("as", as);

            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;
            }

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

            if (from != null ? !from.equals(that.from) : that.from != null) {
                return false;
            }
            if (let != null ? !let.equals(that.let) : that.let != null) {
                return false;
            }
            if (pipeline != null ? !pipeline.equals(that.pipeline) : that.pipeline != null) {
                return false;
            }
            return as != null ? as.equals(that.as) : that.as == null;
        }

        @Override
        public int hashCode() {
            int result = from != null ? from.hashCode() : 0;
            result = 31 * result + (let != null ? let.hashCode() : 0);
            result = 31 * result + (pipeline != null ? pipeline.hashCode() : 0);
            result = 31 * result + (as != null ? as.hashCode() : 0);
            return result;
        }

        @Override
        public String toString() {
            return "Stage{"
                    + "name='$lookup'"
                    + ", from='" + from + '\''
                    + ", let=" + let
                    + ", pipeline=" + pipeline
                    + ", as='" + as + '\''
                    + '}';
        }
    }

    private static final class GraphLookupStage<TExpression> implements Bson {
        private final String from;
        private final TExpression startWith;
        private final String connectFromField;
        private final String connectToField;
        private final String as;
        private final GraphLookupOptions options;

        private GraphLookupStage(final String from, final TExpression startWith, final String connectFromField, final String connectToField,
                                 final String as, final GraphLookupOptions options) {
            this.from = from;
            this.startWith = startWith;
            this.connectFromField = connectFromField;
            this.connectToField = connectToField;
            this.as = as;
            this.options = options;
        }

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

            writer.writeStartDocument();

            writer.writeStartDocument("$graphLookup");

            writer.writeString("from", from);
            writer.writeName("startWith");
            BuildersHelper.encodeValue(writer, startWith, codecRegistry);

            writer.writeString("connectFromField", connectFromField);
            writer.writeString("connectToField", connectToField);
            writer.writeString("as", as);
            Integer maxDepth = options.getMaxDepth();
            if (maxDepth != null) {
                writer.writeInt32("maxDepth", maxDepth);
            }
            String depthField = options.getDepthField();
            if (depthField != null) {
                writer.writeString("depthField", depthField);
            }
            Bson restrictSearchWithMatch = options.getRestrictSearchWithMatch();
            if (restrictSearchWithMatch != null) {
                writer.writeName("restrictSearchWithMatch");
                BuildersHelper.encodeValue(writer, restrictSearchWithMatch, 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;
            }

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

            if (from != null ? !from.equals(that.from) : that.from != null) {
                return false;
            }
            if (startWith != null ? !startWith.equals(that.startWith) : that.startWith != null) {
                return false;
            }
            if (connectFromField != null ? !connectFromField.equals(that.connectFromField) : that.connectFromField != null) {
                return false;
            }
            if (connectToField != null ? !connectToField.equals(that.connectToField) : that.connectToField != null) {
                return false;
            }
            if (as != null ? !as.equals(that.as) : that.as != null) {
                return false;
            }
            return options != null ? options.equals(that.options) : that.options == null;
        }

        @Override
        public int hashCode() {
            int result = from != null ? from.hashCode() : 0;
            result = 31 * result + (startWith != null ? startWith.hashCode() : 0);
            result = 31 * result + (connectFromField != null ? connectFromField.hashCode() : 0);
            result = 31 * result + (connectToField != null ? connectToField.hashCode() : 0);
            result = 31 * result + (as != null ? as.hashCode() : 0);
            result = 31 * result + (options != null ? options.hashCode() : 0);
            return result;
        }

        @Override
        public String toString() {
            return "Stage{"
                + "name='$graphLookup'"
                + ", as='" + as + '\''
                + ", connectFromField='" + connectFromField + '\''
                + ", connectToField='" + connectToField + '\''
                + ", from='" + from + '\''
                + ", options=" + options
                + ", startWith=" + startWith
                + '}';
        }
    }

    private static class GroupStage<TExpression> implements Bson {
        private final TExpression id;
        private final List<BsonField> fieldAccumulators;

        GroupStage(final TExpression id, final List<BsonField> fieldAccumulators) {
            this.id = id;
            this.fieldAccumulators = fieldAccumulators;
        }

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

            writer.writeStartDocument();

            writer.writeStartDocument("$group");

            writer.writeName("_id");
            BuildersHelper.encodeValue(writer, id, codecRegistry);

            for (BsonField fieldAccumulator : fieldAccumulators) {
                writer.writeName(fieldAccumulator.getName());
                BuildersHelper.encodeValue(writer, fieldAccumulator.getValue(), 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;
            }

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

            if (id != null ? !id.equals(that.id) : that.id != null) {
                return false;
            }
            return fieldAccumulators != null ? fieldAccumulators.equals(that.fieldAccumulators) : that.fieldAccumulators == null;
        }

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

        @Override
        public String toString() {
            return "Stage{"
                           + "name='$group'"
                           + ", id=" + id
                           + ", fieldAccumulators=" + fieldAccumulators
                           + '}';
        }
    }

    private static class SortByCountStage<TExpression> implements Bson {
        private final TExpression filter;

        SortByCountStage(final TExpression filter) {
            this.filter = filter;
        }

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

            writer.writeStartDocument();

            writer.writeName("$sortByCount");
            BuildersHelper.encodeValue(writer, filter, 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;
            }

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

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

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

        @Override
        public String toString() {
            return "Stage{"
                + "name='$sortByCount'"
                + ", id=" + filter
                + '}';
        }
    }

    private static class FacetStage implements Bson {

        private final List<Facet> facets;
        FacetStage(final List<Facet> facets) {
            this.facets = facets;
        }

        @Override
        public <TDocument> BsonDocument toBsonDocument(final Class<TDocument> tDocumentClass, final CodecRegistry codecRegistry) {
            BsonDocumentWriter writer = new BsonDocumentWriter(new BsonDocument());
            writer.writeStartDocument();
            writer.writeName("$facet");
            writer.writeStartDocument();
            for (Facet facet : facets) {
                writer.writeName(facet.getName());
                writer.writeStartArray();
                for (Bson bson : facet.getPipeline()) {
                    BuildersHelper.encodeValue(writer, bson, 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;
            }

            FacetStage that = (FacetStage) o;

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

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

        @Override
        public String toString() {
            return "Stage{"
                + "name='$facet', "
                + "facets=" + facets + '}';
        }

    }

    private static class AddFieldsStage implements Bson {
        private final List<Field<?>> fields;

        AddFieldsStage(final List<Field<?>> fields) {
            this.fields = fields;
        }

        @Override
        public <TDocument> BsonDocument toBsonDocument(final Class<TDocument> tDocumentClass, final CodecRegistry codecRegistry) {
            BsonDocumentWriter writer = new BsonDocumentWriter(new BsonDocument());
            writer.writeStartDocument();
            writer.writeName("$addFields");
            writer.writeStartDocument();
            for (Field<?> field : fields) {
                writer.writeName(field.getName());
                BuildersHelper.encodeValue(writer, field.getValue(), 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;
            }

            AddFieldsStage that = (AddFieldsStage) o;

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

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

        @Override
        public String toString() {
            return "Stage{"
                + "name='$addFields', "
                + "fields=" + fields
                + '}';
        }
    }

    private static class ReplaceRootStage<TExpression> implements Bson {
        private final TExpression value;

        ReplaceRootStage(final TExpression value) {
            this.value = value;
        }

        @Override
        public <TDocument> BsonDocument toBsonDocument(final Class<TDocument> tDocumentClass, final CodecRegistry codecRegistry) {
            BsonDocumentWriter writer = new BsonDocumentWriter(new BsonDocument());
            writer.writeStartDocument();
            writer.writeName("$replaceRoot");
            writer.writeStartDocument();
            writer.writeName("newRoot");
            BuildersHelper.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;
            }

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

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

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

        @Override
        public String toString() {
            return "Stage{"
                + "name='$replaceRoot', "
                + "value=" + value
                + '}';
        }
    }

    private static class OutStage implements Bson {

        private final String collectionName;
        private final AggregateOutStageOptions options;

        OutStage(final String collectionName, final AggregateOutStageOptions options) {
            this.collectionName = collectionName;
            this.options = options;
        }

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

            writer.writeName("$out");
            if (optionsAreAllDefault()) {
                writer.writeString(collectionName);
            } else {
                writer.writeStartDocument();
                writer.writeName("mode");
                switch (options.getMode()) {
                    case REPLACE_COLLECTION:
                        writer.writeString("replaceCollection");
                        break;
                    case REPLACE_DOCUMENTS:
                        writer.writeString("replaceDocuments");
                        break;
                    case INSERT_DOCUMENTS:
                        writer.writeString("insertDocuments");
                        break;
                    default:
                        throw new IllegalArgumentException("Unsupported mode: " + options.getMode());
                }
                writer.writeString("to", collectionName);

                String databaseName = options.getDatabaseName();
                if (databaseName != null) {
                    writer.writeString("db", databaseName);
                }

                Bson uniqueKey = options.getUniqueKey();
                if (uniqueKey != null) {
                    writer.writeName("uniqueKey");
                    BuildersHelper.encodeValue(writer, uniqueKey, 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;
            }

            OutStage outStage = (OutStage) o;

            if (collectionName != null ? !collectionName.equals(outStage.collectionName) : outStage.collectionName != null) {
                return false;
            }
            return options != null ? options.equals(outStage.options) : outStage.options == null;
        }

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

        private boolean optionsAreAllDefault() {
            return options.getMode() == REPLACE_COLLECTION && options.getDatabaseName() == null && options.getUniqueKey() == null;
        }
    }

    private Aggregates() {
    }
}


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

package org.bson;

import org.bson.codecs.DecoderContext;
import org.bson.codecs.UuidCodec;
import org.bson.io.ByteBufferBsonInput;
import org.bson.types.BSONTimestamp;
import org.bson.types.Binary;
import org.bson.types.Code;
import org.bson.types.CodeWScope;
import org.bson.types.MaxKey;
import org.bson.types.MinKey;
import org.bson.types.Symbol;

import java.io.IOException;
import java.io.OutputStream;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import static org.bson.BsonBinarySubType.BINARY;
import static org.bson.BsonBinarySubType.OLD_BINARY;

/**
 * An immutable {@code BSONObject} backed by a byte buffer that lazily provides keys and values on request. This is useful for transferring
 * BSON documents between servers when you don't want to pay the performance penalty of encoding or decoding them fully.
 */
@SuppressWarnings("deprecation")
public class LazyBSONObject implements BSONObject {
    private final byte[] bytes;
    private final int offset;
    private final LazyBSONCallback callback;

    /**
     * Construct an instance.
     *
     * @param bytes the raw bytes
     * @param callback the callback to use to construct nested values
     */
    public LazyBSONObject(final byte[] bytes, final LazyBSONCallback callback) {
        this(bytes, 0, callback);
    }

    /**
     * Construct an instance.
     *
     * @param bytes the raw bytes
     * @param offset the offset into the raw bytes representing the start of the document
     * @param callback the callback to use to construct nested values
     */
    public LazyBSONObject(final byte[] bytes, final int offset, final LazyBSONCallback callback) {
        this.bytes = bytes;
        this.callback = callback;
        this.offset = offset;
    }

    /**
     * Gets the offset into the raw bytes representing the start of the document
     *
     * @return the offset
     */
    protected int getOffset() {
        return offset;
    }

    /**
     * Gets the raw bytes.
     *
     * @return the raw bytes
     */
    protected byte[] getBytes() {
        return bytes;
    }

    @Override
    public Object get(final String key) {
        BsonBinaryReader reader = getBsonReader();
        Object value;
        try {
            reader.readStartDocument();
            value = null;
            while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
                if (key.equals(reader.readName())) {
                    value = readValue(reader);
                    break;
                } else {
                    reader.skipValue();
                }
            }
        } finally {
            reader.close();
        }
        return value;
    }

    @Override
    @Deprecated
    public boolean containsKey(final String key) {
        return containsField(key);
    }

    @Override
    public boolean containsField(final String s) {
        BsonBinaryReader reader = getBsonReader();
        try {
            reader.readStartDocument();
            while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
                if (reader.readName().equals(s)) {
                    return true;
                } else {
                    reader.skipValue();
                }
            }
        } finally {
            reader.close();
        }
        return false;
    }

    @Override
    public Set<String> keySet() {
        Set<String> keys = new LinkedHashSet<String>();
        BsonBinaryReader reader = getBsonReader();
        try {
            reader.readStartDocument();
            while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
                keys.add(reader.readName());
                reader.skipValue();
            }
            reader.readEndDocument();
        } finally {
            reader.close();
        }
        return Collections.unmodifiableSet(keys);
    }

    Object readValue(final BsonBinaryReader reader) {
        switch (reader.getCurrentBsonType()) {
            case DOCUMENT:
                return readDocument(reader);
            case ARRAY:
                return readArray(reader);
            case DOUBLE:
                return reader.readDouble();
            case STRING:
                return reader.readString();
            case BINARY:
                byte binarySubType = reader.peekBinarySubType();
                if (BsonBinarySubType.isUuid(binarySubType) && reader.peekBinarySize() == 16) {
                    return new UuidCodec().decode(reader, DecoderContext.builder().build());
                }
                BsonBinary binary = reader.readBinaryData();
                if (binarySubType == BINARY.getValue() || binarySubType == OLD_BINARY.getValue()) {
                    return binary.getData();
                } else {
                    return new Binary(binary.getType(), binary.getData());
                }
            case NULL:
                reader.readNull();
                return null;
            case UNDEFINED:
                reader.readUndefined();
                return null;
            case OBJECT_ID:
                return reader.readObjectId();
            case BOOLEAN:
                return reader.readBoolean();
            case DATE_TIME:
                return new Date(reader.readDateTime());
            case REGULAR_EXPRESSION:
                BsonRegularExpression regularExpression = reader.readRegularExpression();
                return Pattern.compile(
                                      regularExpression.getPattern(),
                                      BSON.regexFlags(regularExpression.getOptions())
                                      );
            case DB_POINTER:
                BsonDbPointer dbPointer = reader.readDBPointer();
                return callback.createDBRef(dbPointer.getNamespace(), dbPointer.getId());
            case JAVASCRIPT:
                return new Code(reader.readJavaScript());
            case SYMBOL:
                return new Symbol(reader.readSymbol());
            case JAVASCRIPT_WITH_SCOPE:
                return new CodeWScope(reader.readJavaScriptWithScope(), (BSONObject) readJavaScriptWithScopeDocument(reader));
            case INT32:
                return reader.readInt32();
            case TIMESTAMP:
                BsonTimestamp timestamp = reader.readTimestamp();
                return new BSONTimestamp(timestamp.getTime(), timestamp.getInc());
            case INT64:
                return reader.readInt64();
            case DECIMAL128:
                return reader.readDecimal128();
            case MIN_KEY:
                reader.readMinKey();
                return new MinKey();
            case MAX_KEY:
                reader.readMaxKey();
                return new MaxKey();
            default:
                throw new IllegalArgumentException("unhandled BSON type: " + reader.getCurrentBsonType());
        }
    }

    private Object readArray(final BsonBinaryReader reader) {
        int position = reader.getBsonInput().getPosition();
        reader.skipValue();
        return callback.createArray(bytes, offset + position);
    }

    private Object readDocument(final BsonBinaryReader reader) {
        int position = reader.getBsonInput().getPosition();
        reader.skipValue();
        return callback.createObject(bytes, offset + position);
    }

    private Object readJavaScriptWithScopeDocument(final BsonBinaryReader reader) {
        int position = reader.getBsonInput().getPosition();
        reader.readStartDocument();
        while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
            reader.skipName();
            reader.skipValue();
        }
        reader.readEndDocument();
        return callback.createObject(bytes, offset + position);
    }

    BsonBinaryReader getBsonReader() {
        ByteBuffer buffer = getBufferForInternalBytes();
        return new BsonBinaryReader(new ByteBufferBsonInput(new ByteBufNIO(buffer)));
    }

    private ByteBuffer getBufferForInternalBytes() {
        ByteBuffer buffer = ByteBuffer.wrap(bytes, offset, bytes.length - offset).slice();
        buffer.order(ByteOrder.LITTLE_ENDIAN);
        ((Buffer) buffer).limit(buffer.getInt());
        ((Buffer) buffer).rewind();
        return buffer;
    }

    /**
     * Gets whether this is an empty {@code BSONObject}.
     *
     * @return true if this has no keys
     */
    public boolean isEmpty() {
        return keySet().size() == 0;
    }

    /**
     * Gets the size in bytes of the BSON document.
     *
     * @return the size in bytes
     */
    public int getBSONSize() {
        return getBufferForInternalBytes().getInt();
    }

    /**
     * Pipe the raw bytes into the given output stream.
     *
     * @param os the output stream
     * @return the number of bytes written
     * @throws IOException any IOException thrown by the output stream
     */
    public int pipe(final OutputStream os) throws IOException {
        WritableByteChannel channel = Channels.newChannel(os);
        return channel.write(getBufferForInternalBytes());
    }

    /**
     * Gets the entry set for all the key/value pairs in this {@code BSONObject}.  The returned set is immutable.
     *
     * @return then entry set
     */
    public Set<Map.Entry<String, Object>> entrySet() {
        final List<Map.Entry<String, Object>> entries = new ArrayList<Map.Entry<String, Object>>();
        BsonBinaryReader reader = getBsonReader();
        try {
            reader.readStartDocument();
            while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
                entries.add(new AbstractMap.SimpleImmutableEntry<String, Object>(reader.readName(), readValue(reader)));
            }
            reader.readEndDocument();
        } finally {
            reader.close();
        }
        return new Set<Map.Entry<String, Object>>() {
            @Override
            public int size() {
                return entries.size();
            }

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

            @Override
            public Iterator<Map.Entry<String, Object>> iterator() {
                return entries.iterator();
            }

            @Override
            public Object[] toArray() {
                return entries.toArray();
            }

            @Override
            public <T> T[] toArray(final T[] a) {
                return entries.toArray(a);
            }

            @Override
            public boolean contains(final Object o) {
                return entries.contains(o);
            }

            @Override
            public boolean containsAll(final Collection<?> c) {
                return entries.containsAll(c);
            }

            @Override
            public boolean add(final Map.Entry<String, Object> stringObjectEntry) {
                throw new UnsupportedOperationException();
            }

            @Override
            public boolean remove(final Object o) {
                throw new UnsupportedOperationException();
            }

            @Override
            public boolean addAll(final Collection<? extends Map.Entry<String, Object>> c) {
                throw new UnsupportedOperationException();
            }

            @Override
            public boolean retainAll(final Collection<?> c) {
                throw new UnsupportedOperationException();
            }

            @Override
            public boolean removeAll(final Collection<?> c) {
                throw new UnsupportedOperationException();
            }

            @Override
            public void clear() {
                throw new UnsupportedOperationException();
            }
        };
    }

    @Override
    public int hashCode() {
        int result = 1;
        int size = getBSONSize();
        for (int i = offset; i < offset + size; i++) {
            result = 31 * result + bytes[i];
        }
        return result;
    }

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

        if (this.bytes == other.bytes && this.offset == other.offset) {
            return true;
        }
        if (this.bytes == null || other.bytes == null) {
            return false;
        }

        if (this.bytes.length == 0 || other.bytes.length == 0) {
            return false;
        }

        //comparing document length
        int length = this.bytes[this.offset];
        if (other.bytes[other.offset] != length) {
            return false;
        }

        //comparing document contents
        for (int i = 0; i < length; i++) {
            if (this.bytes[this.offset + i] != other.bytes[other.offset + i]) {
                return false;
            }
        }

        return true;
    }

    /* ----------------- Unsupported operations --------------------- */

    /**
     * Always throws {@code UnsupportedOperationException}.
     *
     * @param key Name to set
     * @param v   Corresponding value
     * @return will not return normally
     * @throws java.lang.UnsupportedOperationException the object is read only
     */
    @Override
    public Object put(final String key, final Object v) {
        throw new UnsupportedOperationException("Object is read only");
    }

    /**
     * Always throws {@code UnsupportedOperationException}.
     *
     * @param o the object
     * @throws java.lang.UnsupportedOperationException the object is read only
     */
    @Override
    public void putAll(final BSONObject o) {
        throw new UnsupportedOperationException("Object is read only");
    }

    /**
     * Always throws {@code UnsupportedOperationException}.
     *
     * @param m the map
     * @throws java.lang.UnsupportedOperationException the object is read only
     */@Override
    @SuppressWarnings("rawtypes")
    public void putAll(final Map m) {
        throw new UnsupportedOperationException("Object is read only");
    }

    /**
     * Always throws {@code UnsupportedOperationException}.
     *
     * @param key The name of the field to remove
     * @return will not return normally
     * @throws java.lang.UnsupportedOperationException the object is read only
     */
    @Override
    public Object removeField(final String key) {
        throw new UnsupportedOperationException("Object is read only");
    }

    @Override
    @SuppressWarnings("rawtypes")
    public Map toMap() {
        Map<String, Object> map = new LinkedHashMap<String, Object>();
        for (final Map.Entry<String, Object> entry : entrySet()) {
            map.put(entry.getKey(), entry.getValue());
        }
        return Collections.unmodifiableMap(map);
    }
}


/*
 * 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.async.SingleResultCallback;
import com.mongodb.async.client.gridfs.AsyncInputStream;
import com.mongodb.async.client.gridfs.AsyncOutputStream;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousByteChannel;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;

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

/**
 * A helper class to convert to {@link AsynchronousByteChannel} or {@link AsynchronousFileChannel} instances into {@link AsyncInputStream}
 * or {@link AsyncOutputStream} instances.
 *
 *
Requires Java 7 or greater.

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

    /**
     * Converts a {@link AsynchronousByteChannel} into a {@link AsyncInputStream}
     *
     * @param asynchronousByteChannel the AsynchronousByteChannel
     * @return the AsyncInputStream
     */
    public static AsyncInputStream channelToInputStream(final AsynchronousByteChannel asynchronousByteChannel) {
        notNull("asynchronousByteChannel", asynchronousByteChannel);
        return new AsyncInputStream() {
            @Override
            public void read(final ByteBuffer dst, final SingleResultCallback<Integer> callback) {
                asynchronousByteChannel.read(dst, null, new CompletionHandler<Integer, Object>() {
                    @Override
                    public void completed(final Integer result, final Object attachment) {
                        callback.onResult(result, null);
                    }

                    @Override
                    public void failed(final Throwable exc, final Object attachment) {
                        callback.onResult(null, exc);
                    }
                });
            }

            @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) {
                try {
                    asynchronousByteChannel.close();
                    callback.onResult(null, null);
                } catch (Exception e) {
                    callback.onResult(null, e);
                }
            }
        };
    }

    /**
     * Converts a {@link AsynchronousFileChannel} into a {@link AsyncInputStream}
     *
     * @param asynchronousFileChannel the AsynchronousFileChannel
     * @return the AsyncInputStream
     */
    public static AsyncInputStream channelToInputStream(final AsynchronousFileChannel asynchronousFileChannel) {
        notNull("asynchronousByteChannel", asynchronousFileChannel);
        return new AsyncInputStream() {
            private int position = 0;
            @Override
            public void read(final ByteBuffer dst, final SingleResultCallback<Integer> callback) {
                asynchronousFileChannel.read(dst, position, null, new CompletionHandler<Integer, Object>() {
                    @Override
                    public void completed(final Integer result, final Object attachment) {
                        position += result;
                        callback.onResult(result, null);
                    }

                    @Override
                    public void failed(final Throwable exc, final Object attachment) {
                        callback.onResult(null, exc);
                    }
                });
            }

            @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) {
                try {
                    asynchronousFileChannel.close();
                    callback.onResult(null, null);
                } catch (Exception e) {
                    callback.onResult(null, e);
                }
            }
        };
    }

    /**
     * Converts a {@link AsynchronousByteChannel} into a {@link AsyncOutputStream}
     *
     * @param asynchronousByteChannel the AsynchronousByteChannel
     * @return the AsyncOutputStream
     */
    public static AsyncOutputStream channelToOutputStream(final AsynchronousByteChannel asynchronousByteChannel) {
        notNull("asynchronousByteChannel", asynchronousByteChannel);
        return new AsyncOutputStream() {
            @Override
            public void write(final ByteBuffer src, final SingleResultCallback<Integer> callback) {
                asynchronousByteChannel.write(src, null, new CompletionHandler<Integer, Object>() {
                    @Override
                    public void completed(final Integer result, final Object attachment) {
                        callback.onResult(result, null);
                    }

                    @Override
                    public void failed(final Throwable exc, final Object attachment) {
                        callback.onResult(null, exc);
                    }
                });
            }

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

    /**
     * Converts a {@link AsynchronousFileChannel} into a {@link AsyncOutputStream}
     *
     * @param asynchronousFileChannel the AsynchronousFileChannel
     * @return the AsyncOutputStream
     */
    public static AsyncOutputStream channelToOutputStream(final AsynchronousFileChannel asynchronousFileChannel) {
        notNull("asynchronousByteChannel", asynchronousFileChannel);
        return new AsyncOutputStream() {
            private int position = 0;
            @Override
            public void write(final ByteBuffer src, final SingleResultCallback<Integer> callback) {
                asynchronousFileChannel.write(src, position, null, new CompletionHandler<Integer, Object>() {
                    @Override
                    public void completed(final Integer result, final Object attachment) {
                        position += result;
                        callback.onResult(result, null);
                    }

                    @Override
                    public void failed(final Throwable exc, final Object attachment) {
                        callback.onResult(null, exc);
                    }
                });
            }

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

    private AsynchronousChannelHelper() {
    }
}


/*
 * Copyright 2008-present MongoDB, Inc.
 * Copyright (c) 2008-2014 Atlassian Pty Ltd
 *
 * 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.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 *
A thread-safe variant of {@link java.util.Map} in which all mutative operations (the "destructive" operations described by {@link
 * java.util.Map} put, remove and so on) are implemented by making a fresh copy of the underlying map.

 *
 *
This is ordinarily too costly, but may be more efficient than alternatives when traversal operations vastly out-number
 * mutations, and is useful when you cannot or don't want to synchronize traversals, yet need to preclude interference among concurrent
 * threads. The "snapshot" style iterators on the collections returned by {@link #entrySet()}, {@link #keySet()} and {@link #values()} use a
 * reference to the internal map at the point that the iterator was created. This map never changes during the lifetime of the iterator, so
 * interference is impossible and the iterator is guaranteed not to throw {@code ConcurrentModificationException}. The iterators will not
 * reflect additions, removals, or changes to the list since the iterator was created. Removing elements via these iterators is not
 * supported. The mutable operations on these collections (remove, retain etc.) are supported but as with the {@link java.util.Map}
 * interface, add and addAll are not and throw {@link UnsupportedOperationException}.

The actual copy is performed by an abstract
 * {@link #copy(java.util.Map)} method. The method is responsible for the underlying Map implementation (for instance a {@link
 * java.util.HashMap}, {@link java.util.TreeMap}, {@link java.util.LinkedHashMap} etc.) and therefore the semantics of what this map will
 * cope with as far as null keys and values, iteration ordering etc. See the note below about suitable candidates for underlying Map
 * implementations

 *
 *
There are supplied implementations for the common j.u.c {@link java.util.Map} implementations via the {@link CopyOnWriteMap} static
 * {@link Builder}.

 *
 *
Collection views of the keys, values and entries are optionally {@link View.Type#LIVE live} or {@link View.Type#STABLE stable}. Live
 * views are modifiable will cause a copy if a modifying method is called on them. Methods on these will reflect the current state of the
 * collection, although iterators will be snapshot style. If the collection views are stable they are unmodifiable, and will be a snapshot
 * of the state of the map at the time the collection was asked for.

 *
 *
Please note that the thread-safety guarantees are limited to the thread-safety of the non-mutative (non-destructive)
 * operations of the underlying map implementation. For instance some implementations such as {@link java.util.WeakHashMap} and {@link
 * java.util.LinkedHashMap} with access ordering are actually structurally modified by the {@link #get(Object)} method and are therefore not
 * suitable candidates as delegates for this class.
 *
 * @param <K> the key type
 * @param <V> the value type
 * @author Jed Wesley-Smith
 */
abstract class CopyOnWriteMap<K, V> extends AbstractCopyOnWriteMap<K, V, Map<K, V>> {
    private static final long serialVersionUID = 7935514534647505917L;

    /**
     * Get a {@link Builder} for a CopyOnWriteMap instance.
     *
     * @param <K> key type
     * @param <V> value type
     * @return a fresh builder
     */
    public static <K, V> Builder<K, V> builder() {
        return new Builder<K, V>();
    }

    /**
     * Build a {@link CopyOnWriteMap} and specify all the options.
     *
     * @param <K> key type
     * @param <V> value type
     */
    public static class Builder<K, V> {
        private View.Type viewType = View.Type.STABLE;
        private final Map<K, V> initialValues = new HashMap<K, V>();

        Builder() {
        }

        /**
         * Views are stable (fixed in time) and unmodifiable.
         */
        public Builder<K, V> stableViews() {
            viewType = View.Type.STABLE;
            return this;
        }

        /**
         * Views are live (reflecting concurrent updates) and mutator methods are supported.
         */
        public Builder<K, V> addAll(final Map<? extends K, ? extends V> values) {
            initialValues.putAll(values);
            return this;
        }

        /**
         * Views are live (reflecting concurrent updates) and mutator methods are supported.
         */
        public Builder<K, V> liveViews() {
            viewType = View.Type.LIVE;
            return this;
        }

        public CopyOnWriteMap<K, V> newHashMap() {
            return new Hash<K, V>(initialValues, viewType);
        }

        public CopyOnWriteMap<K, V> newLinkedMap() {
            return new Linked<K, V>(initialValues, viewType);
        }
    }

    /**
     *
 Creates a new {@link CopyOnWriteMap} with an underlying {@link HashMap}.

     *
     *
This map has {@link View.Type#STABLE stable} views.

     */
    public static <K, V> CopyOnWriteMap<K, V> newHashMap() {
        Builder<K, V> builder = builder();
        return builder.newHashMap();
    }

    /**
     *
 Creates a new {@link CopyOnWriteMap} with an underlying {@link HashMap} using the supplied map as the initial values.

     *
     *
This map has {@link View.Type#STABLE stable} views.

     */
    public static <K, V> CopyOnWriteMap<K, V> newHashMap(final Map<? extends K, ? extends V> map) {
        Builder<K, V> builder = builder();
        return builder.addAll(map).newHashMap();
    }

    /**
     *
Creates a new {@link CopyOnWriteMap} with an underlying {@link java.util.LinkedHashMap}. Iterators for this map will be return
     * elements in insertion order.

     *
     *
This map has {@link View.Type#STABLE stable} views.

     */
    public static <K, V> CopyOnWriteMap<K, V> newLinkedMap() {
        Builder<K, V> builder = builder();
        return builder.newLinkedMap();
    }

    /**
     *
Creates a new {@link CopyOnWriteMap} with an underlying {@link java.util.LinkedHashMap} using the supplied map as the initial
     * values. Iterators for this map will be return elements in insertion order.

     *
     *
This map has {@link View.Type#STABLE stable} views.

     */
    public static <K, V> CopyOnWriteMap<K, V> newLinkedMap(final Map<? extends K, ? extends V> map) {
        Builder<K, V> builder = builder();
        return builder.addAll(map).newLinkedMap();
    }

    //
    // constructors
    //

    /**
     * Create a new {@link CopyOnWriteMap} with the supplied {@link Map} to initialize the values.
     *
     * @param map the initial map to initialize with
     */
    protected CopyOnWriteMap(final Map<? extends K, ? extends V> map) {
        this(map, View.Type.LIVE);
    }

    /**
     * Create a new empty {@link CopyOnWriteMap}.
     */
    protected CopyOnWriteMap() {
        this(Collections.<K, V>emptyMap(), View.Type.LIVE);
    }

    /**
     * Create a new {@link CopyOnWriteMap} with the supplied {@link Map} to initialize the values. This map may be optionally modified using
     * any of the key, entry or value views
     *
     * @param map the initial map to initialize with
     */
    protected CopyOnWriteMap(final Map<? extends K, ? extends V> map, final View.Type viewType) {
        super(map, viewType);
    }

    /**
     * Create a new empty {@link CopyOnWriteMap}. This map may be optionally modified using any of the key, entry or value views
     */
    protected CopyOnWriteMap(final View.Type viewType) {
        super(Collections.<K, V>emptyMap(), viewType);
    }

    @Override
    // @GuardedBy("internal-lock")
    protected abstract <N extends Map<? extends K, ? extends V>> Map<K, V> copy(N map);

    //
    // inner classes
    //

    /**
     * Uses {@link HashMap} instances as its internal storage.
     */
    static class Hash<K, V> extends CopyOnWriteMap<K, V> {
        private static final long serialVersionUID = 5221824943734164497L;

        Hash(final Map<? extends K, ? extends V> map, final View.Type viewType) {
            super(map, viewType);
        }

        @Override
        public <N extends Map<? extends K, ? extends V>> Map<K, V> copy(final N map) {
            return new HashMap<K, V>(map);
        }
    }

    /**
     * Uses {@link java.util.LinkedHashMap} instances as its internal storage.
     */
    static class Linked<K, V> extends CopyOnWriteMap<K, V> {
        private static final long serialVersionUID = -8659999465009072124L;

        Linked(final Map<? extends K, ? extends V> map, final View.Type viewType) {
            super(map, viewType);
        }

        @Override
        public <N extends Map<? extends K, ? extends V>> Map<K, V> copy(final N map) {
            return new LinkedHashMap<K, V>(map);
        }
    }
}