Editor/ControlWidgets/TypeCreatorControlWidget.cs
using Editor;
using ExtendedEditor.Attributes;
using ExtendedEditor.TypeLibraryFixes;
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace ExtendedEditor.ControlWidgets;

[CustomEditor(typeof(TypeCreator))]
public class TypeCreatorControlWidget : ControlObjectWidget
{
    private const string ArgumentsPropertyName = "Arguments";

    private Type? Type
    {
        get
        {
            return SerializedObject.GetProperty(nameof(TypeCreator.Type))?.GetValue<Type>();
        }
    }

    private readonly Layout _argumentsLayout;
    private ConstructorInfo? _constructor;

    public TypeCreatorControlWidget(SerializedProperty property) : base(property.Fix(), true)
    {
        SerializedObject.OnPropertyChanged += OnPropertyChanged;

        Layout = Layout.Column();
        Layout.Margin = 4;
        Layout.Spacing = 4;

        var attributes = SerializedProperty.GetAttributes();

        var limiter = new TypeSelectAttribute() { AllowNone = false, AllowAbstract = false };
        if(attributes.FirstOrDefault(x => x is TypeSelectAttribute) is TypeSelectAttribute outsideLimiter)
        {
            if(outsideLimiter.ValidatorName is not null)
            {
                outsideLimiter = new TypeSelectAttribute(outsideLimiter);
                outsideLimiter.FindAndAppendValidatorMethod(SerializedProperty);
            }

            attributes = attributes.Where(x => x is not TypeSelectAttribute);
            limiter = outsideLimiter.RestrictBy(limiter);
        }

        var attributesWithLimiter = attributes.Append(limiter);

        var typeProperty = SerializedObject.GetProperty(nameof(TypeCreator.Type));
        var typePropertyProxy = typeProperty.CreateProxy(attributesWithLimiter);

        var argumentsProperty = SerializedObject.GetProperty(ArgumentsPropertyName);
        var argumentsPropertyProxy = argumentsProperty.CreateProxy(argumentsProperty.GetAttributes());


        Layout.Add(new GenericTypeSelectControlWidget(typePropertyProxy));
        _argumentsLayout = Layout.Add(Layout.Row());

        if(!UpdateArguments())
            RebuildArgumentsLayout();
    }

    private void RebuildArgumentsLayout()
    {
        _argumentsLayout.Clear(true);

        var ignoreAttributes = SerializedProperty.GetAttributes<IgnoreArgumentAttribute>();
        var parameters = (_constructor?.GetParameters() ?? [])
            .Index().Where(x => !ignoreAttributes.Any(a => a.Index == x.Index)).Select(x => x.Item).ToArray();

        Layout.Margin = parameters.Length > 0 ? 4 : 0;
        if(parameters.Length == 0)
            return;

        var argumentsProperty = SerializedObject.GetProperty(ArgumentsPropertyName);

        var argumentsList = Layout.Column();
        _argumentsLayout.Add(argumentsList);

        var arguments = argumentsProperty.GetValue<Any<object>[]>() ?? [];
        for(int i = 0; i < arguments.Length; ++i)
        {
            int index = i;
            var argument = arguments[i];
            if(argument.Value is null)
                continue;

            var type = parameters[i].ParameterType;

            var attributes = new Attribute[] {
                new TypeSelectAttribute(false, false)
                {
                    RequiredBaseTypes = [type],
                },
                new InlineEditorAttribute()
            };

            var name = parameters[i].Name!;
            var property = TypeLibrary.CreatePropertyFixed(name, () => arguments[index],
                v => arguments[index] = v, attributes, SerializedObject);

            argumentsList.Add(ControlSheet.CreateRow(property));
        }
    }

    private void OnPropertyChanged(SerializedProperty serializedProperty)
    {
        if(serializedProperty.Name == nameof(TypeCreator.Type) && serializedProperty.Parent == SerializedObject)
            UpdateArguments();
    }

    private bool UpdateArguments()
    {
        var ignoreAttributes = SerializedProperty.GetAttributes<IgnoreArgumentAttribute>();
        var requireArgumentAttributes = SerializedProperty.GetAttributes<RequireArgumentAttribute>();

        var argumentsProperty = SerializedObject.GetProperty(ArgumentsPropertyName);

        var type = Type;
        if(type is null)
        {
            argumentsProperty.SetValue(Array.Empty<Any<object>>());
            RebuildArgumentsLayout();
            return true;
        }

        var oldConstructor = _constructor;
        _constructor = type.GetConstructors(BindingFlags.Instance | BindingFlags.Public)
            .Where(x =>
            {
                var parameters = x.GetParameters();

                foreach(var attribute in requireArgumentAttributes)
                {
                    if(attribute.Index < 0 || attribute.Index >= parameters.Length || attribute.Type == null)
                        continue;

                    var parameterInfo = parameters[attribute.Index];
                    var parameterType = parameterInfo.ParameterType;
                    if(parameterType.IsByRef)
                        parameterType = parameterType.GetElementType();

                    if(attribute.Type != parameterType)
                        return false;
                }

                return true;
            })
            .OrderBy(x => x.GetParameters().Length).FirstOrDefault();

        if(_constructor == oldConstructor)
            return false;

        if(_constructor is null)
        {
            Log.Error($"Constructor for {type.Name} not found.");
            argumentsProperty.SetValue(Array.Empty<Any<object>>());
            RebuildArgumentsLayout();
            return true;
        }

        var parameters = _constructor.GetParameters().Index()
            .Where(x => !ignoreAttributes.Any(a => a.Index == x.Index))
            .Select(x => x.Item).ToArray();

        var arguments = new Any<object>[parameters.Length];
        var oldArguments = argumentsProperty.GetValue<Any<object>[]>() ?? [];

        bool changedArgumentsValues = arguments.Length != oldArguments.Length;
        for(int i = 0; i < arguments.Length; ++i)
        {
            var parameterInfo = parameters[i];
            var parameterType = parameterInfo.ParameterType;
            if(parameterType.IsByRef)
            {
                if(parameterInfo.IsOut)
                {
                    arguments[i] = default!;
                    continue;
                }

                parameterType = parameterType.GetElementType()!;
            }

            if(oldArguments.Length > i && (oldArguments[i] is Any<object> oldSerialzableObject) && (oldSerialzableObject.Value?.GetType().IsAssignableTo(parameterType) ?? false))
            {
                arguments[i] = oldArguments[i];
            }
            else
            {
                if(parameterType.IsGenericType && parameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
                {
                    var listType = typeof(List<>).MakeGenericType([parameterType.GetGenericArguments()[0]]);
                    arguments[i] = new(TypeLibrary.Create<object>(listType));
                }
                else
                {
                    arguments[i] = new(TypeLibrary.Create<object>(parameterType));
                }

                changedArgumentsValues = true;
            }

            if(arguments[i].Value is null)
                Log.Error($"Couldn't create parameter of type {parameterType}.");
        }

        if(changedArgumentsValues)
        {
            argumentsProperty.SetValue(arguments);
            RebuildArgumentsLayout();
        }

        return changedArgumentsValues;
    }

    protected override void PaintUnder()
    {
        if(!PaintBackground)
            return;

        var backgroundColor = Theme.ControlBackground.Lighten(0.5f);

        Paint.ClearPen();
        Paint.SetBrush(backgroundColor);
        Paint.DrawRect(LocalRect, Theme.ControlRadius);
    }
}