#nullable disable

using System;
using System.Buffers;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using static SharpCompress.Compressors.Rar.UnpackV2017.PackDef;
using static SharpCompress.Compressors.Rar.UnpackV2017.UnpackGlobal;
using size_t = System.UInt32;

namespace SharpCompress.Compressors.Rar.UnpackV2017;

internal sealed partial class Unpack : BitInput
{
    private bool disposed;

    public Unpack( /* ComprDataIO *DataIO */
    )
        //:Inp(true),VMCodeInp(true)
        : base(true)
    {
        _UnpackCtor();

        //UnpIO=DataIO;
        Window = null;
        Fragmented = false;
        Suspended = false;
        UnpAllBuf = false;
        UnpSomeRead = false;
        /*#if RarV2017_RAR_SMP
                MaxUserThreads = 1;
                UnpThreadPool = CreateThreadPool();
                ReadBufMT = null;
                UnpThreadData = null;
        #endif*/
        MaxWinSize = 0;
        MaxWinMask = 0;

        // Perform initialization, which should be done only once for all files.
        // It prevents crash if first DoUnpack call is later made with wrong
        // (true) 'Solid' value.
        UnpInitData(false);
        // RAR 1.5 decompression initialization
        UnpInitData15(false);
        InitHuff();
    }

    public override void Dispose()
    {
        if (disposed)
        {
            return;
        }

        base.Dispose();
        if (Window != null)
        {
            ArrayPool<byte>.Shared.Return(Window);
            Window = null;
        }

        FragWindow.Reset();
        if (FilterSrcMemory.Length != 0)
        {
            ArrayPool<byte>.Shared.Return(FilterSrcMemory);
            FilterSrcMemory = Array.Empty<byte>();
        }

        if (FilterDstMemory.Length != 0)
        {
            ArrayPool<byte>.Shared.Return(FilterDstMemory);
            FilterDstMemory = Array.Empty<byte>();
        }

        disposed = true;
    }

    private void Init(size_t WinSize, bool Solid)
    {
        // If 32-bit RAR unpacks an archive with 4 GB dictionary, the window size
        // will be 0 because of size_t overflow. Let's issue the memory error.
        if (WinSize == 0)
        //ErrHandler.MemoryError();
        {
            throw new InvalidFormatException(
                "invalid window size (possibly due to a rar file with a 4GB being unpacked on a 32-bit platform)"
            );
        }

        // Minimum window size must be at least twice more than maximum possible
        // size of filter block, which is 0x10000 in RAR now. If window size is
        // smaller, we can have a block with never cleared flt->NextWindow flag
        // in UnpWriteBuf(). Minimum window size 0x20000 would be enough, but let's
        // use 0x40000 for extra safety and possible filter area size expansion.
        const size_t MinAllocSize = 0x40000;
        if (WinSize < MinAllocSize)
        {
            WinSize = MinAllocSize;
        }

        if (WinSize <= MaxWinSize) // Use the already allocated window.
        {
            return;
        }

        if ((WinSize >> 16) > 0x10000) // Window size must not exceed 4 GB.
        {
            return;
        }

        // Archiving code guarantees that window size does not grow in the same
        // solid stream. So if we are here, we are either creating a new window
        // or increasing the size of non-solid window. So we could safely reject
        // current window data without copying them to a new window, though being
        // extra cautious, we still handle the solid window grow case below.
        var Grow = Solid && (Window != null || Fragmented);

        // We do not handle growth for existing fragmented window.
        if (Grow && Fragmented)
        //throw std::bad_alloc();
        {
            throw new InvalidFormatException("Grow && Fragmented");
        }

        byte[] NewWindow = null;
        try
        {
            NewWindow = Fragmented ? null : ArrayPool<byte>.Shared.Rent((int)WinSize);

            if (NewWindow == null)
            {
                if (Grow || WinSize < 0x1000000)
                {
                    // We do not support growth for new fragmented window.
                    // Also exclude RAR4 and small dictionaries.
                    //throw std::bad_alloc();
                    throw new InvalidFormatException("Grow || WinSize<0x1000000");
                }
                else
                {
                    if (Window != null) // If allocated by preceding files.
                    {
                        //free(Window);
                        ArrayPool<byte>.Shared.Return(Window);
                        Window = null;
                    }

                    FragWindow.Init(WinSize);
                    Fragmented = true;
                }
            }

            if (!Fragmented)
            {
                // Clean the window to generate the same output when unpacking corrupt
                // RAR files, which may access unused areas of sliding dictionary.
                // sharpcompress: don't need this, freshly allocated above
                //memset(NewWindow,0,WinSize);

                // If Window is not NULL, it means that window size has grown.
                // In solid streams we need to copy data to a new window in such case.
                // RAR archiving code does not allow it in solid streams now,
                // but let's implement it anyway just in case we'll change it sometimes.
                if (Grow)
                {
                    for (size_t I = 1; I <= MaxWinSize; I++)
                    {
                        NewWindow[(UnpPtr - I) & (WinSize - 1)] = Window[
                            (UnpPtr - I) & (MaxWinSize - 1)
                        ];
                    }
                }

                if (Window != null)
                {
                    ArrayPool<byte>.Shared.Return(Window);
                }

                Window = NewWindow;
                NewWindow = null;
            }
        }
        finally
        {
            if (NewWindow != null)
            {
                ArrayPool<byte>.Shared.Return(NewWindow);
            }
        }

        MaxWinSize = WinSize;
        MaxWinMask = MaxWinSize - 1;
    }

    private void DoUnpack(uint Method, bool Solid)
    {
        // Methods <50 will crash in Fragmented mode when accessing NULL Window.
        // They cannot be called in such mode now, but we check it below anyway
        // just for extra safety.
        switch (Method)
        {
            case 15: // rar 1.5 compression
                if (!Fragmented)
                {
                    Unpack15(Solid);
                }

                break;
            case 20: // rar 2.x compression
            case 26: // files larger than 2GB
                if (!Fragmented)
                {
                    Unpack20(Solid);
                }

                break;
            case 29: // rar 3.x compression
                if (!Fragmented)
                {
                    throw new NotImplementedException();
                }

                break;
            case 50: // RAR 5.0 compression algorithm.
                Unpack5(Solid);
                break;
#if !Rar2017_NOSTRICT
            default:
                throw new InvalidFormatException("unknown compression method " + Method);
#endif
        }
    }

    private async ValueTask DoUnpackAsync(
        uint Method,
        bool Solid,
        CancellationToken cancellationToken = default
    )
    {
        // Methods <50 will crash in Fragmented mode when accessing NULL Window.
        // They cannot be called in such mode now, but we check it below anyway
        // just for extra safety.
        switch (Method)
        {
#if !RarV2017_SFX_MODULE
            case 15: // rar 1.5 compression
                if (!Fragmented)
                {
                    await Unpack15Async(Solid, cancellationToken).ConfigureAwait(false);
                }

                break;
            case 20: // rar 2.x compression
            case 26: // files larger than 2GB
                if (!Fragmented)
                {
                    await Unpack20Async(Solid, cancellationToken).ConfigureAwait(false);
                }

                break;
#endif
#if !RarV2017_RAR5ONLY
            case 29: // rar 3.x compression
                if (!Fragmented)
                {
                    // TODO: Create Unpack29Async when ready
                    throw new NotImplementedException();
                }

                break;
#endif
            case 50: // RAR 5.0 compression algorithm.
                // RAR 5.0 has full async support via UnpReadBufAsync and UnpWriteBuf
                await Unpack5Async(Solid, cancellationToken).ConfigureAwait(false);
                break;
#if !Rar2017_NOSTRICT
            default:
                throw new InvalidFormatException("unknown compression method " + Method);
#endif
        }
    }

    private void UnpInitData(bool Solid)
    {
        if (!Solid)
        {
            new Span<uint>(OldDist).Clear();
            OldDistPtr = 0;
            LastDist = LastLength = 0;
            //    memset(Window,0,MaxWinSize);
            //memset(&BlockTables,0,sizeof(BlockTables));
            BlockTables = new UnpackBlockTables();
            // sharpcompress: no default ctor for struct
            BlockTables.Init();
            UnpPtr = WrPtr = 0;
            WriteBorder = Math.Min(MaxWinSize, UNPACK_MAX_WRITE) & MaxWinMask;
        }

        // Filters never share several solid files, so we can safely reset them
        // even in solid archive.
        InitFilters();

        Inp.InitBitInput();
        WrittenFileSize = 0;
        ReadTop = 0;
        ReadBorder = 0;

        //memset(&BlockHeader,0,sizeof(BlockHeader));
        BlockHeader = new UnpackBlockHeader();
        BlockHeader.BlockSize = -1; // '-1' means not defined yet.
#if !RarV2017_SFX_MODULE
        UnpInitData20(Solid);
#endif
        //UnpInitData30(Solid);
        UnpInitData50(Solid);
    }

    // LengthTable contains the length in bits for every element of alphabet.
    // Dec is the structure to decode Huffman code/
    // Size is size of length table and DecodeNum field in Dec structure,
    private void MakeDecodeTables(Span<byte> LengthTable, int offset, DecodeTable Dec, uint Size)
    {
        // Size of alphabet and DecodePos array.
        Dec.MaxNum = Size;

        // Calculate how many entries for every bit length in LengthTable we have.
        Span<uint> LengthCount = stackalloc uint[16];
        //memset(LengthCount,0,sizeof(LengthCount));
        for (size_t I = 0; I < Size; I++)
        {
            LengthCount[LengthTable[checked((int)(offset + I))] & 0xf]++;
        }

        // We must not calculate the number of zero length codes.
        LengthCount[0] = 0;

        // Set the entire DecodeNum to zero.
        //memset(Dec->DecodeNum,0,Size*sizeof(*Dec->DecodeNum));
        new Span<ushort>(Dec.DecodeNum).Clear();

        // Initialize not really used entry for zero length code.
        Dec.DecodePos[0] = 0;

        // Start code for bit length 1 is 0.
        Dec.DecodeLen[0] = 0;

        // Right aligned upper limit code for current bit length.
        uint UpperLimit = 0;

        for (var I = 1; I < 16; I++)
        {
            // Adjust the upper limit code.
            UpperLimit += LengthCount[I];

            // Left aligned upper limit code.
            var LeftAligned = UpperLimit << (16 - I);

            // Prepare the upper limit code for next bit length.
            UpperLimit *= 2;

            // Store the left aligned upper limit code.
            Dec.DecodeLen[I] = LeftAligned;

            // Every item of this array contains the sum of all preceding items.
            // So it contains the start position in code list for every bit length.
            Dec.DecodePos[I] = Dec.DecodePos[I - 1] + LengthCount[I - 1];
        }

        // Prepare the copy of DecodePos. We'll modify this copy below,
        // so we cannot use the original DecodePos.
        Span<uint> CopyDecodePos = stackalloc uint[16];
        //memcpy(CopyDecodePos,Dec->DecodePos,sizeof(CopyDecodePos));
        Dec.DecodePos.AsSpan().CopyTo(CopyDecodePos);

        // For every bit length in the bit length table and so for every item
        // of alphabet.
        for (uint I = 0; I < Size; I++)
        {
            // Get the current bit length.
            var _CurBitLength = (byte)(LengthTable[checked((int)(offset + I))] & 0xf);

            if (_CurBitLength != 0)
            {
                // Last position in code list for current bit length.
                var LastPos = CopyDecodePos[_CurBitLength];

                // Prepare the decode table, so this position in code list will be
                // decoded to current alphabet item number.
                Dec.DecodeNum[LastPos] = (ushort)I;

                // We'll use next position number for this bit length next time.
                // So we pass through the entire range of positions available
                // for every bit length.
                CopyDecodePos[_CurBitLength]++;
            }
        }

        // Define the number of bits to process in quick mode. We use more bits
        // for larger alphabets. More bits means that more codes will be processed
        // in quick mode, but also that more time will be spent to preparation
        // of tables for quick decode.
        switch (Size)
        {
            case NC:
            case NC20:
            case NC30:
                Dec.QuickBits = MAX_QUICK_DECODE_BITS;
                break;
            default:
                Dec.QuickBits = MAX_QUICK_DECODE_BITS - 3;
                break;
        }

        // Size of tables for quick mode.
        var QuickDataSize = 1U << (int)Dec.QuickBits;

        // Bit length for current code, start from 1 bit codes. It is important
        // to use 1 bit instead of 0 for minimum code length, so we are moving
        // forward even when processing a corrupt archive.
        //uint CurBitLength=1;
        byte CurBitLength = 1;

        // For every right aligned bit string which supports the quick decoding.
        for (uint Code = 0; Code < QuickDataSize; Code++)
        {
            // Left align the current code, so it will be in usual bit field format.
            var BitField = Code << (int)(16 - Dec.QuickBits);

            // Prepare the table for quick decoding of bit lengths.

            // Find the upper limit for current bit field and adjust the bit length
            // accordingly if necessary.
            while (CurBitLength < Dec.DecodeLen.Length && BitField >= Dec.DecodeLen[CurBitLength])
            {
                CurBitLength++;
            }

            // Translation of right aligned bit string to bit length.
            Dec.QuickLen[Code] = CurBitLength;

            // Prepare the table for quick translation of position in code list
            // to position in alphabet.

            // Calculate the distance from the start code for current bit length.
            var Dist = BitField - Dec.DecodeLen[CurBitLength - 1];

            // Right align the distance.
            Dist >>= (16 - CurBitLength);

            // Now we can calculate the position in the code list. It is the sum
            // of first position for current bit length and right aligned distance
            // between our bit field and start code for current bit length.
            uint Pos;
            if (
                CurBitLength < Dec.DecodePos.Length
                && (Pos = Dec.DecodePos[CurBitLength] + Dist) < Size
            )
            {
                // Define the code to alphabet number translation.
                Dec.QuickNum[Code] = Dec.DecodeNum[Pos];
            }
            else
            {
                // Can be here for length table filled with zeroes only (empty).
                Dec.QuickNum[Code] = 0;
            }
        }
    }
}
