namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip;

/// <summary>
///     Computes byte offsets for declared UDT members under Logix natural-alignment rules so
///     a single whole-UDT read can decode each member from one buffer without
///     re-reading per member. Declaration-driven — the caller supplies
///     <see cref="AbCipStructureMember"/> rows; this helper produces the offset each member
///     sits at in the parent tag's read buffer.
/// </summary>
/// <remarks>
///     <para>Alignment rules applied per Rockwell "Logix 5000 Data Access" manual + the
///     libplctag test fixtures: each member aligns to its natural boundary (SInt 1, Int 2,
///     DInt/Real/Dt 4, LInt/ULInt/LReal 8), padding inserted before the member as needed.
///     The total size is padded to the alignment of the largest member so arrays-of-UDT also
///     work at element stride — though this helper is used only on single instances today.</para>
///
///     <para><see cref="TryBuild"/> returns <c>null</c> on unsupported member types
///     (<see cref="AbCipDataType.Bool"/>, <see cref="AbCipDataType.String"/>,
///     <see cref="AbCipDataType.Structure"/>). Whole-UDT grouping opts out of those groups
///     and falls back to the per-tag read path — BOOL members are packed into a hidden host
///     byte at the top of the UDT under Logix, so their offset can't be computed from
///     declared-member order alone. The CIP Template Object reader produces a
///     <see cref="AbCipUdtShape"/> that carries real offsets for BOOL + nested structs; when
///     that shape is cached the driver can take the richer path instead.</para>
/// </remarks>
public static class AbCipUdtMemberLayout
{
    /// <summary>
    ///     Try to compute member offsets for the supplied declared members. Returns <c>null</c>
    ///     if any member type is unsupported for declaration-only layout.
    /// </summary>
    /// <param name="members">The list of UDT member declarations.</param>
    /// <returns>A dictionary of member offsets keyed by name, or <c>null</c> if any member type is unsupported.</returns>
    public static IReadOnlyDictionary<string, int>? TryBuild(
        IReadOnlyList<AbCipStructureMember> members)
    {
        ArgumentNullException.ThrowIfNull(members);
        if (members.Count == 0) return null;

        var offsets = new Dictionary<string, int>(members.Count, StringComparer.OrdinalIgnoreCase);
        var cursor = 0;

        foreach (var member in members)
        {
            if (!TryGetSizeAlign(member.DataType, out var size, out var align))
                return null;

            // An array member can't be placed by declaration-only layout: the
            // whole-UDT grouped read decodes one scalar per member at its offset and can't return
            // an array, and advancing the cursor by the scalar size (not size * count) would
            // mis-place every member after it. Opt the whole group out so array members fall back
            // to the per-tag read path, which reads them as typed arrays.
            if (member.IsArray || member.ElementCount > 1)
                return null;

            if (cursor % align != 0)
                cursor += align - (cursor % align);

            offsets[member.Name] = cursor;
            cursor += size;
        }

        return offsets;
    }

    /// <summary>
    ///     Natural size + alignment for a Logix atomic type. <c>false</c> for types excluded
    ///     from declaration-only grouping (Bool / String / Structure).
    /// </summary>
    private static bool TryGetSizeAlign(AbCipDataType type, out int size, out int align)
    {
        switch (type)
        {
            case AbCipDataType.SInt: case AbCipDataType.USInt:
                size = 1; align = 1; return true;
            case AbCipDataType.Int: case AbCipDataType.UInt:
                size = 2; align = 2; return true;
            case AbCipDataType.DInt: case AbCipDataType.UDInt:
            case AbCipDataType.Real: case AbCipDataType.Dt:
                size = 4; align = 4; return true;
            case AbCipDataType.LInt: case AbCipDataType.ULInt:
            case AbCipDataType.LReal:
                size = 8; align = 8; return true;
            default:
                size = 0; align = 0; return false;
        }
    }
}
