using Mono;
using System;
using GlitchyEngine.Core;
using System.Collections;
using GlitchyEngine.Scripting.Classes;

namespace GlitchyEngine.Scripting;

using internal GlitchyEngine.Scripting;

public struct ScriptField
{
	public StringView Name;
	internal MonoClassField* _monoField;
	public bool IsStatic;
	public ScriptFieldType FieldType;
	public SharpType SharpType;
	public int SizeInBytes;

	internal this(StringView name, MonoClassField* monoField, bool isStatic, ScriptFieldType fieldType, int sizeInBytes, SharpType sharpType)
	{
		Name = name;
		_monoField = monoField;
		IsStatic = isStatic;
		FieldType = fieldType;
		SharpType = sharpType;
		SizeInBytes = sizeInBytes;
	}

	public bool IsType(SharpClass otherClass, bool checkIfSubtype)
	{
		return IsType(otherClass.GetMonoType(), checkIfSubtype);
	}

	public bool IsType(MonoType* otherType, bool checkIfSubtype)
	{
		MonoType* myType = GetMonoType();

		if (checkIfSubtype)
		{
			MonoClass* myClass = Mono.mono_type_get_class(myType);
			MonoClass* otherClass = Mono.mono_type_get_class(otherType);

			return Mono.mono_class_is_subclass_of(myClass, otherClass, false);
		}
		else
		{
			return myType == otherType;
		}
	}

	public MonoType* GetMonoType()
	{
		return Mono.mono_field_get_type(_monoField);
	}
}

public struct ScriptFieldInstance
{
	public ScriptFieldType Type;
	// TODO: I hate this, but we need to be able to store an entire matrix
	internal uint8[sizeof(GlitchyEngine.Math.Matrix)] _data;

	public this(ScriptFieldType type)
	{
		Type = type;
		_data = default;
	}

	public T GetData<T>()
	{
#unwarn
		return *(T*)&_data;
	}
	
	public void SetData<T>(T value) mut
	{
		*(T*)&_data = value;
	}
}

abstract class SharpType : RefCounter
{
	protected String _namespace ~ delete _;
	protected String _className ~ delete _;
	protected String _fullName ~ delete _;
	protected ScriptFieldType _scriptType;
	
	protected Dictionary<StringView, ScriptField> _monoFields ~ delete _;

	public StringView Namespace => _namespace;
	public StringView ClassName => _className;
	public StringView FullName => _fullName;
	public ScriptFieldType ScriptType => _scriptType;
	
	public Dictionary<StringView, ScriptField> Fields => _monoFields;
	
	public this(StringView classNamespace, StringView className, ScriptFieldType scriptType)
	{
		_namespace = new String(classNamespace);
		_className = new String(className);
		_fullName = new $"{_namespace}.{_className}";
		_scriptType = scriptType;
		
		ScriptEngine.RegisterSharpType(this);
	}
}

class SharpClass : SharpType
{
	protected internal MonoClass* _monoClass;
	
	public this(StringView classNamespace, StringView className, MonoImage* image, ScriptFieldType fieldType = .Class) :
		base(classNamespace, className, fieldType)
	{
		_monoClass = Mono.mono_class_from_name(image, _namespace, _className);

		Log.EngineLogger.AssertDebug(_monoClass != null);

		ExtractFields();
	}
	
	internal this(MonoClass* monoClass, ScriptFieldType fieldType = .Class) :
		base(StringView(Mono.mono_class_get_namespace(monoClass)), StringView(Mono.mono_class_get_name(monoClass)), fieldType)
	{
		_monoClass = monoClass;

		Log.EngineLogger.AssertDebug(_monoClass != null);

		ExtractFields();
	}

	internal MonoType* GetMonoType()
	{
		return Mono.mono_class_get_type(_monoClass);
	}

	internal bool IsType(MonoType* type)
	{
		return GetMonoType() == type;
	}

	internal bool IsSubclass(MonoClass* @class)
	{
		return Mono.mono_class_is_subclass_of(_monoClass, @class, false);
	}

	private void ExtractFields()
	{
		//_monoFields = new List<MonoClassField*>();´
		_monoFields = new Dictionary<StringView, ScriptField>();
		
		void* iterator = null;
		MonoClassField* currentField = null;
		while ((currentField = Mono.mono_class_get_fields(_monoClass, &iterator)) != null)
		{
			StringView name = StringView(Mono.mono_field_get_name(currentField));

			MonoType* type = Mono.mono_field_get_type(currentField);

			ScriptFieldType fieldType = ScriptEngineHelper.GetScriptFieldType(type);

			SharpType sharpType = null;

			// If field type is none the field might be a struct, class or enum
			if (fieldType == .None)
			{
				sharpType = ScriptEngine.GetSharpType(type);
				fieldType = sharpType?.ScriptType ?? .None;
				
				if (sharpType == null)
					continue;

				sharpType.ReleaseRef();
			}

			//Mono.MonoTypeEnum fieldType = Mono.Mono.mono_type_get_type(type);

			FieldAttribute flags = (.)Mono.mono_field_get_flags(currentField);

			MonoCustomAttrInfo* attributes = Mono.mono_custom_attrs_from_field(_monoClass, currentField);

			if (flags.HasFlag(.Public) ||
				(attributes != null &&
				Mono.mono_custom_attrs_has_attr(attributes, ScriptEngine.Attributes.s_ShowInEditorAttribute)))
			{
				MonoClass* fieldClass = Mono.mono_type_get_class(type);

				int sizeInBytes = 8;

				if (fieldClass != null)
				{
					sizeInBytes = Mono.mono_class_instance_size(fieldClass);
				}
				else
				{

				}

				_monoFields[name] = .(name, currentField, flags.HasFlag(.Static), fieldType, sizeInBytes, sharpType);
			}
		}
	}
}

struct EnumValue
{
	public StringView Name;

	public uint64 Value;

	public this(StringView name, uint64 value)
	{
		Name = name;
		Value = value;
	}
}

class SharpEnum : SharpClass
{
	private append Dictionary<uint64, EnumValue> _values = .();

	public Dictionary<uint64, EnumValue> Values => _values;

	private int _underlyingSize = 0;

	public this(StringView classNamespace, StringView className, MonoImage* image)
		: base(classNamespace, className, image, .Enum)
	{
		ExtractEnumValues();
	}

	private void ExtractEnumValues()
	{
		_underlyingSize = Mono.mono_class_instance_size(_monoClass);
		Log.EngineLogger.Assert(_underlyingSize != 0);

		var vtable = Mono.mono_class_vtable(ScriptEngine.[Friend]s_AppDomain, _monoClass);

		void* iterator = null;
		MonoClassField* currentField = null;
		while ((currentField = Mono.mono_class_get_fields(_monoClass, &iterator)) != null)
		{
			MonoType* fieldType = Mono.mono_field_get_type(currentField);
			MonoClass* fieldClass = Mono.mono_type_get_class(fieldType);

			FieldAttribute fieldFlags = (.)Mono.mono_field_get_flags(currentField);

			if (fieldFlags.HasFlag(.Public) && fieldFlags.HasFlag(.Static) &&
				fieldClass != null && Mono.mono_class_is_subclass_of(fieldClass, _monoClass, false))
			{
				StringView fieldName = StringView(Mono.mono_field_get_name(currentField));

				uint64 value = 0;
				Mono.mono_field_static_get_value(vtable, currentField, &value);

				EnumValue enumValue = .(fieldName, value);
	
				_values.Add(value, enumValue);
			}
		}
	}
}

class SharpStruct : SharpClass
{
	protected int _size;

	public int Size => _size;

	public this(StringView classNamespace, StringView className, MonoImage* image)
		: base(classNamespace, className, image)
	{

	}
}

class ScriptClass : SharpClass
{
	typealias OnCreateMethod = function MonoObject*(MonoObject* instance, MonoException** exception);
	typealias OnUpdateMethod = function MonoObject*(MonoObject* instance, float deltaTime, MonoException** exception);
	typealias OnDestroyMethod = function MonoObject*(MonoObject* instance, MonoException** exception);

	private bool _runInEditMode;

	private MonoMethod* _constructor;

	private OnCreateMethod _onCreate;
	private OnUpdateMethod _onUpdate;
	private OnDestroyMethod _onDestroy;
	
	private function [CallingConvention(.Cdecl)] MonoObject*(MonoObject* instance, Collision2D collision, MonoException** exception) _onCollisionEnter2D;
	private function [CallingConvention(.Cdecl)] MonoObject*(MonoObject* instance, Collision2D collision, MonoException** exception) _onCollisionLeave2D;

	//private OnCollisionEnter2DMethod _onCollisionEnter2D;
	//private OnDestroyMethod _onCollisionLeave2D;
	private MonoMethod* _onCollisionEnter2DMethod;
	private MonoMethod* _onCollisionLeave2DMethod;

	public bool HasCollisionEnter2D => _onCollisionEnter2DMethod != null;
	public bool HasCollisionLeave2D => _onCollisionLeave2DMethod != null;

	public bool RunInEditMode => _runInEditMode;

	[AllowAppend]
	public this(StringView classNamespace, StringView className, MonoImage* image, ScriptFieldType scriptFieldType = .Class) :
		base(classNamespace, className, image, scriptFieldType)
	{
		_constructor = FindConstructor();
		_onCreate = (OnCreateMethod)GetMethodThunk("OnCreate");
		_onUpdate = (OnUpdateMethod)GetMethodThunk("OnUpdate", 1);
		_onDestroy = (OnDestroyMethod)GetMethodThunk("OnDestroy");

		_onCollisionEnter2D = (.)GetMethodThunk("OnCollisionEnter2D", 1);
		_onCollisionLeave2D = (.)GetMethodThunk("OnCollisionLeave2D", 1);

		_onCollisionEnter2DMethod = GetMethod("OnCollisionEnter2D", 1);
		_onCollisionLeave2DMethod = GetMethod("OnCollisionLeave2D", 1);

		DetermineRunInEditMode();
	}

	/// Determines whether or not scripts of this class will be executed in edit mode.
	private void DetermineRunInEditMode()
	{
		// TODO: This is only relevant for the editor!
		MonoCustomAttrInfo* attributes = Mono.mono_custom_attrs_from_class(_monoClass);

		if (attributes != null)
		_runInEditMode = Mono.mono_custom_attrs_has_attr(attributes, ScriptEngine.Attributes.s_RunInEditModeAttribute);

	}

	private MonoMethod* FindConstructor()
	{
		MonoMethod* constructor = null;

		MonoMethodDesc* constructorDesc = Mono.mono_method_desc_new(":.ctor(GlitchyEngine.Core.UUID)", true);
		
		// Entities usually don't have a constructor that takes the UUID, so we have to look for it in the parents.
		for (MonoClass* @class = _monoClass; @class != null; @class = Mono.mono_class_get_parent(@class))
		{
			constructor = Mono.mono_method_desc_search_in_class(constructorDesc, @class);

			if (constructor != null)
				break;
		}

		Mono.mono_method_desc_free(constructorDesc);

		return constructor;
	}

	private void CallConstructorChain(MonoObject* instance, UUID uuid, out MonoException* exception)
	{
		exception = null;

		if (_constructor == null)
			return;

#unwarn
		void*[1] args = void*[](&uuid);

		Mono.mono_runtime_invoke(_constructor, instance, &args, (.)&exception);
	}

	public void OnCreate(MonoObject* instance, out MonoException* exception)
	{
		exception = null;

		if (_onCreate != null)
			_onCreate(instance, &exception);
	}

	public void OnUpdate(MonoObject* instance, float deltaTime, out MonoException* exception)
	{
		exception = null;

		if (_onUpdate != null)
			_onUpdate(instance, deltaTime, &exception);
	}

	public void OnDestroy(MonoObject* instance, out MonoException* exception)
	{
		exception = null;

		if (_onDestroy != null)
			_onDestroy(instance, &exception);
	}
	
	public void OnCollisionEnter2D(MonoObject* instance, Collision2D collision, out MonoException* exception)
	{
		exception = null;

		// TODO: use method thunk
		if (_onCollisionEnter2DMethod != null)
		{
#unwarn
			void*[1] args = .(&collision);

			Invoke(_onCollisionEnter2DMethod, instance, (.)&args);
		}
		// if (_onCollisionEnter2D != null)
		//	_onCollisionEnter2D(instance, collision, &exception);
	}
	
	public void OnCollisionLeave2D(MonoObject* instance, Collision2D collision, out MonoException* exception)
	{
		exception = null;

		// TODO: use method thunk
		if (_onCollisionLeave2DMethod != null)
		{
#unwarn
			void*[1] args = .(&collision);

			Invoke(_onCollisionLeave2DMethod, instance, (.)&args);
		}
		// if (_onCollisionEnter2D != null)
		//	_onCollisionEnter2D(instance, collision, &exception);
	}

	public MonoObject* CreateInstance(UUID uuid, out MonoException* exception)
	{
		MonoObject* instance = Mono.mono_object_new(ScriptEngine.[Friend]s_AppDomain, _monoClass);

		// Invoke empty constructor to initialize fields with default values specified in the script itself
		Mono.mono_runtime_object_init(instance);

		// Invoke UUID Constructors:
		CallConstructorChain(instance, uuid, out exception);

		// Invoke constructor with UUID
//#unwarn
		//ScriptEngine.[Friend]s_EngineObject.Invoke(ScriptEngine.[Friend]s_EngineObject._constructor, instance, out exception, &uuid);

		return instance;
	}

	public MonoObject* CreateInstance()
	{
		MonoObject* instance = Mono.mono_object_new(ScriptEngine.[Friend]s_AppDomain, _monoClass);

		// Invoke empty constructor to fill fields
		Mono.mono_runtime_object_init(instance);

		return instance;
	}

	public MonoMethod* GetMethod(StringView name, int argCount = 0)
	{
		return Mono.mono_class_get_method_from_name(_monoClass, name.ToScopeCStr!(), argCount);
	}

	public void* GetMethodThunk(StringView name, int argCount = 0)
	{
		MonoMethod* method = GetMethod(name, argCount);

		if (method == null)
			return null;

		return Mono.mono_method_get_unmanaged_thunk(method);
	}

	public MonoObject* Invoke(MonoMethod* method, MonoObject* instance, void** args = null)
 	{
		 MonoObject* exception = null;

		 MonoObject* result = Mono.mono_runtime_invoke(method, instance, args, &exception);

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

		 return result;
	}

	public MonoObject* Invoke(MonoMethod* method, MonoObject* instance, out MonoException* exception, params void*[] args)
 	{
		exception = null;

		MonoObject* result = Mono.mono_runtime_invoke(method, instance, args.Ptr, (.)&exception);

		return result;
	}

	public T Invoke<T>(MonoMethod* method, MonoObject* instance, params void*[] args)
 	{
		 MonoObject* object = Invoke(method, instance, args.Ptr);
		 return *(T*)Mono.mono_object_unbox(object);
	}

	public T GetFieldValue<T>(MonoObject* instance, MonoClassField* field)
	{
		T value = default;
		Mono.mono_field_get_value(instance, field, &value);
		return value;
	}

	public void SetFieldValue<T>(MonoObject* instance, MonoClassField* field, in T value)
	{
		Mono.mono_field_set_value(instance, field, &value);
	}

	public void SetFieldValue<T>(MonoObject* instance, MonoClassField* field, in T value) where T : struct*
	{
		Mono.mono_field_set_value(instance, field, value);
	}
}
