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

namespace Google.ProtocolBuffers.Serialization
{
    /// <summary>
    /// Parses a proto buffer from an XML document or fragment.  .NET 3.5 users may also
    /// use this class to process Json by setting the options to support Json and providing
    /// an XmlReader obtained from <see cref="System.Runtime.Serialization.Json.JsonReaderWriterFactory"/>.
    /// </summary>
    public class XmlFormatReader : AbstractTextReader
    {
        public const string DefaultRootElementName = XmlFormatWriter.DefaultRootElementName;
        private readonly XmlReader _input;
        // Tracks the message element for each nested message read
        private readonly Stack<ElementStackEntry> _elements;
        // The default element name for ReadMessageStart
        private string _rootElementName;

        private struct ElementStackEntry
        {
            public readonly string LocalName;
            public readonly int Depth;
            public readonly bool IsEmpty;

            public ElementStackEntry(string localName, int depth, bool isEmpty) : this()
            {
                LocalName = localName;
                IsEmpty = isEmpty;
                Depth = depth;
            }
        }

        private static XmlReaderSettings DefaultSettings
        {
            get
            {
                return new XmlReaderSettings()
                           {CheckCharacters = false, IgnoreComments = true, IgnoreProcessingInstructions = true};
            }
        }

        /// <summary>
        /// Constructs the XmlFormatReader using the stream provided as the xml
        /// </summary>
        public static XmlFormatReader CreateInstance(byte[] input)
        {
            return new XmlFormatReader(XmlReader.Create(new MemoryStream(input, false), DefaultSettings));
        }

        /// <summary>
        /// Constructs the XmlFormatReader using the stream provided as the xml
        /// </summary>
        public static XmlFormatReader CreateInstance(Stream input)
        {
            return new XmlFormatReader(XmlReader.Create(input, DefaultSettings));
        }

        /// <summary>
        /// Constructs the XmlFormatReader using the string provided as the xml to be read
        /// </summary>
        public static XmlFormatReader CreateInstance(String input)
        {
            return new XmlFormatReader(XmlReader.Create(new StringReader(input), DefaultSettings));
        }

        /// <summary>
        /// Constructs the XmlFormatReader using the xml in the TextReader
        /// </summary>
        public static XmlFormatReader CreateInstance(TextReader input)
        {
            return new XmlFormatReader(XmlReader.Create(input, DefaultSettings));
        }

        /// <summary>
        /// Constructs the XmlFormatReader with the XmlReader
        /// </summary>
        public static XmlFormatReader CreateInstance(XmlReader input)
        {
            return new XmlFormatReader(input);
        }

        /// <summary>
        /// Constructs the XmlFormatReader with the XmlReader and options
        /// </summary>
        protected XmlFormatReader(XmlReader input)
        {
            _input = input;
            _rootElementName = DefaultRootElementName;
            _elements = new Stack<ElementStackEntry>();
            Options = XmlReaderOptions.None;
        }

        /// <summary>
        /// Gets or sets the options to use when reading the xml
        /// </summary>
        public XmlReaderOptions Options { get; set; }

        /// <summary>
        /// Sets the options to use while generating the XML
        /// </summary>
        public XmlFormatReader SetOptions(XmlReaderOptions options)
        {
            Options = options;
            return this;
        }

        /// <summary>
        /// Gets or sets the default element name to use when using the Merge&lt;TBuilder>()
        /// </summary>
        public string RootElementName
        {
            get { return _rootElementName; }
            set
            {
                ThrowHelper.ThrowIfNull(value, "RootElementName");
                _rootElementName = value;
            }
        }

        [DebuggerNonUserCode]
        private static void Assert(bool cond)
        {
            if (!cond)
            {
                throw new FormatException();
            }
        }

        /// <summary>
        /// Reads the root-message preamble specific to this formatter
        /// </summary>
        public override void ReadMessageStart()
        {
            ReadMessageStart(_rootElementName);
        }

        /// <summary>
        /// Reads the root-message preamble specific to this formatter
        /// </summary>
        public void ReadMessageStart(string element)
        {
            while (!_input.IsStartElement() && _input.Read())
            {
                continue;
            }
            Assert(_input.IsStartElement() && _input.LocalName == element);
            _elements.Push(new ElementStackEntry(element, _input.Depth, _input.IsEmptyElement));
            _input.Read();
        }

        /// <summary>
        /// Reads the root-message close specific to this formatter, MUST be called
        /// on the reader obtained from ReadMessageStart(string element).
        /// </summary>
        public override void ReadMessageEnd()
        {
            Assert(_elements.Count > 0);

            ElementStackEntry stop = _elements.Peek();
            while (_input.NodeType != XmlNodeType.EndElement && _input.NodeType != XmlNodeType.Element
                   && _input.Depth > stop.Depth && _input.Read())
            {
                continue;
            }

            if (!stop.IsEmpty)
            {
                Assert(_input.NodeType == XmlNodeType.EndElement
                       && _input.LocalName == stop.LocalName
                       && _input.Depth == stop.Depth);

                _input.Read();
            }
            _elements.Pop();
        }

        /// <summary>
        /// Merge the provided builder as an element named <see cref="RootElementName"/> in the current context
        /// </summary>
        public override TBuilder Merge<TBuilder>(TBuilder builder, ExtensionRegistry registry)
        {
            return Merge(_rootElementName, builder, registry);
        }

        /// <summary>
        /// Merge the provided builder as an element of the current context
        /// </summary>
        public TBuilder Merge<TBuilder>(string element, TBuilder builder) where TBuilder : IBuilderLite
        {
            return Merge(element, builder, ExtensionRegistry.Empty);
        }

        /// <summary>
        /// Merge the provided builder as an element of the current context
        /// </summary>
        public TBuilder Merge<TBuilder>(string element, TBuilder builder, ExtensionRegistry registry)
            where TBuilder : IBuilderLite
        {
            ReadMessageStart(element);
            builder.WeakMergeFrom(this, registry);
            ReadMessageEnd();
            return builder;
        }

        /// <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)
        {
            ElementStackEntry stopNode;
            if (_elements.Count == 0)
            {
                stopNode = new ElementStackEntry(null, _input.Depth - 1, false);
            }
            else
            {
                stopNode = _elements.Peek();
            }

            while (!_input.IsStartElement() && _input.Depth > stopNode.Depth && _input.Read())
            {
                continue;
            }

            if (_input.IsStartElement())
            {
                field = _input.LocalName;
                return true;
            }
            field = null;
            return false;
        }

        /// <summary>
        /// Causes the reader to skip past this field
        /// </summary>
        protected override void Skip()
        {
            if (_input.IsStartElement())
            {
                if (!_input.IsEmptyElement)
                {
                    int depth = _input.Depth;
                    while (_input.Depth >= depth && _input.NodeType != XmlNodeType.EndElement)
                    {
                        Assert(_input.Read());
                    }
                }
                _input.Read();
            }
        }

        /// <summary>
        /// returns true if it was able to read a single value into the value reference.  The value
        /// stored may be of type System.String, System.Int32, or an IEnumLite from the IEnumLiteMap.
        /// </summary>
        protected override bool ReadEnum(ref object value)
        {
            int number;
            string temp;
            if (null != (temp = _input.GetAttribute("value")) && int.TryParse(temp, out number))
            {
                Skip();
                value = number;
                return true;
            }
            return base.ReadEnum(ref value);
        }

        /// <summary>
        /// Returns true if it was able to read a String from the input
        /// </summary>
        protected override bool ReadAsText(ref string value, Type type)
        {
            Assert(_input.NodeType == XmlNodeType.Element);
            value = _input.ReadElementContentAsString();

            return true;
        }

        /// <summary>
        /// Merges the input stream into the provided IBuilderLite 
        /// </summary>
        protected override bool ReadMessage(IBuilderLite builder, ExtensionRegistry registry)
        {
            Assert(_input.IsStartElement());
            ReadMessageStart(_input.LocalName);
            builder.WeakMergeFrom(this, registry);
            ReadMessageEnd();
            return true;
        }

        private IEnumerable<string> NonNestedArrayItems(string field)
        {
            return base.ForeachArrayItem(field);
        }

        /// <summary>
        /// Cursors through the array elements and stops at the end of the array
        /// </summary>
        protected override IEnumerable<string> ForeachArrayItem(string field)
        {
            bool isNested = (Options & XmlReaderOptions.ReadNestedArrays) != 0;

            if (!isNested)
            {
                foreach (string item in NonNestedArrayItems(field))
                {
                    yield return item;
                }
            }
            else
            {
                string found;
                ReadMessageStart(field);
                if (PeekNext(out found) && found == "item")
                {
                    foreach (string item in NonNestedArrayItems("item"))
                    {
                        yield return item;
                    }
                }
                ReadMessageEnd();
            }
        }
    }
}