#if UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using MyBox.EditorTools;
using UnityEditor;

namespace MyBox.Internal
{
	public static class CustomDrawerUtility
	{
		/// <summary>
		/// Key is Associated with drawer type (the T in [CustomPropertyDrawer(typeof(T))])
		/// Value is PropertyDrawer Type
		/// </summary>
		private static readonly Dictionary<Type, Type> PropertyDrawersInAssembly = new Dictionary<Type, Type>();
		private static readonly Dictionary<int, PropertyDrawer> PropertyDrawersCache = new Dictionary<int, PropertyDrawer>();
		private static readonly string IgnoreScope = typeof(int).Module.ScopeName;

		/// <summary>
		/// Create PropertyDrawer for specified property if any PropertyDrawerType for such property is found.
		/// FieldInfo and Attribute will be inserted in created drawer.
		/// </summary>
		public static PropertyDrawer GetPropertyDrawerForProperty(SerializedProperty property, FieldInfo fieldInfo, Attribute attribute)
		{
			var propertyId = property.GetUniquePropertyId();
			if (PropertyDrawersCache.TryGetValue(propertyId, out var drawer)) return drawer;

			var targetType = fieldInfo.FieldType;
			var drawerType = GetPropertyDrawerTypeForFieldType(targetType);
			if (drawerType != null)
			{
				drawer = InstantiatePropertyDrawer(drawerType, fieldInfo, attribute);
				
				if (drawer == null) 
					WarningsPool.LogWarning(property, 
						$"Unable to instantiate CustomDrawer of type {drawerType} for {fieldInfo.FieldType}", 
						property.serializedObject.targetObject);
			}

			PropertyDrawersCache[propertyId] = drawer;
			return drawer;
		}
		
		public static PropertyDrawer InstantiatePropertyDrawer(Type drawerType, FieldInfo fieldInfo, Attribute insertAttribute)
		{
			try
			{
				var drawerInstance = (PropertyDrawer)Activator.CreateInstance(drawerType);
					
				// Reassign the attribute and fieldInfo fields in the drawer so it can access the argument values
				var fieldInfoField = drawerType.GetField("m_FieldInfo", BindingFlags.Instance | BindingFlags.NonPublic);
				if (fieldInfoField != null) fieldInfoField.SetValue(drawerInstance, fieldInfo);
				var attributeField = drawerType.GetField("m_Attribute", BindingFlags.Instance | BindingFlags.NonPublic);
				if (attributeField != null) attributeField.SetValue(drawerInstance, insertAttribute);

				return drawerInstance;
			}
			catch (Exception)
			{
				return null;
			}
		}
		
		/// <summary>
		/// Try to get PropertyDrawer for a target Type, or any Base Type for a target Type
		/// </summary>
		public static Type GetPropertyDrawerTypeForFieldType(Type drawerTarget)
		{
			// Ignore .net types from mscorlib.dll
			if (drawerTarget.Module.ScopeName.Equals(IgnoreScope)) return null;
			CacheDrawersInAssembly();
			
			// Of all property drawers in the assembly we need to find one that affects target type
			// or one of the base types of target type
			var checkType = drawerTarget;
			while (checkType != null)
			{
				if (PropertyDrawersInAssembly.TryGetValue(drawerTarget, out var drawer)) return drawer;
				checkType = checkType.BaseType;
			}

			return null;
		}

		private static Type[] GetTypesSafe(Assembly assembly)
		{
			try
			{
				return assembly.GetTypes();
			}
			catch (ReflectionTypeLoadException e)
			{
				return e.Types;
			}
		}
		
		private static void CacheDrawersInAssembly()
		{
			if (PropertyDrawersInAssembly.NotNullOrEmpty()) return;
			
			var propertyDrawerType = typeof(PropertyDrawer);
			var allDrawerTypesInDomain = AppDomain.CurrentDomain.GetAssemblies()
				.SelectMany(GetTypesSafe)
				.Where(t => t != null && propertyDrawerType.IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract);

			foreach (var drawerType in allDrawerTypesInDomain)
			{
				var propertyDrawerAttribute = CustomAttributeData.GetCustomAttributes(drawerType).FirstOrDefault();
				if (propertyDrawerAttribute == null) continue;
				var drawerTargetType = propertyDrawerAttribute.ConstructorArguments.FirstOrDefault().Value as Type;
				if (drawerTargetType == null) continue;

				if (PropertyDrawersInAssembly.ContainsKey(drawerTargetType)) continue;
				PropertyDrawersInAssembly.Add(drawerTargetType, drawerType);
			}
		}
	}
}
#endif