//
// Copyright (c) 2012-2021 Antmicro
//
// This file is licensed under the MIT License.
// Full license text is available in the LICENSE file.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using Antmicro.Migrant.Hooks;
using Antmicro.Migrant.Utilities;

namespace Antmicro.Migrant.Generators
{
    internal class CompletedGenerator : DynamicReadMethodGenerator<CompleteMethodDelegate>
    {
        public CompletedGenerator(Type type, SwapList objectsForSurrogates, bool disableStamping, bool treatCollectionAsUserObject, bool callPostDeserializationCallback)
            : base(type, "ObjectCompleted", disableStamping, treatCollectionAsUserObject, OpCodes.Ldarg_1, OpCodes.Ldarg_0)
        {
            this.objectsForSurrogates = objectsForSurrogates;
            this.callPostDeserializationCallback = callPostDeserializationCallback;
        }

        protected override void InnerGenerate(ReaderGenerationContext context)
        {
            var factoryId = objectsForSurrogates.FindMatchingIndex(type);

            GenerateCallPostDeserializationHooks(context, factoryId);
            GenerateDesurrogate(context, factoryId);
        }

        private void GenerateCallPostDeserializationHooks(ReaderGenerationContext context, int factoryId)
        {
            var methods = Helpers.GetMethodsWithAttribute(typeof(PostDeserializationAttribute), type).ToArray();
            foreach(var method in methods)
            {
                if(!method.IsStatic)
                {
                    context.PushDeserializedObjectOntoStack(context.PushObjectIdOntoStack);
                    context.Generator.Emit(OpCodes.Castclass, method.ReflectedType);
                }
                if(method.IsVirtual)
                {
                    context.Generator.Emit(OpCodes.Callvirt, method);
                }
                else
                {
                    context.Generator.Emit(OpCodes.Call, method);
                }
            }

            methods = Helpers.GetMethodsWithAttribute(typeof(LatePostDeserializationAttribute), type).ToArray();
            if(factoryId != -1 && methods.Length != 0)
            {
                throw new InvalidOperationException(
                    string.Format(ObjectReader.LateHookAndSurrogateError, type));
            }

            foreach(var method in methods)
            {
                context.PushObjectReaderOntoStack();
                context.Generator.PushFieldValueOntoStack<ObjectReader, List<Action>>(x => x.latePostDeserializationHooks);

                context.Generator.PushTypeOntoStack(typeof(Action));
                if(method.IsStatic)
                {
                    context.Generator.Emit(OpCodes.Ldnull);
                }
                else
                {
                    context.PushDeserializedObjectOntoStack(context.PushObjectIdOntoStack);
                    context.Generator.Emit(OpCodes.Castclass, method.ReflectedType);
                }

                context.Generator.Emit(OpCodes.Ldtoken, method);
                if(method.DeclaringType.IsGenericType)
                {
                    context.Generator.Emit(OpCodes.Ldtoken, method.DeclaringType);

                    context.Generator.Emit(OpCodes.Call, Helpers.GetMethodInfo<object, MethodBase>(x => MethodBase.GetMethodFromHandle(new RuntimeMethodHandle(), new RuntimeTypeHandle())));
                }
                else
                {
                    context.Generator.Emit(OpCodes.Call, Helpers.GetMethodInfo<object, MethodBase>(x => MethodBase.GetMethodFromHandle(new RuntimeMethodHandle())));
                }

                context.Generator.Emit(OpCodes.Castclass, typeof(MethodInfo));
                context.Generator.Emit(OpCodes.Call, Helpers.GetMethodInfo<object, Delegate>(x => Delegate.CreateDelegate(null, null, method)));

                context.Generator.Emit(OpCodes.Castclass, typeof(Action));

                context.Generator.Emit(OpCodes.Call, Helpers.GetMethodInfo<List<Action>>(x => x.Add(null)));
            }

            if(callPostDeserializationCallback)
            {
                context.PushObjectReaderOntoStack();
                context.Generator.PushFieldValueOntoStack<ObjectReader, Action<object>>(x => x.postDeserializationCallback);
                context.PushDeserializedObjectOntoStack(context.PushObjectIdOntoStack);
                context.Generator.Emit(OpCodes.Call, Helpers.GetMethodInfo<Action<object>>(x => x.Invoke(null)));
            }
        }

        private void GenerateDesurrogate(ReaderGenerationContext context, int factoryId)
        {
            if(factoryId == -1)
            {
                return;
            }

            var desurrogatedObjectLocal = context.Generator.DeclareLocal(typeof(object));

            // obtain surrogate factory
            context.PushObjectReaderOntoStack();
            context.Generator.PushFieldValueOntoStack<ObjectReader, SwapList>(x => x.objectsForSurrogates);
            context.Generator.PushIntegerOntoStack(factoryId);
            context.Generator.Call<SwapList>(x => x.GetByIndex(0));

            // recreate an object from the surrogate
            var delegateType = typeof(Func<,>).MakeGenericType(type, typeof(object));
            context.Generator.Emit(OpCodes.Castclass, delegateType);
            context.PushDeserializedObjectOntoStack(context.PushObjectIdOntoStack);
            context.Generator.Emit(OpCodes.Call, delegateType.GetMethod("Invoke"));

            context.Generator.StoreLocalValueFromStack(desurrogatedObjectLocal);

            // clone context
            context.PushObjectReaderOntoStack();
            context.Generator.PushFieldValueOntoStack<ObjectReader, Serializer.ReadMethods>(x => x.readMethods);
            context.Generator.PushFieldValueOntoStack<Serializer.ReadMethods, DynamicMethodProvider<CloneMethodDelegate>>(x => x.cloneContentMehtodsProvider);
            context.Generator.PushLocalValueOntoStack(desurrogatedObjectLocal);
            context.Generator.Call<object>(x => x.GetType());
            context.Generator.Call<DynamicMethodProvider<CloneMethodDelegate>>(x => x.GetOrCreate(typeof(void)));
            context.Generator.PushLocalValueOntoStack(desurrogatedObjectLocal);
            context.PushDeserializedObjectOntoStack(context.PushObjectIdOntoStack, true);
            context.Generator.Call<CloneMethodDelegate>(x => x.Invoke(null, null));

            //remove object reference from surrogatesWhileReading collection
            context.PushObjectReaderOntoStack();
            context.Generator.PushFieldValueOntoStack<ObjectReader, OneToOneMap<int, object>>(x => x.surrogatesWhileReading);

            context.PushObjectIdOntoStack();
            context.Generator.Call<OneToOneMap<int, object>>(x => x.Remove(0));
        }

        private readonly SwapList objectsForSurrogates;
        private readonly bool callPostDeserializationCallback;
    }
}

