/*
 * Copyright 2015 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 CONObjectITIONS OF ANY KINObject, 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.client.model.geojson.Geometry;
import com.mongodb.client.model.geojson.Point;
import org.bson.BsonArray;
import org.bson.BsonBoolean;
import org.bson.BsonDocument;
import org.bson.BsonDocumentWriter;
import org.bson.BsonDouble;
import org.bson.BsonInt32;
import org.bson.BsonInt64;
import org.bson.BsonRegularExpression;
import org.bson.BsonString;
import org.bson.BsonType;
import org.bson.BsonValue;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.conversions.Bson;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

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

/**
 * A factory for query filters. A convenient way to use this class is to statically import all of its methods, which allows usage like:
 * <blockquote><pre>
 *    collection.find(and(eq("x", 1), lt("y", 3)));
 * </pre></blockquote>
 * @since 3.0
 */
public final class Filters {

    private Filters() {
    }

    /**
     * Creates a filter that matches all documents where the value of the field name equals the specified value. Note that this does
     * actually generate a $eq operator, as the query language doesn't require it.
     *
     * @param fieldName the field name
     * @param value     the value
     * @param <TItem>   the value type
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/eq $eq
     */
    public static <TItem> Bson eq(final String fieldName, final TItem value) {
        return new SimpleEncodingFilter<TItem>(fieldName, value);
    }

    /**
     * Creates a filter that matches all documents where the value of the field name does not equal the specified value.
     *
     * @param fieldName the field name
     * @param value     the value
     * @param <TItem>   the value type
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/ne $ne
     */
    public static <TItem> Bson ne(final String fieldName, final TItem value) {
        return new OperatorFilter<TItem>("$ne", fieldName, value);
    }

    /**
     * Creates a filter that matches all documents where the value of the given field is greater than the specified value.
     *
     * @param fieldName the field name
     * @param value     the value
     * @param <TItem>   the value type
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/gt $gt
     */
    public static <TItem> Bson gt(final String fieldName, final TItem value) {
        return new OperatorFilter<TItem>("$gt", fieldName, value);
    }

    /**
     * Creates a filter that matches all documents where the value of the given field is less than the specified value.
     *
     * @param fieldName the field name
     * @param value     the value
     * @param <TItem>   the value type
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/lt $lt
     */
    public static <TItem> Bson lt(final String fieldName, final TItem value) {
        return new OperatorFilter<TItem>("$lt", fieldName, value);
    }

    /**
     * Creates a filter that matches all documents where the value of the given field is greater than or equal to the specified value.
     *
     * @param fieldName the field name
     * @param value     the value
     * @param <TItem>   the value type
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/gte $gte
     */
    public static <TItem> Bson gte(final String fieldName, final TItem value) {
        return new OperatorFilter<TItem>("$gte", fieldName, value);
    }

    /**
     * Creates a filter that matches all documents where the value of the given field is less than or equal to the specified value.
     *
     * @param fieldName the field name
     * @param value     the value
     * @param <TItem>   the value type
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/lte $lte
     */
    public static <TItem> Bson lte(final String fieldName, final TItem value) {
        return new OperatorFilter<TItem>("$lte", fieldName, value);
    }

    /**
     * Creates a filter that matches all documents where the value of a field equals any value in the list of specified values.
     *
     * @param fieldName the field name
     * @param values    the list of values
     * @param <TItem>   the value type
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/in $in
     */
    public static <TItem> Bson in(final String fieldName, final TItem... values) {
        return in(fieldName, asList(values));
    }

    /**
     * Creates a filter that matches all documents where the value of a field equals any value in the list of specified values.
     *
     * @param fieldName the field name
     * @param values    the list of values
     * @param <TItem>   the value type
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/in $in
     */
    public static <TItem> Bson in(final String fieldName, final Iterable<TItem> values) {
        return new IterableOperatorFilter<TItem>(fieldName, "$in", values);
    }

    /**
     * Creates a filter that matches all documents where the value of a field does not equal any of the specified values or does not exist.
     *
     * @param fieldName the field name
     * @param values    the list of values
     * @param <TItem>   the value type
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/nin $nin
     */
    public static <TItem> Bson nin(final String fieldName, final TItem... values) {
        return nin(fieldName, asList(values));
    }

    /**
     * Creates a filter that matches all documents where the value of a field does not equal any of the specified values or does not exist.
     *
     * @param fieldName the field name
     * @param values    the list of values
     * @param <TItem>   the value type
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/nin $nin
     */
    public static <TItem> Bson nin(final String fieldName, final Iterable<TItem> values) {
        return new IterableOperatorFilter<TItem>(fieldName, "$nin", values);
    }

    /**
     * Creates a filter that performs a logical AND of the provided list of filters.  Note that this will only generate a "$and"
     * operator if absolutely necessary, as the query language implicity ands together all the keys.  In other words, a query expression
     * like:
     *
     * <blockquote><pre>
     *    and(eq("x", 1), lt("y", 3))
     * </pre></blockquote>
     *
     * will generate a MongoDB query like:
     * <blockquote><pre>
     *    {x : 1, y : {$lt : 3}}
     * </pre></blockquote>
     *
     * @param filters the list of filters to and together
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/and $and
     */
    public static Bson and(final Iterable<Bson> filters) {
        return new AndFilter(filters);
    }

    /**
     * Creates a filter that performs a logical AND of the provided list of filters.  Note that this will only generate a "$and"
     * operator if absolutely necessary, as the query language implicity ands together all the keys.  In other words, a query expression
     * like:
     *
     * <blockquote><pre>
     *    and(eq("x", 1), lt("y", 3))
     * </pre></blockquote>
     *
     * will generate a MongoDB query like:
     *
     * <blockquote><pre>
     *    {x : 1, y : {$lt : 3}}
     * </pre></blockquote>
     *
     * @param filters the list of filters to and together
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/and $and
     */
    public static Bson and(final Bson... filters) {
        return and(asList(filters));
    }

    /**
     * Creates a filter that preforms a logical OR of the provided list of filters.
     *
     * @param filters the list of filters to and together
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/or $or
     */
    public static Bson or(final Iterable<Bson> filters) {
        return new OrNorFilter(OrNorFilter.Operator.OR, filters);
    }

    /**
     * Creates a filter that preforms a logical OR of the provided list of filters.
     *
     * @param filters the list of filters to and together
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/or $or
     */
    public static Bson or(final Bson... filters) {
        return or(asList(filters));
    }

    /**
     * Creates a filter that matches all documents that do not match the passed in filter.
     * Requires the field name to passed as part of the value passed in and lifts it to create a valid "$not" query:
     *
     * <blockquote><pre>
     *    not(eq("x", 1))
     * </pre></blockquote>
     *
     * will generate a MongoDB query like:
     * <blockquote><pre>
     *    {x : $not: {$eq : 1}}
     * </pre></blockquote>
     *
     * @param filter the value
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/not $not
     */
    public static Bson not(final Bson filter) {
        return new NotFilter(filter);
    }

    /**
     * Creates a filter that performs a logical NOR operation on all the specified filters.
     *
     * @param filters the list of values
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/nor $nor
     */
    public static Bson nor(final Bson... filters) {
        return nor(asList(filters));
    }

    /**
     * Creates a filter that performs a logical NOR operation on all the specified filters.
     *
     * @param filters the list of values
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/nor $nor
     */
    public static Bson nor(final Iterable<Bson> filters) {
        return new OrNorFilter(OrNorFilter.Operator.NOR, filters);
    }

    /**
     * Creates a filter that matches all documents that contain the given field.
     *
     * @param fieldName the field name
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/exists $exists
     */
    public static Bson exists(final String fieldName) {
        return exists(fieldName, true);
    }

    /**
     * Creates a filter that matches all documents that either contain or do not contain the given field, depending on the value of the
     * exists parameter.
     *
     * @param fieldName the field name
     * @param exists    true to check for existence, false to check for absence
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/exists $exists
     */

    public static Bson exists(final String fieldName, final boolean exists) {
        return new OperatorFilter<BsonBoolean>("$exists", fieldName, BsonBoolean.valueOf(exists));
    }

    /**
     * Creates a filter that matches all documents where the value of the field is of the specified BSON type.
     *
     * @param fieldName the field name
     * @param type      the BSON type
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/type $type
     */
    public static Bson type(final String fieldName, final BsonType type) {
        return new OperatorFilter<BsonInt32>("$type", fieldName, new BsonInt32(type.getValue()));
    }

    /**
     * Creates a filter that matches all documents where the value of the field is of the specified BSON type.
     *
     * @param fieldName the field name
     * @param type      the string representation of the BSON type
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/type $type
     */
    public static Bson type(final String fieldName, final String type) {
        return new OperatorFilter<BsonString>("$type", fieldName, new BsonString(type));
    }

    /**
     * Creates a filter that matches all documents where the value of a field divided by a divisor has the specified remainder (i.e. perform
     * a modulo operation to select documents).
     *
     * @param fieldName the field name
     * @param divisor   the modulus
     * @param remainder the remainder
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/mod $mod
     */
    public static Bson mod(final String fieldName, final long divisor, final long remainder) {
        return new OperatorFilter<BsonArray>("$mod", fieldName, new BsonArray(asList(new BsonInt64(divisor), new BsonInt64(remainder))));
    }

    /**
     * Creates a filter that matches all documents where the value of the field matches the given regular expression pattern with the given
     * options applied.
     *
     * @param fieldName the field name
     * @param pattern   the pattern
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/regex $regex
     */
    public static Bson regex(final String fieldName, final String pattern) {
        return regex(fieldName, pattern, null);
    }

    /**
     * Creates a filter that matches all documents where the value of the field matches the given regular expression pattern with the given
     * options applied.
     *
     * @param fieldName the field name
     * @param pattern   the pattern
     * @param options   the options
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/regex $regex
     */
    public static Bson regex(final String fieldName, final String pattern, final String options) {
        notNull("pattern", pattern);
        return new SimpleFilter(fieldName, new BsonRegularExpression(pattern, options));
    }

    /**
     * Creates a filter that matches all documents where the value of the field matches the given regular expression pattern with the given
     * options applied.
     *
     * @param fieldName the field name
     * @param pattern   the pattern
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/regex $regex
     */
    public static Bson regex(final String fieldName, final Pattern pattern) {
        notNull("pattern", pattern);
        return new SimpleEncodingFilter<Pattern>(fieldName, pattern);
    }

    /**
     * Creates a filter that matches all documents matching the given search term.
     *
     * @param search the search term
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/text $text
     */
    public static Bson text(final String search) {
        notNull("search", search);
        return text(search, new TextSearchOptions());
    }

    /**
     * Creates a filter that matches all documents matching the given search term using the given language.
     *
     * @param search   the search term
     * @param language the language to use for stop words
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/text $text
     * @deprecated use {@link Filters#text(String, TextSearchOptions)}
     */
    @Deprecated
    public static Bson text(final String search, final String language) {
        notNull("search", search);
        return text(search, new TextSearchOptions().language(language));
    }

    /**
     * Creates a filter that matches all documents matching the given the search term with the given text search options.
     *
     * @param search            the search term
     * @param textSearchOptions the text search options to use
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/text $text
     * @since 3.2
     */
    public static Bson text(final String search, final TextSearchOptions textSearchOptions) {
        notNull("search", search);
        notNull("textSearchOptions", textSearchOptions);
        return new TextFilter(search, textSearchOptions);
    }

    /**
     * Creates a filter that matches all documents for which the given expression is true.
     *
     * @param javaScriptExpression the JavaScript expression
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/where $where
     */
    public static Bson where(final String javaScriptExpression) {
        notNull("javaScriptExpression", javaScriptExpression);
        return new BsonDocument("$where", new BsonString(javaScriptExpression));
    }

    /**
     * Creates a filter that matches all documents where the value of a field is an array that contains all the specified values.
     *
     * @param fieldName the field name
     * @param values    the list of values
     * @param <TItem>   the value type
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/all $all
     */
    public static <TItem> Bson all(final String fieldName, final TItem... values) {
        return all(fieldName, asList(values));
    }

    /**
     * Creates a filter that matches all documents where the value of a field is an array that contains all the specified values.
     *
     * @param fieldName the field name
     * @param values    the list of values
     * @param <TItem>   the value type
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/all $all
     */
    public static <TItem> Bson all(final String fieldName, final Iterable<TItem> values) {
        return new IterableOperatorFilter<TItem>(fieldName, "$all", values);
    }

    /**
     * Creates a filter that matches all documents containing a field that is an array where at least one member of the array matches the
     * given filter.
     *
     * @param fieldName the field name
     * @param filter    the filter to apply to each element
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/elemMatch $elemMatch
     */
    public static Bson elemMatch(final String fieldName, final Bson filter) {
        return new Bson() {
            @Override
            public <TDocument> BsonDocument toBsonDocument(final Class<TDocument> documentClass, final CodecRegistry codecRegistry) {
                return new BsonDocument(fieldName, new BsonDocument("$elemMatch", filter.toBsonDocument(documentClass, codecRegistry)));
            }
        };
    }

    /**
     * Creates a filter that matches all documents where the value of a field is an array of the specified size.
     *
     * @param fieldName the field name
     * @param size      the size of the array
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/size $size
     */
    public static Bson size(final String fieldName, final int size) {
        return new OperatorFilter<Integer>("$size", fieldName, size);
    }

    /**
     * Creates a filter that matches all documents where all of the bit positions are clear in the field.
     *
     * @param fieldName the field name
     * @param bitmask   the bitmask
     * @return the filter
     * @mongodb.server.release 3.2
     * @mongodb.driver.manual reference/operator/query/bitsAllClear $bitsAllClear
     * @since 3.2
     */
    public static Bson bitsAllClear(final String fieldName, final long bitmask) {
        return new OperatorFilter<Long>("$bitsAllClear", fieldName, bitmask);
    }

    /**
     * Creates a filter that matches all documents where all of the bit positions are set in the field.
     *
     * @param fieldName the field name
     * @param bitmask   the bitmask
     * @return the filter
     * @mongodb.server.release 3.2
     * @mongodb.driver.manual reference/operator/query/bitsAllSet $bitsAllSet
     * @since 3.2
     */
    public static Bson bitsAllSet(final String fieldName, final long bitmask) {
        return new OperatorFilter<Long>("$bitsAllSet", fieldName, bitmask);
    }

    /**
     * Creates a filter that matches all documents where any of the bit positions are clear in the field.
     *
     * @param fieldName the field name
     * @param bitmask   the bitmask
     * @return the filter
     * @mongodb.server.release 3.2
     * @mongodb.driver.manual reference/operator/query/bitsAllClear $bitsAllClear
     * @since 3.2
     */
    public static Bson bitsAnyClear(final String fieldName, final long bitmask) {
        return new OperatorFilter<Long>("$bitsAnyClear", fieldName, bitmask);
    }

    /**
     * Creates a filter that matches all documents where any of the bit positions are set in the field.
     *
     * @param fieldName the field name
     * @param bitmask   the bitmask
     * @return the filter
     * @mongodb.server.release 3.2
     * @mongodb.driver.manual reference/operator/query/bitsAnySet $bitsAnySet
     * @since 3.2
     */
    public static Bson bitsAnySet(final String fieldName, final long bitmask) {
        return new OperatorFilter<Long>("$bitsAnySet", fieldName, bitmask);
    }

    /**
     * Creates a filter that matches all documents containing a field with geospatial data that exists entirely within the specified shape.
     *
     * @param fieldName the field name
     * @param geometry  the bounding GeoJSON geometry object
     * @return the filter
     * @since 3.1
     * @mongodb.driver.manual reference/operator/query/geoWithin/ $geoWithin
     * @mongodb.server.release 2.4
     */
    public static Bson geoWithin(final String fieldName, final Geometry geometry) {
        return new GeometryOperatorFilter<Geometry>("$geoWithin", fieldName, geometry);
    }

    /**
     * Creates a filter that matches all documents containing a field with geospatial data that exists entirely within the specified shape.
     *
     * @param fieldName the field name
     * @param geometry  the bounding GeoJSON geometry object
     * @return the filter
     * @since 3.1
     * @mongodb.driver.manual reference/operator/query/geoWithin/ $geoWithin
     * @mongodb.server.release 2.4
     */
    public static Bson geoWithin(final String fieldName, final Bson geometry) {
        return new GeometryOperatorFilter<Bson>("$geoWithin", fieldName, geometry);
    }

    /**
     * Creates a filter that matches all documents containing a field with grid coordinates data that exist entirely within the specified
     * box.
     *
     * @param fieldName   the field name
     * @param lowerLeftX  the lower left x coordinate of the box
     * @param lowerLeftY  the lower left y coordinate of the box
     * @param upperRightX the upper left x coordinate of the box
     * @param upperRightY the upper left y coordinate of the box
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/geoWithin/ $geoWithin
     * @mongodb.driver.manual reference/operator/query/box/#op._S_box $box
     * @mongodb.server.release 2.4
     * @since 3.1
     */
    public static Bson geoWithinBox(final String fieldName, final double lowerLeftX, final double lowerLeftY, final double upperRightX,
                                    final double upperRightY) {
        BsonDocument box = new BsonDocument("$box",
                                                   new BsonArray(asList(new BsonArray(asList(new BsonDouble(lowerLeftX),
                                                           new BsonDouble(lowerLeftY))),
                                                           new BsonArray(asList(new BsonDouble(upperRightX),
                                                                   new BsonDouble(upperRightY))))));
        return new OperatorFilter<BsonDocument>("$geoWithin", fieldName, box);
    }

    /**
     * Creates a filter that matches all documents containing a field with grid coordinates data that exist entirely within the specified
     * polygon.
     *
     * @param fieldName the field name
     * @param points    a list of pairs of x, y coordinates.  Any extra dimensions are ignored
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/geoWithin/ $geoWithin
     * @mongodb.driver.manual reference/operator/query/polygon/#op._S_polygon $polygon
     * @mongodb.server.release 2.4
     * @since 3.1
     */
    public static Bson geoWithinPolygon(final String fieldName, final List<List<Double>> points) {
        BsonArray pointsArray = new BsonArray();
        for (List<Double> point : points) {
            pointsArray.add(new BsonArray(asList(new BsonDouble(point.get(0)), new BsonDouble(point.get(1)))));
        }
        BsonDocument polygon = new BsonDocument("$polygon", pointsArray);
        return new OperatorFilter<BsonDocument>("$geoWithin", fieldName, polygon);
    }

    /**
     * Creates a filter that matches all documents containing a field with grid coordinates data that exist entirely within the specified
     * circle.
     *
     * @param fieldName the field name
     * @param x         the x coordinate of the circle
     * @param y         the y coordinate of the circle
     * @param radius    the radius of the circle, as measured in the units used by the coordinate system
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/geoWithin/ $geoWithin
     * @mongodb.driver.manual reference/operator/query/center/#op._S_center $center
     * @mongodb.server.release 2.4
     * @since 3.1
     */
    public static Bson geoWithinCenter(final String fieldName, final double x, final double y, final double radius) {
        BsonDocument center = new BsonDocument("$center",
                                                      new BsonArray(Arrays.<BsonValue>asList(new BsonArray(asList(new BsonDouble(x),
                                                              new BsonDouble(y))),
                                                              new BsonDouble(radius))));
        return new OperatorFilter<BsonDocument>("$geoWithin", fieldName, center);
    }

    /**
     * Creates a filter that matches all documents containing a field with geospatial data (GeoJSON or legacy coordinate pairs) that exist
     * entirely within the specified circle, using spherical geometry.  If using longitude and latitude, specify longitude first.
     *
     * @param fieldName the field name
     * @param x         the x coordinate of the circle
     * @param y         the y coordinate of the circle
     * @param radius    the radius of the circle, in radians
     * @return the filter
     * @mongodb.driver.manual reference/operator/query/geoWithin/ $geoWithin
     * @mongodb.driver.manual reference/operator/query/centerSphere/#op._S_centerSphere $centerSphere
     * @mongodb.server.release 2.4
     * @since 3.1
     */
    public static Bson geoWithinCenterSphere(final String fieldName, final double x, final double y, final double radius) {
        BsonDocument centerSphere = new BsonDocument("$centerSphere",
                                                            new BsonArray(Arrays.<BsonValue>asList(new BsonArray(asList(new BsonDouble(x),
                                                                    new BsonDouble(y))),
                                                                    new BsonDouble(radius))));
        return new OperatorFilter<BsonDocument>("$geoWithin", fieldName, centerSphere);
    }

    /**
     * Creates a filter that matches all documents containing a field with geospatial data that intersects with the specified shape.
     *
     * @param fieldName the field name
     * @param geometry  the bounding GeoJSON geometry object
     * @return the filter
     * @since 3.1
     * @mongodb.driver.manual reference/operator/query/geoIntersects/ $geoIntersects
     * @mongodb.server.release 2.4
     */
    public static Bson geoIntersects(final String fieldName, final Bson geometry) {
        return new GeometryOperatorFilter<Bson>("$geoIntersects", fieldName, geometry);
    }

    /**
     * Creates a filter that matches all documents containing a field with geospatial data that intersects with the specified shape.
     *
     * @param fieldName the field name
     * @param geometry  the bounding GeoJSON geometry object
     * @return the filter
     * @since 3.1
     * @mongodb.driver.manual reference/operator/query/geoIntersects/ $geoIntersects
     * @mongodb.server.release 2.4
     */
    public static Bson geoIntersects(final String fieldName, final Geometry geometry) {
        return new GeometryOperatorFilter<Geometry>("$geoIntersects", fieldName, geometry);
    }

    /**
     * Creates a filter that matches all documents containing a field with geospatial data that is near the specified GeoJSON point.
     *
     * @param fieldName   the field name
     * @param geometry    the bounding GeoJSON geometry object
     * @param maxDistance the maximum distance from the point, in meters
     * @param minDistance the minimum distance from the point, in meters
     * @return the filter
     * @since 3.1
     * @mongodb.driver.manual reference/operator/query/near/ $near
     * @mongodb.server.release 2.4
     */
    public static Bson near(final String fieldName, final Point geometry, final Double maxDistance, final Double minDistance) {
        return new GeometryOperatorFilter<Point>("$near", fieldName, geometry, maxDistance, minDistance);
    }

    /**
     * Creates a filter that matches all documents containing a field with geospatial data that is near the specified GeoJSON point.
     *
     * @param fieldName   the field name
     * @param geometry    the bounding GeoJSON geometry object
     * @param maxDistance the maximum distance from the point, in meters
     * @param minDistance the minimum distance from the point, in meters
     * @return the filter
     * @since 3.1
     * @mongodb.driver.manual reference/operator/query/near/ $near
     * @mongodb.server.release 2.4
     */
    public static Bson near(final String fieldName, final Bson geometry, final Double maxDistance, final Double minDistance) {
        return new GeometryOperatorFilter<Bson>("$near", fieldName, geometry, maxDistance, minDistance);
    }

    /**
     * Creates a filter that matches all documents containing a field with geospatial data that is near the specified point.
     *
     * @param fieldName   the field name
     * @param x           the x coordinate
     * @param y           the y coordinate
     * @param maxDistance the maximum distance from the point, in radians
     * @param minDistance the minimum distance from the point, in radians
     * @return the filter
     * @since 3.1
     * @mongodb.driver.manual reference/operator/query/near/ $near
     * @mongodb.server.release 2.4
     */
    public static Bson near(final String fieldName, final double x, final double y, final Double maxDistance, final Double minDistance) {
        return createNearFilterDocument(fieldName, x, y, maxDistance, minDistance, "$near");
    }

    /**
     * Creates a filter that matches all documents containing a field with geospatial data that is near the specified GeoJSON point using
     * spherical geometry.
     *
     * @param fieldName   the field name
     * @param geometry    the bounding GeoJSON geometry object
     * @param maxDistance the maximum distance from the point, in meters
     * @param minDistance the minimum distance from the point, in meters
     * @return the filter
     * @since 3.1
     * @mongodb.driver.manual reference/operator/query/near/ $near
     * @mongodb.server.release 2.4
     */
    public static Bson nearSphere(final String fieldName, final Point geometry, final Double maxDistance, final Double minDistance) {
        return new GeometryOperatorFilter<Point>("$nearSphere", fieldName, geometry, maxDistance, minDistance);
    }

    /**
     * Creates a filter that matches all documents containing a field with geospatial data that is near the specified GeoJSON point using
     * spherical geometry.
     *
     * @param fieldName   the field name
     * @param geometry    the bounding GeoJSON geometry object
     * @param maxDistance the maximum distance from the point, in meters
     * @param minDistance the minimum distance from the point, in meters
     * @return the filter
     * @since 3.1
     * @mongodb.driver.manual reference/operator/query/near/ $near
     * @mongodb.server.release 2.4
     */
    public static Bson nearSphere(final String fieldName, final Bson geometry, final Double maxDistance, final Double minDistance) {
        return new GeometryOperatorFilter<Bson>("$nearSphere", fieldName, geometry, maxDistance, minDistance);
    }

    /**
     * Creates a filter that matches all documents containing a field with geospatial data that is near the specified point using
     * spherical geometry.
     *
     * @param fieldName   the field name
     * @param x           the x coordinate
     * @param y           the y coordinate
     * @param maxDistance the maximum distance from the point, in radians
     * @param minDistance the minimum distance from the point, in radians
     * @return the filter
     * @since 3.1
     * @mongodb.driver.manual reference/operator/query/near/ $near
     * @mongodb.server.release 2.4
     */
    public static Bson nearSphere(final String fieldName, final double x, final double y, final Double maxDistance,
                                  final Double minDistance) {
        return createNearFilterDocument(fieldName, x, y, maxDistance, minDistance, "$nearSphere");
    }

    private static Bson createNearFilterDocument(final String fieldName, final double x, final double y, final Double maxDistance,
                                                 final Double minDistance, final String operator) {
        BsonDocument nearFilter = new BsonDocument(operator, new BsonArray(Arrays.asList(new BsonDouble(x), new BsonDouble(y))));
        if (maxDistance != null) {
            nearFilter.append("$maxDistance", new BsonDouble(maxDistance));
        }
        if (minDistance != null) {
            nearFilter.append("$minDistance", new BsonDouble(minDistance));
        }
        return new BsonDocument(fieldName, nearFilter);
    }

    private static String operatorFilterToString(final String fieldName, final String operator, final Object value) {
        return "Operator Filter{"
                       + "fieldName='" + fieldName + '\''
                       + ", operator='" + operator + '\''
                       + ", value=" + value
                       + '}';
    }

    private static final class SimpleFilter implements Bson {
        private final String fieldName;
        private final BsonValue value;

        private SimpleFilter(final String fieldName, final BsonValue value) {
            this.fieldName = notNull("fieldName", fieldName);
            this.value = notNull("value", value);
        }

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

        @Override
        public String toString() {
            return operatorFilterToString(fieldName, "$eq", value);
        }
    }

    private static final class OperatorFilter<TItem> implements Bson {
        private final String operatorName;
        private final String fieldName;
        private final TItem value;

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

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

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

            return writer.getDocument();
        }

        @Override
        public String toString() {
            return operatorFilterToString(fieldName, operatorName, value);
        }
    }

    private static class AndFilter implements Bson {
        private final Iterable<Bson> filters;

        public AndFilter(final Iterable<Bson> filters) {
            this.filters = notNull("filters", filters);
        }

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

            for (Bson filter : filters) {
                BsonDocument renderedRenderable = filter.toBsonDocument(documentClass, codecRegistry);
                for (Map.Entry<String, BsonValue> element : renderedRenderable.entrySet()) {
                    addClause(andRenderable, element);
                }
            }

            if (andRenderable.isEmpty()) {
                andRenderable.append("$and", new BsonArray());
            }

            return andRenderable;
        }

        private void addClause(final BsonDocument document, final Map.Entry<String, BsonValue> clause) {
            if (clause.getKey().equals("$and")) {
                for (BsonValue value : clause.getValue().asArray()) {
                    for (Map.Entry<String, BsonValue> element : value.asDocument().entrySet()) {
                        addClause(document, element);
                    }
                }
            } else if (document.size() == 1 && document.keySet().iterator().next().equals("$and")) {
                document.get("$and").asArray().add(new BsonDocument(clause.getKey(), clause.getValue()));
            } else if (document.containsKey(clause.getKey())) {
                if (document.get(clause.getKey()).isDocument() && clause.getValue().isDocument()) {
                    BsonDocument existingClauseValue = document.get(clause.getKey()).asDocument();
                    BsonDocument clauseValue = clause.getValue().asDocument();
                    if (keysIntersect(clauseValue, existingClauseValue)) {
                        promoteRenderableToDollarForm(document, clause);
                    } else {
                        existingClauseValue.putAll(clauseValue);
                    }
                } else {
                    promoteRenderableToDollarForm(document, clause);
                }
            } else {
                document.append(clause.getKey(), clause.getValue());
            }
        }

        private boolean keysIntersect(final BsonDocument first, final BsonDocument second) {
            for (String name : first.keySet()) {
                if (second.containsKey(name)) {
                    return true;
                }
            }
            return false;
        }

        private void promoteRenderableToDollarForm(final BsonDocument document, final Map.Entry<String, BsonValue> clause) {
            BsonArray clauses = new BsonArray();
            for (Map.Entry<String, BsonValue> queryElement : document.entrySet()) {
                clauses.add(new BsonDocument(queryElement.getKey(), queryElement.getValue()));
            }
            clauses.add(new BsonDocument(clause.getKey(), clause.getValue()));
            document.clear();
            document.put("$and", clauses);
        }

        @Override
        public String toString() {
            return "And Filter{"
                           + "filters=" + filters
                           + '}';
        }
    }

    private static class OrNorFilter implements Bson {
        private enum Operator {
            OR("$or", "Or"),
            NOR("$nor", "Nor");

            private final String name;
            private final String toStringName;

            Operator(final String name, final String toStringName) {
                this.name = name;
                this.toStringName = toStringName;
            }
        }

        private final Operator operator;
        private final Iterable<Bson> filters;

        public OrNorFilter(final Operator operator, final Iterable<Bson> filters) {
            this.operator = notNull("operator", operator);
            this.filters = notNull("filters", filters);
        }

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

            BsonArray filtersArray = new BsonArray();
            for (Bson filter : filters) {
                filtersArray.add(filter.toBsonDocument(documentClass, codecRegistry));
            }

            orRenderable.put(operator.name, filtersArray);

            return orRenderable;
        }

        @Override
        public String toString() {
            return operator.toStringName + " Filter{"
                           + "filters=" + filters
                           + '}';
        }
    }

    private static class IterableOperatorFilter<TItem> implements Bson {
        private final String fieldName;
        private final String operatorName;
        private final Iterable<TItem> values;

        IterableOperatorFilter(final String fieldName, final String operatorName, final Iterable<TItem> values) {
            this.fieldName = notNull("fieldName", fieldName);
            this.operatorName = notNull("operatorName", operatorName);
            this.values = notNull("values", values);
        }

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

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

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

            writer.writeEndDocument();

            return writer.getDocument();
        }

        @Override
        public String toString() {
            return operatorFilterToString(fieldName, operatorName, values);
        }
    }

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

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

        @Override
        public <TDocument> BsonDocument toBsonDocument(final Class<TDocument> documentClass, 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 String toString() {
            return "Filter{"
                           + "fieldName='" + fieldName + '\''
                           + ", value=" + value
                           + '}';
        }
    }

    private static class NotFilter implements Bson {
        private final Bson filter;

        public NotFilter(final Bson filter) {
            this.filter = notNull("filter", filter);
        }

        @Override
        public <TDocument> BsonDocument toBsonDocument(final Class<TDocument> documentClass, final CodecRegistry codecRegistry) {
            BsonDocument filterDocument = filter.toBsonDocument(documentClass, codecRegistry);
            if (filterDocument.size() == 1) {
                Map.Entry<String, BsonValue> entry = filterDocument.entrySet().iterator().next();
                return createFilter(entry.getKey(), entry.getValue());
            } else {
                BsonArray values = new BsonArray();
                for (Map.Entry<String, BsonValue> docs : filterDocument.entrySet()) {
                    values.add(new BsonDocument(docs.getKey(), docs.getValue()));
                }
                return createFilter("$and", values);
            }
        }

        private BsonDocument createFilter(final String fieldName, final BsonValue value) {
            if (fieldName.startsWith("$")) {
                return new BsonDocument("$not", new BsonDocument(fieldName, value));
            } else if (value.isDocument() || value.isRegularExpression()) {
                return new BsonDocument(fieldName, new BsonDocument("$not", value));
            }
            return new BsonDocument(fieldName, new BsonDocument("$not", new BsonDocument("$eq", value)));
        }

        @Override
        public String toString() {
            return "Not Filter{"
                           + "filter=" + filter
                           + '}';
        }
    }

    private static class GeometryOperatorFilter<TItem> implements Bson {
        private final String operatorName;
        private final String fieldName;
        private final TItem geometry;
        private final Double maxDistance;
        private final Double minDistance;

        public GeometryOperatorFilter(final String operatorName, final String fieldName, final TItem geometry) {
            this(operatorName, fieldName, geometry, null, null);
        }

        public GeometryOperatorFilter(final String operatorName, final String fieldName, final TItem geometry,
                                      final Double maxDistance, final Double minDistance) {
            this.operatorName = operatorName;
            this.fieldName = notNull("fieldName", fieldName);
            this.geometry = notNull("geometry", geometry);
            this.maxDistance = maxDistance;
            this.minDistance = minDistance;
        }

        @Override
        public <TDocument> BsonDocument toBsonDocument(final Class<TDocument> documentClass, final CodecRegistry codecRegistry) {
            BsonDocumentWriter writer = new BsonDocumentWriter(new BsonDocument());
            writer.writeStartDocument();
            writer.writeName(fieldName);
            writer.writeStartDocument();
            writer.writeName(operatorName);
            writer.writeStartDocument();
            writer.writeName("$geometry");
            encodeValue(writer, geometry, codecRegistry);
            if (maxDistance != null) {
                writer.writeDouble("$maxDistance", maxDistance);
            }
            if (minDistance != null) {
                writer.writeDouble("$minDistance", minDistance);
            }
            writer.writeEndDocument();
            writer.writeEndDocument();
            writer.writeEndDocument();

            return writer.getDocument();
        }

        @Override
        public String toString() {
            return "Geometry Operator Filter{"
                           + "fieldName='" + fieldName + '\''
                           + ", operator='" + operatorName + '\''
                           + ", geometry=" + geometry
                           + ", maxDistance=" + maxDistance
                           + ", minDistance=" + minDistance
                           + '}';
        }
    }

    private static class TextFilter implements Bson {
        private final String search;
        private final TextSearchOptions textSearchOptions;

        public TextFilter(final String search, final TextSearchOptions textSearchOptions) {
            this.search = search;
            this.textSearchOptions = textSearchOptions;
        }

        @Override
        public <TDocument> BsonDocument toBsonDocument(final Class<TDocument> documentClass, final CodecRegistry codecRegistry) {
            BsonDocument searchDocument = new BsonDocument("$search", new BsonString(search));
            if (textSearchOptions.getLanguage() != null) {
                searchDocument.put("$language", new BsonString(textSearchOptions.getLanguage()));
            }
            if (textSearchOptions.getCaseSensitive() != null) {
                searchDocument.put("$caseSensitive", BsonBoolean.valueOf(textSearchOptions.getCaseSensitive()));
            }
            if (textSearchOptions.getDiacriticSensitive() != null) {
                searchDocument.put("$diacriticSensitive", BsonBoolean.valueOf(textSearchOptions.getDiacriticSensitive()));
            }
            return new BsonDocument("$text", searchDocument);
        }

        @Override
        public String toString() {
            return "Text Filter{"
                           + "search='" + search + '\''
                           + ", textSearchOptions=" + textSearchOptions
                           + '}';
        }
    }

}
