#nullable enable
using System.Diagnostics.CodeAnalysis;
using T3.Core.Operator;
using T3.Core.Operator.Slots;
using T3.Core.Settings;
using T3.Editor.Gui.InputUi.SimpleInputUis;
namespace T3.Editor.UiModel.Helpers;
///
/// Aggregates information about all symbols (warnings, dependencies, usage, examples),
/// and also provides single-symbol analysis for dialogs and tools.
///
internal static class SymbolAnalysis
{
///
/// Detailed info per symbol id, filled by UpdateDetails().
///
internal static readonly Dictionary InformationForSymbolIds = new(1000);
internal static int TotalUsageCount;
///
/// All connections between input slots stored as hashes (sourceSlotId x targetSlotId).
/// Used by SymbolBrowser for relevancy weighting of frequent combinations.
///
internal static Dictionary ConnectionHashCounts = new();
internal static bool DetailsInitialized { get; private set; }
// Cache for usage counts (used by both bulk and single-symbol queries)
private static readonly Dictionary _latestUsageCounts = new();
internal static void LogInvalidSymbolDependencies()
{
if (!DetailsInitialized)
UpdateDetails();
foreach (var symbol in SymbolRegistry.SymbolsFromAllPackages()
.OrderBy(s => s.Namespace)
.ThenBy(s => s.Name))
{
if (!InformationForSymbolIds.TryGetValue(symbol.Id, out var infos))
continue;
if (!_packageDependencies.TryGetValue(symbol.SymbolPackage.Name, out var deps))
continue;
foreach (var requiredSymbolId in infos.RequiredSymbolIds)
{
if (!SymbolRegistry.TryGetSymbol(requiredSymbolId, out var depSymbol))
continue;
if (!deps.Contains(depSymbol.SymbolPackage.Name))
{
Log.Warning($"{symbol.Namespace}.{symbol.Name} depends on {depSymbol.Namespace}.{depSymbol.Name}");
}
}
}
}
internal static void LogInvalidAssetReference()
{
foreach (var symbol in SymbolRegistry.SymbolsFromAllPackages()
.OrderBy(s => s.Namespace)
.ThenBy(s => s.Name))
{
if (!_packageDependencies.TryGetValue(symbol.SymbolPackage.Name, out var deps))
continue;
// Symbol children
foreach (var child in symbol.Children.Values)
{
foreach (var input in child.Inputs.Values)
{
if (input.IsDefault)
continue;
if (input.InputDefinition.ValueType != typeof(string))
continue;
if (input.Value is not InputValue stringValue)
continue;
var pathValue = stringValue.Value;
if (string.IsNullOrEmpty(pathValue))
continue;
if (!SymbolUiRegistry.TryGetSymbolUi(child.Symbol.Id, out var childSymbolUi))
continue;
if (!childSymbolUi.InputUis.TryGetValue(input.Id, out var inputUi))
continue;
if (inputUi is not StringInputUi stringUi)
continue;
switch (stringUi.Usage)
{
case StringInputUi.UsageType.FilePath:
case StringInputUi.UsageType.DirectoryPath:
{
var assetPackageNameIndex = pathValue.IndexOf(':');
if (assetPackageNameIndex < 1)
break;
var assetPackageName = pathValue[..(assetPackageNameIndex)];
if (!deps.Contains(assetPackageName))
{
Log.Warning($"{symbol.Namespace}.[{symbol.Name}].{childSymbolUi.Symbol.Name} references {pathValue}");
}
break;
}
}
}
}
}
}
private static readonly string[] _libPackages = [FileLocations.LibPackageName, FileLocations.TypesPackageName, "Mediapipe", "t3.ndi", "t3.spout", "t3.unsplash"];
private static readonly Dictionary _packageDependencies
= new()
{
{ FileLocations.LibPackageName, [.._libPackages] },
{ FileLocations.ExamplesPackageName, [.._libPackages, FileLocations.ExamplesPackageName] },
{ FileLocations.SkillsPackageName, [.._libPackages, FileLocations.SkillsPackageName] }
};
///
/// Basic usage info used by symbol browser and relevancy search.
///
internal static void UpdateSymbolUsageCounts()
{
Log.Debug("Updating symbol counts...");
var usages = CollectSymbolUsageCounts();
ConnectionHashCounts = new Dictionary();
foreach (var symbolUi in EditorSymbolPackage.AllSymbolUis)
{
var symbolId = symbolUi.Symbol.Id;
if (!InformationForSymbolIds.TryGetValue(symbolId, out var info))
{
info = new SymbolInformation();
InformationForSymbolIds[symbolId] = info;
}
// Update connection counts
foreach (var connection in symbolUi.Symbol.Connections)
{
var hash = connection.SourceSlotId.GetHashCode() * 31
+ connection.TargetSlotId.GetHashCode();
ConnectionHashCounts.TryGetValue(hash, out var connectionCount);
ConnectionHashCounts[hash] = connectionCount + 1;
}
usages.TryGetValue(symbolId, out var count);
info.UsageCount = count;
}
}
///
/// Update collection with details useful for
/// library structure cleanup, browsing, etc.
///
internal static void UpdateDetails()
{
var usages = CollectSymbolUsageCounts();
InformationForSymbolIds.Clear();
TotalUsageCount = 0;
// Precompute reverse dependencies once for all symbols
var (_, reverseDeps) = CollectUsageAndReverseDependencies();
foreach (var symbolUi in EditorSymbolPackage.AllSymbolUis)
{
var symbol = symbolUi.Symbol;
usages.TryGetValue(symbol.Id, out var usageCount);
TotalUsageCount += usageCount;
var requiredSymbols = CollectRequiredSymbols(symbol);
var invalidRequirements = CollectInvalidRequirements(symbol, requiredSymbols).ToList();
reverseDeps.TryGetValue(symbol.Id, out var deps);
var dependingSymbols = deps ?? new HashSet();
InformationForSymbolIds[symbol.Id] = BuildSymbolInformation(
symbol,
symbolUi,
requiredSymbols,
invalidRequirements,
dependingSymbols,
usageCount);
}
DetailsInitialized = true;
}
///
/// Fast single-symbol analysis for dialogs, menus, etc.
/// Uses cached bulk data when available, otherwise computes on-demand.
///
/// The symbol to analyze.
/// Returns the collected information for the symbol.
///
/// If true, forces a fresh analysis for this symbol even if cached data is available.
/// If false, uses the cached information when possible.
///
internal static bool TryGetSymbolInfo(Symbol symbol, out SymbolInformation info, bool forceUpdate = false)
{
info = new SymbolInformation();
if (!symbol.TryGetSymbolUi(out var symbolUi))
return false;
// If UpdateDetails has run and no force update is requested, return the cached info
if (!forceUpdate && DetailsInitialized && InformationForSymbolIds.TryGetValue(symbol.Id, out var cached))
{
// Keep a copy so callers cannot accidentally mutate the cache.
info = cached;
// Keep usage count fresh if we have a newer value
if (_latestUsageCounts.TryGetValue(symbol.Id, out var usage))
info.UsageCount = usage;
return true;
}
// On-demand analysis for this single symbol
var requiredSymbols = CollectRequiredSymbols(symbol);
var invalidRequirements = CollectInvalidRequirements(symbol, requiredSymbols).ToList();
var (usageCounts, reverseDeps) = CollectUsageAndReverseDependencies();
usageCounts.TryGetValue(symbol.Id, out var usageCount);
reverseDeps.TryGetValue(symbol.Id, out var deps);
var dependingSymbols = deps ?? new HashSet();
info = BuildSymbolInformation(
symbol,
symbolUi,
requiredSymbols,
invalidRequirements,
dependingSymbols,
usageCount);
// Optionally refresh the cached entry if details are already initialized
// so that subsequent calls without forceUpdate can benefit.
if (DetailsInitialized)
{
InformationForSymbolIds[symbol.Id] = info;
}
return true;
}
///
/// Unified structure with all known symbol metadata.
///
public sealed class SymbolInformation
{
public List Warnings = [];
internal HashSet RequiredSymbolIds = [];
internal HashSet DependingSymbols = [];
public List InvalidRequiredIds = [];
public IReadOnlyList ExampleSymbolsIds = [];
internal int UsageCount;
internal bool LacksDescription;
internal bool LacksAllParameterDescription;
internal bool LacksSomeParameterDescription;
internal bool LacksParameterGrouping;
internal bool DependsOnObsoleteOps;
internal SymbolUi.SymbolTags Tags; // Copy to avoid reference to symbolUi
internal OperatorClassification OperatorType;
}
public enum OperatorClassification
{
Unknown = 0,
Lib,
Type,
Example,
T3,
Skill,
}
// Shared helpers (used by both bulk and single analysis)
#region Shared Helpers
internal static bool TryGetOperatorType(Symbol symbol, out OperatorClassification opType)
{
var ns = symbol.Namespace ?? string.Empty;
var rootSegment = ns.Split('.')[0];
opType = rootSegment switch
{
FileLocations.LibPackageName => OperatorClassification.Lib,
FileLocations.TypesPackageName => OperatorClassification.Type,
FileLocations.ExamplesPackageName => OperatorClassification.Example,
"t3" => OperatorClassification.T3,
FileLocations.SkillsPackageName => OperatorClassification.Skill,
_ => OperatorClassification.Unknown
};
return opType != OperatorClassification.Unknown;
}
private static SymbolInformation BuildSymbolInformation(Symbol symbol,
SymbolUi symbolUi,
HashSet requiredSymbols,
List invalidRequirements,
HashSet dependingSymbols,
int usageCount)
{
var inputUis = symbolUi.InputUis.Values;
var lacksDescription = string.IsNullOrWhiteSpace(symbolUi.Description);
var hasManyInputs = symbolUi.InputUis.Count > 2;
var lacksAllParamDesc = hasManyInputs &&
inputUis.All(i => string.IsNullOrWhiteSpace(i.Description));
var lacksSomeParamDesc = hasManyInputs &&
inputUis.Any(i => string.IsNullOrWhiteSpace(i.Description));
var lacksParameterGrouping = symbolUi.InputUis.Count > 4 &&
!inputUis.Any(i => i.AddPadding || !string.IsNullOrEmpty(i.GroupTitle));
var dependsOnObsolete = requiredSymbols
.Select(s => s.GetSymbolUi())
.Any(ui => ui != null && ui.Tags.HasFlag(SymbolUi.SymbolTags.Obsolete));
TryGetOperatorType(symbol, out var opType);
return new SymbolInformation
{
Warnings = new List(),
RequiredSymbolIds = requiredSymbols.Select(s => s.Id).ToHashSet(),
DependingSymbols = dependingSymbols,
InvalidRequiredIds = invalidRequirements,
ExampleSymbolsIds = ExampleSymbolLinking.GetExampleIds(symbol.Id),
UsageCount = usageCount,
LacksDescription = lacksDescription,
LacksAllParameterDescription = lacksAllParamDesc,
LacksSomeParameterDescription = lacksSomeParamDesc,
LacksParameterGrouping = lacksParameterGrouping,
DependsOnObsoleteOps = dependsOnObsolete,
Tags = symbolUi.Tags,
OperatorType = opType
};
}
private static HashSet CollectRequiredSymbols(Symbol root)
{
var all = new HashSet();
Collect(root);
return all;
void Collect(Symbol symbol)
{
foreach (var symbolChild in symbol.Children.Values)
{
if (!all.Add(symbolChild.Symbol))
continue;
Collect(symbolChild.Symbol);
}
}
}
///
/// Collect Ids of required symbols that are not within the list of projects
///
private static IEnumerable CollectInvalidRequirements(Symbol root, HashSet requiredSymbols)
{
var result = new List();
// Todo: implement this correctly?
HashSet validPackagesNames = new()
{
FileLocations.TypesPackageName,
FileLocations.LibPackageName,
FileLocations.ExamplesPackageName,
root.SymbolPackage.RootNamespace,
};
foreach (var r in requiredSymbols)
{
var projectId = r.SymbolPackage.RootNamespace;
if (validPackagesNames.Contains(projectId))
continue;
result.Add(r);
}
return result
.OrderBy(s => s.Namespace)
.ThenBy(s => s.Name)
.Select(s => s.Id);
}
private static Dictionary CollectSymbolUsageCounts()
{
var results = new Dictionary();
TotalUsageCount = 0;
foreach (var s in EditorSymbolPackage.AllSymbols)
{
foreach (var child in s.Children.Values)
{
results.TryGetValue(child.Symbol.Id, out var currentCount);
results[child.Symbol.Id] = currentCount + 1;
TotalUsageCount++;
}
}
_latestUsageCounts.Clear();
foreach (var kvp in results)
_latestUsageCounts[kvp.Key] = kvp.Value;
return results;
}
///
/// Builds per-symbol usage counts and a reverse dependency map in a single pass
/// over .
/// This is used by single-symbol analysis without running the full detail update.
///
private static (Dictionary UsageCounts, Dictionary> ReverseDependencies)
CollectUsageAndReverseDependencies()
{
var usageCounts = new Dictionary();
var reverseDeps = new Dictionary>();
foreach (var container in EditorSymbolPackage.AllSymbols)
{
var containerId = container.Id;
foreach (var child in container.Children.Values)
{
var childId = child.Symbol.Id;
usageCounts.TryGetValue(childId, out var current);
usageCounts[childId] = current + 1;
if (!reverseDeps.TryGetValue(childId, out var set))
{
set = new HashSet();
reverseDeps[childId] = set;
}
set.Add(containerId);
}
}
return (usageCounts, reverseDeps);
}
#endregion
public static bool TryGetFileInputFromInstance(Instance instance,
[NotNullWhen(true)] out InputSlot? stringInput,
[NotNullWhen(true)] out StringInputUi? stringInputUi)
{
stringInput = null;
stringInputUi = null;
var symbolUi = instance.GetSymbolUi();
foreach (var input in instance.Inputs)
{
if (input is not InputSlot tmpStringInput)
continue;
stringInput = tmpStringInput;
var inputUi = symbolUi.InputUis[input.Id];
if (inputUi is not StringInputUi { Usage: StringInputUi.UsageType.FilePath } tmpStringInputUi)
continue;
stringInputUi = tmpStringInputUi;
return true;
}
return false;
}
}