using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Deployment;
///
/// Pure (no Blazor/DI) builder that turns a flat list of streamed attribute (and
/// alarm) events into a collapsible composition forest of
/// . Path-qualified canonical names are split on
/// '.' to derive branch nodes; the terminal segment carries the payload.
///
public static class DebugTreeBuilder
{
private const char Separator = '.';
///
/// Build the attribute composition forest.
///
/// Latest attribute value events (one per attribute).
///
/// Optional name-contains filter (case-insensitive, matched against
/// ). Null/empty/whitespace
/// keeps everything; matching leaves carry along their ancestor branches.
///
/// The root nodes of the attribute composition forest.
///
/// The caller is expected to pass at most one
/// per . The DebugView page
/// enforces this by keying a dictionary on the attribute name before calling this
/// method. Passing duplicate names would produce sibling leaves with identical
/// keys under the same parent branch.
///
public static IReadOnlyList BuildAttributeTree(
IEnumerable attributes, string? filter)
{
ArgumentNullException.ThrowIfNull(attributes);
var hasFilter = !string.IsNullOrWhiteSpace(filter);
// Roots keyed by the first path segment; branch nodes deduped by full prefix.
var roots = new List();
var branchByKey = new Dictionary(StringComparer.Ordinal);
foreach (var attr in attributes)
{
if (hasFilter &&
attr.AttributeName.IndexOf(filter!, StringComparison.OrdinalIgnoreCase) < 0)
{
continue;
}
var segments = attr.AttributeName.Split(Separator);
var parent = WalkBranches(segments, segments.Length - 1, roots, branchByKey);
parent.Add(new DebugTreeNode
{
Key = attr.AttributeName,
Segment = segments[^1],
Attribute = attr,
});
}
SortAndRollUp(roots);
return roots.AsReadOnly();
}
///
/// Build the alarm composition forest from a flat list of .
///
/// Latest alarm-state events (computed alarms, native conditions, placeholders).
///
/// Optional name-contains filter (case-insensitive). An alarm is kept when the filter
/// is a substring of any of ,
/// , or
/// . Null/empty/whitespace
/// keeps everything; kept items carry along their ancestor branches and, for native
/// conditions, their binding node.
///
/// The root nodes of the alarm composition forest.
///
/// Two flavours are placed differently:
///
/// - Computed ():
/// is a path-qualified canonical name; the alarm becomes a LEAF at that path, exactly
/// like an attribute.
/// - Native (OPC UA / MxAccess): the condition belongs to a source BINDING
/// identified by — itself a
/// path-qualified canonical name placed as a branch with
/// set. Non-placeholder events become
/// condition children (keyed canonical::sourceReference); a placeholder event
/// only materialises the (childless) binding node so the page can render
/// "no active conditions".
///
///
public static IReadOnlyList BuildAlarmTree(
IEnumerable alarms, string? filter)
{
ArgumentNullException.ThrowIfNull(alarms);
var hasFilter = !string.IsNullOrWhiteSpace(filter);
// Roots keyed by the first path segment; branch nodes (and native binding
// nodes) deduped by their full accumulated prefix / canonical name.
var roots = new List();
var branchByKey = new Dictionary(StringComparer.Ordinal);
foreach (var alarm in alarms)
{
if (hasFilter && !MatchesFilter(alarm, filter!))
{
continue;
}
if (alarm.Kind == AlarmKind.Computed)
{
AddComputedLeaf(alarm, roots, branchByKey);
}
else
{
AddNativeCondition(alarm, roots, branchByKey);
}
}
SortAndRollUpAlarms(roots);
return roots.AsReadOnly();
}
/// True when the filter is a case-insensitive substring of any of the alarm's name fields.
private static bool MatchesFilter(AlarmStateChanged alarm, string filter)
=> alarm.AlarmName.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0
|| alarm.SourceReference.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0
|| alarm.NativeSourceCanonicalName.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0;
/// Place a computed alarm as a leaf at its path-qualified canonical name.
private static void AddComputedLeaf(
AlarmStateChanged alarm,
List roots,
Dictionary branchByKey)
{
var segments = alarm.AlarmName.Split(Separator);
var parent = WalkBranches(segments, segments.Length - 1, roots, branchByKey);
parent.Add(new DebugTreeNode
{
Key = alarm.AlarmName,
Segment = segments[^1],
Alarm = alarm,
});
}
///
/// Place a native condition under its source-binding branch node. A placeholder only
/// materialises the (childless) binding node; a real condition adds a child leaf keyed
/// canonical::sourceReference.
///
private static void AddNativeCondition(
AlarmStateChanged alarm,
List roots,
Dictionary branchByKey)
{
if (string.IsNullOrEmpty(alarm.NativeSourceCanonicalName)) return;
var canonical = alarm.NativeSourceCanonicalName;
var segments = canonical.Split(Separator);
// Walk/create the ancestor branches above the binding node, then get-or-create
// the binding node itself (deduped by canonical name, flagged IsNativeBinding).
var parent = WalkBranches(segments, segments.Length - 1, roots, branchByKey);
if (!branchByKey.TryGetValue(canonical, out var binding))
{
binding = new DebugTreeNode
{
Key = canonical,
Segment = segments[^1],
IsNativeBinding = true,
};
branchByKey[canonical] = binding;
parent.Add(binding);
}
// Placeholder: leave the binding node childless ("no active conditions").
if (alarm.IsConfiguredPlaceholder)
{
return;
}
binding.Children.Add(new DebugTreeNode
{
Key = canonical + "::" + alarm.SourceReference,
Segment = alarm.SourceReference,
Alarm = alarm,
});
}
///
/// Walk (creating as needed) the branch nodes for the first
/// segments, deduping by accumulated prefix, and return the child list at that depth.
///
private static List WalkBranches(
string[] segments,
int count,
List roots,
Dictionary branchByKey)
{
var currentLevel = roots;
var prefix = string.Empty;
for (var i = 0; i < count; i++)
{
prefix = prefix.Length == 0 ? segments[i] : prefix + Separator + segments[i];
if (!branchByKey.TryGetValue(prefix, out var branch))
{
branch = new DebugTreeNode { Key = prefix, Segment = segments[i] };
branchByKey[prefix] = branch;
currentLevel.Add(branch);
}
currentLevel = branch.Children;
}
return currentLevel;
}
///
/// Recursively sort each node's children by
/// (ordinal) and roll up alarm state post-order:
/// is when any descendant alarm leaf/condition is Active
/// (placeholders excluded), and tallies those
/// active descendants. A native binding node rolls up over its condition children the
/// same way. (Bad-quality is attribute-only and left default for alarm nodes.)
///
private static void SortAndRollUpAlarms(List nodes)
{
nodes.Sort(static (a, b) => string.CompareOrdinal(a.Segment, b.Segment));
foreach (var node in nodes)
{
// A leaf's own contribution: Active and not a configured placeholder.
var ownActive = IsActiveAlarm(node);
var activeCount = ownActive ? 1 : 0;
if (node.Children.Count > 0)
{
SortAndRollUpAlarms(node.Children);
activeCount += node.Children.Sum(static c => c.ActiveCount);
}
node.ActiveCount = activeCount;
node.WorstState = activeCount > 0 ? AlarmState.Active : AlarmState.Normal;
}
}
private static bool IsActiveAlarm(DebugTreeNode node)
=> node.Alarm is { State: AlarmState.Active, IsConfiguredPlaceholder: false };
///
/// Recursively sort each node's children by
/// (ordinal) and roll up bad-quality: a node has bad quality when its own
/// attribute is off-Good or any descendant leaf is off-Good. Post-order so a
/// child's roll-up is settled before its parent reads it.
///
private static void SortAndRollUp(List nodes)
{
nodes.Sort(static (a, b) => string.CompareOrdinal(a.Segment, b.Segment));
foreach (var node in nodes)
{
// Leaf's own quality (branches normally have no attribute).
var bad = node.Attribute is not null && !IsGood(node.Attribute.Quality);
if (node.Children.Count > 0)
{
SortAndRollUp(node.Children);
bad = bad || node.Children.Any(static c => c.HasBadQuality);
}
node.HasBadQuality = bad;
}
}
private static bool IsGood(string quality)
=> string.Equals(quality, "Good", StringComparison.Ordinal);
}