📄 src/Payloads/DltPayload.cs
using System.Text;

namespace SharpDlt;

/// <summary>The payload of a DLT message.</summary>
public abstract record DltPayload
{
    /// <summary>Gets a human readable representation of the payload.</summary>
    public abstract override string ToString();
}

/// <summary>The payload of a DLT message in non-verbose mode.</summary>
/// <param name="MessageId">Unique ID of a specific DLT message.</param>
/// <param name="NonStaticData">All non-static information of the DLT message.</param>
public sealed record NonVerbosePayload(uint MessageId, ReadOnlyMemory<byte> NonStaticData) : DltPayload
{
    /// <summary>Minimum length of a non-verbose payload in bytes.</summary>
    public const int MessageIdLength = 4;

    /// <inheritdoc />
    public override string ToString() => $"[{MessageId}] {Convert.ToHexString(NonStaticData.Span).ToLowerInvariant()}";

    /// <summary>Determines whether the payload equals <paramref name="other"/> by value.</summary>
    public bool Equals(NonVerbosePayload? other) =>
        other is not null && MessageId == other.MessageId && NonStaticData.Span.SequenceEqual(other.NonStaticData.Span);

    /// <inheritdoc />
    public override int GetHashCode()
    {
        var hash = new HashCode();
        hash.Add(MessageId);
        hash.AddBytes(NonStaticData.Span);
        return hash.ToHashCode();
    }

    /// <summary>Parses a non-verbose payload from <paramref name="data"/>.</summary>
    /// <param name="data">The complete payload data.</param>
    /// <param name="msbFirst">If set, the payload data is big endian, otherwise little endian.</param>
    /// <exception cref="DltParseException">The data is shorter than the message ID.</exception>
    public static NonVerbosePayload Parse(ReadOnlySpan<byte> data, bool msbFirst)
    {
        if (data.Length < MessageIdLength)
        {
            throw new DltParseException(
                $"Unexpected length of the data: {data.Length} / "
                    + $"payload of non-verbose mode must not be < {MessageIdLength}."
            );
        }

        var messageId = Internal.SpanReader.ReadUInt32(data, msbFirst);
        return new NonVerbosePayload(messageId, data[MessageIdLength..].ToArray());
    }
}

/// <summary>The payload of a DLT message in verbose mode.</summary>
/// <param name="Arguments">The arguments of the payload.</param>
public sealed record VerbosePayload(IReadOnlyList<DltArgument> Arguments) : DltPayload
{
    /// <inheritdoc />
    public override string ToString() => string.Join(' ', Arguments);

    /// <summary>Determines whether the payload equals <paramref name="other"/> by value.</summary>
    public bool Equals(VerbosePayload? other) => other is not null && Arguments.SequenceEqual(other.Arguments);

    /// <inheritdoc />
    public override int GetHashCode()
    {
        var hash = new HashCode();
        foreach (var argument in Arguments)
        {
            hash.Add(argument);
        }

        return hash.ToHashCode();
    }

    /// <summary>Parses a verbose payload from <paramref name="data"/>.</summary>
    /// <param name="data">The complete payload data.</param>
    /// <param name="msbFirst">If set, the payload data is big endian, otherwise little endian.</param>
    /// <param name="numberOfArguments">Number of arguments within the payload.</param>
    /// <param name="encoding">Encoding used for 8 bit coded strings. Defaults to ASCII.</param>
    /// <exception cref="DltParseException">The data is malformed.</exception>
    public static VerbosePayload Parse(
        ReadOnlySpan<byte> data,
        bool msbFirst,
        int numberOfArguments,
        Encoding? encoding = null
    )
    {
        ArgumentOutOfRangeException.ThrowIfNegative(numberOfArguments);

        var arguments = new List<DltArgument>(numberOfArguments);
        var offset = 0;
        for (var i = 0; i < numberOfArguments; i++)
        {
            if (offset > data.Length)
            {
                throw new DltParseException(
                    $"Unexpected end of the payload while reading argument {i + 1} of {numberOfArguments}."
                );
            }

            arguments.Add(DltArgument.Parse(data[offset..], msbFirst, encoding, out var bytesRead));
            offset += bytesRead;
        }

        return new VerbosePayload(arguments);
    }
}