using System;
using System.Collections.Generic;
using System.IO;

public sealed class ListByteArrayStream : Stream
{
    private readonly IReadOnlyList<byte[]> _buffers;
    private readonly long _sourceLength;
    private long _position;
    private long _length;

    public ListByteArrayStream(List<byte[]> buffers)
    {
        _buffers = buffers ?? throw new ArgumentNullException(nameof(buffers));

        long length = 0;
        for (int i = 0; i < _buffers.Count; i++)
            length += _buffers[i]?.Length ?? 0;

        _sourceLength = length;
        _length = length;
        _position = 0;
    }

    public override bool CanRead => true;
    public override bool CanSeek => true;
    public override bool CanWrite => false;

    public override long Length => _length;

    public override long Position
    {
        get => _position;
        set
        {
            if (value < 0 || value > _length)
                throw new ArgumentOutOfRangeException(nameof(value));

            _position = value;
        }
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        ArgumentNullException.ThrowIfNull(buffer);

        if (offset < 0 || count < 0 || offset > buffer.Length - count)
            throw new ArgumentOutOfRangeException();

        if (count == 0 || _position >= _length)
            return 0;

        int totalRead = 0;

        while (count > 0 && _position < _length)
        {
            LocatePosition(_position, out int bufferIndex, out int bufferOffset);
            byte[] current = _buffers[bufferIndex] ?? Array.Empty<byte>();

            long maxReadableInCurrentStream = Math.Min(current.Length - bufferOffset, _length - _position);
            int toCopy = (int)Math.Min(count, maxReadableInCurrentStream);

            Buffer.BlockCopy(current, bufferOffset, buffer, offset, toCopy);

            offset += toCopy;
            count -= toCopy;
            totalRead += toCopy;
            _position += toCopy;
        }

        return totalRead;
    }

    public override int Read(Span<byte> destination)
    {
        if (destination.IsEmpty || _position >= _length)
            return 0;

        int totalRead = 0;

        while (!destination.IsEmpty && _position < _length)
        {
            LocatePosition(_position, out int bufferIndex, out int bufferOffset);
            byte[] current = _buffers[bufferIndex] ?? Array.Empty<byte>();

            long maxReadableInCurrentStream = Math.Min(current.Length - bufferOffset, _length - _position);
            int toCopy = (int)Math.Min(destination.Length, maxReadableInCurrentStream);

            current.AsSpan(bufferOffset, toCopy).CopyTo(destination);

            destination = destination.Slice(toCopy);
            totalRead += toCopy;
            _position += toCopy;
        }

        return totalRead;
    }

    public override int ReadByte()
    {
        if (_position >= _length)
            return -1;

        LocatePosition(_position, out int bufferIndex, out int bufferOffset);
        byte[] current = _buffers[bufferIndex] ?? Array.Empty<byte>();

        if (bufferOffset >= current.Length)
            return -1;

        _position++;
        return current[bufferOffset];
    }

    public override long Seek(long offset, SeekOrigin origin)
    {
        long newPosition = origin switch
        {
            SeekOrigin.Begin => offset,
            SeekOrigin.Current => _position + offset,
            SeekOrigin.End => _length + offset,
            _ => throw new ArgumentOutOfRangeException(nameof(origin))
        };

        if (newPosition < 0 || newPosition > _length)
            throw new IOException("Attempted to seek outside the stream bounds.");

        _position = newPosition;
        return _position;
    }

    public override void Flush()
    {
        // No-op
    }

    public override void SetLength(long value)
    {
        if (value < 0)
            throw new ArgumentOutOfRangeException(nameof(value));

        if (value > _sourceLength)
            throw new NotSupportedException("Enlarging the stream is not supported.");

        _length = value;

        if (_position > _length)
            _position = _length;
    }

    public override void Write(byte[] buffer, int offset, int count)
        => throw new NotSupportedException("Writing is not supported.");

    public override void Write(ReadOnlySpan<byte> buffer)
        => throw new NotSupportedException("Writing is not supported.");

    private void LocatePosition(long position, out int bufferIndex, out int bufferOffset)
    {
        long remaining = position;

        for (int i = 0; i < _buffers.Count; i++)
        {
            int currentLength = _buffers[i]?.Length ?? 0;

            if (remaining < currentLength)
            {
                bufferIndex = i;
                bufferOffset = (int)remaining;
                return;
            }

            remaining -= currentLength;
        }

        bufferIndex = _buffers.Count - 1;
        bufferOffset = 0;
    }
}
