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

//Disable CS3011: only CLS-compliant members can be abstract
#pragma warning disable 3011

namespace Google.ProtocolBuffers.Serialization
{
    /// <summary>
    /// Provides a base-class that provides some basic functionality for handling type dispatching
    /// </summary>
    public abstract class AbstractReader : ICodedInputStream
    {
        private const int DefaultMaxDepth = 64;
        private int _depth;
        
        /// <summary> Constructs a new reader </summary>
        protected AbstractReader() { MaxDepth = DefaultMaxDepth; }

        /// <summary> Gets or sets the maximum recursion depth allowed </summary>
        public int MaxDepth { get; set; }

        /// <summary>
        /// Merges the contents of stream into the provided message builder
        /// </summary>
        public TBuilder Merge<TBuilder>(TBuilder builder) where TBuilder : IBuilderLite
        {
            return Merge(builder, ExtensionRegistry.Empty);
        }

        /// <summary>
        /// Merges the contents of stream into the provided message builder
        /// </summary>
        public abstract TBuilder Merge<TBuilder>(TBuilder builder, ExtensionRegistry registry)
            where TBuilder : IBuilderLite;

        /// <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 abstract bool PeekNext(out string field);

        /// <summary>
        /// Causes the reader to skip past this field
        /// </summary>
        protected abstract void Skip();

        /// <summary>
        /// Returns true if it was able to read a Boolean from the input
        /// </summary>
        protected abstract bool Read(ref bool value);

        /// <summary>
        /// Returns true if it was able to read a Int32 from the input
        /// </summary>
        protected abstract bool Read(ref int value);

        /// <summary>
        /// Returns true if it was able to read a UInt32 from the input
        /// </summary>
        [CLSCompliant(false)]
        protected abstract bool Read(ref uint value);

        /// <summary>
        /// Returns true if it was able to read a Int64 from the input
        /// </summary>
        protected abstract bool Read(ref long value);

        /// <summary>
        /// Returns true if it was able to read a UInt64 from the input
        /// </summary>
        [CLSCompliant(false)]
        protected abstract bool Read(ref ulong value);

        /// <summary>
        /// Returns true if it was able to read a Single from the input
        /// </summary>
        protected abstract bool Read(ref float value);

        /// <summary>
        /// Returns true if it was able to read a Double from the input
        /// </summary>
        protected abstract bool Read(ref double value);

        /// <summary>
        /// Returns true if it was able to read a String from the input
        /// </summary>
        protected abstract bool Read(ref string value);

        /// <summary>
        /// Returns true if it was able to read a ByteString from the input
        /// </summary>
        protected abstract bool Read(ref ByteString value);

        /// <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 abstract bool ReadEnum(ref object value);

        /// <summary>
        /// Merges the input stream into the provided IBuilderLite 
        /// </summary>
        protected abstract bool ReadMessage(IBuilderLite builder, ExtensionRegistry registry);
        
        /// <summary>
        /// Reads the root-message preamble specific to this formatter
        /// </summary>
        public abstract void ReadMessageStart();

        /// <summary>
        /// Reads the root-message close specific to this formatter
        /// </summary>
        public abstract void ReadMessageEnd();

        /// <summary>
        /// Merges the input stream into the provided IBuilderLite 
        /// </summary>
        public virtual bool ReadGroup(IBuilderLite value, ExtensionRegistry registry)
        {
            return ReadMessage(value, registry);
        }

        /// <summary>
        /// Cursors through the array elements and stops at the end of the array
        /// </summary>
        protected virtual IEnumerable<string> ForeachArrayItem(string field)
        {
            string next = field;
            while (true)
            {
                yield return next;

                if (!PeekNext(out next) || next != field)
                {
                    break;
                }
            }
        }

        /// <summary>
        /// Reads an array of T messages
        /// </summary>
        public virtual bool ReadMessageArray<T>(string field, ICollection<T> items, IMessageLite messageType,
                                                ExtensionRegistry registry)
        {
            bool success = false;
            foreach (string next in ForeachArrayItem(field))
            {
                IBuilderLite builder = messageType.WeakCreateBuilderForType();
                if (ReadMessage(builder, registry))
                {
                    items.Add((T) builder.WeakBuild());
                    success |= true;
                }
            }
            return success;
        }

        /// <summary>
        /// Reads an array of T messages as a proto-buffer group
        /// </summary>
        public virtual bool ReadGroupArray<T>(string field, ICollection<T> items, IMessageLite messageType,
                                              ExtensionRegistry registry)
        {
            bool success = false;
            foreach (string next in ForeachArrayItem(field))
            {
                IBuilderLite builder = messageType.WeakCreateBuilderForType();
                if (ReadGroup(builder, registry))
                {
                    items.Add((T) builder.WeakBuild());
                    success |= true;
                }
            }
            return success;
        }

        /// <summary>
        /// Reads an array of System.Enum type T and adds them to the collection
        /// </summary>
        public virtual bool ReadEnumArray(string field, ICollection<object> items)
        {
            bool success = false;
            foreach (string next in ForeachArrayItem(field))
            {
                object temp = null;
                if (ReadEnum(ref temp))
                {
                    items.Add(temp);
                    success |= true;
                }
            }
            return success;
        }

        /// <summary>
        /// Reads an array of T, where T is a primitive type defined by FieldType
        /// </summary>
        public virtual bool ReadArray<T>(FieldType type, string field, ICollection<T> items)
        {
            bool success = false;
            foreach (string next in ForeachArrayItem(field))
            {
                object temp = null;
                if (ReadField(type, ref temp))
                {
                    items.Add((T) temp);
                    success |= true;
                }
            }
            return success;
        }

        /// <summary>
        /// returns true if it was able to read a single primitive value of FieldType into the value reference
        /// </summary>
        public virtual bool ReadField(FieldType type, ref object value)
        {
            switch (type)
            {
                case FieldType.Bool:
                    {
                        bool temp = false;
                        if (Read(ref temp))
                        {
                            value = temp;
                        }
                        else
                        {
                            return false;
                        }
                        break;
                    }
                case FieldType.Int64:
                case FieldType.SInt64:
                case FieldType.SFixed64:
                    {
                        long temp = 0;
                        if (Read(ref temp))
                        {
                            value = temp;
                        }
                        else
                        {
                            return false;
                        }
                        break;
                    }
                case FieldType.UInt64:
                case FieldType.Fixed64:
                    {
                        ulong temp = 0;
                        if (Read(ref temp))
                        {
                            value = temp;
                        }
                        else
                        {
                            return false;
                        }
                        break;
                    }
                case FieldType.Int32:
                case FieldType.SInt32:
                case FieldType.SFixed32:
                    {
                        int temp = 0;
                        if (Read(ref temp))
                        {
                            value = temp;
                        }
                        else
                        {
                            return false;
                        }
                        break;
                    }
                case FieldType.UInt32:
                case FieldType.Fixed32:
                    {
                        uint temp = 0;
                        if (Read(ref temp))
                        {
                            value = temp;
                        }
                        else
                        {
                            return false;
                        }
                        break;
                    }
                case FieldType.Float:
                    {
                        float temp = float.NaN;
                        if (Read(ref temp))
                        {
                            value = temp;
                        }
                        else
                        {
                            return false;
                        }
                        break;
                    }
                case FieldType.Double:
                    {
                        double temp = float.NaN;
                        if (Read(ref temp))
                        {
                            value = temp;
                        }
                        else
                        {
                            return false;
                        }
                        break;
                    }
                case FieldType.String:
                    {
                        string temp = null;
                        if (Read(ref temp))
                        {
                            value = temp;
                        }
                        else
                        {
                            return false;
                        }
                        break;
                    }
                case FieldType.Bytes:
                    {
                        ByteString temp = null;
                        if (Read(ref temp))
                        {
                            value = temp;
                        }
                        else
                        {
                            return false;
                        }
                        break;
                    }
                default:
                    throw InvalidProtocolBufferException.InvalidTag();
            }
            return true;
        }

        #region ICodedInputStream Members

        bool ICodedInputStream.ReadTag(out uint fieldTag, out string fieldName)
        {
            fieldTag = 0;
            if (PeekNext(out fieldName))
            {
                return true;
            }
            return false;
        }

        bool ICodedInputStream.ReadDouble(ref double value)
        {
            return Read(ref value);
        }

        bool ICodedInputStream.ReadFloat(ref float value)
        {
            return Read(ref value);
        }

        bool ICodedInputStream.ReadUInt64(ref ulong value)
        {
            return Read(ref value);
        }

        bool ICodedInputStream.ReadInt64(ref long value)
        {
            return Read(ref value);
        }

        bool ICodedInputStream.ReadInt32(ref int value)
        {
            return Read(ref value);
        }

        bool ICodedInputStream.ReadFixed64(ref ulong value)
        {
            return Read(ref value);
        }

        bool ICodedInputStream.ReadFixed32(ref uint value)
        {
            return Read(ref value);
        }

        bool ICodedInputStream.ReadBool(ref bool value)
        {
            return Read(ref value);
        }

        bool ICodedInputStream.ReadString(ref string value)
        {
            return Read(ref value);
        }

        void ICodedInputStream.ReadGroup(int fieldNumber, IBuilderLite builder, ExtensionRegistry extensionRegistry)
        {
            if (_depth++ > MaxDepth)
            {
                throw new RecursionLimitExceededException();
            }
            ReadGroup(builder, extensionRegistry);
            _depth--;
        }

        void ICodedInputStream.ReadUnknownGroup(int fieldNumber, IBuilderLite builder)
        {
            throw new NotSupportedException();
        }

        void ICodedInputStream.ReadMessage(IBuilderLite builder, ExtensionRegistry extensionRegistry)
        {
            if (_depth++ > MaxDepth)
            {
                throw new RecursionLimitExceededException();
            }
            ReadMessage(builder, extensionRegistry);
            _depth--;
        }

        bool ICodedInputStream.ReadBytes(ref ByteString value)
        {
            return Read(ref value);
        }

        bool ICodedInputStream.ReadUInt32(ref uint value)
        {
            return Read(ref value);
        }

        bool ICodedInputStream.ReadEnum(ref IEnumLite value, out object unknown, IEnumLiteMap mapping)
        {
            value = null;
            unknown = null;
            if (ReadEnum(ref unknown))
            {
                if (unknown is int)
                {
                    value = mapping.FindValueByNumber((int) unknown);
                }
                else if (unknown is string)
                {
                    value = mapping.FindValueByName((string) unknown);
                }
                return value != null;
            }
            return false;
        }

        bool ICodedInputStream.ReadEnum<T>(ref T value, out object rawValue)
        {
            rawValue = null;
            if (ReadEnum(ref rawValue))
            {
                if (Enum.IsDefined(typeof (T), rawValue))
                {
                    if (rawValue is int)
                    {
                        value = (T) rawValue;
                    }
                    else if (rawValue is string)
                    {
                        value = (T) Enum.Parse(typeof (T), (string) rawValue, false);
                    }
                    else
                    {
                        value = default(T);
                        return false;
                    }
                    return true;
                }
            }
            return false;
        }

        bool ICodedInputStream.ReadSFixed32(ref int value)
        {
            return Read(ref value);
        }

        bool ICodedInputStream.ReadSFixed64(ref long value)
        {
            return Read(ref value);
        }

        bool ICodedInputStream.ReadSInt32(ref int value)
        {
            return Read(ref value);
        }

        bool ICodedInputStream.ReadSInt64(ref long value)
        {
            return Read(ref value);
        }

        void ICodedInputStream.ReadPrimitiveArray(FieldType fieldType, uint fieldTag, string fieldName,
                                                  ICollection<object> list)
        {
            ReadArray(fieldType, fieldName, list);
        }

        void ICodedInputStream.ReadEnumArray(uint fieldTag, string fieldName, ICollection<IEnumLite> list,
                                             out ICollection<object> unknown, IEnumLiteMap mapping)
        {
            unknown = null;
            List<object> array = new List<object>();
            if (ReadEnumArray(fieldName, array))
            {
                foreach (object rawValue in array)
                {
                    IEnumLite item = null;
                    if (rawValue is int)
                    {
                        item = mapping.FindValueByNumber((int) rawValue);
                    }
                    else if (rawValue is string)
                    {
                        item = mapping.FindValueByName((string) rawValue);
                    }

                    if (item != null)
                    {
                        list.Add(item);
                    }
                    else
                    {
                        if (unknown == null)
                        {
                            unknown = new List<object>();
                        }
                        unknown.Add(rawValue);
                    }
                }
            }
        }

        void ICodedInputStream.ReadEnumArray<T>(uint fieldTag, string fieldName, ICollection<T> list,
                                                out ICollection<object> unknown)
        {
            unknown = null;
            List<object> array = new List<object>();
            if (ReadEnumArray(fieldName, array))
            {
                foreach (object rawValue in array)
                {
                    if (rawValue is int)
                    {
                        list.Add((T) rawValue);
                    }
                    else if (rawValue is string)
                    {
                        list.Add((T) Enum.Parse(typeof (T), (string) rawValue, false));
                    }
                    else
                    {
                        if (unknown == null)
                        {
                            unknown = new List<object>();
                        }
                        unknown.Add(rawValue);
                    }
                }
            }
        }

        void ICodedInputStream.ReadMessageArray<T>(uint fieldTag, string fieldName, ICollection<T> list, T messageType,
                                                   ExtensionRegistry registry)
        {
            if (_depth++ > MaxDepth)
            {
                throw new RecursionLimitExceededException();
            }
            ReadMessageArray(fieldName, list, messageType, registry);
            _depth--;
        }

        void ICodedInputStream.ReadGroupArray<T>(uint fieldTag, string fieldName, ICollection<T> list, T messageType,
                                                 ExtensionRegistry registry)
        {
            if (_depth++ > MaxDepth)
            {
                throw new RecursionLimitExceededException();
            }
            ReadGroupArray(fieldName, list, messageType, registry);
            _depth--;
        }

        bool ICodedInputStream.ReadPrimitiveField(FieldType fieldType, ref object value)
        {
            return ReadField(fieldType, ref value);
        }

        bool ICodedInputStream.IsAtEnd
        {
            get
            {
                string next;
                return PeekNext(out next) == false;
            }
        }

        bool ICodedInputStream.SkipField()
        {
            Skip();
            return true;
        }

        void ICodedInputStream.ReadStringArray(uint fieldTag, string fieldName, ICollection<string> list)
        {
            ReadArray(FieldType.String, fieldName, list);
        }

        void ICodedInputStream.ReadBytesArray(uint fieldTag, string fieldName, ICollection<ByteString> list)
        {
            ReadArray(FieldType.Bytes, fieldName, list);
        }

        void ICodedInputStream.ReadBoolArray(uint fieldTag, string fieldName, ICollection<bool> list)
        {
            ReadArray(FieldType.Bool, fieldName, list);
        }

        void ICodedInputStream.ReadInt32Array(uint fieldTag, string fieldName, ICollection<int> list)
        {
            ReadArray(FieldType.Int32, fieldName, list);
        }

        void ICodedInputStream.ReadSInt32Array(uint fieldTag, string fieldName, ICollection<int> list)
        {
            ReadArray(FieldType.SInt32, fieldName, list);
        }

        void ICodedInputStream.ReadUInt32Array(uint fieldTag, string fieldName, ICollection<uint> list)
        {
            ReadArray(FieldType.UInt32, fieldName, list);
        }

        void ICodedInputStream.ReadFixed32Array(uint fieldTag, string fieldName, ICollection<uint> list)
        {
            ReadArray(FieldType.Fixed32, fieldName, list);
        }

        void ICodedInputStream.ReadSFixed32Array(uint fieldTag, string fieldName, ICollection<int> list)
        {
            ReadArray(FieldType.SFixed32, fieldName, list);
        }

        void ICodedInputStream.ReadInt64Array(uint fieldTag, string fieldName, ICollection<long> list)
        {
            ReadArray(FieldType.Int64, fieldName, list);
        }

        void ICodedInputStream.ReadSInt64Array(uint fieldTag, string fieldName, ICollection<long> list)
        {
            ReadArray(FieldType.SInt64, fieldName, list);
        }

        void ICodedInputStream.ReadUInt64Array(uint fieldTag, string fieldName, ICollection<ulong> list)
        {
            ReadArray(FieldType.UInt64, fieldName, list);
        }

        void ICodedInputStream.ReadFixed64Array(uint fieldTag, string fieldName, ICollection<ulong> list)
        {
            ReadArray(FieldType.Fixed64, fieldName, list);
        }

        void ICodedInputStream.ReadSFixed64Array(uint fieldTag, string fieldName, ICollection<long> list)
        {
            ReadArray(FieldType.SFixed64, fieldName, list);
        }

        void ICodedInputStream.ReadDoubleArray(uint fieldTag, string fieldName, ICollection<double> list)
        {
            ReadArray(FieldType.Double, fieldName, list);
        }

        void ICodedInputStream.ReadFloatArray(uint fieldTag, string fieldName, ICollection<float> list)
        {
            ReadArray(FieldType.Float, fieldName, list);
        }

        #endregion
    }
}