using System.Buffers.Binary;
using System.Text;

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

/// <summary>
///     Decoder for the CIP Symbol Object (class 0x6B) response returned by Logix controllers
///     when a client reads the <c>@tags</c> pseudo-tag. Parses the concatenated tag-info
///     entries into a sequence of <see cref="AbCipDiscoveredTag"/>s that the driver can stream
///     into the address-space builder.
/// </summary>
/// <remarks>
///     <para>Entry layout (little-endian) per Rockwell CIP Vol 1 + Logix 5000 CIP Programming
///     Manual (1756-PM019 chapter "Symbol Object"), cross-checked against libplctag's
///     <c>ab/cip.c</c> <c>handle_listed_tags_reply</c>:</para>
///     <list type="table">
///         <item><term>u32</term><description>Symbol Instance ID — opaque identifier for the tag.</description></item>
///         <item><term>u16</term><description>Symbol Type — lower 12 bits = CIP type code (0xC1 BOOL,
///             0xC2 SINT, …, 0xD0 STRING). Bit 12 = system-tag flag. Bit 13 = reserved.
///             Bit 15 = struct flag; when set, the lower 12 bits are the template instance id
///             (not a primitive type code).</description></item>
///         <item><term>u16</term><description>Element length — bytes per element (e.g. 4 for DINT).</description></item>
///         <item><term>u32 × 3</term><description>Array dimensions — zero for scalar tags.</description></item>
///         <item><term>u16</term><description>Symbol name length in bytes.</description></item>
///         <item><term>u8 × N</term><description>ASCII symbol name, padded to an even byte boundary.</description></item>
///     </list>
///
///     <para><c>Program:</c>-scope tags arrive with their scope prefix baked into the name
///     (<c>Program:MainProgram.StepIndex</c>); decoder strips the prefix + emits the scope
///     separately so the driver's IAddressSpaceBuilder can organise them.</para>
/// </remarks>
public static class CipSymbolObjectDecoder
{
    // Fixed header size in bytes — instance-id(4) + symbol-type(2) + element-length(2)
    // + array-dims(4×3) + name-length(2) = 22.
    private const int FixedHeaderSize = 22;

    private const ushort SymbolTypeSystemFlag = 0x1000;
    private const ushort SymbolTypeStructFlag = 0x8000;
    private const ushort SymbolTypeTypeCodeMask = 0x0FFF;

    /// <summary>
    ///     Decode the raw <c>@tags</c> blob into an enumerable sequence. Malformed entries at
    ///     the tail cause decoding to stop gracefully — the caller gets whatever it could parse
    ///     cleanly before the corruption.
    /// </summary>
    /// <param name="buffer">The raw byte buffer from the Symbol Object response.</param>
    /// <returns>An enumerable sequence of discovered CIP tags.</returns>
    public static IEnumerable<AbCipDiscoveredTag> Decode(byte[] buffer)
    {
        ArgumentNullException.ThrowIfNull(buffer);
        return DecodeImpl(buffer);
    }

    private static IEnumerable<AbCipDiscoveredTag> DecodeImpl(byte[] buffer)
    {
        var pos = 0;
        while (pos + FixedHeaderSize <= buffer.Length)
        {
            var instanceId = BinaryPrimitives.ReadUInt32LittleEndian(buffer.AsSpan(pos));
            var symbolType = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(pos + 4));
            // element_length at pos+6 (u16) — useful for array sizing but not surfaced here
            // array_dims at pos+8, pos+12, pos+16 — same (scalar-tag case has all zeros)
            var nameLength = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(pos + 20));
            pos += FixedHeaderSize;

            if (pos + nameLength > buffer.Length) break;
            var name = Encoding.ASCII.GetString(buffer, pos, nameLength);
            pos += nameLength;
            if ((pos & 1) != 0) pos++; // even-align for the next entry

            if (string.IsNullOrWhiteSpace(name)) continue;

            var isSystem = (symbolType & SymbolTypeSystemFlag) != 0;
            var isStruct = (symbolType & SymbolTypeStructFlag) != 0;
            var typeCode = symbolType & SymbolTypeTypeCodeMask;

            var (programScope, simpleName) = SplitProgramScope(name);
            var dataType = isStruct ? AbCipDataType.Structure : MapTypeCode((byte)typeCode);

            // For a struct symbol the lower 12 bits (typeCode) are NOT a primitive CIP type code —
            // they are the CIP template instance id (per the bit-15 struct-flag remarks above). Surface
            // it so the driver can fetch the UDT's Template Object during discovery. Atomic symbols
            // carry no template id.
            var templateInstanceId = isStruct ? (uint?)typeCode : null;

            yield return new AbCipDiscoveredTag(
                Name: simpleName,
                ProgramScope: programScope,
                DataType: dataType ?? AbCipDataType.Structure, // unknown type code → treat as opaque
                ReadOnly: false, // Symbol Object doesn't carry write-protection bits; lift via AccessControl Object later
                IsSystemTag: isSystem,
                TemplateInstanceId: templateInstanceId);

            _ = instanceId; // retained in the wire format for diagnostics; not surfaced to the driver today
        }
    }

    /// <summary>
    ///     Split a <c>Program:MainProgram.StepIndex</c>-style name into its scope + local
    ///     parts. Names without the <c>Program:</c> prefix pass through unchanged.
    /// </summary>
    /// <param name="fullName">The full tag name possibly prefixed with a program scope.</param>
    /// <returns>A tuple containing the program scope (or null) and the simple name.</returns>
    internal static (string? programScope, string simpleName) SplitProgramScope(string fullName)
    {
        const string prefix = "Program:";
        if (!fullName.StartsWith(prefix, StringComparison.Ordinal)) return (null, fullName);
        var afterPrefix = fullName[prefix.Length..];
        var dot = afterPrefix.IndexOf('.');
        if (dot <= 0) return (null, fullName); // malformed scope — surface the raw name
        return (afterPrefix[..dot], afterPrefix[(dot + 1)..]);
    }

    /// <summary>
    ///     Map a CIP atomic type code (lower 12 bits of the symbol-type field) to our
    ///     <see cref="AbCipDataType"/> surface. Returns <c>null</c> for unrecognised codes —
    ///     caller treats those as <see cref="AbCipDataType.Structure"/> so the symbol is still
    ///     surfaced + downstream config can add a concrete type override.
    /// </summary>
    /// <param name="typeCode">The CIP type code to map.</param>
    /// <returns>The corresponding AbCipDataType, or null if unrecognized.</returns>
    internal static AbCipDataType? MapTypeCode(byte typeCode) => typeCode switch
    {
        0xC1 => AbCipDataType.Bool,
        0xC2 => AbCipDataType.SInt,
        0xC3 => AbCipDataType.Int,
        0xC4 => AbCipDataType.DInt,
        0xC5 => AbCipDataType.LInt,
        0xC6 => AbCipDataType.USInt,
        0xC7 => AbCipDataType.UInt,
        0xC8 => AbCipDataType.UDInt,
        0xC9 => AbCipDataType.ULInt,
        0xCA => AbCipDataType.Real,
        0xCB => AbCipDataType.LReal,
        0xCD => AbCipDataType.Dt,     // DATE
        0xCF => AbCipDataType.Dt,     // DATE_AND_TIME
        0xD0 => AbCipDataType.String,
        _ => null,
    };
}
