📄 src/Headers/StandardHeader.cs
using System.Buffers.Binary;
using SharpDlt.Internal;

namespace SharpDlt;

/// <summary>
/// The Standard Header of a DLT message.
/// </summary>
/// <param name="UseExtendedHeader">If set, the Extended Header is transmitted.</param>
/// <param name="MsbFirst">If set, the payload data is big endian, otherwise little endian.</param>
/// <param name="VersionNumber">Version number of the DLT data protocol.</param>
/// <param name="MessageCounter">Continuous number of the message.</param>
/// <param name="Length">Length of the complete message (without Storage Header) in bytes.</param>
/// <param name="EcuId">Unique address of the sender, if transmitted.</param>
/// <param name="SessionId">Session number, if transmitted.</param>
/// <param name="Timestamp">Ticks of the ECU at the moment the message was sent, if transmitted.</param>
public sealed record StandardHeader(
    bool UseExtendedHeader,
    bool MsbFirst,
    int VersionNumber,
    byte MessageCounter,
    ushort Length,
    string? EcuId = null,
    uint? SessionId = null,
    uint? Timestamp = null
)
{
    /// <summary>Minimum length of the Standard Header in bytes.</summary>
    public const int MinimumLength = 4;

    private const byte UseExtendedHeaderMask = 0b0000_0001;
    private const byte MsbFirstMask = 0b0000_0010;
    private const byte WithEcuIdMask = 0b0000_0100;
    private const byte WithSessionIdMask = 0b0000_1000;
    private const byte WithTimestampMask = 0b0001_0000;
    private const byte VersionNumberMask = 0b1110_0000;
    private const int VersionNumberShift = 5;

    /// <summary>Gets a value indicating whether the header carries an ECU ID.</summary>
    public bool WithEcuId => EcuId is not null;

    /// <summary>Gets a value indicating whether the header carries a session ID.</summary>
    public bool WithSessionId => SessionId is not null;

    /// <summary>Gets a value indicating whether the header carries a timestamp.</summary>
    public bool WithTimestamp => Timestamp is not null;

    /// <summary>Gets the length of this header in bytes.</summary>
    public int ByteLength => MinimumLength + (WithEcuId ? 4 : 0) + (WithSessionId ? 4 : 0) + (WithTimestamp ? 4 : 0);

    /// <summary>Gets the Header Type (HTYP) byte.</summary>
    public byte HeaderType
    {
        get
        {
            var htyp = (VersionNumber << VersionNumberShift) & VersionNumberMask;
            if (UseExtendedHeader)
            {
                htyp |= UseExtendedHeaderMask;
            }

            if (MsbFirst)
            {
                htyp |= MsbFirstMask;
            }

            if (WithEcuId)
            {
                htyp |= WithEcuIdMask;
            }

            if (WithSessionId)
            {
                htyp |= WithSessionIdMask;
            }

            if (WithTimestamp)
            {
                htyp |= WithTimestampMask;
            }

            return (byte)htyp;
        }
    }

    /// <summary>
    /// Reads the message length field of a Standard Header which starts at the beginning of
    /// <paramref name="data"/>.
    /// </summary>
    /// <exception cref="DltParseException">The data is shorter than the minimum header length.</exception>
    public static ushort ReadMessageLength(ReadOnlySpan<byte> data)
    {
        if (data.Length < MinimumLength)
        {
            throw new DltParseException(
                $"Unexpected length of the data: {data.Length} / Standard Header must be {MinimumLength} or more."
            );
        }

        return BinaryPrimitives.ReadUInt16BigEndian(data[2..]);
    }

    /// <summary>Parses a Standard Header from the beginning of <paramref name="data"/>.</summary>
    /// <exception cref="DltParseException">The data is too short for the encoded header type.</exception>
    public static StandardHeader Parse(ReadOnlySpan<byte> data)
    {
        if (data.Length < MinimumLength)
        {
            throw new DltParseException(
                $"Unexpected length of the data: {data.Length} / Standard Header must be {MinimumLength} or more."
            );
        }

        var headerType = data[0];
        var useExtendedHeader = (headerType & UseExtendedHeaderMask) != 0;
        var msbFirst = (headerType & MsbFirstMask) != 0;
        var withEcuId = (headerType & WithEcuIdMask) != 0;
        var withSessionId = (headerType & WithSessionIdMask) != 0;
        var withTimestamp = (headerType & WithTimestampMask) != 0;
        var versionNumber = (headerType & VersionNumberMask) >> VersionNumberShift;

        var expectedLength = MinimumLength + (withEcuId ? 4 : 0) + (withSessionId ? 4 : 0) + (withTimestamp ? 4 : 0);
        if (data.Length < expectedLength)
        {
            throw new DltParseException(
                $"Unexpected length of the data: {data.Length} / Standard Header with Header Type: "
                    + $"WEID={withEcuId} WSID={withSessionId} WTMS={withTimestamp} "
                    + $"must be {expectedLength} or more."
            );
        }

        var messageCounter = data[1];
        var length = BinaryPrimitives.ReadUInt16BigEndian(data[2..]);

        var offset = MinimumLength;
        string? ecuId = null;
        if (withEcuId)
        {
            ecuId = AsciiText.Decode(data.Slice(offset, 4));
            offset += 4;
        }

        uint? sessionId = null;
        if (withSessionId)
        {
            sessionId = BinaryPrimitives.ReadUInt32BigEndian(data[offset..]);
            offset += 4;
        }

        uint? timestamp = null;
        if (withTimestamp)
        {
            timestamp = BinaryPrimitives.ReadUInt32BigEndian(data[offset..]);
        }

        return new StandardHeader(
            useExtendedHeader,
            msbFirst,
            versionNumber,
            messageCounter,
            length,
            ecuId,
            sessionId,
            timestamp
        );
    }
}