Editor/ContructedGenericTypeControlWidget.cs
using System.Reflection;
using System.Runtime.CompilerServices;
using Facepunch.ActionGraphs;
using Sandbox.Diagnostics;
using Nodebox.Attributes;
using Editor.NodeEditor;
using Nodebox.Extensions;

namespace Nodebox.Editor;

[CustomEditor(typeof(Type), WithAllAttributes = [typeof(GenericAttribute)])]
public class ConstructedGenericTypeControlWidget : ControlWidget {
	protected static HashSet<Type> GenSystemTypes() => [
		typeof(object),
		typeof(char), typeof(string),
		typeof(bool),
		typeof(byte), typeof(sbyte),
		typeof(ushort), typeof(short),
		typeof(uint), typeof(int),
		typeof(ulong), typeof(long),
		typeof(float), typeof(double),
		typeof(Nullable<>),
		typeof(List<>),
		typeof(Dictionary<,>),
		typeof(HashSet<>),
		typeof(Array),
		typeof(IList<>),
		typeof(ValueTuple<>),
		typeof(ValueTuple<,>),
		typeof(ValueTuple<,,>),
		typeof(ValueTuple<,,,>),
		typeof(ValueTuple<,,,,>),
		typeof(ValueTuple<,,,,,>),
		typeof(ValueTuple<,,,,,,>),
		typeof(ValueTuple<,,,,,,,>),
	];
	protected static HashSet<Type> SystemTypes { get; set; } = GenSystemTypes();

	// Whether or not this control supports multi-editing (if you have multiple GameObjects selected)
	public override bool SupportsMultiEdit => false;

	public Type ParentType { get; set; } = null;
	protected Menu _menu;

	protected Layout GenericsLayout { get; set; }

	public ConstructedGenericTypeControlWidget(SerializedProperty property) : base(property) {
		Cursor = CursorShape.Finger;

		Layout = Layout.Row();
		Layout.Spacing = 2;

		if (property.TryGetAttribute<TargetTypeAttribute>(out var targetTypeAttribute)) {
			ParentType = targetTypeAttribute.Type;
		}

		Layout.AddSeparator();
		GenericsLayout = Layout.AddRow();
		GenericsLayout.Spacing = 2;
	}

	public int[] GenericPath { get; protected set; } = null;
	public ConstructedGenericTypeControlWidget(SerializedProperty property, int[] genericPath) : this(property) {
		GenericPath = genericPath;

		LayoutGenerics();
	}

	protected override void PaintControl() {
		base.PaintControl();
		var value = SerializedProperty.GetValue<Type>();

		var color = IsControlHovered ? Theme.Blue : Theme.TextControl;
		var rect = LocalRect;

		rect = rect.Shrink(8, 0);

		var desc = value is not null ? TypeLibrary.GetType(value) : null;

		Paint.SetPen(color);
		Paint.DrawText(rect, desc == null ? "None" : Sandbox.DisplayInfo.ForGenericType(desc?.TargetType).Name, TextFlag.LeftCenter);

		Paint.SetPen(color);
		Paint.DrawIcon(rect, "Arrow_Drop_Down", 17, TextFlag.RightCenter);
	}

	protected override void OnMousePress(MouseEvent e) {
		base.OnMousePress(e);

		if (e.LeftMouseButton && !_menu.IsValid()) {
			OpenMenu();
		}
	}

	protected void OpenMenu() {
		_menu = CreateMenu(
			null,
			null,
			[.. SerializedProperty.GetAttributes()],
			TypeSelected,
			ParentType,
			true
		);

		_menu.DeleteOnClose = true;
		_menu.OpenAtCursor(true);
		_menu.MinimumWidth = ScreenRect.Width;
	}

	protected void TypeSelected(Type selectedType) {
		if (GenericPath == null || GenericPath.Length == 0) {
			SerializedProperty.SetValue(selectedType);
		} else {
			var type = SerializedProperty.GetValue<Type>();

			// Add<float, List<float>>
			// SetTypeAt(Add<float, List<float>>, int, [1, 0]);

			// SetTypeAt(Add<float, List<float>>, int, [1, 0], 0);
			// inargs'0 = [float, List<float>]
			// SetTypeAt(List<float>, int, [1, 0], 1);
			// inargs'1 = [float]
			// SetTypeAt(float, int, [1, 0], 2);
			// return int;
			// inargs'1 = [int]
			// return List<int>;
			// inargs'0 = [float, List<int>]
			// return Add<float, List<int>>

			static Type SetTypeAt(Type type, Type typeArgument, int[] path, int level = 0) {
				if (level > path.Length - 1) {
					return typeArgument;
				}

				var typeDesc = TypeLibrary.GetType(type);
				var inargs = TypeLibrary.GetGenericArguments(type);
				var index = path[level];
				inargs[index] = SetTypeAt(inargs[index], typeArgument, path, level + 1);
				return typeDesc.MakeGenericType(inargs);
			}

			type = SetTypeAt(type, selectedType, GenericPath);

			SerializedProperty.SetValue(type);
		}

		SignalValuesChanged();
		LayoutGenerics();
	}

	[EditorEvent.Hotload]
	protected void LayoutGenerics() {
		var attribute = SerializedProperty.GetAttributes<GenericAttribute>().FirstOrDefault();
		if (!attribute.WithArguments) { return; }
		GenericsLayout.Clear(true);

		var type = SerializedProperty.GetValue<Type>();
		if (type == null) { return; }
		var typeDesc = TypeLibrary.GetType(type);
		if (!typeDesc.IsGenericType) {
			return;
		}

		Type[] arguments = type.GetGenericArguments();
		if (GenericPath != null) {
			for (int i = 0; i < GenericPath.Length; i++) {
				type = arguments[GenericPath[i]];
				typeDesc = TypeLibrary.GetType(type);
				arguments = type.GetGenericArguments();
			}
		}

		if (arguments == null) { return; }

		foreach (var pair in arguments.Enumerate()) {
			var genericPathCopy = (GenericPath ?? []).Append(pair.Index).ToArray();
			var row = GenericsLayout.AddRow();
			row.Add(new ConstructedGenericTypeControlWidget(SerializedProperty, genericPathCopy));
		}
	}

	public static Menu CreateMenu(Type[] options = null, Type genericParam = null, Attribute[] attributes = null, Action<Type> action = null, Type parentType = null, bool canSetNone = false) {
		var types = GetPossibleTypes(options, genericParam, attributes, parentType)
			.ToArray();

		var menu = new ContextMenu(null);

		menu.AddLineEdit("Filter",
			placeholder: "Filter Types..",
			autoFocus: true,
			onChange: s => PopulateTypeMenu(menu, types, action, s));

		menu.AboutToShow += () => {
			PopulateTypeMenu(menu, types, action, canSetNone: canSetNone);
		};

		return menu;
	}

	protected record TypeOption(Menu.PathElement[] Path, Type Type, string Title, string Description, string Icon);

	protected static string FormatAssemblyName(Assembly asm) {
		var name = asm.GetName().Name!;

		if (name.StartsWith("package.", StringComparison.OrdinalIgnoreCase)) {
			name = name["package.".Length..];
		}

		if (name.StartsWith("local.", StringComparison.OrdinalIgnoreCase)) {
			name = name["local.".Length..];
		}

		return name.ToTitleCase();
	}

	protected static string GetAssemblyQualifiedPath(TypeDescription typeDesc) {
		var path = !string.IsNullOrEmpty(typeDesc.Namespace)
			? typeDesc.Namespace.Replace('.', '/')
			: FormatAssemblyName(typeDesc.TargetType.Assembly);

		return path;
	}

	protected static string GetTypePath(TypeDescription typeDesc) {
		if (typeDesc.TargetType.DeclaringType != null) {
			return $"{GetTypePath(TypeLibrary.GetType(typeDesc.TargetType.DeclaringType))}/{typeDesc.Title}";
		}

		// var prefix = typeDesc.Group ?? GetAssemblyQualifiedPath(typeDesc);
		var prefix = GetAssemblyQualifiedPath(typeDesc);
		var icon = typeDesc.Icon;

		if (typeDesc.TargetType.IsAssignableTo(typeof(Resource))) {
			prefix = $"Resource/{prefix}";
			icon ??= "description";
		} else if (typeDesc.TargetType.IsAssignableTo(typeof(Component))) {
			prefix = $"Component/{prefix}";
			icon ??= "category";
		} else if (SystemTypes.Contains(typeDesc.TargetType)) {
			prefix = typeDesc.Group ?? typeDesc.Namespace?.Replace('.', '/') ?? "Sandbox";
		}

		icon ??= "check_box_outline_blank";

		return $"{prefix}/{Sandbox.DisplayInfo.ForGenericType(typeDesc.TargetType).Name}:{icon}@2000";
	}

	protected static TypeOption GetTypeOption(Type type) {
		var typeDesc = TypeLibrary.GetType(type);

		var path = typeDesc is { }
			? GetTypePath(typeDesc)
			: $"System/{type.Name}";

		return new TypeOption(Menu.GetSplitPath(path),
			type,
			type.Name,
			typeDesc?.Description,
			typeDesc?.Icon);
	}

	protected static IEnumerable<TypeOption> GetPossibleTypes(Type[] options = null, Type genericParam = null, Attribute[] attributes = null, Type parentType = null) {
		if (options is not null) {
			foreach (var option in options) {
				yield return GetTypeOption(option);
			}

			yield break;
		}

		var listedTypes = new HashSet<Type>();
		// var convertFrom = attributes?.OfType<HasConversionFromAttribute>().FirstOrDefault()?.Type;

		foreach (var type in SystemTypes) {
			if (parentType is not null && !type.IsAssignableTo(parentType)) { continue; }
			if (!listedTypes.Add(type)) { continue; }
			if (!SatisfiesConstraints(type, genericParam)) { continue; }
			// if (convertFrom is not null && !CanConvert(convertFrom, type)) continue;
			yield return GetTypeOption(type);
		}

		var componentTypes = TypeLibrary.GetTypes<Component>();
		var resourceTypes = TypeLibrary.GetTypes<GameResource>();
		var userTypes = TypeLibrary.GetTypes()
			.Where(x => x.TargetType.Assembly.IsPackage());

		foreach (var typeDesc in componentTypes.Union(resourceTypes).Union(userTypes)) {
			if (parentType is not null && !typeDesc.TargetType.IsAssignableTo(parentType)) { continue; }
			if (typeDesc.IsStatic) { continue; }
			// if (typeDesc.IsGenericType) { continue; }
			if (typeDesc.HasAttribute<CompilerGeneratedAttribute>()) { continue; }
			if (typeDesc.Name.StartsWith('<') || typeDesc.Name.StartsWith('_')) { continue; }
			if (!listedTypes.Add(typeDesc.TargetType)) { continue; }
			if (!SatisfiesConstraints(typeDesc.TargetType, genericParam)) { continue; }
			// if (convertFrom is not null && !CanConvert(convertFrom, typeDesc.TargetType)) continue;

			yield return GetTypeOption(typeDesc.TargetType);
		}
	}

	protected static bool SatisfiesConstraints(Type type, Type genericParam) {
		if (genericParam is null) {
			return true;
		}

		if (!genericParam.GenericParameterAttributes.AreSatisfiedBy(type)) {
			return false;
		}

		foreach (var constraint in genericParam.GetGenericParameterConstraints()) {
			if (ApplyGenericArgument(constraint, genericParam, type) is not { } resolvedConstraint) {
				return false;
			}

			if (resolvedConstraint.IsGenericParameter) {
				// A bit worried about a stack overflow here

				if (resolvedConstraint != genericParam && !SatisfiesConstraints(type, resolvedConstraint)) {
					return false;
				}
			} else if (!type.IsAssignableTo(resolvedConstraint)) {
				return false;
			}
		}

		foreach (var hasImpl in genericParam.GetCustomAttributes<HasImplementationAttribute>()) {
			// Easy case

			if (type.IsAssignableTo(hasImpl.BaseType)) {
				continue;
			}

			var anyImplementing = TypeLibrary.GetTypes(hasImpl.BaseType)
				.Where(x => !x.IsAbstract && !x.IsInterface)
				.Any(x => x.TargetType.IsAssignableTo(type));

			if (!anyImplementing) {
				return false;
			}
		}

		return true;
	}

	protected static Type ApplyGenericArgument(Type type, Type genericParam, Type genericArg) {
		if (type == genericParam) {
			return genericArg;
		}

		if (!type.ContainsGenericParameters) {
			return type;
		}

		if (type.IsByRef || type.IsArray) {
			if (ApplyGenericArgument(type.GetElementType(), genericParam, genericArg) is not { } elemType) {
				return null;
			}

			if (type.IsByRef) {
				return elemType.MakeByRefType();
			}

			Assert.True(type.IsArray);

			return type.GetArrayRank() == 1
				? elemType.MakeArrayType()
				: elemType.MakeArrayType(type.GetArrayRank());
		}

		if (!type.IsGenericType) {
			return null;
		}

		try {
			return type.GetGenericTypeDefinition()
				.MakeGenericType([.. type.GetGenericArguments().Select(x => ApplyGenericArgument(x, genericParam, genericArg))]);
		}
		catch (ArgumentException) {
			// Generic constraints not satisfied

			return null;
		}
	}

	protected static void PopulateTypeMenu(Menu menu, IEnumerable<TypeOption> types, Action<Type> action, string filter = null, bool canSetNone = false) {
		menu.RemoveMenus();
		menu.RemoveOptions();

		foreach (var widget in menu.Widgets.Skip(1)) {
			menu.RemoveWidget(widget);
		}

		if (canSetNone) {
			menu.AddOption("None", "cancel", () => action?.Invoke(null));
		}

		const int maxFiltered = 10;

		var useFilter = !string.IsNullOrEmpty(filter);
		var truncated = 0;

		if (useFilter) {
			var filtered = types.Where(x => x.Type.Name.Contains(filter, StringComparison.OrdinalIgnoreCase)).ToArray();

			if (filtered.Length > maxFiltered + 1) {
				truncated = filtered.Length - maxFiltered;
				types = filtered.Take(maxFiltered);
			} else {
				types = filtered;
			}
		}

		menu.AddOptions(types, x => x.Path, x => action?.Invoke(x.Type), flat: useFilter);

		if (truncated > 0) {
			menu.AddOption($"...and {truncated} more");
		}

		menu.AdjustSize();
		menu.Update();
	}

	[EditorEvent.Hotload]
	public static void OnHotload() {
		SystemTypes = GenSystemTypes();
	}
}