using Mono;
using System;
using System.IO;
using System.Collections;
using GlitchyEngine.Core;
using GlitchyEngine.Math;
using GlitchyEngine.World;
using System.Diagnostics;

namespace GlitchyEngine.Scripting;

using internal GlitchyEngine.Scripting;

static sealed class ScriptEngineHelper
{
	private static Dictionary<StringView, ScriptFieldType> _scriptFieldTypes = new Dictionary<StringView, ScriptFieldType>()
	{
		("System.String", .String),

		("System.Boolean", .Bool),
		("System.Char", .Char),

		("System.SByte", .SByte),
		("System.Int16", .Short),
		("System.Int32", .Int),
		("System.Int64", .Long),
		
		("System.Byte", .Byte),
		("System.UInt16", .UShort),
		("System.UInt32", .UInt),
		("System.UInt64", .ULong),

		("System.Single", .Float),
		("GlitchyEngine.Math.float2", .float2),
		("GlitchyEngine.Math.float3", .float3),
		("GlitchyEngine.Math.float4", .float4),

		("System.Double", .Double),

		("GlitchyEngine.Entity", .Entity),
	} ~ delete _;

	public static ScriptFieldType GetScriptFieldType(MonoType* monoType)
	{
		StringView typeName = StringView(Mono.mono_type_get_name(monoType));

		return _scriptFieldTypes.TryGetValue(typeName, .. let scriptFieldType);
	}
}

class EngineClasses
{
	private ScriptClass s_ComponentRoot ~ _?.ReleaseRef();
	private ScriptClass s_EntityRoot ~ _?.ReleaseRef();
	private ScriptClass s_EngineObject ~ _?.ReleaseRef();

	private ScriptClass s_EntityEditor ~ _?.ReleaseRef();

	private ScriptClass s_EntitySerializer ~ _?.ReleaseRef();
	private ScriptClass s_SerializationContext ~ _?.ReleaseRef();

	public ScriptClass ComponentRoot => s_ComponentRoot;
	public ScriptClass EntityRoot => s_EntityRoot;
	public ScriptClass EngineObject => s_EngineObject;

	public ScriptClass EntityEditor => s_EntityEditor;

	public ScriptClass EntitySerializer => s_EntitySerializer;
	//public ScriptClass SerializationContext => s_SerializationContext;

	internal void ReleaseAndNullify()
	{
		ReleaseRefAndNullify!(s_EngineObject);
		ReleaseRefAndNullify!(s_EntityRoot);
		ReleaseRefAndNullify!(s_ComponentRoot);

		ReleaseRefAndNullify!(s_EntityEditor);

		ReleaseRefAndNullify!(s_EntitySerializer);
		ReleaseRefAndNullify!(s_SerializationContext);
	}

	internal void LoadClasses(MonoImage* image)
	{
		ReleaseAndNullify();

		s_EngineObject = new ScriptClass("GlitchyEngine.Core", "EngineObject", image, .Class);
		s_EntityRoot = new ScriptClass("GlitchyEngine", "Entity", image, .Entity);
		s_ComponentRoot = new ScriptClass("GlitchyEngine.Core", "Component", image, .Component);

		// Editor classes
		s_EntityEditor = new ScriptClass("GlitchyEngine.Editor", "EntityEditor", image, .Class);

		s_EntitySerializer = new ScriptClass("GlitchyEngine.Serialization", "EntitySerializer", image, .Class);
		//s_SerializationContext = new ScriptClass("GlitchyEngine.Serialization", "EntitySerializer.SerializationContext", image, .Class);
	}
}

static class ScriptEngine
{
	private static MonoDomain* s_RootDomain;
	private static MonoDomain* s_AppDomain;

	private static MonoAssembly* s_CoreAssembly;
	private static MonoImage* s_CoreAssemblyImage;

	private static MonoAssembly* s_AppAssembly;
	private static MonoImage* s_AppAssemblyImage;

	private static Scene s_Context ~ _?.ReleaseRef();

	private static append EngineClasses _classes = .();

	public static EngineClasses Classes => _classes;

	private static Dictionary<StringView, SharpType> _sharpClasses = new .() ~ DeleteDictionaryAndReleaseValues!(_);
	
	private static Dictionary<StringView, ScriptClass> _entityScripts = new .() ~ DeleteDictionaryAndReleaseValues!(_);
	private static Dictionary<StringView, ScriptClass> _componentClasses = new .() ~ DeleteDictionaryAndReleaseValues!(_);
	
	private static Dictionary<UUID, ScriptInstance> _entityScriptInstances = new .() ~ {
		for (var entry in _)
		{
			entry.value?.ReleaseRef();
		}
		delete _;
	}

	public static Dictionary<StringView, ScriptClass> EntityClasses => _entityScripts;
	public static Dictionary<StringView, ScriptClass> ComponentClasses => _componentClasses;

	/// Gets or sets the current scene context for the runtime.
	public static Scene Context
	{
		get => s_Context;
		private set => SetReference!(s_Context, value);
	}
	
	private static FileSystemWatcher _userAssemblyWatcher ~ delete _;

	// TODO: This should be a global setting somewhere
	private static bool _debuggingEnabled = true;

	private static String _appAssemblyPath = new .() ~ delete _;

	internal static class Attributes
	{
		internal static MonoClass* s_ShowInEditorAttribute;
		internal static MonoClass* s_RunInEditModeAttribute;
	}

	public class ApplicationData
	{
		private bool _isEditor;
		private bool _isPlayer;

		private bool _isInEditMode;
		private bool _isInPlayMode;

		public bool IsEditor
		{
			get => _isEditor;
			set
			{
				_isEditor = value;

				if (_isEditor)
					_isPlayer = false;
			}
		}
		
		public bool IsPlayer
		{
			get => _isPlayer;
			set
			{
				_isPlayer = value;

				if (_isPlayer)
					_isEditor = false;
			}
		}
		
		public bool IsInEditMode
		{
			get => _isInEditMode;
			set
			{
				_isInEditMode = value;

				if (_isInEditMode)
					_isInPlayMode = false;
			}
		}

		public bool IsInPlayMode
		{
			get => _isInPlayMode;
			set
			{
				_isInPlayMode = value;

				if (_isInPlayMode)
					_isInEditMode = false;
			}
		}
	}

	private static ApplicationData _applicationData = new ApplicationData() ~ delete _;

	/// Contains information about the application context
	public static ApplicationData ApplicationInfo => _applicationData;

	public static void Init()
	{
		InitMono();

		LoadScriptAssemblies();
	}

	/// Changes the path to the current app assembly.
	public static void SetAppAssemblyPath(StringView appAssemblyPath)
	{
		_appAssemblyPath.Set(appAssemblyPath);

		ReloadAssemblies();
	}

	static void InitMono()
	{
		Mono.mono_set_assemblies_path("mono/lib/4.5");

		if (_debuggingEnabled)
		{
			char8*[2] options = .(
				  "--debugger-agent=transport=dt_socket,address=127.0.0.1:2550,server=y,suspend=n,loglevel=3,logfile=MonoDebugger.log",
				  "--soft-breakpoints"
				);

			Mono.mono_jit_parse_options(options.Count, &options);
			Mono.mono_debug_init(.Mono);
		}

		s_RootDomain = Mono.mono_jit_init("GlitchyEngineJITRuntime");
		Log.EngineLogger.Assert(s_RootDomain != null, "Failed to initialize mono root domain");

		if (_debuggingEnabled)
		{
			Mono.mono_debug_domain_create(s_RootDomain);
		}

		Mono.mono_thread_set_main(Mono.mono_thread_current());
	}
	
	static bool _requestingReload = false;

	static void InitAssemblyWatcher()
	{
		// We don't need a file system watcher, if we have nothing to watch...
		if (!File.Exists(_appAssemblyPath))
			return;

		String directory = scope .();
		Path.GetDirectoryPath(_appAssemblyPath, directory);

		/*if (_userAssemblyWatcher != null) // _userAssemblyWatcher.Directory != directory
		{
			delete _userAssemblyWatcher;
		}*/

		String fileName = scope .("*/");
		Path.GetFileName(_appAssemblyPath, fileName);

		if (_userAssemblyWatcher == null)
		{
			_userAssemblyWatcher = new FileSystemWatcher(directory, fileName);
			_userAssemblyWatcher.OnChanged.Add(new (fileName) =>
			{
				_userAssemblyWatcher.StopRaisingEvents();
				
				if (_requestingReload)
					return;

				_requestingReload = true;

				Log.EngineLogger.Info("Script reload requested.");

				Application.Instance.InvokeOnMainThread(new () =>
				{
					ReloadAssemblies();
					
					_requestingReload = false;
					_userAssemblyWatcher.StartRaisingEvents();
				});
	
			});
		}

		_userAssemblyWatcher.StartRaisingEvents();
	}

	static void LoadScriptAssemblies()
	{
		Debug.Profiler.ProfileFunction!();

		ScriptGlue.Init();

		CreateAppDomain("GlitchyEngineScriptRuntime");
		(s_CoreAssembly, s_CoreAssemblyImage) = LoadAssembly("resources/scripts/ScriptCore.dll", _debuggingEnabled);

		if (File.Exists(_appAssemblyPath))
			(s_AppAssembly, s_AppAssemblyImage) = LoadAssembly(_appAssemblyPath, _debuggingEnabled);
		else
		{
			s_AppAssembly = null;
			s_AppAssemblyImage = null;
		}

		GetCoreAttributes();
		Classes.LoadClasses(s_CoreAssemblyImage);

		ClearDictionaryAndReleaseValues!(_sharpClasses);
		ClearDictionaryAndReleaseValues!(_componentClasses);
		ClearDictionaryAndReleaseValues!(_entityScripts);
		
		GetComponentsFromAssemblies();

		GetEntitiesFromAssemblies();

		ScriptGlue.RegisterManagedComponents();

		InitAssemblyWatcher();
	}

	/// Starts the script runtime and sets the context scene.
	public static void StartRuntime(Scene context)
	{
		/// At the moment only set the context.

		Debug.Assert(s_Context == null, "StartRuntime was called twice without StopRuntime in between!");
		Context = context;
	}

	/// Stopts the script runtime and disposes of all script instances.
	public static void StopRuntime()
	{
		if (Context == null)
		{
			// There should be nothing to do, if we have no context
			Debug.Assert(_entityScriptInstances.Count == 0, "There are script instances but no context scene.");
			return;
		}

		for (let (id, instance) in _entityScriptInstances)
		{
			instance?.ReleaseRef();

			Entity entity = Context.GetEntityByID(id);

			// Remove Instance from script
			ScriptComponent* script = entity.GetComponent<ScriptComponent>();
			script.Instance = null;
		}
		_entityScriptInstances.Clear();

		Context = null;
	}

	/// Instantiates the entities script components script and runs the constructor.
	/// Disposes of and replaces the old instance, if one exists.
	public static bool InitializeInstance(Entity entity, ScriptComponent* script)
	{
		ScriptClass scriptClass = GetScriptClass(script.ScriptClassName);

		if (scriptClass == null)
			return false;

		UUID entityId = entity.UUID;

		script.Instance = new ScriptInstance(entityId, scriptClass);
		script.Instance..ReleaseRef();

		if (_entityScriptInstances.TryGetValue(entityId, let currentInstance))
			currentInstance.ReleaseRef();

		_entityScriptInstances[entityId] = script.Instance..AddRef();

		script.Instance.Instantiate(entityId);

		return true;
	}

	public static void DestroyInstance(Entity entity, ScriptComponent* script)
	{
		UUID entityId = entity.UUID;

		if (script.Instance != null)
			script.Instance = null;

		script.ScriptClassName = null;

		if (_entityScriptInstances.TryGetValue(entityId, let currentInstance))
			currentInstance.ReleaseRef();

		_entityScriptInstances[entityId] = null;
	}

	/// Returns an instance that can be used as a reference to the entity with the given ID in Scripts
	private static MonoObject* GetOrCreateScriptReferenceInstance(UUID entityId)
	{
		MonoObject* referencedEntity = GetManagedInstance(entityId);

		if (referencedEntity == null)
		{	 
			referencedEntity = Classes.EntityRoot.CreateInstance(entityId, let exception);
			
			if (exception != null)
				ScriptEngine.HandleMonoException(exception, null);
		}

		return referencedEntity;
	}

	/// Creates an instance of the given component class referencing the specified entity instance.
	private static MonoObject* CreateComponentReferenceInstance(ScriptClass componentClass, UUID id)//MonoObject* entityReferenceInstance)
	{
		MonoObject* componentInstance = componentClass.CreateInstance();

		MonoClassField* idField = Mono.mono_class_get_field_from_name(componentClass.[Friend]_monoClass, "_uuid");

		Classes.ComponentRoot.SetFieldValue<UUID>(componentInstance, idField, id);

		/*// TODO: We could cache the property, but this might be fine
		MonoProperty* entityProperty = Mono.mono_class_get_property_from_name(componentClass.[Friend]_monoClass, "Entity");

		MonoObject* exception = null;
		
//#unwarn
		//Mono.mono_property_set_value(entityProperty, componentInstance, (void**)&entityReferenceInstance, &exception);
#unwarn
		Mono.mono_property_set_value(entityProperty, componentInstance, (void**)&id, &exception);

		if (exception != null)
			ScriptEngine.HandleMonoException((MonoException*)exception, null);*/

		return componentInstance;
	}

	public static void CopyFieldsToInstance(ScriptComponent* targetScript, ScriptComponent* sourceScript, Dictionary<UUID, UUID> sourceIdToTargetId)
	{
		Debug.Profiler.ProfileFunction!();

		Log.EngineLogger.AssertDebug(targetScript.Instance.ScriptClass == sourceScript.Instance.ScriptClass);

		for (let (name, scriptField) in sourceScript.Instance.ScriptClass.Fields)
		{
			Debug.Profiler.ProfileScope!("Copy Field");

			targetScript.Instance.CopyFieldValue(scriptField, sourceScript.Instance);
			switch (scriptField.FieldType)
			{
			case .Entity:
				let sourceEntityReference = sourceScript.Instance.GetFieldValue<MonoObject*>(scriptField);

				MonoObject* referencedEntity = sourceEntityReference;

				if (sourceEntityReference != null)
				{
					let idField = Mono.mono_class_get_field_from_name(Classes.EntityRoot._monoClass, "_uuid");
					UUID sourceId = Classes.EntityRoot.GetFieldValue<UUID>(sourceEntityReference, idField);

					// Check if we need to translate, copy otherwise
					if (sourceIdToTargetId.TryGetValue(sourceId, let referencedId))
					{
						// On the C# side we actually differentiate between an Entity and the Script
						// in the sense that getting an entity and a script yields two different results (one creates a new Entity-Class instance, the other returns the actual instance).
						// But here its just easier to always use the script instance.
						// Obviously breaks once we support multiple scripts per entity.
						referencedEntity = GetOrCreateScriptReferenceInstance(referencedId);
					}
				}

				targetScript.Instance.SetFieldValue(scriptField, referencedEntity);
			case .Component:
				
				let sourceComponentReference = sourceScript.Instance.GetFieldValue<MonoObject*>(scriptField);
				
				MonoObject* componentInstance = sourceComponentReference;
				
				// Get or create entity reference
				if (sourceComponentReference != null)
				{
					let idField = Mono.mono_class_get_field_from_name(Classes.EngineObject._monoClass, "_uuid");
					UUID sourceId = Classes.EntityRoot.GetFieldValue<UUID>(sourceComponentReference, idField);
					
					MonoType* fieldMonoType = scriptField.GetMonoType();

					SharpType componentType = ScriptEngine.GetSharpType(fieldMonoType);

					var componentClass = ComponentClasses[componentType.FullName];

					if (sourceIdToTargetId.TryGetValue(sourceId, let targetId))
					{
						// Create reference for translated id
						componentInstance = CreateComponentReferenceInstance(componentClass, targetId);
					}
					
					componentType.ReleaseRef();
				}
				
				targetScript.Instance.SetFieldValue(scriptField, componentInstance);

			default:
				targetScript.Instance.CopyFieldValue(scriptField, sourceScript.Instance);
			}
		}
	}

	private static MonoAssembly* LoadCSharpAssembly(StringView assemblyPath, bool loadPDB = false)
	{
		Debug.Profiler.ProfileFunction!();

		List<uint8> data = new List<uint8>(1024);

		File.ReadAll(assemblyPath, data);
	
	    // NOTE: We can't use this image for anything other than loading the assembly because this image doesn't have a reference to the assembly
	    MonoImageOpenStatus status = .ImageInvalid;
	    MonoImage* image = Mono.mono_image_open_from_data_full(data.Ptr, (.)data.Count, true, &status, false);

		delete data;

	    if (status != .Ok)
	    {
	        char8* errorMessage = Mono.mono_image_strerror(status);

			Log.EngineLogger.Error($"Failed to load C# Assembly: \"{StringView(errorMessage)}\"");

	        return null;
	    }

		if (loadPDB)
		{
			String pdbPath = scope .();
			Path.ChangeExtension(assemblyPath, ".pdb", pdbPath);

			if (File.Exists(pdbPath))
			{
				Log.EngineLogger.Trace($"Loading PDB \"{pdbPath}\"...");

				List<uint8> pdbData = new List<uint8>(1024);

				let result = File.ReadAll(pdbPath, pdbData);

				if (result case .Err(let error))
				{
					Log.EngineLogger.Error("Failed to load PDB file ({error}).");
				}

				Mono.mono_debug_open_image_from_memory(image, pdbData.Ptr, (int32)pdbData.Count);

				delete pdbData;
			}
			else
			{
				Log.EngineLogger.Warning($"Debugging enabled but no PDB-File found \"{assemblyPath}\".");
			}
		}
	
	    MonoAssembly* assembly = Mono.mono_assembly_load_from_full(image, assemblyPath.ToScopeCStr!(), &status, 0);

	    Mono.mono_image_close(image);

	    return assembly;
	}

	static void CreateAppDomain(StringView name)
	{
		s_AppDomain = Mono.mono_domain_create_appdomain(name.ToScopeCStr!(), null);
		Mono.mono_domain_set(s_AppDomain, true);
	}

	static (MonoAssembly* assembly, MonoImage* image) LoadAssembly(StringView filepath, bool loadPDB = false)
	{
		Debug.Profiler.ProfileFunction!();

		MonoAssembly* assembly = LoadCSharpAssembly(filepath, loadPDB);
		MonoImage* image = Mono.mono_assembly_get_image(assembly);

		return (assembly, image);
	}

	/// Retrieves the classes for Attributes that are defined in the Core library
	static void GetCoreAttributes()
	{
		// We definitely need these attributes!
		// TODO: Some only for the editor!
		Attributes.s_ShowInEditorAttribute = Mono.mono_class_from_name(s_CoreAssemblyImage, "GlitchyEngine.Editor", "ShowInEditorAttribute");
		Debug.Assert(Attributes.s_ShowInEditorAttribute != null);
		Attributes.s_RunInEditModeAttribute = Mono.mono_class_from_name(s_CoreAssemblyImage, "GlitchyEngine.Editor", "RunInEditModeAttribute");
		Debug.Assert(Attributes.s_RunInEditModeAttribute != null);
	}

	private static void GetEntitiesFromAssemblies()
	{
		Debug.Profiler.ProfileFunction!();

		if (s_AppAssemblyImage == null)
			return;

	    MonoTableInfo* typeDefinitionsTable = Mono.mono_image_get_table_info(s_AppAssemblyImage, .MONO_TABLE_TYPEDEF);
	    int32 numTypes = Mono.mono_table_info_get_rows(typeDefinitionsTable);

	    for (int32 i = 0; i < numTypes; i++)
	    {
	        int32[(.)SOME_RANDOM_ENUM.MONO_TYPEDEF_SIZE] cols = .();
	        Mono.mono_metadata_decode_row(typeDefinitionsTable, i, (.)&cols, (.)SOME_RANDOM_ENUM.MONO_TYPEDEF_SIZE);

	        char8* nameSpace = Mono.mono_metadata_string_heap(s_AppAssemblyImage, (.)cols[(.)SOME_RANDOM_ENUM.MONO_TYPEDEF_NAMESPACE]);
			char8* name = Mono.mono_metadata_string_heap(s_AppAssemblyImage, (.)cols[(.)SOME_RANDOM_ENUM.MONO_TYPEDEF_NAME]);
			
			MonoClass* monoClass = Mono.mono_class_from_name(s_AppAssemblyImage, nameSpace, name);
			
			// Check if it is an entity
			if (monoClass != null && Mono.mono_class_is_subclass_of(monoClass, Classes.EntityRoot.[Friend]_monoClass, false))
			{
				ScriptClass entityScript = new ScriptClass(StringView(nameSpace), StringView(name), s_AppAssemblyImage, .Entity);
				_entityScripts.Add(entityScript.FullName, entityScript);

				Log.EngineLogger.Info($"Added entity \"{entityScript.FullName}\"");
			}
	    }
	}

	private static void GetComponentsFromAssemblies()
	{
		Debug.Profiler.ProfileFunction!();

	    MonoTableInfo* typeDefinitionsTable = Mono.mono_image_get_table_info(s_CoreAssemblyImage, .MONO_TABLE_TYPEDEF);
	    int32 numTypes = Mono.mono_table_info_get_rows(typeDefinitionsTable);

	    for (int32 i = 0; i < numTypes; i++)
	    {
	        int32[(.)SOME_RANDOM_ENUM.MONO_TYPEDEF_SIZE] cols = .();
	        Mono.mono_metadata_decode_row(typeDefinitionsTable, i, (.)&cols, (.)SOME_RANDOM_ENUM.MONO_TYPEDEF_SIZE);

	        char8* nameSpace = Mono.mono_metadata_string_heap(s_CoreAssemblyImage, (.)cols[(.)SOME_RANDOM_ENUM.MONO_TYPEDEF_NAMESPACE]);
			char8* name = Mono.mono_metadata_string_heap(s_CoreAssemblyImage, (.)cols[(.)SOME_RANDOM_ENUM.MONO_TYPEDEF_NAME]);
			
			MonoClass* monoClass = Mono.mono_class_from_name(s_CoreAssemblyImage, nameSpace, name);
			
			// Check if it is a component but not the root Component class
			if (monoClass != null && monoClass != Classes.ComponentRoot.[Friend]_monoClass && Mono.mono_class_is_subclass_of(monoClass, Classes.ComponentRoot.[Friend]_monoClass, false))
			{
				ScriptClass componentClass = new ScriptClass(StringView(nameSpace), StringView(name), s_CoreAssemblyImage, .Component);
				_componentClasses.Add(componentClass.FullName, componentClass);

				Log.EngineLogger.Info($"Added component \"{componentClass.FullName}\"");
			}
	    }
	}

	public static void ReloadAssemblies()
	{
		Debug.Profiler.ProfileFunction!();

		Log.EngineLogger.Info("Reloading script assemblies.");

		Dictionary<UUID, SerializedObject> serializedData = scope .();

		SerializeScriptInstances(serializedData);

		Mono.mono_domain_set(s_RootDomain, true);

		Mono.mono_domain_unload(s_AppDomain);

		LoadScriptAssemblies();

		{
			Debug.Profiler.ProfileScope!("Initialize Instances");
			
			// We need to create a new instance for every entity
			for (let (id, scriptInstance) in _entityScriptInstances)
			{
				if (Context.GetEntityByID(id) case .Ok(let entity))
				{
					ScriptComponent* script = entity.GetComponent<ScriptComponent>();
					InitializeInstance(entity, script);
				}
				else
				{
					Log.EngineLogger.AssertDebug(false, "Entities script was just serialized but the entity doesn't exist anymore.");
				}
			}
		}

		DeserializeScriptInstances(serializedData);

		ClearDictionaryAndDeleteValues!(serializedData);

		Log.EngineLogger.Info("Script assemblies reloaded!");
	}

	public static void Shutdown()
	{
		Mono.mono_domain_set(s_RootDomain, false);

		Mono.mono_domain_unload(s_AppDomain);
		s_AppDomain = null;

		Mono.mono_jit_cleanup(s_RootDomain);
		s_RootDomain = null;
	}

	internal static void RegisterSharpType(SharpType sharpType)
	{
		_sharpClasses.Add(sharpType.FullName, sharpType..AddRef());
	}

	internal static SharpType GetSharpType(MonoType* monoType)
	{
		StringView typeName = StringView(Mono.mono_type_get_name(monoType));

		if (_sharpClasses.TryGetValue(typeName, let sharpType))
			return sharpType..AddRef();

		Mono.MonoTypeEnum fieldType = Mono.Mono.mono_type_get_type(monoType);

		MonoClass* monoClass = Mono.mono_class_from_mono_type(monoType);

		if (monoClass == null)
			return null;

		StringView className = StringView(Mono.mono_class_get_name(monoClass));
		StringView classNamespace = StringView(Mono.mono_class_get_namespace(monoClass));

		// TODO: at the moment only allow user-structs
		//if (classNamespace.StartsWith("GlitchyEngine"))
		//	return null;
		
		ScriptFieldType scriptType = .None;

		if (Mono.mono_class_is_enum(monoClass))
		{
			scriptType = .Enum;

			return new SharpEnum(classNamespace, className, Mono.mono_class_get_image(monoClass));
		}
		else if (fieldType == .Class)
		{
			if (Mono.mono_class_is_subclass_of(monoClass, Classes.EntityRoot._monoClass, false))
			{
				scriptType = .Entity;
			}
			else if (Mono.mono_class_is_subclass_of(monoClass, Classes.ComponentRoot._monoClass, false))
			{
				scriptType = .Component;
			}
			else
			{
				scriptType = .Class;
			}
		}
		else if (fieldType == .Valuetype)
		{
			scriptType = .Struct;
		}
		// TODO! ?!
		else if (fieldType == .Genericinst)
		{
			scriptType = .GenericClass;

			return null;
			//return new SharpClass(monoClass, scriptType);
		}
		else if (fieldType == .SzArray)
		{
			scriptType = .Array;
			return null;
		}
		else if (fieldType == .Object)
		{
			// TODO: Help
			return null;
		}

		Log.EngineLogger.AssertDebug(scriptType != .None);

		return new SharpClass(classNamespace, className, Mono.mono_class_get_image(monoClass), scriptType);
	}

	/// Returns the script instance or null.
	public static MonoObject* GetManagedInstance(UUID entityId)
	{
		if (_entityScriptInstances.TryGetValue(entityId, let scriptInstance))
			return scriptInstance.MonoInstance;

		return null;
	}

	public static ScriptClass GetScriptClass(StringView name)
	{
		EntityClasses.TryGetValue(name, let scriptClass);

		return scriptClass;
	}

	internal static void HandleMonoException(MonoException* exception, ScriptInstance sourceInstance = null)
	{
		MonoExceptionHelper wrappedException = new MonoExceptionHelper(exception);

		String entityInfo = scope .();

		if (sourceInstance != null)
		{
			wrappedException.Instance = sourceInstance.EntityId;

			Result<Entity> sourceEntity = Context.GetEntityByID(sourceInstance.EntityId);

			if (sourceEntity case .Ok(let e))
			{
				entityInfo.AppendF($" ({e.Name} | {sourceInstance.EntityId})");
			}
		}

		Log.ClientLogger.Error($"Mono Exception \"{wrappedException.FullName}\": \"{wrappedException.Message}\"{entityInfo}\nStackTrace:\n{wrappedException.StackTrace}", wrappedException);

		wrappedException.ReleaseRef();
	}

	public static void ShowScriptEditor(Entity entity, ScriptComponent* scriptComponent)
	{
		if (scriptComponent.Instance == null)
			return;

		ScriptClass scriptClass = ScriptEngine.GetScriptClass(scriptComponent.ScriptClassName);

		if (scriptClass == null)
			return;

		let monoType = scriptClass.GetMonoType();
		let monoReflectionType = Mono.mono_type_get_object(s_AppDomain, monoType);

		let entityId = entity.UUID;

#unwarn
		void*[2] args = .(&entityId, monoReflectionType);

		let method = Classes.EntityEditor.GetMethod("ShowDefaultEntityEditor", 2);

		Classes.EntityEditor.Invoke(method, null, &args);
	}

	/// Serializes all script instances
	public static void SerializeScriptInstances(Dictionary<UUID, SerializedObject> allObjects)
	{
		Debug.Profiler.ProfileFunction!();

		for (let (id, script) in _entityScriptInstances)
		{
			SerializedObject object = new SerializedObject(allObjects, script.ScriptClass.FullName, script.EntityId);
			object.Serialize(script);
		}
	}

	/// Deserializes the given data into the script instances
	public static void DeserializeScriptInstances(Dictionary<UUID, SerializedObject> allObjects)
	{
		Debug.Profiler.ProfileFunction!();

		for (let (id, script) in _entityScriptInstances)
		{
			if (allObjects.TryGetValue(id, let object))
			{
				object.Deserialize(script);
			}
		}
	}
}
