#if !NO_RUNTIME
using System;
using System.Collections;
using System.Reflection;
using ProtoBuf.Meta;

namespace ProtoBuf.Serializers
{
    sealed class ArrayDecorator : ProtoDecoratorBase
    {
        private readonly int fieldNumber;
        private const byte
                   OPTIONS_WritePacked = 1,
                   OPTIONS_OverwriteList = 2,
                   OPTIONS_SupportNull = 4;
        private readonly byte options;
        private readonly WireType packedWireType;
        public ArrayDecorator(TypeModel model, IProtoSerializer tail, int fieldNumber, bool writePacked, WireType packedWireType, Type arrayType, bool overwriteList, bool supportNull)
            : base(tail)
        {
            Helpers.DebugAssert(arrayType != null, "arrayType should be non-null");
            Helpers.DebugAssert(arrayType.IsArray && arrayType.GetArrayRank() == 1, "should be single-dimension array; " + arrayType.FullName);
            this.itemType = arrayType.GetElementType();
            Type underlyingItemType = supportNull ? itemType : (Helpers.GetUnderlyingType(itemType) ?? itemType);

            Helpers.DebugAssert(underlyingItemType == Tail.ExpectedType
                || (Tail.ExpectedType == model.MapType(typeof(object)) && !Helpers.IsValueType(underlyingItemType)), "invalid tail");
            Helpers.DebugAssert(Tail.ExpectedType != model.MapType(typeof(byte)), "Should have used BlobSerializer");
            if ((writePacked || packedWireType != WireType.None) && fieldNumber <= 0) throw new ArgumentOutOfRangeException("fieldNumber");
            if (!ListDecorator.CanPack(packedWireType))
            {
                if (writePacked) throw new InvalidOperationException("Only simple data-types can use packed encoding");
                packedWireType = WireType.None;
            }
            this.fieldNumber = fieldNumber;
            this.packedWireType = packedWireType;
            if (writePacked) options |= OPTIONS_WritePacked;
            if (overwriteList) options |= OPTIONS_OverwriteList;
            if (supportNull) options |= OPTIONS_SupportNull;
            this.arrayType = arrayType;
        }
        readonly Type arrayType, itemType; // this is, for example, typeof(int[])
        public override Type ExpectedType { get { return arrayType; } }
        public override bool RequiresOldValue { get { return AppendToCollection; } }
        public override bool ReturnsValue { get { return true; } }
        private bool CanUsePackedPrefix() => CanUsePackedPrefix(packedWireType, itemType);

        internal static bool CanUsePackedPrefix(WireType packedWireType, Type itemType)
        {
            // needs to be a suitably simple type *and* be definitely not nullable
            switch (packedWireType)
            {
                case WireType.Fixed32:
                case WireType.Fixed64:
                    break;
                default:
                    return false; // nope
            }
            if (!Helpers.IsValueType(itemType)) return false;
            return Helpers.GetUnderlyingType(itemType) == null;
        }

#if FEAT_COMPILER
        protected override void EmitWrite(ProtoBuf.Compiler.CompilerContext ctx, ProtoBuf.Compiler.Local valueFrom)
        {
            // int i and T[] arr
            using (Compiler.Local arr = ctx.GetLocalWithValue(arrayType, valueFrom))
            using (Compiler.Local i = new ProtoBuf.Compiler.Local(ctx, ctx.MapType(typeof(int))))
            {
                bool writePacked = (options & OPTIONS_WritePacked) != 0;
                bool fixedLengthPacked = writePacked && CanUsePackedPrefix();

                using (Compiler.Local token = (writePacked && !fixedLengthPacked) ? new Compiler.Local(ctx, ctx.MapType(typeof(SubItemToken))) : null)
                {
                    Type mappedWriter = ctx.MapType(typeof(ProtoWriter));
                    if (writePacked)
                    {
                        ctx.LoadValue(fieldNumber);
                        ctx.LoadValue((int)WireType.String);
                        ctx.LoadReaderWriter();
                        ctx.EmitCall(mappedWriter.GetMethod("WriteFieldHeader"));

                        if (fixedLengthPacked)
                        {
                            // write directly - no need for buffering
                            ctx.LoadLength(arr, false);
                            ctx.LoadValue((int)packedWireType);
                            ctx.LoadReaderWriter();
                            ctx.EmitCall(mappedWriter.GetMethod("WritePackedPrefix"));
                        }
                        else
                        {
                            ctx.LoadValue(arr);
                            ctx.LoadReaderWriter();
                            ctx.EmitCall(mappedWriter.GetMethod("StartSubItem"));
                            ctx.StoreValue(token);
                        }
                        ctx.LoadValue(fieldNumber);
                        ctx.LoadReaderWriter();
                        ctx.EmitCall(mappedWriter.GetMethod("SetPackedField"));
                    }
                    EmitWriteArrayLoop(ctx, i, arr);

                    if (writePacked)
                    {
                        if (fixedLengthPacked)
                        {
                            ctx.LoadValue(fieldNumber);
                            ctx.LoadReaderWriter();
                            ctx.EmitCall(mappedWriter.GetMethod("ClearPackedField"));
                        }
                        else
                        {
                            ctx.LoadValue(token);
                            ctx.LoadReaderWriter();
                            ctx.EmitCall(mappedWriter.GetMethod("EndSubItem"));
                        }
                    }
                }
            }
        }

        private void EmitWriteArrayLoop(Compiler.CompilerContext ctx, Compiler.Local i, Compiler.Local arr)
        {
            // i = 0
            ctx.LoadValue(0);
            ctx.StoreValue(i);

            // range test is last (to minimise branches)
            Compiler.CodeLabel loopTest = ctx.DefineLabel(), processItem = ctx.DefineLabel();
            ctx.Branch(loopTest, false);
            ctx.MarkLabel(processItem);

            // {...}
            ctx.LoadArrayValue(arr, i);
            if (SupportNull)
            {
                Tail.EmitWrite(ctx, null);
            }
            else
            {
                ctx.WriteNullCheckedTail(itemType, Tail, null);
            }

            // i++
            ctx.LoadValue(i);
            ctx.LoadValue(1);
            ctx.Add();
            ctx.StoreValue(i);

            // i < arr.Length
            ctx.MarkLabel(loopTest);
            ctx.LoadValue(i);
            ctx.LoadLength(arr, false);
            ctx.BranchIfLess(processItem, false);
        }
#endif
        private bool AppendToCollection => (options & OPTIONS_OverwriteList) == 0;

        private bool SupportNull { get { return (options & OPTIONS_SupportNull) != 0; } }

        public override void Write(object value, ProtoWriter dest)
        {
            IList arr = (IList)value;
            int len = arr.Count;
            SubItemToken token;
            bool writePacked = (options & OPTIONS_WritePacked) != 0;
            bool fixedLengthPacked = writePacked && CanUsePackedPrefix();

            if (writePacked)
            {
                ProtoWriter.WriteFieldHeader(fieldNumber, WireType.String, dest);

                if (fixedLengthPacked)
                {
                    ProtoWriter.WritePackedPrefix(arr.Count, packedWireType, dest);
                    token = new SubItemToken(); // default
                }
                else
                {
                    token = ProtoWriter.StartSubItem(value, dest);
                }
                ProtoWriter.SetPackedField(fieldNumber, dest);
            }
            else
            {
                token = new SubItemToken(); // default
            }
            bool checkForNull = !SupportNull;
            for (int i = 0; i < len; i++)
            {
                object obj = arr[i];
                if (checkForNull && obj == null) { throw new NullReferenceException(); }
                Tail.Write(obj, dest);
            }
            if (writePacked)
            {
                if (fixedLengthPacked)
                {
                    ProtoWriter.ClearPackedField(fieldNumber, dest);
                }
                else
                {
                    ProtoWriter.EndSubItem(token, dest);
                }
            }
        }
        public override object Read(object value, ProtoReader source)
        {
            int field = source.FieldNumber;
            BasicList list = new BasicList();
            if (packedWireType != WireType.None && source.WireType == WireType.String)
            {
                SubItemToken token = ProtoReader.StartSubItem(source);
                while (ProtoReader.HasSubValue(packedWireType, source))
                {
                    list.Add(Tail.Read(null, source));
                }
                ProtoReader.EndSubItem(token, source);
            }
            else
            {
                do
                {
                    list.Add(Tail.Read(null, source));
                } while (source.TryReadFieldHeader(field));
            }
            int oldLen = AppendToCollection ? ((value == null ? 0 : ((Array)value).Length)) : 0;
            Array result = Array.CreateInstance(itemType, oldLen + list.Count);
            if (oldLen != 0) ((Array)value).CopyTo(result, 0);
            list.CopyTo(result, oldLen);
            return result;
        }

#if FEAT_COMPILER
        protected override void EmitRead(ProtoBuf.Compiler.CompilerContext ctx, ProtoBuf.Compiler.Local valueFrom)
        {
            Type listType;
            listType = ctx.MapType(typeof(System.Collections.Generic.List<>)).MakeGenericType(itemType);
            Type expected = ExpectedType;
            using (Compiler.Local oldArr = AppendToCollection ? ctx.GetLocalWithValue(expected, valueFrom) : null)
            using (Compiler.Local newArr = new Compiler.Local(ctx, expected))
            using (Compiler.Local list = new Compiler.Local(ctx, listType))
            {
                ctx.EmitCtor(listType);
                ctx.StoreValue(list);
                ListDecorator.EmitReadList(ctx, list, Tail, listType.GetMethod("Add"), packedWireType, false);

                // leave this "using" here, as it can share the "FieldNumber" local with EmitReadList
                using (Compiler.Local oldLen = AppendToCollection ? new ProtoBuf.Compiler.Local(ctx, ctx.MapType(typeof(int))) : null)
                {
                    Type[] copyToArrayInt32Args = new Type[] { ctx.MapType(typeof(Array)), ctx.MapType(typeof(int)) };

                    if (AppendToCollection)
                    {
                        ctx.LoadLength(oldArr, true);
                        ctx.CopyValue();
                        ctx.StoreValue(oldLen);

                        ctx.LoadAddress(list, listType);
                        ctx.LoadValue(listType.GetProperty("Count"));
                        ctx.Add();
                        ctx.CreateArray(itemType, null); // length is on the stack
                        ctx.StoreValue(newArr);

                        ctx.LoadValue(oldLen);
                        Compiler.CodeLabel nothingToCopy = ctx.DefineLabel();
                        ctx.BranchIfFalse(nothingToCopy, true);
                        ctx.LoadValue(oldArr);
                        ctx.LoadValue(newArr);
                        ctx.LoadValue(0); // index in target

                        ctx.EmitCall(expected.GetMethod("CopyTo", copyToArrayInt32Args));
                        ctx.MarkLabel(nothingToCopy);

                        ctx.LoadValue(list);
                        ctx.LoadValue(newArr);
                        ctx.LoadValue(oldLen);

                    }
                    else
                    {
                        ctx.LoadAddress(list, listType);
                        ctx.LoadValue(listType.GetProperty("Count"));
                        ctx.CreateArray(itemType, null);
                        ctx.StoreValue(newArr);

                        ctx.LoadAddress(list, listType);
                        ctx.LoadValue(newArr);
                        ctx.LoadValue(0);
                    }

                    copyToArrayInt32Args[0] = expected; // // prefer: CopyTo(T[], int)
                    MethodInfo copyTo = listType.GetMethod("CopyTo", copyToArrayInt32Args);
                    if (copyTo == null)
                    { // fallback: CopyTo(Array, int)
                        copyToArrayInt32Args[1] = ctx.MapType(typeof(Array));
                        copyTo = listType.GetMethod("CopyTo", copyToArrayInt32Args);
                    }
                    ctx.EmitCall(copyTo);
                }
                ctx.LoadValue(newArr);
            }


        }
#endif
    }
}
#endif