Editor/Handlers/EditorSession.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace SboxMcp.Handlers;

/// <summary>
/// Reflective wrapper around <c>Editor.SceneEditorSession</c>. Direct compile-time
/// references to that type fail in the publish-wizard's library compile context
/// because Sandbox.Tools.dll isn't linked there — even though the type IS resolvable
/// when the editor mounts the addon at runtime.
///
/// All access goes through <see cref="ActiveObject"/> which lazily looks up
/// <c>Editor.SceneEditorSession.Active</c> via reflection. If the editor isn't
/// running (or the assembly isn't loaded), every property/method here returns a
/// safe default and methods become no-ops.
/// </summary>
public static class EditorSession
{
	private static readonly Lazy<Type> _sessionType = new( ResolveSessionType );

	private static Type ResolveSessionType()
	{
		// First try the well-known assembly-qualified name.
		var t = Type.GetType( "Editor.SceneEditorSession, Sandbox.Tools" );
		if ( t is not null ) return t;

		// Fall back to scanning all loaded assemblies — Sandbox.Tools.dll is
		// loaded but might be ResolveAssembly-named differently in some builds.
		foreach ( var asm in AppDomain.CurrentDomain.GetAssemblies() )
		{
			t = asm.GetType( "Editor.SceneEditorSession" );
			if ( t is not null ) return t;
		}
		return null;
	}

	/// <summary>The active <c>SceneEditorSession</c> as a boxed object, or null if no editor session.</summary>
	public static object ActiveObject
	{
		get
		{
			var t = _sessionType.Value;
			if ( t is null ) return null;
			return t.GetProperty( "Active", BindingFlags.Public | BindingFlags.Static )?.GetValue( null );
		}
	}

	public static Scene ActiveScene
	{
		get
		{
			var session = ActiveObject;
			if ( session is null ) return null;
			return session.GetType().GetProperty( "Scene" )?.GetValue( session ) as Scene;
		}
	}

	public static bool IsPlaying
	{
		get
		{
			var session = ActiveObject;
			if ( session is null ) return false;
			return session.GetType().GetProperty( "IsPlaying" )?.GetValue( session ) as bool? ?? false;
		}
	}

	public static bool HasUnsavedChanges
	{
		get
		{
			var session = ActiveObject;
			if ( session is null ) return false;
			return session.GetType().GetProperty( "HasUnsavedChanges" )?.GetValue( session ) as bool? ?? false;
		}
		set
		{
			var session = ActiveObject;
			if ( session is null ) return;
			var prop = session.GetType().GetProperty( "HasUnsavedChanges" );
			if ( prop is not null && prop.CanWrite ) prop.SetValue( session, value );
		}
	}

	public static IEnumerable<GameObject> SelectionGameObjects
	{
		get
		{
			var session = ActiveObject;
			if ( session is null ) return Enumerable.Empty<GameObject>();
			var sel = session.GetType().GetProperty( "Selection" )?.GetValue( session );
			if ( sel is not IEnumerable e ) return Enumerable.Empty<GameObject>();
			return e.OfType<GameObject>();
		}
	}

	public static void SelectionClear()
	{
		var session = ActiveObject;
		if ( session is null ) return;
		var sel = session.GetType().GetProperty( "Selection" )?.GetValue( session );
		sel?.GetType().GetMethod( "Clear", Type.EmptyTypes )?.Invoke( sel, null );
	}

	public static void SelectionAdd( object item )
	{
		var session = ActiveObject;
		if ( session is null ) return;
		var sel = session.GetType().GetProperty( "Selection" )?.GetValue( session );
		if ( sel is null ) return;
		var add = sel.GetType().GetMethods()
			.FirstOrDefault( m => m.Name == "Add" && m.GetParameters().Length == 1 );
		add?.Invoke( sel, new[] { item } );
	}

	public static void Undo()
	{
		var session = ActiveObject;
		var undoSys = session?.GetType().GetProperty( "UndoSystem" )?.GetValue( session );
		undoSys?.GetType().GetMethod( "Undo", Type.EmptyTypes )?.Invoke( undoSys, null );
	}

	public static void Redo()
	{
		var session = ActiveObject;
		var undoSys = session?.GetType().GetProperty( "UndoSystem" )?.GetValue( session );
		undoSys?.GetType().GetMethod( "Redo", Type.EmptyTypes )?.Invoke( undoSys, null );
	}

	public static void Save( bool saveAs = false )
	{
		var session = ActiveObject;
		session?.GetType().GetMethod( "Save", new[] { typeof( bool ) } )?.Invoke( session, new object[] { saveAs } );
	}

	public static void StopPlaying()
	{
		var session = ActiveObject;
		session?.GetType().GetMethod( "StopPlaying", Type.EmptyTypes )?.Invoke( session, null );
	}

	public static void SetPlaying( Scene scene )
	{
		var session = ActiveObject;
		session?.GetType().GetMethod( "SetPlaying", new[] { typeof( Scene ) } )?.Invoke( session, new object[] { scene } );
	}

	public static void OnEdited()
	{
		var session = ActiveObject;
		session?.GetType().GetMethod( "OnEdited",
			BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
			binder: null, types: Type.EmptyTypes, modifiers: null )
			?.Invoke( session, null );
	}

	public static void FullUndoSnapshot( string label )
	{
		var session = ActiveObject;
		session?.GetType().GetMethod( "FullUndoSnapshot", new[] { typeof( string ) } )
			?.Invoke( session, new object[] { label } );
	}

	public static bool CreateFromPath( string path )
	{
		var t = _sessionType.Value;
		if ( t is null ) return false;
		var m = t.GetMethod( "CreateFromPath", BindingFlags.Public | BindingFlags.Static );
		if ( m is null ) return false;
		m.Invoke( null, new object[] { path } );
		return true;
	}
}