using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Google.ProtocolBuffers.Descriptors;

namespace Google.ProtocolBuffers.Serialization
{
    /// <summary>
    /// JsonFormatWriter is a .NET 2.0 friendly json formatter for proto buffer messages.  For .NET 3.5
    /// you may also use the XmlFormatWriter with an XmlWriter created by the
    /// <see cref="System.Runtime.Serialization.Json.JsonReaderWriterFactory">JsonReaderWriterFactory</see>.
    /// </summary>
    public abstract class JsonFormatWriter : AbstractTextWriter
    {
        #region buffering implementations

        private class JsonTextWriter : JsonFormatWriter
        {
            private readonly char[] _buffer;
            private TextWriter _output;
            private int _bufferPos;

            public JsonTextWriter(TextWriter output)
            {
                _buffer = new char[4096];
                _bufferPos = 0;
                _output = output;
                _counter.Add(0);
            }

            /// <summary>
            /// Returns the output of TextWriter.ToString() where TextWriter is the ctor argument.
            /// </summary>
            public override string ToString()
            {
                Flush();

                if (_output != null)
                {
                    return _output.ToString();
                }

                return new String(_buffer, 0, _bufferPos);
            }

            protected override void WriteToOutput(char[] chars, int offset, int len)
            {
                if (_bufferPos + len >= _buffer.Length)
                {
                    if (_output == null)
                    {
                        _output = new StringWriter(new StringBuilder(_buffer.Length*2 + len));
                    }
                    Flush();
                }

                if (len < _buffer.Length)
                {
                    if (len <= 12)
                    {
                        int stop = offset + len;
                        for (int i = offset; i < stop; i++)
                        {
                            _buffer[_bufferPos++] = chars[i];
                        }
                    }
                    else
                    {
                        Buffer.BlockCopy(chars, offset << 1, _buffer, _bufferPos << 1, len << 1);
                        _bufferPos += len;
                    }
                }
                else
                {
                    _output.Write(chars, offset, len);
                }
            }

            protected override void WriteToOutput(char ch)
            {
                if (_bufferPos >= _buffer.Length)
                {
                    Flush();
                }
                _buffer[_bufferPos++] = ch;
            }

            public override void Flush()
            {
                if (_bufferPos > 0 && _output != null)
                {
                    _output.Write(_buffer, 0, _bufferPos);
                    _bufferPos = 0;
                }
                base.Flush();
            }
        }

        private class JsonStreamWriter : JsonFormatWriter
        {
#if SILVERLIGHT2 || COMPACT_FRAMEWORK_35
            static readonly Encoding Encoding = new UTF8Encoding(false);
#else
            private static readonly Encoding Encoding = Encoding.ASCII;
#endif
            private readonly byte[] _buffer;
            private Stream _output;
            private int _bufferPos;

            public JsonStreamWriter(Stream output)
            {
                _buffer = new byte[8192];
                _bufferPos = 0;
                _output = output;
                _counter.Add(0);
            }

            protected override void WriteToOutput(char[] chars, int offset, int len)
            {
                if (_bufferPos + len >= _buffer.Length)
                {
                    Flush();
                }

                if (len < _buffer.Length)
                {
                    if (len <= 12)
                    {
                        int stop = offset + len;
                        for (int i = offset; i < stop; i++)
                        {
                            _buffer[_bufferPos++] = (byte) chars[i];
                        }
                    }
                    else
                    {
                        _bufferPos += Encoding.GetBytes(chars, offset, len, _buffer, _bufferPos);
                    }
                }
                else
                {
                    byte[] temp = Encoding.GetBytes(chars, offset, len);
                    _output.Write(temp, 0, temp.Length);
                }
            }

            protected override void WriteToOutput(char ch)
            {
                if (_bufferPos >= _buffer.Length)
                {
                    Flush();
                }
                _buffer[_bufferPos++] = (byte) ch;
            }

            public override void Flush()
            {
                if (_bufferPos > 0 && _output != null)
                {
                    _output.Write(_buffer, 0, _bufferPos);
                    _bufferPos = 0;
                }
                base.Flush();
            }
        }

        #endregion

        //Tracks the writer depth and the array element count at that depth.
        private readonly List<int> _counter;
        //True if the top-level of the writer is an array as opposed to a single message.
        private bool _isArray;

        /// <summary>
        /// Constructs a JsonFormatWriter, use the ToString() member to extract the final Json on completion.
        /// </summary>
        protected JsonFormatWriter()
        {
            _counter = new List<int>();
        }

        /// <summary>
        /// Constructs a JsonFormatWriter, use ToString() to extract the final output
        /// </summary>
        public static JsonFormatWriter CreateInstance()
        {
            return new JsonTextWriter(null);
        }

        /// <summary>
        /// Constructs a JsonFormatWriter to output to the given text writer
        /// </summary>
        public static JsonFormatWriter CreateInstance(TextWriter output)
        {
            return new JsonTextWriter(output);
        }

        /// <summary>
        /// Constructs a JsonFormatWriter to output to the given stream
        /// </summary>
        public static JsonFormatWriter CreateInstance(Stream output)
        {
            return new JsonStreamWriter(output);
        }

        /// <summary> Write to the output stream </summary>
        protected void WriteToOutput(string format, params object[] args)
        {
            WriteToOutput(String.Format(format, args));
        }

        /// <summary> Write to the output stream </summary>
        protected void WriteToOutput(string text)
        {
            WriteToOutput(text.ToCharArray(), 0, text.Length);
        }

        /// <summary> Write to the output stream </summary>
        protected abstract void WriteToOutput(char ch);

        /// <summary> Write to the output stream </summary>
        protected abstract void WriteToOutput(char[] chars, int offset, int len);

        /// <summary> Sets the output formatting to use Environment.NewLine with 4-character indentions </summary>
        public JsonFormatWriter Formatted()
        {
            NewLine = Environment.NewLine;
            Indent = "    ";
            Whitespace = " ";
            return this;
        }

        /// <summary> Gets or sets the characters to use for the new-line, default = empty </summary>
        public string NewLine { get; set; }

        /// <summary> Gets or sets the text to use for indenting, default = empty </summary>
        public string Indent { get; set; }

        /// <summary> Gets or sets the whitespace to use to separate the text, default = empty </summary>
        public string Whitespace { get; set; }

        private void Seperator()
        {
            if (_counter.Count == 0)
            {
                throw new InvalidOperationException("Mismatched open/close in Json writer.");
            }

            int index = _counter.Count - 1;
            if (_counter[index] > 0)
            {
                WriteToOutput(',');
            }

            WriteLine(String.Empty);
            _counter[index] = _counter[index] + 1;
        }

        private void WriteLine(string content)
        {
            if (!String.IsNullOrEmpty(NewLine))
            {
                WriteToOutput(NewLine);
                for (int i = 1; i < _counter.Count; i++)
                {
                    WriteToOutput(Indent);
                }
            }
            else if (!String.IsNullOrEmpty(Whitespace))
            {
                WriteToOutput(Whitespace);
            }

            WriteToOutput(content);
        }

        private void WriteName(string field)
        {
            Seperator();
            if (!String.IsNullOrEmpty(field))
            {
                WriteToOutput('"');
                WriteToOutput(field);
                WriteToOutput('"');
                WriteToOutput(':');
                if (!String.IsNullOrEmpty(Whitespace))
                {
                    WriteToOutput(Whitespace);
                }
            }
        }

        private void EncodeText(string value)
        {
            char[] text = value.ToCharArray();
            int len = text.Length;
            int pos = 0;

            while (pos < len)
            {
                int next = pos;
                while (next < len && text[next] >= 32 && text[next] < 127 && text[next] != '\\' && text[next] != '/' &&
                       text[next] != '"')
                {
                    next++;
                }
                WriteToOutput(text, pos, next - pos);
                if (next < len)
                {
                    switch (text[next])
                    {
                        case '"':
                            WriteToOutput(@"\""");
                            break;
                        case '\\':
                            WriteToOutput(@"\\");
                            break;
                            //odd at best to escape '/', most Json implementations don't, but it is defined in the rfc-4627
                        case '/':
                            WriteToOutput(@"\/");
                            break;
                        case '\b':
                            WriteToOutput(@"\b");
                            break;
                        case '\f':
                            WriteToOutput(@"\f");
                            break;
                        case '\n':
                            WriteToOutput(@"\n");
                            break;
                        case '\r':
                            WriteToOutput(@"\r");
                            break;
                        case '\t':
                            WriteToOutput(@"\t");
                            break;
                        default:
                            WriteToOutput(@"\u{0:x4}", (int) text[next]);
                            break;
                    }
                    next++;
                }
                pos = next;
            }
        }

        /// <summary>
        /// Writes a String value
        /// </summary>
        protected override void WriteAsText(string field, string textValue, object typedValue)
        {
            WriteName(field);
            if (typedValue is bool || typedValue is int || typedValue is uint || typedValue is long ||
                typedValue is ulong || typedValue is double || typedValue is float)
            {
                WriteToOutput(textValue);
            }
            else
            {
                WriteToOutput('"');
                if (typedValue is string)
                {
                    EncodeText(textValue);
                }
                else
                {
                    WriteToOutput(textValue);
                }
                WriteToOutput('"');
            }
        }

        /// <summary>
        /// Writes a Double value
        /// </summary>
        protected override void Write(string field, double value)
        {
            if (double.IsNaN(value) || double.IsNegativeInfinity(value) || double.IsPositiveInfinity(value))
            {
                throw new InvalidOperationException("This format does not support NaN, Infinity, or -Infinity");
            }
            base.Write(field, value);
        }

        /// <summary>
        /// Writes a Single value
        /// </summary>
        protected override void Write(string field, float value)
        {
            if (float.IsNaN(value) || float.IsNegativeInfinity(value) || float.IsPositiveInfinity(value))
            {
                throw new InvalidOperationException("This format does not support NaN, Infinity, or -Infinity");
            }
            base.Write(field, value);
        }

        // Treat enum as string
        protected override void WriteEnum(string field, int number, string name)
        {
            Write(field, name);
        }

        /// <summary>
        /// Writes an array of field values
        /// </summary>
        protected override void WriteArray(FieldType type, string field, IEnumerable items)
        {
            IEnumerator enumerator = items.GetEnumerator();
            try
            {
                if (!enumerator.MoveNext())
                {
                    return;
                }
            }
            finally
            {
                if (enumerator is IDisposable)
                {
                    ((IDisposable) enumerator).Dispose();
                }
            }

            WriteName(field);
            WriteToOutput("[");
            _counter.Add(0);

            base.WriteArray(type, String.Empty, items);

            _counter.RemoveAt(_counter.Count - 1);
            WriteLine("]");
        }

        /// <summary>
        /// Writes a message
        /// </summary>
        protected override void WriteMessageOrGroup(string field, IMessageLite message)
        {
            WriteName(field);
            WriteMessage(message);
        }

        /// <summary>
        /// Writes the message to the the formatted stream.
        /// </summary>
        public override void WriteMessage(IMessageLite message)
        {
            WriteMessageStart();
            message.WriteTo(this);
            WriteMessageEnd();
        }

        /// <summary>
        /// Used to write the root-message preamble, in json this is the left-curly brace '{'.
        /// After this call you can call IMessageLite.MergeTo(...) and complete the message with
        /// a call to WriteMessageEnd().
        /// </summary>
        public override void WriteMessageStart()
        {
            if (_isArray)
            {
                Seperator();
            }
            WriteToOutput("{");
            _counter.Add(0);
        }

        /// <summary>
        /// Used to complete a root-message previously started with a call to WriteMessageStart()
        /// </summary>
        public override void WriteMessageEnd()
        {
            _counter.RemoveAt(_counter.Count - 1);
            WriteLine("}");
            Flush();
        }

        /// <summary>
        /// Used in streaming arrays of objects to the writer
        /// </summary>
        /// <example>
        /// <code>
        /// using(writer.StartArray())
        ///     foreach(IMessageLite m in messages)
        ///         writer.WriteMessage(m);
        /// </code>
        /// </example>
        public sealed class JsonArray : IDisposable
        {
            private JsonFormatWriter _writer;

            internal JsonArray(JsonFormatWriter writer)
            {
                _writer = writer;
                _writer.WriteToOutput("[");
                _writer._counter.Add(0);
            }

            /// <summary>
            /// Causes the end of the array character to be written.
            /// </summary>
            private void EndArray()
            {
                if (_writer != null)
                {
                    _writer._counter.RemoveAt(_writer._counter.Count - 1);
                    _writer.WriteLine("]");
                    _writer.Flush();
                }
                _writer = null;
            }

            void IDisposable.Dispose()
            {
                EndArray();
            }
        }

        /// <summary>
        /// Used to write an array of messages as the output rather than a single message.
        /// </summary>
        /// <example>
        /// <code>
        /// using(writer.StartArray())
        ///     foreach(IMessageLite m in messages)
        ///         writer.WriteMessage(m);
        /// </code>
        /// </example>
        public JsonArray StartArray()
        {
            if (_isArray)
            {
                Seperator();
            }
            _isArray = true;
            return new JsonArray(this);
        }
    }
}