/* Copyright 2010-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.
*/

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;

namespace MongoDB.Bson.IO
{
    /// <summary>
    /// Represents a BSON reader for a JSON string.
    /// </summary>
    public class JsonReader : BsonReader
    {
        #region static
        private static readonly string[] __variableLengthIso8601Formats = new string[]
        {
            "yyyy-MM-ddTHH:mm:ss.FFFFFFFK",
            "yyyy-MM-ddTHH:mm:ss.FFFFFFFzz",
            "yyyyMMddTHHmmss.FFFFFFFK",
            "yyyyMMddTHHmmss.FFFFFFFzz"
        };

        private static readonly string[][] __fixedLengthIso8601Formats = new string[][]
        {
            null, // length = 0
            null, // length = 1
            null, // length = 2
            null, // length = 3
            new [] { "yyyy" }, // length = 4
            null, // length = 5
            null, // length = 6
            new [] { "yyyy-MM" }, // length = 7
            new [] { "yyyyMMdd" }, // length = 8
            null, // length = 9
            new [] { "yyyy-MM-dd" }, // length = 10
            new [] { "yyyyMMddTHH" }, // length = 11
            new [] { "yyyyMMddTHHZ" }, // length = 12
            new [] { "yyyy-MM-ddTHH" , "yyyyMMddTHHmm" }, // length = 13
            new [] { "yyyy-MM-ddTHHZ", "yyyyMMddTHHmmZ", "yyyyMMddTHHzz" }, // length = 14
            null, // length = 15
            new [] { "yyyy-MM-ddTHH:mm", "yyyy-MM-ddTHHzz", "yyyyMMddTHHmmssZ", "yyyyMMddTHHmmzz" }, // length = 16
            new [] { "yyyy-MM-ddTHH:mmZ", "yyyyMMddTHHzzz" }, // length = 17
            new [] { "yyyyMMddTHHmmsszz" }, // length = 18
            new [] { "yyyy-MM-ddTHH:mm:ss", "yyyy-MM-ddTHHzzz", "yyyy-MM-ddTHH:mmzz", "yyyyMMddTHHmmzzz" }, // length = 19
            null, // length = 20
            null, // length = 21
            new [] { "yyyy-MM-ddTHH:mmzzz", "yyyy-MM-ddTHH:mm:sszz" } // length = 22
        };
        #endregion

        // private fields
        private readonly JsonBuffer _buffer;
        private readonly JsonReaderSettings _jsonReaderSettings; // same value as in base class just declared as derived class
        private JsonReaderContext _context;
        private JsonToken _currentToken;
        private BsonValue _currentValue;
        private JsonToken _pushedToken;

        // constructors
        /// <summary>
        /// Initializes a new instance of the JsonReader class.
        /// </summary>
        /// <param name="json">The JSON string.</param>
        public JsonReader(string json)
            : this(json, JsonReaderSettings.Defaults)
        {
        }

        /// <summary>
        /// Initializes a new instance of the JsonReader class.
        /// </summary>
        /// <param name="json">The JSON string.</param>
        /// <param name="settings">The reader settings.</param>
        public JsonReader(string json, JsonReaderSettings settings)
            : this(new JsonBuffer(json), settings)
        {
        }

        /// <summary>
        /// Initializes a new instance of the JsonReader class.
        /// </summary>
        /// <param name="textReader">The TextReader.</param>
        public JsonReader(TextReader textReader)
            : this(textReader, JsonReaderSettings.Defaults)
        {
        }

        /// <summary>
        /// Initializes a new instance of the JsonReader class.
        /// </summary>
        /// <param name="textReader">The TextReader.</param>
        /// <param name="settings">The reader settings.</param>
        public JsonReader(TextReader textReader, JsonReaderSettings settings)
            : this(new JsonBuffer(textReader), settings)
        {
        }

        private JsonReader(JsonBuffer buffer, JsonReaderSettings settings)
            : base(settings)
        {
            _buffer = buffer;
            _jsonReaderSettings = settings; // already frozen by base class
            _context = new JsonReaderContext(null, ContextType.TopLevel);
        }

        // public methods
        /// <summary>
        /// Closes the reader.
        /// </summary>
        public override void Close()
        {
            // Close can be called on Disposed objects
            State = BsonReaderState.Closed;
        }

        /// <summary>
        /// Gets a bookmark to the reader's current position and state.
        /// </summary>
        /// <returns>A bookmark.</returns>
        public override BsonReaderBookmark GetBookmark()
        {
            return new JsonReaderBookmark(State, CurrentBsonType, CurrentName, _context, _currentToken, _currentValue, _pushedToken, _buffer.Position);
        }

        /// <summary>
        /// Determines whether this reader is at end of file.
        /// </summary>
        /// <returns>
        /// Whether this reader is at end of file.
        /// </returns>
        public override bool IsAtEndOfFile()
        {
            int c;
            while ((c = _buffer.Read()) != -1)
            {
                if (!char.IsWhiteSpace((char)c))
                {
                    _buffer.UnRead(c);
                    return false;
                }
            }
            return true;
        }

        /// <summary>
        /// Reads BSON binary data from the reader.
        /// </summary>
        /// <returns>A BsonBinaryData.</returns>
        public override BsonBinaryData ReadBinaryData()
        {
            if (Disposed) { ThrowObjectDisposedException(); }
            VerifyBsonType("ReadBinaryData", BsonType.Binary);
            State = GetNextState();
            return _currentValue.AsBsonBinaryData;
        }

        /// <summary>
        /// Reads a BSON boolean from the reader.
        /// </summary>
        /// <returns>A Boolean.</returns>
        public override bool ReadBoolean()
        {
            if (Disposed) { ThrowObjectDisposedException(); }
            VerifyBsonType("ReadBoolean", BsonType.Boolean);
            State = GetNextState();
            return _currentValue.AsBoolean;
        }

        /// <summary>
        /// Reads a BsonType from the reader.
        /// </summary>
        /// <returns>A BsonType.</returns>
        public override BsonType ReadBsonType()
        {
            if (Disposed) { ThrowObjectDisposedException(); }
            if (State == BsonReaderState.Initial || State == BsonReaderState.ScopeDocument)
            {
                if (State == BsonReaderState.Initial)
                {
                    _buffer.ResetBuffer();
                }

                // in JSON the top level value can be of any type so fall through
                State = BsonReaderState.Type;
            }
            if (State != BsonReaderState.Type)
            {
                ThrowInvalidState("ReadBsonType", BsonReaderState.Type);
            }

            if (_context.ContextType == ContextType.Document)
            {
                var nameToken = PopToken();
                switch (nameToken.Type)
                {
                    case JsonTokenType.String:
                    case JsonTokenType.UnquotedString:
                        CurrentName = nameToken.StringValue;
                        break;
                    case JsonTokenType.EndObject:
                        State = BsonReaderState.EndOfDocument;
                        return BsonType.EndOfDocument;
                    default:
                        var message = string.Format("JSON reader was expecting a name but found '{0}'.", nameToken.Lexeme);
                        throw new FormatException(message);
                }

                var colonToken = PopToken();
                if (colonToken.Type != JsonTokenType.Colon)
                {
                    var message = string.Format("JSON reader was expecting ':' but found '{0}'.", colonToken.Lexeme);
                    throw new FormatException(message);
                }
            }

            var valueToken = PopToken();
            if (_context.ContextType == ContextType.Array && valueToken.Type == JsonTokenType.EndArray)
            {
                State = BsonReaderState.EndOfArray;
                return BsonType.EndOfDocument;
            }

            var noValueFound = false;
            switch (valueToken.Type)
            {
                case JsonTokenType.BeginArray:
                    CurrentBsonType = BsonType.Array;
                    break;
                case JsonTokenType.BeginObject:
                    CurrentBsonType = ParseExtendedJson();
                    break;
                case JsonTokenType.DateTime:
                    CurrentBsonType = BsonType.DateTime;
                    _currentValue = valueToken.DateTimeValue;
                    break;
                case JsonTokenType.Double:
                    CurrentBsonType = BsonType.Double;
                    _currentValue = valueToken.DoubleValue;
                    break;
                case JsonTokenType.EndOfFile:
                    CurrentBsonType = BsonType.EndOfDocument;
                    break;
                case JsonTokenType.Int32:
                    CurrentBsonType = BsonType.Int32;
                    _currentValue = valueToken.Int32Value;
                    break;
                case JsonTokenType.Int64:
                    CurrentBsonType = BsonType.Int64;
                    _currentValue = valueToken.Int64Value;
                    break;
                case JsonTokenType.ObjectId:
                    CurrentBsonType = BsonType.ObjectId;
                    _currentValue = valueToken.ObjectIdValue;
                    break;
                case JsonTokenType.RegularExpression:
                    CurrentBsonType = BsonType.RegularExpression;
                    _currentValue = valueToken.RegularExpressionValue;
                    break;
                case JsonTokenType.String:
                    CurrentBsonType = BsonType.String;
                    _currentValue = valueToken.StringValue;
                    break;
                case JsonTokenType.UnquotedString:
                    switch (valueToken.Lexeme)
                    {
                        case "false":
                        case "true":
                            CurrentBsonType = BsonType.Boolean;
                            _currentValue = JsonConvert.ToBoolean(valueToken.Lexeme);
                            break;
                        case "Infinity":
                            CurrentBsonType = BsonType.Double;
                            _currentValue = double.PositiveInfinity;
                            break;
                        case "NaN":
                            CurrentBsonType = BsonType.Double;
                            _currentValue = double.NaN;
                            break;
                        case "null":
                            CurrentBsonType = BsonType.Null;
                            break;
                        case "undefined":
                            CurrentBsonType = BsonType.Undefined;
                            break;
                        case "BinData":
                            CurrentBsonType = BsonType.Binary;
                            _currentValue = ParseBinDataConstructor();
                            break;
                        case "Date":
                            CurrentBsonType = BsonType.String;
                            _currentValue = ParseDateTimeConstructor(false); // withNew = false
                            break;
                        case "HexData":
                            CurrentBsonType = BsonType.Binary;
                            _currentValue = ParseHexDataConstructor();
                            break;
                        case "ISODate":
                            CurrentBsonType = BsonType.DateTime;
                            _currentValue = ParseISODateTimeConstructor();
                            break;
                        case "MaxKey":
                            CurrentBsonType = BsonType.MaxKey;
                            _currentValue = BsonMaxKey.Value;
                            break;
                        case "MinKey":
                            CurrentBsonType = BsonType.MinKey;
                            _currentValue = BsonMinKey.Value;
                            break;
                        case "NumberDecimal":
                            CurrentBsonType = BsonType.Decimal128;
                            _currentValue = ParseNumberDecimalConstructor();
                            break;
                        case "Number":
                        case "NumberInt":
                            CurrentBsonType = BsonType.Int32;
                            _currentValue = ParseNumberConstructor();
                            break;
                        case "NumberLong":
                            CurrentBsonType = BsonType.Int64;
                            _currentValue = ParseNumberLongConstructor();
                            break;
                        case "ObjectId":
                            CurrentBsonType = BsonType.ObjectId;
                            _currentValue = ParseObjectIdConstructor();
                            break;
                        case "RegExp":
                            CurrentBsonType = BsonType.RegularExpression;
                            _currentValue = ParseRegularExpressionConstructor();
                            break;
                        case "Timestamp":
                            CurrentBsonType = BsonType.Timestamp;
                            _currentValue = ParseTimestampConstructor();
                            break;
                        case "UUID":
                        case "GUID":
                        case "CSUUID":
                        case "CSGUID":
                        case "JUUID":
                        case "JGUID":
                        case "PYUUID":
                        case "PYGUID":
                            CurrentBsonType = BsonType.Binary;
                            _currentValue = ParseUUIDConstructor(valueToken.Lexeme);
                            break;
                        case "new":
                            CurrentBsonType = ParseNew(out _currentValue);
                            break;
                        default:
                            noValueFound = true;
                            break;
                    }
                    break;
                default:
                    noValueFound = true;
                    break;
            }
            if (noValueFound)
            {
                var message = string.Format("JSON reader was expecting a value but found '{0}'.", valueToken.Lexeme);
                throw new FormatException(message);
            }
            _currentToken = valueToken;

            if (_context.ContextType == ContextType.Array || _context.ContextType == ContextType.Document)
            {
                var commaToken = PopToken();
                if (commaToken.Type != JsonTokenType.Comma)
                {
                    PushToken(commaToken);
                }
            }

            switch (_context.ContextType)
            {
                case ContextType.Document:
                case ContextType.ScopeDocument:
                default:
                    State = BsonReaderState.Name;
                    break;
                case ContextType.Array:
                case ContextType.JavaScriptWithScope:
                case ContextType.TopLevel:
                    State = BsonReaderState.Value;
                    break;
            }
            return CurrentBsonType;
        }

        /// <summary>
        /// Reads BSON binary data from the reader.
        /// </summary>
        /// <returns>A byte array.</returns>
        public override byte[] ReadBytes()
        {
#pragma warning disable 618
            if (Disposed) { ThrowObjectDisposedException(); }
            VerifyBsonType("ReadBinaryData", BsonType.Binary);
            State = GetNextState();
            var binaryData = _currentValue.AsBsonBinaryData;

            var subType = binaryData.SubType;
            if (subType != BsonBinarySubType.Binary && subType != BsonBinarySubType.OldBinary)
            {
                var message = string.Format("ReadBytes requires the binary sub type to be Binary, not {0}.", subType);
                throw new FormatException(message);
            }

            return binaryData.Bytes;
#pragma warning restore
        }

        /// <summary>
        /// Reads a BSON DateTime from the reader.
        /// </summary>
        /// <returns>The number of milliseconds since the Unix epoch.</returns>
        public override long ReadDateTime()
        {
            if (Disposed) { ThrowObjectDisposedException(); }
            VerifyBsonType("ReadDateTime", BsonType.DateTime);
            State = GetNextState();
            return _currentValue.AsBsonDateTime.MillisecondsSinceEpoch;
        }

        /// <inheritdoc />
        public override Decimal128 ReadDecimal128()
        {
            if (Disposed) { ThrowObjectDisposedException(); }
            VerifyBsonType(nameof(ReadDecimal128), BsonType.Decimal128);
            State = GetNextState();
            return _currentValue.AsDecimal128;
        }

        /// <summary>
        /// Reads a BSON Double from the reader.
        /// </summary>
        /// <returns>A Double.</returns>
        public override double ReadDouble()
        {
            if (Disposed) { ThrowObjectDisposedException(); }
            VerifyBsonType("ReadDouble", BsonType.Double);
            State = GetNextState();
            return _currentValue.AsDouble;
        }

        /// <summary>
        /// Reads the end of a BSON array from the reader.
        /// </summary>
        public override void ReadEndArray()
        {
            if (Disposed) { ThrowObjectDisposedException(); }
            if (_context.ContextType != ContextType.Array)
            {
                ThrowInvalidContextType("ReadEndArray", _context.ContextType, ContextType.Array);
            }
            if (State == BsonReaderState.Type)
            {
                ReadBsonType(); // will set state to EndOfArray if at end of array
            }
            if (State != BsonReaderState.EndOfArray)
            {
                ThrowInvalidState("ReadEndArray", BsonReaderState.EndOfArray);
            }

            _context = _context.PopContext();
            switch (_context.ContextType)
            {
                case ContextType.Array: State = BsonReaderState.Type; break;
                case ContextType.Document: State = BsonReaderState.Type; break;
                case ContextType.TopLevel: State = BsonReaderState.Initial; break;
                default: throw new BsonInternalException("Unexpected ContextType.");
            }

            if (_context.ContextType == ContextType.Array || _context.ContextType == ContextType.Document)
            {
                var commaToken = PopToken();
                if (commaToken.Type != JsonTokenType.Comma)
                {
                    PushToken(commaToken);
                }
            }
        }

        /// <summary>
        /// Reads the end of a BSON document from the reader.
        /// </summary>
        public override void ReadEndDocument()
        {
            if (Disposed) { ThrowObjectDisposedException(); }
            if (_context.ContextType != ContextType.Document && _context.ContextType != ContextType.ScopeDocument)
            {
                ThrowInvalidContextType("ReadEndDocument", _context.ContextType, ContextType.Document, ContextType.ScopeDocument);
            }
            if (State == BsonReaderState.Type)
            {
                ReadBsonType(); // will set state to EndOfDocument if at end of document
            }
            if (State != BsonReaderState.EndOfDocument)
            {
                ThrowInvalidState("ReadEndDocument", BsonReaderState.EndOfDocument);
            }

            _context = _context.PopContext();
            if (_context != null && _context.ContextType == ContextType.JavaScriptWithScope)
            {
                _context = _context.PopContext(); // JavaScriptWithScope
                VerifyToken("}"); // outermost closing bracket for JavaScriptWithScope
            }
            switch (_context.ContextType)
            {
                case ContextType.Array: State = BsonReaderState.Type; break;
                case ContextType.Document: State = BsonReaderState.Type; break;
                case ContextType.TopLevel: State = BsonReaderState.Initial; break;
                default: throw new BsonInternalException("Unexpected ContextType");
            }

            if (_context.ContextType == ContextType.Array || _context.ContextType == ContextType.Document)
            {
                var commaToken = PopToken();
                if (commaToken.Type != JsonTokenType.Comma)
                {
                    PushToken(commaToken);
                }
            }
        }

        /// <summary>
        /// Reads a BSON Int32 from the reader.
        /// </summary>
        /// <returns>An Int32.</returns>
        public override int ReadInt32()
        {
            if (Disposed) { ThrowObjectDisposedException(); }
            VerifyBsonType("ReadInt32", BsonType.Int32);
            State = GetNextState();
            return _currentValue.AsInt32;
        }

        /// <summary>
        /// Reads a BSON Int64 from the reader.
        /// </summary>
        /// <returns>An Int64.</returns>
        public override long ReadInt64()
        {
            if (Disposed) { ThrowObjectDisposedException(); }
            VerifyBsonType("ReadInt64", BsonType.Int64);
            State = GetNextState();
            return _currentValue.AsInt64;
        }

        /// <summary>
        /// Reads a BSON JavaScript from the reader.
        /// </summary>
        /// <returns>A string.</returns>
        public override string ReadJavaScript()
        {
            if (Disposed) { ThrowObjectDisposedException(); }
            VerifyBsonType("ReadJavaScript", BsonType.JavaScript);
            State = GetNextState();
            return _currentValue.AsString;
        }

        /// <summary>
        /// Reads a BSON JavaScript with scope from the reader (call ReadStartDocument next to read the scope).
        /// </summary>
        /// <returns>A string.</returns>
        public override string ReadJavaScriptWithScope()
        {
            if (Disposed) { ThrowObjectDisposedException(); }
            VerifyBsonType("ReadJavaScriptWithScope", BsonType.JavaScriptWithScope);
            _context = new JsonReaderContext(_context, ContextType.JavaScriptWithScope);
            State = BsonReaderState.ScopeDocument;
            return _currentValue.AsString;
        }

        /// <summary>
        /// Reads a BSON MaxKey from the reader.
        /// </summary>
        public override void ReadMaxKey()
        {
            if (Disposed) { ThrowObjectDisposedException(); }
            VerifyBsonType("ReadMaxKey", BsonType.MaxKey);
            State = GetNextState();
        }

        /// <summary>
        /// Reads a BSON MinKey from the reader.
        /// </summary>
        public override void ReadMinKey()
        {
            if (Disposed) { ThrowObjectDisposedException(); }
            VerifyBsonType("ReadMinKey", BsonType.MinKey);
            State = GetNextState();
        }

        /// <summary>
        /// Reads the name of an element from the reader.
        /// </summary>
        /// <param name="nameDecoder">The name decoder.</param>
        /// <returns>
        /// The name of the element.
        /// </returns>
        public override string ReadName(INameDecoder nameDecoder)
        {
            if (nameDecoder == null)
            {
                throw new ArgumentNullException("nameDecoder");
            }

            if (Disposed) { ThrowObjectDisposedException(); }
            if (State == BsonReaderState.Type)
            {
                ReadBsonType();
            }
            if (State != BsonReaderState.Name)
            {
                ThrowInvalidState("ReadName", BsonReaderState.Name);
            }

            nameDecoder.Inform(CurrentName);
            State = BsonReaderState.Value;
            return CurrentName;
        }

        /// <summary>
        /// Reads a BSON null from the reader.
        /// </summary>
        public override void ReadNull()
        {
            if (Disposed) { ThrowObjectDisposedException(); }
            VerifyBsonType("ReadNull", BsonType.Null);
            State = GetNextState();
        }

        /// <summary>
        /// Reads a BSON ObjectId from the reader.
        /// </summary>
        /// <returns>An ObjectId.</returns>
        public override ObjectId ReadObjectId()
        {
            if (Disposed) { ThrowObjectDisposedException(); }
            VerifyBsonType("ReadObjectId", BsonType.ObjectId);
            State = GetNextState();
            return _currentValue.AsObjectId;
        }

        /// <summary>
        /// Reads a BSON regular expression from the reader.
        /// </summary>
        /// <returns>A BsonRegularExpression.</returns>
        public override BsonRegularExpression ReadRegularExpression()
        {
            if (Disposed) { ThrowObjectDisposedException(); }
            VerifyBsonType("ReadRegularExpression", BsonType.RegularExpression);
            State = GetNextState();
            return _currentValue.AsBsonRegularExpression;
        }

        /// <summary>
        /// Reads the start of a BSON array.
        /// </summary>
        public override void ReadStartArray()
        {
            if (Disposed) { ThrowObjectDisposedException(); }
            VerifyBsonType("ReadStartArray", BsonType.Array);

            _context = new JsonReaderContext(_context, ContextType.Array);
            State = BsonReaderState.Type;
        }

        /// <summary>
        /// Reads the start of a BSON document.
        /// </summary>
        public override void ReadStartDocument()
        {
            if (Disposed) { ThrowObjectDisposedException(); }
            VerifyBsonType("ReadStartDocument", BsonType.Document);

            _context = new JsonReaderContext(_context, ContextType.Document);
            State = BsonReaderState.Type;
        }

        /// <summary>
        /// Reads a BSON string from the reader.
        /// </summary>
        /// <returns>A String.</returns>
        public override string ReadString()
        {
            if (Disposed) { ThrowObjectDisposedException(); }
            VerifyBsonType("ReadString", BsonType.String);
            State = GetNextState();
            return _currentValue.AsString;
        }

        /// <summary>
        /// Reads a BSON symbol from the reader.
        /// </summary>
        /// <returns>A string.</returns>
        public override string ReadSymbol()
        {
            if (Disposed) { ThrowObjectDisposedException(); }
            VerifyBsonType("ReadSymbol", BsonType.Symbol);
            State = GetNextState();
            return _currentValue.AsString;
        }

        /// <summary>
        /// Reads a BSON timestamp from the reader.
        /// </summary>
        /// <returns>The combined timestamp/increment.</returns>
        public override long ReadTimestamp()
        {
            if (Disposed) { ThrowObjectDisposedException(); }
            VerifyBsonType("ReadTimestamp", BsonType.Timestamp);
            State = GetNextState();
            var timestamp = _currentValue.AsBsonTimestamp;
            return timestamp.Value;
        }

        /// <summary>
        /// Reads a BSON undefined from the reader.
        /// </summary>
        public override void ReadUndefined()
        {
            if (Disposed) { ThrowObjectDisposedException(); }
            VerifyBsonType("ReadUndefined", BsonType.Undefined);
            State = GetNextState();
        }

        /// <summary>
        /// Returns the reader to previously bookmarked position and state.
        /// </summary>
        /// <param name="bookmark">The bookmark.</param>
        public override void ReturnToBookmark(BsonReaderBookmark bookmark)
        {
            if (Disposed) { ThrowObjectDisposedException(); }
            var jsonReaderBookmark = (JsonReaderBookmark)bookmark;
            State = jsonReaderBookmark.State;
            CurrentBsonType = jsonReaderBookmark.CurrentBsonType;
            CurrentName = jsonReaderBookmark.CurrentName;
            _context = jsonReaderBookmark.CloneContext();
            _currentToken = jsonReaderBookmark.CurrentToken;
            _currentValue = jsonReaderBookmark.CurrentValue;
            _pushedToken = jsonReaderBookmark.PushedToken;
            _buffer.Position = jsonReaderBookmark.Position;
        }

        /// <summary>
        /// Skips the name (reader must be positioned on a name).
        /// </summary>
        public override void SkipName()
        {
            if (Disposed) { ThrowObjectDisposedException(); }
            if (State != BsonReaderState.Name)
            {
                ThrowInvalidState("SkipName", BsonReaderState.Name);
            }

            State = BsonReaderState.Value;
        }

        /// <summary>
        /// Skips the value (reader must be positioned on a value).
        /// </summary>
        public override void SkipValue()
        {
            if (Disposed) { ThrowObjectDisposedException(); }
            if (State != BsonReaderState.Value)
            {
                ThrowInvalidState("SkipValue", BsonReaderState.Value);
            }

            switch (CurrentBsonType)
            {
                case BsonType.Array:
                    ReadStartArray();
                    while (ReadBsonType() != BsonType.EndOfDocument)
                    {
                        SkipValue();
                    }
                    ReadEndArray();
                    break;
                case BsonType.Binary:
                    ReadBinaryData();
                    break;
                case BsonType.Boolean:
                    ReadBoolean();
                    break;
                case BsonType.DateTime:
                    ReadDateTime();
                    break;
                case BsonType.Document:
                    ReadStartDocument();
                    while (ReadBsonType() != BsonType.EndOfDocument)
                    {
                        SkipName();
                        SkipValue();
                    }
                    ReadEndDocument();
                    break;
                case BsonType.Double:
                    ReadDouble();
                    break;
                case BsonType.Int32:
                    ReadInt32();
                    break;
                case BsonType.Int64:
                    ReadInt64();
                    break;
                case BsonType.JavaScript:
                    ReadJavaScript();
                    break;
                case BsonType.JavaScriptWithScope:
                    ReadJavaScriptWithScope();
                    ReadStartDocument();
                    while (ReadBsonType() != BsonType.EndOfDocument)
                    {
                        SkipName();
                        SkipValue();
                    }
                    ReadEndDocument();
                    break;
                case BsonType.MaxKey:
                    ReadMaxKey();
                    break;
                case BsonType.MinKey:
                    ReadMinKey();
                    break;
                case BsonType.Null:
                    ReadNull();
                    break;
                case BsonType.ObjectId:
                    ReadObjectId();
                    break;
                case BsonType.RegularExpression:
                    ReadRegularExpression();
                    break;
                case BsonType.String:
                    ReadString();
                    break;
                case BsonType.Symbol:
                    ReadSymbol();
                    break;
                case BsonType.Timestamp:
                    ReadTimestamp();
                    break;
                case BsonType.Undefined:
                    ReadUndefined();
                    break;
                default:
                    throw new BsonInternalException("Invalid BsonType.");
            }
        }

        // protected methods
        /// <summary>
        /// Disposes of any resources used by the reader.
        /// </summary>
        /// <param name="disposing">True if called from Dispose.</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                try
                {
                    Close();
                }
                catch { } // ignore exceptions
            }
            base.Dispose(disposing);
        }

        // private methods
        private string FormatInvalidTokenMessage(JsonToken token)
        {
            return string.Format("Invalid JSON token: '{0}'", token.Lexeme);
        }

        private string FormatJavaScriptDateTimeString(DateTime dateTime)
        {
            var utc = BsonUtils.ToUniversalTime(dateTime);
            var local = BsonUtils.ToLocalTime(utc);
            var offset = local - utc;
            var offsetSign = "+";
            if (offset < TimeSpan.Zero)
            {
                offset = -offset;
                offsetSign = "-";
            }
            var timeZone = TimeZoneInfo.Local;
            var timeZoneName = local.IsDaylightSavingTime() ? timeZone.DaylightName : timeZone.StandardName;
            var dateTimeString = string.Format(
                "{0} GMT{1}{2:D2}{3:D2} ({4})",
                local.ToString("ddd MMM dd yyyy HH:mm:ss"), offsetSign, offset.Hours, offset.Minutes, timeZoneName);
            return dateTimeString;
        }

        private BsonReaderState GetNextState()
        {
            switch (_context.ContextType)
            {
                case ContextType.Array:
                case ContextType.Document:
                    return BsonReaderState.Type;
                case ContextType.TopLevel:
                    return BsonReaderState.Initial;
                default:
                    throw new BsonInternalException("Unexpected ContextType.");
            }
        }

        private BsonValue ParseBinDataConstructor()
        {
            VerifyToken("(");
            var subTypeToken = PopToken();
            if (subTypeToken.Type != JsonTokenType.Int32)
            {
                var message = string.Format("JSON reader expected a binary subtype but found '{0}'.", subTypeToken.Lexeme);
                throw new FormatException(message);
            }
            VerifyToken(",");
            var bytesToken = PopToken();
            if (bytesToken.Type != JsonTokenType.String)
            {
                var message = string.Format("JSON reader expected a string but found '{0}'.", bytesToken.Lexeme);
                throw new FormatException(message);
            }
            VerifyToken(")");
            var bytes = Convert.FromBase64String(bytesToken.StringValue);
            var subType = (BsonBinarySubType)subTypeToken.Int32Value;
            GuidRepresentation guidRepresentation;
            switch (subType)
            {
                case BsonBinarySubType.UuidLegacy: guidRepresentation = _jsonReaderSettings.GuidRepresentation; break;
                case BsonBinarySubType.UuidStandard: guidRepresentation = GuidRepresentation.Standard; break;
                default: guidRepresentation = GuidRepresentation.Unspecified; break;
            }
            return new BsonBinaryData(bytes, subType, guidRepresentation);
        }

        private BsonValue ParseBinDataExtendedJson()
        {
            VerifyToken(":");

            var bytesToken = PopToken();
            if (bytesToken.Type != JsonTokenType.String)
            {
                var message = string.Format("JSON reader expected a string but found '{0}'.", bytesToken.Lexeme);
                throw new FormatException(message);
            }
            var bytes = Convert.FromBase64String(bytesToken.StringValue);

            VerifyToken(",");
            VerifyString("$type");
            VerifyToken(":");

            BsonBinarySubType subType;
            var subTypeToken = PopToken();
            if (subTypeToken.Type == JsonTokenType.String)
            {
                subType = (BsonBinarySubType)Convert.ToInt32(subTypeToken.StringValue, 16);
            }
            else if (subTypeToken.Type == JsonTokenType.Int32 || subTypeToken.Type == JsonTokenType.Int64)
            {
                subType = (BsonBinarySubType)subTypeToken.Int32Value;
            }
            else
            {
                var message = string.Format("JSON reader expected a string or integer but found '{0}'.", subTypeToken.Lexeme);
                throw new FormatException(message);
            }

            VerifyToken("}");

            GuidRepresentation guidRepresentation;
            switch (subType)
            {
                case BsonBinarySubType.UuidLegacy: guidRepresentation = _jsonReaderSettings.GuidRepresentation; break;
                case BsonBinarySubType.UuidStandard: guidRepresentation = GuidRepresentation.Standard; break;
                default: guidRepresentation = GuidRepresentation.Unspecified; break;
            }

            return new BsonBinaryData(bytes, subType, guidRepresentation);
        }

        private BsonValue ParseHexDataConstructor()
        {
            VerifyToken("(");
            var subTypeToken = PopToken();
            if (subTypeToken.Type != JsonTokenType.Int32)
            {
                var message = string.Format("JSON reader expected a binary subtype but found '{0}'.", subTypeToken.Lexeme);
                throw new FormatException(message);
            }
            VerifyToken(",");
            var bytesToken = PopToken();
            if (bytesToken.Type != JsonTokenType.String)
            {
                var message = string.Format("JSON reader expected a string but found '{0}'.", bytesToken.Lexeme);
                throw new FormatException(message);
            }
            VerifyToken(")");
            var bytes = BsonUtils.ParseHexString(bytesToken.StringValue);
            var subType = (BsonBinarySubType)subTypeToken.Int32Value;
            GuidRepresentation guidRepresentation;
            switch (subType)
            {
                case BsonBinarySubType.UuidLegacy: guidRepresentation = _jsonReaderSettings.GuidRepresentation; break;
                case BsonBinarySubType.UuidStandard: guidRepresentation = GuidRepresentation.Standard; break;
                default: guidRepresentation = GuidRepresentation.Unspecified; break;
            }
            return new BsonBinaryData(bytes, subType, guidRepresentation);
        }

        private BsonType ParseJavaScriptExtendedJson(out BsonValue value)
        {
            VerifyToken(":");
            var codeToken = PopToken();
            if (codeToken.Type != JsonTokenType.String)
            {
                var message = string.Format("JSON reader expected a string but found '{0}'.", codeToken.Lexeme);
                throw new FormatException(message);
            }
            var nextToken = PopToken();
            switch (nextToken.Type)
            {
                case JsonTokenType.Comma:
                    VerifyString("$scope");
                    VerifyToken(":");
                    State = BsonReaderState.Value;
                    value = codeToken.StringValue;
                    return BsonType.JavaScriptWithScope;
                case JsonTokenType.EndObject:
                    value = codeToken.StringValue;
                    return BsonType.JavaScript;
                default:
                    var message = string.Format("JSON reader expected ',' or '}}' but found '{0}'.", codeToken.Lexeme);
                    throw new FormatException(message);
            }
        }

        private BsonValue ParseISODateTimeConstructor()
        {
            VerifyToken("(");
            var valueToken = PopToken();
            if (valueToken.Type != JsonTokenType.String)
            {
                var message = string.Format("JSON reader expected a string but found '{0}'.", valueToken.Lexeme);
                throw new FormatException(message);
            }
            VerifyToken(")");
            var value = valueToken.StringValue;
            string[] formats = null;
            if (!value.Contains(".") && value.Length < __fixedLengthIso8601Formats.Length)
            {
                formats = __fixedLengthIso8601Formats[value.Length];
            }
            if (formats == null)
            {
                formats = __variableLengthIso8601Formats;
            }
            var utcDateTime = DateTime.ParseExact(value, formats, null, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
            return new BsonDateTime(utcDateTime);
        }

        private BsonValue ParseDateTimeExtendedJson()
        {
            VerifyToken(":");
            var valueToken = PopToken();

            long millisecondsSinceEpoch;
            if (valueToken.Type == JsonTokenType.Int32 || valueToken.Type == JsonTokenType.Int64)
            {
                millisecondsSinceEpoch = valueToken.Int64Value;
            }
            else if (valueToken.Type == JsonTokenType.String)
            {
                DateTime dateTime;
                var dateTimeStyles = DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal;
                if (!DateTime.TryParse(valueToken.StringValue, CultureInfo.InvariantCulture, dateTimeStyles, out dateTime))
                {
                    var message = string.Format("Invalid $date string: '{0}'.", valueToken.StringValue);
                    throw new FormatException(message);
                }
                millisecondsSinceEpoch = BsonUtils.ToMillisecondsSinceEpoch(dateTime);
            }
            else if (valueToken.Type == JsonTokenType.BeginObject)
            {
                VerifyToken("$numberLong");
                VerifyToken(":");
                var millisecondsSinceEpochToken = PopToken();
                if (millisecondsSinceEpochToken.Type == JsonTokenType.String)
                {
                    millisecondsSinceEpoch = long.Parse(millisecondsSinceEpochToken.StringValue, CultureInfo.InvariantCulture);
                }
                else if (millisecondsSinceEpochToken.Type == JsonTokenType.Int32 || millisecondsSinceEpochToken.Type == JsonTokenType.Int64)
                {
                    millisecondsSinceEpoch = millisecondsSinceEpochToken.Int64Value;
                }
                else
                {
                    var message = string.Format("JSON reader expected an integer or a string for {{ $date : {{ $numberLong : ... }} }} but found a '{0}'.", valueToken.Lexeme);
                    throw new FormatException(message);
                }
                VerifyToken("}");
            }
            else
            {
                var message = string.Format("JSON reader expected an ISO 8601 string, an integer, or {{ $numberLong : ... }} for $date but found a '{0}'.", valueToken.Lexeme);
                throw new FormatException(message);
            }

            VerifyToken("}");
            return new BsonDateTime(millisecondsSinceEpoch);
        }

        private BsonValue ParseDateTimeConstructor(bool withNew)
        {
            VerifyToken("(");

            // Date when used without "new" behaves differently (JavaScript has some weird parts)
            if (!withNew)
            {
                VerifyToken(")");
                var dateTimeString = FormatJavaScriptDateTimeString(DateTime.UtcNow);
                return new BsonString(dateTimeString);
            }

            var token = PopToken();
            if (token.Lexeme == ")")
            {
                return new BsonDateTime(DateTime.UtcNow);
            }
            else if (token.Type == JsonTokenType.String)
            {
                VerifyToken(")");
                var dateTimeString = token.StringValue;
                var dateTime = ParseJavaScriptDateTimeString(dateTimeString);
                return new BsonDateTime(dateTime);
            }
            else if (token.Type == JsonTokenType.Int32 || token.Type == JsonTokenType.Int64)
            {
                var args = new List<long>();
                while (true)
                {
                    args.Add(token.Int64Value);
                    token = PopToken();
                    if (token.Lexeme == ")")
                    {
                        break;
                    }
                    if (token.Lexeme != ",")
                    {
                        var message = string.Format("JSON reader expected a ',' or a ')' but found '{0}'.", token.Lexeme);
                        throw new FormatException(message);
                    }
                    token = PopToken();
                    if (token.Type != JsonTokenType.Int32 && token.Type != JsonTokenType.Int64)
                    {
                        var message = string.Format("JSON reader expected an integer but found '{0}'.", token.Lexeme);
                        throw new FormatException(message);
                    }
                }
                switch (args.Count)
                {
                    case 1:
                        return new BsonDateTime(args[0]);
                    case 3:
                    case 4:
                    case 5:
                    case 6:
                    case 7:
                        var year = (int)args[0];
                        var month = (int)args[1] + 1; // JavaScript starts at 0 but .NET starts at 1
                        var day = (int)args[2];
                        var hours = (args.Count >= 4) ? (int)args[3] : 0;
                        var minutes = (args.Count >= 5) ? (int)args[4] : 0;
                        var seconds = (args.Count >= 6) ? (int)args[5] : 0;
                        var milliseconds = (args.Count == 7) ? (int)args[6] : 0;
                        var dateTime = new DateTime(year, month, day, hours, minutes, seconds, milliseconds, DateTimeKind.Utc);
                        return new BsonDateTime(dateTime);
                    default:
                        var message = string.Format("JSON reader expected 1 or 3-7 integers but found {0}.", args.Count);
                        throw new FormatException(message);
                }
            }
            else
            {
                var message = string.Format("JSON reader expected an integer or a string but found '{0}'.", token.Lexeme);
                throw new FormatException(message);
            }
        }

        private BsonType ParseExtendedJson()
        {
            var nameToken = PopToken();
            if (nameToken.Type == JsonTokenType.String || nameToken.Type == JsonTokenType.UnquotedString)
            {
                switch (nameToken.StringValue)
                {
                    case "$binary": _currentValue = ParseBinDataExtendedJson(); return BsonType.Binary;
                    case "$code": return ParseJavaScriptExtendedJson(out _currentValue);
                    case "$date": _currentValue = ParseDateTimeExtendedJson(); return BsonType.DateTime;
                    case "$maxkey": case "$maxKey": _currentValue = ParseMaxKeyExtendedJson(); return BsonType.MaxKey;
                    case "$minkey": case "$minKey": _currentValue = ParseMinKeyExtendedJson(); return BsonType.MinKey;
                    case "$numberDecimal": _currentValue = ParseNumberDecimalExtendedJson(); return BsonType.Decimal128;
                    case "$numberInt": _currentValue = ParseNumberIntExtendedJson(); return BsonType.Int32;
                    case "$numberLong": _currentValue = ParseNumberLongExtendedJson(); return BsonType.Int64;
                    case "$oid": _currentValue = ParseObjectIdExtendedJson(); return BsonType.ObjectId;
                    case "$regex": _currentValue = ParseRegularExpressionExtendedJson(); return BsonType.RegularExpression;
                    case "$symbol": _currentValue = ParseSymbolExtendedJson(); return BsonType.Symbol;
                    case "$timestamp": _currentValue = ParseTimestampExtendedJson(); return BsonType.Timestamp;
                    case "$undefined": _currentValue = ParseUndefinedExtendedJson(); return BsonType.Undefined;
                }
            }
            PushToken(nameToken);
            return BsonType.Document;
        }

        private DateTime ParseJavaScriptDateTimeString(string dateTimeString)
        {
            // if DateTime.TryParse succeeds we're done, otherwise assume it's an RFC 822 formatted DateTime string
            DateTime dateTime;
            if (DateTime.TryParse(dateTimeString, out dateTime))
            {
                return dateTime;
            }
            else
            {
                var rfc822DateTimePattern =
                    @"^((?<dayOfWeek>(Mon|Tue|Wed|Thu|Fri|Sat|Sun)), )?" +
                    @"(?<day>\d{1,2}) +" +
                    @"(?<monthName>Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) " +
                    @"(?<year>\d{2}|\d{4}) " +
                    @"(?<hour>\d{1,2}):" +
                    @"(?<minutes>\d{1,2}):" +
                    @"(?<seconds>\d{1,2}(.\d{1,7})?) " +
                    @"(?<zone>UT|GMT|EST|EDT|CST|CDT|MST|MDT|PST|PDT|[A-Z]|([+-]\d{4}))$";
                var match = Regex.Match(dateTimeString, rfc822DateTimePattern);
                if (match.Success)
                {
                    var day = int.Parse(match.Groups["day"].Value);

                    int month;
                    var monthName = match.Groups["monthName"].Value;
                    switch (monthName)
                    {
                        case "Jan": month = 1; break;
                        case "Feb": month = 2; break;
                        case "Mar": month = 3; break;
                        case "Apr": month = 4; break;
                        case "May": month = 5; break;
                        case "Jun": month = 6; break;
                        case "Jul": month = 7; break;
                        case "Aug": month = 8; break;
                        case "Sep": month = 9; break;
                        case "Oct": month = 10; break;
                        case "Nov": month = 11; break;
                        case "Dec": month = 12; break;
                        default:
                            var message = string.Format("\"{0}\" is not a valid RFC 822 month name.", monthName);
                            throw new FormatException(message);
                    }

                    var yearString = match.Groups["year"].Value;
                    int year = int.Parse(yearString);
                    if (yearString.Length == 2)
                    {
                        year += 2000;
                        if (year - DateTime.UtcNow.Year >= 19) { year -= 100; }
                    }

                    var hour = int.Parse(match.Groups["hour"].Value);
                    var minutes = int.Parse(match.Groups["minutes"].Value);
                    var secondsString = match.Groups["seconds"].Value;
                    int seconds;
                    double milliseconds;
                    if (secondsString.IndexOf('.') != -1)
                    {
                        var timeSpan = TimeSpan.FromSeconds(double.Parse(secondsString));
                        seconds = timeSpan.Seconds;
                        milliseconds = timeSpan.TotalMilliseconds - seconds * 1000;
                    }
                    else
                    {
                        seconds = int.Parse(secondsString);
                        milliseconds = 0;
                    }

                    dateTime = new DateTime(year, month, day, hour, minutes, seconds, DateTimeKind.Utc).AddMilliseconds(milliseconds);

                    // check day of week before converting to UTC
                    var dayOfWeekString = match.Groups["dayOfWeek"].Value;
                    if (dayOfWeekString != "")
                    {
                        DayOfWeek dayOfWeek;
                        switch (dayOfWeekString)
                        {
                            case "Mon": dayOfWeek = DayOfWeek.Monday; break;
                            case "Tue": dayOfWeek = DayOfWeek.Tuesday; break;
                            case "Wed": dayOfWeek = DayOfWeek.Wednesday; break;
                            case "Thu": dayOfWeek = DayOfWeek.Thursday; break;
                            case "Fri": dayOfWeek = DayOfWeek.Friday; break;
                            case "Sat": dayOfWeek = DayOfWeek.Saturday; break;
                            case "Sun": dayOfWeek = DayOfWeek.Sunday; break;
                            default:
                                var message = string.Format("\"{0}\" is not a valid RFC 822 day name.", dayOfWeekString);
                                throw new FormatException(message);
                        }
                        if (dateTime.DayOfWeek != dayOfWeek)
                        {
                            var message = string.Format("\"{0}\" is not the right day of the week for {1}.", dayOfWeekString, dateTime.ToString("o"));
                            throw new FormatException(message);
                        }
                    }

                    TimeSpan offset;
                    var zone = match.Groups["zone"].Value;
                    switch (zone)
                    {
                        case "UT": case "GMT": case "Z": offset = TimeSpan.Zero; break;
                        case "EST": offset = TimeSpan.FromHours(-5); break;
                        case "EDT": offset = TimeSpan.FromHours(-4); break;
                        case "CST": offset = TimeSpan.FromHours(-6); break;
                        case "CDT": offset = TimeSpan.FromHours(-5); break;
                        case "MST": offset = TimeSpan.FromHours(-7); break;
                        case "MDT": offset = TimeSpan.FromHours(-6); break;
                        case "PST": offset = TimeSpan.FromHours(-8); break;
                        case "PDT": offset = TimeSpan.FromHours(-7); break;
                        case "A": offset = TimeSpan.FromHours(-1); break;
                        case "B": offset = TimeSpan.FromHours(-2); break;
                        case "C": offset = TimeSpan.FromHours(-3); break;
                        case "D": offset = TimeSpan.FromHours(-4); break;
                        case "E": offset = TimeSpan.FromHours(-5); break;
                        case "F": offset = TimeSpan.FromHours(-6); break;
                        case "G": offset = TimeSpan.FromHours(-7); break;
                        case "H": offset = TimeSpan.FromHours(-8); break;
                        case "I": offset = TimeSpan.FromHours(-9); break;
                        case "K": offset = TimeSpan.FromHours(-10); break;
                        case "L": offset = TimeSpan.FromHours(-11); break;
                        case "M": offset = TimeSpan.FromHours(-12); break;
                        case "N": offset = TimeSpan.FromHours(1); break;
                        case "O": offset = TimeSpan.FromHours(2); break;
                        case "P": offset = TimeSpan.FromHours(3); break;
                        case "Q": offset = TimeSpan.FromHours(4); break;
                        case "R": offset = TimeSpan.FromHours(5); break;
                        case "S": offset = TimeSpan.FromHours(6); break;
                        case "T": offset = TimeSpan.FromHours(7); break;
                        case "U": offset = TimeSpan.FromHours(8); break;
                        case "V": offset = TimeSpan.FromHours(9); break;
                        case "W": offset = TimeSpan.FromHours(10); break;
                        case "X": offset = TimeSpan.FromHours(11); break;
                        case "Y": offset = TimeSpan.FromHours(12); break;
                        default:
                            var offsetSign = zone.Substring(0);
                            var offsetHours = zone.Substring(1, 2);
                            var offsetMinutes = zone.Substring(3, 2);
                            offset = TimeSpan.FromHours(int.Parse(offsetHours)) + TimeSpan.FromMinutes(int.Parse(offsetMinutes));
                            if (offsetSign == "-")
                            {
                                offset = -offset;
                            }
                            break;
                    }

                    return dateTime.Add(-offset);
                }
                else
                {
                    var message = string.Format("The DateTime string \"{0}\" is not a valid DateTime string for either .NET or JavaScript.", dateTimeString);
                    throw new FormatException(message);
                }
            }
        }

        private BsonValue ParseMaxKeyExtendedJson()
        {
            VerifyToken(":");
            VerifyToken("1");
            VerifyToken("}");
            return BsonMaxKey.Value;
        }

        private BsonValue ParseMinKeyExtendedJson()
        {
            VerifyToken(":");
            VerifyToken("1");
            VerifyToken("}");
            return BsonMinKey.Value;
        }

        private BsonType ParseNew(out BsonValue value)
        {
            var typeToken = PopToken();
            if (typeToken.Type != JsonTokenType.UnquotedString)
            {
                var message = string.Format("JSON reader expected a type name but found '{0}'.", typeToken.Lexeme);
                throw new FormatException(message);
            }
            switch (typeToken.Lexeme)
            {
                case "BinData":
                    value = ParseBinDataConstructor();
                    return BsonType.Binary;
                case "Date":
                    value = ParseDateTimeConstructor(true); // withNew = true
                    return BsonType.DateTime;
                case "HexData":
                    value = ParseHexDataConstructor();
                    return BsonType.Binary;
                case "ISODate":
                    value = ParseISODateTimeConstructor();
                    return BsonType.DateTime;
                case "NumberDecimal":
                    value = ParseNumberDecimalConstructor();
                    return BsonType.Decimal128;
                case "NumberInt":
                    value = ParseNumberConstructor();
                    return BsonType.Int32;
                case "NumberLong":
                    value = ParseNumberLongConstructor();
                    return BsonType.Int64;
                case "ObjectId":
                    value = ParseObjectIdConstructor();
                    return BsonType.ObjectId;
                case "RegExp":
                    value = ParseRegularExpressionConstructor();
                    return BsonType.RegularExpression;
                case "Timestamp":
                    value = ParseTimestampConstructor();
                    return BsonType.Timestamp;
                case "UUID":
                case "GUID":
                case "CSUUID":
                case "CSGUID":
                case "JUUID":
                case "JGUID":
                case "PYUUID":
                case "PYGUID":
                    value = ParseUUIDConstructor(typeToken.Lexeme);
                    return BsonType.Binary;
                default:
                    var message = string.Format("JSON reader expected a type name but found '{0}'.", typeToken.Lexeme);
                    throw new FormatException(message);
            }
        }

        private BsonValue ParseNumberConstructor()
        {
            VerifyToken("(");
            var valueToken = PopToken();
            int value;
            if (valueToken.IsNumber)
            {
                value = valueToken.Int32Value;
            }
            else if (valueToken.Type == JsonTokenType.String)
            {
                value = int.Parse(valueToken.StringValue);
            }
            else
            {
                var message = string.Format("JSON reader expected an integer or a string but found '{0}'.", valueToken.Lexeme);
                throw new FormatException(message);
            }
            VerifyToken(")");
            return (BsonInt32)value;
        }

        private BsonValue ParseNumberDecimalConstructor()
        {
            VerifyToken("(");
            var valueToken = PopToken();
            Decimal128 value;
            if (valueToken.Type == JsonTokenType.String)
            {
                value = Decimal128.Parse(valueToken.StringValue);
            }
            else if (valueToken.Type == JsonTokenType.Int32 || valueToken.Type == JsonTokenType.Int64)
            {
                value = new Decimal128(valueToken.Int64Value);
            }
            else
            {
                var message = string.Format("JSON reader expected an integer or a string but found '{0}'.", valueToken.Lexeme);
                throw new FormatException(message);
            }
            VerifyToken(")");
            return (BsonDecimal128)value;
        }

        private BsonValue ParseNumberLongConstructor()
        {
            VerifyToken("(");
            var valueToken = PopToken();
            long value;
            if (valueToken.Type == JsonTokenType.Int32 || valueToken.Type == JsonTokenType.Int64)
            {
                value = valueToken.Int64Value;
            }
            else if (valueToken.Type == JsonTokenType.String)
            {
                value = long.Parse(valueToken.StringValue);
            }
            else
            {
                var message = string.Format("JSON reader expected an integer or a string but found '{0}'.", valueToken.Lexeme);
                throw new FormatException(message);
            }
            VerifyToken(")");
            return (BsonInt64)value;
        }

        private BsonValue ParseNumberDecimalExtendedJson()
        {
            VerifyToken(":");

            Decimal128 value;
            var valueToken = PopToken();
            if (valueToken.Type == JsonTokenType.String)
            {
                value = Decimal128.Parse(valueToken.StringValue);
            }
            else if (valueToken.Type == JsonTokenType.Int32 || valueToken.Type == JsonTokenType.Int64)
            {
                value = new Decimal128(valueToken.Int64Value);
            }
            else
            {
                var message = string.Format("JSON reader expected a string or an integer but found '{0}'.", valueToken.Lexeme);
                throw new FormatException(message);
            }

            VerifyToken("}");
            return (BsonDecimal128)value;
        }

        private BsonValue ParseNumberIntExtendedJson()
        {
            VerifyToken(":");

            int value;
            var valueToken = PopToken();
            if (valueToken.Type == JsonTokenType.Int32)
            {
                value = valueToken.Int32Value;
            }
            else if (valueToken.Type == JsonTokenType.String)
            {
                value = JsonConvert.ToInt32(valueToken.StringValue);
            }
            else
            {
                var message = string.Format("JSON reader expected an integer but found '{0}'.", valueToken.Lexeme);
                throw new FormatException(message);
            }

            VerifyToken("}");
            return (BsonInt32)value;
        }

        private BsonValue ParseNumberLongExtendedJson()
        {
            VerifyToken(":");

            long value;
            var valueToken = PopToken();
            if (valueToken.Type == JsonTokenType.String)
            {
                value = long.Parse(valueToken.StringValue, CultureInfo.InvariantCulture);
            }
            else if (valueToken.Type == JsonTokenType.Int32 || valueToken.Type == JsonTokenType.Int64)
            {
                value = valueToken.Int64Value;
            }
            else
            {
                var message = string.Format("JSON reader expected a string or an integer but found '{0}'.", valueToken.Lexeme);
                throw new FormatException(message);
            }

            VerifyToken("}");
            return (BsonInt64)value;
        }

        private BsonValue ParseObjectIdConstructor()
        {
            VerifyToken("(");
            var valueToken = PopToken();
            if (valueToken.Type != JsonTokenType.String)
            {
                var message = string.Format("JSON reader expected a string but found '{0}'.", valueToken.Lexeme);
                throw new FormatException(message);
            }
            VerifyToken(")");
            return new BsonObjectId(ObjectId.Parse(valueToken.StringValue));
        }

        private BsonValue ParseObjectIdExtendedJson()
        {
            VerifyToken(":");
            var valueToken = PopToken();
            if (valueToken.Type != JsonTokenType.String)
            {
                var message = string.Format("JSON reader expected a string but found '{0}'.", valueToken.Lexeme);
                throw new FormatException(message);
            }
            VerifyToken("}");
            return new BsonObjectId(ObjectId.Parse(valueToken.StringValue));
        }

        private BsonValue ParseRegularExpressionConstructor()
        {
            VerifyToken("(");
            var patternToken = PopToken();
            if (patternToken.Type != JsonTokenType.String)
            {
                var message = string.Format("JSON reader expected a string but found '{0}'.", patternToken.Lexeme);
                throw new FormatException(message);
            }
            var options = "";
            var commaToken = PopToken();
            if (commaToken.Lexeme == ",")
            {
                var optionsToken = PopToken();
                if (optionsToken.Type != JsonTokenType.String)
                {
                    var message = string.Format("JSON reader expected a string but found '{0}'.", optionsToken.Lexeme);
                    throw new FormatException(message);
                }
                options = optionsToken.StringValue;
            }
            else
            {
                PushToken(commaToken);
            }
            VerifyToken(")");
            return new BsonRegularExpression(patternToken.StringValue, options);
        }

        private BsonValue ParseRegularExpressionExtendedJson()
        {
            VerifyToken(":");
            var patternToken = PopToken();
            if (patternToken.Type != JsonTokenType.String)
            {
                var message = string.Format("JSON reader expected a string but found '{0}'.", patternToken.Lexeme);
                throw new FormatException(message);
            }
            var options = "";
            var commaToken = PopToken();
            if (commaToken.Lexeme == ",")
            {
                VerifyString("$options");
                VerifyToken(":");
                var optionsToken = PopToken();
                if (optionsToken.Type != JsonTokenType.String)
                {
                    var message = string.Format("JSON reader expected a string but found '{0}'.", optionsToken.Lexeme);
                    throw new FormatException(message);
                }
                options = optionsToken.StringValue;
            }
            else
            {
                PushToken(commaToken);
            }
            VerifyToken("}");
            return new BsonRegularExpression(patternToken.StringValue, options);
        }

        private BsonValue ParseSymbolExtendedJson()
        {
            VerifyToken(":");
            var nameToken = PopToken();
            if (nameToken.Type != JsonTokenType.String)
            {
                var message = string.Format("JSON reader expected a string but found '{0}'.", nameToken.Lexeme);
                throw new FormatException(message);
            }
            VerifyToken("}");
            return (BsonString)nameToken.StringValue; // will be converted to a BsonSymbol at a higher level
        }

        private BsonValue ParseTimestampConstructor()
        {
            VerifyToken("(");
            int secondsSinceEpoch;
            var secondsSinceEpochToken = PopToken();
            if (secondsSinceEpochToken.IsNumber)
            {
                secondsSinceEpoch = secondsSinceEpochToken.Int32Value;
            }
            else
            {
                var message = string.Format("JSON reader expected a number but found '{0}'.", secondsSinceEpochToken.Lexeme);
                throw new FormatException(message);
            }
            VerifyToken(",");
            int increment;
            var incrementToken = PopToken();
            if (secondsSinceEpochToken.IsNumber)
            {
                increment = incrementToken.Int32Value;
            }
            else
            {
                var message = string.Format("JSON reader expected a number but found '{0}'.", secondsSinceEpochToken.Lexeme);
                throw new FormatException(message);
            }
            VerifyToken(")");
            return new BsonTimestamp(secondsSinceEpoch, increment);
        }

        private BsonValue ParseTimestampExtendedJson()
        {
            VerifyToken(":");
            var nextToken = PopToken();
            if (nextToken.Type == JsonTokenType.BeginObject)
            {
                return ParseTimestampExtendedJsonNewRepresentation();
            }
            else
            {
                return ParseTimestampExtendedJsonOldRepresentation(nextToken);
            }
        }

        private BsonValue ParseTimestampExtendedJsonNewRepresentation()
        {
            int? timestamp = null;
            int? increment = null;

            while (true)
            {
                var token = PopToken();
                if (token.Type != JsonTokenType.String && token.Type != JsonTokenType.UnquotedString)
                {
                    throw new FormatException($"JSON reader expected an element name but found '{token.Lexeme}'.");
                }
                var name = token.StringValue;

                token = PopToken();
                if (token.Type != JsonTokenType.Colon)
                {
                    throw new FormatException($"JSON reader expected ':' but found '{name}'.");
                }

                token = PopToken();
                if (token.Type != JsonTokenType.Int32)
                {
                    throw new FormatException($"JSON reader expected an integer but found '{token.Lexeme}'.");
                }
                var value = token.Int32Value;

                switch (name)
                {
                    case "t":
                        timestamp = value;
                        break;

                    case "i":
                        increment = value;
                        break;

                    default:
                        throw new FormatException($"JSON reader expected 't' or 'i' element names but found '{name}'.");
                }

                token = PopToken();
                if (token.Type == JsonTokenType.Comma)
                {
                    continue;
                }
                else if (token.Type == JsonTokenType.EndObject)
                {
                    break;
                }
                else
                {
                    throw new FormatException($"JSON reader expected ',' or '}}'  but found '{token.Lexeme}'.");
                }
            }
            VerifyToken("}");

            if (!timestamp.HasValue)
            {
                throw new FormatException("JSON reader did not find the required \"t\" element.");
            }
            if (!increment.HasValue)
            {
                throw new FormatException("JSON reader did not find the required \"i\" element.");
            }

            return new BsonTimestamp(timestamp.Value, increment.Value);
        }

        private BsonValue ParseTimestampExtendedJsonOldRepresentation(JsonToken valueToken)
        {
            long value;
            if (valueToken.Type == JsonTokenType.Int32 || valueToken.Type == JsonTokenType.Int64)
            {
                value = valueToken.Int64Value;
            }
            else if (valueToken.Type == JsonTokenType.UnquotedString && valueToken.Lexeme == "NumberLong")
            {
                value = ParseNumberLongConstructor().AsInt64;
            }
            else
            {
                var message = string.Format("JSON reader expected an integer but found '{0}'.", valueToken.Lexeme);
                throw new FormatException(message);
            }
            VerifyToken("}");
            return new BsonTimestamp(value);
        }

        private BsonValue ParseUndefinedExtendedJson()
        {
            VerifyToken(":");
            VerifyToken("true");
            VerifyToken("}");
            return BsonMaxKey.Value;
        }

        private BsonValue ParseUUIDConstructor(string uuidConstructorName)
        {
            VerifyToken("(");
            var bytesToken = PopToken();
            if (bytesToken.Type != JsonTokenType.String)
            {
                var message = string.Format("JSON reader expected a string but found '{0}'.", bytesToken.Lexeme);
                throw new FormatException(message);
            }
            VerifyToken(")");
            var hexString = bytesToken.StringValue.Replace("{", "").Replace("}", "").Replace("-", "");
            var bytes = BsonUtils.ParseHexString(hexString);
            var guid = GuidConverter.FromBytes(bytes, GuidRepresentation.Standard);
            GuidRepresentation guidRepresentation;
            switch (uuidConstructorName)
            {
                case "CSUUID":
                case "CSGUID":
                    guidRepresentation = GuidRepresentation.CSharpLegacy;
                    break;
                case "JUUID":
                case "JGUID":
                    guidRepresentation = GuidRepresentation.JavaLegacy;
                    break;
                case "PYUUID":
                case "PYGUID":
                    guidRepresentation = GuidRepresentation.PythonLegacy;
                    break;
                case "UUID":
                case "GUID":
                    guidRepresentation = GuidRepresentation.Standard;
                    break;
                default:
                    throw new BsonInternalException("Unexpected uuidConstructorName");
            }
            bytes = GuidConverter.ToBytes(guid, guidRepresentation);
            var subType = (guidRepresentation == GuidRepresentation.Standard) ? BsonBinarySubType.UuidStandard : BsonBinarySubType.UuidLegacy;
            return new BsonBinaryData(bytes, subType, guidRepresentation);
        }

        private JsonToken PopToken()
        {
            if (_pushedToken != null)
            {
                var token = _pushedToken;
                _pushedToken = null;
                return token;
            }
            else
            {
                return JsonScanner.GetNextToken(_buffer);
            }
        }

        private void PushToken(JsonToken token)
        {
            if (_pushedToken == null)
            {
                _pushedToken = token;
            }
            else
            {
                throw new BsonInternalException("There is already a pending token.");
            }
        }

        private void VerifyString(string expectedString)
        {
            var token = PopToken();
            if ((token.Type != JsonTokenType.String && token.Type != JsonTokenType.UnquotedString) || token.StringValue != expectedString)
            {
                var message = string.Format("JSON reader expected '{0}' but found '{1}'.", expectedString, token.StringValue);
                throw new FormatException(message);
            }
        }

        private void VerifyToken(string expectedLexeme)
        {
            var token = PopToken();
            if (token.Lexeme != expectedLexeme)
            {
                var message = string.Format("JSON reader expected '{0}' but found '{1}'.", expectedLexeme, token.Lexeme);
                throw new FormatException(message);
            }
        }
    }
}
