using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browse;
///
/// Translates a Galaxy object hierarchy (from ) into
/// calls — folders for each gobject, variables for
/// each dynamic attribute. Alarm-bearing attributes get all five sub-attribute refs
/// populated via so the server-level alarm subsystem
/// (PR 2.2) can subscribe + ack without help from the driver.
///
///
/// Hierarchy materialisation rules (mirror legacy MxAccessGalaxyBackend.DiscoverAsync):
///
/// - Browse name = contained_name when present; falls back to tag_name.
/// - Folder per gobject; variables placed inside their owner folder.
/// - Variable's full reference = tag_name.attribute_name — the format MXAccess
/// expects for read/write addressing (translated from the contained-name browse path).
/// - Hierarchy is nested by parent_gobject_id: each gobject's folder is created
/// under the folder of the gobject named by its parent_gobject_id (resolved
/// order-independently and memoised, so the parent can appear before or after the child).
/// A gobject degrades to the driver root when its parent_gobject_id is 0,
/// self-referential, or names a gobject not present in the returned set — so a model
/// that carries no parentage still renders flat.
///
///
public sealed class GalaxyDiscoverer
{
private readonly IGalaxyHierarchySource _source;
/// Initializes a new GalaxyDiscoverer with the specified hierarchy source.
/// The Galaxy hierarchy source to use for discovery.
public GalaxyDiscoverer(IGalaxyHierarchySource source)
{
_source = source ?? throw new ArgumentNullException(nameof(source));
}
///
/// Drive the supplied builder with one folder + N variables per Galaxy object the
/// gateway returns. Idempotent — caller can re-invoke after a redeploy event.
///
/// The address space builder to populate with discovery results.
/// The cancellation token for the operation.
public async Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(builder);
var objects = await _source.GetHierarchyAsync(cancellationToken).ConfigureAwait(false);
// Pass 1 — collect gobjects with a usable browse identity (order preserved), and index
// them by gobject_id so pass 2 can resolve a parent the source returned AFTER the child
// (order-independent). Only the FIRST gobject per non-zero id is indexed as a parent
// target; the index excludes id 0 so identity-less / un-keyed objects can't collide.
var usable = new List();
var byId = new Dictionary();
foreach (var obj in objects)
{
var browseName = string.IsNullOrEmpty(obj.ContainedName) ? obj.TagName : obj.ContainedName;
if (string.IsNullOrEmpty(browseName)) continue; // skip objects with no usable identity
usable.Add(obj);
if (obj.GobjectId != 0)
{
byId.TryAdd(obj.GobjectId, obj);
}
}
// Pass 2 — create each gobject's folder under its parent (memoised), then its variables.
// Iterating the ordered list (not the index) keeps the flat case stable and emits a
// folder even for objects whose gobject_id is 0 or duplicated.
var folders = new Dictionary();
foreach (var obj in usable)
{
var folder = EnsureFolder(obj, builder, byId, folders);
foreach (var attr in obj.Attributes)
{
if (string.IsNullOrEmpty(attr.AttributeName)) continue;
var fullReference = !string.IsNullOrEmpty(attr.FullTagReference)
? StripArraySuffix(attr.FullTagReference)
: obj.TagName + "." + attr.AttributeName;
var info = new DriverAttributeInfo(
FullName: fullReference,
DriverDataType: DataTypeMap.Map(attr.MxDataType),
IsArray: attr.IsArray,
ArrayDim: attr.IsArray && attr.ArrayDimensionPresent && attr.ArrayDimension > 0
? (uint)attr.ArrayDimension
: null,
SecurityClass: SecurityMap.Map(attr.SecurityClassification),
IsHistorized: attr.IsHistorized,
IsAlarm: attr.IsAlarm);
var handle = folder.Variable(attr.AttributeName, attr.AttributeName, info);
// Alarm-bearing attributes ship the full sub-attribute ref set so the server's
// AlarmConditionService can subscribe + ack-write without re-deriving the names.
if (attr.IsAlarm)
{
handle.MarkAsAlarmCondition(AlarmRefBuilder.Build(fullReference));
}
}
}
}
// Resolve (and memoise) the folder for a gobject, recursively creating its parent's folder
// first so the result nests under it. Falls back to the driver root when the parent_gobject_id
// is 0, self-referential, absent from the returned set, or already mid-resolution (cycle guard).
// Memoisation is keyed by gobject_id and only applies to non-zero ids — an id-0 object always
// gets its own freshly-created folder (it can be neither a parent target nor shared).
private static IAddressSpaceBuilder EnsureFolder(
GalaxyObject obj,
IAddressSpaceBuilder root,
IReadOnlyDictionary byId,
Dictionary folders,
HashSet? building = null)
{
if (obj.GobjectId != 0 && folders.TryGetValue(obj.GobjectId, out var existing)) return existing;
var browseName = string.IsNullOrEmpty(obj.ContainedName) ? obj.TagName : obj.ContainedName;
building ??= [];
IAddressSpaceBuilder parentBuilder = root;
if (obj.ParentGobjectId != 0
&& obj.ParentGobjectId != obj.GobjectId
&& !building.Contains(obj.ParentGobjectId)
&& byId.TryGetValue(obj.ParentGobjectId, out var parentObj))
{
building.Add(obj.GobjectId);
parentBuilder = EnsureFolder(parentObj, root, byId, folders, building);
building.Remove(obj.GobjectId);
}
var folder = parentBuilder.Folder(browseName, browseName);
if (obj.GobjectId != 0)
{
folders[obj.GobjectId] = folder;
}
return folder;
}
// PR 5.W workaround for mxaccessgw GalaxyRepository.cs:173-175 — the gateway's
// SQL appends `[]` to array-typed `full_tag_reference` values, but MxAccess COM
// `IInstance.AddItem` doesn't accept `[]`-suffixed addresses (so any downstream
// Subscribe/Read/Write through the worker would fail with the suffixed form).
// Strip defensively here so the parity matrix can run today; remove once the
// gw fix (mxaccessgw/requirements-array-suffix-fix.md) lands.
private static string StripArraySuffix(string fullReference) =>
fullReference.EndsWith("[]", StringComparison.Ordinal)
? fullReference[..^2]
: fullReference;
}