using CPF.Mac.Foundation;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.InteropServices;

namespace CPF.Mac.ObjCRuntime
{
    public class Class : INativeObject
    {
        [MonoNativeFunctionWrapper]
        private delegate int getFrameLengthDelegate(IntPtr @this, IntPtr sel);

        [MonoNativeFunctionWrapper]
        private delegate IntPtr addPropertyDelegate(IntPtr cls, string name, objc_attribute_prop[] attributes, int count);

        private struct objc_attribute_prop
        {
            [MarshalAs(UnmanagedType.LPStr)]
            internal string name;

            [MarshalAs(UnmanagedType.LPStr)]
            internal string value;
        }

        public static bool ThrowOnInitFailure = true;

        private static Dictionary<IntPtr, Type> type_map = new Dictionary<IntPtr, Type>();

        private static Dictionary<Type, Type> custom_types = new Dictionary<Type, Type>();

        private static List<Delegate> method_wrappers = new List<Delegate>();

        private static object lock_obj = new object();

        internal IntPtr handle;

        private static IntPtr memory;

        private static int size_left;

        private static getFrameLengthDelegate getFrameLength = Selector.GetFrameLength;

        private static IntPtr getFrameLengthPtr = Marshal.GetFunctionPointerForDelegate(getFrameLength);

        private static addPropertyDelegate addProperty;

        private static bool addPropertyInitialized;

        public IntPtr Handle => handle;

        public IntPtr SuperClass => class_getSuperclass(handle);

        public string Name => Messaging.StringFromNativeUtf8(class_getName(handle));

        public Class(string name)
        {
            handle = objc_getClass(name);
            if (handle == IntPtr.Zero)
            {
                throw new ArgumentException($"name {name} is an unknown class", "name");
            }
        }

        public Class(Type type)
        {
            handle = Register(type);
        }

        public Class(IntPtr handle)
        {
            this.handle = handle;
        }

        internal static Class Construct(IntPtr handle)
        {
            return new Class(handle);
        }

        internal static string GetName(IntPtr @class)
        {
            return Messaging.StringFromNativeUtf8(class_getName(@class));
        }

        public static IntPtr GetHandle(string name)
        {
            return objc_getClass(name);
        }

        public static IntPtr GetHandle(Type type)
        {
            RegisterAttribute registerAttribute = (RegisterAttribute)Attribute.GetCustomAttribute(type, typeof(RegisterAttribute), inherit: false);
            string name = (registerAttribute == null) ? type.FullName : (registerAttribute.Name ?? type.FullName);
            bool is_wrapper = registerAttribute?.IsWrapper ?? false;
            IntPtr intPtr = objc_getClass(name);
            if (intPtr == IntPtr.Zero)
            {
                intPtr = Register(type, name, is_wrapper);
            }
            return intPtr;
        }

        public static bool IsCustomType(Type type)
        {
            lock (lock_obj)
            {
                return custom_types.ContainsKey(type);
            }
        }

        internal static Type Lookup(IntPtr klass)
        {
            return Lookup(klass, throw_on_error: true);
        }

        internal static Type Lookup(IntPtr klass, bool throw_on_error)
        {
            lock (lock_obj)
            {
                if (type_map.TryGetValue(klass, out Type value))
                {
                    return value;
                }
                IntPtr key = klass;
                while (true)
                {
                    IntPtr intPtr = class_getSuperclass(klass);
                    if (type_map.TryGetValue(intPtr, out value))
                    {
                        type_map[key] = value;
                        return value;
                    }
                    if (intPtr == IntPtr.Zero)
                    {
                        break;
                    }
                    klass = intPtr;
                }
                if (throw_on_error)
                {
                    throw new ArgumentException("Could not find a valid superclass for type " + new Class(key).Name + ". Did you forget to register the bindings at " + typeof(Class).FullName + ".Register() or call NSApplication.Init()?");
                }
                return null;
            }
        }

        internal static IntPtr Register(Type type)
        {
            RegisterAttribute registerAttribute = (RegisterAttribute)Attribute.GetCustomAttribute(type, typeof(RegisterAttribute), inherit: false);
            string name = (registerAttribute == null) ? type.FullName : (registerAttribute.Name ?? type.FullName);
            bool is_wrapper = registerAttribute?.IsWrapper ?? false;
            return Register(type, name, is_wrapper);
        }

        private static IntPtr Register(Type type, string name, bool is_wrapper)
        {
            IntPtr zero = IntPtr.Zero;
            IntPtr zero2 = IntPtr.Zero;
            zero2 = objc_getClass(name);
            lock (lock_obj)
            {
                if (zero2 != IntPtr.Zero)
                {
                    if (!type_map.ContainsKey(zero2))
                    {
                        type_map[zero2] = type;
                    }
                    return zero2;
                }
                if (objc_getProtocol(name) != IntPtr.Zero)
                {
                    throw new ArgumentException("Attempting to register a class named: " + name + " which is a valid protocol");
                }
                if (is_wrapper)
                {
                    return IntPtr.Zero;
                }
                Type baseType = type.BaseType;
                string text = null;
                while (Attribute.IsDefined(baseType, typeof(ModelAttribute), inherit: false))
                {
                    baseType = baseType.BaseType;
                }
                RegisterAttribute registerAttribute = (RegisterAttribute)Attribute.GetCustomAttribute(baseType, typeof(RegisterAttribute), inherit: false);
                text = ((registerAttribute == null) ? baseType.FullName : (registerAttribute.Name ?? baseType.FullName));
                zero = objc_getClass(text);
                if (zero == IntPtr.Zero && (baseType.Assembly != NSObject.MonoMacAssembly || typeof(NotMonoMac).IsAssignableFrom(baseType)))
                {
                    bool is_wrapper2 = registerAttribute?.IsWrapper ?? false;
                    Register(baseType, text, is_wrapper2);
                    zero = objc_getClass(text);
                }
                if (zero == IntPtr.Zero)
                {
                    zero = objc_getClass("NSObject");
                }
                zero2 = objc_allocateClassPair(zero, name, IntPtr.Zero);
                PropertyInfo[] properties = type.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
                foreach (PropertyInfo propertyInfo in properties)
                {
                    ConnectAttribute connectAttribute = (ConnectAttribute)Attribute.GetCustomAttribute(propertyInfo, typeof(ConnectAttribute));
                    if (connectAttribute != null)
                    {
                        string name2 = connectAttribute.Name ?? propertyInfo.Name;
                        class_addIvar(zero2, name2, (IntPtr)Marshal.SizeOf(typeof(IntPtr)), (ushort)Math.Log(Marshal.SizeOf(typeof(IntPtr)), 2.0), "@");
                    }
                    RegisterProperty(propertyInfo, type, zero2);
                }
                NSObject.OverrideRetainAndRelease(zero2);
                MethodInfo[] methods = type.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
                for (int i = 0; i < methods.Length; i++)
                {
                    RegisterMethod(methods[i], type, zero2);
                }
                ConstructorInfo constructor = type.GetConstructor(Type.EmptyTypes);
                if (constructor != null)
                {
                    NativeConstructorBuilder nativeConstructorBuilder = new NativeConstructorBuilder(constructor);
                    class_addMethod(zero2, nativeConstructorBuilder.Selector, nativeConstructorBuilder.Delegate, nativeConstructorBuilder.Signature);
                    method_wrappers.Add(nativeConstructorBuilder.Delegate);
                }
                ConstructorInfo[] constructors = type.GetConstructors(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
                foreach (ConstructorInfo constructorInfo in constructors)
                {
                    if ((ExportAttribute)Attribute.GetCustomAttribute(constructorInfo, typeof(ExportAttribute)) != null)
                    {
                        NativeConstructorBuilder nativeConstructorBuilder2 = new NativeConstructorBuilder(constructorInfo);
                        class_addMethod(zero2, nativeConstructorBuilder2.Selector, nativeConstructorBuilder2.Delegate, nativeConstructorBuilder2.Signature);
                        method_wrappers.Add(nativeConstructorBuilder2.Delegate);
                    }
                }
                var inter = type.GetInterfaces();
                if (inter != null)
                {
                    foreach (var item in inter)
                    {
                        if (item.GetCustomAttribute<ProtocolAttribute>() != null)
                        {
                            var prot = objc_getProtocol(item.Name);
                            if (prot == null)
                            {
                                throw new Exception("未找到Protocol：" + item.Name);
                            }
                            class_addProtocol(zero2, prot);
                        }
                    }
                }
                objc_registerClassPair(zero2);
                type_map[zero2] = type;
                custom_types.Add(type, type);
                return zero2;
            }
        }

        internal static void RegisterProperty(PropertyInfo prop, Type type, IntPtr handle)
        {
            ExportAttribute exportAttribute = (ExportAttribute)Attribute.GetCustomAttribute(prop, typeof(ExportAttribute));
            if (exportAttribute != null)
            {
                if (prop.PropertyType.IsGenericType || prop.PropertyType.IsGenericTypeDefinition)
                {
                    throw new ArgumentException($"Cannot export the property '{prop.DeclaringType.FullName}.{prop.Name}': it is generic.");
                }
                MethodInfo getMethod = prop.GetGetMethod(nonPublic: true);
                if (getMethod != null)
                {
                    RegisterMethod(getMethod, exportAttribute.ToGetter(prop), type, handle);
                }
                getMethod = prop.GetSetMethod(nonPublic: true);
                if (getMethod != null)
                {
                    RegisterMethod(getMethod, exportAttribute.ToSetter(prop), type, handle);
                }
                int count = 0;
                objc_attribute_prop[] array = new objc_attribute_prop[3];
                objc_attribute_prop objc_attribute_prop = array[count++] = new objc_attribute_prop
                {
                    name = "T",
                    value = TypeConverter.ToNative(prop.PropertyType)
                };
                switch (exportAttribute.ArgumentSemantic)
                {
                    case ArgumentSemantic.Copy:
                        {
                            int num2 = count++;
                            objc_attribute_prop = new objc_attribute_prop
                            {
                                name = "C",
                                value = ""
                            };
                            array[num2] = objc_attribute_prop;
                            break;
                        }
                    case ArgumentSemantic.Retain:
                        {
                            int num = count++;
                            objc_attribute_prop = new objc_attribute_prop
                            {
                                name = "&",
                                value = ""
                            };
                            array[num] = objc_attribute_prop;
                            break;
                        }
                }
                objc_attribute_prop = (array[count++] = new objc_attribute_prop
                {
                    name = "V",
                    value = exportAttribute.Selector
                });
                class_addProperty(handle, exportAttribute.Selector, array, count);
            }
        }

        internal static void RegisterMethod(MethodInfo minfo, Type type, IntPtr handle)
        {
            ExportAttribute exportAttribute = (ExportAttribute)Attribute.GetCustomAttribute(minfo.GetBaseDefinition(), typeof(ExportAttribute));
            if (exportAttribute != null && (!minfo.IsVirtual || !(minfo.DeclaringType != type) || (minfo.DeclaringType.Assembly != NSObject.MonoMacAssembly || typeof(NotMonoMac).IsAssignableFrom(minfo.DeclaringType))))
            {
                RegisterMethod(minfo, exportAttribute, type, handle);
            }
        }

        private static IntPtr AllocExecMemory(int size)
        {
            if (size_left < size)
            {
                size_left = 4096;
                memory = Marshal.AllocHGlobal(size_left);
                if (memory == IntPtr.Zero)
                {
                    throw new Exception($"Could not allocate memory for specialized x86 floating point stret delegate thunk: {Marshal.GetLastWin32Error()}");
                }
                if (mprotect(memory, size_left, 7) != 0)
                {
                    throw new Exception($"Could not make allocated memory for specialized x86 floating point stret delegate thunk code executable: {Marshal.GetLastWin32Error()}");
                }
            }
            IntPtr result = memory;
            size_left -= size;
            memory = new IntPtr(memory.ToInt32() + size);
            return result;
        }

        private static bool TypeRequiresFloatingPointTrampoline(Type t)
        {
            if (IntPtr.Size != 4)
            {
                return false;
            }
            if (typeof(float) == t || typeof(double) == t)
            {
                return false;
            }
            if (!t.IsValueType || t.IsEnum)
            {
                return false;
            }
            if (Marshal.SizeOf(t) <= 8)
            {
                return false;
            }
            return TypeContainsFloatingPoint(t);
        }

        private static bool TypeContainsFloatingPoint(Type t)
        {
            if (!t.IsValueType || t.IsEnum || t.IsPrimitive)
            {
                return false;
            }
            FieldInfo[] fields = t.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
            foreach (FieldInfo fieldInfo in fields)
            {
                if (fieldInfo.FieldType == typeof(double) || fieldInfo.FieldType == typeof(float))
                {
                    return true;
                }
                if (!(fieldInfo.FieldType == t) && TypeContainsFloatingPoint(fieldInfo.FieldType))
                {
                    return true;
                }
            }
            return false;
        }

        private static IntPtr GetFunctionPointer(MethodInfo minfo, Delegate @delegate)
        {
            IntPtr functionPointerForDelegate = Marshal.GetFunctionPointerForDelegate(@delegate);
            if (!TypeRequiresFloatingPointTrampoline(minfo.ReturnType))
            {
                return functionPointerForDelegate;
            }
            IntPtr intPtr = AllocExecMemory(83);
            IntPtr intPtr2 = new IntPtr(functionPointerForDelegate.ToInt32() - intPtr.ToInt32() - 70);
            IntPtr intPtr3 = new IntPtr(getFrameLengthPtr.ToInt32() - intPtr.ToInt32() - 27);
            byte[] bytes = BitConverter.GetBytes(intPtr2.ToInt32());
            byte[] bytes2 = BitConverter.GetBytes(intPtr3.ToInt32());
            byte[] obj = new byte[83]
            {
                85,
                137,
                229,
                86,
                87,
                83,
                131,
                236,
                60,
                139,
                69,
                16,
                137,
                68,
                36,
                4,
                139,
                69,
                12,
                137,
                4,
                36,
                232,
                0,
                0,
                0,
                0,
                137,
                69,
                240,
                131,
                192,
                15,
                193,
                232,
                4,
                193,
                224,
                4,
                41,
                196,
                139,
                77,
                240,
                141,
                117,
                8,
                137,
                231,
                131,
                249,
                0,
                116,
                11,
                131,
                233,
                4,
                139,
                4,
                14,
                137,
                4,
                15,
                235,
                240,
                232,
                0,
                0,
                0,
                0,
                139,
                93,
                244,
                139,
                125,
                248,
                139,
                117,
                252,
                201,
                194,
                4,
                0
            };
            obj[23] = bytes2[0];
            obj[24] = bytes2[1];
            obj[25] = bytes2[2];
            obj[26] = bytes2[3];
            obj[66] = bytes[0];
            obj[67] = bytes[1];
            obj[68] = bytes[2];
            obj[69] = bytes[3];
            byte[] array = obj;
            Marshal.Copy(array, 0, intPtr, array.Length);
            return intPtr;
        }

        internal static void RegisterMethod(MethodInfo minfo, ExportAttribute ea, Type type, IntPtr handle)
        {
            NativeMethodBuilder nativeMethodBuilder = new NativeMethodBuilder(minfo, type, ea);
            class_addMethod(minfo.IsStatic ? object_getClass(handle) : handle, nativeMethodBuilder.Selector, GetFunctionPointer(minfo, nativeMethodBuilder.Delegate), nativeMethodBuilder.Signature);
            lock (lock_obj)
            {
                method_wrappers.Add(nativeMethodBuilder.Delegate);
            }
        }

        [DllImport("libc", SetLastError = true)]
        private static extern int mprotect(IntPtr addr, int len, int prot);

        [DllImport("libc", SetLastError = true)]
        private static extern IntPtr mmap(IntPtr start, ulong length, int prot, int flags, int fd, long offset);

        [DllImport("/usr/lib/libobjc.dylib")]
        internal static extern IntPtr objc_allocateClassPair(IntPtr superclass, string name, IntPtr extraBytes);

        [DllImport("/usr/lib/libobjc.dylib")]
        internal static extern IntPtr objc_getClass(string name);

        [DllImport("/usr/lib/libobjc.dylib")]
        internal static extern IntPtr objc_getProtocol(string name);

        [DllImport("/usr/lib/libobjc.dylib")]
        internal static extern bool class_addProtocol(IntPtr cls, IntPtr protocol);

        [DllImport("/usr/lib/libobjc.dylib")]
        private static extern void objc_registerClassPair(IntPtr cls);

        [DllImport("/usr/lib/libobjc.dylib")]
        private static extern bool class_addIvar(IntPtr cls, string name, IntPtr size, ushort alignment, string types);

        [DllImport("/usr/lib/libobjc.dylib")]
        internal static extern bool class_addMethod(IntPtr cls, IntPtr name, Delegate imp, string types);

        [DllImport("/usr/lib/libobjc.dylib")]
        internal static extern bool class_addMethod(IntPtr cls, IntPtr name, IntPtr imp, string types);

        [DllImport("/usr/lib/libobjc.dylib")]
        private static extern IntPtr class_getName(IntPtr cls);

        [DllImport("/usr/lib/libobjc.dylib")]
        internal static extern IntPtr class_getSuperclass(IntPtr cls);

        [DllImport("/usr/lib/libobjc.dylib")]
        internal static extern IntPtr object_getClass(IntPtr obj);

        [DllImport("/usr/lib/libobjc.dylib")]
        internal static extern IntPtr class_getMethodImplementation(IntPtr cls, IntPtr sel);

        [DllImport("/usr/lib/libobjc.dylib")]
        internal static extern IntPtr class_getInstanceVariable(IntPtr cls, string name);

        private static IntPtr class_addProperty(IntPtr cls, string name, objc_attribute_prop[] attributes, int count)
        {
            if (!addPropertyInitialized)
            {
                IntPtr intPtr = Dlfcn.dlopen("/usr/lib/libobjc.dylib", 0);
                try
                {
                    IntPtr intPtr2 = Dlfcn.dlsym(intPtr, "class_addProperty");
                    if (intPtr2 != IntPtr.Zero)
                    {
                        addProperty = (addPropertyDelegate)Marshal.GetDelegateForFunctionPointer(intPtr2, typeof(addPropertyDelegate));
                    }
                }
                finally
                {
                    Dlfcn.dlclose(intPtr);
                }
                addPropertyInitialized = true;
            }
            if (addProperty == null)
            {
                return IntPtr.Zero;
            }
            return addProperty(cls, name, attributes, count);
        }
        [DllImport("/usr/lib/libobjc.dylib")]
        internal static extern IntPtr objc_setAssociatedObject(IntPtr obj, IntPtr name, IntPtr value, long policy);

        internal enum objc_AssociationPolicy
        {
            OBJC_ASSOCIATION_ASSIGN = 0,
            OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
            OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
            OBJC_ASSOCIATION_RETAIN = 01401,
            OBJC_ASSOCIATION_COPY = 01403
        }
    }
}
