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

namespace SboxMcpServer;

/// <summary>
/// Handlers for scene-inspection MCP tools:
/// get_scene_summary, get_scene_hierarchy, find_game_objects,
/// find_game_objects_in_radius, get_game_object_details,
/// get_component_properties, and get_prefab_instances.
/// </summary>
internal static class SceneToolHandlers
{
	// ── get_scene_summary ──────────────────────────────────────────────────

	internal static object GetSceneSummary( JsonSerializerOptions jsonOptions )
	{
		var scene = ResolveScene();
		if ( scene == null )
			return ToolHandlerBase.TextResult( "No active scene. Open a scene or prefab in the editor." );

		var allObjects  = SceneQueryHelpers.WalkAll( scene, includeDisabled: true ).ToList();
		var rootObjects = scene.Children.ToList();
		int totalCount     = allObjects.Count;
		int rootCount      = rootObjects.Count;
		int enabledCount   = allObjects.Count( g => g.Enabled );
		int disabledCount  = allObjects.Count( g => !g.Enabled );

		// Component type frequency
		var compCounts = new Dictionary<string, int>( StringComparer.OrdinalIgnoreCase );
		foreach ( var go in allObjects )
			foreach ( var comp in go.Components.GetAll() )
			{
				var typeName = comp.GetType().Name;
				compCounts.TryGetValue( typeName, out var existing );
				compCounts[typeName] = existing + 1;
			}
		var topComponents = compCounts
			.OrderByDescending( kv => kv.Value )
			.Select( kv => new Dictionary<string, object> { ["type"] = kv.Key, ["count"] = kv.Value } )
			.ToList();

		// All unique tags
		var allTags = new HashSet<string>( StringComparer.OrdinalIgnoreCase );
		foreach ( var go in allObjects )
			foreach ( var tag in go.Tags.TryGetAll() )
				allTags.Add( tag );

		// Prefab source breakdown
		var prefabCounts = new Dictionary<string, int>( StringComparer.OrdinalIgnoreCase );
		foreach ( var go in allObjects.Where( g => g.IsPrefabInstance && g.PrefabInstanceSource != null ) )
		{
			var src = go.PrefabInstanceSource;
			prefabCounts.TryGetValue( src, out var existing );
			prefabCounts[src] = existing + 1;
		}
		var prefabBreakdown = prefabCounts
			.OrderByDescending( kv => kv.Value )
			.Select( kv => new Dictionary<string, object> { ["prefab"] = kv.Key, ["instances"] = kv.Value } )
			.ToList();

		// Network mode distribution
		var netModeCounts = new Dictionary<string, int>( StringComparer.OrdinalIgnoreCase );
		foreach ( var go in allObjects )
		{
			var mode = go.NetworkMode.ToString();
			netModeCounts.TryGetValue( mode, out var existing );
			netModeCounts[mode] = existing + 1;
		}

		// Root object quick list
		var rootNames = rootObjects.Select( g => new Dictionary<string, object>
		{
			["name"]       = g.Name,
			["id"]         = g.Id.ToString(),
			["enabled"]    = g.Enabled,
			["childCount"] = g.Children.Count,
			["components"] = SceneQueryHelpers.GetComponentNames( g )
		} ).ToList();

		var summary = new Dictionary<string, object>
		{
			["sceneName"]            = scene.Name,
			["totalObjects"]         = totalCount,
			["rootObjects"]          = rootCount,
			["enabledObjects"]       = enabledCount,
			["disabledObjects"]      = disabledCount,
			["uniqueTags"]           = allTags.OrderBy( t => t ).ToList(),
			["componentBreakdown"]   = topComponents,
			["prefabBreakdown"]      = prefabBreakdown,
			["networkModeBreakdown"] = netModeCounts,
			["rootObjectList"]       = rootNames
		};

		var json = JsonSerializer.Serialize( summary, jsonOptions );
		return ToolHandlerBase.TextResult( json );
	}

	// ── get_scene_hierarchy ────────────────────────────────────────────────

	internal static object GetSceneHierarchy( JsonElement args )
	{
		bool rootOnly = args.ValueKind != JsonValueKind.Undefined &&
			args.TryGetProperty( "rootOnly", out var roP ) && roP.GetBoolean();

		bool includeDisabled = true;
		if ( args.ValueKind != JsonValueKind.Undefined &&
			args.TryGetProperty( "includeDisabled", out var idP ) )
			includeDisabled = idP.GetBoolean();

		string rootId = null;
		if ( args.ValueKind != JsonValueKind.Undefined &&
			args.TryGetProperty( "rootId", out var ridP ) )
			rootId = ridP.GetString();

		var sb    = new StringBuilder();
		var scene = ResolveScene();

		if ( scene == null )
		{
			sb.Append( "No active scene. Open a scene or prefab in the editor." );
		}
		else
		{
			sb.AppendLine( $"Scene: {scene.Name}" );

			if ( !string.IsNullOrEmpty( rootId ) && Guid.TryParse( rootId, out var guid ) )
			{
				var subtreeRoot = SceneQueryHelpers.WalkAll( scene )
					.FirstOrDefault( g => g.Id == guid );

				if ( subtreeRoot == null )
				{
					sb.Append( $"No GameObject found with id='{rootId}'." );
				}
				else
				{
					void WalkSub( GameObject go, int depth )
					{
						if ( !includeDisabled && !go.Enabled ) return;
						if ( go.Name?.IndexOf( SceneQueryHelpers.IgnoreMarker, StringComparison.OrdinalIgnoreCase ) >= 0 ) return;
						if ( go.Tags.Has( SceneQueryHelpers.IgnoreTag ) ) return;
						ToolHandlerBase.AppendHierarchyLine( sb, go, depth, showChildCount: rootOnly );
						if ( !rootOnly )
							foreach ( var child in go.Children )
								WalkSub( child, depth + 1 );
					}
					WalkSub( subtreeRoot, 0 );
				}
			}
			else if ( rootOnly )
			{
				foreach ( var go in scene.Children )
				{
					if ( !includeDisabled && !go.Enabled ) continue;
					if ( go.Name?.IndexOf( SceneQueryHelpers.IgnoreMarker, StringComparison.OrdinalIgnoreCase ) >= 0 ) continue;
					if ( go.Tags.Has( SceneQueryHelpers.IgnoreTag ) ) continue;
					ToolHandlerBase.AppendHierarchyLine( sb, go, 0, showChildCount: true );
				}
			}
			else
			{
				void Walk( GameObject go, int depth )
				{
					if ( !includeDisabled && !go.Enabled ) return;
					if ( go.Name?.IndexOf( SceneQueryHelpers.IgnoreMarker, StringComparison.OrdinalIgnoreCase ) >= 0 ) return;
					if ( go.Tags.Has( SceneQueryHelpers.IgnoreTag ) ) return;
					ToolHandlerBase.AppendHierarchyLine( sb, go, depth, showChildCount: false );
					foreach ( var child in go.Children )
						Walk( child, depth + 1 );
				}
				foreach ( var go in scene.Children )
					Walk( go, 0 );
			}
		}

		return ToolHandlerBase.TextResult( sb.ToString() );
	}

	// ── find_game_objects ──────────────────────────────────────────────────

	internal static object FindGameObjects( JsonElement args, JsonSerializerOptions jsonOptions )
	{
		string nameContains  = null;
		string hasTag        = null;
		string hasComponent  = null;
		string pathContains  = null;
		bool   enabledOnly   = false;
		bool?  isNetworkRoot = null;
		bool?  isPrefabInst  = null;
		int    maxResults    = 50;
		string sortBy        = null;
		float? sortOriginX   = null;
		float? sortOriginY   = null;
		float? sortOriginZ   = null;

		if ( args.ValueKind != JsonValueKind.Undefined )
		{
			if ( args.TryGetProperty( "nameContains",     out var nc  ) ) nameContains  = nc.GetString();
			if ( args.TryGetProperty( "hasTag",           out var ht  ) ) hasTag        = ht.GetString();
			if ( args.TryGetProperty( "hasComponent",     out var hc  ) ) hasComponent  = hc.GetString();
			if ( args.TryGetProperty( "pathContains",     out var pc  ) ) pathContains  = pc.GetString();
			if ( args.TryGetProperty( "enabledOnly",      out var eo  ) ) enabledOnly   = eo.GetBoolean();
			if ( args.TryGetProperty( "isNetworkRoot",    out var inr ) ) isNetworkRoot = inr.GetBoolean();
			if ( args.TryGetProperty( "isPrefabInstance", out var ipi ) ) isPrefabInst  = ipi.GetBoolean();
			if ( args.TryGetProperty( "maxResults",       out var mr  ) ) maxResults    = Math.Clamp( mr.GetInt32(), 1, 500 );
			if ( args.TryGetProperty( "sortBy",           out var sb2 ) ) sortBy        = sb2.GetString();
			if ( args.TryGetProperty( "sortOriginX",      out var sox ) ) sortOriginX   = sox.GetSingle();
			if ( args.TryGetProperty( "sortOriginY",      out var soy ) ) sortOriginY   = soy.GetSingle();
			if ( args.TryGetProperty( "sortOriginZ",      out var soz ) ) sortOriginZ   = soz.GetSingle();
		}

		var scene = ResolveScene();
		if ( scene == null )
			return ToolHandlerBase.TextResult( "No active scene. Open a scene or prefab in the editor." );

		var allObjects = SceneQueryHelpers.WalkAll( scene, includeDisabled: true );
		var matches    = new List<Dictionary<string, object>>();
		int totalSearched = 0;

		foreach ( var go in allObjects )
		{
			totalSearched++;
			if ( enabledOnly && !go.Enabled ) continue;
			if ( !string.IsNullOrEmpty( nameContains ) &&
				go.Name.IndexOf( nameContains, StringComparison.OrdinalIgnoreCase ) < 0 ) continue;
			if ( !string.IsNullOrEmpty( hasTag ) && !go.Tags.Has( hasTag ) ) continue;
			if ( !string.IsNullOrEmpty( hasComponent ) )
			{
				bool found = go.Components.GetAll().Any( c =>
					c.GetType().Name.IndexOf( hasComponent, StringComparison.OrdinalIgnoreCase ) >= 0 );
				if ( !found ) continue;
			}
			if ( !string.IsNullOrEmpty( pathContains ) )
			{
				var path = SceneQueryHelpers.GetObjectPath( go );
				if ( path.IndexOf( pathContains, StringComparison.OrdinalIgnoreCase ) < 0 ) continue;
			}
			if ( isNetworkRoot.HasValue && go.IsNetworkRoot != isNetworkRoot.Value ) continue;
			if ( isPrefabInst.HasValue  && go.IsPrefabInstance != isPrefabInst.Value ) continue;

			matches.Add( SceneQueryHelpers.BuildObjectSummary( go ) );
		}

		matches = ApplySorting( matches, sortBy, sortOriginX, sortOriginY, sortOriginZ );

		bool truncated = matches.Count > maxResults;
		if ( truncated ) matches = matches.Take( maxResults ).ToList();

		var summary = $"Found {matches.Count} matching object(s) (searched {totalSearched} total).";
		if ( truncated )
			summary += $" Result limit ({maxResults}) reached — refine your filters for more specific results.";

		var json = JsonSerializer.Serialize( new { summary, results = matches }, jsonOptions );
		return ToolHandlerBase.TextResult( json );
	}

	// ── find_game_objects_in_radius ────────────────────────────────────────

	internal static object FindGameObjectsInRadius( JsonElement args, JsonSerializerOptions jsonOptions )
	{
		float  x           = 0f;
		float  y           = 0f;
		float  z           = 0f;
		float  radius      = 1000f;
		string hasTag      = null;
		string hasComponent = null;
		bool   enabledOnly = false;
		int    maxResults  = 50;

		if ( args.ValueKind != JsonValueKind.Undefined )
		{
			if ( args.TryGetProperty( "x",           out var xP  ) ) x           = xP.GetSingle();
			if ( args.TryGetProperty( "y",           out var yP  ) ) y           = yP.GetSingle();
			if ( args.TryGetProperty( "z",           out var zP  ) ) z           = zP.GetSingle();
			if ( args.TryGetProperty( "radius",      out var rP  ) ) radius      = rP.GetSingle();
			if ( args.TryGetProperty( "hasTag",      out var ht  ) ) hasTag      = ht.GetString();
			if ( args.TryGetProperty( "hasComponent",out var hc  ) ) hasComponent = hc.GetString();
			if ( args.TryGetProperty( "enabledOnly", out var eo  ) ) enabledOnly = eo.GetBoolean();
			if ( args.TryGetProperty( "maxResults",  out var mr  ) ) maxResults  = Math.Clamp( mr.GetInt32(), 1, 500 );
		}

		var scene = ResolveScene();
		if ( scene == null )
			return ToolHandlerBase.TextResult( "No active scene. Open a scene or prefab in the editor." );

		var origin     = new Vector3( x, y, z );
		float radiusSq = radius * radius;

		var matches = new List<(float dist, Dictionary<string, object> summary)>();

		foreach ( var go in SceneQueryHelpers.WalkAll( scene, includeDisabled: true ) )
		{
			if ( enabledOnly && !go.Enabled ) continue;
			if ( !string.IsNullOrEmpty( hasTag ) && !go.Tags.Has( hasTag ) ) continue;
			if ( !string.IsNullOrEmpty( hasComponent ) )
			{
				bool found = go.Components.GetAll().Any( c =>
					c.GetType().Name.IndexOf( hasComponent, StringComparison.OrdinalIgnoreCase ) >= 0 );
				if ( !found ) continue;
			}

			var pos    = go.WorldPosition;
			var diff   = pos - origin;
			var distSq = diff.x * diff.x + diff.y * diff.y + diff.z * diff.z;
			if ( distSq > radiusSq ) continue;

			matches.Add( (MathF.Sqrt( distSq ), SceneQueryHelpers.BuildObjectSummary( go )) );
		}

		matches.Sort( ( a, b ) => a.dist.CompareTo( b.dist ) );

		int totalCandidates = matches.Count;
		bool truncated = matches.Count > maxResults;
		var results = matches.Take( maxResults )
			.Select( m =>
			{
				m.summary["distanceFromOrigin"] = MathF.Round( m.dist, 2 );
				return m.summary;
			} )
			.ToList();

		var summary = $"Found {results.Count} object(s) within radius {radius} of ({x},{y},{z}) (searched {totalCandidates} candidates).";
		if ( truncated )
			summary += $" Result limit ({maxResults}) reached.";

		var json = JsonSerializer.Serialize( new { summary, results }, jsonOptions );
		return ToolHandlerBase.TextResult( json );
	}

	// ── get_game_object_details ────────────────────────────────────────────

	internal static object GetGameObjectDetails( JsonElement args, JsonSerializerOptions jsonOptions )
	{
		string idStr   = null;
		string nameStr = null;
		bool   recurse = false;

		if ( args.ValueKind != JsonValueKind.Undefined )
		{
			if ( args.TryGetProperty( "id",                       out var idP   ) ) idStr   = idP.GetString();
			if ( args.TryGetProperty( "name",                     out var nameP ) ) nameStr = nameP.GetString();
			if ( args.TryGetProperty( "includeChildrenRecursive", out var recP  ) ) recurse = recP.GetBoolean();
		}

		if ( string.IsNullOrEmpty( idStr ) && string.IsNullOrEmpty( nameStr ) )
			throw new ArgumentException( "Provide either 'id' or 'name'." );

		var scene = ResolveScene();
		if ( scene == null )
			return ToolHandlerBase.TextResult( "No active scene. Open a scene or prefab in the editor." );

		var target = FindGameObject( scene, idStr, nameStr );

		if ( target == null )
			return ToolHandlerBase.TextResult( $"No GameObject found matching id='{idStr}' name='{nameStr}'." );

		var json = JsonSerializer.Serialize( SceneQueryHelpers.BuildObjectDetail( target, recurse ), jsonOptions );
		return ToolHandlerBase.TextResult( json );
	}

	// ── get_component_properties ──────────────────────────────────────────

	internal static object GetComponentProperties( JsonElement args, JsonSerializerOptions jsonOptions )
	{
		string idStr         = null;
		string nameStr       = null;
		string componentType = null;

		if ( args.ValueKind != JsonValueKind.Undefined )
		{
			if ( args.TryGetProperty( "id",            out var idP ) ) idStr         = idP.GetString();
			if ( args.TryGetProperty( "name",          out var nmP ) ) nameStr       = nmP.GetString();
			if ( args.TryGetProperty( "componentType", out var ctP ) ) componentType = ctP.GetString();
		}

		if ( string.IsNullOrEmpty( idStr ) && string.IsNullOrEmpty( nameStr ) )
			throw new ArgumentException( "Provide either 'id' or 'name'." );
		if ( string.IsNullOrEmpty( componentType ) )
			throw new ArgumentException( "Provide 'componentType'." );

		var scene = ResolveScene();
		if ( scene == null )
			return ToolHandlerBase.TextResult( "No active scene. Open a scene or prefab in the editor." );

		var target = FindGameObject( scene, idStr, nameStr );

		if ( target == null )
			return ToolHandlerBase.TextResult( $"No GameObject found matching id='{idStr}' name='{nameStr}'." );

		var comp = target.Components.GetAll().FirstOrDefault( c =>
			c.GetType().Name.IndexOf( componentType, StringComparison.OrdinalIgnoreCase ) >= 0 );

		if ( comp == null )
			return ToolHandlerBase.TextResult( $"No component matching '{componentType}' found on '{target.Name}'." );

		var props = new Dictionary<string, object>();
		var type  = comp.GetType();

		foreach ( var prop in type.GetProperties( System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance ) )
		{
			if ( !prop.CanRead ) continue;
			try
			{
				var val = prop.GetValue( comp );
				props[prop.Name] = val switch
				{
					null     => null,
					bool b   => (object)b,
					int i    => (object)i,
					float f  => (object)MathF.Round( f, 4 ),
					double d => (object)Math.Round( d, 4 ),
					string s => (object)s,
					Enum e   => (object)e.ToString(),
					Vector3 v => (object)new { x = MathF.Round( v.x, 2 ), y = MathF.Round( v.y, 2 ), z = MathF.Round( v.z, 2 ) },
					_        => (object)val.ToString()
				};
			}
			catch
			{
				props[prop.Name] = "<error reading value>";
			}
		}

		var result = new Dictionary<string, object>
		{
			["gameObjectId"]   = target.Id.ToString(),
			["gameObjectName"] = target.Name,
			["componentType"]  = comp.GetType().Name,
			["enabled"]        = comp.Enabled,
			["properties"]     = props
		};

		var json = JsonSerializer.Serialize( result, jsonOptions );
		return ToolHandlerBase.TextResult( json );
	}

	// ── get_prefab_instances ───────────────────────────────────────────────

	internal static object GetPrefabInstances( JsonElement args, JsonSerializerOptions jsonOptions )
	{
		string prefabPath  = null;
		bool   enabledOnly = false;
		int    maxResults  = 100;

		if ( args.ValueKind != JsonValueKind.Undefined )
		{
			if ( args.TryGetProperty( "prefabPath",  out var pp ) ) prefabPath  = pp.GetString();
			if ( args.TryGetProperty( "enabledOnly", out var eo ) ) enabledOnly = eo.GetBoolean();
			if ( args.TryGetProperty( "maxResults",  out var mr ) ) maxResults  = Math.Clamp( mr.GetInt32(), 1, 500 );
		}

		var scene = ResolveScene();
		if ( scene == null )
			return ToolHandlerBase.TextResult( "No active scene. Open a scene or prefab in the editor." );

		// No prefabPath — return a full breakdown of all prefab sources
		if ( string.IsNullOrEmpty( prefabPath ) )
		{
			var counts = new Dictionary<string, int>( StringComparer.OrdinalIgnoreCase );
			foreach ( var go in SceneQueryHelpers.WalkAll( scene, includeDisabled: true ) )
			{
				if ( !go.IsPrefabInstance || go.PrefabInstanceSource == null ) continue;
				if ( enabledOnly && !go.Enabled ) continue;
				counts.TryGetValue( go.PrefabInstanceSource, out var c );
				counts[go.PrefabInstanceSource] = c + 1;
			}
			var breakdown = counts
				.OrderByDescending( kv => kv.Value )
				.Select( kv => new Dictionary<string, object> { ["prefab"] = kv.Key, ["instances"] = kv.Value } )
				.ToList();
			var bJson = JsonSerializer.Serialize( new { summary = $"{counts.Count} unique prefab(s) in scene.", breakdown }, jsonOptions );
			return ToolHandlerBase.TextResult( bJson );
		}

		// Return instances of a specific prefab
		var matches = new List<Dictionary<string, object>>();
		foreach ( var go in SceneQueryHelpers.WalkAll( scene, includeDisabled: true ) )
		{
			if ( matches.Count >= maxResults ) break;
			if ( !go.IsPrefabInstance ) continue;
			if ( enabledOnly && !go.Enabled ) continue;
			if ( go.PrefabInstanceSource == null ) continue;
			if ( go.PrefabInstanceSource.IndexOf( prefabPath, StringComparison.OrdinalIgnoreCase ) < 0 ) continue;
			matches.Add( SceneQueryHelpers.BuildObjectSummary( go ) );
		}

		bool truncated = matches.Count >= maxResults;
		var sumStr = $"Found {matches.Count} instance(s) of '{prefabPath}'.";
		if ( truncated ) sumStr += $" Result limit ({maxResults}) reached.";

		var json = JsonSerializer.Serialize( new { summary = sumStr, results = matches }, jsonOptions );
		return ToolHandlerBase.TextResult( json );
	}

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

	/// <summary>
	/// Returns the best available Scene: the live game scene if playing,
	/// then the active editor session scene (prefab editor, scene editor),
	/// then null if nothing is open.
	/// </summary>
	private static Scene ResolveScene()
	{
		// Prefer the editor session scene — this is what the user sees in the hierarchy panel.
		try
		{
			var session = SceneEditorSession.Active;
			if ( session?.Scene != null ) return session.Scene;

			// Fall back to the first available session
			foreach ( var s in SceneEditorSession.All )
				if ( s?.Scene != null ) return s.Scene;
		}
		catch { /* Editor API unavailable at runtime */ }

		// Fall back to runtime scene (only meaningful during play mode or tests)
		if ( Game.ActiveScene != null ) return Game.ActiveScene;
		return null;
	}

	/// <summary>
	/// Locates a GameObject by GUID or name, checking WalkAll first then
	/// scene.Children directly to catch disabled root objects.
	/// </summary>
	private static GameObject FindGameObject( Scene scene, string idStr, string nameStr )
	{
		GameObject target = null;

		if ( !string.IsNullOrEmpty( idStr ) && Guid.TryParse( idStr, out var guid ) )
		{
			target = SceneQueryHelpers.WalkAll( scene, includeDisabled: true ).FirstOrDefault( g => g.Id == guid );
			if ( target == null )
				target = scene.Children.FirstOrDefault( g => g.Id == guid );
		}

		if ( target == null && !string.IsNullOrEmpty( nameStr ) )
		{
			target = SceneQueryHelpers.WalkAll( scene, includeDisabled: true ).FirstOrDefault( g =>
				string.Equals( g.Name, nameStr, StringComparison.OrdinalIgnoreCase ) );
			if ( target == null )
				target = scene.Children.FirstOrDefault( g =>
					string.Equals( g.Name, nameStr, StringComparison.OrdinalIgnoreCase ) );
		}

		return target;
	}

	/// <summary>Applies optional sorting to a list of object summary dictionaries.</summary>
	private static List<Dictionary<string, object>> ApplySorting(
		List<Dictionary<string, object>> matches,
		string sortBy,
		float? sortOriginX, float? sortOriginY, float? sortOriginZ )
	{
		if ( string.IsNullOrEmpty( sortBy ) ) return matches;

		if ( sortBy.Equals( "name", StringComparison.OrdinalIgnoreCase ) )
			return matches.OrderBy( m => m["name"]?.ToString() ).ToList();

		if ( sortBy.Equals( "distance", StringComparison.OrdinalIgnoreCase ) &&
			sortOriginX.HasValue && sortOriginY.HasValue && sortOriginZ.HasValue )
		{
			var ox = sortOriginX.Value;
			var oy = sortOriginY.Value;
			var oz = sortOriginZ.Value;
			return matches.OrderBy( m =>
			{
				var pos = (Dictionary<string, object>)m["position"];
				var dx  = (float)(double)pos["x"] - ox;
				var dy  = (float)(double)pos["y"] - oy;
				var dz  = (float)(double)pos["z"] - oz;
				return MathF.Sqrt( dx * dx + dy * dy + dz * dz );
			} ).ToList();
		}

		if ( sortBy.Equals( "componentCount", StringComparison.OrdinalIgnoreCase ) )
			return matches.OrderByDescending( m => ( (List<string>)m["components"] ).Count ).ToList();

		return matches;
	}
}