#nullable disable

using System;
using static SharpCompress.Compressors.Rar.UnpackV2017.PackDef;
using static SharpCompress.Compressors.Rar.UnpackV2017.UnpackGlobal;
#if !Rar2017_64bit
using size_t = System.UInt32;
#else
using nint = System.Int64;
using nuint = System.UInt64;
using size_t = System.UInt64;
#endif

namespace SharpCompress.Compressors.Rar.UnpackV2017;

internal partial class Unpack
{
    private void Unpack5(bool Solid)
    {
        FileExtracted = true;

        if (!Suspended)
        {
            UnpInitData(Solid);
            if (!UnpReadBuf())
            {
                return;
            }

            // Check TablesRead5 to be sure that we read tables at least once
            // regardless of current block header TablePresent flag.
            // So we can safefly use these tables below.
            if (
                !ReadBlockHeader(Inp, ref BlockHeader)
                || !ReadTables(Inp, ref BlockHeader, ref BlockTables)
                || !TablesRead5
            )
            {
                return;
            }
        }

        while (true)
        {
            UnpPtr &= MaxWinMask;

            if (Inp.InAddr >= ReadBorder)
            {
                var FileDone = false;

                // We use 'while', because for empty block containing only Huffman table,
                // we'll be on the block border once again just after reading the table.
                while (
                    Inp.InAddr > BlockHeader.BlockStart + BlockHeader.BlockSize - 1
                    || Inp.InAddr == BlockHeader.BlockStart + BlockHeader.BlockSize - 1
                        && Inp.InBit >= BlockHeader.BlockBitSize
                )
                {
                    if (BlockHeader.LastBlockInFile)
                    {
                        FileDone = true;
                        break;
                    }
                    if (
                        !ReadBlockHeader(Inp, ref BlockHeader)
                        || !ReadTables(Inp, ref BlockHeader, ref BlockTables)
                    )
                    {
                        return;
                    }
                }
                if (FileDone || !UnpReadBuf())
                {
                    break;
                }
            }

            if (((WriteBorder - UnpPtr) & MaxWinMask) < MAX_LZ_MATCH + 3 && WriteBorder != UnpPtr)
            {
                UnpWriteBuf();
                if (WrittenFileSize > DestUnpSize)
                {
                    return;
                }

                if (Suspended)
                {
                    FileExtracted = false;
                    return;
                }
            }

            var MainSlot = DecodeNumber(Inp, BlockTables.LD);
            if (MainSlot < 256)
            {
                if (Fragmented)
                {
                    FragWindow[UnpPtr++] = (byte)MainSlot;
                }
                else
                {
                    Window[UnpPtr++] = (byte)MainSlot;
                }

                continue;
            }
            if (MainSlot >= 262)
            {
                var Length = SlotToLength(Inp, MainSlot - 262);

                uint DBits,
                    Distance = 1,
                    DistSlot = DecodeNumber(Inp, BlockTables.DD);
                if (DistSlot < 4)
                {
                    DBits = 0;
                    Distance += DistSlot;
                }
                else
                {
                    DBits = (DistSlot / 2) - 1;
                    Distance += (2 | (DistSlot & 1)) << (int)DBits;
                }

                if (DBits > 0)
                {
                    if (DBits >= 4)
                    {
                        if (DBits > 4)
                        {
                            Distance += ((Inp.getbits32() >> (int)(36 - DBits)) << 4);
                            Inp.addbits(DBits - 4);
                        }
                        var LowDist = DecodeNumber(Inp, BlockTables.LDD);
                        Distance += LowDist;
                    }
                    else
                    {
                        Distance += Inp.getbits32() >> (int)(32 - DBits);
                        Inp.addbits(DBits);
                    }
                }

                if (Distance > 0x100)
                {
                    Length++;
                    if (Distance > 0x2000)
                    {
                        Length++;
                        if (Distance > 0x40000)
                        {
                            Length++;
                        }
                    }
                }

                InsertOldDist(Distance);
                LastLength = Length;
                if (Fragmented)
                {
                    FragWindow.CopyString(Length, Distance, ref UnpPtr, MaxWinMask);
                }
                else
                {
                    CopyString(Length, Distance);
                }

                continue;
            }
            if (MainSlot == 256)
            {
                var Filter = new UnpackFilter();
                if (!ReadFilter(Inp, Filter) || !AddFilter(Filter))
                {
                    break;
                }

                continue;
            }
            if (MainSlot == 257)
            {
                if (LastLength != 0)
                {
                    if (Fragmented)
                    {
                        FragWindow.CopyString(LastLength, OldDist[0], ref UnpPtr, MaxWinMask);
                    }
                    else
                    {
                        CopyString(LastLength, OldDist[0]);
                    }
                }

                continue;
            }
            if (MainSlot < 262)
            {
                var DistNum = MainSlot - 258;
                var Distance = OldDist[DistNum];
                for (var I = DistNum; I > 0; I--)
                {
                    OldDist[I] = OldDist[I - 1];
                }

                OldDist[0] = Distance;

                var LengthSlot = DecodeNumber(Inp, BlockTables.RD);
                var Length = SlotToLength(Inp, LengthSlot);
                LastLength = Length;
                if (Fragmented)
                {
                    FragWindow.CopyString(Length, Distance, ref UnpPtr, MaxWinMask);
                }
                else
                {
                    CopyString(Length, Distance);
                }

                continue;
            }
        }
        UnpWriteBuf();
    }

    private async System.Threading.Tasks.Task Unpack5Async(
        bool Solid,
        System.Threading.CancellationToken cancellationToken = default
    )
    {
        FileExtracted = true;

        if (!Suspended)
        {
            UnpInitData(Solid);
            if (!await UnpReadBufAsync(cancellationToken).ConfigureAwait(false))
            {
                return;
            }

            if (
                !ReadBlockHeader(Inp, ref BlockHeader)
                || !ReadTables(Inp, ref BlockHeader, ref BlockTables)
                || !TablesRead5
            )
            {
                return;
            }
        }

        while (true)
        {
            UnpPtr &= MaxWinMask;

            if (Inp.InAddr >= ReadBorder)
            {
                var FileDone = false;

                while (
                    Inp.InAddr > BlockHeader.BlockStart + BlockHeader.BlockSize - 1
                    || Inp.InAddr == BlockHeader.BlockStart + BlockHeader.BlockSize - 1
                        && Inp.InBit >= BlockHeader.BlockBitSize
                )
                {
                    if (BlockHeader.LastBlockInFile)
                    {
                        FileDone = true;
                        break;
                    }
                    if (
                        !ReadBlockHeader(Inp, ref BlockHeader)
                        || !ReadTables(Inp, ref BlockHeader, ref BlockTables)
                    )
                    {
                        return;
                    }
                }
                if (FileDone || !await UnpReadBufAsync(cancellationToken).ConfigureAwait(false))
                {
                    break;
                }
            }

            if (((WriteBorder - UnpPtr) & MaxWinMask) < MAX_LZ_MATCH + 3 && WriteBorder != UnpPtr)
            {
                await UnpWriteBufAsync(cancellationToken).ConfigureAwait(false);
                if (WrittenFileSize > DestUnpSize)
                {
                    return;
                }
            }

            uint MainSlot = DecodeNumber(Inp, BlockTables.LD);
            if (MainSlot < 256)
            {
                if (Fragmented)
                {
                    FragWindow[UnpPtr++] = (byte)MainSlot;
                }
                else
                {
                    Window[UnpPtr++] = (byte)MainSlot;
                }
                continue;
            }
            if (MainSlot >= 262)
            {
                uint Length = SlotToLength(Inp, MainSlot - 262);

                uint DBits,
                    Distance = 1,
                    DistSlot = DecodeNumber(Inp, BlockTables.DD);
                if (DistSlot < 4)
                {
                    DBits = 0;
                    Distance += DistSlot;
                }
                else
                {
                    DBits = (DistSlot / 2) - 1;
                    Distance += (2 | (DistSlot & 1)) << (int)DBits;
                }

                if (DBits > 0)
                {
                    if (DBits >= 4)
                    {
                        if (DBits > 4)
                        {
                            Distance += ((Inp.getbits() >> (int)(20 - DBits)) << 4);
                            Inp.addbits(DBits - 4);
                        }

                        uint LowDist = DecodeNumber(Inp, BlockTables.LDD);
                        Distance += LowDist;
                    }
                    else
                    {
                        Distance += Inp.getbits() >> (int)(16 - DBits);
                        Inp.addbits(DBits);
                    }
                }

                if (Distance > 0x100)
                {
                    Length++;
                    if (Distance > 0x2000)
                    {
                        Length++;
                        if (Distance > 0x40000)
                        {
                            Length++;
                        }
                    }
                }

                InsertOldDist(Distance);
                LastLength = Length;
                CopyString(Length, Distance);
                continue;
            }
            if (MainSlot == 256)
            {
                var Filter = new UnpackFilter();
                if (!ReadFilter(Inp, Filter) || !AddFilter(Filter))
                {
                    break;
                }
                continue;
            }
            if (MainSlot == 257)
            {
                if (LastLength != 0)
                {
                    CopyString((uint)LastLength, (uint)OldDist[0]);
                }
                continue;
            }
            if (MainSlot < 262)
            {
                uint DistNum = MainSlot - 258;
                uint Distance = (uint)OldDist[(int)DistNum];
                for (var I = (int)DistNum; I > 0; I--)
                {
                    OldDist[I] = OldDist[I - 1];
                }
                OldDist[0] = Distance;

                uint LengthSlot = DecodeNumber(Inp, BlockTables.RD);
                uint Length = SlotToLength(Inp, LengthSlot);
                LastLength = Length;
                CopyString(Length, Distance);

                continue;
            }
        }
        await UnpWriteBufAsync(cancellationToken).ConfigureAwait(false);
    }

    private uint ReadFilterData(BitInput Inp)
    {
        var ByteCount = (Inp.fgetbits() >> 14) + 1;
        Inp.addbits(2);

        uint Data = 0;
        for (uint I = 0; I < ByteCount; I++)
        {
            Data += (Inp.fgetbits() >> 8) << (int)(I * 8);
            Inp.addbits(8);
        }
        return Data;
    }

    private bool ReadFilter(BitInput Inp, UnpackFilter Filter)
    {
        if (!Inp.ExternalBuffer && Inp.InAddr > ReadTop - 16)
        {
            if (!UnpReadBuf())
            {
                return false;
            }
        }

        Filter.BlockStart = ReadFilterData(Inp);
        Filter.BlockLength = ReadFilterData(Inp);
        if (Filter.BlockLength > MAX_FILTER_BLOCK_SIZE)
        {
            Filter.BlockLength = 0;
        }

        Filter.Type = (byte)(Inp.fgetbits() >> 13);
        Inp.faddbits(3);

        if (Filter.Type == FILTER_DELTA)
        {
            Filter.Channels = (byte)((Inp.fgetbits() >> 11) + 1);
            Inp.faddbits(5);
        }

        return true;
    }

    private bool AddFilter(UnpackFilter Filter)
    {
        if (Filters.Count >= MAX_UNPACK_FILTERS)
        {
            UnpWriteBuf(); // Write data, apply and flush filters.
            if (Filters.Count >= MAX_UNPACK_FILTERS)
            {
                InitFilters(); // Still too many filters, prevent excessive memory use.
            }
        }

        // If distance to filter start is that large that due to circular dictionary
        // mode now it points to old not written yet data, then we set 'NextWindow'
        // flag and process this filter only after processing that older data.
        Filter.NextWindow = WrPtr != UnpPtr && ((WrPtr - UnpPtr) & MaxWinMask) <= Filter.BlockStart;

        Filter.BlockStart = (Filter.BlockStart + UnpPtr) & MaxWinMask;
        Filters.Add(Filter);
        return true;
    }

    private bool UnpReadBuf()
    {
        var DataSize = ReadTop - Inp.InAddr; // Data left to process.
        if (DataSize < 0)
        {
            return false;
        }

        BlockHeader.BlockSize -= Inp.InAddr - BlockHeader.BlockStart;
        if (Inp.InAddr > MAX_SIZE / 2)
        {
            // If we already processed more than half of buffer, let's move
            // remaining data into beginning to free more space for new data
            // and ensure that calling function does not cross the buffer border
            // even if we did not read anything here. Also it ensures that read size
            // is not less than CRYPT_BLOCK_SIZE, so we can align it without risk
            // to make it zero.
            if (DataSize > 0)
            //x memmove(Inp.InBuf,Inp.InBuf+Inp.InAddr,DataSize);
            {
                Buffer.BlockCopy(Inp.InBuf, Inp.InAddr, Inp.InBuf, 0, DataSize);
            }

            Inp.InAddr = 0;
            ReadTop = DataSize;
        }
        else
        {
            DataSize = ReadTop;
        }

        var ReadCode = 0;
        if (MAX_SIZE != DataSize)
        {
            ReadCode = UnpIO_UnpRead(Inp.InBuf, DataSize, MAX_SIZE - DataSize);
        }

        if (ReadCode > 0) // Can be also -1.
        {
            ReadTop += ReadCode;
        }

        ReadBorder = ReadTop - 30;
        BlockHeader.BlockStart = Inp.InAddr;
        if (BlockHeader.BlockSize != -1) // '-1' means not defined yet.
        {
            // We may need to quit from main extraction loop and read new block header
            // and trees earlier than data in input buffer ends.
            ReadBorder = Math.Min(ReadBorder, BlockHeader.BlockStart + BlockHeader.BlockSize - 1);
        }
        return ReadCode != -1;
    }

    private async System.Threading.Tasks.Task<bool> UnpReadBufAsync(
        System.Threading.CancellationToken cancellationToken = default
    )
    {
        var DataSize = ReadTop - Inp.InAddr; // Data left to process.
        if (DataSize < 0)
        {
            return false;
        }

        BlockHeader.BlockSize -= Inp.InAddr - BlockHeader.BlockStart;
        if (Inp.InAddr > MAX_SIZE / 2)
        {
            if (DataSize > 0)
            {
                Buffer.BlockCopy(Inp.InBuf, Inp.InAddr, Inp.InBuf, 0, DataSize);
            }

            Inp.InAddr = 0;
            ReadTop = DataSize;
        }
        else
        {
            DataSize = ReadTop;
        }

        var ReadCode = 0;
        if (MAX_SIZE != DataSize)
        {
            ReadCode = await UnpIO_UnpReadAsync(
                    Inp.InBuf,
                    DataSize,
                    MAX_SIZE - DataSize,
                    cancellationToken
                )
                .ConfigureAwait(false);
        }

        if (ReadCode > 0) // Can be also -1.
        {
            ReadTop += ReadCode;
        }

        ReadBorder = ReadTop - 30;
        BlockHeader.BlockStart = Inp.InAddr;
        if (BlockHeader.BlockSize != -1) // '-1' means not defined yet.
        {
            ReadBorder = Math.Min(ReadBorder, BlockHeader.BlockStart + BlockHeader.BlockSize - 1);
        }
        return ReadCode != -1;
    }

    private void UnpWriteBuf()
    {
        var WrittenBorder = WrPtr;
        var FullWriteSize = (UnpPtr - WrittenBorder) & MaxWinMask;
        var WriteSizeLeft = FullWriteSize;
        var NotAllFiltersProcessed = false;
        //for (size_t I=0;I<Filters.Count;I++)
        // sharpcompress: size_t -> int
        for (var I = 0; I < Filters.Count; I++)
        {
            // Here we apply filters to data which we need to write.
            // We always copy data to another memory block before processing.
            // We cannot process them just in place in Window buffer, because
            // these data can be used for future string matches, so we must
            // preserve them in original form.

            var flt = Filters[I];
            if (flt.Type == FILTER_NONE)
            {
                continue;
            }

            if (flt.NextWindow)
            {
                // Here we skip filters which have block start in current data range
                // due to address wrap around in circular dictionary, but actually
                // belong to next dictionary block. If such filter start position
                // is included to current write range, then we reset 'NextWindow' flag.
                // In fact we can reset it even without such check, because current
                // implementation seems to guarantee 'NextWindow' flag reset after
                // buffer writing for all existing filters. But let's keep this check
                // just in case. Compressor guarantees that distance between
                // filter block start and filter storing position cannot exceed
                // the dictionary size. So if we covered the filter block start with
                // our write here, we can safely assume that filter is applicable
                // to next block on no further wrap arounds is possible.
                if (((flt.BlockStart - WrPtr) & MaxWinMask) <= FullWriteSize)
                {
                    flt.NextWindow = false;
                }

                continue;
            }
            var BlockStart = flt.BlockStart;
            var BlockLength = flt.BlockLength;
            if (((BlockStart - WrittenBorder) & MaxWinMask) < WriteSizeLeft)
            {
                if (WrittenBorder != BlockStart)
                {
                    UnpWriteArea(WrittenBorder, BlockStart);
                    WrittenBorder = BlockStart;
                    WriteSizeLeft = (UnpPtr - WrittenBorder) & MaxWinMask;
                }
                if (BlockLength <= WriteSizeLeft)
                {
                    if (BlockLength > 0) // We set it to 0 also for invalid filters.
                    {
                        var BlockEnd = (BlockStart + BlockLength) & MaxWinMask;

                        //x FilterSrcMemory.Alloc(BlockLength);
                        FilterSrcMemory = EnsureCapacity(
                            FilterSrcMemory,
                            checked((int)BlockLength)
                        );
                        var Mem = FilterSrcMemory;
                        if (BlockStart < BlockEnd || BlockEnd == 0)
                        {
                            if (Fragmented)
                            {
                                FragWindow.CopyData(Mem, 0, BlockStart, BlockLength);
                            }
                            else
                            //x memcpy(Mem,Window+BlockStart,BlockLength);
                            {
                                Buffer.BlockCopy(Window, (int)BlockStart, Mem, 0, (int)BlockLength);
                            }
                        }
                        else
                        {
                            var FirstPartLength = MaxWinSize - BlockStart;
                            if (Fragmented)
                            {
                                FragWindow.CopyData(Mem, 0, BlockStart, FirstPartLength);
                                FragWindow.CopyData(Mem, FirstPartLength, 0, BlockEnd);
                            }
                            else
                            {
                                //x memcpy(Mem,Window+BlockStart,FirstPartLength);
                                Buffer.BlockCopy(
                                    Window,
                                    (int)BlockStart,
                                    Mem,
                                    0,
                                    (int)FirstPartLength
                                );
                                //x memcpy(Mem+FirstPartLength,Window,BlockEnd);
                                Buffer.BlockCopy(
                                    Window,
                                    0,
                                    Mem,
                                    (int)FirstPartLength,
                                    (int)BlockEnd
                                );
                            }
                        }

                        var OutMem = ApplyFilter(Mem, BlockLength, flt);

                        Filters[I].Type = FILTER_NONE;

                        if (OutMem != null)
                        {
                            UnpIO_UnpWrite(OutMem, 0, BlockLength);
                        }

                        UnpSomeRead = true;
                        WrittenFileSize += BlockLength;
                        WrittenBorder = BlockEnd;
                        WriteSizeLeft = (UnpPtr - WrittenBorder) & MaxWinMask;
                    }
                }
                else
                {
                    // Current filter intersects the window write border, so we adjust
                    // the window border to process this filter next time, not now.
                    WrPtr = WrittenBorder;

                    // Since Filter start position can only increase, we quit processing
                    // all following filters for this data block and reset 'NextWindow'
                    // flag for them.
                    //for (size_t J=I;J<Filters.Count;J++)
                    // sharpcompress: size_t -> int
                    for (var J = I; J < Filters.Count; J++)
                    {
                        var _flt = Filters[J];
                        if (_flt.Type != FILTER_NONE)
                        {
                            _flt.NextWindow = false;
                        }
                    }

                    // Do not write data left after current filter now.
                    NotAllFiltersProcessed = true;
                    break;
                }
            }
        }

        // Remove processed filters from queue.
        // sharpcompress: size_t -> int
        var EmptyCount = 0;
        // sharpcompress: size_t -> int
        for (var I = 0; I < Filters.Count; I++)
        {
            if (EmptyCount > 0)
            {
                Filters[I - EmptyCount] = Filters[I];
            }

            if (Filters[I].Type == FILTER_NONE)
            {
                EmptyCount++;
            }
        }
        if (EmptyCount > 0)
        //Filters.Alloc(Filters.Count-EmptyCount);
        {
            Filters.RemoveRange(Filters.Count - EmptyCount, EmptyCount);
        }

        if (!NotAllFiltersProcessed) // Only if all filters are processed.
        {
            // Write data left after last filter.
            UnpWriteArea(WrittenBorder, UnpPtr);
            WrPtr = UnpPtr;
        }

        // We prefer to write data in blocks not exceeding UNPACK_MAX_WRITE
        // instead of potentially huge MaxWinSize blocks. It also allows us
        // to keep the size of Filters array reasonable.
        WriteBorder = (UnpPtr + Math.Min(MaxWinSize, UNPACK_MAX_WRITE)) & MaxWinMask;

        // Choose the nearest among WriteBorder and WrPtr actual written border.
        // If border is equal to UnpPtr, it means that we have MaxWinSize data ahead.
        if (
            WriteBorder == UnpPtr
            || WrPtr != UnpPtr
                && ((WrPtr - UnpPtr) & MaxWinMask) < ((WriteBorder - UnpPtr) & MaxWinMask)
        )
        {
            WriteBorder = WrPtr;
        }
    }

    private async System.Threading.Tasks.Task UnpWriteBufAsync(
        System.Threading.CancellationToken cancellationToken = default
    )
    {
        var WrittenBorder = WrPtr;
        var FullWriteSize = (UnpPtr - WrittenBorder) & MaxWinMask;
        var WriteSizeLeft = FullWriteSize;
        var NotAllFiltersProcessed = false;

        for (var I = 0; I < Filters.Count; I++)
        {
            var flt = Filters[I];
            if (flt.Type == FILTER_NONE)
            {
                continue;
            }

            if (flt.NextWindow)
            {
                if (((flt.BlockStart - WrPtr) & MaxWinMask) <= FullWriteSize)
                {
                    flt.NextWindow = false;
                }
                continue;
            }

            var BlockStart = flt.BlockStart;
            var BlockLength = flt.BlockLength;
            if (((BlockStart - WrittenBorder) & MaxWinMask) < WriteSizeLeft)
            {
                if (WrittenBorder != BlockStart)
                {
                    await UnpWriteAreaAsync(WrittenBorder, BlockStart, cancellationToken)
                        .ConfigureAwait(false);
                    WrittenBorder = BlockStart;
                    WriteSizeLeft = (UnpPtr - WrittenBorder) & MaxWinMask;
                }
                if (BlockLength <= WriteSizeLeft)
                {
                    if (BlockLength > 0)
                    {
                        var BlockEnd = (BlockStart + BlockLength) & MaxWinMask;

                        FilterSrcMemory = EnsureCapacity(
                            FilterSrcMemory,
                            checked((int)BlockLength)
                        );
                        var Mem = FilterSrcMemory;
                        if (BlockStart < BlockEnd || BlockEnd == 0)
                        {
                            if (Fragmented)
                            {
                                FragWindow.CopyData(Mem, 0, BlockStart, BlockLength);
                            }
                            else
                            {
                                Buffer.BlockCopy(Window, (int)BlockStart, Mem, 0, (int)BlockLength);
                            }
                        }
                        else
                        {
                            var FirstPartLength = MaxWinSize - BlockStart;
                            if (Fragmented)
                            {
                                FragWindow.CopyData(Mem, 0, BlockStart, FirstPartLength);
                                FragWindow.CopyData(Mem, FirstPartLength, 0, BlockEnd);
                            }
                            else
                            {
                                Buffer.BlockCopy(
                                    Window,
                                    (int)BlockStart,
                                    Mem,
                                    0,
                                    (int)FirstPartLength
                                );
                                Buffer.BlockCopy(
                                    Window,
                                    0,
                                    Mem,
                                    (int)FirstPartLength,
                                    (int)BlockEnd
                                );
                            }
                        }

                        var OutMem = ApplyFilter(Mem, BlockLength, flt);

                        Filters[I].Type = FILTER_NONE;

                        if (OutMem != null)
                        {
                            await UnpIO_UnpWriteAsync(OutMem, 0, BlockLength, cancellationToken)
                                .ConfigureAwait(false);
                            WrittenFileSize += BlockLength;
                        }

                        WrittenBorder = BlockEnd;
                        WriteSizeLeft = (UnpPtr - WrittenBorder) & MaxWinMask;
                    }
                }
                else
                {
                    NotAllFiltersProcessed = true;
                    for (var J = I; J < Filters.Count; J++)
                    {
                        var fltj = Filters[J];
                        if (
                            fltj.Type != FILTER_NONE
                            && fltj.NextWindow == false
                            && ((fltj.BlockStart - WrPtr) & MaxWinMask) < FullWriteSize
                        )
                        {
                            fltj.NextWindow = true;
                        }
                    }
                    break;
                }
            }
        }

        var EmptyCount = 0;
        for (var I = 0; I < Filters.Count; I++)
        {
            if (EmptyCount > 0)
            {
                Filters[I - EmptyCount] = Filters[I];
            }

            if (Filters[I].Type == FILTER_NONE)
            {
                EmptyCount++;
            }
        }
        if (EmptyCount > 0)
        {
            Filters.RemoveRange(Filters.Count - EmptyCount, EmptyCount);
        }

        if (!NotAllFiltersProcessed)
        {
            await UnpWriteAreaAsync(WrittenBorder, UnpPtr, cancellationToken).ConfigureAwait(false);
            WrPtr = UnpPtr;
        }

        WriteBorder = (UnpPtr + Math.Min(MaxWinSize, UNPACK_MAX_WRITE)) & MaxWinMask;

        if (
            WriteBorder == UnpPtr
            || WrPtr != UnpPtr
                && ((WrPtr - UnpPtr) & MaxWinMask) < ((WriteBorder - UnpPtr) & MaxWinMask)
        )
        {
            WriteBorder = WrPtr;
        }
    }

    private byte[] ApplyFilter(byte[] __d, uint DataSize, UnpackFilter Flt)
    {
        var Data = 0;
        var SrcData = __d;
        switch (Flt.Type)
        {
            case FILTER_E8:
            case FILTER_E8E9:
                {
                    var FileOffset = (uint)WrittenFileSize;

                    const uint FileSize = 0x1000000;
                    var CmpByte2 = Flt.Type == FILTER_E8E9 ? (byte)0xe9 : (byte)0xe8;
                    // DataSize is unsigned, so we use "CurPos+4" and not "DataSize-4"
                    // to avoid overflow for DataSize<4.
                    for (uint CurPos = 0; CurPos + 4 < DataSize; )
                    {
                        //x byte CurByte=*(Data++);
                        var CurByte = __d[Data++];
                        CurPos++;
                        if (CurByte == 0xe8 || CurByte == CmpByte2)
                        {
                            var Offset = (CurPos + FileOffset) % FileSize;
                            var Addr = RawGet4(__d, Data);

                            // We check 0x80000000 bit instead of '< 0' comparison
                            // not assuming int32 presence or uint size and endianness.
                            if ((Addr & 0x80000000) != 0) // Addr<0
                            {
                                if (((Addr + Offset) & 0x80000000) == 0) // Addr+Offset>=0
                                {
                                    RawPut4(Addr + FileSize, __d, Data);
                                }
                            }
                            else if (((Addr - FileSize) & 0x80000000) != 0) // Addr<FileSize
                            {
                                RawPut4(Addr - Offset, __d, Data);
                            }

                            Data += 4;
                            CurPos += 4;
                        }
                    }
                }
                return SrcData;
            case FILTER_ARM:
                {
                    var FileOffset = (uint)WrittenFileSize;
                    // DataSize is unsigned, so we use "CurPos+3" and not "DataSize-3"
                    // to avoid overflow for DataSize<3.
                    for (uint CurPos = 0; CurPos + 3 < DataSize; CurPos += 4)
                    {
                        var D = Data + CurPos;
                        if (__d[D + 3] == 0xeb) // BL command with '1110' (Always) condition.
                        {
                            var Offset =
                                __d[D]
                                + ((uint)(__d[D + 1]) * 0x100)
                                + ((uint)(__d[D + 2]) * 0x10000);
                            Offset -= (FileOffset + CurPos) / 4;
                            __d[D] = (byte)Offset;
                            __d[D + 1] = (byte)(Offset >> 8);
                            __d[D + 2] = (byte)(Offset >> 16);
                        }
                    }
                }
                return SrcData;
            case FILTER_DELTA:
            {
                // Unlike RAR3, we do not need to reject excessive channel
                // values here, since RAR5 uses only 5 bits to store channel.
                uint Channels = Flt.Channels,
                    SrcPos = 0;

                //x FilterDstMemory.Alloc(DataSize);
                FilterDstMemory = EnsureCapacity(FilterDstMemory, checked((int)DataSize));

                var DstData = FilterDstMemory;

                // Bytes from same channels are grouped to continual data blocks,
                // so we need to place them back to their interleaving positions.
                for (uint CurChannel = 0; CurChannel < Channels; CurChannel++)
                {
                    byte PrevByte = 0;
                    for (var DestPos = CurChannel; DestPos < DataSize; DestPos += Channels)
                    {
                        DstData[DestPos] = (PrevByte -= __d[Data + SrcPos++]);
                    }
                }
                return DstData;
            }
        }
        return null;
    }

    private void UnpWriteArea(size_t StartPtr, size_t EndPtr)
    {
        if (EndPtr != StartPtr)
        {
            UnpSomeRead = true;
        }

        if (EndPtr < StartPtr)
        {
            UnpAllBuf = true;
        }

        if (Fragmented)
        {
            var SizeToWrite = (EndPtr - StartPtr) & MaxWinMask;
            while (SizeToWrite > 0)
            {
                var BlockSize = FragWindow.GetBlockSize(StartPtr, SizeToWrite);
                //UnpWriteData(&FragWindow[StartPtr],BlockSize);
                FragWindow.GetBuffer(StartPtr, out var __buffer, out var __offset);
                UnpWriteData(__buffer, __offset, BlockSize);
                SizeToWrite -= BlockSize;
                StartPtr = (StartPtr + BlockSize) & MaxWinMask;
            }
        }
        else if (EndPtr < StartPtr)
        {
            UnpWriteData(Window, StartPtr, MaxWinSize - StartPtr);
            UnpWriteData(Window, 0, EndPtr);
        }
        else
        {
            UnpWriteData(Window, StartPtr, EndPtr - StartPtr);
        }
    }

    private async System.Threading.Tasks.Task UnpWriteAreaAsync(
        size_t StartPtr,
        size_t EndPtr,
        System.Threading.CancellationToken cancellationToken = default
    )
    {
        if (EndPtr != StartPtr)
        {
            UnpSomeRead = true;
        }

        if (EndPtr < StartPtr)
        {
            UnpAllBuf = true;
        }

        if (Fragmented)
        {
            var SizeToWrite = (EndPtr - StartPtr) & MaxWinMask;
            while (SizeToWrite > 0)
            {
                var BlockSize = FragWindow.GetBlockSize(StartPtr, SizeToWrite);
                FragWindow.GetBuffer(StartPtr, out var __buffer, out var __offset);
                await UnpWriteDataAsync(__buffer, __offset, BlockSize, cancellationToken)
                    .ConfigureAwait(false);
                SizeToWrite -= BlockSize;
                StartPtr = (StartPtr + BlockSize) & MaxWinMask;
            }
        }
        else if (EndPtr < StartPtr)
        {
            await UnpWriteDataAsync(Window, StartPtr, MaxWinSize - StartPtr, cancellationToken)
                .ConfigureAwait(false);
            await UnpWriteDataAsync(Window, 0, EndPtr, cancellationToken).ConfigureAwait(false);
        }
        else
        {
            await UnpWriteDataAsync(Window, StartPtr, EndPtr - StartPtr, cancellationToken)
                .ConfigureAwait(false);
        }
    }

    private void UnpWriteData(byte[] Data, size_t offset, size_t Size)
    {
        if (WrittenFileSize >= DestUnpSize)
        {
            return;
        }

        var WriteSize = Size;
        var LeftToWrite = DestUnpSize - WrittenFileSize;
        if (WriteSize > LeftToWrite)
        {
            WriteSize = (size_t)LeftToWrite;
        }

        UnpIO_UnpWrite(Data, offset, WriteSize);
        WrittenFileSize += Size;
    }

    private async System.Threading.Tasks.Task UnpWriteDataAsync(
        byte[] Data,
        size_t offset,
        size_t Size,
        System.Threading.CancellationToken cancellationToken = default
    )
    {
        if (WrittenFileSize >= DestUnpSize)
        {
            return;
        }

        var WriteSize = Size;
        var LeftToWrite = DestUnpSize - WrittenFileSize;
        if (WriteSize > LeftToWrite)
        {
            WriteSize = (size_t)LeftToWrite;
        }

        await UnpIO_UnpWriteAsync(Data, offset, WriteSize, cancellationToken).ConfigureAwait(false);
        WrittenFileSize += Size;
    }

    private void UnpInitData50(bool Solid)
    {
        if (!Solid)
        {
            TablesRead5 = false;
        }
    }

    private bool ReadBlockHeader(BitInput Inp, ref UnpackBlockHeader Header)
    {
        Header.HeaderSize = 0;

        if (!Inp.ExternalBuffer && Inp.InAddr > ReadTop - 7)
        {
            if (!UnpReadBuf())
            {
                return false;
            }
        }

        Inp.faddbits((uint)((8 - Inp.InBit) & 7));

        var BlockFlags = (byte)(Inp.fgetbits() >> 8);
        Inp.faddbits(8);
        var ByteCount = (uint)(((BlockFlags >> 3) & 3) + 1); // Block size byte count.

        if (ByteCount == 4)
        {
            return false;
        }

        Header.HeaderSize = (int)(2 + ByteCount);

        Header.BlockBitSize = (BlockFlags & 7) + 1;

        var SavedCheckSum = (byte)(Inp.fgetbits() >> 8);
        Inp.faddbits(8);

        var BlockSize = 0;
        for (uint I = 0; I < ByteCount; I++)
        {
            BlockSize += (int)((Inp.fgetbits() >> 8) << (int)(I * 8));
            Inp.addbits(8);
        }

        Header.BlockSize = BlockSize;
        var CheckSum = (byte)(0x5a ^ BlockFlags ^ BlockSize ^ (BlockSize >> 8) ^ (BlockSize >> 16));
        if (CheckSum != SavedCheckSum)
        {
            return false;
        }

        Header.BlockStart = Inp.InAddr;
        ReadBorder = Math.Min(ReadBorder, Header.BlockStart + Header.BlockSize - 1);

        Header.LastBlockInFile = (BlockFlags & 0x40) != 0;
        Header.TablePresent = (BlockFlags & 0x80) != 0;
        return true;
    }

    private bool ReadTables(
        BitInput Inp,
        ref UnpackBlockHeader Header,
        ref UnpackBlockTables Tables
    )
    {
        if (!Header.TablePresent)
        {
            return true;
        }

        if (!Inp.ExternalBuffer && Inp.InAddr > ReadTop - 25)
        {
            if (!UnpReadBuf())
            {
                return false;
            }
        }

        Span<byte> BitLength = stackalloc byte[checked((int)BC)];
        for (int I = 0; I < BC; I++)
        {
            uint Length = (byte)(Inp.fgetbits() >> 12);
            Inp.faddbits(4);
            if (Length == 15)
            {
                uint ZeroCount = (byte)(Inp.fgetbits() >> 12);
                Inp.faddbits(4);
                if (ZeroCount == 0)
                {
                    BitLength[I] = 15;
                }
                else
                {
                    ZeroCount += 2;
                    while (ZeroCount-- > 0 && I < BitLength.Length)
                    {
                        BitLength[I++] = 0;
                    }

                    I--;
                }
            }
            else
            {
                BitLength[I] = (byte)Length;
            }
        }

        MakeDecodeTables(BitLength, 0, Tables.BD, BC);

        Span<byte> Table = stackalloc byte[checked((int)HUFF_TABLE_SIZE)];
        const int TableSize = checked((int)HUFF_TABLE_SIZE);
        for (int I = 0; I < TableSize; )
        {
            if (!Inp.ExternalBuffer && Inp.InAddr > ReadTop - 5)
            {
                if (!UnpReadBuf())
                {
                    return false;
                }
            }

            var Number = DecodeNumber(Inp, Tables.BD);
            if (Number < 16)
            {
                Table[I] = (byte)Number;
                I++;
            }
            else if (Number < 18)
            {
                uint N;
                if (Number == 16)
                {
                    N = (Inp.fgetbits() >> 13) + 3;
                    Inp.faddbits(3);
                }
                else
                {
                    N = (Inp.fgetbits() >> 9) + 11;
                    Inp.faddbits(7);
                }
                if (I == 0)
                {
                    // We cannot have "repeat previous" code at the first position.
                    // Multiple such codes would shift Inp position without changing I,
                    // which can lead to reading beyond of Inp boundary in mutithreading
                    // mode, where Inp.ExternalBuffer disables bounds check and we just
                    // reserve a lot of buffer space to not need such check normally.
                    return false;
                }
                else
                {
                    while (N-- > 0 && I < TableSize)
                    {
                        Table[I] = Table[I - 1];
                        I++;
                    }
                }
            }
            else
            {
                uint N;
                if (Number == 18)
                {
                    N = (Inp.fgetbits() >> 13) + 3;
                    Inp.faddbits(3);
                }
                else
                {
                    N = (Inp.fgetbits() >> 9) + 11;
                    Inp.faddbits(7);
                }
                while (N-- > 0 && I < TableSize)
                {
                    Table[I++] = 0;
                }
            }
        }
        TablesRead5 = true;
        if (!Inp.ExternalBuffer && Inp.InAddr > ReadTop)
        {
            return false;
        }

        MakeDecodeTables(Table, 0, Tables.LD, NC);
        MakeDecodeTables(Table, (int)NC, Tables.DD, DC);
        MakeDecodeTables(Table, (int)(NC + DC), Tables.LDD, LDC);
        MakeDecodeTables(Table, (int)(NC + DC + LDC), Tables.RD, RC);
        return true;
    }

    private void InitFilters() =>
        //Filters.SoftReset();
        Filters.Clear();
}
