using ScadaLink.Commons.Entities.Templates;
namespace ScadaLink.TemplateEngine;
///
/// Walks inheritance and composition chains to resolve effective template members.
/// Produces canonical (path-qualified) names for composed module members.
///
public static class TemplateResolver
{
///
/// Represents a resolved member from any point in the inheritance/composition hierarchy.
///
public sealed record ResolvedTemplateMember
{
public string CanonicalName { get; init; }
public string MemberType { get; init; } // "Attribute", "Alarm", "Script"
public int SourceTemplateId { get; init; }
public int MemberId { get; init; }
public bool IsLocked { get; init; }
public string? ModulePath { get; init; }
public ResolvedTemplateMember(string canonicalName, string memberType, int sourceTemplateId, int memberId, bool isLocked, string? modulePath = null)
{
CanonicalName = canonicalName;
MemberType = memberType;
SourceTemplateId = sourceTemplateId;
MemberId = memberId;
IsLocked = isLocked;
ModulePath = modulePath;
}
}
///
/// Resolves all effective members for a template, walking inheritance and composition chains.
/// Child members override parent members of the same canonical name (unless locked).
///
public static IReadOnlyList ResolveAllMembers(
int templateId,
IReadOnlyList allTemplates)
{
var lookup = allTemplates.ToDictionary(t => t.Id);
if (!lookup.TryGetValue(templateId, out var template))
return Array.Empty();
// Build inheritance chain from root to leaf (root first, child last)
var chain = BuildInheritanceChain(templateId, lookup);
// Start with root members, apply overrides from each child
var effectiveMembers = new Dictionary(StringComparer.Ordinal);
foreach (var tmpl in chain)
{
// Direct members
AddDirectMembers(tmpl, prefix: null, effectiveMembers);
// Composed module members
foreach (var composition in tmpl.Compositions)
{
if (lookup.TryGetValue(composition.ComposedTemplateId, out var composedTemplate))
{
AddComposedMembers(composedTemplate, composition.InstanceName, lookup, effectiveMembers, new HashSet());
}
}
}
return effectiveMembers.Values.ToList();
}
///
/// Gets the inheritance chain from root ancestor to the specified template.
///
public static IReadOnlyList BuildInheritanceChain(
int templateId,
IReadOnlyDictionary lookup)
{
var chain = new List();
var currentId = templateId;
var visited = new HashSet();
while (currentId != 0 && lookup.TryGetValue(currentId, out var current))
{
if (!visited.Add(currentId))
break; // Safety: cycle detected
chain.Add(current);
currentId = current.ParentTemplateId ?? 0;
}
chain.Reverse(); // Root first
return chain;
}
///
/// Finds a member by canonical name in the resolved member set.
/// Used to check override/lock constraints.
///
public static ResolvedTemplateMember? FindMemberByCanonicalName(
string canonicalName,
int parentTemplateId,
IReadOnlyList allTemplates)
{
var members = ResolveAllMembers(parentTemplateId, allTemplates);
return members.FirstOrDefault(m => m.CanonicalName == canonicalName);
}
private static void AddDirectMembers(
Template template,
string? prefix,
Dictionary effectiveMembers)
{
foreach (var attr in template.Attributes)
{
var canonicalName = prefix == null ? attr.Name : $"{prefix}.{attr.Name}";
effectiveMembers[canonicalName] = new ResolvedTemplateMember(
canonicalName, "Attribute", template.Id, attr.Id, attr.IsLocked, prefix);
}
foreach (var alarm in template.Alarms)
{
var canonicalName = prefix == null ? alarm.Name : $"{prefix}.{alarm.Name}";
effectiveMembers[canonicalName] = new ResolvedTemplateMember(
canonicalName, "Alarm", template.Id, alarm.Id, alarm.IsLocked, prefix);
}
foreach (var script in template.Scripts)
{
var canonicalName = prefix == null ? script.Name : $"{prefix}.{script.Name}";
effectiveMembers[canonicalName] = new ResolvedTemplateMember(
canonicalName, "Script", template.Id, script.Id, script.IsLocked, prefix);
}
}
private static void AddComposedMembers(
Template template,
string prefix,
Dictionary lookup,
Dictionary effectiveMembers,
HashSet visited)
{
if (!visited.Add(template.Id))
return;
AddDirectMembers(template, prefix, effectiveMembers);
foreach (var composition in template.Compositions)
{
if (lookup.TryGetValue(composition.ComposedTemplateId, out var nested))
{
var nestedPrefix = $"{prefix}.{composition.InstanceName}";
AddComposedMembers(nested, nestedPrefix, lookup, effectiveMembers, visited);
}
}
}
}