using System.Buffers.Binary;
using System.Text;
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;
///
/// Decoder for the UDT template buffer libplctag returns for an @udt/<id> tag (CIP
/// Template Object, class 0x6C). Produces an describing the UDT's
/// name, total size, + ordered member list with per-member offset + type + array length.
///
///
/// The layout below is what libplctag hands back from Tag.GetBuffer() — its own
/// header followed by the controller's Read Template reply — matching libplctag's
/// list_tags_logix example and pinned by real captures in
/// CipTemplateObjectWireCaptureTests (CompactLogix 5069-L320ERS2 rev 34.11). An earlier
/// version assumed a 12-byte header led by the member count and [info][array][offset]
/// blocks; every real buffer decoded to null under that assumption.
///
/// Header (14 bytes, little-endian):
///
/// - u16Template instance id.
/// - u32Member-definition size in 32-bit words — not used here.
/// - u32Instance size — bytes per structure instance.
/// - u16Member count.
/// - u16Struct handle (opaque).
///
///
/// Then member_count member blocks (8 bytes each):
///
/// - u16Info — the array length for an array member, the bit
/// number for a BOOL packed into a host integer, otherwise 0.
/// - u16Type — bit 15 = struct, bit 13 = array, low 12 bits =
/// CIP atomic type code, or for a struct the nested template instance id.
/// - u32Byte offset from the struct start.
///
///
/// Then the strings: the UDT name terminated by ; (followed by an encoded tail that
/// runs to a NUL — discarded), then each member name terminated by a NUL. Anything after the
/// last member name is ignored.
///
public static class CipTemplateObjectDecoder
{
private const int HeaderSize = 14; // u16 id + u32 def size + u32 instance size + u16 count + u16 handle
private const int MemberBlockSize = 8; // u16 info + u16 type + u32 offset
private const ushort TypeStructFlag = 0x8000;
private const ushort TypeArrayFlag = 0x2000;
private const ushort TypeCodeMask = 0x0FFF;
///
/// Decode the raw template buffer. Returns null when the header indicates zero
/// members or the buffer is too short to hold the header + member blocks + type name.
///
/// The raw template buffer to decode.
/// An AbCipUdtShape describing the structure, or null if decoding fails.
public static AbCipUdtShape? Decode(byte[] buffer)
{
ArgumentNullException.ThrowIfNull(buffer);
if (buffer.Length < HeaderSize) return null;
// bytes 0-1: template instance id; bytes 2-5: definition size in words — neither needed.
var instanceSize = BinaryPrimitives.ReadUInt32LittleEndian(buffer.AsSpan(6));
var memberCount = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(10));
// bytes 12-13: struct handle — opaque.
if (memberCount == 0) return null;
var namesOffset = HeaderSize + (MemberBlockSize * memberCount);
if (namesOffset > buffer.Length) return null;
var strings = ParseTemplateStrings(buffer.AsSpan(namesOffset), memberCount);
if (strings.Count == 0) return null;
var udtName = strings[0];
var members = new List(memberCount);
for (var i = 0; i < memberCount; i++)
{
var blockOffset = HeaderSize + (i * MemberBlockSize);
var info = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(blockOffset));
var type = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(blockOffset + 2));
var offset = (int)BinaryPrimitives.ReadUInt32LittleEndian(buffer.AsSpan(blockOffset + 4));
var isStruct = (type & TypeStructFlag) != 0;
var isArray = (type & TypeArrayFlag) != 0;
// Atomic CIP type codes (0xC1-0xD0) fit a byte; for a struct the low 12 bits are the nested
// template id, which can exceed a byte, so it is taken from the full 12-bit mask below.
var dataType = isStruct
? AbCipDataType.Structure
: (CipSymbolObjectDecoder.MapTypeCode((byte)(type & TypeCodeMask)) ?? AbCipDataType.Structure);
var nestedTemplateId = isStruct ? (uint?)(type & TypeCodeMask) : null;
// info is the element count only for an array member; for a BOOL it is the bit number,
// which must never be read as a length.
var arrayLength = isArray && info > 0 ? info : 1;
var memberName = i + 1 < strings.Count ? strings[i + 1] : $"";
members.Add(new AbCipUdtMember(
Name: memberName,
Offset: offset,
DataType: dataType,
ArrayLength: arrayLength,
NestedTemplateId: nestedTemplateId));
}
return new AbCipUdtShape(
TypeName: udtName,
TotalSize: (int)instanceSize,
Members: members);
}
///
/// Split the template string section into the UDT name (cut at its first ;) followed
/// by up to NUL-terminated member names. Stops at the end of
/// the buffer; trailing bytes past the last member name are ignored.
///
/// The string section, starting right after the last member block.
/// Member names to read after the UDT name.
/// The UDT name followed by the member names found.
internal static List ParseTemplateStrings(ReadOnlySpan span, int memberCount)
{
var result = new List(memberCount + 1);
var pos = 0;
while (pos < span.Length && result.Count < memberCount + 1)
{
var nul = span[pos..].IndexOf((byte)0);
var end = nul < 0 ? span.Length : pos + nul;
var raw = span[pos..end];
if (result.Count == 0)
{
// The type name carries an encoded tail after ';' ("STRING;nEAIA").
var semi = raw.IndexOf((byte)';');
if (semi >= 0) raw = raw[..semi];
}
result.Add(Encoding.ASCII.GetString(raw));
pos = end + 1;
}
return result;
}
}