/*
 * 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.types.Decimal128;
import org.bson.types.ObjectId;

import static java.lang.String.format;
import static java.util.Arrays.asList;

/**
 * Abstract base class for BsonReader implementations.
 *
 * @since 3.0
 */
public abstract class AbstractBsonReader implements BsonReader {
    private State state;
    private Context context;
    private BsonType currentBsonType;
    private String currentName;
    private boolean closed;

    /**
     * Initializes a new instance of the BsonReader class.
     */
    protected AbstractBsonReader() {
        state = State.INITIAL;
    }

    @Override
    public BsonType getCurrentBsonType() {
        return currentBsonType;
    }

    @Override
    public String getCurrentName() {
        if (state != State.VALUE) {
            throwInvalidState("getCurrentName", State.VALUE);
        }
        return currentName;
    }

    /**
     * Sets the type of the current value being read.
     *
     * @param newType the BSON Type.
     */
    protected void setCurrentBsonType(final BsonType newType) {
        currentBsonType = newType;
    }

    /**
     * @return The current state of the reader.
     */
    public State getState() {
        return state;
    }

    /**
     * Sets the new current state of this reader.
     *
     * @param newState the state to set this reader to.
     */
    protected void setState(final State newState) {
        state = newState;
    }

    /**
     * Sets the field name for the key/value pair being read.
     *
     * @param newName the field name
     */
    protected void setCurrentName(final String newName) {
        currentName = newName;
    }

    /**
     * Closes the reader.
     */
    public void close() {
        closed = true;
    }

    /**
     * Return true if the reader has been closed.
     *
     * @return true if closed
     */
    protected boolean isClosed() {
        return closed;
    }

    /**
     * Handles the logic to read binary data
     *
     * @return the BsonBinary value
     */
    protected abstract BsonBinary doReadBinaryData();

    /**
     * Handles the logic to peek at the binary subtype.
     *
     * @return the binary subtype
     */
    protected abstract byte doPeekBinarySubType();

    /**
     * Handles the logic to peek at the binary size.
     *
     * @return the binary size
     * @since 3.4
     */
    protected abstract int doPeekBinarySize();

    /**
     * Handles the logic to read booleans
     *
     * @return the boolean value
     */
    protected abstract boolean doReadBoolean();

    /**
     * Handles the logic to read date time
     *
     * @return the long value
     */
    protected abstract long doReadDateTime();

    /**
     * Handles the logic to read doubles
     *
     * @return the double value
     */
    protected abstract double doReadDouble();

    /**
     * Handles the logic when reading the end of an array
     */
    protected abstract void doReadEndArray();

    /**
     * Handles the logic when reading the end of a document
     */
    protected abstract void doReadEndDocument();

    /**
     * Handles the logic to read 32 bit ints
     *
     * @return the int value
     */
    protected abstract int doReadInt32();

    /**
     * Handles the logic to read 64 bit ints
     *
     * @return the long value
     */
    protected abstract long doReadInt64();

    /**
     * Handles the logic to read Decimal128
     *
     * @return the Decimal128 value
     * @since 3.4
     */
    protected abstract Decimal128 doReadDecimal128();

    /**
     * Handles the logic to read Javascript functions
     *
     * @return the String value
     */
    protected abstract String doReadJavaScript();

    /**
     * Handles the logic to read scoped Javascript functions
     *
     * @return the String value
     */
    protected abstract String doReadJavaScriptWithScope();

    /**
     * Handles the logic to read a Max key
     */
    protected abstract void doReadMaxKey();

    /**
     * Handles the logic to read a Min key
     */
    protected abstract void doReadMinKey();

    /**
     * Handles the logic to read a null value
     */
    protected abstract void doReadNull();

    /**
     * Handles the logic to read an ObjectId
     *
     * @return the ObjectId value
     */
    protected abstract ObjectId doReadObjectId();

    /**
     * Handles the logic to read a regular expression
     *
     * @return the BsonRegularExpression value
     */
    protected abstract BsonRegularExpression doReadRegularExpression();

    /**
     * Handles the logic to read a DBPointer
     *
     * @return the BsonDbPointer value
     */
    protected abstract BsonDbPointer doReadDBPointer();

    /**
     * Handles the logic to read the start of an array
     */
    protected abstract void doReadStartArray();

    /**
     * Handles the logic to read the start of a document
     */
    protected abstract void doReadStartDocument();

    /**
     * Handles the logic to read a String
     *
     * @return the String value
     */
    protected abstract String doReadString();

    /**
     * Handles the logic to read a Symbol
     *
     * @return the String value
     */
    protected abstract String doReadSymbol();

    /**
     * Handles the logic to read a timestamp
     *
     * @return the BsonTimestamp value
     */
    protected abstract BsonTimestamp doReadTimestamp();

    /**
     * Handles the logic to read an Undefined value
     */
    protected abstract void doReadUndefined();

    /**
     * Handles any logic required to skip the name (reader must be positioned on a name).
     */
    protected abstract void doSkipName();

    /**
     * Handles any logic required to skip the value (reader must be positioned on a value).
     */
    protected abstract void doSkipValue();

    @Override
    public BsonBinary readBinaryData() {
        checkPreconditions("readBinaryData", BsonType.BINARY);
        setState(getNextState());
        return doReadBinaryData();
    }

    @Override
    public byte peekBinarySubType() {
        checkPreconditions("readBinaryData", BsonType.BINARY);
        return doPeekBinarySubType();
    }

    @Override
    public int peekBinarySize() {
        checkPreconditions("readBinaryData", BsonType.BINARY);
        return doPeekBinarySize();
    }

    @Override
    public boolean readBoolean() {
        checkPreconditions("readBoolean", BsonType.BOOLEAN);
        setState(getNextState());
        return doReadBoolean();
    }

    @Override
    public abstract BsonType readBsonType();

    @Override
    public long readDateTime() {
        checkPreconditions("readDateTime", BsonType.DATE_TIME);
        setState(getNextState());
        return doReadDateTime();
    }

    @Override
    public double readDouble() {
        checkPreconditions("readDouble", BsonType.DOUBLE);
        setState(getNextState());
        return doReadDouble();
    }

    @Override
    public void readEndArray() {
        if (isClosed()) {
            throw new IllegalStateException("BSONBinaryWriter");
        }
        if (getContext().getContextType() != BsonContextType.ARRAY) {
            throwInvalidContextType("readEndArray", getContext().getContextType(), BsonContextType.ARRAY);
        }
        if (getState() == State.TYPE) {
            readBsonType(); // will set state to EndOfArray if at end of array
        }
        if (getState() != State.END_OF_ARRAY) {
            throwInvalidState("ReadEndArray", State.END_OF_ARRAY);
        }

        doReadEndArray();

        setStateOnEnd();
    }

    @Override
    public void readEndDocument() {
        if (isClosed()) {
            throw new IllegalStateException("BSONBinaryWriter");
        }
        if (getContext().getContextType() != BsonContextType.DOCUMENT && getContext().getContextType() != BsonContextType.SCOPE_DOCUMENT) {
            throwInvalidContextType("readEndDocument",
                                    getContext().getContextType(), BsonContextType.DOCUMENT, BsonContextType.SCOPE_DOCUMENT);
        }
        if (getState() == State.TYPE) {
            readBsonType(); // will set state to EndOfDocument if at end of document
        }
        if (getState() != State.END_OF_DOCUMENT) {
            throwInvalidState("readEndDocument", State.END_OF_DOCUMENT);
        }

        doReadEndDocument();

        setStateOnEnd();
    }

    @Override
    public int readInt32() {
        checkPreconditions("readInt32", BsonType.INT32);
        setState(getNextState());
        return doReadInt32();

    }

    @Override
    public long readInt64() {
        checkPreconditions("readInt64", BsonType.INT64);
        setState(getNextState());
        return doReadInt64();
    }

    @Override
    public Decimal128 readDecimal128() {
        checkPreconditions("readDecimal", BsonType.DECIMAL128);
        setState(getNextState());
        return doReadDecimal128();
    }

    @Override
    public String readJavaScript() {
        checkPreconditions("readJavaScript", BsonType.JAVASCRIPT);
        setState(getNextState());
        return doReadJavaScript();
    }

    @Override
    public String readJavaScriptWithScope() {
        checkPreconditions("readJavaScriptWithScope", BsonType.JAVASCRIPT_WITH_SCOPE);
        setState(State.SCOPE_DOCUMENT);
        return doReadJavaScriptWithScope();
    }

    @Override
    public void readMaxKey() {
        checkPreconditions("readMaxKey", BsonType.MAX_KEY);
        setState(getNextState());
        doReadMaxKey();
    }

    @Override
    public void readMinKey() {
        checkPreconditions("readMinKey", BsonType.MIN_KEY);
        setState(getNextState());
        doReadMinKey();
    }

    @Override
    public void readNull() {
        checkPreconditions("readNull", BsonType.NULL);
        setState(getNextState());
        doReadNull();
    }

    @Override
    public ObjectId readObjectId() {
        checkPreconditions("readObjectId", BsonType.OBJECT_ID);
        setState(getNextState());
        return doReadObjectId();
    }

    @Override
    public BsonRegularExpression readRegularExpression() {
        checkPreconditions("readRegularExpression", BsonType.REGULAR_EXPRESSION);
        setState(getNextState());
        return doReadRegularExpression();
    }

    @Override
    public BsonDbPointer readDBPointer() {
        checkPreconditions("readDBPointer", BsonType.DB_POINTER);
        setState(getNextState());
        return doReadDBPointer();
    }

    @Override
    public void readStartArray() {
        checkPreconditions("readStartArray", BsonType.ARRAY);
        doReadStartArray();
        setState(State.TYPE);
    }

    @Override
    public void readStartDocument() {
        checkPreconditions("readStartDocument", BsonType.DOCUMENT);
        doReadStartDocument();
        setState(State.TYPE);
    }

    @Override
    public String readString() {
        checkPreconditions("readString", BsonType.STRING);
        setState(getNextState());
        return doReadString();
    }

    @Override
    public String readSymbol() {
        checkPreconditions("readSymbol", BsonType.SYMBOL);
        setState(getNextState());
        return doReadSymbol();
    }

    @Override
    public BsonTimestamp readTimestamp() {
        checkPreconditions("readTimestamp", BsonType.TIMESTAMP);
        setState(getNextState());
        return doReadTimestamp();
    }

    @Override
    public void readUndefined() {
        checkPreconditions("readUndefined", BsonType.UNDEFINED);
        setState(getNextState());
        doReadUndefined();
    }

    @Override
    public void skipName() {
        if (isClosed()) {
            throw new IllegalStateException("This instance has been closed");
        }
        if (getState() != State.NAME) {
            throwInvalidState("skipName", State.NAME);
        }
        setState(State.VALUE);
        doSkipName();
    }

    @Override
    public void skipValue() {
        if (isClosed()) {
            throw new IllegalStateException("BSONBinaryWriter");
        }
        if (getState() != State.VALUE) {
            throwInvalidState("skipValue", State.VALUE);
        }

        doSkipValue();

        setState(State.TYPE);
    }

    @Override
    public BsonBinary readBinaryData(final String name) {
        verifyName(name);
        return readBinaryData();
    }

    @Override
    public boolean readBoolean(final String name) {
        verifyName(name);
        return readBoolean();
    }

    @Override
    public long readDateTime(final String name) {
        verifyName(name);
        return readDateTime();
    }

    @Override
    public double readDouble(final String name) {
        verifyName(name);
        return readDouble();
    }

    @Override
    public int readInt32(final String name) {
        verifyName(name);
        return readInt32();
    }

    @Override
    public long readInt64(final String name) {
        verifyName(name);
        return readInt64();
    }

    @Override
    public Decimal128 readDecimal128(final String name) {
        verifyName(name);
        return readDecimal128();
    }

    @Override
    public String readJavaScript(final String name) {
        verifyName(name);
        return readJavaScript();
    }

    @Override
    public String readJavaScriptWithScope(final String name) {
        verifyName(name);
        return readJavaScriptWithScope();
    }

    @Override
    public void readMaxKey(final String name) {
        verifyName(name);
        readMaxKey();
    }

    @Override
    public void readMinKey(final String name) {
        verifyName(name);
        readMinKey();
    }

    @Override
    public String readName() {
        if (state == State.TYPE) {
            readBsonType();
        }
        if (state != State.NAME) {
            throwInvalidState("readName", State.NAME);
        }

        state = State.VALUE;
        return currentName;
    }

    @Override
    public void readName(final String name) {
        verifyName(name);
    }

    @Override
    public void readNull(final String name) {
        verifyName(name);
        readNull();
    }

    @Override
    public ObjectId readObjectId(final String name) {
        verifyName(name);
        return readObjectId();
    }

    @Override
    public BsonRegularExpression readRegularExpression(final String name) {
        verifyName(name);
        return readRegularExpression();
    }

    @Override
    public BsonDbPointer readDBPointer(final String name) {
        verifyName(name);
        return readDBPointer();
    }

    @Override
    public String readString(final String name) {
        verifyName(name);
        return readString();
    }

    @Override
    public String readSymbol(final String name) {
        verifyName(name);
        return readSymbol();
    }

    @Override
    public BsonTimestamp readTimestamp(final String name) {
        verifyName(name);
        return readTimestamp();
    }

    @Override
    public void readUndefined(final String name) {
        verifyName(name);
        readUndefined();
    }

    /**
     * Throws an InvalidOperationException when the method called is not valid for the current ContextType.
     *
     * @param methodName        The name of the method.
     * @param actualContextType The actual ContextType.
     * @param validContextTypes The valid ContextTypes.
     * @throws BsonInvalidOperationException when the method called is not valid for the current ContextType.
     */
    protected void throwInvalidContextType(final String methodName, final BsonContextType actualContextType,
                                           final BsonContextType... validContextTypes) {
        String validContextTypesString = StringUtils.join(" or ", asList(validContextTypes));
        String message = format("%s can only be called when ContextType is %s, not when ContextType is %s.",
                                methodName, validContextTypesString, actualContextType);
        throw new BsonInvalidOperationException(message);
    }

    /**
     * Throws an InvalidOperationException when the method called is not valid for the current state.
     *
     * @param methodName  The name of the method.
     * @param validStates The valid states.
     * @throws BsonInvalidOperationException when the method called is not valid for the current state.
     */
    protected void throwInvalidState(final String methodName, final State... validStates) {
        String validStatesString = StringUtils.join(" or ", asList(validStates));
        String message = format("%s can only be called when State is %s, not when State is %s.",
                                methodName, validStatesString, state);
        throw new BsonInvalidOperationException(message);
    }

    /**
     * Verifies the current state and BSONType of the reader.
     *
     * @param methodName       The name of the method calling this one.
     * @param requiredBsonType The required BSON type.
     */
    protected void verifyBSONType(final String methodName, final BsonType requiredBsonType) {
        if (state == State.INITIAL || state == State.SCOPE_DOCUMENT || state == State.TYPE) {
            readBsonType();
        }
        if (state == State.NAME) {
            // ignore name
            skipName();
        }
        if (state != State.VALUE) {
            throwInvalidState(methodName, State.VALUE);
        }
        if (currentBsonType != requiredBsonType) {
            throw new BsonInvalidOperationException(format("%s can only be called when CurrentBSONType is %s, "
                                                           + "not when CurrentBSONType is %s.",
                                                           methodName, requiredBsonType, currentBsonType));
        }
    }

    /**
     * Verifies the name of the current element.
     *
     * @param expectedName The expected name.
     * @throws BsonSerializationException when the name read is not the expected name
     */
    protected void verifyName(final String expectedName) {
        readBsonType();
        String actualName = readName();
        if (!actualName.equals(expectedName)) {
            throw new BsonSerializationException(format("Expected element name to be '%s', not '%s'.",
                                                        expectedName, actualName));
        }
    }

    /**
     * Ensures any conditions are met before reading commences.  Throws exceptions if the conditions are not met.
     *
     * @param methodName the name of the current method, which will indicate the field being read
     * @param type       the type of this field
     */
    protected void checkPreconditions(final String methodName, final BsonType type) {
        if (isClosed()) {
            throw new IllegalStateException("BsonWriter is closed");
        }

        verifyBSONType(methodName, type);
    }

    /**
     * Get the context, which will indicate which state the reader is in, for example which part of a document it's currently reading.
     *
     * @return the context
     */
    protected Context getContext() {
        return context;
    }

    /**
     * Set the context, which will indicate which state the reader is in, for example which part of a document it's currently reading.
     *
     * @param context the current context.
     */
    protected void setContext(final Context context) {
        this.context = context;
    }

    /**
     * Returns the next {@code State} to transition to, based on the {@link org.bson.AbstractBsonReader.Context} of this reader.
     *
     * @return the next state
     */
    protected State getNextState() {
        switch (context.getContextType()) {
            case ARRAY:
            case DOCUMENT:
            case SCOPE_DOCUMENT:
                return State.TYPE;
            case TOP_LEVEL:
                return State.DONE;
            default:
                throw new BSONException(format("Unexpected ContextType %s.", context.getContextType()));
        }
    }

    private void setStateOnEnd() {
        switch (getContext().getContextType()) {
            case ARRAY:
            case DOCUMENT:
                setState(State.TYPE);
                break;
            case TOP_LEVEL:
                setState(State.DONE);
                break;
            default:
                throw new BSONException(format("Unexpected ContextType %s.", getContext().getContextType()));
        }
    }

    protected class Mark implements BsonReaderMark {
        private final State state;
        private final Context parentContext;
        private final BsonContextType contextType;
        private final BsonType currentBsonType;
        private final String currentName;

        protected Context getParentContext() {
            return parentContext;
        }

        protected BsonContextType getContextType() {
            return contextType;
        }

        protected Mark() {
            state = AbstractBsonReader.this.state;
            parentContext = AbstractBsonReader.this.context.parentContext;
            contextType = AbstractBsonReader.this.context.contextType;
            currentBsonType = AbstractBsonReader.this.currentBsonType;
            currentName = AbstractBsonReader.this.currentName;
        }

        public void reset() {
            AbstractBsonReader.this.state = state;
            AbstractBsonReader.this.currentBsonType = currentBsonType;
            AbstractBsonReader.this.currentName = currentName;
        }
    }

    /**
     * The context for the reader. Records the parent context, creating a bread crumb trail to trace back up to the root context of the
     * reader. Also records the {@link org.bson.BsonContextType}, indicating whether the reader is reading a document, array, or other
     * complex sub-structure.
     */
    protected abstract class Context {

        private final Context parentContext;
        private final BsonContextType contextType;

        /**
         * Creates a new instance.
         *
         * @param parentContext a possibly null value for the context that came before this one
         * @param contextType   the type of this context
         */
        protected Context(final Context parentContext, final BsonContextType contextType) {
            this.parentContext = parentContext;
            this.contextType = contextType;
        }

        /**
         * Returns the parent context.  Allows users of this context object to transition to this parent context.
         *
         * @return the context that came before this one
         */
        protected Context getParentContext() {
            return parentContext;
        }

        /**
         * Return the type of this context.
         *
         * @return the context type.
         */
        protected BsonContextType getContextType() {
            return contextType;
        }
    }

    /**
     * The state of a reader.  Indicates where in a document the reader is.
     */
    public enum State {
        /**
         * The initial state.
         */
        INITIAL,

        /**
         * The reader is positioned at the type of an element or value.
         */
        TYPE,

        /**
         * The reader is positioned at the name of an element.
         */
        NAME,

        /**
         * The reader is positioned at a value.
         */
        VALUE,

        /**
         * The reader is positioned at a scope document.
         */
        SCOPE_DOCUMENT,

        /**
         * The reader is positioned at the end of a document.
         */
        END_OF_DOCUMENT,

        /**
         * The reader is positioned at the end of an array.
         */
        END_OF_ARRAY,

        /**
         * The reader has finished reading a document.
         */
        DONE,

        /**
         * The reader is closed.
         */
        CLOSED
    }
}


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

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.HashSet;
import java.util.Set;

import static java.math.MathContext.DECIMAL128;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;

/**
 * A binary integer decimal representation of a 128-bit decimal value, supporting 34 decimal digits of significand and an exponent range
 * of -6143 to +6144.
 *
 * @since 3.4
 * @see BSON Decimal128
 * specification
 * @see binary integer decimal
 * @see decimal128 floating-point format
 * @see 754-2008 - IEEE Standard for Floating-Point Arithmetic
 */
public final class Decimal128 extends Number implements Comparable<Decimal128> {

    private static final long serialVersionUID = 4570973266503637887L;

    private static final long INFINITY_MASK = 0x7800000000000000L;
    private static final long NaN_MASK = 0x7c00000000000000L;
    private static final long SIGN_BIT_MASK = 1L << 63;
    private static final int MIN_EXPONENT = -6176;
    private static final int MAX_EXPONENT = 6111;

    private static final int EXPONENT_OFFSET = 6176;
    private static final int MAX_BIT_LENGTH = 113;

    private static final BigInteger BIG_INT_TEN = new BigInteger("10");
    private static final BigInteger BIG_INT_ONE = new BigInteger("1");
    private static final BigInteger BIG_INT_ZERO = new BigInteger("0");

    private static final Set<String> NaN_STRINGS = new HashSet<String>(singletonList("nan"));
    private static final Set<String> NEGATIVE_NaN_STRINGS = new HashSet<String>(singletonList("-nan"));
    private static final Set<String> POSITIVE_INFINITY_STRINGS = new HashSet<String>(asList("inf", "+inf", "infinity", "+infinity"));
    private static final Set<String> NEGATIVE_INFINITY_STRINGS = new HashSet<String>(asList("-inf", "-infinity"));

    /**
     * A constant holding the positive infinity of type {@code Decimal128}.  It is equal to the value return by
     * {@code Decimal128.valueOf("Infinity")}.
     */
    public static final Decimal128 POSITIVE_INFINITY = fromIEEE754BIDEncoding(INFINITY_MASK, 0);

    /**
     * A constant holding the negative infinity of type {@code Decimal128}.  It is equal to the value return by
     * {@code Decimal128.valueOf("-Infinity")}.
     */
    public static final Decimal128 NEGATIVE_INFINITY = fromIEEE754BIDEncoding(INFINITY_MASK | SIGN_BIT_MASK, 0);

    /**
     * A constant holding a negative Not-a-Number (-NaN) value of type {@code Decimal128}.  It is equal to the value return by
     * {@code Decimal128.valueOf("-NaN")}.
     */
    public static final Decimal128 NEGATIVE_NaN = fromIEEE754BIDEncoding(NaN_MASK | SIGN_BIT_MASK, 0);

    /**
     * A constant holding a Not-a-Number (NaN) value of type {@code Decimal128}.  It is equal to the value return by
     * {@code Decimal128.valueOf("NaN")}.
     */
    public static final Decimal128 NaN = fromIEEE754BIDEncoding(NaN_MASK, 0);

    /**
     * A constant holding a postive zero value of type {@code Decimal128}.  It is equal to the value return by
     * {@code Decimal128.valueOf("0")}.
     */
    public static final Decimal128 POSITIVE_ZERO = fromIEEE754BIDEncoding(0x3040000000000000L, 0x0000000000000000L);

    /**
     * A constant holding a negative zero value of type {@code Decimal128}.  It is equal to the value return by
     * {@code Decimal128.valueOf("-0")}.
     */
    public static final Decimal128 NEGATIVE_ZERO = fromIEEE754BIDEncoding(0xb040000000000000L, 0x0000000000000000L);

    private final long high;
    private final long low;

    /**
     * Returns a Decimal128 value representing the given String.
     *
     * @param value the Decimal128 value represented as a String
     * @return the Decimal128 value representing the given String
     * @throws NumberFormatException if the value is out of the Decimal128 range
     * @see
     *
     *     From-String Specification
     */
    public static Decimal128 parse(final String value) {
        String lowerCasedValue = value.toLowerCase();

        if (NaN_STRINGS.contains(lowerCasedValue)) {
            return NaN;
        }
        if (NEGATIVE_NaN_STRINGS.contains(lowerCasedValue)) {
            return NEGATIVE_NaN;
        }
        if (POSITIVE_INFINITY_STRINGS.contains(lowerCasedValue)) {
            return POSITIVE_INFINITY;
        }
        if (NEGATIVE_INFINITY_STRINGS.contains(lowerCasedValue)) {
            return NEGATIVE_INFINITY;
        }
        return new Decimal128(new BigDecimal(value), value.charAt(0) == '-');
    }

    /**
     * Create an instance with the given high and low order bits representing this Decimal128 as an IEEE 754-2008 128-bit decimal
     * floating point using the BID encoding scheme.
     *
     * @param high the high-order 64 bits
     * @param low  the low-order 64 bits
     * @return the Decimal128 value representing the given high and low order bits
     */
    public static Decimal128 fromIEEE754BIDEncoding(final long high, final long low) {
        return new Decimal128(high, low);
    }

    /**
     * Constructs a Decimal128 value representing the given long.
     *
     * @param value the Decimal128 value represented as a long
     */
    public Decimal128(final long value) {
        this(new BigDecimal(value, DECIMAL128));
    }

    /**
     * Constructs a Decimal128 value representing the given BigDecimal.
     *
     * @param value the Decimal128 value represented as a BigDecimal
     * @throws NumberFormatException if the value is out of the Decimal128 range
     */
    public Decimal128(final BigDecimal value) {
        this(value, value.signum() == -1);
    }

    private Decimal128(final long high, final long low) {
        this.high = high;
        this.low = low;
    }

    // isNegative is necessary to detect -0, which can't be represented with a BigDecimal
    private Decimal128(final BigDecimal initialValue, final boolean isNegative) {
        long localHigh = 0;
        long localLow = 0;

        BigDecimal value = clampAndRound(initialValue);

        long exponent = -value.scale();

        if ((exponent < MIN_EXPONENT) || (exponent > MAX_EXPONENT)) {
            throw new AssertionError("Exponent is out of range for Decimal128 encoding: " + exponent); }

        if (value.unscaledValue().bitLength() > MAX_BIT_LENGTH) {
            throw new AssertionError("Unscaled roundedValue is out of range for Decimal128 encoding:" + value.unscaledValue());
        }

        BigInteger significand = value.unscaledValue().abs();
        int bitLength = significand.bitLength();

        for (int i = 0; i < Math.min(64, bitLength); i++) {
            if (significand.testBit(i)) {
                localLow |= 1L << i;
            }
        }

        for (int i = 64; i < bitLength; i++) {
            if (significand.testBit(i)) {
                localHigh |= 1L << (i - 64);
            }
        }

        long biasedExponent = exponent + EXPONENT_OFFSET;

        localHigh |= biasedExponent << 49;

        if (value.signum() == -1 || isNegative) {
            localHigh |= SIGN_BIT_MASK;
        }

        high = localHigh;
        low = localLow;
    }

    private BigDecimal clampAndRound(final BigDecimal initialValue) {
        BigDecimal value;
        if (-initialValue.scale() > MAX_EXPONENT) {
            int diff = -initialValue.scale() - MAX_EXPONENT;
            if (initialValue.unscaledValue().equals(BIG_INT_ZERO)) {
                value = new BigDecimal(initialValue.unscaledValue(), -MAX_EXPONENT);
            } else if (diff + initialValue.precision() > 34) {
                throw new NumberFormatException("Exponent is out of range for Decimal128 encoding of " + initialValue);
            } else {
                BigInteger multiplier = BIG_INT_TEN.pow(diff);
                value = new BigDecimal(initialValue.unscaledValue().multiply(multiplier), initialValue.scale() + diff);
            }
        } else if (-initialValue.scale() < MIN_EXPONENT) {
            // Increasing a very negative exponent may require decreasing precision, which is rounding
            // Only round exactly (by removing precision that is all zeroes).  An exception is thrown if the rounding would be inexact:
            // Exact:     .000...0011000  => 11000E-6177  => 1100E-6176  => .000001100
            // Inexact:   .000...0011001  => 11001E-6177  => 1100E-6176  => .000001100
            int diff = initialValue.scale() + MIN_EXPONENT;
            int undiscardedPrecision = ensureExactRounding(initialValue, diff);
            BigInteger divisor = undiscardedPrecision == 0 ? BIG_INT_ONE : BIG_INT_TEN.pow(diff);
            value = new BigDecimal(initialValue.unscaledValue().divide(divisor), initialValue.scale() - diff);
        } else {
            value = initialValue.round(DECIMAL128);
            int extraPrecision = initialValue.precision() - value.precision();
            if (extraPrecision > 0) {
                // Again, only round exactly
                ensureExactRounding(initialValue, extraPrecision);
            }
        }
        return value;
    }

    private int ensureExactRounding(final BigDecimal initialValue, final int extraPrecision) {
        String significand = initialValue.unscaledValue().abs().toString();
        int undiscardedPrecision = Math.max(0, significand.length() - extraPrecision);
        for (int i = undiscardedPrecision; i < significand.length(); i++) {
            if (significand.charAt(i) != '0') {
                throw new NumberFormatException("Conversion to Decimal128 would require inexact rounding of " + initialValue);
            }
        }
        return undiscardedPrecision;
    }

    /**
     * Gets the high-order 64 bits of the IEEE 754-2008 128-bit decimal floating point encoding for this Decimal128, using the BID encoding
     * scheme.
     *
     * @return the high-order 64 bits of this Decimal128
     */
    public long getHigh() {
        return high;
    }

    /**
     * Gets the low-order 64 bits of the IEEE 754-2008 128-bit decimal floating point encoding for this Decimal128, using the BID encoding
     * scheme.
     *
     * @return the low-order 64 bits of this Decimal128
     */
    public long getLow() {
        return low;
    }

    /**
     * Gets a BigDecimal that is equivalent to this Decimal128.
     *
     * @return a BigDecimal that is equivalent to this Decimal128
     * @throws ArithmeticException if the Decimal128 value is NaN, Infinity, -Infinity, or -0, none of which can be represented as a
     * BigDecimal
     */
    public BigDecimal bigDecimalValue() {

        if (isNaN()) {
            throw new ArithmeticException("NaN can not be converted to a BigDecimal");
        }

        if (isInfinite()) {
            throw new ArithmeticException("Infinity can not be converted to a BigDecimal");
        }

        BigDecimal bigDecimal = bigDecimalValueNoNegativeZeroCheck();

        // If the BigDecimal is 0, but the Decimal128 is negative, that means we have -0.
        if (isNegative() && bigDecimal.signum() == 0) {
            throw new ArithmeticException("Negative zero can not be converted to a BigDecimal");
        }

        return bigDecimal;
    }

    // Make sure that the argument comes from a call to bigDecimalValueNoNegativeZeroCheck on this instance
    private boolean hasDifferentSign(final BigDecimal bigDecimal) {
        return isNegative() && bigDecimal.signum() == 0;
    }

    private boolean isZero(final BigDecimal bigDecimal) {
        return !isNaN() && !isInfinite() && bigDecimal.compareTo(BigDecimal.ZERO) == 0;
    }

    private BigDecimal bigDecimalValueNoNegativeZeroCheck() {
        int scale = -getExponent();

        if (twoHighestCombinationBitsAreSet()) {
            return BigDecimal.valueOf(0, scale);
        }

        return new BigDecimal(new BigInteger(isNegative() ? -1 : 1, getBytes()), scale);
    }

    // May have leading zeros.  Strip them before considering making this method public
    private byte[] getBytes() {
        byte[] bytes = new byte[15];

        long mask = 0x00000000000000ff;
        for (int i = 14; i >= 7; i--) {
            bytes[i] = (byte) ((low & mask) >>> ((14 - i) << 3));
            mask = mask << 8;
        }

        mask = 0x00000000000000ff;
        for (int i = 6; i >= 1; i--) {
            bytes[i] = (byte) ((high & mask) >>> ((6 - i) << 3));
            mask = mask << 8;
        }

        mask = 0x0001000000000000L;
        bytes[0] = (byte) ((high & mask) >>> 48);
        return bytes;
    }

    private int getExponent() {
        if (twoHighestCombinationBitsAreSet()) {
            return (int) ((high & 0x1fffe00000000000L) >>> 47) - EXPONENT_OFFSET;
        } else {
            return (int) ((high & 0x7fff800000000000L) >>> 49) - EXPONENT_OFFSET;
        }
    }

    private boolean twoHighestCombinationBitsAreSet() {
        return (high & 3L << 61) == 3L << 61;
    }

    /**
     * Returns true if this Decimal128 is negative.
     *
     * @return true if this Decimal128 is negative
     */
    public boolean isNegative() {
        return (high & SIGN_BIT_MASK) == SIGN_BIT_MASK;
    }

    /**
     * Returns true if this Decimal128 is infinite.
     *
     * @return true if this Decimal128 is infinite
     */
    public boolean isInfinite() {
        return (high & INFINITY_MASK) == INFINITY_MASK;
    }

    /**
     * Returns true if this Decimal128 is finite.
     *
     * @return true if this Decimal128 is finite
     */
    public boolean isFinite() {
        return !isInfinite();
    }

    /**
     * Returns true if this Decimal128 is Not-A-Number (NaN).
     *
     * @return true if this Decimal128 is Not-A-Number
     */
    public boolean isNaN() {
        return (high & NaN_MASK) == NaN_MASK;
    }

    @Override
    public int compareTo(final Decimal128 o) {
        if (isNaN()) {
            return o.isNaN() ? 0 : 1;
        }
        if (isInfinite()) {
            if (isNegative()) {
                if (o.isInfinite() && o.isNegative()) {
                    return 0;
                } else {
                    return -1;
                }
            } else {
                if (o.isNaN()) {
                    return -1;
                } else if (o.isInfinite() && !o.isNegative()) {
                    return 0;
                } else {
                    return 1;
                }
            }
        }
        BigDecimal bigDecimal = bigDecimalValueNoNegativeZeroCheck();
        BigDecimal otherBigDecimal = o.bigDecimalValueNoNegativeZeroCheck();

        if (isZero(bigDecimal) && o.isZero(otherBigDecimal)) {
            if (hasDifferentSign(bigDecimal)) {
                if (o.hasDifferentSign(otherBigDecimal)) {
                    return 0;
                }
                else {
                    return -1;
                }
            } else if (o.hasDifferentSign(otherBigDecimal)) {
                return 1;
            }
        }

        if (o.isNaN()) {
            return -1;
        } else if (o.isInfinite()) {
            if (o.isNegative()) {
                return 1;
            } else {
                return -1;
            }
        } else {
            return bigDecimal.compareTo(otherBigDecimal);
        }
    }

    /**
     * Converts this {@code Decimal128} to a {@code int}. This conversion is analogous to the narrowing primitive conversion from
     * {@code double} to {@code int} as defined in The Java™ Language Specification: any fractional part of this
     * {@code Decimal128} will be discarded, and if the resulting integral value is too big to fit in a {@code int}, only the
     * low-order 32 bits are returned. Note that this conversion can lose information about the overall magnitude and precision of this
     * {@code Decimal128} value as well as return a result with the opposite sign. Note that {@code #NEGATIVE_ZERO} is converted to
     * {@code 0}.
     *
     * @return this {@code Decimal128} converted to a {@code int}.
     * @since 3.10
     */
    @Override
    public int intValue() {
        return (int) doubleValue();
    }

    /**
     * Converts this {@code Decimal128} to a {@code long}. This conversion is analogous to the narrowing primitive conversion from
     * {@code double} to {@code long} as defined in The Java™ Language Specification: any fractional part of this
     * {@code Decimal128} will be discarded, and if the resulting integral value is too big to fit in a {@code long}, only the
     * low-order 64 bits are returned. Note that this conversion can lose information about the overall magnitude and precision of this
     * {@code Decimal128} value as well as return a result with the opposite sign. Note that {@code #NEGATIVE_ZERO} is converted to
     * {@code 0L}.
     *
     * @return this {@code Decimal128} converted to a {@code long}.
     * @since 3.10
     */
    @Override
    public long longValue() {
        return (long) doubleValue();
    }

    /**
     * Converts this {@code Decimal128} to a {@code float}. This conversion is similar to the narrowing primitive conversion from
     * {@code double} to {@code float} as defined in The Java™ Language Specification: if this {@code Decimal128} has
     * too great a magnitude to represent as a {@code float}, it will be converted to {@link Float#NEGATIVE_INFINITY} or
     * {@link Float#POSITIVE_INFINITY} as appropriate.  Note that even when the return value is finite, this conversion can lose
     * information about the precision of the {@code Decimal128} value. Note that {@code #NEGATIVE_ZERO} is converted to {@code 0.0f}.
     *
     * @return this {@code Decimal128} converted to a {@code float}.
     * @since 3.10
     */
    @Override
    public float floatValue() {
        return (float) doubleValue();
    }

    /**
     * Converts this {@code Decimal128} to a {@code double}. This conversion is similar to the narrowing primitive conversion from
     * {@code double} to {@code float} as defined in The Java™ Language Specification: if this {@code Decimal128} has
     * too great a magnitude to represent as a {@code double}, it will be converted to {@link Double#NEGATIVE_INFINITY} or
     * {@link Double#POSITIVE_INFINITY} as appropriate.  Note that even when the return value is finite, this conversion can lose
     * information about the precision of the {@code Decimal128} value. Note that {@code #NEGATIVE_ZERO} is converted to {@code 0.0d}.
     *
     * @return this {@code Decimal128} converted to a {@code double}.
     * @since 3.10
     */
    @Override
    public double doubleValue() {
        if (isNaN()) {
            return Double.NaN;
        }
        if (isInfinite()) {
            if (isNegative()) {
                return Double.NEGATIVE_INFINITY;
            } else {
                return Double.POSITIVE_INFINITY;
            }
        }

        BigDecimal bigDecimal = bigDecimalValueNoNegativeZeroCheck();

        if (hasDifferentSign(bigDecimal)) {
            return -0.0d;
        }

        return bigDecimal.doubleValue();
    }

    /**
     * Returns true if the encoded representation of this instance is the same as the encoded representation of {@code o}.
     *

     * One consequence is that, whereas {@code Double.NaN != Double.NaN},
     * {@code new Decimal128("NaN").equals(new Decimal128("NaN")} returns true.
     *

     *

     * Another consequence is that, as with BigDecimal, {@code new Decimal128("1.0").equals(new Decimal128("1.00")} returns false,
     * because the precision is not the same and therefore the representation is not the same.
     *

     *
     * @param o the object to compare for equality
     * @return true if the instances are equal
     */
    @Override
    public boolean equals(final Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        Decimal128 that = (Decimal128) o;

        if (high != that.high) {
            return false;
        }
        if (low != that.low) {
            return false;
        }

        return true;
    }

    @Override
    public int hashCode() {
        int result = (int) (low ^ (low >>> 32));
        result = 31 * result + (int) (high ^ (high >>> 32));
        return result;
    }

    /**
     * Returns the String representation of the Decimal128 value.
     *
     * @return the String representation
     * @see
     *     To-String Sprecification
     */
    @Override
    public String toString() {
        if (isNaN()) {
            return "NaN";
        }
        if (isInfinite()) {
            if (isNegative()) {
                return "-Infinity";
            } else {
                return "Infinity";
            }
        }
        return toStringWithBigDecimal();
    }

    private String toStringWithBigDecimal() {
        StringBuilder buffer = new StringBuilder();

        BigDecimal bigDecimal = bigDecimalValueNoNegativeZeroCheck();
        String significand = bigDecimal.unscaledValue().abs().toString();

        if (isNegative()) {
            buffer.append('-');
        }

        int exponent = -bigDecimal.scale();
        int adjustedExponent = exponent + (significand.length() - 1);
        if (exponent <= 0 && adjustedExponent >= -6) {
            if (exponent == 0) {
                buffer.append(significand);
            } else {
                int pad = -exponent - significand.length();
                if (pad >= 0) {
                    buffer.append('0');
                    buffer.append('.');
                    for (int i = 0; i < pad; i++) {
                        buffer.append('0');
                    }
                    buffer.append(significand, 0, significand.length());
                } else {
                    buffer.append(significand, 0, -pad);
                    buffer.append('.');
                    buffer.append(significand, -pad, -pad - exponent);
                }
            }
        } else {
            buffer.append(significand.charAt(0));
            if (significand.length() > 1) {
                buffer.append('.');
                buffer.append(significand, 1, significand.length());
            }
            buffer.append('E');
            if (adjustedExponent > 0) {
                buffer.append('+');
            }
            buffer.append(adjustedExponent);
        }
        return buffer.toString();
    }
}


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

package com.mongodb.operation;

import com.mongodb.MongoNamespace;
import com.mongodb.async.SingleResultCallback;
import com.mongodb.binding.AsyncReadBinding;
import com.mongodb.binding.ReadBinding;
import com.mongodb.client.model.Collation;
import com.mongodb.connection.ConnectionDescription;
import com.mongodb.connection.ServerDescription;
import org.bson.BsonArray;
import org.bson.BsonBoolean;
import org.bson.BsonDocument;
import org.bson.BsonInt64;
import org.bson.BsonString;
import org.bson.BsonValue;

import java.util.List;
import java.util.concurrent.TimeUnit;

import static com.mongodb.assertions.Assertions.isTrueArgument;
import static com.mongodb.assertions.Assertions.notNull;
import static com.mongodb.internal.async.ErrorHandlingResultCallback.errorHandlingCallback;
import static com.mongodb.operation.CommandOperationHelper.CommandCreator;
import static com.mongodb.operation.CommandOperationHelper.IdentityTransformerAsync;
import static com.mongodb.operation.CommandOperationHelper.executeCommand;
import static com.mongodb.operation.CommandOperationHelper.executeCommandAsync;
import static com.mongodb.operation.OperationHelper.LOGGER;
import static com.mongodb.operation.OperationHelper.validateCollation;

// an operation that executes an explain on an aggregation pipeline
class AggregateExplainOperation implements AsyncReadOperation<BsonDocument>, ReadOperation<BsonDocument> {
    private final MongoNamespace namespace;
    private final List<BsonDocument> pipeline;
    private boolean retryReads;
    private Boolean allowDiskUse;
    private long maxTimeMS;
    private Collation collation;
    private BsonValue hint;

    AggregateExplainOperation(final MongoNamespace namespace, final List<BsonDocument> pipeline) {
        this.namespace = notNull("namespace", namespace);
        this.pipeline = notNull("pipeline", pipeline);
    }

    /**
     * Enables writing to temporary files. A null value indicates that it's unspecified.
     *
     * @param allowDiskUse true if writing to temporary files is enabled
     * @return this
     * @mongodb.driver.manual reference/command/aggregate/ Aggregation
     * @mongodb.server.release 2.6
     */
    public AggregateExplainOperation allowDiskUse(final Boolean allowDiskUse) {
        this.allowDiskUse = allowDiskUse;
        return this;
    }

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

    /**
     * Enables retryable reads if a read fails due to a network error. A null value indicates that it's unspecified.
     *
     * @param retryReads true if retryable reads is enabled
     * @return this
     * @mongodb.driver.manual reference/command/aggregate/ Aggregation
     * @mongodb.server.release 3.6
     * @since 3.11
     */
    public AggregateExplainOperation retryReads(final boolean retryReads) {
        this.retryReads = retryReads;
        return this;
    }

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

    /**
     * Sets the collation options
     *
     *
A null value represents the server default.

     * @param collation the collation options to use
     * @return this
     * @since 3.4
     * @mongodb.driver.manual reference/command/aggregate/ Aggregation
     * @mongodb.server.release 3.4
     */
    public AggregateExplainOperation collation(final Collation collation) {
        this.collation = collation;
        return this;
    }

    /**
     * Returns the hint for which index to use. The default is not to set a hint.
     *
     * @return the hint
     * @since 3.6
     * @mongodb.server.release 3.6
     */
    public BsonDocument getHint() {
        if (hint == null) {
            return null;
        }
        if (!hint.isDocument()) {
            throw new IllegalArgumentException("Hint is not a BsonDocument please use the #getHintBsonValue() method. ");
        }
        return hint.asDocument();
    }

    /**
     * Returns the hint BsonValue for which index to use. The default is not to set a hint.
     *
     *
Hints can either be a BsonString or a BsonDocument.

     *
     * @return the hint
     * @since 3.8
     * @mongodb.server.release 3.6
     */
    public BsonValue getHintBsonValue() {
        return hint;
    }

    /**
     * Sets the hint for which index to use. A null value means no hint is set.
     *
     * @param hint the hint
     * @return this
     * @since 3.6
     * @mongodb.server.release 3.6
     */
    public AggregateExplainOperation hint(final BsonValue hint) {
        isTrueArgument("BsonString or BsonDocument", hint == null || hint.isDocument() || hint.isString());
        this.hint = hint;
        return this;
    }

    @Override
    public BsonDocument execute(final ReadBinding binding) {
        return executeCommand(binding, namespace.getDatabaseName(), getCommandCreator(), retryReads);
    }

    @Override
    public void executeAsync(final AsyncReadBinding binding, final SingleResultCallback<BsonDocument> callback) {
        SingleResultCallback<BsonDocument> errHandlingCallback = errorHandlingCallback(callback, LOGGER);
        executeCommandAsync(binding, namespace.getDatabaseName(), getCommandCreator(),
                new IdentityTransformerAsync<BsonDocument>(), retryReads, errHandlingCallback);
    }

    private CommandCreator getCommandCreator() {
        return new CommandCreator() {
            @Override
            public BsonDocument create(final ServerDescription serverDescription, final ConnectionDescription connectionDescription) {
                validateCollation(connectionDescription, collation);
                return getCommand();
            }
        };
    }

    private BsonDocument getCommand() {
        BsonDocument commandDocument = new BsonDocument("aggregate", new BsonString(namespace.getCollectionName()));
        commandDocument.put("pipeline", new BsonArray(pipeline));
        commandDocument.put("explain", BsonBoolean.TRUE);
        if (maxTimeMS > 0) {
            commandDocument.put("maxTimeMS", new BsonInt64(maxTimeMS));
        }
        if (allowDiskUse != null) {
            commandDocument.put("allowDiskUse", BsonBoolean.valueOf(allowDiskUse));
        }
        if (collation != null) {
            commandDocument.put("collation", collation.asDocument());
        }
        if (hint != null) {
            commandDocument.put("hint", hint);
        }
        return commandDocument;
    }

}


/*
 * 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.annotations.NotThreadSafe;
import com.mongodb.lang.Nullable;
import org.bson.BsonBoolean;
import org.bson.BsonDocument;
import org.bson.BsonInt32;
import org.bson.BsonString;

/**
 * The options regarding collation support in MongoDB 3.4+
 *
 * @mongodb.driver.manual reference/method/db.createCollection/ Create Collection
 * @mongodb.driver.manual reference/command/createIndexes Index options
 * @since 3.4
 * @mongodb.server.release 3.4
 */
public final class Collation {
    private final String locale;
    private final Boolean caseLevel;
    private final CollationCaseFirst caseFirst;
    private final CollationStrength strength;
    private final Boolean numericOrdering;
    private final CollationAlternate alternate;
    private final CollationMaxVariable maxVariable;
    private final Boolean normalization;
    private final Boolean backwards;

    /**
     * Convenience method to create a Builder.
     *
     * @return a builder
     */
    public static Builder builder() {
        return new Builder();
    }

    /**
     * Convenience method to create a from an existing {@code Collation}.
     *
     * @param options create a builder from existing options
     * @return a builder
     */
    public static Builder builder(final Collation options) {
        return new Builder(options);
    }

    /**
     * A Collation builder.
     */
    @NotThreadSafe
    public static final class Builder {
        private String locale;
        private Boolean caseLevel;
        private CollationCaseFirst caseFirst;
        private CollationStrength strength;
        private Boolean numericOrdering;
        private CollationAlternate alternate;
        private CollationMaxVariable maxVariable;
        private Boolean normalization;
        private Boolean backwards;

        private Builder() {
        }

        private Builder(final Collation options) {
            this.locale = options.getLocale();
            this.caseLevel = options.getCaseLevel();
            this.caseFirst = options.getCaseFirst();
            this.strength = options.getStrength();
            this.numericOrdering = options.getNumericOrdering();
            this.alternate = options.getAlternate();
            this.maxVariable = options.getMaxVariable();
            this.normalization = options.getNormalization();
            this.backwards = options.getBackwards();
        }

        /**
         * Sets the locale
         *
         * @param locale the locale
         * @see ICU User Guide - Locale
         * @return this
         */
        public Builder locale(@Nullable final String locale) {
            this.locale = locale;
            return this;
        }

        /**
         * Sets the case level value
         *
         *
Turns on case sensitivity

         * @param caseLevel the case level value
         * @return this
         */
        public Builder caseLevel(@Nullable final Boolean caseLevel) {
            this.caseLevel = caseLevel;
            return this;
        }

        /**
         * Sets the collation case first value
         *
         *
Determines if Uppercase or lowercase values should come first

         * @param caseFirst the collation case first value
         * @return this
         */
        public Builder collationCaseFirst(@Nullable final CollationCaseFirst caseFirst) {
            this.caseFirst = caseFirst;
            return this;
        }

        /**
         * Sets the collation strength
         *
         * @param strength the strength
         * @return this
         */
        public Builder collationStrength(@Nullable final CollationStrength strength) {
            this.strength = strength;
            return this;
        }

        /**
         * Sets the numeric ordering
         *
         * @param numericOrdering if true will order numbers based on numerical order and not collation order
         * @return this
         */
        public Builder numericOrdering(@Nullable final Boolean numericOrdering) {
            this.numericOrdering = numericOrdering;
            return this;
        }

        /**
         * Sets the alternate
         *
         *
Controls whether spaces and punctuation are considered base characters

         *
         * @param alternate the alternate
         * @return this
         */
        public Builder collationAlternate(@Nullable final CollationAlternate alternate) {
            this.alternate = alternate;
            return this;
        }

        /**
         * Sets the maxVariable
         *
         * @param maxVariable the maxVariable
         * @return this
         */
        public Builder collationMaxVariable(@Nullable final CollationMaxVariable maxVariable) {
            this.maxVariable = maxVariable;
            return this;
        }

        /**
         * Sets the normalization value
         *
         *
If true, normalizes text into Unicode NFD.

         * @param normalization the normalization value
         * @return this
         */
        public Builder normalization(@Nullable final Boolean normalization) {
            this.normalization = normalization;
            return this;
        }

        /**
         * Sets the backwards value
         *
         *
Causes secondary differences to be considered in reverse order, as it is done in the French language

         *
         * @param backwards the backwards value
         * @return this
         */
        public Builder backwards(@Nullable final Boolean backwards) {
            this.backwards = backwards;
            return this;
        }

        /**
         * Creates a new Collation object with the settings initialised on this builder.
         *
         * @return a new Collation object
         */
        public Collation build() {
            return new Collation(this);
        }
    }

    /**
     * Returns the locale
     *
     * @see ICU User Guide - Locale
     * @return the locale
     */
    @Nullable
    public String getLocale() {
        return locale;
    }

    /**
     * Returns the case level value
     *
     * @return the case level value
     */
    @Nullable
    public Boolean getCaseLevel() {
        return caseLevel;
    }

    /**
     * Returns the collation case first value
     *
     * @return the collation case first value
     */
    @Nullable
    public CollationCaseFirst getCaseFirst() {
        return caseFirst;
    }

    /**
     * Returns the collation strength
     *
     * @return the collation strength
     */
    @Nullable
    public CollationStrength getStrength() {
        return strength;
    }

    /**
     * Returns the numeric ordering, if true will order numbers based on numerical order and not collation order.
     *
     * @return the numeric ordering
     */
    @Nullable
    public Boolean getNumericOrdering() {
        return numericOrdering;
    }

    /**
     * Returns the collation alternate
     *
     * @return the alternate
     */
    @Nullable
    public CollationAlternate getAlternate() {
        return alternate;
    }

    /**
     * Returns the maxVariable
     *
     *
Controls which characters are affected by collection alternate {@link CollationAlternate#SHIFTED}.

     * @return the maxVariable
     */
    @Nullable
    public CollationMaxVariable getMaxVariable() {
        return maxVariable;
    }

    /**
     * Returns the normalization value
     *
     *
If true, normalizes text into Unicode NFD.

     * @return the normalization
     */
    @Nullable
    public Boolean getNormalization() {
        return normalization;
    }

    /**
     * Returns the backwards value
     *
     * @return the backwards value
     */
    @Nullable
    public Boolean getBackwards() {
        return backwards;
    }

    /**
     * Gets this collation options as a document.
     *
     * @return The collation options as a BsonDocument
     */
    public BsonDocument asDocument() {
        BsonDocument collation = new BsonDocument();
        if (locale != null) {
            collation.put("locale", new BsonString(locale));
        }
        if (caseLevel != null) {
            collation.put("caseLevel", new BsonBoolean(caseLevel));
        }
        if (caseFirst != null) {
            collation.put("caseFirst", new BsonString(caseFirst.getValue()));
        }
        if (strength != null) {
            collation.put("strength", new BsonInt32(strength.getIntRepresentation()));
        }
        if (numericOrdering != null) {
            collation.put("numericOrdering", new BsonBoolean(numericOrdering));
        }
        if (alternate != null) {
            collation.put("alternate", new BsonString(alternate.getValue()));
        }
        if (maxVariable != null) {
            collation.put("maxVariable", new BsonString(maxVariable.getValue()));
        }
        if (normalization != null) {
            collation.put("normalization", new BsonBoolean(normalization));
        }
        if (backwards != null) {
            collation.put("backwards", new BsonBoolean(backwards));
        }
        return collation;
    }

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

        if (locale != null ? !locale.equals(that.getLocale()) : that.getLocale() != null) {
            return false;
        }
        if (caseLevel != null ? !caseLevel.equals(that.getCaseLevel()) : that.getCaseLevel() != null) {
            return false;
        }
        if (getCaseFirst() != that.getCaseFirst()) {
            return false;
        }
        if (getStrength() != that.getStrength()) {
            return false;
        }
        if (numericOrdering != null ? !numericOrdering.equals(that.getNumericOrdering()) : that.getNumericOrdering() != null) {
            return false;
        }
        if (getAlternate() != that.getAlternate()) {
            return false;
        }
        if (getMaxVariable() != that.getMaxVariable()) {
            return false;
        }
        if (normalization != null ? !normalization.equals(that.getNormalization()) : that.getNormalization() != null) {
            return false;
        }
        if (backwards != null ? !backwards.equals(that.getBackwards()) : that.getBackwards() != null) {
            return false;
        }
        return true;
    }

    @Override
    public int hashCode() {
        int result = locale != null ? locale.hashCode() : 0;
        result = 31 * result + (caseLevel != null ? caseLevel.hashCode() : 0);
        result = 31 * result + (caseFirst != null ? caseFirst.hashCode() : 0);
        result = 31 * result + (strength != null ? strength.hashCode() : 0);
        result = 31 * result + (numericOrdering != null ? numericOrdering.hashCode() : 0);
        result = 31 * result + (alternate != null ? alternate.hashCode() : 0);
        result = 31 * result + (maxVariable != null ? maxVariable.hashCode() : 0);
        result = 31 * result + (normalization != null ? normalization.hashCode() : 0);
        result = 31 * result + (backwards != null ? backwards.hashCode() : 0);
        return result;
    }

    @Override
    public String toString() {
        return "Collation{"
                + "locale='" + locale + "'"
                + ", caseLevel=" + caseLevel
                + ", caseFirst=" + caseFirst
                + ", strength=" + strength
                + ", numericOrdering=" + numericOrdering
                + ", alternate=" + alternate
                + ", maxVariable=" + maxVariable
                + ", normalization=" + normalization
                + ", backwards=" + backwards
                + "}";
    }

    private Collation(final Builder builder) {
        this.locale = builder.locale;
        this.caseLevel = builder.caseLevel;
        this.caseFirst = builder.caseFirst;
        this.strength = builder.strength;
        this.numericOrdering = builder.numericOrdering;
        this.alternate = builder.alternate;
        this.maxVariable = builder.maxVariable;
        this.normalization = builder.normalization;
        this.backwards = builder.backwards;
    }
}


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

package com.mongodb;

import com.mongodb.lang.Nullable;

import java.util.List;

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

/**
 * Represents a URI
 * which can be used to create a MongoClient instance. The URI describes the hosts to
 * be used and options.
 *
The format of the URI is:
 *

 *   mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database.collection][?options]]
 *

 *

 *
{@code mongodb://} is a required prefix to identify that this is a string in the standard connection format.

 *
{@code username:password@} are optional.  If given, the driver will attempt to login to a database after
 * connecting to a database server.  For some authentication mechanisms, only the username is specified and the password is not,
 * in which case the ":" after the username is left off as well

 *
{@code host1} is the only required part of the connection string. It identifies a server address to connect to.
 * Support for Unix domain sockets was added in 3.7. Note: The path must be urlencoded eg: {@code mongodb://%2Ftmp%2Fmongodb-27017.sock}
 * and the {@code jnr.unixsocket} library installed.
 *
{@code :portX} is optional and defaults to :27017 if not provided.

 *
{@code /database} is the name of the database to login to and thus is only relevant if the
 * {@code username:password@} syntax is used. If not specified the "admin" database will be used by default.

 *
{@code ?options} are connection options. Note that if {@code database} is absent there is still a {@code /}
 * required between the last host and the {@code ?} introducing the options. Options are name=value pairs and the pairs
 * are separated by "&". For backwards compatibility, ";" is accepted as a separator in addition to "&",
 * but should be considered as deprecated.

 *

 *
An alternative format, using the mongodb+srv protocol, is:
 *

 *   mongodb+srv://[username:password@]host[/[database][?options]]
 *

 *

 *
{@code mongodb+srv://} is a required prefix for this format.

 *
{@code username:password@} are optional.  If given, the driver will attempt to login to a database after
 * connecting to a database server.  For some authentication mechanisms, only the username is specified and the password is not,
 * in which case the ":" after the username is left off as well

 *
{@code host} is the only required part of the URI.  It identifies a single host name for which SRV records are looked up
 * from a Domain Name Server after prefixing the host name with {@code "_mongodb._tcp"}.  The host/port for each SRV record becomes the
 * seed list used to connect, as if each one were provided as host/port pair in a URI using the normal mongodb protocol.

 *
{@code /database} is the name of the database to login to and thus is only relevant if the
 * {@code username:password@} syntax is used. If not specified the "admin" database will be used by default.

 *
{@code ?options} are connection options. Note that if {@code database} is absent there is still a {@code /}
 * required between the last host and the {@code ?} introducing the options. Options are name=value pairs and the pairs
 * are separated by "&". For backwards compatibility, ";" is accepted as a separator in addition to "&",
 * but should be considered as deprecated. Additionally with the mongodb+srv protocol, TXT records are looked up from a Domain Name
 * Server for the given host, and the text value of each one is prepended to any options on the URI itself.  Because the last specified
 * value for any option wins, that means that options provided on the URI will override any that are provided via TXT records.

 *

 *
The following options are supported (case insensitive):

 *
 *
Server Selection Configuration:

 *

 *
{@code serverSelectionTimeoutMS=ms}: How long the driver will wait for server selection to succeed before throwing an exception.

 *
{@code localThresholdMS=ms}: When choosing among multiple MongoDB servers to send a request, the driver will only
 * send that request to a server whose ping time is less than or equal to the server with the fastest ping time plus the local
 * threshold.

 *

 *
Server Monitoring Configuration:

 *

 *
{@code heartbeatFrequencyMS=ms}: The frequency that the driver will attempt to determine the current state of each server in the
 * cluster.

 *

 *
Replica set configuration:

 *

 *
{@code replicaSet=name}: Implies that the hosts given are a seed list, and the driver will attempt to find
 * all members of the set.

 *

 *
 *
Connection Configuration:

 *

 *
{@code ssl=true|false}: Whether to connect using TLS.

 *
{@code tls=true|false}: Whether to connect using TLS. Supersedes the ssl option

 *
{@code tlsInsecure=true|false}: If connecting with TLS, this option enables insecure TLS connections. Currently this has the
 * same effect of setting tlsAllowInvalidHostnames to true. Other mechanism for relaxing TLS security constraints must be handled in
 * the application by customizing the {@link javax.net.ssl.SSLContext}

 *
{@code sslInvalidHostNameAllowed=true|false}: Whether to allow invalid host names for TLS connections.

 *
{@code tlsAllowInvalidHostnames=true|false}: Whether to allow invalid host names for TLS connections. Supersedes the
 * sslInvalidHostNameAllowed option

 *
{@code connectTimeoutMS=ms}: How long a connection can take to be opened before timing out.

 *
{@code socketTimeoutMS=ms}: How long a send or receive on a socket can take before timing out.

 *
{@code maxIdleTimeMS=ms}: Maximum idle time of a pooled connection. A connection that exceeds this limit will be closed

 *
{@code maxLifeTimeMS=ms}: Maximum life time of a pooled connection. A connection that exceeds this limit will be closed

 *

 *
 *
Connection pool configuration:

 *

 *
{@code maxPoolSize=n}: The maximum number of connections in the connection pool.

 *
{@code waitQueueMultiple=n} : this multiplier, multiplied with the maxPoolSize setting, gives the maximum number of
 * threads that may be waiting for a connection to become available from the pool.  All further threads will get an
 * exception right away.

 *
{@code waitQueueTimeoutMS=ms}: The maximum wait time in milliseconds that a thread may wait for a connection to
 * become available.

 *

 *
 *
Write concern configuration:

 *

 *
{@code safe=true|false}
 *

 *
{@code true}: the driver ensures that all writes are acknowledged by the MongoDB server, or else throws an exception.
 * (see also {@code w} and {@code wtimeoutMS}).

 *
{@code false}: the driver does not ensure that all writes are acknowledged by the MongoDB server.

 *

 *

 *
{@code journal=true|false}
 *

 *
{@code true}: the driver waits for the server to group commit to the journal file on disk.

 *
{@code false}: the driver does not wait for the server to group commit to the journal file on disk.

 *

 *

 *
{@code w=wValue}
 *

 *
The driver adds { w : wValue } to all write commands. Implies {@code safe=true}.

 *
wValue is typically a number, but can be any string in order to allow for specifications like
 * {@code "majority"}

 *

 *

 *
{@code wtimeoutMS=ms}
 *

 *
The driver adds { wtimeout : ms } to all write commands. Implies {@code safe=true}.

 *
Used in combination with {@code w}

 *

 *

 *
{@code retryWrites=true|false}. If true the driver will retry supported write operations if they fail due to a network error.
 *  Defaults to false.

 *
{@code retryReads=true|false}. If true the driver will retry supported read operations if they fail due to a network error.
 *  Defaults to false.

 *

 *
 *
 *
Read preference configuration:

 *

 *
{@code slaveOk=true|false}: Whether a driver connected to a replica set will send reads to slaves/secondaries.

 *
{@code readPreference=enum}: The read preference for this connection.  If set, it overrides any slaveOk value.
 *

 *
Enumerated values:
 *

 *
{@code primary}

 *
{@code primaryPreferred}

 *
{@code secondary}

 *
{@code secondaryPreferred}

 *
{@code nearest}

 *

 *

 *

 *

 *
{@code readPreferenceTags=string}.  A representation of a tag set as a comma-separated list of colon-separated
 * key-value pairs, e.g. {@code "dc:ny,rack:1}".  Spaces are stripped from beginning and end of all keys and values.
 * To specify a list of tag sets, using multiple readPreferenceTags,
 * e.g. {@code readPreferenceTags=dc:ny,rack:1;readPreferenceTags=dc:ny;readPreferenceTags=}
 *

 *
Note the empty value for the last one, which means match any secondary as a last resort.

 *
Order matters when using multiple readPreferenceTags.

 *

 *

 *
{@code maxStalenessSeconds=seconds}. The maximum staleness in seconds. For use with any non-primary read preference, the driver
 * estimates the staleness of each secondary, based on lastWriteDate values provided in server isMaster responses, and selects only those
 * secondaries whose staleness is less than or equal to maxStalenessSeconds.  Not providing the parameter or explicitly setting it to -1
 * indicates that there should be no max staleness check.  The maximum staleness feature is designed to prevent badly-lagging servers from
 * being selected. The staleness estimate is imprecise and shouldn't be used to try to select "up-to-date" secondaries.  The minimum value
 * is either 90 seconds, or the heartbeat frequency plus 10 seconds, whichever is greatest.
 *

 *

 *
Authentication configuration:

 *

 *
{@code authMechanism=MONGO-CR|GSSAPI|PLAIN|MONGODB-X509}: The authentication mechanism to use if a credential was supplied.
 * The default is unspecified, in which case the client will pick the most secure mechanism available based on the sever version.  For the
 * GSSAPI and MONGODB-X509 mechanisms, no password is accepted, only the username.
 *

 *
{@code authSource=string}: The source of the authentication credentials.  This is typically the database that
 * the credentials have been created.  The value defaults to the database specified in the path portion of the URI.
 * If the database is specified in neither place, the default value is "admin".  This option is only respected when using the MONGO-CR
 * mechanism (the default).
 *

 *
{@code gssapiServiceName=string}: This option only applies to the GSSAPI mechanism and is used to alter the service name..
 *

 *

 *
Server Handshake configuration:

 *

 *
{@code appName=string}: Sets the logical name of the application.  The application name may be used by the client to identify
 * the application to the server, for use in server logs, slow query logs, and profile collection.

 *

 *
Compressor configuration:

 *

 *
{@code compressors=string}: A comma-separated list of compressors to request from the server.  The supported compressors
 * currently are 'zlib', 'snappy' and 'zstd'.

 *
{@code zlibCompressionLevel=integer}: Integer value from -1 to 9 representing the zlib compression level. Lower values will make
 * compression faster, while higher values will make compression better.

 *

 *
General configuration:

 *

 *
{@code retryWrites=true|false}. If true the driver will retry supported write operations if they fail due to a network error.
 *  Defaults to true.

 *
{@code retryReads=true|false}. If true the driver will retry supported read operations if they fail due to a network error.
 *  Defaults to true.

 *

 *
 *
Note: This class is a replacement for {@code MongoURI}, to be used with {@code MongoClient}.  The main difference in
 * behavior is that the default write concern is {@code WriteConcern.ACKNOWLEDGED}.

 *
 * @mongodb.driver.manual reference/connection-string Connection String URI Format
 * @see MongoClientOptions for the default values for all options
 * @since 2.10.0
 */
public class MongoClientURI {
    private final ConnectionString proxied;
    private final MongoClientOptions.Builder builder;

    /**
     * Creates a MongoURI from the given string.
     *
     * @param uri the URI
     */
    public MongoClientURI(final String uri) {
        this(uri, new MongoClientOptions.Builder());
    }

    /**
     * Creates a MongoURI from the given URI string, and MongoClientOptions.Builder.  The builder can be configured with default options,
     * which may be overridden by options specified in the URI string.
     *
     *

     * The {@code MongoClientURI} takes ownership of the {@code MongoClientOptions.Builder} instance that is passed to this constructor,
     * and may modify it.
     *

     *
     * @param uri     the URI
     * @param builder a non-null Builder, which may be modified within this constructor,
     * @since 2.11.0
     */
    public MongoClientURI(final String uri, final MongoClientOptions.Builder builder) {
        this.builder = notNull("builder", builder);
        proxied = new ConnectionString(uri);
    }

    ConnectionString getProxied() {
        return proxied;
    }

    // ---------------------------------

    /**
     * Gets the username
     *
     * @return the username
     */
    @Nullable
    public String getUsername() {
        return proxied.getUsername();
    }

    /**
     * Gets the password
     *
     * @return the password
     */
    @Nullable
    public char[] getPassword() {
        return proxied.getPassword();
    }

    /**
     * Gets the list of hosts
     *
     * @return the host list
     */
    public List<String> getHosts() {
        return proxied.getHosts();
    }

    /**
     * Gets the database name
     *
     * @return the database name
     */
    @Nullable
    public String getDatabase() {
        return proxied.getDatabase();
    }

    /**
     * Gets the collection name
     *
     * @return the collection name
     */
    @Nullable
    public String getCollection() {
        return proxied.getCollection();
    }

    /**
     * Get the unparsed URI.
     *
     * @return the URI
     */
    public String getURI() {
        return proxied.getConnectionString();
    }

    /**
     * Gets the credentials.
     *
     * @return the credentials
     */
    @Nullable
    public MongoCredential getCredentials() {
        return proxied.getCredential();
    }

    /**
     * Gets the options
     *
     * @return the MongoClientOptions based on this URI.
     */
    public MongoClientOptions getOptions() {
        ReadPreference readPreference = proxied.getReadPreference();
        if (readPreference != null) {
            builder.readPreference(readPreference);
        }
        ReadConcern readConcern = proxied.getReadConcern();
        if (readConcern != null) {
            builder.readConcern(readConcern);
        }
        WriteConcern writeConcern = proxied.getWriteConcern();
        if (writeConcern != null) {
            builder.writeConcern(writeConcern);
        }
        if (proxied.getRetryWritesValue() != null) {
            builder.retryWrites(proxied.getRetryWritesValue());
        }
        if (proxied.getRetryReads() != null) {
            builder.retryReads(proxied.getRetryReads());
        }

        Integer maxConnectionPoolSize = proxied.getMaxConnectionPoolSize();
        if (maxConnectionPoolSize != null) {
            builder.connectionsPerHost(maxConnectionPoolSize);
        }
        Integer integer = proxied.getMinConnectionPoolSize();
        if (integer != null) {
            builder.minConnectionsPerHost(integer);
        }
        Integer maxWaitTime = proxied.getMaxWaitTime();
        if (maxWaitTime != null) {
            builder.maxWaitTime(maxWaitTime);
        }
        Integer threadsAllowedToBlockForConnectionMultiplier = proxied.getThreadsAllowedToBlockForConnectionMultiplier();
        if (threadsAllowedToBlockForConnectionMultiplier != null) {
            builder.threadsAllowedToBlockForConnectionMultiplier(threadsAllowedToBlockForConnectionMultiplier);
        }
        Integer maxConnectionIdleTime = proxied.getMaxConnectionIdleTime();
        if (maxConnectionIdleTime != null) {
            builder.maxConnectionIdleTime(maxConnectionIdleTime);
        }
        Integer maxConnectionLifeTime = proxied.getMaxConnectionLifeTime();
        if (maxConnectionLifeTime != null) {
            builder.maxConnectionLifeTime(maxConnectionLifeTime);
        }
        Integer socketTimeout = proxied.getSocketTimeout();
        if (socketTimeout != null) {
            builder.socketTimeout(socketTimeout);
        }
        Integer connectTimeout = proxied.getConnectTimeout();
        if (connectTimeout != null) {
            builder.connectTimeout(connectTimeout);
        }
        String requiredReplicaSetName = proxied.getRequiredReplicaSetName();
        if (requiredReplicaSetName != null) {
            builder.requiredReplicaSetName(requiredReplicaSetName);
        }
        Boolean sslEnabled = proxied.getSslEnabled();
        if (sslEnabled != null) {
            builder.sslEnabled(sslEnabled);
        }
        Boolean sslInvalidHostnameAllowed = proxied.getSslInvalidHostnameAllowed();
        if (sslInvalidHostnameAllowed != null) {
            builder.sslInvalidHostNameAllowed(sslInvalidHostnameAllowed);
        }
        Integer serverSelectionTimeout = proxied.getServerSelectionTimeout();
        if (serverSelectionTimeout != null) {
            builder.serverSelectionTimeout(serverSelectionTimeout);
        }
        Integer localThreshold = proxied.getLocalThreshold();
        if (localThreshold != null) {
            builder.localThreshold(localThreshold);
        }
        Integer heartbeatFrequency = proxied.getHeartbeatFrequency();
        if (heartbeatFrequency != null) {
            builder.heartbeatFrequency(heartbeatFrequency);
        }
        String applicationName = proxied.getApplicationName();
        if (applicationName != null) {
            builder.applicationName(applicationName);
        }
        if (!proxied.getCompressorList().isEmpty()) {
            builder.compressorList(proxied.getCompressorList());
        }
        return builder.build();
    }

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

        MongoClientURI that = (MongoClientURI) o;

        if (!getHosts().equals(that.getHosts())) {
            return false;
        }
        String database = getDatabase();
        if (database != null ? !database.equals(that.getDatabase()) : that.getDatabase() != null) {
            return false;
        }
        String collection = getCollection();
        if (collection != null ? !collection.equals(that.getCollection()) : that.getCollection() != null) {
            return false;
        }
        MongoCredential credentials = getCredentials();
        if (credentials != null ? !credentials.equals(that.getCredentials()) : that.getCredentials() != null) {
            return false;
        }
        if (!getOptions().equals(that.getOptions())) {
            return false;
        }

        return true;
    }

    @Override
    public int hashCode() {
        int result = getOptions().hashCode();
        result = 31 * result + getHosts().hashCode();

        MongoCredential credentials = getCredentials();
        result = 31 * result + (credentials != null ? credentials.hashCode() : 0);

        String database = getDatabase();
        result = 31 * result + (database != null ? database.hashCode() : 0);

        String collection = getCollection();
        result = 31 * result + (collection != null ? collection.hashCode() : 0);

        return result;
    }

    @Override
    public String toString() {
        return proxied.toString();
    }
}


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

import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import com.mongodb.MongoException;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * This class enables retrieving a GridFS file metadata and content. Operations include:
 *

 *
Writing data to a file on disk or an OutputStream

 *
Creating an {@code InputStream} to stream the data into

 *

 *
 * @mongodb.driver.manual core/gridfs/ GridFS
 */
public class GridFSDBFile extends GridFSFile {
    /**
     * Returns an InputStream from which data can be read.
     *
     * @return the input stream
     */
    public InputStream getInputStream() {
        return new GridFSInputStream();
    }

    /**
     * Writes the file's data to a file on disk.
     *
     * @param filename the file name on disk
     * @return number of bytes written
     * @throws IOException if there are problems writing to the file
     */
    public long writeTo(final String filename) throws IOException {
        return writeTo(new File(filename));
    }

    /**
     * Writes the file's data to a file on disk.
     *
     * @param file the File object
     * @return number of bytes written
     * @throws IOException if there are problems writing to the {@code file}
     */
    public long writeTo(final File file) throws IOException {
        FileOutputStream out = null;
        try {
            out = new FileOutputStream(file);
            return writeTo(out);
        } finally {
            if (out != null) {
                out.close();
            }
        }
    }

    /**
     * Writes the file's data to an OutputStream.
     *
     * @param out the OutputStream
     * @return number of bytes written
     * @throws IOException if there are problems writing to {@code out}
     */
    public long writeTo(final OutputStream out) throws IOException {
        int nc = numChunks();
        for (int i = 0; i < nc; i++) {
            out.write(getChunk(i));
        }
        return length;
    }

    private byte[] getChunk(final int chunkNumber) {
        if (fs == null) {
            throw new IllegalStateException("No GridFS instance defined!");
        }

        DBObject chunk = fs.getChunksCollection().findOne(new BasicDBObject("files_id", id).append("n", chunkNumber));
        if (chunk == null) {
            throw new MongoException("Can't find a chunk!  file id: " + id + " chunk: " + chunkNumber);
        }

        return (byte[]) chunk.get("data");
    }

    /**
     * Removes file from GridFS i.e. removes documents from files and chunks collections.
     */
    void remove() {
        fs.getFilesCollection().remove(new BasicDBObject("_id", id));
        fs.getChunksCollection().remove(new BasicDBObject("files_id", id));
    }

    private class GridFSInputStream extends InputStream {

        private final int numberOfChunks;
        private int currentChunkId = -1;
        private int offset = 0;
        private byte[] buffer = null;

        GridFSInputStream() {
            this.numberOfChunks = numChunks();
        }

        @Override
        public int available() {
            if (buffer == null) {
                return 0;
            }
            return buffer.length - offset;
        }

        @Override
        public int read() {
            byte[] b = new byte[1];
            int res = read(b);
            if (res < 0) {
                return -1;
            }
            return b[0] & 0xFF;
        }

        @Override
        public int read(final byte[] b) {
            return read(b, 0, b.length);
        }

        @Override
        public int read(final byte[] b, final int off, final int len) {

            if (buffer == null || offset >= buffer.length) {
                if (currentChunkId + 1 >= numberOfChunks) {
                    return -1;
                }

                buffer = getChunk(++currentChunkId);
                offset = 0;
            }

            int r = Math.min(len, buffer.length - offset);
            System.arraycopy(buffer, offset, b, off, r);
            offset += r;
            return r;
        }

        /**
         * Will smartly skip over chunks without fetching them if possible.
         */
        @Override
        public long skip(final long bytesToSkip) throws IOException {
            if (bytesToSkip <= 0) {
                return 0;
            }

            if (currentChunkId == numberOfChunks) {
                //We're actually skipping over the back end of the file, short-circuit here
                //Don't count those extra bytes to skip in with the return value
                return 0;
            }

            // offset in the whole file
            long offsetInFile = 0;
            if (currentChunkId >= 0) {
                offsetInFile = currentChunkId * chunkSize + offset;
            }
            if (bytesToSkip + offsetInFile >= length) {
                currentChunkId = numberOfChunks;
                buffer = null;
                return length - offsetInFile;
            }

            int temp = currentChunkId;
            currentChunkId = (int) ((bytesToSkip + offsetInFile) / chunkSize);
            if (temp != currentChunkId) {
                buffer = getChunk(currentChunkId);
            }
            offset = (int) ((bytesToSkip + offsetInFile) % chunkSize);

            return bytesToSkip;
        }
    }
}