using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Numerics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using GlitchyEngine.Core;
using GlitchyEngine.Extensions;
using GlitchyEngine.Math;
using ImGuiNET;

namespace GlitchyEngine.Editor;

class DidNotChange
{
}

internal class EntityEditor
{
    public static readonly DidNotChange DidNotChange = new DidNotChange();
    
    private delegate object? ShowCustomEditorMethod(object? reference, Type fieldType, string fieldName);

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

    static EntityEditor()
    {
        foreach (Type type in TypeExtension.EnumerateAllTypes())
        {
            if (type.TryGetCustomAttribute<CustomEditorAttribute>(out var attribute))
            {
                MethodInfo? showEditorMethod = type.GetMethod("ShowEditor", BindingFlags.Static | BindingFlags.Public,
                    null,
                    new []{ typeof(object), typeof(Type), typeof(string) }, null);

                if (showEditorMethod == null)
                {
                    Log.Error($"No ShowEditor-method found for type {type}");
                }
                else
                {
                    ShowCustomEditorMethod method = showEditorMethod.GetDelegate<ShowCustomEditorMethod>();

                    _customEditors.Add(attribute.Type, method);
                }
            }
        }
    }
    
    private static bool TryShowCustomEditor(object? reference, Type fieldType, string fieldName, out object? editedValue)
    {
        editedValue = DidNotChange;

        try
        {
            // Try to match the concrete type first (e.g. Foo -> Foo and Foo<Bar> -> List<Bar>)
            // Note: Foo<Bar> wont match a serializer for Foo<>
            if (_customEditors.TryGetValue(fieldType, out ShowCustomEditorMethod customEditorMethod))
            {
                editedValue = customEditorMethod(reference, fieldType, fieldName);
                return true;
            }
            
            if (fieldType.IsGenericType && _customEditors.TryGetValue(fieldType.GetGenericTypeDefinition(), out customEditorMethod))
            {
                editedValue = customEditorMethod(reference, fieldType, fieldName);
                return true;
            }
        }
        catch (Exception e)
        {
            Log.Exception(e);
        }

        return false;
    }

    public static bool ShowFieldInEditor(FieldInfo fieldInfo)
    {
        if (fieldInfo.IsPublic)
        {
            // Public fields are visible by default (No HideInEditorAttribute)
            if (fieldInfo.HasCustomAttribute<HideInEditorAttribute>())
                return false;

            return true;
        }
        
        // Private, protected and internal fields are hidden by default (No ShowInEditorAttribute)
        if (fieldInfo.HasCustomAttribute<ShowInEditorAttribute>())
            return true;

        return false;
    }
    
    public static void ShowDefaultEntityEditor(UUID entityId, Type entityType)
    {
        // TODO: Call custom editors here!

        ScriptGlue.Entity_GetScriptInstance(entityId, out object? instance);

        if (instance != null)
            ShowEditor(entityType, instance);
    }

    private static T? GetAttribute<T>(IEnumerable<Attribute>? attributes) where T : Attribute
    {
        return (T?)attributes?.FirstOrDefault(a => a is T);
    }

    private static object ShowDecimalEditor(object? reference, Type fieldType, string fieldName, IEnumerable<Attribute>? attributes)
    {
        if (reference is not decimal value)
            return DidNotChange;

        string stringValue = value.ToString(CultureInfo.InvariantCulture);

        byte[] bytes = new byte[30 * 4];

        Encoding.UTF8.GetBytes(stringValue, 0, stringValue.Length, bytes, 0);

        ImGui.PushID("Decimal");

        if (ImGui.InputText(fieldName, bytes, (uint)bytes.Length, ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.CharsDecimal))
        {
            string text = Encoding.UTF8.GetString(bytes);

            if (decimal.TryParse(text, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out decimal newValue))
            {
                ImGui.PopID();
                return newValue;
            }
        }
        ImGui.PopID();

        return DidNotChange;
    }

    private static object? ShowPrimitiveEditor(object? reference, Type fieldType, string fieldName, IEnumerable<Attribute>? attributes)
    {
        object newValue = DidNotChange;

        unsafe void DragScalar<T>(ImGuiDataType dataType) where T : unmanaged
        {
            T ChangeTypeSafe(object value)
            {
                try
                {
                    return (T)Convert.ChangeType(value, typeof(T));
                }
                catch (OverflowException e)
                {
                    // Log.Error($"Could not apply value: {e.Message}");
                    ImGui.TextColored(new Vector4(1, 0, 0, 1), $"Value {value} can not be converted to type {typeof(T)}: {e.Message}.");

                    return default;
                }
            }

            RangeAttribute? range = GetAttribute<RangeAttribute>(attributes);

            // Get Min and Max Values from Type T
            T min = ReadStaticField<T>("MinValue");
            T max = ReadStaticField<T>("MaxValue");
            float dragSpeed = 1.0f;
            
            // RangeAttribute takes precedence over Minimum- and Maximum-Attribute
            if (range != null)
            {
                min = ChangeTypeSafe(range.Min);
                max = ChangeTypeSafe(range.Max);
                dragSpeed = range.Speed;
            }
            else
            {
                MinimumAttribute? minimum = GetAttribute<MinimumAttribute>(attributes);
                MaximumAttribute? maximum = GetAttribute<MaximumAttribute>(attributes);
                
                // Don't use range, if it is invalid
                if (minimum != null && maximum != null && minimum.Min > maximum.Max)
                {
                    //Log.Error($"Minimum value ({minimum.Min}) specified for field {fieldName} must not be greater than the maximum value ({maximum.Max})! Ignoring range...");
                    ImGui.TextColored(new Vector4(1, 0, 0, 1), $"Minimum value ({minimum.Min}) specified for field {fieldName} must not be greater than the maximum value ({maximum.Max})! Ignoring range...");
                }
                else
                {
                    if (minimum != null)
                        min = ChangeTypeSafe(minimum.Min);

                    if (maximum != null)
                        max = ChangeTypeSafe(maximum.Max);
                }
            }

            var value = (T)reference;

            if (range?.Slider == true)
            {
                if (ImGui.SliderScalar(fieldName, dataType, (IntPtr)(&value), (IntPtr)(&min), (IntPtr)(&max)))
                {
                    newValue = value;
                }
            }
            else
            {
                if (ImGui.DragScalar(fieldName, dataType, (IntPtr)(&value), dragSpeed, (IntPtr)(&min), (IntPtr)(&max)))
                {
                    newValue = value;
                }
            }
        }

        if (reference != null)
        {
            if (fieldType == typeof(bool))
            {
                bool value = (bool)reference;
                if (ImGui.Checkbox(fieldName, ref value))
                    newValue = value;
            }
            else if (fieldType == typeof(char))
            {
                unsafe
                {
                    // TODO: Allow escaped unicode chars \u XXXX
                    // TODO: Add text input validation, so that it isn't possible to type invalid chars

                    char* value = stackalloc char[4];
                    switch((char)reference)
                    {
                        case '\a':
                            value[0] = '\\';
                            value[1] = 'a';
                            break;
                        case '\b':
                            value[0] = '\\';
                            value[1] = 'b';
                            break;
                        case '\f':
                            value[0] = '\\';
                            value[1] = 'f';
                            break;
                        case '\n':
                            value[0] = '\\';
                            value[1] = 'n';
                            break;
                        case '\r':
                            value[0] = '\\';
                            value[1] = 'r';
                            break;
                        case '\t':
                            value[0] = '\\';
                            value[1] = 't';
                            break;
                        case '\v':
                            value[0] = '\\';
                            value[1] = 'v';
                            break;
                        default:
                            value[0] = (char)reference;
                            value[1] = '\0';
                            break;
                    };

                    // Enough space to store two code points
                    byte* buffer = stackalloc byte[8];
                    int encodedBytes = Encoding.UTF8.GetBytes(value, 2, buffer, 8);

                    // Place string delimiter after encoded UTF8 sequence.
                    buffer[encodedBytes] = (byte)'\0';

                    if (ImGui.InputText(fieldName, (IntPtr)buffer, 8))
                    {
                        if (buffer[0] == '\\' && buffer[1] != '\0')
                        {
                            newValue = (char)buffer[1] switch
                            {
                                'a' => newValue = '\a',
                                'b' => newValue = '\b',
                                'f' => newValue = '\f',
                                'n' => newValue = '\n',
                                'r' => newValue = '\r',
                                't' => newValue = '\t',
                                'v' => newValue = '\v',
                                _ => newValue = '\\'
                            };
                        }
                        else
                        {
                            // Only decode first codepoint
                            Encoding.UTF8.GetChars(buffer, 4, value, 4);

                            newValue = value[0];
                        }
                    }   
                }
            }
            else if (fieldType == typeof(byte))
                DragScalar<byte>(ImGuiDataType.U8);
            else if (fieldType == typeof(sbyte))
                DragScalar<sbyte>(ImGuiDataType.S8);
            else if (fieldType == typeof(short))
                DragScalar<short>(ImGuiDataType.S16);
            else if (fieldType == typeof(ushort))
                DragScalar<ushort>(ImGuiDataType.U16);
            else if (fieldType == typeof(int))
                DragScalar<int>(ImGuiDataType.S32);
            else if (fieldType == typeof(uint))
                DragScalar<uint>(ImGuiDataType.U32);
            else if (fieldType == typeof(long))
                DragScalar<long>(ImGuiDataType.S64);
            else if (fieldType == typeof(ulong))
                DragScalar<ulong>(ImGuiDataType.U64);
            else if (fieldType == typeof(float))
                DragScalar<float>(ImGuiDataType.Float);
            else if (fieldType == typeof(double))
                DragScalar<double>(ImGuiDataType.Double);
            else
            {
                ImGui.TextColored(new Vector4(1, 0, 0, 1), $"{fieldName}: Type {fieldType} is not implemented.");
            }
        }
        else
        {
            return ActivatorExtension.CreateInstanceSafe(fieldType);
        }

        return newValue;
    }

    private static object ShowEnumEditor(object? reference, Type fieldType, string fieldName)
    {
        object newValue = DidNotChange;

        if (ImGui.BeginCombo(fieldName, reference?.ToString()))
        {
            foreach (object enumValue in Enum.GetValues(fieldType))
            {
                if (ImGui.Selectable(enumValue.ToString(), enumValue == reference))
                {
                    newValue = enumValue;
                }
            }

            ImGui.EndCombo();
        }

        return newValue;
    }

    public static object? ShowFieldEditor(object? reference, Type fieldType, string fieldName, IEnumerable<Attribute>? attributes = null)
    {
        ReadonlyAttribute? readonlyAttribute = GetAttribute<ReadonlyAttribute>(attributes);
        
        ImGui.BeginDisabled(readonlyAttribute != null);

        object? newValue;
        
        if (TryShowCustomEditor(reference, fieldType, fieldName, out newValue))
        {

        }
        else if (fieldType.IsGenericType)
        {
            if (fieldType.GetGenericTypeDefinition() == typeof(List<>))
            {
                newValue = ShowListEditor(fieldType, reference, fieldName);
            }
            else
            {
                // TODO: Try to use a custom editor
                ImGui.TextColored(new Vector4(1, 0, 0, 1), $"{fieldName}: Type {fieldType} is not implemented.");
            }
        }
        else if (fieldType.IsArray)
        {
            newValue = ShowArrayEditor(fieldType, reference, fieldName);
        }
        else if (fieldType.IsEnum)
        {
            newValue = ShowEnumEditor(reference, fieldType, fieldName);
        }
        else if (fieldType.IsPrimitive)
        {
            newValue = ShowPrimitiveEditor(reference, fieldType, fieldName, attributes);
        }
        else if (fieldType.IsValueType)
        {
            if (fieldType == typeof(decimal))
            {
                newValue = ShowDecimalEditor(reference, fieldType, fieldName, attributes);
            }
            else if (fieldType == typeof(bool2))
            {
                bool2 value = (bool2)reference!;
                    
                ImGui.TextUnformatted(fieldName);

                ImGui.SameLine();

                if (ImGui.Checkbox($"##{fieldName}X", ref value.X))
                    newValue = value;
                    
                ImGui.SameLine();

                if (ImGui.Checkbox($"##{fieldName}Y", ref value.Y))
                    newValue = value;
            }
            else
            {
                ImGui.EndDisabled();

                // Struct
                if (ImGui.TreeNode(fieldName))
                {
                    ImGui.BeginDisabled(readonlyAttribute != null);

                    ShowEditor(fieldType, reference);
                
                    ImGui.TreePop();

                    newValue = reference;
                }
                else
                {
                    ImGui.BeginDisabled(readonlyAttribute != null);
                }
            }
        }
        else if (fieldType.IsSubclassOf(typeof(Entity)) || fieldType == typeof(Entity))
        {
            newValue = ShowEntityDropTarget(fieldName, fieldType, reference);
        }
        else if (fieldType.IsSubclassOf(typeof(Component)))
        {
            newValue = ShowComponentDropTarget(fieldName, fieldType, reference);
        }
        else if (fieldType == typeof(string))
        {
            newValue = ShowStringEditor(reference, fieldType, fieldName, attributes);
        }
        else
        {
            ImGui.EndDisabled();

            if (ImGui.TreeNode(fieldName))
            {
                ImGui.BeginDisabled(readonlyAttribute != null);

                if (reference == null)
                {
                    ImGui.SameLine(ImGui.GetWindowContentRegionMax().X - ImGui.CalcTextSize("Create").X - 2 * ImGui.GetStyle().FramePadding.X);

                    if (ImGui.BeginCombo("Create", "Create", ImGuiComboFlags.HeightSmall | ImGuiComboFlags.NoArrowButton))
                    {
                        foreach (Type t in TypeExtension.FindDerivedTypes(fieldType))
                        {
                            if (ImGui.Selectable(t.Name))
                            {
                                newValue = ActivatorExtension.CreateInstanceSafe(t);
                            }
                        }

                        ImGui.EndCombo();
                    }
                }
                else
                {
                    ImGui.SameLine(ImGui.GetWindowContentRegionMax().X - ImGui.CalcTextSize("Remove").X - 2 * ImGui.GetStyle().FramePadding.X);

                    if (ImGui.SmallButton("Remove"))
                    {
                        newValue = null;
                    }
                }

                if (reference == null)
                    ImGui.Text("(NULL)");
                else
                    ShowEditor(reference.GetType(), reference);

                ImGui.TreePop();
            }
            else
            {
                ImGui.BeginDisabled(readonlyAttribute != null);
            }
        }

        ImGui.EndDisabled();

        return newValue;
    }

    private static object? ShowComponentDropTarget(string fieldName, Type fieldType, object? currentValue)
    {
        object? newValue = DidNotChange;
        
        ImGui.Text($"{fieldName}: ");
        ImGui.SameLine();

        Component? component = currentValue as Component;

        string entityName = "None";

        if (component != null)
        {
            entityName = component.Entity.Name;
        }
        
        Type? actualType = component?.GetType();

        entityName += $" ({(actualType ?? fieldType).Name})";
        
        if (ImGui.Button(entityName))
        {
            if (component?.Entity != null)
            {
                // TODO: Hightlight Entity in hierarchy
                // TODO: Highlight Component in editor
            }
        }

        if (ImGui.BeginDragDropTarget())
        {
            ImGuiPayloadPtr peekPayload = ImGui.AcceptDragDropPayload("ENTITY", ImGuiDragDropFlags.AcceptPeekOnly);

            bool allowDrop = false;
            
            unsafe
            {
                if (peekPayload.NativePtr != null)
                {
                    UUID draggedId = *(UUID*)peekPayload.Data;

                    Entity draggedEntity = new Entity(draggedId);

                    allowDrop = draggedEntity.HasComponent(fieldType);
                }

                if (allowDrop)
                {
                    ImGuiPayloadPtr payload = ImGui.AcceptDragDropPayload("ENTITY");
                    
                    if (payload.NativePtr != null)
                    {
                        UUID droppedId = *(UUID*)payload.Data;
                        
                        Entity draggedEntity = new Entity(droppedId);

                        try
                        {
                            newValue = draggedEntity.GetComponent(fieldType);
                        }
                        catch (Exception e)
                        {
                            Log.Exception(e);
                        }
                    }
                }
            }

            ImGui.EndDragDropTarget();
        }

        return newValue;
    }

    private static object? ShowEntityDropTarget(string fieldName, Type fieldType, object? currentValue)
    {
        object? newValue = DidNotChange;

        ImGui.Text($"{fieldName}: ");
        ImGui.SameLine();

        string entityName = "None";
        
        Entity? entity = currentValue as Entity;

        if (entity != null)
        {
            entityName = entity.Name;
        }

        Type? actualType = entity?.GetType();

        entityName += $" ({(actualType ?? fieldType).Name})";

        if (ImGui.Button(entityName))
        {
            if (entity != null)
            {
                // TODO: Hightlight Entity in hierarchy
            }
        }

        if (ImGui.BeginDragDropTarget())
        {
            ImGuiPayloadPtr peekPayload = ImGui.AcceptDragDropPayload("ENTITY", ImGuiDragDropFlags.AcceptPeekOnly);

            bool allowDrop = false;
            
            unsafe
            {
                if (peekPayload.NativePtr != null)
                {
                    UUID draggedId = *(UUID*)peekPayload.Data;

                    Entity draggedEntity = new Entity(draggedId);

                    // TODO: Check if entity is valid?

                    if (fieldType == typeof(Entity))
                    {
                        allowDrop = true;
                    }
                    else
                    {
                        Entity? scriptInstance = Entity.GetScriptReference(draggedId, typeof(Entity));

                        allowDrop = fieldType.IsInstanceOfType(scriptInstance);
                    }
                }

                if (allowDrop)
                {
                    ImGuiPayloadPtr payload = ImGui.AcceptDragDropPayload("ENTITY");
                    
                    if (payload.NativePtr != null)
                    {
                        UUID droppedId = *(UUID*)payload.Data;
                        
                        if (fieldType == typeof(Entity))
                        {
                            newValue = new Entity(droppedId);
                        }
                        else
                        {
                            newValue = Entity.GetScriptReference(droppedId, typeof(Entity));
                        }
                    }
                }
            }

            ImGui.EndDragDropTarget();
        }

        ImGui.SameLine();

        if (ImGui.Button("..."))
        {
            // TODO: Show entity selector
        }

        ImGuiExtension.AttachTooltip("Search entity...");

        return newValue;
    }

    private static object ShowStringEditor(object? reference, Type fieldType, string fieldName, IEnumerable<Attribute>? attributes)
    {
        object newValue = DidNotChange;

        TextFieldAttribute? textField = GetAttribute<TextFieldAttribute>(attributes);

        string value = reference as string ?? $"{float.MinValue}";

        if (textField?.Multiline == true)
        {
            ImGui.Text(fieldName);
            if (ImGui.InputTextMultiline($"##{fieldName}", ref value, 1000, new Vector2(-1.0f, ImGui.GetTextLineHeight() * textField.TextFieldLines)))
                newValue = value;
        }
        else
        {
            if (ImGui.InputText(fieldName, ref value, 1000))
                newValue = value;
        }

        return newValue;
    }

    public static void ShowEditor(Type type, object? reference)
    {
        int i = 0;

        // Iterate all fields
        foreach (FieldInfo field in type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public))
        {
            i++;
            ImGui.PushID(i);

            if (ShowFieldInEditor(field))
            {
                object value = field.GetValue(reference);

                IEnumerable<Attribute> attributes = field.GetCustomAttributes();

                object? newValue = ShowFieldEditor(value, value?.GetType() ?? field.FieldType, field.Name, attributes);

                if (newValue != DidNotChange)
                {
                    // Reference was changed inside Field Editor -> write back to field (only necessary for value types)
                    field.SetValue(reference, newValue);
                }
            }
            
            ImGui.PopID();
        }

        ShowButtons(type, reference);
    }

    /// <summary>
    /// Shows all buttons in the UI.
    /// </summary>
    private static void ShowButtons(Type type, object? reference)
    {
        ImGui.PushID("Buttons");

        int i = 0;

        // Iterate all methods
        foreach (MethodInfo method in type.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public))
        {
            var showButton = method.GetCustomAttribute<ShowButtonAttribute>();

            if (showButton == null)
                continue;

            if (Application.IsInEditMode && !showButton.Visibility.HasFlag(ButtonVisibility.InEditMode))
                continue;
        
            if (Application.IsInPlayMode && !showButton.Visibility.HasFlag(ButtonVisibility.InPlayMode))
                continue;

            i++;
            ImGui.PushID(i);

            if (method.GetParameters().Length > 0)
            {
                Log.Error($"Method {method.Name} cannot be executed from editor, because it expects arguments.");
            }
            else if (ImGui.Button(showButton.ButtonText))
            {
                try
                {
                    method.Invoke(reference, null);
                }
                catch (TargetInvocationException ex)
                {
                    Log.Exception(ex.InnerException ?? ex);
                }
                catch (Exception ex)
                {
                    Log.Exception(ex);
                }                
            }
    
            ImGui.PopID();
        }

        ImGui.PopID();
    }

    struct ListPayload
    {
        public IList List;
        public int Element;
    }

    private static object? ShowListEditor(Type fieldType, object? fieldValue, string fieldName)
    {
        Type elementType = fieldType.GenericTypeArguments[0];

        void AddElement(IList? list, out object? newList)
        {
            newList = DidNotChange;

            if (list == null)
            {
                newList = Activator.CreateInstance(fieldType);
                list = newList as IList;

                Debug.Assert(list != null);
            }
            
            try
            {
                // Reference values can be null
                object? newElement = elementType.IsByRef ? null : ActivatorExtension.CreateInstanceSafe(elementType);

                list?.Add(newElement);
            }
            catch (Exception ex)
            {
                Log.Error($"Failed to add new element to list: {ex}");
            }
        }

        void RemoveElement(IList? list, out object? newList)
        {
            newList = DidNotChange;

            if (list == null)
                return;

            if (list.Count == 0)
            {
                newList = null;
            }
            else
            {
                list.RemoveAt(list.Count - 1);
            }
        }

        return ShowGenericListEditor(fieldType, elementType, (IEnumerable?)fieldValue, fieldName, 
            AddElement, RemoveElement);
    }

    private static object? ShowArrayEditor(Type fieldType, object? fieldValue, string fieldName)
    {
        Debug.Assert(fieldType.IsArray);

        Type? elementType = fieldType.GetElementType();

        Debug.Assert(elementType != null);

        Array? myArray = fieldValue as Array;
        
        if (myArray?.Rank > 1)
        {
            throw new NotImplementedException("Multidimensional arrays are not yet supported.");
        }

        void AddElement(IList? list, out object? newList)
        {
            newList = DidNotChange;

            int newIndex = 0;

            if (list == null)
            {
                newList = Array.CreateInstance(elementType, 1);
            }
            else
            {
                newIndex = list.Count;

                Array newArray = Array.CreateInstance(elementType, list.Count + 1);
                Array.Copy((Array)list, newArray, list.Count);

                newList = newArray;
            }

            object newElement = Activator.CreateInstance(elementType);
            ((IList)newList)[newIndex] = newElement;
        }

        void RemoveElement(IList? list, out object? newList)
        {
            newList = DidNotChange;

            if (list == null)
                return;

            if (list.Count == 0)
            {
                newList = null;
            }
            else
            {
                Array newArray = Array.CreateInstance(elementType, list.Count - 1);
                Array.Copy((Array)list, newArray, newArray.Length);

                newList = newArray;
            }
        }

        return ShowGenericListEditor(fieldType, elementType!, (IEnumerable?)fieldValue, fieldName, 
            AddElement, RemoveElement);
    }

    public delegate void ModifyList(IList? currentList, out object? oldList);

    private static object? ShowGenericListEditor(Type listType, Type elementType, IEnumerable? list, string fieldName, 
        ModifyList addElement, ModifyList removeElement)
    {
        object? newList = DidNotChange;
        IList? myList = list as IList;

        // The buttons should always be visible -> save whether the node is open
        // If we have no instance, the user shouldn't be able to open the list
        bool listOpen = ImGui.TreeNodeEx(fieldName,  ImGuiTreeNodeFlags.AllowOverlap | ImGuiTreeNodeFlags.SpanFullWidth |
            (myList == null ? ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen : ImGuiTreeNodeFlags.Framed));

        if (myList == null)
            listOpen = false;

        var addButtonWidth = ImGui.CalcTextSize("+").X + 2 * ImGui.GetStyle().FramePadding.X;
        var removeButtonWidth = ImGui.CalcTextSize("-").X + 2 * ImGui.GetStyle().FramePadding.X;

        ImGui.SameLine(ImGui.GetWindowContentRegionMax().X - addButtonWidth - ImGui.GetStyle().FramePadding.X - removeButtonWidth);

        if (ImGui.SmallButton("+"))
        {
            addElement(myList, out newList);
        }

        ImGuiExtension.AttachTooltip("Add a new Element at the end of the list.");

        ImGui.SameLine(ImGui.GetWindowContentRegionMax().X - removeButtonWidth);

        ImGui.BeginDisabled(myList == null);

        if (ImGui.SmallButton("-"))
        {
            removeElement(myList, out newList);
        }

        ImGuiExtension.AttachTooltip("Remove the last Element from the list.");

        ImGui.EndDisabled();

        if (listOpen)
        {
            Debug.Assert(myList != null, "Opened tree node even though it should have been a leaf!");

            // Returns true if something was dropped into the droptarget
            bool DropTarget(int insertIndex, string tooltip)
            {
                bool dropped = false;

                if (ImGui.BeginDragDropTarget())
                {
                    ImGui.SetTooltip(tooltip);

                    var payloadPtr = ImGui.AcceptDragDropPayload("SCRIPT_EDITOR_LIST_ELEMENT");

                    unsafe
                    {
                        if (payloadPtr.NativePtr != null)
                        {
                            ref ListPayload payload = ref Unsafe.AsRef<ListPayload>((void*)payloadPtr.Data);

                            // Make sure the index is still correct
                            if (payload.Element < myList!.Count)
                            {
                                object item = myList[payload.Element];

                                myList.RemoveAt(payload.Element);

                                if (payload.Element >= insertIndex)
                                    myList.Insert(insertIndex, item);
                                else
                                    // If the original index is before the insert index, the insert index will be moved after removing the element
                                    // thus we have to subtract one.
                                    myList.Insert(insertIndex - 1, item);

                                dropped = true;
                            }
                        }
                    }

                    ImGui.EndDragDropTarget();
                }

                return dropped;
            }

            // Drop at index 0
            ImGui.Separator();
            DropTarget(0, "Drop before Element 0.");

            bool orderChanged = false;

            for (int i = 0, id = 0; i < myList!.Count; i++, id++)
            {
                ImGui.PushID(id);

                object element = myList[i];

                // A Bullet point to grab the element
                ImGui.Bullet();
                if (ImGui.BeginDragDropSource(ImGuiDragDropFlags.SourceAllowNullID))
                {
                    unsafe
                    {
                        ListPayload payload = new() { List = myList, Element = i };

                        ImGui.SetDragDropPayload("SCRIPT_EDITOR_LIST_ELEMENT", (IntPtr)Unsafe.AsPointer(ref payload), (uint)Unsafe.SizeOf<ListPayload>());
                    }

                    ImGui.SetTooltip($"Move Element {i}");

                    ImGui.EndDragDropSource();
                }

                orderChanged |= DropTarget(i, $"Drop before Element {i}.");

                if (ImGui.BeginPopupContextWindow($"ListElementContext{i}"))
                {
                    if (ImGui.MenuItem($"Remove Element {i}"))
                    {
                        myList.RemoveAt(i);
                        i--;
                        ImGui.EndPopup();
                        ImGui.PopID();
                        continue;
                    }

                    ImGui.EndPopup();
                }
                
                ImGui.SameLine();

                object? newValue = ShowFieldEditor(element, element?.GetType() ?? elementType, $"Element {i}");

                // Don't apply changes, when the order changed
                if (newValue != DidNotChange && !orderChanged)
                {
                    myList[i] = newValue;
                }

                // Drop after current element
                ImGui.Separator();
                
                orderChanged |= DropTarget(i + 1, $"Drop after Element {i}.");

                ImGui.PopID();
            }

            ImGui.TreePop();
        }

        return newList;
    }

    private static T ReadStaticField<T>(string name)
    {
        FieldInfo? field = typeof(T).GetField(name, BindingFlags.Public | BindingFlags.Static);
        
        if (field == null)
        {
            throw new InvalidOperationException($"Type {typeof(T).Name} has no static field \"{name}\"");
        }

        return (T)field.GetValue(null);
    }
}
