Editor/Lifecycle/ReflectionHelpers.cs
using System;
using System.Linq;
using System.Reflection;

namespace Sandbox.SecBox.Lifecycle;

// Reflection-only helpers for poking at engine internals secbox needs to wire
// into. Kept in one place so policy reviewers can audit the unsafe surface
// area quickly. Every method here documents *why* the engine doesn't expose a
// public alternative.
internal static class ReflectionHelpers
{
	// Locate Sandbox.PackageManager (internal static class). Returns null if
	// the engine renames or removes it - caller must handle gracefully.
	public static Type PackageManagerType()
	{
		// First try direct: Sandbox.Engine assembly is loaded by the time secbox
		// runs (we reference it). Scan loaded assemblies for the type.
		foreach ( var asm in AppDomain.CurrentDomain.GetAssemblies() )
		{
			if ( asm.GetName().Name != "Sandbox.Engine" ) continue;
			var t = asm.GetType( "Sandbox.PackageManager", throwOnError: false );
			if ( t != null ) return t;
		}
		return Type.GetType( "Sandbox.PackageManager, Sandbox.Engine", throwOnError: false );
	}

	// Insert a handler at the *front* of a field-like event's invocation list,
	// even though the C# `+=` operator can only append. The backing field of a
	// field-like event has the same name as the event and is typically private
	// static. We swap the field directly so our handler runs first.
	//
	// Why this matters: GameInstanceDll/ToolsDll subscribe at engine boot. If
	// secbox subscribes normally it runs LAST in the chain - by which point
	// the engine has already called LoadPackage and the package's static
	// constructors have run. Running first lets a synchronous scan + modal
	// dialog block before the engine ever sees the install event.
	public static bool InsertFirstInChain( Type containingType, string eventName, Delegate ourHandler )
	{
		if ( containingType == null || ourHandler == null ) return false;

		var field = containingType.GetField( eventName,
			BindingFlags.Static | BindingFlags.Instance |
			BindingFlags.NonPublic | BindingFlags.Public );

		if ( field == null ) return false;

		var existing = field.GetValue( null ) as Delegate;
		Delegate newChain = ourHandler;
		if ( existing != null )
		{
			foreach ( var d in existing.GetInvocationList() )
				newChain = Delegate.Combine( newChain, d );
		}
		field.SetValue( null, newChain );
		return true;
	}

	// Fallback subscription via the public event accessor - appends to the end
	// of the chain. Returns true on success.
	public static bool AppendToChain( Type containingType, string eventName, Delegate ourHandler )
	{
		if ( containingType == null || ourHandler == null ) return false;

		var ev = containingType.GetEvent( eventName,
			BindingFlags.Static | BindingFlags.Instance |
			BindingFlags.NonPublic | BindingFlags.Public );

		if ( ev == null ) return false;

		try { ev.AddEventHandler( null, ourHandler ); return true; }
		catch { return false; }
	}

	// Pull a property by name off an object via reflection, returning null on
	// any failure. For grabbing PackageManager.ActivePackage members from
	// outside Sandbox.Engine.
	public static object GetProp( object instance, string propertyName )
	{
		if ( instance == null ) return null;
		try
		{
			return instance.GetType().GetProperty( propertyName,
				BindingFlags.Public | BindingFlags.NonPublic |
				BindingFlags.Instance | BindingFlags.Static )?.GetValue( instance );
		}
		catch { return null; }
	}

	// Resolve a type by full name when it's declared `internal` in another
	// assembly. Editor.LibraryManager.LibraryList / LibraryDetail are the
	// callers - we can't reference them at compile time, but their hosting
	// assembly is loaded by the time editor code runs.
	//
	// Anchor type lets us short-circuit to the right assembly when known.
	// Falls back to scanning every loaded assembly. Returns null if the type
	// has been renamed or removed - caller must log and degrade gracefully.
	public static Type ResolveEditorType( string fullName, Type anchor = null )
	{
		if ( string.IsNullOrEmpty( fullName ) ) return null;
		try
		{
			if ( anchor != null )
			{
				var t = anchor.Assembly.GetType( fullName, throwOnError: false );
				if ( t != null ) return t;
			}
			foreach ( var asm in AppDomain.CurrentDomain.GetAssemblies() )
			{
				try
				{
					var t = asm.GetType( fullName, throwOnError: false );
					if ( t != null ) return t;
				}
				catch { /* dynamic / refl-only assemblies throw - skip */ }
			}
		}
		catch { }
		return null;
	}

	// Invoke a non-public instance method via reflection. Used to fall through
	// to BaseItemWidget.PaintItem(VirtualWidget) from inside our ItemPaint
	// wrapper so default row rendering still happens when we decorate.
	public static object InvokeNonPublic( object instance, string methodName, params object[] args )
	{
		if ( instance == null ) return null;
		try
		{
			var mi = instance.GetType().GetMethod( methodName,
				BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public );
			return mi?.Invoke( instance, args );
		}
		catch { return null; }
	}
}