Code/Attributes/TypeSelectAttribute.cs
using System;
using System.Collections.Generic;
using System.Linq;

namespace ExtendedEditor.Attributes;

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class TypeSelectAttribute : Attribute
{
    public bool? AllowNone { get; set; }
    public bool? AllowAbstract { get; set; }

    public Type[] AllowedTypes { get; set; } = [];
    public Type[] DisallowedTypes { get; set; } = [];

    public Type[] AllowedBaseTypes { get; set; } = [];
    public Type[] DisallowedBaseTypes { get; set; } = [];

    public Type[] RequiredBaseTypes { get; set; } = [];

    public string? ValidatorName { get; set; } = null;

    public Func<Type?, bool>? Validator { get; set; } = null;

    public TypeSelectAttribute()
    {
    }

    public TypeSelectAttribute(bool allowNone = false, bool allowAbstract = false)
    {
        AllowNone = allowNone;
        AllowAbstract = allowAbstract;
    }

    public TypeSelectAttribute(TypeSelectAttribute other)
    {
        AllowAbstract = other.AllowAbstract;
        AllowedTypes = other.AllowedTypes;
        DisallowedTypes = other.DisallowedTypes;
        AllowedBaseTypes = other.AllowedBaseTypes;
        DisallowedBaseTypes = other.DisallowedBaseTypes;
        RequiredBaseTypes = other.RequiredBaseTypes;
        Validator = other.Validator;
        ValidatorName = other.ValidatorName;
    }

    /// <summary>
    /// Returns true if type is allowed, disallowed otherwise.
    /// </summary>
    /// <remarks>
    /// <para>Quick explanation: read from top to bottom, first hit is the result</para>
    /// <para>If type is null and <see cref="AllowNone"/> is false - it is disallowed</para>
    /// <para>If type is abstract and <see cref="AllowAbstract"/> is false - it is disallowed</para>
    /// <para>If type doesn't pass method with <see cref="ValidatorName"/> name in class where <see cref="TypeSelectAttribute"/> us used - it is disallowed. (This method doesn't validate it, only via Editor)</para>
    /// <para>If type doesn't pass <see cref="Validator"/> - it is disallowed</para>
    /// <para>If type is it is not assignable to all of the types in <see cref="RequiredBaseTypes"/> - it is disallowed</para>
    /// <para>If type is in <see cref="AllowedTypes"/> - it is allowed</para>
    /// <para>If type is in <see cref="DisallowedTypes"/> - it is disallowed</para>
    /// <para>If type extends any of the types in <see cref="AllowedBaseTypes"/> - it is allowed</para>
    /// <para>If type extends any of the types in <see cref="DisallowedBaseTypes"/> - it is disallowed</para>
    /// <para>Otherwise - allowed</para>
    /// </remarks>
    public bool IsAllowed(Type? type)
    {
        if(type is null && !(AllowNone ?? true))
            return false;

        bool foundAllowed = false;

        if(type is not null)
        {
            if(type.IsAbstract && !(AllowAbstract ?? true))
                return false;

            if(RequiredBaseTypes is not null && RequiredBaseTypes.Any(t => !type.IsAssignableTo(t)))
                return false;

            if(AllowedTypes is not null && AllowedTypes.Contains(type))
                foundAllowed = true;

            if(!foundAllowed && DisallowedTypes is not null && DisallowedTypes.Contains(type))
                return false;

            if(!foundAllowed && AllowedBaseTypes is not null && AllowedBaseTypes.Any(t => type.IsAssignableTo(t)))
                foundAllowed = true;

            if(!foundAllowed && DisallowedBaseTypes is not null && DisallowedBaseTypes.Any(t => type.IsAssignableTo(t)))
                return false;
        }

        if(Validator is not null && !Validator.Invoke(type)) // do it last because it can be slow
            return false;

        return true;
    }

    public TypeSelectAttribute RestrictBy(TypeSelectAttribute restrictor)
    {
        bool? allowNone = (restrictor.AllowNone.HasValue && !restrictor.AllowNone.Value) ? false : AllowNone;
        bool? allowAbstract = (restrictor.AllowAbstract.HasValue && !restrictor.AllowAbstract.Value) ? false : AllowAbstract;
        var disallowedTypes = (DisallowedTypes ?? []).Concat(restrictor.DisallowedTypes ?? []);
        var disallowedBaseTypes = (DisallowedBaseTypes ?? []).Concat(restrictor.DisallowedBaseTypes ?? []);
        var requiredBaseTypes = (RequiredBaseTypes ?? []).Concat(restrictor.RequiredBaseTypes ?? []);

        IEnumerable<Type> allowedTypes = (AllowedTypes ?? []).Where(restrictor.IsAllowed).Concat(restrictor.AllowedTypes);
        IEnumerable<Type> allowedBaseTypes = (AllowedBaseTypes ?? []).Where(restrictor.IsAllowed).Concat(restrictor.AllowedBaseTypes);

        bool tester(Type? t) => (restrictor.Validator?.Invoke(t) ?? true) && (Validator?.Invoke(t) ?? true);

        return new TypeSelectAttribute()
        {
            AllowNone = allowNone,
            AllowAbstract = allowAbstract,
            AllowedTypes = [.. allowedTypes],
            DisallowedTypes = [.. disallowedTypes],
            AllowedBaseTypes = [.. allowedBaseTypes],
            DisallowedBaseTypes = [.. disallowedBaseTypes],
            RequiredBaseTypes = [.. requiredBaseTypes],
            Validator = tester,
        };
    }
}