📄 src/Payloads/DltArgument.Parsing.cs
using System.Text;
using SharpDlt.Internal;

namespace SharpDlt;

public abstract partial record DltArgument
{
    /// <summary>Length of the Type Info field in bytes.</summary>
    internal const int TypeInfoLength = 4;

    private const uint MaskBaseType = 0b0000_0000_0110_0111_1111_0000;
    private const uint MaskTypeLength = 0b1111;
    private const uint MaskStringCoding = 0b0011_1000_0000_0000_0000;

    private const uint TypeBool = 0x0000_0010;
    private const uint TypeSigned = 0x0000_0020;
    private const uint TypeUnsigned = 0x0000_0040;
    private const uint TypeFloat = 0x0000_0080;
    private const uint TypeString = 0x0000_0200;
    private const uint TypeRaw = 0x0000_0400;

    private const uint StringCodingAscii = 0x0000_0000;
    private const uint StringCodingUtf8 = 0x0000_8000;

    private const int LengthFieldSize = 2;

    /// <summary>Parses a single argument from the beginning of <paramref name="data"/>.</summary>
    /// <param name="data">Data starting with the Type Info of the argument.</param>
    /// <param name="msbFirst">If set, the payload data is big endian, otherwise little endian.</param>
    /// <param name="encoding">Encoding used for 8 bit coded strings. Defaults to ASCII.</param>
    /// <param name="bytesRead">Number of bytes consumed by the argument.</param>
    /// <exception cref="DltParseException">The data is malformed or uses an unsupported type.</exception>
    public static DltArgument Parse(ReadOnlySpan<byte> data, bool msbFirst, Encoding? encoding, out int bytesRead)
    {
        if (data.Length < TypeInfoLength)
        {
            throw new DltParseException(
                $"Unexpected length of the data: {data.Length} / an argument must be {TypeInfoLength} or more."
            );
        }

        var typeInfo = SpanReader.ReadUInt32(data, msbFirst);
        var payload = data[TypeInfoLength..];

        return (typeInfo & MaskBaseType) switch
        {
            TypeBool => ParseBool(payload, typeInfo, out bytesRead),
            TypeSigned => ParseSigned(payload, msbFirst, typeInfo, out bytesRead),
            TypeUnsigned => ParseUnsigned(payload, msbFirst, typeInfo, out bytesRead),
            TypeFloat => ParseFloat(payload, msbFirst, typeInfo, out bytesRead),
            TypeString => ParseString(payload, msbFirst, typeInfo, encoding, out bytesRead),
            TypeRaw => ParseRaw(payload, msbFirst, typeInfo, out bytesRead),
            _ => throw UnsupportedTypeInfo(typeInfo),
        };
    }

    private static DltArgument ParseBool(ReadOnlySpan<byte> payload, uint typeInfo, out int bytesRead)
    {
        if (payload.IsEmpty)
        {
            throw new DltParseException(
                $"There is no data for a bool argument. Type Info: {FormatTypeInfo(typeInfo)}."
            );
        }

        bytesRead = TypeInfoLength + 1;
        return new DltBoolArgument(payload[0] != 0);
    }

    private static DltArgument ParseSigned(ReadOnlySpan<byte> payload, bool msbFirst, uint typeInfo, out int bytesRead)
    {
        var width = ResolveIntegerWidth(payload.Length, typeInfo, "signed integer");
        bytesRead = TypeInfoLength + (int)width;
        long value = width switch
        {
            DltIntegerWidth.Bits8 => (sbyte)payload[0],
            DltIntegerWidth.Bits16 => SpanReader.ReadInt16(payload, msbFirst),
            DltIntegerWidth.Bits32 => SpanReader.ReadInt32(payload, msbFirst),
            _ => SpanReader.ReadInt64(payload, msbFirst),
        };
        return new DltSignedArgument(value, width);
    }

    private static DltArgument ParseUnsigned(
        ReadOnlySpan<byte> payload,
        bool msbFirst,
        uint typeInfo,
        out int bytesRead
    )
    {
        var width = ResolveIntegerWidth(payload.Length, typeInfo, "unsigned integer");
        bytesRead = TypeInfoLength + (int)width;
        ulong value = width switch
        {
            DltIntegerWidth.Bits8 => payload[0],
            DltIntegerWidth.Bits16 => SpanReader.ReadUInt16(payload, msbFirst),
            DltIntegerWidth.Bits32 => SpanReader.ReadUInt32(payload, msbFirst),
            _ => SpanReader.ReadUInt64(payload, msbFirst),
        };
        return new DltUnsignedArgument(value, width);
    }

    private static DltArgument ParseFloat(ReadOnlySpan<byte> payload, bool msbFirst, uint typeInfo, out int bytesRead)
    {
        var width = (typeInfo & MaskTypeLength) switch
        {
            3 => DltFloatWidth.Bits32,
            4 => DltFloatWidth.Bits64,
            _ => throw UnsupportedTypeInfo(typeInfo),
        };

        if (payload.Length < (int)width)
        {
            throw new DltParseException(
                $"There is no data for a {(int)width * 8} bit float. " + $"Type Info: {FormatTypeInfo(typeInfo)}."
            );
        }

        bytesRead = TypeInfoLength + (int)width;
        var value =
            width == DltFloatWidth.Bits32
                ? SpanReader.ReadSingle(payload, msbFirst)
                : SpanReader.ReadDouble(payload, msbFirst);
        return new DltFloatArgument(value, width);
    }

    private static DltArgument ParseString(
        ReadOnlySpan<byte> payload,
        bool msbFirst,
        uint typeInfo,
        Encoding? encoding,
        out int bytesRead
    )
    {
        var coding = (typeInfo & MaskStringCoding) switch
        {
            StringCodingAscii => DltStringCoding.Ascii,
            StringCodingUtf8 => DltStringCoding.Utf8,
            _ => throw UnsupportedTypeInfo(typeInfo),
        };

        var text = ReadLengthPrefixed(payload, msbFirst, typeInfo, out bytesRead, out var complete);

        // The string on the wire is NUL terminated; the terminator is not part of the value.
        if (complete && !text.IsEmpty)
        {
            text = text[..^1];
        }

        var effectiveEncoding = coding == DltStringCoding.Utf8 ? Encoding.UTF8 : encoding ?? Encoding.ASCII;
        return new DltStringArgument(effectiveEncoding.GetString(text), coding);
    }

    private static DltArgument ParseRaw(ReadOnlySpan<byte> payload, bool msbFirst, uint typeInfo, out int bytesRead)
    {
        var raw = ReadLengthPrefixed(payload, msbFirst, typeInfo, out bytesRead, out _);
        return new DltRawArgument(raw.ToArray());
    }

    private static ReadOnlySpan<byte> ReadLengthPrefixed(
        ReadOnlySpan<byte> payload,
        bool msbFirst,
        uint typeInfo,
        out int bytesRead,
        out bool complete
    )
    {
        if (payload.Length < LengthFieldSize)
        {
            throw new DltParseException(
                $"There is no length field for the argument. Type Info: {FormatTypeInfo(typeInfo)}."
            );
        }

        int length = SpanReader.ReadUInt16(payload, msbFirst);
        bytesRead = TypeInfoLength + LengthFieldSize + length;

        var available = payload[LengthFieldSize..];
        complete = length <= available.Length;
        return complete ? available[..length] : available;
    }

    /// <summary>
    /// Resolves the width of an integer argument. If the remaining data is shorter than the
    /// declared width, the next smaller width is used, mirroring the behavior of DLT Viewer.
    /// </summary>
    private static DltIntegerWidth ResolveIntegerWidth(int available, uint typeInfo, string typeName)
    {
        var declared = (typeInfo & MaskTypeLength) switch
        {
            1 => DltIntegerWidth.Bits8,
            2 => DltIntegerWidth.Bits16,
            3 => DltIntegerWidth.Bits32,
            4 => DltIntegerWidth.Bits64,
            _ => throw UnsupportedTypeInfo(typeInfo),
        };

        foreach (
            var width in (ReadOnlySpan<DltIntegerWidth>)
                [DltIntegerWidth.Bits64, DltIntegerWidth.Bits32, DltIntegerWidth.Bits16, DltIntegerWidth.Bits8]
        )
        {
            if (width <= declared && (int)width <= available)
            {
                return width;
            }
        }

        throw new DltParseException(
            $"There is no data for a {(int)declared * 8} bit {typeName}. " + $"Type Info: {FormatTypeInfo(typeInfo)}."
        );
    }

    private static DltParseException UnsupportedTypeInfo(uint typeInfo) =>
        new($"Unsupported Type Info: {FormatTypeInfo(typeInfo)}.");

    private static string FormatTypeInfo(uint typeInfo) => $"0x{typeInfo:X8}";
}