using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy; namespace ZB.MOM.WW.MxGateway.Server.Galaxy; public sealed class GalaxyHierarchyIndex { private GalaxyHierarchyIndex( IReadOnlyList objectViews, IReadOnlyDictionary objectViewsById, IReadOnlyDictionary tagsByAddress, IReadOnlyDictionary> childrenByParent, IReadOnlyDictionary objectViewsByTagName, IReadOnlyDictionary objectViewsByContainedPath) { ObjectViews = objectViews; ObjectViewsById = objectViewsById; TagsByAddress = tagsByAddress; ChildrenByParent = childrenByParent; ObjectViewsByTagName = objectViewsByTagName; ObjectViewsByContainedPath = objectViewsByContainedPath; } /// Gets an empty Galaxy hierarchy index. public static GalaxyHierarchyIndex Empty { get; } = new( Array.Empty(), new Dictionary(), new Dictionary(StringComparer.OrdinalIgnoreCase), new Dictionary>(), new Dictionary(StringComparer.OrdinalIgnoreCase), new Dictionary(StringComparer.OrdinalIgnoreCase)); /// Gets the object views. public IReadOnlyList ObjectViews { get; } /// Gets the object views indexed by GUID. public IReadOnlyDictionary ObjectViewsById { get; } /// Gets tags indexed by address. public IReadOnlyDictionary TagsByAddress { get; } /// Gets direct children grouped by parent gobject id. Root objects (no parent, or self-parented) live under key 0. Each list is sorted areas-first, then by display name (OrdinalIgnoreCase). public IReadOnlyDictionary> ChildrenByParent { get; } /// Gets object views indexed by (OrdinalIgnoreCase). Lets browse/discover handlers resolve parents/roots by tag name in O(1) instead of scanning . public IReadOnlyDictionary ObjectViewsByTagName { get; } /// Gets object views indexed by contained path (OrdinalIgnoreCase). Lets browse/discover handlers resolve parents/roots by path in O(1) instead of scanning . public IReadOnlyDictionary ObjectViewsByContainedPath { get; } /// Builds a Galaxy hierarchy index from the given objects. /// The Galaxy objects to index. /// A new Galaxy hierarchy index. public static GalaxyHierarchyIndex Build(IReadOnlyList objects) { if (objects.Count == 0) { return Empty; } Dictionary objectsById = new(); foreach (GalaxyObject obj in objects) { objectsById.TryAdd(obj.GobjectId, obj); } List views = new(objects.Count); Dictionary viewsById = new(); Dictionary tagsByAddress = new(StringComparer.OrdinalIgnoreCase); Dictionary viewsByTagName = new(StringComparer.OrdinalIgnoreCase); Dictionary viewsByContainedPath = new(StringComparer.OrdinalIgnoreCase); foreach (GalaxyObject obj in objects) { string path = BuildContainedPath(obj, objectsById); int depth = string.IsNullOrWhiteSpace(path) ? 0 : path.Count(character => character == '/'); GalaxyObjectView view = new(obj, path, depth); views.Add(view); viewsById.TryAdd(obj.GobjectId, view); if (!string.IsNullOrWhiteSpace(obj.TagName)) { tagsByAddress.TryAdd(obj.TagName, new GalaxyTagLookup(obj, Attribute: null, path)); viewsByTagName.TryAdd(obj.TagName, view); } if (!string.IsNullOrWhiteSpace(path)) { viewsByContainedPath.TryAdd(path, view); } foreach (GalaxyAttribute attribute in obj.Attributes) { if (!string.IsNullOrWhiteSpace(attribute.FullTagReference)) { tagsByAddress.TryAdd(attribute.FullTagReference, new GalaxyTagLookup(obj, attribute, path)); } } } Dictionary> childrenByParent = new(); foreach (GalaxyObjectView view in views) { int parentKey = view.Object.ParentGobjectId; // Treat self-parented (corrupt) rows as roots; matches DashboardBrowseTreeBuilder. if (parentKey == view.Object.GobjectId) { parentKey = 0; } if (!childrenByParent.TryGetValue(parentKey, out List? bucket)) { bucket = []; childrenByParent[parentKey] = bucket; } bucket.Add(view); } foreach (List bucket in childrenByParent.Values) { bucket.Sort(CompareByAreaThenDisplayName); } Dictionary> readOnlyChildren = new(childrenByParent.Count); foreach (KeyValuePair> kvp in childrenByParent) { readOnlyChildren[kvp.Key] = kvp.Value; } return new GalaxyHierarchyIndex( views, viewsById, tagsByAddress, readOnlyChildren, viewsByTagName, viewsByContainedPath); } private static string BuildContainedPath( GalaxyObject obj, IReadOnlyDictionary objectsById) { Stack names = new(); HashSet seen = []; GalaxyObject? current = obj; while (current is not null && seen.Add(current.GobjectId)) { names.Push(ResolvePathSegment(current)); current = current.ParentGobjectId != 0 && objectsById.TryGetValue(current.ParentGobjectId, out GalaxyObject? parent) ? parent : null; } return string.Join('/', names.Where(name => !string.IsNullOrWhiteSpace(name))); } private static string ResolvePathSegment(GalaxyObject obj) { if (!string.IsNullOrWhiteSpace(obj.ContainedName)) { return obj.ContainedName; } if (!string.IsNullOrWhiteSpace(obj.BrowseName)) { return obj.BrowseName; } return obj.TagName; } private static int CompareByAreaThenDisplayName(GalaxyObjectView left, GalaxyObjectView right) { if (left.Object.IsArea != right.Object.IsArea) { return left.Object.IsArea ? -1 : 1; } return string.Compare(DisplayNameOf(left), DisplayNameOf(right), StringComparison.OrdinalIgnoreCase); } private static string DisplayNameOf(GalaxyObjectView view) { GalaxyObject obj = view.Object; if (!string.IsNullOrWhiteSpace(obj.BrowseName)) { return obj.BrowseName; } if (!string.IsNullOrWhiteSpace(obj.ContainedName)) { return obj.ContainedName; } return obj.TagName; } }