Editor/SceneQueryHelpers.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace SboxMcpServer;

/// <summary>
/// Pure scene-data helpers: path building, component/tag enumeration, and
/// the canonical summary/detail object builders used by all tool handlers.
/// </summary>
internal static class SceneQueryHelpers
{
	/// <summary>Returns the scene-path of a GameObject, e.g. "Root/Parent/Child".</summary>
	internal static string GetObjectPath( GameObject go )
	{
		var parts = new List<string>();
		var current = go;
		while ( current != null )
		{
			parts.Insert( 0, current.Name );
			current = current.Parent;
		}
		return string.Join( "/", parts );
	}

	/// <summary>Returns a compact list of component type names for a GameObject.</summary>
	internal static List<string> GetComponentNames( GameObject go )
	{
		var names = new List<string>();
		foreach ( var comp in go.Components.GetAll() )
			names.Add( comp.GetType().Name );
		return names;
	}

	/// <summary>Returns all tags on a GameObject as a list of strings.</summary>
	internal static List<string> GetTags( GameObject go )
	{
		return go.Tags.TryGetAll().ToList();
	}

	/// <summary>
	/// Recursively walks all GameObjects in the scene, including those inside
	/// disabled parents. Use this instead of scene.GetAllObjects(true) whenever
	/// you need to find disabled objects.
	/// </summary>
	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>
	/// Recursively walks a subtree rooted at <paramref name="root"/>.
	/// </summary>
	/// <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;
	}

	/// <summary>
	/// Compact summary used in list results (find_game_objects, get_scene_hierarchy).
	/// </summary>
	internal static Dictionary<string, object> BuildObjectSummary( GameObject go )
	{
		var pos = go.WorldPosition;
		return new Dictionary<string, object>
		{
			["id"]         = go.Id.ToString(),
			["name"]       = go.Name,
			["path"]       = GetObjectPath( go ),
			["enabled"]    = go.Enabled,
			["active"]     = go.Active,
			["tags"]       = GetTags( go ),
			["components"] = GetComponentNames( go ),
			["position"]   = new Dictionary<string, object>
			{
				["x"] = MathF.Round( pos.x, 2 ),
				["y"] = MathF.Round( pos.y, 2 ),
				["z"] = MathF.Round( pos.z, 2 )
			},
			["childCount"]       = go.Children.Count,
			["isPrefabInstance"] = go.IsPrefabInstance,
			["prefabSource"]     = go.IsPrefabInstance ? go.PrefabInstanceSource : null,
			["isNetworkRoot"]    = go.IsNetworkRoot,
			["networkMode"]      = go.NetworkMode.ToString()
		};
	}

	/// <summary>
	/// Full detail object used by get_game_object_details.
	/// </summary>
	internal static Dictionary<string, object> BuildObjectDetail( GameObject go, bool includeChildrenRecursive = false )
	{
		var wp = go.WorldPosition;
		var wr = go.WorldRotation;
		var ws = go.WorldScale;
		var lp = go.LocalPosition;
		var lr = go.LocalRotation;
		var ls = go.LocalScale;

		var components = new List<Dictionary<string, object>>();
		foreach ( var comp in go.Components.GetAll() )
		{
			components.Add( new Dictionary<string, object>
			{
				["type"]    = comp.GetType().Name,
				["enabled"] = comp.Enabled
			} );
		}

		List<object> children;
		if ( includeChildrenRecursive )
		{
			children = go.Children
				.Select( c => (object)BuildObjectDetail( 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,
			["active"]  = go.Active,
			["tags"]    = GetTags( go ),
			["components"] = components,
			["worldTransform"] = new Dictionary<string, object>
			{
				["position"] = Vec3Dict( wp ),
				["rotation"] = RotDict( wr ),
				["scale"]    = Vec3Dict( ws )
			},
			["localTransform"] = new Dictionary<string, object>
			{
				["position"] = Vec3Dict( lp ),
				["rotation"] = RotDict( lr ),
				["scale"]    = Vec3Dict( ls )
			},
			["parent"] = go.Parent != null ? new Dictionary<string, object>
			{
				["id"]   = go.Parent.Id.ToString(),
				["name"] = go.Parent.Name
			} : null,
			["children"]         = children,
			["isRoot"]           = go.IsRoot,
			["isNetworkRoot"]    = go.IsNetworkRoot,
			["isPrefabInstance"] = go.IsPrefabInstance,
			["prefabSource"]     = go.IsPrefabInstance ? go.PrefabInstanceSource : null,
			["networkMode"]      = go.NetworkMode.ToString()
		};
	}

	// ── Private formatting helpers ─────────────────────────────────────────

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

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