using System;
using System.Collections.Generic;
using System.Linq;
using Serilog;
using ZB.MOM.WW.OtOpcUa.Host.Domain;
namespace ZB.MOM.WW.OtOpcUa.Host.OpcUa
{
///
/// Builds the tag reference mappings from Galaxy hierarchy and attributes.
/// Testable without an OPC UA server. (OPC-002, OPC-003, OPC-004)
///
public class AddressSpaceBuilder
{
private static readonly ILogger Log = Serilog.Log.ForContext();
///
/// Builds an in-memory model of the Galaxy hierarchy and attribute mappings before the OPC UA server materializes
/// nodes.
///
/// The Galaxy object hierarchy returned by the repository.
/// The Galaxy attribute rows associated with the hierarchy.
/// An address-space model containing roots, variables, and tag-reference mappings.
public static AddressSpaceModel Build(List hierarchy, List attributes)
{
var model = new AddressSpaceModel();
var objectMap = hierarchy.ToDictionary(h => h.GobjectId);
var attrsByObject = attributes
.GroupBy(a => a.GobjectId)
.ToDictionary(g => g.Key, g => g.ToList());
// Build parent→children map
var childrenByParent = hierarchy.GroupBy(h => h.ParentGobjectId)
.ToDictionary(g => g.Key, g => g.ToList());
// Find root objects (parent not in hierarchy)
var knownIds = new HashSet(hierarchy.Select(h => h.GobjectId));
foreach (var obj in hierarchy)
{
var nodeInfo = BuildNodeInfo(obj, attrsByObject, childrenByParent, model);
if (!knownIds.Contains(obj.ParentGobjectId))
model.RootNodes.Add(nodeInfo);
}
Log.Information("Address space model: {Objects} objects, {Variables} variables, {Mappings} tag refs",
model.ObjectCount, model.VariableCount, model.NodeIdToTagReference.Count);
return model;
}
private static NodeInfo BuildNodeInfo(GalaxyObjectInfo obj,
Dictionary> attrsByObject,
Dictionary> childrenByParent,
AddressSpaceModel model)
{
var node = new NodeInfo
{
GobjectId = obj.GobjectId,
TagName = obj.TagName,
BrowseName = obj.BrowseName,
ParentGobjectId = obj.ParentGobjectId,
IsArea = obj.IsArea
};
if (!obj.IsArea)
model.ObjectCount++;
if (attrsByObject.TryGetValue(obj.GobjectId, out var attrs))
foreach (var attr in attrs)
{
node.Attributes.Add(new AttributeNodeInfo
{
AttributeName = attr.AttributeName,
FullTagReference = attr.FullTagReference,
MxDataType = attr.MxDataType,
IsArray = attr.IsArray,
ArrayDimension = attr.ArrayDimension,
PrimitiveName = attr.PrimitiveName ?? "",
SecurityClassification = attr.SecurityClassification,
IsHistorized = attr.IsHistorized,
IsAlarm = attr.IsAlarm
});
model.NodeIdToTagReference[GetNodeIdentifier(attr)] = attr.FullTagReference;
model.VariableCount++;
}
return node;
}
private static string GetNodeIdentifier(GalaxyAttributeInfo attr)
{
if (!attr.IsArray)
return attr.FullTagReference;
return attr.FullTagReference.EndsWith("[]", StringComparison.Ordinal)
? attr.FullTagReference.Substring(0, attr.FullTagReference.Length - 2)
: attr.FullTagReference;
}
///
/// Node info for the address space tree.
///
public class NodeInfo
{
///
/// Gets or sets the Galaxy object identifier represented by this address-space node.
///
public int GobjectId { get; set; }
///
/// Gets or sets the runtime tag name used to tie the node back to Galaxy metadata.
///
public string TagName { get; set; } = "";
///
/// Gets or sets the browse name exposed to OPC UA clients for this hierarchy node.
///
public string BrowseName { get; set; } = "";
///
/// Gets or sets the parent Galaxy object identifier used to assemble the tree.
///
public int ParentGobjectId { get; set; }
///
/// Gets or sets a value indicating whether the node represents a Galaxy area folder.
///
public bool IsArea { get; set; }
///
/// Gets or sets the attribute nodes published beneath this object.
///
public List Attributes { get; set; } = new();
///
/// Gets or sets the child nodes that appear under this branch of the Galaxy hierarchy.
///
public List Children { get; set; } = new();
}
///
/// Lightweight description of an attribute node that will become an OPC UA variable.
///
public class AttributeNodeInfo
{
///
/// Gets or sets the Galaxy attribute name published under the object.
///
public string AttributeName { get; set; } = "";
///
/// Gets or sets the fully qualified runtime reference used for reads, writes, and subscriptions.
///
public string FullTagReference { get; set; } = "";
///
/// Gets or sets the Galaxy data type code used to pick the OPC UA variable type.
///
public int MxDataType { get; set; }
///
/// Gets or sets a value indicating whether the attribute is modeled as an array.
///
public bool IsArray { get; set; }
///
/// Gets or sets the declared array length when the attribute is a fixed-size array.
///
public int? ArrayDimension { get; set; }
///
/// Gets or sets the primitive name that groups the attribute under a sub-object node.
/// Empty for root-level attributes.
///
public string PrimitiveName { get; set; } = "";
///
/// Gets or sets the Galaxy security classification that determines OPC UA write access.
///
public int SecurityClassification { get; set; } = 1;
///
/// Gets or sets a value indicating whether the attribute is historized.
///
public bool IsHistorized { get; set; }
///
/// Gets or sets a value indicating whether the attribute is an alarm.
///
public bool IsAlarm { get; set; }
}
///
/// Result of building the address space model.
///
public class AddressSpaceModel
{
///
/// Gets or sets the root nodes that become the top-level browse entries in the Galaxy namespace.
///
public List RootNodes { get; set; } = new();
///
/// Gets or sets the mapping from OPC UA node identifiers to runtime tag references.
///
public Dictionary NodeIdToTagReference { get; set; } =
new(StringComparer.OrdinalIgnoreCase);
///
/// Gets or sets the number of non-area Galaxy objects included in the model.
///
public int ObjectCount { get; set; }
///
/// Gets or sets the number of variable nodes created from Galaxy attributes.
///
public int VariableCount { get; set; }
}
}
}