using System;
using System.Collections.Generic;
using System.IO;
using System.Xml;

namespace Google.ProtocolBuffers.Serialization
{
    /// <summary>
    /// JsonFormatReader is used to parse Json into a message or an array of messages
    /// </summary>
    public class JsonFormatReader : AbstractTextReader
    {
        private readonly JsonCursor _input;
        private readonly Stack<int> _stopChar;

        private enum ReaderState
        {
            Start,
            BeginValue,
            EndValue,
            BeginObject,
            BeginArray
        }

        private string _current;
        private ReaderState _state;

        /// <summary>
        /// Constructs a JsonFormatReader to parse Json into a message, this method does not use text encoding, all bytes MUST
        /// represent ASCII character values.
        /// </summary>
        public static JsonFormatReader CreateInstance(Stream stream)
        {
            return new JsonFormatReader(JsonCursor.CreateInstance(stream));
        }

        /// <summary>
        /// Constructs a JsonFormatReader to parse Json into a message, this method does not use text encoding, all bytes MUST
        /// represent ASCII character values.
        /// </summary>
        public static JsonFormatReader CreateInstance(byte[] bytes)
        {
            return new JsonFormatReader(JsonCursor.CreateInstance(bytes));
        }

        /// <summary>
        /// Constructs a JsonFormatReader to parse Json into a message
        /// </summary>
        public static JsonFormatReader CreateInstance(string jsonText)
        {
            return new JsonFormatReader(JsonCursor.CreateInstance(jsonText));
        }

        /// <summary>
        /// Constructs a JsonFormatReader to parse Json into a message
        /// </summary>
        public static JsonFormatReader CreateInstance(TextReader input)
        {
            return new JsonFormatReader(JsonCursor.CreateInstance(input));
        }

        /// <summary>
        /// Constructs a JsonFormatReader to parse Json into a message
        /// </summary>
        internal JsonFormatReader(JsonCursor input)
        {
            _input = input;
            _stopChar = new Stack<int>();
            _stopChar.Push(-1);
            _state = ReaderState.Start;
        }

        /// <summary>
        /// Constructs a JsonFormatReader to parse Json into a message
        /// </summary>
        protected JsonFormatReader(TextReader input)
            : this(JsonCursor.CreateInstance(input))
        {
        }

        /// <summary>
        /// Returns true if the reader is currently on an array element
        /// </summary>
        public bool IsArrayMessage
        {
            get { return _input.NextChar == '['; }
        }

        /// <summary>
        /// Returns an enumerator that is used to cursor over an array of messages
        /// </summary>
        /// <remarks>
        /// This is generally used when receiving an array of messages rather than a single root message
        /// </remarks>
        public IEnumerable<JsonFormatReader> EnumerateArray()
        {
            foreach (string ignored in ForeachArrayItem(_current))
            {
                yield return this;
            }
        }

        /// <summary>
        /// Reads the root-message preamble specific to this formatter
        /// </summary>
        public override AbstractReader ReadStartMessage()
        {
            _input.Consume('{');
            _stopChar.Push('}');

            _state = ReaderState.BeginObject;
            return this;
        }

        /// <summary>
        /// Reads the root-message close specific to this formatter
        /// </summary>
        public override void ReadEndMessage()
        {
            _input.Consume((char)_stopChar.Pop());
            _state = ReaderState.EndValue;
        }

        /// <summary>
        /// Merges the contents of stream into the provided message builder
        /// </summary>
        public override TBuilder Merge<TBuilder>(TBuilder builder, ExtensionRegistry registry)
        {
            AbstractReader rdr = ReadStartMessage();
            builder.WeakMergeFrom(rdr, registry);
            rdr.ReadEndMessage();
            return builder;
        }

        /// <summary>
        /// Causes the reader to skip past this field
        /// </summary>
        protected override void Skip()
        {
            object temp;
            _input.ReadVariant(out temp);
            _state = ReaderState.EndValue;
        }

        /// <summary>
        /// Peeks at the next field in the input stream and returns what information is available.
        /// </summary>
        /// <remarks>
        /// This may be called multiple times without actually reading the field.  Only after the field
        /// is either read, or skipped, should PeekNext return a different value.
        /// </remarks>
        protected override bool PeekNext(out string field)
        {
            field = _current;
            if (_state == ReaderState.BeginValue)
            {
                return true;
            }

            int next = _input.NextChar;
            if (next == _stopChar.Peek())
            {
                return false;
            }

            _input.Assert(next != -1, "Unexpected end of file.");

            //not sure about this yet, it will allow {, "a":true }
            if (_state == ReaderState.EndValue && !_input.TryConsume(','))
            {
                return false;
            }

            field = _current = _input.ReadString();
            _input.Consume(':');
            _state = ReaderState.BeginValue;
            return true;
        }

        /// <summary>
        /// Returns true if it was able to read a String from the input
        /// </summary>
        protected override bool ReadAsText(ref string value, Type typeInfo)
        {
            object temp;
            JsonCursor.JsType type = _input.ReadVariant(out temp);
            _state = ReaderState.EndValue;

            _input.Assert(type != JsonCursor.JsType.Array && type != JsonCursor.JsType.Object,
                          "Encountered {0} while expecting {1}", type, typeInfo);
            if (type == JsonCursor.JsType.Null)
            {
                return false;
            }
            if (type == JsonCursor.JsType.True)
            {
                value = "1";
            }
            else if (type == JsonCursor.JsType.False)
            {
                value = "0";
            }
            else
            {
                value = temp as string;
            }

            //exponent representation of integer number:
            if (value != null && type == JsonCursor.JsType.Number &&
                (typeInfo != typeof (double) && typeInfo != typeof (float)) &&
                value.IndexOf("e", StringComparison.OrdinalIgnoreCase) > 0)
            {
                value = XmlConvert.ToString((long) Math.Round(XmlConvert.ToDouble(value), 0));
            }
            return value != null;
        }

        /// <summary>
        /// Returns true if it was able to read a ByteString from the input
        /// </summary>
        protected override bool Read(ref ByteString value)
        {
            string bytes = null;
            if (Read(ref bytes))
            {
                value = ByteString.FromBase64(bytes);
                return true;
            }
            return false;
        }

        /// <summary>
        /// Cursors through the array elements and stops at the end of the array
        /// </summary>
        protected override IEnumerable<string> ForeachArrayItem(string field)
        {
            _input.Consume('[');
            _stopChar.Push(']');
            _state = ReaderState.BeginArray;
            while (_input.NextChar != ']')
            {
                _current = field;
                yield return field;
                if (!_input.TryConsume(','))
                {
                    break;
                }
            }
            _input.Consume((char) _stopChar.Pop());
            _state = ReaderState.EndValue;
        }

        /// <summary>
        /// Merges the input stream into the provided IBuilderLite 
        /// </summary>
        protected override bool ReadMessage(IBuilderLite builder, ExtensionRegistry registry)
        {
            Merge(builder, registry);
            return true;
        }
    }
}