Code/Core/Node.Collection.cs
namespace Nodebox;

public partial class Node {
    public class Collection : TypeCollection {
        public Collection() : base() { }
        public Collection(IEnumerable<SerializableType> nodeTypes) : base(nodeTypes) { }
        public Collection(IEnumerable<Type> nodeTypes) : base(nodeTypes.Select(x => new SerializableType(x))) { }

        public static Collection From(ICollection<Type> nodeTypes, ICollection<Type> genericArguments) {
            var collection = new Collection();
            var availableTypes = genericArguments.ToList();

            foreach (var nodeType in nodeTypes) {
                if (nodeType == typeof(Node)) { continue; }
                var nodeTypeDesc = TypeLibrary.GetType(nodeType);
                if (!nodeTypeDesc.IsGenericType) {
                    collection.Add(nodeType);
                    continue;
                }

                var genericArgumentsCount = nodeTypeDesc.GenericArguments.Length;

                var permuteSource = new List<Type>[genericArgumentsCount];
                foreach (var i in Enumerable.Range(0, genericArgumentsCount)) {
                    permuteSource[i] = availableTypes;
                }

                foreach (var typeArgs in Permutations.Calculate(permuteSource)) {
                    if (!TryMakeGenericType(nodeType, typeArgs, out var type)) {
                        continue;
                    }

                    collection.Add(type);
                }
            }

            return collection;
        }
        public static Collection FromAll(ICollection<Type> genericArguments) =>
            From([.. TypeLibrary.GetTypes<Node>().Select(x => x.TargetType)], genericArguments);
        public static Collection FromAllWithIntrinsics() =>
            FromAll([.. TypeLibrary.IntrinsicTypes.Where(x => !x.IsGenericType)]);

        public override IEnumerable<SerializableType> Find(string query, int maxDistance = 2) {
            var q = string.IsNullOrEmpty(query) ? "" : query.ToLowerInvariant();
            return this
                .Select(s => {
                    var td = TypeLibrary.GetType(s.Type);
                    var dist = Distance(s.Type, q);
                    return (item: s, dist, td);
                })
                .Where(p => !p.td.HasAttribute<HideAttribute>())
                .Where(p => p.dist <= maxDistance)
                .OrderBy(p => p.dist)
                .ThenBy(p => p.td.Name, StringComparer.OrdinalIgnoreCase)
                .Select(p => p.item)
                .Select(x => {
                    var attr = TypeLibrary.GetType(x).GetAttribute<IsSpecializationOfAttribute>(false);
                    if (attr == null) { return x; }
                    return new SerializableType(attr.Type);
                })
                .DistinctBy(x => x.GetHashCode());
        }
        public virtual IEnumerable<SerializableType> FindWithPin(Flow flow, Type pinType, string query, int maxDistance = 2) {
            var q = string.IsNullOrEmpty(query) ? "" : query.ToLowerInvariant();
            return this
                .Select(s => {
                    var td = TypeLibrary.GetType(s.Type);
                    var dist = Distance(s.Type, q);
                    return (item: s, td, dist, name: td.Name);
                })
                .Where(p => !p.td.HasAttribute<HideAttribute>())
                .Where(p => p.dist <= maxDistance)
                .Where(p => {
                    if (pinType == typeof(object)) { return true; }
                    var (inputPins, outputPins) = GetInitialPins(p.item);
                    var pins = flow == Flow.Input ? inputPins : outputPins;
                    return pins.Any(pin => pin.Type == pinType || pin.Type == typeof(object));
                })
                .OrderBy(p => p.dist)
                .ThenBy(p => p.name, StringComparer.OrdinalIgnoreCase)
                .Select(p => {
                    var attr = p.td.GetAttribute<IsSpecializationOfAttribute>(false);
                    if (attr == null) { return p.item; }
                    return new SerializableType(attr.Type);
                })
                .DistinctBy(x => x.GetHashCode());
        }

        [AttributeUsage(AttributeTargets.Class)]
        public class IsSpecializationOfAttribute(Type type) : Attribute {
            public Type Type { get; set; } = type;
        }
    }
}

public class TypeCollection : HashSet<SerializableType> {
    public TypeCollection() : base() { }
    public TypeCollection(IEnumerable<SerializableType> types) : base(types) { }
    public TypeCollection(IEnumerable<Type> types) : base(types.Select(x => new SerializableType(x))) { }

    public virtual IEnumerable<SerializableType> Find(string query, int maxDistance = 2) {
        var q = string.IsNullOrEmpty(query) ? "" : query.ToLowerInvariant();
        return this
            .Select(s => (item: s, dist: Distance(s.Type, q), td: TypeLibrary.GetType(s)))
            .Where(p => !p.td.HasAttribute<HideAttribute>())
            .Where(p => p.dist <= maxDistance)
            .OrderBy(p => p.dist)
            .ThenBy(p => TypeLibrary.GetType(p.item.Type).Name, StringComparer.OrdinalIgnoreCase)
            .Select(p => p.item);
    }

    protected virtual int Distance(Type type, string queryLower) {
        Assert.NotNull(type);
        if (string.IsNullOrEmpty(queryLower)) { return 0; }
        var typeDescription = TypeLibrary.GetType(type);
        int best = 9999999;
        bool CheckDistance(string s) {
            int len = Math.Min(s.Length, queryLower.Length);
            int d = s[..len].ToLowerInvariant().Distance(queryLower);
            if (d < best) {
                best = d;
                if (best == 0) { return true; }
            }
            return false;
        }

        if (CheckDistance(typeDescription.Name)) { return best; }

        foreach (var tag in typeDescription.Tags) {
            if (CheckDistance(tag)) { return best; }
        }

        foreach (var tag in typeDescription.Aliases) {
            if (CheckDistance(tag)) { return best; }
        }

        return best;
    }
}