using System.IO;
using UnityEngine;
using UnityEditor;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Collections.Generic;
using IntelligentProjectAnalyzer.Editor.Graphing;
namespace LLM.Editor.Analysis
{
///
/// Responsible for gathering and serializing context about staged assets
/// into a detailed, human-readable, and referential format for the LLM.
///
public static class ContextBuilder
{
///
/// Builds a detailed Tier 1 summary of the provided objects, including a pruned hierarchy for GameObjects.
///
/// The list of objects from the UI's staging area.
/// A formatted string summarizing the context for the LLM.
public static string BuildTier1Summary(List stagedObjects)
{
if (stagedObjects == null || stagedObjects.Count == 0 || stagedObjects.All(o => o == null))
{
return "No context provided.";
}
var summaryBuilder = new StringBuilder();
for (var i = 0; i < stagedObjects.Count; i++)
{
var obj = stagedObjects[i];
if (obj == null) continue;
var stableId = GetStableIdForObject(obj);
summaryBuilder.AppendLine($"\n## Subject {i + 1}: {obj.name} (ID: {stableId})");
summaryBuilder.AppendLine($"- Type: {obj.GetType().FullName}");
switch (obj)
{
case GameObject go:
AppendGameObjectContext(go, summaryBuilder);
break;
case MonoScript script:
AppendScriptContext(script, summaryBuilder);
break;
case ScriptableObject so:
AppendScriptableObjectContext(so, summaryBuilder);
break;
}
}
return summaryBuilder.ToString();
}
private static void AppendGameObjectContext(GameObject go, StringBuilder builder)
{
builder.AppendLine($"- Tag: {go.tag}");
builder.AppendLine($"- Layer: {LayerMask.LayerToName(go.layer)}");
var prefabPath = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(go);
if (!string.IsNullOrEmpty(prefabPath))
{
builder.AppendLine($"- Prefab Source: {prefabPath}");
}
builder.AppendLine("- Hierarchy (pruned to show nodes with custom scripts):");
AppendHierarchyNode(go.transform, builder, " ");
}
///
/// Recursively builds a string representation of the hierarchy, intelligently pruning branches
/// that do not contain any custom user scripts to keep the context concise.
///
private static void AppendHierarchyNode(Transform transform, StringBuilder builder, string indent)
{
// Always include the root of the hierarchy being analyzed.
builder.AppendLine($"{indent}- {transform.name} (ID: {transform.gameObject.GetInstanceID()})");
// List components on the current GameObject, prioritizing custom scripts.
var components = transform.GetComponents().Where(c => c).ToList();
var customScripts = components.Where(IsCustomScript).ToList();
var otherComponents = components.Except(customScripts);
foreach (var script in customScripts)
{
var monoScript = MonoScript.FromMonoBehaviour(script as MonoBehaviour);
var scriptGuid = monoScript ? AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(monoScript)) : "N/A";
builder.AppendLine($"{indent} - Component: {script.GetType().FullName} (Custom Script, GUID: {scriptGuid})");
}
foreach (var component in otherComponents)
{
builder.AppendLine($"{indent} - Component: {component.GetType().FullName}");
}
// --- Pruning Logic ---
var childrenWithScripts = new List();
var childrenWithoutScripts = new List();
foreach (Transform child in transform)
{
if (HasCustomScriptInChildren(child))
{
childrenWithScripts.Add(child);
}
else
{
childrenWithoutScripts.Add(child);
}
}
// Recurse into children that have scripts.
foreach (var child in childrenWithScripts)
{
AppendHierarchyNode(child, builder, indent + " ");
}
// Summarize the pruned children.
if (childrenWithoutScripts.Count > 0)
{
builder.AppendLine($"{indent} [+ {childrenWithoutScripts.Count} other child object(s) with no custom scripts]");
}
}
private static void AppendScriptContext(MonoScript script, StringBuilder builder)
{
var scriptClass = script.GetClass();
if (scriptClass != null)
{
if (scriptClass.BaseType != null && scriptClass.BaseType != typeof(MonoBehaviour))
{
builder.AppendLine($"- Inherits from: {scriptClass.BaseType.FullName}");
}
var interfaces = scriptClass.GetInterfaces();
if (interfaces.Length > 0)
{
builder.AppendLine($"- Implements: {string.Join(", ", interfaces.Select(i => i.Name))}");
}
var methods = scriptClass.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
.Where(m => !m.IsSpecialName); // Exclude property getters/setters, etc.
var methodInfos = methods as MethodInfo[] ?? methods.ToArray();
if (methodInfos.Any())
{
builder.AppendLine("- Public API:");
foreach (var method in methodInfos)
{
var parameters = string.Join(", ", method.GetParameters().Select(p => $"{p.ParameterType.Name} {p.Name}"));
builder.AppendLine($" - {method.ReturnType.Name} {method.Name}({parameters})");
}
}
}
var scriptPath = AssetDatabase.GetAssetPath(script);
if (!File.Exists(scriptPath)) return;
builder.AppendLine("- Script Content:");
builder.AppendLine("```csharp");
builder.AppendLine(File.ReadAllText(scriptPath));
builder.AppendLine("```");
// Append dependency graph if available
var guid = AssetDatabase.AssetPathToGUID(scriptPath);
_ = GraphJsonExporter.TryAppendHumanReadableGraph(guid, builder);
}
private static void AppendScriptableObjectContext(ScriptableObject so, StringBuilder builder)
{
builder.AppendLine("- Note: This is a ScriptableObject data asset.");
var fields = so.GetType()
.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
.Where(field => field.IsPublic || field.GetCustomAttribute() != null);
var fieldInfos = fields as FieldInfo[] ?? fields.ToArray();
if (!fieldInfos.Any()) return;
builder.AppendLine(" - Public Fields:");
foreach (var field in fieldInfos)
{
builder.AppendLine($" - {field.Name}: {field.GetValue(so)}");
}
}
private static string GetStableIdForObject(Object obj)
{
if (!AssetDatabase.TryGetGUIDAndLocalFileIdentifier(obj, out var guid, out long _)) return obj.GetInstanceID().ToString();
if (!string.IsNullOrEmpty(guid) && guid != "00000000000000000000000000000000")
{
return guid;
}
return obj.GetInstanceID().ToString();
}
///
/// Checks if a component is a user-defined script (i.e., part of the Assets folder).
///
private static bool IsCustomScript(Component component)
{
if (!component || component is not MonoBehaviour monoBehaviour) return false;
var script = MonoScript.FromMonoBehaviour(monoBehaviour);
if (!script) return false;
var path = AssetDatabase.GetAssetPath(script);
return !string.IsNullOrEmpty(path) && path.StartsWith("Assets/");
}
///
/// Recursively checks if a transform or any of its descendants contain a custom script.
///
private static bool HasCustomScriptInChildren(Transform transform)
{
if (transform.GetComponents().Any(IsCustomScript))
{
return true;
}
foreach (Transform child in transform)
{
if (HasCustomScriptInChildren(child))
{
return true;
}
}
return false;
}
}
}