using IPA.Config.Data;
using IPA.Config.Stores.Attributes;
using IPA.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
using System.IO;
using Boolean = IPA.Config.Data.Boolean;
using System.Collections;
using IPA.Utilities;
#if NET3
using Net3_Proxy;
using Array = Net3_Proxy.Array;
#endif

[assembly: InternalsVisibleTo(IPA.Config.Stores.GeneratedExtension.AssemblyVisibilityTarget)]

namespace IPA.Config.Stores
{
    /// <summary>
    /// A class providing an extension for <see cref="Config"/> to make it easy to use generated
    /// config stores.
    /// </summary>
    public static class GeneratedExtension
    {
        /// <summary>
        /// The name of the assembly that internals must be visible to to allow internal protection.
        /// </summary>
        public const string AssemblyVisibilityTarget = GeneratedStore.GeneratedAssemblyName;

        /// <summary>
        /// Creates a generated <see cref="IConfigStore"/> of type <typeparamref name="T"/>, registers it to
        /// the <see cref="Config"/> object, and returns it. This also forces a synchronous config load via
        /// <see cref="Config.LoadSync"/> if <paramref name="loadSync"/> is <see langword="true"/>.
        /// </summary>
        /// <remarks>
        /// <para>
        /// <typeparamref name="T"/> must be a public non-<see langword="sealed"/> class.
        /// It can also be internal, but in that case, then your assembly must have the following attribute
        /// to allow the generated code to reference it.
        /// <code>
        /// [assembly: InternalsVisibleTo(IPA.Config.Stores.GeneratedExtension.AssemblyVisibilityTarget)]
        /// </code>
        /// </para>
        /// <para>
        /// Only fields and properties that are public or protected will be considered, and only properties
        /// where both the getter and setter are public or protected are considered. Any fields or properties
        /// with an <see cref="IgnoreAttribute"/> applied to them are also ignored. Having properties be <see langword="virtual"/> is not strictly
        /// necessary, however it allows the generated type to keep track of changes and lock around them so that the config will auto-save.
        /// </para>
        /// <para>
        /// All of the attributes in the <see cref="Attributes"/> namespace are handled as described by them.
        /// </para>
        /// <para>
        /// If the <typeparamref name="T"/> declares a public or protected, <see langword="virtual"/>
        /// method <c>Changed()</c>, then that method may be called to artificially signal to the runtime that the content of the object 
        /// has changed. That method will also be called after the write locks are released when a property is set anywhere in the owning
        /// tree. This will only be called on the outermost generated object of the config structure, even if the change being signaled
        /// is somewhere deep into the tree. TODO: is this a good idea?
        /// </para>
        /// <para>
        /// Similarly, <typeparamref name="T"/> can declare a public or protected, <see langword="virtual"/> 
        /// method <c>OnReload()</c>, which will be called on the filesystem reader thread after the object has been repopulated with new data 
        /// values. It will be called <i>after</i> the write lock for this object is released. This will only be called on the outermost generated
        /// object of the config structure.
        /// </para>
        /// <para>
        /// Similarly, <typeparamref name="T"/> can declare a public or protected, <see langword="virtual"/> 
        /// method <c>CopyFrom(ConfigType)</c> (the first parameter is the type it is defined on), which may be called to copy the properties from
        /// another object of its type easily, and more importantly, as only one change. Its body will be executed after the values have been copied.
        /// </para>
        /// <para>
        /// TODO: describe details of generated stores
        /// </para>
        /// </remarks>
        /// <typeparam name="T">the type to wrap</typeparam>
        /// <param name="cfg">the <see cref="Config"/> to register to</param>
        /// <param name="loadSync">whether to synchronously load the content, or trigger an async load</param>
        /// <returns>a generated instance of <typeparamref name="T"/> as a special <see cref="IConfigStore"/></returns>
        public static T Generated<T>(this Config cfg, bool loadSync = true) where T : class
        {
            var ret = GeneratedStore.Create<T>();
            cfg.SetStore(ret as IConfigStore);
            if (loadSync)
                cfg.LoadSync();
            else
                cfg.LoadAsync();

            return ret;
        }
    }

    internal static class GeneratedStore
    {
        internal interface IGeneratedStore
        {
            Type Type { get; }
            IGeneratedStore Parent { get; }
            Impl Impl { get; }
            void OnReload();

            void Changed();

            Value Serialize();
            void Deserialize(Value val);
        }
        internal interface IGeneratedStore<T> : IGeneratedStore where T : class
        {
            void CopyFrom(T source, bool useLock);
        }

        internal class Impl : IConfigStore
        {
            private readonly IGeneratedStore generated;

            internal static ConstructorInfo Ctor = typeof(Impl).GetConstructor(new[] { typeof(IGeneratedStore) });
            public Impl(IGeneratedStore store) => generated = store;

            private readonly AutoResetEvent resetEvent = new AutoResetEvent(false);
            public WaitHandle SyncObject => resetEvent;
            public static WaitHandle ImplGetSyncObject(IGeneratedStore s) => FindImpl(s).SyncObject;
            internal static MethodInfo ImplGetSyncObjectMethod = typeof(Impl).GetMethod(nameof(ImplGetSyncObject));

            public ReaderWriterLockSlim WriteSyncObject { get; } = new ReaderWriterLockSlim();
            public static ReaderWriterLockSlim ImplGetWriteSyncObject(IGeneratedStore s) => FindImpl(s)?.WriteSyncObject;
            internal static MethodInfo ImplGetWriteSyncObjectMethod = typeof(Impl).GetMethod(nameof(ImplGetWriteSyncObject));

            internal static MethodInfo ImplSignalChangedMethod = typeof(Impl).GetMethod(nameof(ImplSignalChanged));
            public static void ImplSignalChanged(IGeneratedStore s) => FindImpl(s).SignalChanged();
            public void SignalChanged() => resetEvent.Set();

            internal static MethodInfo ImplInvokeChangedMethod = typeof(Impl).GetMethod(nameof(ImplInvokeChanged));
            public static void ImplInvokeChanged(IGeneratedStore s) => FindImpl(s).InvokeChanged();
            public void InvokeChanged() => generated.Changed();

            internal static MethodInfo ImplTakeReadMethod = typeof(Impl).GetMethod(nameof(ImplTakeRead));
            public static void ImplTakeRead(IGeneratedStore s) => FindImpl(s).TakeRead();
            public void TakeRead() => WriteSyncObject.EnterReadLock();

            internal static MethodInfo ImplReleaseReadMethod = typeof(Impl).GetMethod(nameof(ImplReleaseRead));
            public static void ImplReleaseRead(IGeneratedStore s) => FindImpl(s).ReleaseRead();
            public void ReleaseRead() => WriteSyncObject.ExitReadLock();

            internal static MethodInfo ImplTakeWriteMethod = typeof(Impl).GetMethod(nameof(ImplTakeWrite));
            public static void ImplTakeWrite(IGeneratedStore s) => FindImpl(s).TakeWrite();
            public void TakeWrite() => WriteSyncObject.EnterWriteLock();

            internal static MethodInfo ImplReleaseWriteMethod = typeof(Impl).GetMethod(nameof(ImplReleaseWrite));
            public static void ImplReleaseWrite(IGeneratedStore s) => FindImpl(s).ReleaseWrite();
            public void ReleaseWrite() => WriteSyncObject.ExitWriteLock();


            public static Impl FindImpl(IGeneratedStore store)
            {
                while (store?.Parent != null) store = store.Parent; // walk to the top of the tree
                return store?.Impl;
            }


            internal static MethodInfo ImplReadFromMethod = typeof(Impl).GetMethod(nameof(ImplReadFrom));
            public static void ImplReadFrom(IGeneratedStore s, ConfigProvider provider) => FindImpl(s).ReadFrom(provider);
            public void ReadFrom(ConfigProvider provider)
            {
                var values = provider.Load();
                Logger.config.Debug("Generated impl ReadFrom");
                Logger.config.Debug($"Read {values}");
                generated.Deserialize(values);

                ReleaseWrite();
                generated.OnReload();
                TakeWrite(); // must take again for runtime to be happy (which is unfortunate)
            }

            internal static MethodInfo ImplWriteToMethod = typeof(Impl).GetMethod(nameof(ImplWriteTo));
            public static void ImplWriteTo(IGeneratedStore s, ConfigProvider provider) => FindImpl(s).WriteTo(provider);
            public void WriteTo(ConfigProvider provider)
            {
                var values = generated.Serialize();
                Logger.config.Debug("Generated impl WriteTo");
                Logger.config.Debug($"Serialized {values}");
                provider.Store(values);
            }
        }

        private static readonly Dictionary<Type, (GeneratedStoreCreator ctor, Type type)> generatedCreators = new Dictionary<Type, (GeneratedStoreCreator ctor, Type type)>();

        public static T Create<T>() where T : class => (T)Create(typeof(T));

        public static IConfigStore Create(Type type) => Create(type, null);

        private static readonly MethodInfo CreateGParent = 
            typeof(GeneratedStore).GetMethod(nameof(Create), BindingFlags.NonPublic | BindingFlags.Static, null, 
                                             CallingConventions.Any, new[] { typeof(IGeneratedStore) }, Array.Empty<ParameterModifier>());
        internal static T Create<T>(IGeneratedStore parent) where T : class => (T)Create(typeof(T), parent);

        private static IConfigStore Create(Type type, IGeneratedStore parent)
            => GetCreator(type)(parent);

        internal static GeneratedStoreCreator GetCreator(Type t)
        {
            if (generatedCreators.TryGetValue(t, out var gen))
                return gen.ctor;
            else
            {
                gen = MakeCreator(t);
                generatedCreators.Add(t, gen);
                return gen.ctor;
            }
        }

        internal static Type GetGeneratedType(Type t)
        {
            if (generatedCreators.TryGetValue(t, out var gen))
                return gen.type;
            else
            {
                gen = MakeCreator(t);
                generatedCreators.Add(t, gen);
                return gen.type;
            }
        }

        internal const string GeneratedAssemblyName = "IPA.Config.Generated";

        private static AssemblyBuilder assembly = null;
        private static AssemblyBuilder Assembly
        {
            get
            {
                if (assembly == null)
                {
                    var name = new AssemblyName(GeneratedAssemblyName);
                    assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(name, AssemblyBuilderAccess.RunAndSave);
                }

                return assembly;
            }
        }

        internal static void DebugSaveAssembly(string file)
        {
            Assembly.Save(file);
        }

        private static ModuleBuilder module = null;
        private static ModuleBuilder Module
        {
            get
            {
                if (module == null)
                    module = Assembly.DefineDynamicModule(Assembly.GetName().Name, Assembly.GetName().Name + ".dll");

                return module;
            }
        }

        private class SerializedMemberInfo
        {
            public string Name;
            public MemberInfo Member;
            public Type Type;
            public bool AllowNull;
            public bool IsVirtual;
            public bool IsField;
            public bool IsNullable; // signifies whether this is a Nullable<T>

            public bool HasConverter;
            public bool IsGenericConverter; // used so we can call directly to the generic version if it is
            public Type Converter;
            public Type ConverterBase;
            public Type ConverterTarget;
            public FieldInfo ConverterField;

            // invalid for objects with IsNullabe false
            public Type NullableWrappedType => Nullable.GetUnderlyingType(Type);
            // invalid for objects with IsNullabe false
            public PropertyInfo Nullable_HasValue => Type.GetProperty(nameof(Nullable<int>.HasValue));
            // invalid for objects with IsNullabe false
            public PropertyInfo Nullable_Value => Type.GetProperty(nameof(Nullable<int>.Value));
            // invalid for objects with IsNullabe false
            public ConstructorInfo Nullable_Construct => Type.GetConstructor(new[] { NullableWrappedType });
        }

        internal delegate IConfigStore GeneratedStoreCreator(IGeneratedStore parent);

        private static (GeneratedStoreCreator ctor, Type type) MakeCreator(Type type)
        { // note that this does not and should not use converters by default for everything
            if (!type.IsClass) throw new ArgumentException("Config type is not a class");

            var baseCtor = type.GetConstructor(Type.EmptyTypes); // get a default constructor
            if (baseCtor == null)
                throw new ArgumentException("Config type does not have a public parameterless constructor");

            #region Parse base object structure
            var baseChanged = type.GetMethod("Changed", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, Array.Empty<ParameterModifier>());
            if (baseChanged != null && !baseChanged.IsVirtual) baseChanged = null;
            var baseOnReload = type.GetMethod("OnReload", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, Array.Empty<ParameterModifier>());
            if (baseOnReload != null && !baseOnReload.IsVirtual) baseOnReload = null;
            var baseCopyFrom = type.GetMethod("CopyFrom", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { type }, Array.Empty<ParameterModifier>());
            if (baseCopyFrom != null && !baseCopyFrom.IsVirtual) baseCopyFrom = null;

            var structure = new List<SerializedMemberInfo>();

            bool ProcessAttributesFor(ref SerializedMemberInfo member)
            {
                var attrs = member.Member.GetCustomAttributes(true);
                var ignores = attrs.Select(o => o as IgnoreAttribute).NonNull();
                if (ignores.Any()) // we ignore
                {
                    return false;
                }

                var nonNullables = attrs.Select(o => o as NonNullableAttribute).NonNull();

                member.Name = member.Member.Name;
                member.IsNullable = member.Type.IsGenericType
                          && member.Type.GetGenericTypeDefinition() == typeof(Nullable<>);
                member.AllowNull = !nonNullables.Any() && (!member.Type.IsValueType || member.IsNullable);

                var nameAttr = attrs.Select(o => o as SerializedNameAttribute).NonNull().FirstOrDefault();
                if (nameAttr != null)
                    member.Name = nameAttr.Name;

                member.HasConverter = false;
                var converterAttr = attrs.Select(o => o as UseConverterAttribute).NonNull().FirstOrDefault();
                if (converterAttr != null)
                {
                    member.Converter = converterAttr.ConverterType;
                    member.IsGenericConverter = converterAttr.IsGenericConverter;

                    if (member.Converter.GetConstructor(Type.EmptyTypes) == null)
                    {
                        Logger.config.Warn($"{type.FullName}'s member {member.Member.Name} requests a converter that is not default-constructible");
                        goto endConverterAttr; // is there a better control flow structure to do this?
                    }

                    if (member.Converter.ContainsGenericParameters)
                    {
                        Logger.config.Warn($"{type.FullName}'s member {member.Member.Name} requests a converter that has unfilled type parameters");
                        goto endConverterAttr;
                    }

                    if (member.Converter.IsInterface || member.Converter.IsAbstract)
                    {
                        Logger.config.Warn($"{type.FullName}'s member {member.Member.Name} requests a converter that is not constructible");
                        goto endConverterAttr;
                    }

                    var targetType = converterAttr.ConverterTargetType;
                    if (!member.IsGenericConverter)
                    {
                        try
                        {
                            var conv = Activator.CreateInstance(converterAttr.ConverterType) as IValueConverter;
                            targetType = conv.Type;
                        }
                        catch
                        {
                            Logger.config.Warn($"{type.FullName}'s member {member.Member.Name} requests a converter who's target type could not be determined");
                            goto endConverterAttr;
                        }
                    }
                    if (targetType != member.Type)
                    {
                        Logger.config.Warn($"{type.FullName}'s member {member.Member.Name} requests a converter that is not of the member's type");
                        goto endConverterAttr;
                    }

                    member.ConverterTarget = targetType;
                    if (member.IsGenericConverter)
                        member.ConverterBase = typeof(ValueConverter<>).MakeGenericType(targetType);
                    else
                        member.ConverterBase = typeof(IValueConverter);

                    member.HasConverter = true;
                }
            endConverterAttr:

                return true;
            }

            // only looks at public/protected properties
            foreach (var prop in type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
            {
                if (prop.GetSetMethod(true)?.IsPrivate ?? true)
                { // we enter this block if the setter is inacessible or doesn't exist
                    continue; // ignore props without setter
                }
                if (prop.GetGetMethod(true)?.IsPrivate ?? true)
                { // we enter this block if the getter is inacessible or doesn't exist
                    continue; // ignore props without getter
                }

                var smi = new SerializedMemberInfo
                {
                    Member = prop,
                    IsVirtual = (prop.GetGetMethod(true)?.IsVirtual ?? false) ||
                                (prop.GetSetMethod(true)?.IsVirtual ?? false),
                    IsField = false,
                    Type = prop.PropertyType
                };

                if (!ProcessAttributesFor(ref smi)) continue;

                structure.Add(smi);
            }

            // only look at public/protected fields
            foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
            {
                if (field.IsPrivate) continue;

                var smi = new SerializedMemberInfo
                {
                    Member = field,
                    IsVirtual = false,
                    IsField = true,
                    Type = field.FieldType
                };

                if (!ProcessAttributesFor(ref smi)) continue;

                structure.Add(smi);
            }
            #endregion

            var typeBuilder = Module.DefineType($"{type.FullName}<Generated>", 
                TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Class, type);

            var typeField = typeBuilder.DefineField("<>_type", typeof(Type), FieldAttributes.Private | FieldAttributes.InitOnly);
            var implField = typeBuilder.DefineField("<>_impl", typeof(Impl), FieldAttributes.Private | FieldAttributes.InitOnly);
            var parentField = typeBuilder.DefineField("<>_parent", typeof(IGeneratedStore), FieldAttributes.Private | FieldAttributes.InitOnly);

            #region Converter fields
            var uniqueConverterTypes = structure.Where(m => m.HasConverter).Select(m => m.Converter).Distinct().ToArray();
            var converterFields = new Dictionary<Type, FieldInfo>(uniqueConverterTypes.Length);

            foreach (var convType in uniqueConverterTypes)
            {
                var field = typeBuilder.DefineField($"<converter>_{convType}", convType, 
                    FieldAttributes.Private | FieldAttributes.InitOnly | FieldAttributes.Static);
                converterFields.Add(convType, field);

                foreach (var member in structure.Where(m => m.HasConverter && m.Converter == convType))
                    member.ConverterField = field;
            }
            #endregion

            #region Static constructor
            var cctor = typeBuilder.DefineConstructor(MethodAttributes.Static, CallingConventions.Standard, Type.EmptyTypes);
            {
                var il = cctor.GetILGenerator();
               
                foreach (var kvp in converterFields)
                {
                    var typeCtor = kvp.Key.GetConstructor(Type.EmptyTypes);
                    il.Emit(OpCodes.Newobj, typeCtor);
                    il.Emit(OpCodes.Stsfld, kvp.Value);
                }

                il.Emit(OpCodes.Ret);
            }
            #endregion

            #region Constructor
            var ctor = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new[] { typeof(IGeneratedStore) });
            {
                var il = ctor.GetILGenerator();

                il.Emit(OpCodes.Ldarg_0); // keep this at bottom of stack

                il.Emit(OpCodes.Dup);
                il.Emit(OpCodes.Call, baseCtor);

                il.Emit(OpCodes.Dup);
                il.Emit(OpCodes.Ldarg_1); // load parent
                il.Emit(OpCodes.Stfld, parentField);

                il.Emit(OpCodes.Dup);
                EmitTypeof(il, type);
                il.Emit(OpCodes.Stfld, typeField);

                var noImplLabel = il.DefineLabel();
                il.Emit(OpCodes.Ldarg_1);
                il.Emit(OpCodes.Brtrue, noImplLabel);
                il.Emit(OpCodes.Dup);
                il.Emit(OpCodes.Dup);
                il.Emit(OpCodes.Newobj, Impl.Ctor);
                il.Emit(OpCodes.Stfld, implField);
                il.MarkLabel(noImplLabel);

                var GetLocal = MakeGetLocal(il);

                foreach (var member in structure)
                {
                    EmitStore(il, member, il =>
                    {
                        EmitLoad(il, member); // load the member
                        EmitCorrectMember(il, member, false, true, GetLocal); // correct it
                    });
                }

                il.Emit(OpCodes.Pop);

                il.Emit(OpCodes.Ret);
                
            }
            #endregion

            const MethodAttributes propertyMethodAttr = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig;
            const MethodAttributes virtualPropertyMethodAttr = propertyMethodAttr | MethodAttributes.Virtual | MethodAttributes.Final;
            const MethodAttributes virtualMemberMethod = MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.Final;

            #region IGeneratedStore
            typeBuilder.AddInterfaceImplementation(typeof(IGeneratedStore));

            var IGeneratedStore_t = typeof(IGeneratedStore);
            var IGeneratedStore_GetImpl = IGeneratedStore_t.GetProperty(nameof(IGeneratedStore.Impl)).GetGetMethod();
            var IGeneratedStore_GetType = IGeneratedStore_t.GetProperty(nameof(IGeneratedStore.Type)).GetGetMethod();
            var IGeneratedStore_GetParent = IGeneratedStore_t.GetProperty(nameof(IGeneratedStore.Parent)).GetGetMethod();
            var IGeneratedStore_Serialize = IGeneratedStore_t.GetMethod(nameof(IGeneratedStore.Serialize));
            var IGeneratedStore_Deserialize = IGeneratedStore_t.GetMethod(nameof(IGeneratedStore.Deserialize));
            var IGeneratedStore_OnReload = IGeneratedStore_t.GetMethod(nameof(IGeneratedStore.OnReload));
            var IGeneratedStore_Changed = IGeneratedStore_t.GetMethod(nameof(IGeneratedStore.Changed));

            #region IGeneratedStore.OnReload
            var onReload = typeBuilder.DefineMethod($"<>{nameof(IGeneratedStore.OnReload)}", virtualMemberMethod, null, Type.EmptyTypes);
            typeBuilder.DefineMethodOverride(onReload, IGeneratedStore_OnReload);
            if (baseOnReload != null) typeBuilder.DefineMethodOverride(onReload, baseOnReload);

            {
                var il = onReload.GetILGenerator();

                if (baseOnReload != null)
                {
                    il.Emit(OpCodes.Ldarg_0); // load this
                    il.Emit(OpCodes.Tailcall);
                    il.Emit(OpCodes.Call, baseOnReload); // load impl field
                }
                il.Emit(OpCodes.Ret);
            }
            #endregion
            #region IGeneratedStore.Impl
            var implProp = typeBuilder.DefineProperty(nameof(IGeneratedStore.Impl), PropertyAttributes.None, typeof(Impl), null);
            var implPropGet = typeBuilder.DefineMethod($"<g>{nameof(IGeneratedStore.Impl)}", virtualPropertyMethodAttr, implProp.PropertyType, Type.EmptyTypes);
            implProp.SetGetMethod(implPropGet);
            typeBuilder.DefineMethodOverride(implPropGet, IGeneratedStore_GetImpl);

            {
                var il = implPropGet.GetILGenerator();

                il.Emit(OpCodes.Ldarg_0); // load this
                il.Emit(OpCodes.Ldfld, implField); // load impl field
                il.Emit(OpCodes.Ret);
            }
            #endregion
            #region IGeneratedStore.Type
            var typeProp = typeBuilder.DefineProperty(nameof(IGeneratedStore.Type), PropertyAttributes.None, typeof(Type), null);
            var typePropGet = typeBuilder.DefineMethod($"<g>{nameof(IGeneratedStore.Type)}", virtualPropertyMethodAttr, typeProp.PropertyType, Type.EmptyTypes);
            typeProp.SetGetMethod(typePropGet);
            typeBuilder.DefineMethodOverride(typePropGet, IGeneratedStore_GetType);

            {
                var il = typePropGet.GetILGenerator();

                il.Emit(OpCodes.Ldarg_0); // load this
                il.Emit(OpCodes.Ldfld, typeField); // load impl field
                il.Emit(OpCodes.Ret);
            }
            #endregion
            #region IGeneratedStore.Parent
            var parentProp = typeBuilder.DefineProperty(nameof(IGeneratedStore.Parent), PropertyAttributes.None, typeof(IGeneratedStore), null);
            var parentPropGet = typeBuilder.DefineMethod($"<g>{nameof(IGeneratedStore.Parent)}", virtualPropertyMethodAttr, parentProp.PropertyType, Type.EmptyTypes);
            parentProp.SetGetMethod(parentPropGet);
            typeBuilder.DefineMethodOverride(parentPropGet, IGeneratedStore_GetParent);

            {
                var il = parentPropGet.GetILGenerator();

                il.Emit(OpCodes.Ldarg_0); // load this
                il.Emit(OpCodes.Ldfld, parentField); // load impl field
                il.Emit(OpCodes.Ret);
            }
            #endregion
            #region IGeneratedStore.Serialize
            var serializeGen = typeBuilder.DefineMethod($"<>{nameof(IGeneratedStore.Serialize)}", virtualPropertyMethodAttr, IGeneratedStore_Serialize.ReturnType, Type.EmptyTypes);
            typeBuilder.DefineMethodOverride(serializeGen, IGeneratedStore_Serialize);

            { // this is non-locking because the only code that will call this will already own the correct lock
                var il = serializeGen.GetILGenerator();

                var Map_Add = typeof(Map).GetMethod(nameof(Map.Add));
                var mapLocal = il.DeclareLocal(typeof(Map));

                var GetLocal = MakeGetLocal(il);
                var valLocal = GetLocal(typeof(Value));

                il.Emit(OpCodes.Call, typeof(Value).GetMethod(nameof(Value.Map)));
                il.Emit(OpCodes.Stloc, mapLocal);

                foreach (var member in structure)
                {
                    EmitSerializeMember(il, member, GetLocal);
                    il.Emit(OpCodes.Stloc, valLocal);
                    il.Emit(OpCodes.Ldloc, mapLocal);
                    il.Emit(OpCodes.Ldstr, member.Name);
                    il.Emit(OpCodes.Ldloc, valLocal);
                    il.Emit(OpCodes.Call, Map_Add);
                }

                il.Emit(OpCodes.Ldloc, mapLocal);
                il.Emit(OpCodes.Ret);
            }
            #endregion
            #region IGeneratedStore.Deserialize
            var deserializeGen = typeBuilder.DefineMethod($"<>{nameof(IGeneratedStore.Deserialize)}", virtualPropertyMethodAttr, null,
                new[] { IGeneratedStore_Deserialize.GetParameters()[0].ParameterType });
            typeBuilder.DefineMethodOverride(deserializeGen, IGeneratedStore_Deserialize);

            { // this is non-locking because the only code that will call this will already own the correct lock
                var il = deserializeGen.GetILGenerator();

                var Map_t = typeof(Map);
                var Map_TryGetValue = Map_t.GetMethod(nameof(Map.TryGetValue));
                var Object_GetType = typeof(object).GetMethod(nameof(Object.GetType));

                var valueLocal = il.DeclareLocal(typeof(Value));
                var mapLocal = il.DeclareLocal(typeof(Map));

                var nonNull = il.DefineLabel();

                il.Emit(OpCodes.Ldarg_1);
                il.Emit(OpCodes.Brtrue, nonNull);

                EmitLogError(il, "Attempting to deserialize null", tailcall: true);
                il.Emit(OpCodes.Ret);

                il.MarkLabel(nonNull);
                il.Emit(OpCodes.Ldarg_1);
                il.Emit(OpCodes.Isinst, Map_t);
                il.Emit(OpCodes.Dup); // duplicate cloned value
                il.Emit(OpCodes.Stloc, mapLocal);
                var notMapError = il.DefineLabel();
                il.Emit(OpCodes.Brtrue, notMapError);
                // handle error
                EmitLogError(il, $"Invalid root for deserializing {type.FullName}", tailcall: true,
                    expected: il => EmitTypeof(il, Map_t), found: il =>
                    {
                        il.Emit(OpCodes.Ldarg_1);
                        il.Emit(OpCodes.Callvirt, Object_GetType);
                    });
                il.Emit(OpCodes.Ret);

                var nextLabel = notMapError;

                var GetLocal = MakeGetLocal(il);

                // head of stack is Map instance
                foreach (var member in structure)
                {
                    il.MarkLabel(nextLabel);
                    nextLabel = il.DefineLabel();
                    var endErrorLabel = il.DefineLabel();

                    il.Emit(OpCodes.Ldloc, mapLocal);
                    il.Emit(OpCodes.Ldstr, member.Name);
                    il.Emit(OpCodes.Ldloca_S, valueLocal);
                    il.Emit(OpCodes.Call, Map_TryGetValue);
                    il.Emit(OpCodes.Brtrue_S, endErrorLabel);

                    EmitLogError(il, $"Missing key {member.Name}", tailcall: false);
                    il.Emit(OpCodes.Br, nextLabel);

                    il.MarkLabel(endErrorLabel);

                    il.Emit(OpCodes.Ldloc_S, valueLocal);
                    EmitDeserializeMember(il, member, nextLabel, il => il.Emit(OpCodes.Ldloc_S, valueLocal), GetLocal);
                }

                il.MarkLabel(nextLabel);
                
                il.Emit(OpCodes.Ret);
            }
            #endregion
            #endregion

            #region IGeneratedStore<T>
            var IGeneratedStore_T_t = typeof(IGeneratedStore<>).MakeGenericType(type);
            typeBuilder.AddInterfaceImplementation(IGeneratedStore_T_t);

            var IGeneratedStore_T_CopyFrom = IGeneratedStore_T_t.GetMethod(nameof(IGeneratedStore<Config>.CopyFrom));

            #region IGeneratedStore<T>.CopyFrom
            var copyFrom = typeBuilder.DefineMethod($"<>{nameof(IGeneratedStore<Config>.CopyFrom)}", virtualMemberMethod, null, new[] { type, typeof(bool) });
            typeBuilder.DefineMethodOverride(copyFrom, IGeneratedStore_T_CopyFrom);

            {
                var il = copyFrom.GetILGenerator();

                var startLock = il.DefineLabel();
                il.Emit(OpCodes.Ldarg_2);
                il.Emit(OpCodes.Brfalse, startLock);
                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Call, Impl.ImplTakeWriteMethod); // take the write lock
                il.MarkLabel(startLock);

                var GetLocal = MakeGetLocal(il);

                foreach (var member in structure)
                {
                    il.BeginExceptionBlock();

                    EmitStore(il, member, il =>
                    {
                        EmitLoad(il, member, il => il.Emit(OpCodes.Ldarg_1));
                        EmitCorrectMember(il, member, false, false, GetLocal);
                    });

                    il.BeginCatchBlock(typeof(Exception));

                    EmitWarnException(il, $"Error while copying from member {member.Name}");

                    il.EndExceptionBlock();
                }

                var endLock = il.DefineLabel();
                il.Emit(OpCodes.Ldarg_2);
                il.Emit(OpCodes.Brfalse, endLock);
                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Call, Impl.ImplReleaseWriteMethod); // release write lock
                il.MarkLabel(endLock);
                il.Emit(OpCodes.Ret);
            }
            #endregion
            #endregion

            #region IConfigStore
            typeBuilder.AddInterfaceImplementation(typeof(IConfigStore));

            var IConfigStore_t = typeof(IConfigStore);
            var IConfigStore_GetSyncObject = IConfigStore_t.GetProperty(nameof(IConfigStore.SyncObject)).GetGetMethod();
            var IConfigStore_GetWriteSyncObject = IConfigStore_t.GetProperty(nameof(IConfigStore.WriteSyncObject)).GetGetMethod();
            var IConfigStore_WriteTo = IConfigStore_t.GetMethod(nameof(IConfigStore.WriteTo));
            var IConfigStore_ReadFrom = IConfigStore_t.GetMethod(nameof(IConfigStore.ReadFrom));

            #region IConfigStore.SyncObject
            var syncObjProp = typeBuilder.DefineProperty(nameof(IConfigStore.SyncObject), PropertyAttributes.None, typeof(WaitHandle), null);
            var syncObjPropGet = typeBuilder.DefineMethod($"<g>{nameof(IConfigStore.SyncObject)}", virtualPropertyMethodAttr, syncObjProp.PropertyType, Type.EmptyTypes);
            syncObjProp.SetGetMethod(syncObjPropGet);
            typeBuilder.DefineMethodOverride(syncObjPropGet, IConfigStore_GetSyncObject);

            {
                var il = syncObjPropGet.GetILGenerator();

                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Tailcall);
                il.Emit(OpCodes.Call, Impl.ImplGetSyncObjectMethod);
                il.Emit(OpCodes.Ret);
            }
            #endregion
            #region IConfigStore.WriteSyncObject
            var writeSyncObjProp = typeBuilder.DefineProperty(nameof(IConfigStore.WriteSyncObject), PropertyAttributes.None, typeof(WaitHandle), null);
            var writeSyncObjPropGet = typeBuilder.DefineMethod($"<g>{nameof(IConfigStore.WriteSyncObject)}", virtualPropertyMethodAttr, writeSyncObjProp.PropertyType, Type.EmptyTypes);
            writeSyncObjProp.SetGetMethod(writeSyncObjPropGet);
            typeBuilder.DefineMethodOverride(writeSyncObjPropGet, IConfigStore_GetWriteSyncObject);

            {
                var il = writeSyncObjPropGet.GetILGenerator();

                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Tailcall);
                il.Emit(OpCodes.Call, Impl.ImplGetWriteSyncObjectMethod);
                il.Emit(OpCodes.Ret);
            }
            #endregion
            #region IConfigStore.WriteTo
            var writeTo = typeBuilder.DefineMethod($"<>{nameof(IConfigStore.WriteTo)}", virtualMemberMethod, null, new[] { typeof(ConfigProvider) });
            typeBuilder.DefineMethodOverride(writeTo, IConfigStore_WriteTo);

            {
                var il = writeTo.GetILGenerator();

                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Ldarg_1);
                il.Emit(OpCodes.Tailcall);
                il.Emit(OpCodes.Call, Impl.ImplWriteToMethod);
                il.Emit(OpCodes.Ret);
            }
            #endregion
            #region IConfigStore.ReadFrom
            var readFrom = typeBuilder.DefineMethod($"<>{nameof(IConfigStore.ReadFrom)}", virtualMemberMethod, null, new[] { typeof(ConfigProvider) });
            typeBuilder.DefineMethodOverride(readFrom, IConfigStore_ReadFrom);

            {
                var il = readFrom.GetILGenerator();

                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Ldarg_1);
                il.Emit(OpCodes.Tailcall);
                il.Emit(OpCodes.Call, Impl.ImplReadFromMethod);
                il.Emit(OpCodes.Ret);
            }
            #endregion
            #endregion

            #region Changed
            var coreChanged = typeBuilder.DefineMethod(
                "<>Changed",
                virtualMemberMethod,
                null, Type.EmptyTypes);

            {
                var il = coreChanged.GetILGenerator();

                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Call, Impl.ImplSignalChangedMethod);
                il.Emit(OpCodes.Ret); // simply call our impl's SignalChanged method and return
            }

            if (baseChanged != null) {
                var changedMethod = typeBuilder.DefineMethod( // copy to override baseChanged
                    baseChanged.Name,
                    virtualMemberMethod, 
                    null, Type.EmptyTypes);
                typeBuilder.DefineMethodOverride(changedMethod, baseChanged);

                {
                    var il = changedMethod.GetILGenerator();

                    il.Emit(OpCodes.Ldarg_0);
                    il.Emit(OpCodes.Call, baseChanged); // call base

                    il.Emit(OpCodes.Ldarg_0);
                    il.Emit(OpCodes.Tailcall);
                    il.Emit(OpCodes.Call, coreChanged); // call back to the core change method

                    il.Emit(OpCodes.Ret);
                }

                coreChanged = changedMethod; // switch to calling this version instead of just the default
            }

            typeBuilder.DefineMethodOverride(coreChanged, IGeneratedStore_Changed);
            #endregion

            #region base.CopyFrom
            if (baseCopyFrom != null)
            {
                var pubCopyFrom = typeBuilder.DefineMethod(
                    baseCopyFrom.Name,
                    virtualMemberMethod,
                    null, new[] { type });
                typeBuilder.DefineMethodOverride(pubCopyFrom, baseCopyFrom);

                {
                    var il = pubCopyFrom.GetILGenerator();

                    // TODO: use transactional changes

                    il.Emit(OpCodes.Ldarg_0);
                    il.Emit(OpCodes.Ldarg_1);
                    il.Emit(OpCodes.Ldc_I4_1);
                    il.Emit(OpCodes.Call, copyFrom); // call internal

                    il.Emit(OpCodes.Ldarg_0);
                    il.Emit(OpCodes.Ldarg_1);
                    il.Emit(OpCodes.Call, baseCopyFrom); // call base

                    il.Emit(OpCodes.Ldarg_0);
                    il.Emit(OpCodes.Tailcall);
                    il.Emit(OpCodes.Call, coreChanged); // call changed
                    il.Emit(OpCodes.Ret);
                }
            }
            #endregion

            #region Members
            foreach (var member in structure.Where(m => m.IsVirtual))
            { // IsVirtual implies !IsField
                var prop = member.Member as PropertyInfo;
                var get = prop.GetGetMethod(true);
                var set = prop.GetSetMethod(true);

                var propBuilder = typeBuilder.DefineProperty($"{member.Name}#", PropertyAttributes.None, member.Type, null);
                var propGet = typeBuilder.DefineMethod($"<g>{propBuilder.Name}", virtualPropertyMethodAttr, member.Type, Type.EmptyTypes);
                propBuilder.SetGetMethod(propGet);
                typeBuilder.DefineMethodOverride(propGet, get);

                {
                    var il = propGet.GetILGenerator();

                    var local = il.DeclareLocal(member.Type);

                    il.Emit(OpCodes.Ldarg_0);
                    il.Emit(OpCodes.Call, Impl.ImplTakeReadMethod); // take the read lock

                    il.BeginExceptionBlock();

                    il.Emit(OpCodes.Ldarg_0);
                    il.Emit(OpCodes.Call, get); // call base getter
                    il.Emit(OpCodes.Stloc, local);

                    il.BeginFinallyBlock();

                    il.Emit(OpCodes.Ldarg_0);
                    il.Emit(OpCodes.Call, Impl.ImplReleaseReadMethod); // release the read lock

                    il.EndExceptionBlock();

                    il.Emit(OpCodes.Ldloc, local);
                    il.Emit(OpCodes.Ret);
                }

                var propSet = typeBuilder.DefineMethod($"<s>{propBuilder.Name}", virtualPropertyMethodAttr, null, new[] { member.Type });
                propBuilder.SetSetMethod(propSet);
                typeBuilder.DefineMethodOverride(propSet, set);

                { // TODO: decide if i want to correct the value before or after i take the write lock
                    var il = propSet.GetILGenerator();

                    var GetLocal = MakeGetLocal(il);

                    il.Emit(OpCodes.Ldarg_0);
                    il.Emit(OpCodes.Call, Impl.ImplTakeWriteMethod); // take the write lock

                    il.BeginExceptionBlock();

                    il.Emit(OpCodes.Ldarg_0);
                    il.Emit(OpCodes.Ldarg_1);
                    EmitCorrectMember(il, member, false, false, GetLocal);
                    il.Emit(OpCodes.Call, set);

                    il.BeginFinallyBlock();

                    il.Emit(OpCodes.Ldarg_0);
                    il.Emit(OpCodes.Call, Impl.ImplReleaseWriteMethod); // release the write lock

                    il.EndExceptionBlock();

                    il.Emit(OpCodes.Ldarg_0);
                    il.Emit(OpCodes.Call, Impl.ImplInvokeChangedMethod);

                    il.Emit(OpCodes.Ret);
                }

            }
            #endregion

            var genType = typeBuilder.CreateType();

            var parentParam = Expression.Parameter(typeof(IGeneratedStore), "parent");
            var creatorDel = Expression.Lambda<GeneratedStoreCreator>(
                Expression.New(ctor, parentParam), parentParam
            ).Compile();

            return (creatorDel, genType);
        }

        private delegate LocalBuilder GetLocal(Type type, int idx = 0);

        private static GetLocal MakeGetLocal(ILGenerator il)
        { // TODO: improve this shit a bit so that i can release a hold of a variable and do more auto managing
            var locals = new List<LocalBuilder>();

            LocalBuilder GetLocal(Type ty, int i = 0)
            {
                var builder = locals.Where(b => b.LocalType == ty).Skip(i).FirstOrDefault();
                if (builder == null)
                {
                    builder = il.DeclareLocal(ty);
                    locals.Add(builder);
                }
                return builder;
            }

            return GetLocal;
        }

        private static readonly MethodInfo LogErrorMethod = typeof(GeneratedStore).GetMethod(nameof(LogError), BindingFlags.NonPublic | BindingFlags.Static);
        internal static void LogError(Type expected, Type found, string message)
        {
            Logger.config.Notice($"{message}{(expected == null ? "" : $" (expected {expected}, found {found?.ToString() ?? "null"})")}");
        }
        private static readonly MethodInfo LogWarningMethod = typeof(GeneratedStore).GetMethod(nameof(LogWarning), BindingFlags.NonPublic | BindingFlags.Static);
        internal static void LogWarning(string message)
        {
            Logger.config.Warn(message);
        }
        private static readonly MethodInfo LogWarningExceptionMethod = typeof(GeneratedStore).GetMethod(nameof(LogWarningException), BindingFlags.NonPublic | BindingFlags.Static);
        internal static void LogWarningException(Exception exception)
        {
            Logger.config.Warn(exception);
        }


        private static bool NeedsCorrection(SerializedMemberInfo member)
        {
            var expectType = GetExpectedValueTypeForType(member.IsNullable ? member.NullableWrappedType : member.Type);

            if (expectType == typeof(Map)) // TODO: make this slightly saner
                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, GetLocal GetLocal)
        {
            if (!NeedsCorrection(member)) return;

            var endLabel = il.DefineLabel();

            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());
            }

            // TODO: impl the rest of this

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

            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, il => il.Emit(OpCodes.Ldarg_0));
                il.Emit(OpCodes.Dup);
                il.Emit(OpCodes.Isinst, typeof(IGeneratedStore));
                il.Emit(OpCodes.Brtrue_S, noCreate);
                il.Emit(OpCodes.Pop);
            }
            EmitCreateChildGenerated(il, member.Type);
            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);

            // TODO: impl the rest of this

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

            il.MarkLabel(endLabel);
        }

        #region Utility
        private static void EmitLoad(ILGenerator il, SerializedMemberInfo member, Action<ILGenerator> thisarg = null)
        {
            if (thisarg == null)
                thisarg = il => il.Emit(OpCodes.Ldarg_0);

            thisarg(il); // load this

            if (member.IsField)
                il.Emit(OpCodes.Ldfld, member.Member as FieldInfo);
            else
            { // member is a property
                var prop = member.Member as PropertyInfo;
                var getter = prop.GetGetMethod();
                if (getter == null) throw new InvalidOperationException($"Property {member.Name} does not have a getter and is not ignored");

                il.Emit(OpCodes.Call, getter);
            }
        }

        private static void EmitStore(ILGenerator il, SerializedMemberInfo member, Action<ILGenerator> value)
        {
            il.Emit(OpCodes.Ldarg_0); // load this
            value(il);

            if (member.IsField)
                il.Emit(OpCodes.Stfld, member.Member as FieldInfo);
            else
            { // member is a property
                var prop = member.Member as PropertyInfo;
                var setter = prop.GetSetMethod();
                if (setter == null) throw new InvalidOperationException($"Property {member.Name} does not have a setter and is not ignored");

                il.Emit(OpCodes.Call, setter);
            }
        }

        private static void EmitWarnException(ILGenerator il, string v)
        {
            il.Emit(OpCodes.Ldstr, v);
            il.Emit(OpCodes.Call, LogWarningMethod);
            il.Emit(OpCodes.Call, LogWarningExceptionMethod);
        }

        private static void EmitLogError(ILGenerator il, string message, bool tailcall = false, Action<ILGenerator> expected = null, Action<ILGenerator> found = null)
        {
            if (expected == null) expected = il => il.Emit(OpCodes.Ldnull);
            if (found == null) found = il => il.Emit(OpCodes.Ldnull);

            expected(il);
            found(il);
            il.Emit(OpCodes.Ldstr, message);
            if (tailcall) il.Emit(OpCodes.Tailcall);
            il.Emit(OpCodes.Call, LogErrorMethod);
        }

        private static readonly MethodInfo Type_GetTypeFromHandle = typeof(Type).GetMethod(nameof(Type.GetTypeFromHandle));
        private static void EmitTypeof(ILGenerator il, Type type)
        {
            il.Emit(OpCodes.Ldtoken, type);
            il.Emit(OpCodes.Call, Type_GetTypeFromHandle);
        }

        private static Type Decimal_t = typeof(decimal);
        private static ConstructorInfo Decimal_FromFloat = Decimal_t.GetConstructor(new[] { typeof(float) });
        private static ConstructorInfo Decimal_FromDouble = Decimal_t.GetConstructor(new[] { typeof(double) });
        private static ConstructorInfo Decimal_FromInt = Decimal_t.GetConstructor(new[] { typeof(int) });
        private static ConstructorInfo Decimal_FromUInt = Decimal_t.GetConstructor(new[] { typeof(uint) });
        private static ConstructorInfo Decimal_FromLong = Decimal_t.GetConstructor(new[] { typeof(long) });
        private static ConstructorInfo Decimal_FromULong = Decimal_t.GetConstructor(new[] { typeof(ulong) });
        private static void EmitNumberConvertTo(ILGenerator il, Type to, Type from)
        { // WARNING: THIS USES THE NO-OVERFLOW OPCODES
            if (to == from) return;
            if (to == Decimal_t)
            {
                if (from == typeof(float)) il.Emit(OpCodes.Newobj, Decimal_FromFloat);
                else if (from == typeof(double)) il.Emit(OpCodes.Newobj, Decimal_FromDouble);
                else if (from == typeof(long)) il.Emit(OpCodes.Newobj, Decimal_FromLong);
                else if (from == typeof(ulong)) il.Emit(OpCodes.Newobj, Decimal_FromULong);
                else if (from == typeof(int)) il.Emit(OpCodes.Newobj, Decimal_FromInt);
                else if (from == typeof(uint)) il.Emit(OpCodes.Newobj, Decimal_FromUInt);
                else if (from == typeof(IntPtr))
                {
                    EmitNumberConvertTo(il, typeof(long), from);
                    EmitNumberConvertTo(il, to, typeof(long));
                }
                else if (from == typeof(UIntPtr))
                {
                    EmitNumberConvertTo(il, typeof(ulong), from);
                    EmitNumberConvertTo(il, to, typeof(ulong));
                }
                else 
                { // if the source is anything else, we first convert to int because that can contain all other values
                    EmitNumberConvertTo(il, typeof(int), from);
                    EmitNumberConvertTo(il, to, typeof(int));
                };
            }
            else if (from == Decimal_t)
            {
                if (to == typeof(IntPtr))
                {
                    EmitNumberConvertTo(il, typeof(long), from);
                    EmitNumberConvertTo(il, to, typeof(long));
                }
                else if (to == typeof(UIntPtr))
                {
                    EmitNumberConvertTo(il, typeof(ulong), from);
                    EmitNumberConvertTo(il, to, typeof(ulong));
                }
                else
                {
                    var method = Decimal_t.GetMethod($"To{to.Name}"); // conveniently, this is the pattern of the to* names
                    il.Emit(OpCodes.Call, method);
                }
            }
            else if (to == typeof(IntPtr)) il.Emit(OpCodes.Conv_I);
            else if (to == typeof(UIntPtr)) il.Emit(OpCodes.Conv_U);
            else if (to == typeof(sbyte)) il.Emit(OpCodes.Conv_I1);
            else if (to == typeof(byte)) il.Emit(OpCodes.Conv_U1);
            else if (to == typeof(short)) il.Emit(OpCodes.Conv_I2);
            else if (to == typeof(ushort)) il.Emit(OpCodes.Conv_U2);
            else if (to == typeof(int)) il.Emit(OpCodes.Conv_I4);
            else if (to == typeof(uint)) il.Emit(OpCodes.Conv_U4);
            else if (to == typeof(long)) il.Emit(OpCodes.Conv_I8);
            else if (to == typeof(ulong)) il.Emit(OpCodes.Conv_U8);
            else if (to == typeof(float))
            {
                if (from == typeof(byte)
                 || from == typeof(ushort)
                 || from == typeof(uint)
                 || from == typeof(ulong)
                 || from == typeof(UIntPtr)) il.Emit(OpCodes.Conv_R_Un);
                il.Emit(OpCodes.Conv_R4);
            }
            else if (to == typeof(double))
            {
                if (from == typeof(byte)
                 || from == typeof(ushort)
                 || from == typeof(uint)
                 || from == typeof(ulong)
                 || from == typeof(UIntPtr)) il.Emit(OpCodes.Conv_R_Un);
                il.Emit(OpCodes.Conv_R8);
            }
        }

        private static void EmitCreateChildGenerated(ILGenerator il, Type childType)
        {
            var method = CreateGParent.MakeGenericMethod(childType);
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Call, method);
        }
        #endregion

        #region Serialize

        // emit takes no args, leaves Value at top of stack
        private static void EmitSerializeMember(ILGenerator il, SerializedMemberInfo member, GetLocal GetLocal)
        {
            EmitLoad(il, member);

            var endSerialize = il.DefineLabel();

            if (member.AllowNull)
            {
                var passedNull = il.DefineLabel();

                il.Emit(OpCodes.Dup);
                if (member.IsNullable)
                    il.Emit(OpCodes.Call, member.Nullable_HasValue.GetGetMethod());
                il.Emit(OpCodes.Brtrue, passedNull);

                il.Emit(OpCodes.Pop);
                il.Emit(OpCodes.Ldnull);
                il.Emit(OpCodes.Br, endSerialize);

                il.MarkLabel(passedNull);
            }

            if (member.IsNullable)
                il.Emit(OpCodes.Call, member.Nullable_Value.GetGetMethod());

            var targetType = GetExpectedValueTypeForType(member.Type);
            if (member.HasConverter)
            {
                var stlocal = GetLocal(member.Type);
                var valLocal = GetLocal(typeof(Value));

                il.Emit(OpCodes.Stloc, stlocal);
                il.BeginExceptionBlock();
                il.Emit(OpCodes.Ldsfld, member.ConverterField);
                il.Emit(OpCodes.Ldloc, stlocal);

                if (member.IsGenericConverter)
                {
                    var toValueBase = member.ConverterBase.GetMethod(nameof(ValueConverter<int>.ToValue),
                        new[] { member.ConverterTarget, typeof(object) });
                    var toValue = member.Converter.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
                        .FirstOrDefault(m => m.GetBaseDefinition() == toValueBase) ?? toValueBase;
                    il.Emit(OpCodes.Ldarg_0);
                    il.Emit(OpCodes.Call, toValue);
                }
                else
                {
                    var toValueBase = typeof(IValueConverter).GetMethod(nameof(IValueConverter.ToValue),
                        new[] { typeof(object), typeof(object) });
                    var toValue = member.Converter.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
                        .FirstOrDefault(m => m.GetBaseDefinition() == toValueBase) ?? toValueBase;
                    il.Emit(OpCodes.Box);
                    il.Emit(OpCodes.Ldarg_0);
                    il.Emit(OpCodes.Call, toValue);
                }

                il.Emit(OpCodes.Stloc, valLocal);
                il.BeginCatchBlock(typeof(Exception));
                EmitWarnException(il, "Error serializing member using converter");
                il.Emit(OpCodes.Ldnull);
                il.Emit(OpCodes.Stloc, valLocal);
                il.EndExceptionBlock();
                il.Emit(OpCodes.Ldloc, valLocal);
            }
            else if (targetType == typeof(Text))
            { // only happens when arg is a string or char
                var TextCreate = typeof(Value).GetMethod(nameof(Value.Text));
                if (member.Type == typeof(char))
                {
                    var strFromChar = typeof(char).GetMethod(nameof(char.ToString), new[] { typeof(char) });
                    il.Emit(OpCodes.Call, strFromChar);
                }
                il.Emit(OpCodes.Call, TextCreate);
            }
            else if (targetType == typeof(Boolean))
            {
                var BoolCreate = typeof(Value).GetMethod(nameof(Value.Bool));
                il.Emit(OpCodes.Call, BoolCreate);
            }
            else if (targetType == typeof(Integer))
            {
                var IntCreate = typeof(Value).GetMethod(nameof(Value.Integer));
                EmitNumberConvertTo(il, IntCreate.GetParameters()[0].ParameterType, member.Type);
                il.Emit(OpCodes.Call, IntCreate);
            }
            else if (targetType == typeof(FloatingPoint))
            {
                var FloatCreate = typeof(Value).GetMethod(nameof(Value.Float));
                EmitNumberConvertTo(il, FloatCreate.GetParameters()[0].ParameterType, member.Type);
                il.Emit(OpCodes.Call, FloatCreate);
            }
            else if (targetType == typeof(List))
            {
                // TODO: impl this (enumerables)
                il.Emit(OpCodes.Pop);
                il.Emit(OpCodes.Ldnull);
            }
            else if (targetType == typeof(Map))
            {
                // TODO: support other aggregate types

                // for now, we assume that its a generated type implementing IGeneratedStore
                var IGeneratedStore_Serialize = typeof(IGeneratedStore).GetMethod(nameof(IGeneratedStore.Serialize));
                il.Emit(OpCodes.Callvirt, IGeneratedStore_Serialize);
            }

            il.MarkLabel(endSerialize);
        }
        #endregion

        #region Deserialize

        private static Type GetExpectedValueTypeForType(Type valT)
        {
            if (typeof(Value).IsAssignableFrom(valT)) // this is a Value subtype
                return valT;
            if (valT == typeof(string)
             || valT == typeof(char)) return typeof(Text);
            if (valT == typeof(bool)) return typeof(Boolean);
            if (valT == typeof(byte)
             || valT == typeof(sbyte)
             || valT == typeof(short)
             || valT == typeof(ushort)
             || valT == typeof(int)
             || valT == typeof(uint)
             || valT == typeof(long)
             || valT == typeof(IntPtr)) return typeof(Integer);
            if (valT == typeof(float)
             || valT == typeof(double)
             || valT == typeof(decimal)
             || valT == typeof(ulong) // ulong gets put into this, because decimal can hold it
             || valT == typeof(UIntPtr)) return typeof(FloatingPoint); 
            if (typeof(IEnumerable).IsAssignableFrom(valT)) return typeof(List);

            // TODO: fill this out the rest of the way

            return typeof(Map); // default for various objects
        }

        private static void EmitDeserializeGeneratedValue(ILGenerator il, SerializedMemberInfo member, Type srcType, GetLocal GetLocal)
        {
            var IGeneratedStore_Deserialize = typeof(IGeneratedStore).GetMethod(nameof(IGeneratedStore.Deserialize));

            var valuel = GetLocal(srcType, 0);
            var noCreate = il.DefineLabel();

            il.Emit(OpCodes.Stloc, valuel);
            EmitLoad(il, member, il => il.Emit(OpCodes.Ldarg_0));
            il.Emit(OpCodes.Dup);
            il.Emit(OpCodes.Isinst, typeof(IGeneratedStore));
            il.Emit(OpCodes.Brtrue_S, noCreate);
            il.Emit(OpCodes.Pop);
            EmitCreateChildGenerated(il, member.Type);
            il.MarkLabel(noCreate);

            il.Emit(OpCodes.Dup);
            il.Emit(OpCodes.Ldloc, valuel);
            il.Emit(OpCodes.Callvirt, IGeneratedStore_Deserialize);
        }

        private static void EmitDeserializeNullable(ILGenerator il, SerializedMemberInfo member, Type expected, GetLocal GetLocal)
        {
            EmitDeserializeValue(il, member, member.NullableWrappedType, expected, GetLocal);
            il.Emit(OpCodes.Newobj, member.Nullable_Construct);
        }

        // top of stack is the Value to deserialize; the type will be as returned from GetExpectedValueTypeForType
        // after, top of stack will be thing to write to field
        private static void EmitDeserializeValue(ILGenerator il, SerializedMemberInfo member, Type targetType, Type expected, GetLocal GetLocal)
        {
            if (typeof(Value).IsAssignableFrom(targetType)) return; // do nothing

            if (expected == typeof(Text))
            {
                var getter = expected.GetProperty(nameof(Text.Value)).GetGetMethod();
                il.Emit(OpCodes.Call, getter);
                if (targetType == typeof(char))
                {
                    var strIndex = typeof(string).GetProperty("Chars").GetGetMethod(); // string's indexer is specially named Chars
                    il.Emit(OpCodes.Ldc_I4_0);
                    il.Emit(OpCodes.Call, strIndex);
                }
            }
            else if (expected == typeof(Boolean))
            {
                var getter = expected.GetProperty(nameof(Boolean.Value)).GetGetMethod();
                il.Emit(OpCodes.Call, getter);
            }
            else if (expected == typeof(Integer))
            {
                var getter = expected.GetProperty(nameof(Integer.Value)).GetGetMethod();
                il.Emit(OpCodes.Call, getter);
                EmitNumberConvertTo(il, targetType, getter.ReturnType);
            }
            else if (expected == typeof(FloatingPoint))
            {
                var getter = expected.GetProperty(nameof(FloatingPoint.Value)).GetGetMethod();
                il.Emit(OpCodes.Call, getter);
                EmitNumberConvertTo(il, targetType, getter.ReturnType);
            } // TODO: implement stuff for lists and maps of various types (probably call out somewhere else to figure out what to do)
            else if (expected == typeof(Map))
            {
                EmitDeserializeGeneratedValue(il, member, expected, GetLocal);
            }
            else
            {
                il.Emit(OpCodes.Pop);
                il.Emit(OpCodes.Ldnull);
            }
        }

        private static void EmitDeserializeConverter(ILGenerator il, SerializedMemberInfo member, Label nextLabel, GetLocal GetLocal)
        {
            var stlocal = GetLocal(typeof(Value));
            var valLocal = GetLocal(member.Type);

            il.Emit(OpCodes.Stloc, stlocal);
            il.BeginExceptionBlock();
            il.Emit(OpCodes.Ldsfld, member.ConverterField);
            il.Emit(OpCodes.Ldloc, stlocal);
            il.Emit(OpCodes.Ldarg_0);

            if (member.IsGenericConverter)
            {
                var fromValueBase = member.ConverterBase.GetMethod(nameof(ValueConverter<int>.FromValue),
                    new[] { typeof(Value), typeof(object) });
                var fromValue = member.Converter.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
                    .FirstOrDefault(m => m.GetBaseDefinition() == fromValueBase) ?? fromValueBase;
                il.Emit(OpCodes.Call, fromValue);
            }
            else
            {
                var fromValueBase = typeof(IValueConverter).GetMethod(nameof(IValueConverter.FromValue),
                    new[] { typeof(Value), typeof(object) });
                var fromValue = member.Converter.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
                    .FirstOrDefault(m => m.GetBaseDefinition() == fromValueBase) ?? fromValueBase;
                il.Emit(OpCodes.Call, fromValue);
                if (member.Type.IsValueType)
                    il.Emit(OpCodes.Unbox);
            }

            il.Emit(OpCodes.Stloc, valLocal);
            il.BeginCatchBlock(typeof(Exception));
            EmitWarnException(il, "Error occurred while deserializing");
            il.Emit(OpCodes.Leave, nextLabel);
            il.EndExceptionBlock();
            il.Emit(OpCodes.Ldloc, valLocal);
        }

        // emit takes the value being deserialized, logs on error, leaves nothing on stack
        private static void EmitDeserializeMember(ILGenerator il, SerializedMemberInfo member, Label nextLabel, Action<ILGenerator> getValue, GetLocal GetLocal)
        {
            var Object_GetType = typeof(object).GetMethod(nameof(Object.GetType));

            var implLabel = il.DefineLabel();
            var passedTypeCheck = il.DefineLabel();
            var expectType = GetExpectedValueTypeForType(member.IsNullable ? member.NullableWrappedType : member.Type);

            il.Emit(OpCodes.Dup);
            il.Emit(OpCodes.Brtrue_S, implLabel); // null check

            if (!member.AllowNull)
            {
                il.Emit(OpCodes.Pop);
                EmitLogError(il, $"Member {member.Name} ({member.Type}) not nullable", tailcall: false,
                    expected: il => EmitTypeof(il, expectType));
                il.Emit(OpCodes.Br, nextLabel);
            }
            else if (member.IsNullable)
            {
                il.Emit(OpCodes.Pop);
                var valTLocal = GetLocal(member.Type, 0);
                il.Emit(OpCodes.Ldloca, valTLocal);
                il.Emit(OpCodes.Initobj, member.Type);
                EmitStore(il, member, il => il.Emit(OpCodes.Ldloc, valTLocal));
                il.Emit(OpCodes.Br, nextLabel);
            }
            else
            {
                il.Emit(OpCodes.Pop);
                EmitStore(il, member, il => il.Emit(OpCodes.Ldnull));
                il.Emit(OpCodes.Br, nextLabel);
            }


            if (!member.HasConverter)
            {
                il.MarkLabel(implLabel);
                il.Emit(OpCodes.Isinst, expectType); //replaces on stack
                il.Emit(OpCodes.Dup); // duplicate cloned value
                il.Emit(OpCodes.Brtrue, passedTypeCheck); // null check
            }

            var errorHandle = il.DefineLabel();

            // special cases to handle coersion between Float and Int
            if (member.HasConverter)
                il.MarkLabel(implLabel);
            else if (expectType == typeof(FloatingPoint))
            {
                var specialTypeCheck = il.DefineLabel();
                il.Emit(OpCodes.Pop);
                getValue(il);
                il.Emit(OpCodes.Isinst, typeof(Integer)); //replaces on stack
                il.Emit(OpCodes.Dup); // duplicate cloned value
                il.Emit(OpCodes.Brfalse, errorHandle); // null check

                var Integer_CoerceToFloat = typeof(Integer).GetMethod(nameof(Integer.AsFloat));
                il.Emit(OpCodes.Call, Integer_CoerceToFloat);

                il.Emit(OpCodes.Br, passedTypeCheck);
            }
            else if (expectType == typeof(Integer))
            {
                var specialTypeCheck = il.DefineLabel();
                il.Emit(OpCodes.Pop);
                getValue(il);
                il.Emit(OpCodes.Isinst, typeof(FloatingPoint)); //replaces on stack
                il.Emit(OpCodes.Dup); // duplicate cloned value
                il.Emit(OpCodes.Brfalse, errorHandle); // null check

                var Float_CoerceToInt = typeof(FloatingPoint).GetMethod(nameof(FloatingPoint.AsInteger));
                il.Emit(OpCodes.Call, Float_CoerceToInt);

                il.Emit(OpCodes.Br, passedTypeCheck);
            }

            if (!member.HasConverter)
            {
                il.MarkLabel(errorHandle);
                il.Emit(OpCodes.Pop);
                EmitLogError(il, $"Unexpected type deserializing {member.Name}", tailcall: false,
                    expected: il => EmitTypeof(il, expectType), found: il =>
                    {
                        getValue(il);
                        il.Emit(OpCodes.Callvirt, Object_GetType);
                    });
                il.Emit(OpCodes.Br, nextLabel);
            }

            il.MarkLabel(passedTypeCheck);

            var local = GetLocal(member.Type, 0);
            if (member.HasConverter) EmitDeserializeConverter(il, member, nextLabel, GetLocal);
            else if (member.IsNullable) EmitDeserializeNullable(il, member, expectType, GetLocal);
            else EmitDeserializeValue(il, member, member.Type, expectType, GetLocal);
            il.Emit(OpCodes.Stloc, local);
            EmitStore(il, member, il => il.Emit(OpCodes.Ldloc, local));
        }
        #endregion
    }
}
