public static class BinaryReaderExtensions
{
    public static ushort ReadUInt16LE(this BinaryReader reader)
    {
        ArgumentNullException.ThrowIfNull(reader);
        return reader.ReadUInt16();
    }

    public static uint ReadUInt32LE(this BinaryReader reader)
    {
        ArgumentNullException.ThrowIfNull(reader);
        return reader.ReadUInt32();
    }

    public static ulong ReadUInt64LE(this BinaryReader reader)
    {
        ArgumentNullException.ThrowIfNull(reader);
        return reader.ReadUInt64();
    }

    public static byte[] ReadBytesExact(this BinaryReader reader, int count)
    {
        ArgumentNullException.ThrowIfNull(reader);

        if (count < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(count), "Byte count cannot be negative.");
        }

        var bytes = reader.ReadBytes(count);
        if (bytes.Length != count)
        {
            throw new EndOfStreamException($"Expected {count} bytes but got {bytes.Length}.");
        }

        return bytes;
    }

    public static string ReadNullTerminatedAscii(this BinaryReader reader, int maxBytes)
    {
        ArgumentNullException.ThrowIfNull(reader);

        if (maxBytes <= 0)
        {
            return string.Empty;
        }

        var stream = reader.BaseStream;
        if (!stream.CanRead)
        {
            throw new IOException("The underlying stream is not readable.");
        }

        var buffer = new byte[maxBytes];
        var index = 0;

        while (index < maxBytes && stream.Position < stream.Length)
        {
            var b = reader.ReadByte();
            if (b == 0)
            {
                break;
            }

            buffer[index++] = b;
        }

        return index == 0
            ? string.Empty
            : Encoding.ASCII.GetString(buffer, 0, index);
    }

    public static string ReadFixedAscii(this BinaryReader reader, int count)
    {
        ArgumentNullException.ThrowIfNull(reader);

        if (count < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(count), "Character count cannot be negative.");
        }

        if (count == 0)
        {
            return string.Empty;
        }

        var bytes = reader.ReadBytesExact(count);
        var zero = Array.IndexOf(bytes, (byte)0);

        return zero >= 0
            ? Encoding.ASCII.GetString(bytes, 0, zero)
            : Encoding.ASCII.GetString(bytes);
    }

    public static string ReadUnicodeStringAt(byte[] data, int offset, int maxChars)
    {
        ArgumentNullException.ThrowIfNull(data);

        if (offset < 0 || offset > data.Length)
        {
            return string.Empty;
        }

        if (maxChars <= 0 || offset == data.Length)
        {
            return string.Empty;
        }

        var availableBytes = data.Length - offset;
        var readableChars = Math.Min(maxChars, availableBytes / 2);
        if (readableChars <= 0)
        {
            return string.Empty;
        }

        var sb = new StringBuilder(readableChars);

        for (var i = 0; i < readableChars; i++)
        {
            var currentOffset = offset + (i * 2);
            BoundsChecker.EnsureRange(currentOffset, 2, data.Length, "unicode string read");

            var ch = BinaryPrimitives.ReadUInt16LittleEndian(data.AsSpan(currentOffset, 2));
            if (ch == 0)
            {
                break;
            }

            sb.Append((char)ch);
        }

        return sb.ToString();
    }

    public static string ReadAsciiStringAt(byte[] data, int offset, int maxBytes)
    {
        ArgumentNullException.ThrowIfNull(data);

        if (offset < 0 || offset >= data.Length)
        {
            return string.Empty;
        }

        if (maxBytes <= 0)
        {
            return string.Empty;
        }

        var availableBytes = data.Length - offset;
        var readableBytes = Math.Min(maxBytes, availableBytes);
        if (readableBytes <= 0)
        {
            return string.Empty;
        }

        var end = offset;
        var limit = offset + readableBytes;

        while (end < limit && data[end] != 0)
        {
            end++;
        }

        return end == offset
            ? string.Empty
            : Encoding.ASCII.GetString(data, offset, end - offset);
    }
}