📄 lib/src/DltReader.cs
using System.Runtime.CompilerServices;

namespace SharpDlt;

/// <summary>
/// Reads DLT messages from a stream or a DLT file.
/// </summary>
/// <example>
/// <code>
/// foreach (var message in DltReader.ReadMessages("trace.dlt"))
/// {
///     Console.WriteLine(message);
/// }
/// </code>
/// </example>
public static class DltReader
{
    /// <summary>
    /// Reads all DLT messages from <paramref name="stream"/>.
    /// </summary>
    /// <param name="stream">The stream to read from. The caller keeps ownership of the stream.</param>
    /// <param name="options">Options controlling the parsing, <see cref="DltReaderOptions.Default"/> if omitted.</param>
    /// <returns>A lazily evaluated sequence of DLT messages.</returns>
    /// <remarks>
    /// Reading stops silently as soon as the remaining data no longer contains a complete message.
    /// </remarks>
    /// <exception cref="DltParseException">The data of a message is malformed.</exception>
    public static IEnumerable<DltMessage> ReadMessages(Stream stream, DltReaderOptions? options = null)
    {
        ArgumentNullException.ThrowIfNull(stream);
        return Iterate(stream, options ?? DltReaderOptions.Default);

        static IEnumerable<DltMessage> Iterate(Stream stream, DltReaderOptions options)
        {
            var prefixLength = GetPrefixLength(options);
            var prefix = new byte[prefixLength];

            while (true)
            {
                if (stream.ReadAtLeast(prefix, prefixLength, throwOnEndOfStream: false) < prefixLength)
                {
                    yield break;
                }

                var buffer = CreateMessageBuffer(prefix, options, out var remaining);
                if (
                    remaining > 0
                    && stream.ReadAtLeast(buffer.AsSpan(prefixLength), remaining, throwOnEndOfStream: false) < remaining
                )
                {
                    yield break;
                }

                yield return DltMessage.Parse(buffer, options);
            }
        }
    }

    /// <summary>
    /// Opens the DLT file at <paramref name="path"/> and reads all DLT messages from it.
    /// </summary>
    /// <param name="path">Path of the DLT file.</param>
    /// <param name="options">Options controlling the parsing, <see cref="DltReaderOptions.Default"/> if omitted.</param>
    /// <returns>A lazily evaluated sequence of DLT messages. The file is closed when enumeration ends.</returns>
    /// <exception cref="DltParseException">The data of a message is malformed.</exception>
    public static IEnumerable<DltMessage> ReadMessages(string path, DltReaderOptions? options = null)
    {
        ArgumentException.ThrowIfNullOrEmpty(path);
        return Iterate(path, options);

        static IEnumerable<DltMessage> Iterate(string path, DltReaderOptions? options)
        {
            using var stream = OpenFile(path);
            foreach (var message in ReadMessages(stream, options))
            {
                yield return message;
            }
        }
    }

    /// <summary>
    /// Asynchronously reads all DLT messages from <paramref name="stream"/>.
    /// </summary>
    /// <param name="stream">The stream to read from. The caller keeps ownership of the stream.</param>
    /// <param name="options">Options controlling the parsing, <see cref="DltReaderOptions.Default"/> if omitted.</param>
    /// <param name="cancellationToken">Token to cancel the enumeration.</param>
    /// <returns>A lazily evaluated asynchronous sequence of DLT messages.</returns>
    /// <exception cref="DltParseException">The data of a message is malformed.</exception>
    public static IAsyncEnumerable<DltMessage> ReadMessagesAsync(
        Stream stream,
        DltReaderOptions? options = null,
        CancellationToken cancellationToken = default
    )
    {
        ArgumentNullException.ThrowIfNull(stream);
        return Iterate(stream, options ?? DltReaderOptions.Default, cancellationToken);

        static async IAsyncEnumerable<DltMessage> Iterate(
            Stream stream,
            DltReaderOptions options,
            [EnumeratorCancellation] CancellationToken cancellationToken
        )
        {
            var prefixLength = GetPrefixLength(options);
            var prefix = new byte[prefixLength];

            while (true)
            {
                var read = await stream
                    .ReadAtLeastAsync(prefix, prefixLength, false, cancellationToken)
                    .ConfigureAwait(false);
                if (read < prefixLength)
                {
                    yield break;
                }

                var buffer = CreateMessageBuffer(prefix, options, out var remaining);
                if (remaining > 0)
                {
                    read = await stream
                        .ReadAtLeastAsync(buffer.AsMemory(prefixLength), remaining, false, cancellationToken)
                        .ConfigureAwait(false);
                    if (read < remaining)
                    {
                        yield break;
                    }
                }

                yield return DltMessage.Parse(buffer, options);
            }
        }
    }

    /// <summary>
    /// Opens the DLT file at <paramref name="path"/> and asynchronously reads all DLT messages from it.
    /// </summary>
    /// <param name="path">Path of the DLT file.</param>
    /// <param name="options">Options controlling the parsing, <see cref="DltReaderOptions.Default"/> if omitted.</param>
    /// <param name="cancellationToken">Token to cancel the enumeration.</param>
    /// <returns>A lazily evaluated asynchronous sequence of DLT messages. The file is closed when enumeration ends.</returns>
    /// <exception cref="DltParseException">The data of a message is malformed.</exception>
    public static IAsyncEnumerable<DltMessage> ReadMessagesAsync(
        string path,
        DltReaderOptions? options = null,
        CancellationToken cancellationToken = default
    )
    {
        ArgumentException.ThrowIfNullOrEmpty(path);
        return Iterate(path, options, cancellationToken);

        static async IAsyncEnumerable<DltMessage> Iterate(
            string path,
            DltReaderOptions? options,
            [EnumeratorCancellation] CancellationToken cancellationToken
        )
        {
            await using var stream = OpenFile(path, useAsync: true);
            await foreach (var message in ReadMessagesAsync(stream, options, cancellationToken).ConfigureAwait(false))
            {
                yield return message;
            }
        }
    }

    private static FileStream OpenFile(string path, bool useAsync = false) =>
        new(
            path,
            new FileStreamOptions
            {
                Mode = FileMode.Open,
                Access = FileAccess.Read,
                Share = FileShare.Read,
                Options = FileOptions.SequentialScan | (useAsync ? FileOptions.Asynchronous : 0),
            }
        );

    private static int GetPrefixLength(DltReaderOptions options) =>
        (options.HasStorageHeader ? StorageHeader.Length : 0) + StandardHeader.MinimumLength;

    private static byte[] CreateMessageBuffer(byte[] prefix, DltReaderOptions options, out int remaining)
    {
        var storageHeaderLength = options.HasStorageHeader ? StorageHeader.Length : 0;
        var length = StandardHeader.ReadMessageLength(prefix.AsSpan(storageHeaderLength));
        if (length < StandardHeader.MinimumLength)
        {
            throw new DltParseException(
                $"Invalid length of a DLT message: {length} / it must be " + $"{StandardHeader.MinimumLength} or more."
            );
        }

        var buffer = new byte[storageHeaderLength + length];
        prefix.CopyTo(buffer.AsSpan());
        remaining = buffer.Length - prefix.Length;
        return buffer;
    }
}