/*
 * 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 java.io.Closeable;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import static java.lang.String.format;
import static org.bson.assertions.Assertions.notNull;

/**
 * Represents a BSON writer for some external format (see subclasses).
 *
 * @since 3.0
 */
public abstract class AbstractBsonWriter implements BsonWriter, Closeable {
    private final BsonWriterSettings settings;
    private final Stack<FieldNameValidator> fieldNameValidatorStack = new Stack<FieldNameValidator>();
    private State state;
    private Context context;
    private int serializationDepth;
    private boolean closed;

    /**
     * Initializes a new instance of the BsonWriter class.
     *
     * @param settings The writer settings.
     */
    protected AbstractBsonWriter(final BsonWriterSettings settings) {
        this(settings, new NoOpFieldNameValidator());
    }

    /**
     * Initializes a new instance of the BsonWriter class.
     *
     * @param settings  The writer settings.
     * @param validator the field name validator
     */
    protected AbstractBsonWriter(final BsonWriterSettings settings, final FieldNameValidator validator) {
        if (validator == null) {
            throw new IllegalArgumentException("Validator can not be null");
        }
        this.settings = settings;
        fieldNameValidatorStack.push(validator);
        state = State.INITIAL;
    }

    /**
     * The name of the field being written.
     *
     * @return the name of the field
     */
    protected String getName() {
        return context.name;
    }

    /**
     * Returns whether this writer has been closed.
     *
     * @return true if the {@link #close()} method has been called.
     */
    protected boolean isClosed() {
        return closed;
    }

    /**
     * Sets the current state of the writer. The current state determines what sort of actions are valid for this writer at this time.
     *
     * @param state the state to set this writer to.
     */
    protected void setState(final State state) {
        this.state = state;
    }

    /**
     * Gets the current state of this writer.  The current state determines what sort of actions are valid for this writer at this time.
     *
     * @return the current state of the writer.
     */
    protected State getState() {
        return state;
    }

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

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

    /**
     * Handles the logic to start writing a document
     */
    protected abstract void doWriteStartDocument();

    /**
     * Handles the logic of writing the end of a document
     */
    protected abstract void doWriteEndDocument();

    /**
     * Handles the logic to start writing an array
     */
    protected abstract void doWriteStartArray();

    /**
     * Handles the logic of writing the end of an array
     */
    protected abstract void doWriteEndArray();

    /**
     * Handles the logic of writing a {@code BsonBinary} value
     *
     * @param value the {@code BsonBinary} value to write
     */
    protected abstract void doWriteBinaryData(BsonBinary value);

    /**
     * Handles the logic of writing a boolean value
     *
     * @param value the {@code boolean} value to write
     */
    protected abstract void doWriteBoolean(boolean value);

    /**
     * Handles the logic of writing a date time value
     *
     * @param value the {@code long} value to write
     */
    protected abstract void doWriteDateTime(long value);

    /**
     * Handles the logic of writing a DbPointer value
     *
     * @param value the {@code BsonDbPointer} value to write
     */
    protected abstract void doWriteDBPointer(BsonDbPointer value);

    /**
     * Handles the logic of writing a Double value
     *
     * @param value the {@code double} value to write
     */
    protected abstract void doWriteDouble(double value);

    /**
     * Handles the logic of writing an int32 value
     *
     * @param value the {@code int} value to write
     */
    protected abstract void doWriteInt32(int value);

    /**
     * Handles the logic of writing an int64 value
     *
     * @param value the {@code long} value to write
     */
    protected abstract void doWriteInt64(long value);

    /**
     * Handles the logic of writing a Decimal128 value
     *
     * @param value the {@code Decimal128} value to write
     * @since 3.4
     */
    protected abstract void doWriteDecimal128(Decimal128 value);

    /**
     * Handles the logic of writing a JavaScript function
     *
     * @param value the {@code String} value to write
     */
    protected abstract void doWriteJavaScript(String value);

    /**
     * Handles the logic of writing a scoped JavaScript function
     *
     * @param value the {@code boolean} value to write
     */
    protected abstract void doWriteJavaScriptWithScope(String value);

    /**
     * Handles the logic of writing a Max key
     */
    protected abstract void doWriteMaxKey();

    /**
     * Handles the logic of writing a Min key
     */
    protected abstract void doWriteMinKey();

    /**
     * Handles the logic of writing a Null value
     */
    protected abstract void doWriteNull();

    /**
     * Handles the logic of writing an ObjectId
     *
     * @param value the {@code ObjectId} value to write
     */
    protected abstract void doWriteObjectId(ObjectId value);

    /**
     * Handles the logic of writing a regular expression
     *
     * @param value the {@code BsonRegularExpression} value to write
     */
    protected abstract void doWriteRegularExpression(BsonRegularExpression value);

    /**
     * Handles the logic of writing a String
     *
     * @param value the {@code String} value to write
     */
    protected abstract void doWriteString(String value);

    /**
     * Handles the logic of writing a Symbol
     *
     * @param value the {@code boolean} value to write
     */
    protected abstract void doWriteSymbol(String value);

    /**
     * Handles the logic of writing a timestamp
     *
     * @param value the {@code BsonTimestamp} value to write
     */
    protected abstract void doWriteTimestamp(BsonTimestamp value);

    /**
     * Handles the logic of writing an Undefined  value
     */
    protected abstract void doWriteUndefined();

    @Override
    public void writeStartDocument(final String name) {
        writeName(name);
        writeStartDocument();
    }

    @Override
    public void writeStartDocument() {
        checkPreconditions("writeStartDocument", State.INITIAL, State.VALUE, State.SCOPE_DOCUMENT, State.DONE);
        if (context != null && context.name != null) {
            fieldNameValidatorStack.push(fieldNameValidatorStack.peek().getValidatorForField(getName()));
        }
        serializationDepth++;
        if (serializationDepth > settings.getMaxSerializationDepth()) {
            throw new BsonSerializationException("Maximum serialization depth exceeded (does the object being "
                                                 + "serialized have a circular reference?).");
        }

        doWriteStartDocument();
        setState(State.NAME);
    }

    @Override
    public void writeEndDocument() {
        checkPreconditions("writeEndDocument", State.NAME);

        BsonContextType contextType = getContext().getContextType();
        if (contextType != BsonContextType.DOCUMENT && contextType != BsonContextType.SCOPE_DOCUMENT) {
            throwInvalidContextType("WriteEndDocument", contextType, BsonContextType.DOCUMENT, BsonContextType.SCOPE_DOCUMENT);
        }

        if (context.getParentContext() != null && context.getParentContext().name != null) {
            fieldNameValidatorStack.pop();
        }
        serializationDepth--;

        doWriteEndDocument();

        if (getContext() == null || getContext().getContextType() == BsonContextType.TOP_LEVEL) {
            setState(State.DONE);
        } else {
            setState(getNextState());
        }
    }

    @Override
    public void writeStartArray(final String name) {
        writeName(name);
        writeStartArray();
    }

    @Override
    public void writeStartArray() {
        checkPreconditions("writeStartArray", State.VALUE);

        if (context != null && context.name != null) {
            fieldNameValidatorStack.push(fieldNameValidatorStack.peek().getValidatorForField(getName()));
        }
        serializationDepth++;
        if (serializationDepth > settings.getMaxSerializationDepth()) {
            throw new BsonSerializationException("Maximum serialization depth exceeded (does the object being "
                                                 + "serialized have a circular reference?).");
        }

        doWriteStartArray();
        setState(State.VALUE);
    }

    @Override
    public void writeEndArray() {
        checkPreconditions("writeEndArray", State.VALUE);

        if (getContext().getContextType() != BsonContextType.ARRAY) {
            throwInvalidContextType("WriteEndArray", getContext().getContextType(), BsonContextType.ARRAY);
        }

        if (context.getParentContext() != null && context.getParentContext().name != null) {
            fieldNameValidatorStack.pop();
        }
        serializationDepth--;

        doWriteEndArray();
        setState(getNextState());
    }

    @Override
    public void writeBinaryData(final String name, final BsonBinary binary) {
        notNull("name", name);
        notNull("value", binary);
        writeName(name);
        writeBinaryData(binary);
    }

    @Override
    public void writeBinaryData(final BsonBinary binary) {
        notNull("value", binary);
        checkPreconditions("writeBinaryData", State.VALUE, State.INITIAL);
        doWriteBinaryData(binary);
        setState(getNextState());
    }

    @Override
    public void writeBoolean(final String name, final boolean value) {
        writeName(name);
        writeBoolean(value);
    }

    @Override
    public void writeBoolean(final boolean value) {
        checkPreconditions("writeBoolean", State.VALUE, State.INITIAL);
        doWriteBoolean(value);
        setState(getNextState());
    }

    @Override
    public void writeDateTime(final String name, final long value) {
        writeName(name);
        writeDateTime(value);
    }

    @Override
    public void writeDateTime(final long value) {
        checkPreconditions("writeDateTime", State.VALUE, State.INITIAL);
        doWriteDateTime(value);
        setState(getNextState());
    }

    @Override
    public void writeDBPointer(final String name, final BsonDbPointer value) {
        notNull("name", name);
        notNull("value", value);
        writeName(name);
        writeDBPointer(value);
    }

    @Override
    public void writeDBPointer(final BsonDbPointer value) {
        notNull("value", value);
        checkPreconditions("writeDBPointer", State.VALUE, State.INITIAL);
        doWriteDBPointer(value);
        setState(getNextState());
    }

    @Override
    public void writeDouble(final String name, final double value) {
        writeName(name);
        writeDouble(value);
    }

    @Override
    public void writeDouble(final double value) {
        checkPreconditions("writeDBPointer", State.VALUE, State.INITIAL);
        doWriteDouble(value);
        setState(getNextState());
    }

    @Override
    public void writeInt32(final String name, final int value) {
        writeName(name);
        writeInt32(value);
    }

    @Override
    public void writeInt32(final int value) {
        checkPreconditions("writeInt32", State.VALUE);
        doWriteInt32(value);
        setState(getNextState());
    }

    @Override
    public void writeInt64(final String name, final long value) {
        writeName(name);
        writeInt64(value);
    }

    @Override
    public void writeInt64(final long value) {
        checkPreconditions("writeInt64", State.VALUE);
        doWriteInt64(value);
        setState(getNextState());
    }

    @Override
    public void writeDecimal128(final Decimal128 value) {
        notNull("value", value);
        checkPreconditions("writeInt64", State.VALUE);
        doWriteDecimal128(value);
        setState(getNextState());
    }

    @Override
    public void writeDecimal128(final String name, final Decimal128 value) {
        notNull("name", name);
        notNull("value", value);
        writeName(name);
        writeDecimal128(value);
    }

    @Override
    public void writeJavaScript(final String name, final String code) {
        notNull("name", name);
        notNull("value", code);
        writeName(name);
        writeJavaScript(code);
    }

    @Override
    public void writeJavaScript(final String code) {
        notNull("value", code);
        checkPreconditions("writeJavaScript", State.VALUE);
        doWriteJavaScript(code);
        setState(getNextState());
    }

    @Override
    public void writeJavaScriptWithScope(final String name, final String code) {
        notNull("name", name);
        notNull("value", code);
        writeName(name);
        writeJavaScriptWithScope(code);
    }

    @Override
    public void writeJavaScriptWithScope(final String code) {
        notNull("value", code);
        checkPreconditions("writeJavaScriptWithScope", State.VALUE);
        doWriteJavaScriptWithScope(code);
        setState(State.SCOPE_DOCUMENT);
    }

    @Override
    public void writeMaxKey(final String name) {
        writeName(name);
        writeMaxKey();
    }

    @Override
    public void writeMaxKey() {
        checkPreconditions("writeMaxKey", State.VALUE);
        doWriteMaxKey();
        setState(getNextState());
    }

    @Override
    public void writeMinKey(final String name) {
        writeName(name);
        writeMinKey();
    }

    @Override
    public void writeMinKey() {
        checkPreconditions("writeMinKey", State.VALUE);
        doWriteMinKey();
        setState(getNextState());
    }

    @Override
    public void writeName(final String name) {
        notNull("name", name);
        if (state != State.NAME) {
            throwInvalidState("WriteName", State.NAME);
        }
        if (!fieldNameValidatorStack.peek().validate(name)) {
            throw new IllegalArgumentException(format("Invalid BSON field name %s", name));
        }
        doWriteName(name);
        context.name = name;
        state = State.VALUE;
    }

    /**
     * Handles the logic of writing the element name.
     *
     * @param name the name of the element
     * @since 3.5
     */
    protected void doWriteName(final String name) {
    }

    @Override
    public void writeNull(final String name) {
        writeName(name);
        writeNull();
    }

    @Override
    public void writeNull() {
        checkPreconditions("writeNull", State.VALUE);
        doWriteNull();
        setState(getNextState());
    }

    @Override
    public void writeObjectId(final String name, final ObjectId objectId) {
        notNull("name", name);
        notNull("value", objectId);
        writeName(name);
        writeObjectId(objectId);
    }

    @Override
    public void writeObjectId(final ObjectId objectId) {
        notNull("value", objectId);
        checkPreconditions("writeObjectId", State.VALUE);
        doWriteObjectId(objectId);
        setState(getNextState());
    }

    @Override
    public void writeRegularExpression(final String name, final BsonRegularExpression regularExpression) {
        notNull("name", name);
        notNull("value", regularExpression);
        writeName(name);
        writeRegularExpression(regularExpression);
    }

    @Override
    public void writeRegularExpression(final BsonRegularExpression regularExpression) {
        notNull("value", regularExpression);
        checkPreconditions("writeRegularExpression", State.VALUE);
        doWriteRegularExpression(regularExpression);
        setState(getNextState());
    }

    @Override
    public void writeString(final String name, final String value) {
        notNull("name", name);
        notNull("value", value);
        writeName(name);
        writeString(value);
    }

    @Override
    public void writeString(final String value) {
        notNull("value", value);
        checkPreconditions("writeString", State.VALUE);
        doWriteString(value);
        setState(getNextState());

    }

    @Override
    public void writeSymbol(final String name, final String value) {
        notNull("name", name);
        notNull("value", value);
        writeName(name);
        writeSymbol(value);
    }

    @Override
    public void writeSymbol(final String value) {
        notNull("value", value);
        checkPreconditions("writeSymbol", State.VALUE);
        doWriteSymbol(value);
        setState(getNextState());
    }

    @Override
    public void writeTimestamp(final String name, final BsonTimestamp value) {
        notNull("name", name);
        notNull("value", value);
        writeName(name);
        writeTimestamp(value);
    }

    @Override
    public void writeTimestamp(final BsonTimestamp value) {
        notNull("value", value);
        checkPreconditions("writeTimestamp", State.VALUE);
        doWriteTimestamp(value);
        setState(getNextState());
    }

    @Override
    public void writeUndefined(final String name) {
        writeName(name);
        writeUndefined();
    }

    @Override
    public void writeUndefined() {
        checkPreconditions("writeUndefined", State.VALUE);
        doWriteUndefined();
        setState(getNextState());
    }

    /**
     * Returns the next valid state for this writer.  For example, transitions from {@link State#VALUE} to {@link State#NAME} once a value
     * is written.
     *
     * @return the next {@code State}
     */
    protected State getNextState() {
        if (getContext().getContextType() == BsonContextType.ARRAY) {
            return State.VALUE;
        } else {
            return State.NAME;
        }
    }

    /**
     * Checks if this writer's current state is in the list of given states.
     *
     * @param validStates an array of {@code State}s to compare this writer's state to.
     * @return true if this writer's state is in the given list.
     */
    protected boolean checkState(final State[] validStates) {
        for (final State cur : validStates) {
            if (cur == getState()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Checks the writer is in the correct state. If the writer's current state is in the list of given states, this method will complete
     * without exception.  Throws an {@link java.lang.IllegalStateException} if the writer is closed.  Throws BsonInvalidOperationException
     * if the method is trying to do something that is not permitted in the current state.
     *
     * @param methodName  the name of the method being performed that checks are being performed for
     * @param validStates the list of valid states for this operation
     * @see #throwInvalidState(String, org.bson.AbstractBsonWriter.State...)
     */
    protected void checkPreconditions(final String methodName, final State... validStates) {
        if (isClosed()) {
            throw new IllegalStateException("BsonWriter is closed");
        }

        if (!checkState(validStates)) {
            throwInvalidState(methodName, validStates);
        }
    }

    /**
     * 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 ", Arrays.asList(validContextTypes));
        throw new BsonInvalidOperationException(format("%s can only be called when ContextType is %s, "
                                                       + "not when ContextType is %s.",
                                                       methodName, validContextTypesString, actualContextType));
    }

    /**
     * Throws a {@link BsonInvalidOperationException} 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) {
        if (state == State.INITIAL || state == State.SCOPE_DOCUMENT || state == State.DONE) {
            if (!methodName.startsWith("end") && !methodName.equals("writeName")) { // NOPMD
                //NOPMD collapsing these if statements will not aid readability
                String typeName = methodName.substring(5);
                if (typeName.startsWith("start")) {
                    typeName = typeName.substring(5);
                }
                String article = "A";
                if (Arrays.asList('A', 'E', 'I', 'O', 'U').contains(typeName.charAt(0))) {
                    article = "An";
                }
                throw new BsonInvalidOperationException(format("%s %s value cannot be written to the root level of a BSON document.",
                                                               article, typeName));
            }
        }

        String validStatesString = StringUtils.join(" or ", Arrays.asList(validStates));
        throw new BsonInvalidOperationException(format("%s can only be called when State is %s, not when State is %s",
                                                       methodName, validStatesString, state));
    }

    @Override
    public void close() {
        closed = true;
    }

    @Override
    public void pipe(final BsonReader reader) {
        notNull("reader", reader);
        pipeDocument(reader, null);
    }

    /**
     * Reads a single document from the given BsonReader and writes it to this, appending the given extra elements to the document.
     *
     * @param reader the source of the document
     * @param extraElements the extra elements to append to the document
     * @since 3.6
     */
    public void pipe(final BsonReader reader, final List<BsonElement> extraElements) {
        notNull("reader", reader);
        notNull("extraElements", extraElements);
        pipeDocument(reader, extraElements);
    }

    /**
     * Pipe a list of extra element to this writer
     *
     * @param extraElements the extra elements
     */
    protected void pipeExtraElements(final List<BsonElement> extraElements) {
        notNull("extraElements", extraElements);
        for (BsonElement cur : extraElements) {
            writeName(cur.getName());
            pipeValue(cur.getValue());
        }
    }

    /**
     * Return true if the current execution of the pipe method should be aborted.
     *
     * @return true if the current execution of the pipe method should be aborted.
     *
     * @since 3.7
     */
    protected boolean abortPipe() {
        return false;
    }

    private void pipeDocument(final BsonReader reader, final List<BsonElement> extraElements) {
        reader.readStartDocument();
        writeStartDocument();
        while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
            writeName(reader.readName());
            pipeValue(reader);
            if (abortPipe()) {
                return;
            }
        }
        reader.readEndDocument();
        if (extraElements != null) {
            pipeExtraElements(extraElements);
        }
        writeEndDocument();
    }

    private void pipeJavascriptWithScope(final BsonReader reader) {
        writeJavaScriptWithScope(reader.readJavaScriptWithScope());
        pipeDocument(reader, null);
    }

    private void pipeValue(final BsonReader reader) {
        switch (reader.getCurrentBsonType()) {
            case DOCUMENT:
                pipeDocument(reader, null);
                break;
            case ARRAY:
                pipeArray(reader);
                break;
            case DOUBLE:
                writeDouble(reader.readDouble());
                break;
            case STRING:
                writeString(reader.readString());
                break;
            case BINARY:
                writeBinaryData(reader.readBinaryData());
                break;
            case UNDEFINED:
                reader.readUndefined();
                writeUndefined();
                break;
            case OBJECT_ID:
                writeObjectId(reader.readObjectId());
                break;
            case BOOLEAN:
                writeBoolean(reader.readBoolean());
                break;
            case DATE_TIME:
                writeDateTime(reader.readDateTime());
                break;
            case NULL:
                reader.readNull();
                writeNull();
                break;
            case REGULAR_EXPRESSION:
                writeRegularExpression(reader.readRegularExpression());
                break;
            case JAVASCRIPT:
                writeJavaScript(reader.readJavaScript());
                break;
            case SYMBOL:
                writeSymbol(reader.readSymbol());
                break;
            case JAVASCRIPT_WITH_SCOPE:
                pipeJavascriptWithScope(reader);
                break;
            case INT32:
                writeInt32(reader.readInt32());
                break;
            case TIMESTAMP:
                writeTimestamp(reader.readTimestamp());
                break;
            case INT64:
                writeInt64(reader.readInt64());
                break;
            case DECIMAL128:
                writeDecimal128(reader.readDecimal128());
                break;
            case MIN_KEY:
                reader.readMinKey();
                writeMinKey();
                break;
            case DB_POINTER:
                writeDBPointer(reader.readDBPointer());
                break;
            case MAX_KEY:
                reader.readMaxKey();
                writeMaxKey();
                break;
            default:
                throw new IllegalArgumentException("unhandled BSON type: " + reader.getCurrentBsonType());
        }
    }

    private void pipeDocument(final BsonDocument value) {
        writeStartDocument();
        for (Map.Entry<String, BsonValue> cur : value.entrySet()) {
            writeName(cur.getKey());
            pipeValue(cur.getValue());
        }
        writeEndDocument();
    }

    private void pipeArray(final BsonReader reader) {
        reader.readStartArray();
        writeStartArray();
        while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
            pipeValue(reader);
            if (abortPipe()) {
                return;
            }
        }
        reader.readEndArray();
        writeEndArray();
    }

    private void pipeArray(final BsonArray array) {
        writeStartArray();
        for (BsonValue cur : array) {
            pipeValue(cur);
        }
        writeEndArray();
    }

    private void pipeJavascriptWithScope(final BsonJavaScriptWithScope javaScriptWithScope) {
        writeJavaScriptWithScope(javaScriptWithScope.getCode());
        pipeDocument(javaScriptWithScope.getScope());
    }

    private void pipeValue(final BsonValue value) {
        switch (value.getBsonType()) {
            case DOCUMENT:
                pipeDocument(value.asDocument());
                break;
            case ARRAY:
                pipeArray(value.asArray());
                break;
            case DOUBLE:
                writeDouble(value.asDouble().getValue());
                break;
            case STRING:
                writeString(value.asString().getValue());
                break;
            case BINARY:
                writeBinaryData(value.asBinary());
                break;
            case UNDEFINED:
                writeUndefined();
                break;
            case OBJECT_ID:
                writeObjectId(value.asObjectId().getValue());
                break;
            case BOOLEAN:
                writeBoolean(value.asBoolean().getValue());
                break;
            case DATE_TIME:
                writeDateTime(value.asDateTime().getValue());
                break;
            case NULL:
                writeNull();
                break;
            case REGULAR_EXPRESSION:
                writeRegularExpression(value.asRegularExpression());
                break;
            case JAVASCRIPT:
                writeJavaScript(value.asJavaScript().getCode());
                break;
            case SYMBOL:
                writeSymbol(value.asSymbol().getSymbol());
                break;
            case JAVASCRIPT_WITH_SCOPE:
                pipeJavascriptWithScope(value.asJavaScriptWithScope());
                break;
            case INT32:
                writeInt32(value.asInt32().getValue());
                break;
            case TIMESTAMP:
                writeTimestamp(value.asTimestamp());
                break;
            case INT64:
                writeInt64(value.asInt64().getValue());
                break;
            case DECIMAL128:
                writeDecimal128(value.asDecimal128().getValue());
                break;
            case MIN_KEY:
                writeMinKey();
                break;
            case DB_POINTER:
                writeDBPointer(value.asDBPointer());
                break;
            case MAX_KEY:
                writeMaxKey();
                break;
            default:
                throw new IllegalArgumentException("unhandled BSON type: " + value.getBsonType());
        }
    }

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

        /**
         * The writer is positioned to write a name.
         */
        NAME,

        /**
         * The writer is positioned to write a value.
         */
        VALUE,

        /**
         * The writer is positioned to write a scope document (call WriteStartDocument to start writing the scope document).
         */
        SCOPE_DOCUMENT,

        /**
         * The writer is done.
         */
        DONE,

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

    /**
     * The context for the writer. 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 writer is reading a document, array, or other
     * complex sub-structure.
     */
    public class Context {
        private final Context parentContext;
        private final BsonContextType contextType;
        private String name;

        /**
         * Creates a new instance, copying values from an existing context.
         *
         * @param from the {@code Context} to copy values from
         */
        public Context(final Context from) {
            parentContext = from.parentContext;
            contextType = from.contextType;
        }

        /**
         * Creates a new instance.
         *
         * @param parentContext the context of the parent node
         * @param contextType   the context type.
         */
        public 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
         */
        public Context getParentContext() {
            return parentContext;
        }

        /**
         * Gets the current context type.
         *
         * @return the current context type.
         */
        public BsonContextType getContextType() {
            return contextType;
        }

        /**
         * Copies the values from this {@code Context} into a new instance.
         *
         * @return the new instance with the same values as this context.
         */
        public Context copy() {
            return new Context(this);
        }
    }

    /**
     * Capture the current state of this writer - its {@link org.bson.AbstractBsonWriter.Context}, {@link
     * org.bson.AbstractBsonWriter.State}, field name and depth.
     */
    protected class Mark {
        private final Context markedContext;
        private final State markedState;
        private final String currentName;
        private final int serializationDepth;

        /**
         * Creates a new snapshopt of the current state.
         */
        protected Mark() {
            this.markedContext = AbstractBsonWriter.this.context.copy();
            this.markedState = AbstractBsonWriter.this.state;
            this.currentName = AbstractBsonWriter.this.context.name;
            this.serializationDepth = AbstractBsonWriter.this.serializationDepth;
        }

        /**
         * Resets the {@code AbstractBsonWriter} instance that contains this {@code Mark} to the state the writer was in when the Mark was
         * created.
         */
        protected void reset() {
            setContext(markedContext);
            setState(markedState);
            AbstractBsonWriter.this.context.name = currentName;
            AbstractBsonWriter.this.serializationDepth = serializationDepth;
        }
    }
}


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

package com.mongodb.internal;

import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/**
 * This class is not part of the public API and may be removed or changed at any time.
 */
public final class HexUtils {
    /**
     * Converts the given byte buffer to a hexadecimal string using {@link java.lang.Integer#toHexString(int)}.
     *
     * @param bytes the bytes to convert to hex
     * @return a String containing the hex representation of the given bytes.
     */
    public static String toHex(final byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (final byte b : bytes) {
            String s = Integer.toHexString(0xff & b);

            if (s.length() < 2) {
                sb.append("0");
            }
            sb.append(s);
        }
        return sb.toString();
    }

    /**
     * Produce hex representation of the MD5 digest of a byte array.
     *
     * @param data bytes to digest
     * @return hex string of the MD5 digest
     */
    public static String hexMD5(final byte[] data) {
        try {
            MessageDigest md5 = MessageDigest.getInstance("MD5");

            md5.reset();
            md5.update(data);
            byte[] digest = md5.digest();

            return toHex(digest);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("Error - this implementation of Java doesn't support MD5.");
        }
    }

    /**
     * Produce hex representation of the MD5 digest of a byte array.
     *
     * @param buf    byte buffer containing the bytes to digest
     * @param offset the position to start reading bytes from
     * @param len    the number of bytes to read from the buffer
     * @return hex string of the MD5 digest
     */
    public static String hexMD5(final ByteBuffer buf, final int offset, final int len) {
        byte[] b = new byte[len];
        for (int i = 0; i < len; i++) {
            b[i] = buf.get(offset + i);
        }

        return hexMD5(b);
    }
}


/*
 * 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.io.Serializable;
import java.nio.ByteBuffer;
import java.security.SecureRandom;
import java.util.Date;
import java.util.concurrent.atomic.AtomicInteger;

import static org.bson.assertions.Assertions.isTrueArgument;
import static org.bson.assertions.Assertions.notNull;

/**
 *
A globally unique identifier for objects.

 *
 *
Consists of 12 bytes, divided as follows:

 *

 *
ObjectID layout

 *

 *         0	1	2	3	4	5	6	7	8	9	10	11
 *

 *

 *         time	random value	inc
 *

 *

 *
 *
Instances of this class are immutable.

 *
 * @mongodb.driver.manual core/object-id ObjectId
 */
public final class ObjectId implements Comparable<ObjectId>, Serializable {

    private static final long serialVersionUID = 3670079982654483072L;

    private static final int OBJECT_ID_LENGTH = 12;
    private static final int LOW_ORDER_THREE_BYTES = 0x00ffffff;

    // Use primitives to represent the 5-byte random value.
    private static final int RANDOM_VALUE1;
    private static final short RANDOM_VALUE2;

    private static final AtomicInteger NEXT_COUNTER = new AtomicInteger(new SecureRandom().nextInt());

    private static final char[] HEX_CHARS = new char[] {
            '0', '1', '2', '3', '4', '5', '6', '7',
            '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };

    private final int timestamp;
    private final int counter;
    private final int randomValue1;
    private final short randomValue2;

    /**
     * Gets a new object id.
     *
     * @return the new id
     */
    public static ObjectId get() {
        return new ObjectId();
    }

    /**
     * Checks if a string could be an {@code ObjectId}.
     *
     * @param hexString a potential ObjectId as a String.
     * @return whether the string could be an object id
     * @throws IllegalArgumentException if hexString is null
     */
    public static boolean isValid(final String hexString) {
        if (hexString == null) {
            throw new IllegalArgumentException();
        }

        int len = hexString.length();
        if (len != 24) {
            return false;
        }

        for (int i = 0; i < len; i++) {
            char c = hexString.charAt(i);
            if (c >= '0' && c <= '9') {
                continue;
            }
            if (c >= 'a' && c <= 'f') {
                continue;
            }
            if (c >= 'A' && c <= 'F') {
                continue;
            }

            return false;
        }

        return true;
    }

    /**
     * Create a new object id.
     */
    public ObjectId() {
        this(new Date());
    }

    /**
     * Constructs a new instance using the given date.
     *
     * @param date the date
     */
    public ObjectId(final Date date) {
        this(dateToTimestampSeconds(date), NEXT_COUNTER.getAndIncrement() & LOW_ORDER_THREE_BYTES, false);
    }

    /**
     * Constructs a new instances using the given date and counter.
     *
     * @param date    the date
     * @param counter the counter
     * @throws IllegalArgumentException if the high order byte of counter is not zero
     */
    public ObjectId(final Date date, final int counter) {
        this(dateToTimestampSeconds(date), counter, true);
    }

    /**
     * Constructs a new instances using the given date, machine identifier, process identifier, and counter.
     *
     * @param date              the date
     * @param machineIdentifier the machine identifier
     * @param processIdentifier the process identifier
     * @param counter           the counter
     * @throws IllegalArgumentException if the high order byte of machineIdentifier or counter is not zero
     * @deprecated Use {@link #ObjectId(Date, int)} instead
     */
    @Deprecated
    public ObjectId(final Date date, final int machineIdentifier, final short processIdentifier, final int counter) {
        this(dateToTimestampSeconds(date), machineIdentifier, processIdentifier, counter);
    }

    /**
     * Creates an ObjectId using the given time, machine identifier, process identifier, and counter.
     *
     * @param timestamp         the time in seconds
     * @param machineIdentifier the machine identifier
     * @param processIdentifier the process identifier
     * @param counter           the counter
     * @throws IllegalArgumentException if the high order byte of machineIdentifier or counter is not zero
     * @deprecated Use {@link #ObjectId(int, int)} instead
     */
    @Deprecated
    public ObjectId(final int timestamp, final int machineIdentifier, final short processIdentifier, final int counter) {
        this(timestamp, machineIdentifier, processIdentifier, counter, true);
    }

    /**
     * Creates an ObjectId using the given time, machine identifier, process identifier, and counter.
     *
     * @param timestamp         the time in seconds
     * @param counter           the counter
     * @throws IllegalArgumentException if the high order byte of counter is not zero
     */
    public ObjectId(final int timestamp, final int counter) {
        this(timestamp, counter, true);
    }

    private ObjectId(final int timestamp, final int counter, final boolean checkCounter) {
        this(timestamp, RANDOM_VALUE1, RANDOM_VALUE2, counter, checkCounter);
    }

    private ObjectId(final int timestamp, final int randomValue1, final short randomValue2, final int counter,
                     final boolean checkCounter) {
        if ((randomValue1 & 0xff000000) != 0) {
            throw new IllegalArgumentException("The machine identifier must be between 0 and 16777215 (it must fit in three bytes).");
        }
        if (checkCounter && ((counter & 0xff000000) != 0)) {
            throw new IllegalArgumentException("The counter must be between 0 and 16777215 (it must fit in three bytes).");
        }
        this.timestamp = timestamp;
        this.counter = counter & LOW_ORDER_THREE_BYTES;
        this.randomValue1 = randomValue1;
        this.randomValue2 = randomValue2;
    }

    /**
     * Constructs a new instance from a 24-byte hexadecimal string representation.
     *
     * @param hexString the string to convert
     * @throws IllegalArgumentException if the string is not a valid hex string representation of an ObjectId
     */
    public ObjectId(final String hexString) {
        this(parseHexString(hexString));
    }

    /**
     * Constructs a new instance from the given byte array
     *
     * @param bytes the byte array
     * @throws IllegalArgumentException if array is null or not of length 12
     */
    public ObjectId(final byte[] bytes) {
        this(ByteBuffer.wrap(isTrueArgument("bytes has length of 12", bytes, notNull("bytes", bytes).length == 12)));
    }

    /**
     * Creates an ObjectId
     *
     * @param timestamp                   time in seconds
     * @param machineAndProcessIdentifier machine and process identifier
     * @param counter                     incremental value
     */
    ObjectId(final int timestamp, final int machineAndProcessIdentifier, final int counter) {
        this(legacyToBytes(timestamp, machineAndProcessIdentifier, counter));
    }

    /**
     * Constructs a new instance from the given ByteBuffer
     *
     * @param buffer the ByteBuffer
     * @throws IllegalArgumentException if the buffer is null or does not have at least 12 bytes remaining
     * @since 3.4
     */
    public ObjectId(final ByteBuffer buffer) {
        notNull("buffer", buffer);
        isTrueArgument("buffer.remaining() >=12", buffer.remaining() >= OBJECT_ID_LENGTH);

        // Note: Cannot use ByteBuffer.getInt because it depends on tbe buffer's byte order
        // and ObjectId's are always in big-endian order.
        timestamp = makeInt(buffer.get(), buffer.get(), buffer.get(), buffer.get());
        randomValue1 = makeInt((byte) 0, buffer.get(), buffer.get(), buffer.get());
        randomValue2 = makeShort(buffer.get(), buffer.get());
        counter = makeInt((byte) 0, buffer.get(), buffer.get(), buffer.get());
    }

    private static byte[] legacyToBytes(final int timestamp, final int machineAndProcessIdentifier, final int counter) {
        byte[] bytes = new byte[OBJECT_ID_LENGTH];
        bytes[0] = int3(timestamp);
        bytes[1] = int2(timestamp);
        bytes[2] = int1(timestamp);
        bytes[3] = int0(timestamp);
        bytes[4] = int3(machineAndProcessIdentifier);
        bytes[5] = int2(machineAndProcessIdentifier);
        bytes[6] = int1(machineAndProcessIdentifier);
        bytes[7] = int0(machineAndProcessIdentifier);
        bytes[8] = int3(counter);
        bytes[9] = int2(counter);
        bytes[10] = int1(counter);
        bytes[11] = int0(counter);
        return bytes;
    }

    /**
     * Convert to a byte array.  Note that the numbers are stored in big-endian order.
     *
     * @return the byte array
     */
    public byte[] toByteArray() {
        ByteBuffer buffer = ByteBuffer.allocate(OBJECT_ID_LENGTH);
        putToByteBuffer(buffer);
        return buffer.array();  // using .allocate ensures there is a backing array that can be returned
    }

    /**
     * Convert to bytes and put those bytes to the provided ByteBuffer.
     * Note that the numbers are stored in big-endian order.
     *
     * @param buffer the ByteBuffer
     * @throws IllegalArgumentException if the buffer is null or does not have at least 12 bytes remaining
     * @since 3.4
     */
    public void putToByteBuffer(final ByteBuffer buffer) {
        notNull("buffer", buffer);
        isTrueArgument("buffer.remaining() >=12", buffer.remaining() >= OBJECT_ID_LENGTH);

        buffer.put(int3(timestamp));
        buffer.put(int2(timestamp));
        buffer.put(int1(timestamp));
        buffer.put(int0(timestamp));
        buffer.put(int2(randomValue1));
        buffer.put(int1(randomValue1));
        buffer.put(int0(randomValue1));
        buffer.put(short1(randomValue2));
        buffer.put(short0(randomValue2));
        buffer.put(int2(counter));
        buffer.put(int1(counter));
        buffer.put(int0(counter));
    }

    /**
     * Gets the timestamp (number of seconds since the Unix epoch).
     *
     * @return the timestamp
     */
    public int getTimestamp() {
        return timestamp;
    }

    /**
     * Gets the timestamp as a {@code Date} instance.
     *
     * @return the Date
     */
    public Date getDate() {
        return new Date((timestamp & 0xFFFFFFFFL) * 1000L);
    }

    /**
     * Converts this instance into a 24-byte hexadecimal string representation.
     *
     * @return a string representation of the ObjectId in hexadecimal format
     */
    public String toHexString() {
        char[] chars = new char[OBJECT_ID_LENGTH * 2];
        int i = 0;
        for (byte b : toByteArray()) {
            chars[i++] = HEX_CHARS[b >> 4 & 0xF];
            chars[i++] = HEX_CHARS[b & 0xF];
        }
        return new String(chars);
    }

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

        ObjectId objectId = (ObjectId) o;

        if (counter != objectId.counter) {
            return false;
        }
        if (timestamp != objectId.timestamp) {
            return false;
        }

        if (randomValue1 != objectId.randomValue1) {
            return false;
        }

        if (randomValue2 != objectId.randomValue2) {
            return false;
        }

        return true;
    }

    @Override
    public int hashCode() {
        int result = timestamp;
        result = 31 * result + counter;
        result = 31 * result + randomValue1;
        result = 31 * result + randomValue2;
        return result;
    }

    @Override
    public int compareTo(final ObjectId other) {
        if (other == null) {
            throw new NullPointerException();
        }

        byte[] byteArray = toByteArray();
        byte[] otherByteArray = other.toByteArray();
        for (int i = 0; i < OBJECT_ID_LENGTH; i++) {
            if (byteArray[i] != otherByteArray[i]) {
                return ((byteArray[i] & 0xff) < (otherByteArray[i] & 0xff)) ? -1 : 1;
            }
        }
        return 0;
    }

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

    // Deprecated methods

    /**
     *
Creates an ObjectId using time, machine and inc values.  The Java driver used to create all ObjectIds this way, but it does not
     * match the ObjectId specification, which requires four values, not
     * three. This major release of the Java driver conforms to the specification, but still supports clients that are relying on the
     * behavior of the previous major release by providing this explicit factory method that takes three parameters instead of four.

     *
     *
Ordinary users of the driver will not need this method.  It's only for those that have written there own BSON decoders.

     *
     *
NOTE: This will not break any application that use ObjectIds.  The 12-byte representation will be round-trippable from old to new
     * driver releases.

     *
     * @param time    time in seconds
     * @param machine machine ID
     * @param inc     incremental value
     * @return a new {@code ObjectId} created from the given values
     * @since 2.12.0
     * @deprecated Use {@link #ObjectId(int, int)} instead
     */
    @Deprecated
    public static ObjectId createFromLegacyFormat(final int time, final int machine, final int inc) {
        return new ObjectId(time, machine, inc);
    }

    /**
     * Gets the current value of the auto-incrementing counter.
     *
     * @return the current counter value.
     * @deprecated
     */
    @Deprecated
    public static int getCurrentCounter() {
        return NEXT_COUNTER.get() & LOW_ORDER_THREE_BYTES;
    }

    /**
     * Gets the generated machine identifier.
     *
     * @return an int representing the machine identifier
     * @deprecated
     */
    @Deprecated
    public static int getGeneratedMachineIdentifier() {
        return RANDOM_VALUE1;
    }

    /**
     * Gets the generated process identifier.
     *
     * @return the process id
     * @deprecated
     */
    @Deprecated
    public static int getGeneratedProcessIdentifier() {
        return RANDOM_VALUE2;
    }

    /**
     * Gets the machine identifier.
     *
     * @return the machine identifier
     * @deprecated
     */
    @Deprecated
    public int getMachineIdentifier() {
        return randomValue1;
    }

    /**
     * Gets the process identifier.
     *
     * @return the process identifier
     * @deprecated
     */
    @Deprecated
    public short getProcessIdentifier() {
        return randomValue2;
    }

    /**
     * Gets the counter.
     *
     * @return the counter
     * @deprecated
     */
    @Deprecated
    public int getCounter() {
        return counter;
    }

    /**
     * Gets the time of this ID, in seconds.
     *
     * @return the time component of this ID in seconds
     * @deprecated Use #getTimestamp instead
     */
    @Deprecated
    public int getTimeSecond() {
        return timestamp;
    }

    /**
     * Gets the time of this instance, in milliseconds.
     *
     * @return the time component of this ID in milliseconds
     * @deprecated Use #getDate instead
     */
    @Deprecated
    public long getTime() {
        return (timestamp & 0xFFFFFFFFL) * 1000L;
    }

    /**
     * @return a string representation of the ObjectId in hexadecimal format
     * @see ObjectId#toHexString()
     * @deprecated use {@link #toHexString()}
     */
    @Deprecated
    public String toStringMongod() {
        return toHexString();
    }

    static {
        try {
            SecureRandom secureRandom = new SecureRandom();
            RANDOM_VALUE1 = secureRandom.nextInt(0x01000000);
            RANDOM_VALUE2 = (short) secureRandom.nextInt(0x00008000);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private static byte[] parseHexString(final String s) {
        if (!isValid(s)) {
            throw new IllegalArgumentException("invalid hexadecimal representation of an ObjectId: [" + s + "]");
        }

        byte[] b = new byte[OBJECT_ID_LENGTH];
        for (int i = 0; i < b.length; i++) {
            b[i] = (byte) Integer.parseInt(s.substring(i * 2, i * 2 + 2), 16);
        }
        return b;
    }

    private static int dateToTimestampSeconds(final Date time) {
        return (int) (time.getTime() / 1000);
    }

    // Big-Endian helpers, in this class because all other BSON numbers are little-endian

    private static int makeInt(final byte b3, final byte b2, final byte b1, final byte b0) {
        // CHECKSTYLE:OFF
        return (((b3) << 24) |
                ((b2 & 0xff) << 16) |
                ((b1 & 0xff) << 8) |
                ((b0 & 0xff)));
        // CHECKSTYLE:ON
    }

    private static short makeShort(final byte b1, final byte b0) {
        // CHECKSTYLE:OFF
        return (short) (((b1 & 0xff) << 8) | ((b0 & 0xff)));
        // CHECKSTYLE:ON
    }

    private static byte int3(final int x) {
        return (byte) (x >> 24);
    }

    private static byte int2(final int x) {
        return (byte) (x >> 16);
    }

    private static byte int1(final int x) {
        return (byte) (x >> 8);
    }

    private static byte int0(final int x) {
        return (byte) (x);
    }

    private static byte short1(final short x) {
        return (byte) (x >> 8);
    }

    private static byte short0(final short x) {
        return (byte) (x);
    }

}


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

import org.bson.BsonDocument;
import org.bson.BsonElement;
import org.bson.BsonObjectId;
import org.bson.BsonReader;
import org.bson.BsonType;
import org.bson.BsonValue;
import org.bson.BsonWriter;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.types.ObjectId;

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

import static org.bson.codecs.BsonValueCodecProvider.getBsonTypeClassMap;
import static org.bson.codecs.configuration.CodecRegistries.fromProviders;

/**
 * A codec for BsonDocument instances.
 *
 * @since 3.0
 */
public class BsonDocumentCodec implements CollectibleCodec<BsonDocument> {
    private static final String ID_FIELD_NAME = "_id";
    private static final CodecRegistry DEFAULT_REGISTRY = fromProviders(new BsonValueCodecProvider());

    private final CodecRegistry codecRegistry;
    private final BsonTypeCodecMap bsonTypeCodecMap;

    /**
     * Creates a new instance with a default codec registry that uses the {@link BsonValueCodecProvider}.
     */
    public BsonDocumentCodec() {
        this(DEFAULT_REGISTRY);
    }

    /**
     * Creates a new instance initialised with the given codec registry.
     *
     * @param codecRegistry the {@code CodecRegistry} to use to look up the codecs for encoding and decoding to/from BSON
     */
    public BsonDocumentCodec(final CodecRegistry codecRegistry) {
        if (codecRegistry == null) {
            throw new IllegalArgumentException("Codec registry can not be null");
        }
        this.codecRegistry = codecRegistry;
        this.bsonTypeCodecMap = new BsonTypeCodecMap(getBsonTypeClassMap(), codecRegistry);
    }

    /**
     * Gets the {@code CodecRegistry} for this {@code Codec}.
     *
     * @return the registry
     */
    public CodecRegistry getCodecRegistry() {
        return codecRegistry;
    }

    @Override
    public BsonDocument decode(final BsonReader reader, final DecoderContext decoderContext) {
        List<BsonElement> keyValuePairs = new ArrayList<BsonElement>();

        reader.readStartDocument();
        while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
            String fieldName = reader.readName();
            keyValuePairs.add(new BsonElement(fieldName, readValue(reader, decoderContext)));
        }

        reader.readEndDocument();

        return new BsonDocument(keyValuePairs);
    }

    /**
     * This method may be overridden to change the behavior of reading the current value from the given {@code BsonReader}.  It is required
     * that the value be fully consumed before returning.
     *
     * @param reader         the read to read the value from
     * @param decoderContext the context
     * @return the non-null value read from the reader
     */
    protected BsonValue readValue(final BsonReader reader, final DecoderContext decoderContext) {
        return (BsonValue) bsonTypeCodecMap.get(reader.getCurrentBsonType()).decode(reader, decoderContext);
    }

    @Override
    public void encode(final BsonWriter writer, final BsonDocument value, final EncoderContext encoderContext) {
        writer.writeStartDocument();

        beforeFields(writer, encoderContext, value);
        for (Map.Entry<String, BsonValue> entry : value.entrySet()) {
            if (skipField(encoderContext, entry.getKey())) {
                continue;
            }

            writer.writeName(entry.getKey());
            writeValue(writer, encoderContext, entry.getValue());
        }

        writer.writeEndDocument();
    }

    private void beforeFields(final BsonWriter bsonWriter, final EncoderContext encoderContext, final BsonDocument value) {
        if (encoderContext.isEncodingCollectibleDocument() && value.containsKey(ID_FIELD_NAME)) {
            bsonWriter.writeName(ID_FIELD_NAME);
            writeValue(bsonWriter, encoderContext, value.get(ID_FIELD_NAME));
        }
    }

    private boolean skipField(final EncoderContext encoderContext, final String key) {
        return encoderContext.isEncodingCollectibleDocument() && key.equals(ID_FIELD_NAME);
    }

    @SuppressWarnings({"unchecked", "rawtypes"})
    private void writeValue(final BsonWriter writer, final EncoderContext encoderContext, final BsonValue value) {
        Codec codec = codecRegistry.get(value.getClass());
        encoderContext.encodeWithChildContext(codec, writer, value);
    }

    @Override
    public Class<BsonDocument> getEncoderClass() {
        return BsonDocument.class;
    }

    @Override
    public BsonDocument generateIdIfAbsentFromDocument(final BsonDocument document) {
        if (!documentHasId(document)) {
            document.put(ID_FIELD_NAME, new BsonObjectId(new ObjectId()));
        }
        return document;
    }

    @Override
    public boolean documentHasId(final BsonDocument document) {
        return document.containsKey(ID_FIELD_NAME);
    }

    @Override
    public BsonValue getDocumentId(final BsonDocument document) {
        return document.get(ID_FIELD_NAME);
    }
}


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

package com.mongodb.connection;

import com.mongodb.ServerAddress;
import com.mongodb.TagSet;
import com.mongodb.annotations.Immutable;
import com.mongodb.annotations.NotThreadSafe;
import com.mongodb.lang.Nullable;
import com.mongodb.internal.connection.DecimalFormatHelper;
import com.mongodb.internal.connection.Time;
import org.bson.types.ObjectId;

import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import static com.mongodb.assertions.Assertions.notNull;
import static com.mongodb.connection.ServerConnectionState.CONNECTED;
import static com.mongodb.connection.ServerType.REPLICA_SET_PRIMARY;
import static com.mongodb.connection.ServerType.REPLICA_SET_SECONDARY;
import static com.mongodb.connection.ServerType.SHARD_ROUTER;
import static com.mongodb.connection.ServerType.STANDALONE;
import static com.mongodb.connection.ServerType.UNKNOWN;

/**
 * Immutable snapshot state of a server.
 *
 * @since 3.0
 */
@Immutable
public class ServerDescription {

    /**
     * The minimum supported driver server version
     * @since 3.8
     */
    public static final String MIN_DRIVER_SERVER_VERSION = "2.6";
    /**
     * The minimum supported driver wire version
     * @since 3.8
     */
    public static final int MIN_DRIVER_WIRE_VERSION = 1;
    /**
     * The maximum supported driver wire version
     * @since 3.8
     */
    public static final int MAX_DRIVER_WIRE_VERSION = 8;

    private static final int DEFAULT_MAX_DOCUMENT_SIZE = 0x1000000;  // 16MB

    private final ServerAddress address;

    private final ServerType type;
    private final String canonicalAddress;
    private final Set<String> hosts;
    private final Set<String> passives;
    private final Set<String> arbiters;
    private final String primary;
    private final int maxDocumentSize;
    private final TagSet tagSet;
    private final String setName;
    private final long roundTripTimeNanos;
    private final boolean ok;
    private final ServerConnectionState state;
    private final ServerVersion version;

    private final int minWireVersion;
    private final int maxWireVersion;

    private final ObjectId electionId;
    private final Integer setVersion;
    private final Date lastWriteDate;
    private final long lastUpdateTimeNanos;

    private final Integer logicalSessionTimeoutMinutes;

    private final Throwable exception;

    /**
     * Gets a Builder for creating a new ServerDescription instance.
     *
     * @return a new Builder for ServerDescription.
     */
    public static Builder builder() {
        return new Builder();
    }

    /**
     * Gets the string representing the host name and port that this member of a replica set was configured with,
     * e.g. {@code "somehost:27019"}. This is typically derived from the "me" field from the "isMaster" command response.
     *
     * @return the host name and port that this replica set member is configured with.
     */
    public String getCanonicalAddress() {
        return canonicalAddress;
    }

    /**
     * Gets the session timeout in minutes.
     *
     * @return the session timeout in minutes, or null if sessions are not supported by this server
     * @mongodb.server.release 3.6
     * @since 3.6
     */
    public Integer getLogicalSessionTimeoutMinutes() {
        return logicalSessionTimeoutMinutes;
    }

    /**
     * A builder for creating ServerDescription.
     */
    @NotThreadSafe
    public static class Builder {
        private ServerAddress address;
        private ServerType type = UNKNOWN;
        private String canonicalAddress;
        private Set<String> hosts = Collections.emptySet();
        private Set<String> passives = Collections.emptySet();
        private Set<String> arbiters = Collections.emptySet();
        private String primary;
        private int maxDocumentSize = DEFAULT_MAX_DOCUMENT_SIZE;
        private TagSet tagSet = new TagSet();
        private String setName;
        private long roundTripTimeNanos;
        private boolean ok;
        private ServerConnectionState state;
        private ServerVersion version = new ServerVersion();
        private int minWireVersion = 0;
        private int maxWireVersion = 0;
        private ObjectId electionId;
        private Integer setVersion;
        private Date lastWriteDate;
        private long lastUpdateTimeNanos = Time.nanoTime();
        private Integer logicalSessionTimeoutMinutes;

        private Throwable exception;

        /**
         * Sets the address of the server.
         *
         * @param address the address of the server
         * @return this
         */
        public Builder address(final ServerAddress address) {
            this.address = address;
            return this;
        }

        /**
         * Sets the canonical host name and port of this server. This is typically derived from the "me" field contained in the "isMaster"
         * command. response.
         *
         * @param canonicalAddress the host name and port as a string
         *
         * @return this
         */
        public Builder canonicalAddress(final String canonicalAddress) {
            this.canonicalAddress = canonicalAddress;
            return this;
        }

        /**
         * Sets the type of the server, for example whether it's a standalone or in a replica set.
         *
         * @param type the Server type
         * @return this
         */
        public Builder type(final ServerType type) {
            this.type = notNull("type", type);
            return this;
        }

        /**
         * Sets all members of the replica set that are neither hidden, passive, nor arbiters.
         *
         * @param hosts A Set of strings in the format of "[hostname]:[port]" that contains all members of the replica set that are neither
         *              hidden, passive, nor arbiters.
         * @return this
         */
        public Builder hosts(final Set<String> hosts) {
            this.hosts = hosts == null ? Collections.<String>emptySet() : Collections.unmodifiableSet(new HashSet<String>(hosts));
            return this;
        }

        /**
         * Sets the passive members of the replica set.
         *
         * @param passives A Set of strings in the format of "[hostname]:[port]" listing all members of the replica set which have a
         *                 priority of 0.
         * @return this
         */
        public Builder passives(final Set<String> passives) {
            this.passives = passives == null ? Collections.<String>emptySet() : Collections.unmodifiableSet(new HashSet<String>(passives));
            return this;
        }

        /**
         * Sets the arbiters in the replica set
         *
         * @param arbiters A Set of strings in the format of "[hostname]:[port]" containing all members of the replica set that are
         *                 arbiters.
         * @return this
         */
        public Builder arbiters(final Set<String> arbiters) {
            this.arbiters = arbiters == null ? Collections.<String>emptySet() : Collections.unmodifiableSet(new HashSet<String>(arbiters));
            return this;
        }

        /**
         * Sets the address of the current primary in the replica set
         *
         * @param primary A string in the format of "[hostname]:[port]" listing the current primary member of the replica set.
         * @return this
         */
        public Builder primary(final String primary) {
            this.primary = primary;
            return this;
        }

        /**
         * The maximum permitted size of a BSON object in bytes for this mongod process. Defaults to 16MB.
         *
         * @param maxDocumentSize the maximum size a document can be
         * @return this
         */
        public Builder maxDocumentSize(final int maxDocumentSize) {
            this.maxDocumentSize = maxDocumentSize;
            return this;
        }

        /**
         * A set of any tags assigned to this member.
         *
         * @param tagSet a TagSet with all the tags for this server.
         * @return this
         */
        public Builder tagSet(final TagSet tagSet) {
            this.tagSet = tagSet == null ? new TagSet() : tagSet;
            return this;
        }

        /**
         * Set the time it took to make the round trip for requesting this information from the server
         *
         * @param roundTripTime the time taken
         * @param timeUnit      the units of the time taken
         * @return this
         */
        public Builder roundTripTime(final long roundTripTime, final TimeUnit timeUnit) {
            this.roundTripTimeNanos = timeUnit.toNanos(roundTripTime);
            return this;
        }

        /**
         * Sets the name of the replica set
         *
         * @param setName the name of the replica set
         * @return this
         */
        public Builder setName(final String setName) {
            this.setName = setName;
            return this;
        }

        /**
         * The isOK() result from requesting this information from MongoDB
         *
         * @param ok true if the request executed correctly
         * @return this
         */
        public Builder ok(final boolean ok) {
            this.ok = ok;
            return this;
        }

        /**
         * The current state of the connection to the server.
         *
         * @param state ServerConnectionState representing whether the server has been successfully connected to
         * @return this
         */
        public Builder state(final ServerConnectionState state) {
            this.state = state;
            return this;
        }

        /**
         * Sets the server version
         *
         * @param version a ServerVersion representing which version of MongoDB is running on this server
         * @return this
         * @deprecated Use {@link #maxWireVersion} instead
         */
        @Deprecated
        public Builder version(final ServerVersion version) {
            notNull("version", version);
            this.version = version;
            return this;
        }

        /**
         * The earliest version of the wire protocol that this MongoDB server is capable of using to communicate with clients.
         *
         * @param minWireVersion the minimum protocol version supported by this server
         * @return this
         * @mongodb.server.release 2.6
         */
        public Builder minWireVersion(final int minWireVersion) {
            this.minWireVersion = minWireVersion;
            return this;
        }

        /**
         * The latest version of the wire protocol that this MongoDB server is capable of using to communicate with clients.
         *
         * @param maxWireVersion the maximum protocol version supported by this server
         * @return this
         * @mongodb.server.release 2.6
         */
        public Builder maxWireVersion(final int maxWireVersion) {
            this.maxWireVersion = maxWireVersion;
            return this;
        }

        /**
         * Sets the electionId reported by this server.
         *
         * @param electionId the electionId
         * @return this
         */
        public Builder electionId(final ObjectId electionId) {
            this.electionId = electionId;
            return this;
        }

        /**
         * Sets the setVersion reported by this server.
         *
         * @param setVersion the set version
         * @return this
         */
        public Builder setVersion(final Integer setVersion) {
            this.setVersion = setVersion;
            return this;
        }

        /**
         * Sets the lastWriteDate reported by this server
         *
         * @param lastWriteDate the last write date, which may be null for servers prior to 3.4
         * @return this
         *
         * @since 3.4
         * @mongodb.server.release 3.4
         */
        public Builder lastWriteDate(final Date lastWriteDate) {
            this.lastWriteDate = lastWriteDate;
            return this;
        }

        /**
         * Sets the last update time for this description, which is simply the time that the server description was created.
         * A monotonic clock such as {@link System#nanoTime()} should be used to initialize this value.
         *
         * @param lastUpdateTimeNanos the last update time of this server description
         * @return this
         *
         * @since 3.4
         */
        public Builder lastUpdateTimeNanos(final long lastUpdateTimeNanos) {
            this.lastUpdateTimeNanos = lastUpdateTimeNanos;
            return this;
        }

        /**
         * Sets the session timeout in minutes.
         *
         * @param logicalSessionTimeoutMinutes the session timeout in minutes, or null if sessions are not supported by this server
         * @return this
         * @mongodb.server.release 3.6
         * @since 3.6
         */
        public Builder logicalSessionTimeoutMinutes(final Integer logicalSessionTimeoutMinutes) {
            this.logicalSessionTimeoutMinutes = logicalSessionTimeoutMinutes;
            return this;
        }

        /**
         * Sets the exception thrown while attempting to determine the server description.
         *
         * @param exception the exception
         * @return this
         */
        public Builder exception(final Throwable exception) {
            this.exception = exception;
            return this;
        }

        /**
         * Create a new ServerDescription from the settings in this builder.
         *
         * @return a new server description
         */
        public ServerDescription build() {
            return new ServerDescription(this);
        }
    }

    /**
     * Return whether the server is compatible with the driver. An incompatible server is one that has a min wire version greater that the
     * driver's max wire version or a max wire version less than the driver's min wire version.
     *
     * @return true if the server is compatible with the driver.
     */
    public boolean isCompatibleWithDriver() {
        if (isIncompatiblyOlderThanDriver()) {
            return false;
        }

        if (isIncompatiblyNewerThanDriver()) {
            return false;
        }

        return true;
    }

    /**
     * Return whether the server is compatible with the driver. An incompatible server is one that has a min wire version greater that the
     * driver's max wire version or a max wire version less than the driver's min wire version.
     *
     * @return true if the server is compatible with the driver.
     * @since 3.6
     */
    public boolean isIncompatiblyNewerThanDriver() {
        return ok && minWireVersion > MAX_DRIVER_WIRE_VERSION;
    }

    /**
     * Return whether the server is compatible with the driver. An incompatible server is one that has a min wire version greater that the
     * driver's max wire version or a max wire version less than the driver's min wire version.
     *
     * @return true if the server is compatible with the driver.
     * @since 3.6
     */
    public boolean isIncompatiblyOlderThanDriver() {
        return ok && maxWireVersion < MIN_DRIVER_WIRE_VERSION;
    }

    /**
     * Get the default maximum document size.
     *
     * @return the default maximum document size
     */
    public static int getDefaultMaxDocumentSize() {
        return DEFAULT_MAX_DOCUMENT_SIZE;
    }

    /**
     * Get the default minimum wire version
     *
     * @return the default minimum wire version
     */
    public static int getDefaultMinWireVersion() {
        return 0;
    }

    /**
     * Get the default maximum wire version
     *
     * @return the default maximum wire version
     */
    public static int getDefaultMaxWireVersion() {
        return 0;
    }

    /**
     * Gets the address of this server
     *
     * @return a ServerAddress containing the details of the address of this server.
     */
    public ServerAddress getAddress() {
        return address;
    }

    /**
     * Gets whether this server is a replica set member.
     *
     * @return true if this server is part of a replica set
     */
    public boolean isReplicaSetMember() {
        return type.getClusterType() == ClusterType.REPLICA_SET;
    }

    /**
     * Gets whether this is a server that is the entry point to a sharded instance of MongoDB.
     *
     * @return true if this server is a mongos instance
     */
    public boolean isShardRouter() {
        return type == SHARD_ROUTER;
    }

    /**
     * Gets whether this is part of a replica set/sharded system, or is a single server.
     *
     * @return true if this is a single server
     */
    public boolean isStandAlone() {
        return type == STANDALONE;
    }

    /**
     * Returns whether this can be treated as a primary server.
     *
     * @return true if this server is the primary in a replica set, is a mongos, or is a single standalone server
     */
    public boolean isPrimary() {
        return ok && (type == REPLICA_SET_PRIMARY || type == SHARD_ROUTER || type == STANDALONE);
    }

    /**
     * Returns whether this can be treated as a secondary server.
     *
     * @return true if this server is a secondary in a replica set, is a mongos, or is a single standalone server
     */
    public boolean isSecondary() {
        return ok && (type == REPLICA_SET_SECONDARY || type == SHARD_ROUTER || type == STANDALONE);
    }

    /**
     * Get a Set of strings in the format of "[hostname]:[port]" that contains all members of the replica set that are neither hidden,
     * passive, nor arbiters.
     *
     * @return all members of the replica set that are neither hidden, passive, nor arbiters.
     */
    public Set<String> getHosts() {
        return hosts;
    }

    /**
     * Gets the passive members of the replica set.
     *
     * @return A set of strings in the format of "[hostname]:[port]" listing all members of the replica set which have a priority of 0.
     */
    public Set<String> getPassives() {
        return passives;
    }

    /**
     * Gets the arbiters in the replica set
     *
     * @return A Set of strings in the format of "[hostname]:[port]" containing all members of the replica set that are arbiters.
     */
    public Set<String> getArbiters() {
        return arbiters;
    }

    /**
     * Gets the address of the current primary in the replica set
     *
     * @return A string in the format of "[hostname]:[port]" listing the current primary member of the replica set.
     */
    public String getPrimary() {
        return primary;
    }

    /**
     * The maximum permitted size of a BSON object in bytes for this mongod process. Defaults to 16MB.
     *
     * @return the maximum size a document can be
     */
    public int getMaxDocumentSize() {
        return maxDocumentSize;
    }

    /**
     * A set of all tags assigned to this member.
     *
     * @return a TagSet with all the tags for this server.
     */
    public TagSet getTagSet() {
        return tagSet;
    }

    /**
     * The earliest version of the wire protocol that this MongoDB server is capable of using to communicate with clients.
     *
     * @return the minimum protocol version supported by this server
     * @mongodb.server.release 2.6
     */
    public int getMinWireVersion() {
        return minWireVersion;
    }

    /**
     * The latest version of the wire protocol that this MongoDB server is capable of using to communicate with clients.
     *
     * @return the maximum protocol version supported by this server
     * @mongodb.server.release 2.6
     */
    public int getMaxWireVersion() {
        return maxWireVersion;
    }

    /**
     * The replica set electionid reported by this MongoDB server.
     *
     * @return the electionId, which may be null
     */
    public ObjectId getElectionId() {
        return electionId;
    }

    /**
     * The replica set setVersion reported by this MongoDB server.
     *
     * @return the setVersion, which may be null
     */
    public Integer getSetVersion() {
        return setVersion;
    }

    /**
     * Gets the last write date.
     * @return the last write date, which may be null
     * @since 3.4
     * @mongodb.server.release 3.4
     */
    @Nullable
    public Date getLastWriteDate() {
        return lastWriteDate;
    }

    /**
     * Gets the time that this server description was created, using a monotonic clock like {@link System#nanoTime()}.
     *
     * @param timeUnit the time unit
     * @return the last update time in the given unit
     *
     * @since 3.4
     */
    public long getLastUpdateTime(final TimeUnit timeUnit) {
        return timeUnit.convert(lastUpdateTimeNanos, TimeUnit.NANOSECONDS);
    }

    /**
     * Returns true if the server has the given tags.  A server of either type {@code ServerType.STANDALONE} or {@code
     * ServerType.SHARD_ROUTER} is considered to have all tags, so this method will always return true for instances of either of those
     * types.
     *
     * @param desiredTags the tags
     * @return true if this server has the given tags
     */
    public boolean hasTags(final TagSet desiredTags) {
        if (!ok) {
            return false;
        }

        if (type == STANDALONE || type == SHARD_ROUTER) {
            return true;
        }

        return tagSet.containsAll(desiredTags);
    }

    /**
     * Gets the name of the replica set
     *
     * @return the name of the replica set
     */
    public String getSetName() {
        return setName;
    }

    /**
     * The isOK() result from requesting this information from the server
     *
     * @return true if the request executed correctly
     */
    public boolean isOk() {
        return ok;
    }

    /**
     * Gets the current state of the connection to the server.
     *
     * @return ServerConnectionState representing whether the server has been successfully connected to
     */
    public ServerConnectionState getState() {
        return state;
    }

    /**
     * Gets the type of the server, for example whether it's a standalone or in a replica set.
     *
     * @return the server type
     */
    public ServerType getType() {
        return type;
    }

    /**
     * Gets the type of the cluster this server is in (for example, replica set).
     *
     * @return a ClusterType representing the type of the cluster this server is in
     */
    public ClusterType getClusterType() {
        return type.getClusterType();
    }

    /**
     * Gets the server version
     *
     * @return a ServerVersion representing which version of MongoDB is running on this server
     * @deprecated Use {@link #getMaxWireVersion()} instead
     */
    @Deprecated
    public ServerVersion getVersion() {
        return version;
    }

    /**
     * Get the time it took to make the round trip for requesting this information from the server in nanoseconds.
     *
     * @return the time taken to request the information, in nano seconds
     */
    public long getRoundTripTimeNanos() {
        return roundTripTimeNanos;
    }

    /**
     * Gets the exception thrown while attempting to determine the server description.  This is useful for diagnostic purposed when
     * determining the root cause of a connectivity failure.
     *
     * @return the exception, which may be null
     */
    public Throwable getException() {
        return exception;
    }

    /**
     * Returns true if this instance is equals to @code{o}.  Note that equality is defined to NOT include the round trip time.
     *
     * @param o the object to compare to
     * @return true if this instance is equals to @code{o}
     */
    @Override
    public boolean equals(final Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        ServerDescription that = (ServerDescription) o;

        if (maxDocumentSize != that.maxDocumentSize) {
            return false;
        }
        if (ok != that.ok) {
            return false;
        }
        if (!address.equals(that.address)) {
            return false;
        }
        if (!arbiters.equals(that.arbiters)) {
            return false;
        }
        if (canonicalAddress != null ? !canonicalAddress.equals(that.canonicalAddress) : that.canonicalAddress != null) {
            return false;
        }
        if (!hosts.equals(that.hosts)) {
            return false;
        }
        if (!passives.equals(that.passives)) {
            return false;
        }
        if (primary != null ? !primary.equals(that.primary) : that.primary != null) {
            return false;
        }
        if (setName != null ? !setName.equals(that.setName) : that.setName != null) {
            return false;
        }
        if (state != that.state) {
            return false;
        }
        if (!tagSet.equals(that.tagSet)) {
            return false;
        }
        if (type != that.type) {
            return false;
        }
        if (!version.equals(that.version)) {
            return false;
        }
        if (minWireVersion != that.minWireVersion) {
            return false;
        }
        if (maxWireVersion != that.maxWireVersion) {
            return false;
        }
        if (electionId != null ? !electionId.equals(that.electionId) : that.electionId != null) {
            return false;
        }
        if (setVersion != null ? !setVersion.equals(that.setVersion) : that.setVersion != null) {
            return false;
        }
        if (lastWriteDate != null ? !lastWriteDate.equals(that.lastWriteDate) : that.lastWriteDate != null) {
            return false;
        }

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

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

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

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

        return true;
    }

    @Override
    public int hashCode() {
        int result = address.hashCode();
        result = 31 * result + type.hashCode();
        result = 31 * result + (canonicalAddress != null ? canonicalAddress.hashCode() : 0);
        result = 31 * result + hosts.hashCode();
        result = 31 * result + passives.hashCode();
        result = 31 * result + arbiters.hashCode();
        result = 31 * result + (primary != null ? primary.hashCode() : 0);
        result = 31 * result + maxDocumentSize;
        result = 31 * result + tagSet.hashCode();
        result = 31 * result + (setName != null ? setName.hashCode() : 0);
        result = 31 * result + (electionId != null ? electionId.hashCode() : 0);
        result = 31 * result + (setVersion != null ? setVersion.hashCode() : 0);
        result = 31 * result + (lastWriteDate != null ? lastWriteDate.hashCode() : 0);
        result = 31 * result + (int) (lastUpdateTimeNanos ^ (lastUpdateTimeNanos >>> 32));
        result = 31 * result + (ok ? 1 : 0);
        result = 31 * result + state.hashCode();
        result = 31 * result + version.hashCode();
        result = 31 * result + minWireVersion;
        result = 31 * result + maxWireVersion;
        result = 31 * result + (logicalSessionTimeoutMinutes != null ? logicalSessionTimeoutMinutes.hashCode() : 0);
        result = 31 * result + (exception == null ? 0 : exception.getClass().hashCode());
        result = 31 * result + (exception == null ? 0 : exception.getMessage().hashCode());
        return result;
    }

    @Override
    public String toString() {
        return "ServerDescription{"
               + "address=" + address
               + ", type=" + type
               + ", state=" + state
               + (state == CONNECTED
                  ?
                  ", ok=" + ok
                  + ", version=" + version
                  + ", minWireVersion=" + minWireVersion
                  + ", maxWireVersion=" + maxWireVersion
                  + ", maxDocumentSize=" + maxDocumentSize
                  + ", logicalSessionTimeoutMinutes=" + logicalSessionTimeoutMinutes
                  + ", roundTripTimeNanos=" + roundTripTimeNanos
                  : "")
               + (isReplicaSetMember()
                  ?
                  ", setName='" + setName + '\''
                  + ", canonicalAddress=" + canonicalAddress
                  + ", hosts=" + hosts
                  + ", passives=" + passives
                  + ", arbiters=" + arbiters
                  + ", primary='" + primary + '\''
                  + ", tagSet=" + tagSet
                  + ", electionId=" + electionId
                  + ", setVersion=" + setVersion
                  + ", lastWriteDate=" + lastWriteDate
                  + ", lastUpdateTimeNanos=" + lastUpdateTimeNanos
                : "")
               + (exception == null ? "" : ", exception=" + translateExceptionToString())
               + '}';
    }

    /**
     * Returns a short, pretty description for this ServerDescription.
     *
     * @return a String containing the most pertinent information about this ServerDescription
     */
    public String getShortDescription() {
        return "{"
               + "address=" + address
               + ", type=" + type
               + (!tagSet.iterator().hasNext() ? "" : ", " + tagSet)
               + (state == CONNECTED ? (", roundTripTime=" + getRoundTripFormattedInMilliseconds() + " ms") : "")
               + ", state=" + state
               + (exception == null ? "" : ", exception=" + translateExceptionToString())
               + '}';
    }

    private String translateExceptionToString() {
        StringBuilder builder = new StringBuilder();
        builder.append("{");
        builder.append(exception);
        builder.append("}");
        Throwable cur = exception.getCause();
        while (cur != null) {
            builder.append(", caused by ");
            builder.append("{");
            builder.append(cur);
            builder.append("}");
            cur = cur.getCause();
        }

        return builder.toString();
    }

    private String getRoundTripFormattedInMilliseconds() {
        return DecimalFormatHelper.format("#0.0", roundTripTimeNanos / 1000.0 / 1000.0);
    }

    ServerDescription(final Builder builder) {
        address = notNull("address", builder.address);
        type = notNull("type", builder.type);
        state = notNull("state", builder.state);
        version = notNull("version", builder.version);
        canonicalAddress = builder.canonicalAddress;
        hosts = builder.hosts;
        passives = builder.passives;
        arbiters = builder.arbiters;
        primary = builder.primary;
        maxDocumentSize = builder.maxDocumentSize;
        tagSet = builder.tagSet;
        setName = builder.setName;
        roundTripTimeNanos = builder.roundTripTimeNanos;
        ok = builder.ok;
        minWireVersion = builder.minWireVersion;
        maxWireVersion = builder.maxWireVersion;
        electionId = builder.electionId;
        setVersion = builder.setVersion;
        lastWriteDate = builder.lastWriteDate;
        lastUpdateTimeNanos = builder.lastUpdateTimeNanos;
        logicalSessionTimeoutMinutes = builder.logicalSessionTimeoutMinutes;
        exception = builder.exception;
    }
}


/*
 * 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 java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;

/**
 *
Utility for building complex objects. For example:

 *

 *   {@code BasicDBObjectBuilder.start().add( "name" , "eliot").add("number" , 17).get()}
 *

 */
@SuppressWarnings("rawtypes")
public class BasicDBObjectBuilder {

    /**
     * Creates a builder intialized with an empty document.
     */
    public BasicDBObjectBuilder() {
        _stack = new LinkedList<DBObject>();
        _stack.add(new BasicDBObject());
    }

    /**
     * Creates a builder intialized with an empty document.
     *
     * @return The new empty builder
     */
    public static BasicDBObjectBuilder start() {
        return new BasicDBObjectBuilder();
    }

    /**
     * Creates a builder initialized with the given key/value.
     *
     * @param key The field name
     * @param val The value
     * @return the new builder
     */
    public static BasicDBObjectBuilder start(final String key, final Object val) {
        return (new BasicDBObjectBuilder()).add(key, val);
    }

    /**
     * Creates an object builder from an existing map of key value pairs.
     *
     * @param documentAsMap a document in Map form.
     * @return the new builder
     */
    @SuppressWarnings("unchecked")
    public static BasicDBObjectBuilder start(final Map documentAsMap) {
        BasicDBObjectBuilder builder = new BasicDBObjectBuilder();
        Iterator<Map.Entry> i = documentAsMap.entrySet().iterator();
        while (i.hasNext()) {
            Map.Entry entry = i.next();
            builder.add(entry.getKey().toString(), entry.getValue());
        }
        return builder;
    }

    /**
     * Appends the key/value to the active object
     *
     * @param key the field name
     * @param val the value of the field
     * @return {@code this} so calls can be chained
     */
    public BasicDBObjectBuilder append(final String key, final Object val) {
        _cur().put(key, val);
        return this;
    }

    /**
     * Same as append
     *
     * @param key the field name
     * @param val the value of the field
     * @return {@code this} so calls can be chained
     * @see #append(String, Object)
     */
    public BasicDBObjectBuilder add(final String key, final Object val) {
        return append(key, val);
    }

    /**
     * Creates an new empty object and inserts it into the current object with the given key. The new child object becomes the active one.
     *
     * @param key the field name
     * @return {@code this} so calls can be chained
     */
    public BasicDBObjectBuilder push(final String key) {
        BasicDBObject o = new BasicDBObject();
        _cur().put(key, o);
        _stack.addLast(o);
        return this;
    }

    /**
     * Pops the active object, which means that the parent object becomes active
     *
     * @return {@code this} so calls can be chained
     */
    public BasicDBObjectBuilder pop() {
        if (_stack.size() <= 1) {
            throw new IllegalArgumentException("can't pop last element");
        }
        _stack.removeLast();
        return this;
    }

    /**
     * Gets the top level document.
     *
     * @return The base object
     */
    public DBObject get() {
        return _stack.getFirst();
    }

    /**
     * Returns true if no key/value was inserted into the top level document.
     *
     * @return true if empty
     */
    public boolean isEmpty() {
        return ((BasicDBObject) _stack.getFirst()).size() == 0;
    }

    private DBObject _cur() {
        return _stack.getLast();
    }

    private final LinkedList<DBObject> _stack;

}