using ObjectLayoutInspector;
using System.Collections.Concurrent;
using System.Reflection;
using System.Reflection.Emit;

namespace App
{
    public class SizeCalculator
    {
        private static readonly ConcurrentDictionary<Type, int> _sizes = new();
        private static readonly MethodInfo _getDefaultMethod = typeof(SizeCalculator).GetMethod(nameof(GetDefault), BindingFlags.Static | BindingFlags.NonPublic)!;
        public static readonly SizeCalculator Instance = new();
        public int SizeOf(Type type, Func<object?>? instanceAccessor = null)
        {
            if (_sizes.TryGetValue(type, out var size)) return size;
            if (type.IsValueType) return _sizes.GetOrAdd(type, CalculateValueTypeInstance);

            object? instance;
            try
            {
                instance = instanceAccessor?.Invoke() ?? Activator.CreateInstance(type);
            }
            catch
            {
                throw new InvalidOperationException("The delegate to get instance must be specified.");
            }

            return _sizes.GetOrAdd(type, type => CalculateReferenceTypeInstance(type, instance));
        }
        public int SizeOf<T>(Func<T>? instanceAccessor = null)
        {
            if (instanceAccessor is null) return SizeOf(typeof(T));
            Func<object?> accessor = () => instanceAccessor();
            return SizeOf(typeof(T), accessor);
        }
        public int CalculateValueTypeInstance(Type type)
        {
            var instance = GetDefaultAsObject(type);
            var fields = type.GetFields(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
               .Where(it => !it.IsStatic)
               .ToArray();

            if (fields.Length == 0) return 0;
            var tupleType = typeof(ValueTuple<,>).MakeGenericType(type, type);
            var tupple = tupleType.GetConstructors()[0].Invoke(new object?[] { instance, instance });
            var addresses = GenerateFieldAddressAccessor(tupleType.GetFields()).Invoke(tupple).OrderBy(it => it).ToArray();
            return (int)(addresses[2] - addresses[0]);
        }

        public int CalculateReferenceTypeInstance(Type type, object? instance)
        {
            var fields = GetBaseTypesAndThis(type)
               .SelectMany(type => type.GetFields(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
               .Where(it => !it.IsStatic).ToArray();

            if (fields.Length == 0) return type.IsValueType ? 0 : 3 * IntPtr.Size;
            var addresses = GenerateFieldAddressAccessor(fields).Invoke(instance);
            var list = new List<FieldInfo>(fields);
            list.Insert(0, null!);
            fields = list.ToArray();
            Array.Sort(addresses, fields);

            var lastFieldOffset = (int)(addresses.Last() - addresses.First());
            var lastField = fields.Last();
            var lastFieldSize = lastField.FieldType.IsValueType ? CalculateValueTypeInstance(lastField.FieldType) : IntPtr.Size;
            var size = lastFieldOffset + lastFieldSize;

            // Round up to IntPtr.Size
            int round = IntPtr.Size - 1;
            return ((size + round) & (~round)) + IntPtr.Size;

            static IEnumerable<Type> GetBaseTypesAndThis(Type? type)
            {
                while (type is not null)
                {
                    yield return type;
                    type = type.BaseType;
                }
            }
        }
        private static Func<object?, long[]> GenerateFieldAddressAccessor(FieldInfo[] fields)
        {
            var method = new DynamicMethod(
                name: "GetFieldAddresses",
                returnType: typeof(long[]),
                parameterTypes: new[] { typeof(object) },
                m: typeof(SizeCalculator).Module,
                skipVisibility: true);
            var ilGen = method.GetILGenerator();

            // var addresses = new long[fields.Length + 1];
            ilGen.DeclareLocal(typeof(long[]));
            ilGen.Emit(OpCodes.Ldc_I4, fields.Length + 1);
            ilGen.Emit(OpCodes.Newarr, typeof(long));
            ilGen.Emit(OpCodes.Stloc_0);

            // addresses[0] = address of instace;
            ilGen.Emit(OpCodes.Ldloc_0);
            ilGen.Emit(OpCodes.Ldc_I4, 0);
            ilGen.Emit(OpCodes.Ldarg_0);
            ilGen.Emit(OpCodes.Conv_I8);
            ilGen.Emit(OpCodes.Stelem_I8);

            // addresses[index] = address of field[index + 1];
            for (int index = 0; index < fields.Length; index++)
            {
                ilGen.Emit(OpCodes.Ldloc_0);
                ilGen.Emit(OpCodes.Ldc_I4, index + 1);
                ilGen.Emit(OpCodes.Ldarg_0);
                ilGen.Emit(OpCodes.Ldflda, fields[index]);
                ilGen.Emit(OpCodes.Conv_I8);
                ilGen.Emit(OpCodes.Stelem_I8);
            }

            ilGen.Emit(OpCodes.Ldloc_0);
            ilGen.Emit(OpCodes.Ret);

            return (Func<object?, long[]>)method.CreateDelegate(typeof(Func<object, long[]>));
        }
        private static T GetDefault<T>() where T : struct => default!;
        private static object? GetDefaultAsObject(Type type) => _getDefaultMethod.MakeGenericMethod(type).Invoke(null, Array.Empty<object>());
    }
}
