#nullable enable
using IPA.Config.Data;
using IPA.Config.Stores.Attributes;
using IPA.Logging;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
#if NET3
using Net3_Proxy;
using Array = Net3_Proxy.Array;
#endif

namespace IPA.Config.Stores
{
    internal static partial class GeneratedStoreImpl
    {
        private static bool NeedsCorrection(SerializedMemberInfo member)
        {
            if (member.HasConverter) return false;
            var memberType = member.ConversionType;
            var expectType = GetExpectedValueTypeForType(memberType);

            if (expectType == typeof(Map)) // TODO: make this slightly saner
            {
                if (expectType.IsValueType)
                { // custom value type
                    return ReadObjectMembers(memberType).Any(NeedsCorrection);
                }
                return true;
            }
            return false;
        }

        // expects start value on stack, exits with final value on stack
        private static void EmitCorrectMember(ILGenerator il, SerializedMemberInfo member, bool shouldLock, bool alwaysNew, LocalAllocator GetLocal,
            Action<ILGenerator> thisobj, Action<ILGenerator> parentobj)
        {
            if (!NeedsCorrection(member)) return;

            var endLabel = il.DefineLabel();

            // TODO: when we have a nullable, we need to save to a local to call methods
            if (member.IsNullable)
            {
                il.Emit(OpCodes.Dup);
                il.Emit(OpCodes.Call, member.Nullable_HasValue.GetGetMethod());
                il.Emit(OpCodes.Brfalse, endLabel);
                il.Emit(OpCodes.Call, member.Nullable_Value.GetGetMethod());
            }

            var convType = member.ConversionType;
            if (!convType.IsValueType)
            {
                // currently the only thing for this is where expect == Map, so do generate shit
                var copyFrom = typeof(IGeneratedStore<>).MakeGenericType(convType).GetMethod(nameof(IGeneratedStore<Config>.CopyFrom));
                var noCreate = il.DefineLabel();
                using var valLocal = GetLocal.Allocate(convType);

                if (member.AllowNull)
                {
                    il.Emit(OpCodes.Dup);
                    il.Emit(OpCodes.Brfalse_S, endLabel); // thing is null, just bypass it all
                }
                if (!alwaysNew)
                {
                    il.Emit(OpCodes.Dup);
                    il.Emit(OpCodes.Isinst, typeof(IGeneratedStore));
                    il.Emit(OpCodes.Brtrue_S, endLabel); // our input is already something we like
                }
                il.Emit(OpCodes.Stloc, valLocal);
                if (!alwaysNew)
                {
                    EmitLoad(il, member, thisobj);
                    il.Emit(OpCodes.Dup);
                    il.Emit(OpCodes.Isinst, typeof(IGeneratedStore));
                    il.Emit(OpCodes.Brtrue_S, noCreate);
                    il.Emit(OpCodes.Pop);
                }
                EmitCreateChildGenerated(il, convType, parentobj);
                il.MarkLabel(noCreate);

                il.Emit(OpCodes.Dup);
                il.Emit(OpCodes.Ldloc, valLocal);
                il.Emit(shouldLock ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0);
                il.Emit(OpCodes.Callvirt, copyFrom);
            }
            else
            {
                // for special value types, we'll go ahead and correct each of their members
                var structure = ReadObjectMembers(convType);

                using var valueLocal = GetLocal.Allocate(convType);
                il.Emit(OpCodes.Stloc, valueLocal);

                void LdlocaValueLocal(ILGenerator il)
                    => il.Emit(OpCodes.Ldloca, valueLocal);

                foreach (var mem in structure)
                {
                    if (NeedsCorrection(mem))
                        EmitLoadCorrectStore(il, mem, shouldLock, alwaysNew, GetLocal,
                            LdlocaValueLocal, LdlocaValueLocal, parentobj);
                }

                il.Emit(OpCodes.Ldloc, valueLocal);
            }

            if (member.IsNullable)
                il.Emit(OpCodes.Newobj, member.Nullable_Construct);

            il.MarkLabel(endLabel);
        }

        private static void EmitLoadCorrectStore(ILGenerator il, SerializedMemberInfo member, bool shouldLock, bool alwaysNew, LocalAllocator GetLocal,
            Action<ILGenerator> loadFrom, Action<ILGenerator> storeTo, Action<ILGenerator> parentobj)
        {
            EmitStore(il, member, il =>
            {
                EmitLoad(il, member, loadFrom); // load the member
                EmitCorrectMember(il, member, shouldLock, alwaysNew, GetLocal, storeTo, parentobj); // correct it
            }, storeTo);
        }
    }
}
