using System;
using System.Collections.Generic;
using System.IO;
using SharpCompress.Common;

namespace SharpCompress.Compressors.Arj;

[CLSCompliant(true)]
public sealed partial class LHDecoderStream : Stream
{
    private readonly BitReader _bitReader;

    // Buffer containing *all* bytes decoded so far.
    private readonly List<byte> _buffer = new();

    private long _readPosition;
    private readonly int _originalSize;
    private bool _finishedDecoding;
    private bool _disposed;

    private const int THRESHOLD = 3;

    public LHDecoderStream(Stream compressedStream, int originalSize)
    {
        if (!compressedStream.CanRead)
        {
            throw new ArgumentException(
                "compressedStream must be readable.",
                nameof(compressedStream)
            );
        }

        _bitReader = new BitReader(compressedStream);
        _originalSize = originalSize;
        _readPosition = 0;
        _finishedDecoding = (originalSize == 0);
    }

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

    public override long Length => _originalSize;

    public override long Position
    {
        get => _readPosition;
        set => throw new NotSupportedException();
    }

    /// <summary>
    /// Decodes a single element (literal or back-reference) and appends it to _buffer.
    /// Returns true if data was added, or false if all input has already been decoded.
    /// </summary>
    private bool DecodeNext()
    {
        if (_buffer.Count >= _originalSize)
        {
            _finishedDecoding = true;
            return false;
        }

        int len = DecodeVal(0, 7);
        if (len == 0)
        {
            byte nextChar = (byte)_bitReader.ReadBits(8);
            _buffer.Add(nextChar);
        }
        else
        {
            int repCount = len + THRESHOLD - 1;
            int backPtr = DecodeVal(9, 13);

            if (backPtr >= _buffer.Count)
            {
                throw new InvalidFormatException("Invalid back_ptr in LH stream");
            }

            int srcIndex = _buffer.Count - 1 - backPtr;
            for (int j = 0; j < repCount && _buffer.Count < _originalSize; j++)
            {
                byte b = _buffer[srcIndex];
                _buffer.Add(b);
                srcIndex++;
                // srcIndex may grow; it's allowed (source region can overlap destination)
            }
        }

        if (_buffer.Count >= _originalSize)
        {
            _finishedDecoding = true;
        }

        return true;
    }

    private int DecodeVal(int from, int to)
    {
        int add = 0;
        int bit = from;

        while (bit < to && _bitReader.ReadBits(1) == 1)
        {
            add |= 1 << bit;
            bit++;
        }

        int res = bit > 0 ? _bitReader.ReadBits(bit) : 0;
        return res + add;
    }

    /// <summary>
    /// Reads decompressed bytes into buffer[offset..offset+count].
    /// The method decodes additional data on demand when needed.
    /// </summary>
    public override int Read(byte[] buffer, int offset, int count)
    {
        if (_disposed)
        {
            throw new ObjectDisposedException(nameof(LHDecoderStream));
        }

        ThrowHelper.ThrowIfNull(buffer);

        if (offset < 0 || count < 0 || offset + count > buffer.Length)
        {
            throw new ArgumentOutOfRangeException("offset/count");
        }

        if (_readPosition >= _originalSize)
        {
            return 0; // EOF
        }

        int totalRead = 0;

        while (totalRead < count && _readPosition < _originalSize)
        {
            if (_readPosition >= _buffer.Count)
            {
                bool had = DecodeNext();
                if (!had)
                {
                    break;
                }
            }

            int available = _buffer.Count - (int)_readPosition;
            if (available <= 0)
            {
                if (!_finishedDecoding)
                {
                    continue;
                }
                break;
            }

            int toCopy = Math.Min(available, count - totalRead);
            _buffer.CopyTo((int)_readPosition, buffer, offset + totalRead, toCopy);

            _readPosition += toCopy;
            totalRead += toCopy;
        }

        return totalRead;
    }

    public override void Flush() => throw new NotSupportedException();

    public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();

    public override void SetLength(long value) => throw new NotSupportedException();

    public override void Write(byte[] buffer, int offset, int count) =>
        throw new NotSupportedException();

    protected override void Dispose(bool disposing)
    {
        if (disposing && !_disposed)
        {
            _disposed = true;
        }
        base.Dispose(disposing);
    }
}
