Editor/OzmiumSceneHelpers.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using Editor;
using Sandbox;

namespace SboxMcpServer;

/// <summary>
/// Shared helpers: scene resolution, object tree walking, JSON arg extraction,
/// schema builders, and other utilities used by all handler files.
/// </summary>
internal static class OzmiumSceneHelpers
{
	// ── Scene resolution ────────────────────────────────────────────────────

	/// <summary>
	/// Returns the best available scene:
	/// 1. Game.ActiveScene (game is running)
	/// 2. Active SceneEditorSession.Scene (prefab/scene editor)
	/// 3. First available editor session
	/// 4. null
	/// </summary>
	internal static Scene ResolveScene()
	{
		// Prefer the editor session scene — this is what the user sees in the hierarchy panel.
		try
		{
			var active = SceneEditorSession.Active;
			if ( active?.Scene != null ) return active.Scene;
			foreach ( var s in SceneEditorSession.All )
				if ( s?.Scene != null ) return s.Scene;
		}
		catch { }
		// Fall back to runtime scene (only meaningful during play mode or tests)
		if ( Game.ActiveScene != null ) return Game.ActiveScene;
		return null;
	}

	// ── Tree walking ────────────────────────────────────────────────────────

	internal static IEnumerable<GameObject> WalkAll( Scene scene, bool includeDisabled = true )
	{
		foreach ( var root in scene.Children )
			foreach ( var go in WalkSubtree( root, includeDisabled ) )
				yield return go;
	}

	/// <summary>Name marker that causes MCP to skip an object and its entire subtree.</summary>
	internal const string IgnoreMarker = "(MCP IGNORE)";
	/// <summary>Tag that causes MCP to skip an object and its entire subtree.</summary>
	internal const string IgnoreTag = "mcp_ignore";
	/// <summary>Max children before auto-skipping subtree walk (parent still returned).</summary>
	internal const int MaxAutoWalkChildren = 25;

	internal static IEnumerable<GameObject> WalkSubtree( GameObject root, bool includeDisabled = true )
	{
		if ( !includeDisabled && !root.Enabled ) yield break;
		if ( root.Name != null && root.Name.IndexOf( IgnoreMarker, StringComparison.OrdinalIgnoreCase ) >= 0 ) yield break;
		if ( root.Tags.Has( IgnoreTag ) ) yield break;
		yield return root;
		// Auto-skip children of objects with too many children (performance guard)
		if ( root.Children.Count > MaxAutoWalkChildren ) yield break;
		foreach ( var child in root.Children )
			foreach ( var go in WalkSubtree( child, includeDisabled ) )
				yield return go;
	}

	// ── Object path ─────────────────────────────────────────────────────────

	internal static string GetObjectPath( GameObject go )
	{
		var parts = new List<string>();
		var cur = go;
		while ( cur != null ) { parts.Insert( 0, cur.Name ); cur = cur.Parent; }
		return string.Join( "/", parts );
	}

	// ── Component / tag helpers ─────────────────────────────────────────────

	internal static List<string> GetComponentNames( GameObject go )
		=> go.Components.GetAll().Select( c => c.GetType().Name ).ToList();

	internal static List<string> GetTags( GameObject go )
		=> go.Tags.TryGetAll().ToList();

	// ── Object builders ─────────────────────────────────────────────────────

	internal static Dictionary<string, object> BuildSummary( GameObject go )
	{
		var pos = go.WorldPosition;
		return new Dictionary<string, object>
		{
			["id"]               = go.Id.ToString(),
			["name"]             = go.Name,
			["path"]             = GetObjectPath( go ),
			["enabled"]          = go.Enabled,
			["tags"]             = GetTags( go ),
			["components"]       = GetComponentNames( go ),
			["position"]         = V3( pos ),
			["childCount"]       = go.Children.Count,
			["isPrefabInstance"] = go.IsPrefabInstance,
			["prefabSource"]     = go.IsPrefabInstance ? go.PrefabInstanceSource : null,
			["isNetworkRoot"]    = go.IsNetworkRoot,
			["networkMode"]      = go.NetworkMode.ToString()
		};
	}

	internal static Dictionary<string, object> BuildDetail( GameObject go, bool recurse = false )
	{
		var comps = new List<Dictionary<string, object>>();
		foreach ( var c in go.Components.GetAll() )
			comps.Add( new Dictionary<string, object> { ["type"] = c.GetType().Name, ["enabled"] = c.Enabled } );

		List<object> children;
		if ( recurse )
			children = go.Children.Select( c => (object)BuildDetail( c, true ) ).ToList();
		else
			children = go.Children.Select( c => (object)new Dictionary<string, object>
			{
				["id"] = c.Id.ToString(), ["name"] = c.Name,
				["enabled"] = c.Enabled, ["components"] = GetComponentNames( c )
			} ).ToList();

		return new Dictionary<string, object>
		{
			["id"]             = go.Id.ToString(),
			["name"]           = go.Name,
			["path"]           = GetObjectPath( go ),
			["enabled"]        = go.Enabled,
			["tags"]           = GetTags( go ),
			["components"]     = comps,
			["worldTransform"] = new Dictionary<string, object>
			{
				["position"] = V3( go.WorldPosition ),
				["rotation"] = Rot( go.WorldRotation ),
				["scale"]    = V3( go.WorldScale )
			},
			["localTransform"] = new Dictionary<string, object>
			{
				["position"] = V3( go.LocalPosition ),
				["rotation"] = Rot( go.LocalRotation ),
				["scale"]    = V3( go.LocalScale )
			},
			["parent"] = go.Parent != null
				? (object)new Dictionary<string, object> { ["id"] = go.Parent.Id.ToString(), ["name"] = go.Parent.Name }
				: null,
			["children"]         = children,
			["isNetworkRoot"]    = go.IsNetworkRoot,
			["isPrefabInstance"] = go.IsPrefabInstance,
			["prefabSource"]     = go.IsPrefabInstance ? go.PrefabInstanceSource : null,
			["networkMode"]      = go.NetworkMode.ToString()
		};
	}

	// ── Find by id/name ─────────────────────────────────────────────────────

	internal static GameObject FindGo( Scene scene, string id, string name )
	{
		GameObject target = null;
		if ( !string.IsNullOrEmpty( id ) && Guid.TryParse( id, out var guid ) )
		{
			target = WalkAll( scene, true ).FirstOrDefault( g => g.Id == guid )
			      ?? scene.Children.FirstOrDefault( g => g.Id == guid );
		}
		if ( target == null && !string.IsNullOrEmpty( name ) )
		{
			target = WalkAll( scene, true ).FirstOrDefault( g =>
				string.Equals( g.Name, name, StringComparison.OrdinalIgnoreCase ) )
				?? scene.Children.FirstOrDefault( g =>
				string.Equals( g.Name, name, StringComparison.OrdinalIgnoreCase ) );
		}
		return target;
	}

	/// <summary>
	/// Like FindGo but skips objects that don't have the requested component type.
	/// Resolves name collisions where multiple objects share the same name.
	/// </summary>
	internal static GameObject FindGoWithComponent<T>( Scene scene, string id, string name ) where T : Component
	{
		// If ID provided, try exact match first (ID is unique, no collision possible)
		if ( !string.IsNullOrEmpty( id ) && Guid.TryParse( id, out var guid ) )
		{
			var byId = WalkAll( scene, true ).FirstOrDefault( g => g.Id == guid )
			        ?? scene.Children.FirstOrDefault( g => g.Id == guid );
			if ( byId != null && byId.Components.Get<T>() != null )
				return byId;
		}

		// Name lookup — find first object with this name that has the component
		if ( !string.IsNullOrEmpty( name ) )
		{
			return WalkAll( scene, true ).FirstOrDefault( g =>
				string.Equals( g.Name, name, StringComparison.OrdinalIgnoreCase )
				&& g.Components.Get<T>() != null )
				?? scene.Children.FirstOrDefault( g =>
				string.Equals( g.Name, name, StringComparison.OrdinalIgnoreCase )
				&& g.Components.Get<T>() != null );
		}

		return null;
	}

	// ── Hierarchy line ──────────────────────────────────────────────────────

	internal static void AppendHierarchyLine( StringBuilder sb, GameObject go, int depth, bool showChildren )
	{
		var indent  = new string( ' ', depth * 2 );
		var comps   = GetComponentNames( go );
		var tags    = GetTags( go );
		var compStr = comps.Count > 0 ? $" [{string.Join( ", ", comps )}]" : "";
		var tagStr  = tags.Count > 0 ? $" #{string.Join( " #", tags )}" : "";
		var disStr  = go.Enabled ? "" : " (disabled)";
		var cStr    = showChildren ? $"  children:{go.Children.Count}" : "";
		sb.AppendLine( $"{indent}- {go.Name} (ID: {go.Id}){disStr}{tagStr}{compStr}{cStr}" );
	}

	// ── Formatting ──────────────────────────────────────────────────────────

	internal static Dictionary<string, object> V3( Vector3 v ) => new()
		{ ["x"] = MathF.Round( v.x, 2 ), ["y"] = MathF.Round( v.y, 2 ), ["z"] = MathF.Round( v.z, 2 ) };

	internal static Dictionary<string, object> Rot( Rotation r ) => new()
		{ ["pitch"] = MathF.Round( r.Pitch(), 2 ), ["yaw"] = MathF.Round( r.Yaw(), 2 ), ["roll"] = MathF.Round( r.Roll(), 2 ) };

	// ── JSON helpers (shared across all handler files) ───────────────────

	/// <summary>Wraps a plain text string into the MCP text-result envelope.</summary>
	internal static object Txt( string text ) => new { content = new object[] { new { type = "text", text } } };

	/// <summary>Extracts a typed value from a JsonElement, returning <paramref name="def"/> on missing/invalid.</summary>
	internal static T Get<T>( JsonElement el, string key, T def )
	{
		if ( el.ValueKind == JsonValueKind.Undefined ) return def;
		if ( !el.TryGetProperty( key, out var p ) ) return def;
		try
		{
			var t = typeof( T );
			if ( t == typeof( string ) ) return (T)(object)( p.ValueKind == JsonValueKind.Null ? null : p.GetString() );
			if ( t == typeof( bool ) )   return (T)(object)p.GetBoolean();
			if ( t == typeof( int ) )    return (T)(object)p.GetInt32();
			if ( t == typeof( float ) )  return (T)(object)p.GetSingle();
			return def;
		}
		catch { return def; }
	}

	/// <summary>Builds a tool definition object with name, description, and inputSchema.</summary>
	internal static Dictionary<string, object> S( string name, string desc, Dictionary<string, object> props, string[] req = null )
	{
		var schema = new Dictionary<string, object> { ["type"] = "object", ["properties"] = props };
		if ( req != null ) schema["required"] = req;
		return new Dictionary<string, object> { ["name"] = name, ["description"] = desc, ["inputSchema"] = schema };
	}

	/// <summary>Shorthand for building a property dictionary from (key, type, description) tuples.</summary>
	internal static Dictionary<string, object> Ps( params (string k, string type, string d)[] fields )
	{
		var d = new Dictionary<string, object>();
		foreach ( var (k, tp, desc) in fields )
			d[k] = new Dictionary<string, object> { ["type"] = tp, ["description"] = desc };
		return d;
	}

	/// <summary>Strips a leading "Assets/" or "assets/" prefix so AssetSystem.FindByPath works.</summary>
	internal static string NormalizePath( string path )
	{
		if ( path == null ) return null;
		if ( path.StartsWith( "Assets/", StringComparison.OrdinalIgnoreCase ) )
			path = path.Substring( "Assets/".Length );
		return path;
	}

	// ── Selection helpers ─────────────────────────────────────────────────

	/// <summary>
	/// Returns the currently selected GameObjects in the editor, using reflection
	/// to access the editor Selection API without hard dependencies.
	/// </summary>
	internal static List<GameObject> GetSelectedGameObjects()
	{
		var result = new List<GameObject>();
		try
		{
			var session = SceneEditorSession.Active;
			if ( session == null ) return result;
			var selProp = session.GetType().GetProperty( "Selection",
				System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance );
			var selObj = selProp?.GetValue( session );
			if ( selObj == null ) return result;
			// SelectionSystem implements IEnumerable<object> — enumerate directly
			if ( selObj is IEnumerable<object> objs )
				foreach ( var o in objs )
					if ( o is GameObject go ) result.Add( go );
		}
		catch { }
		return result;
	}
}