/*
* Copyright (c) 2007 innoSysTec (R) GmbH, Germany. All rights reserved.
* Original author: Edmund Wagner
* Creation date: 21.06.2007
*
* the unrar licence applies to all junrar source and binary distributions
* you are not allowed to use this source to re-create the RAR compression algorithm
*/

using System;
using SharpCompress.Compressors.Rar.UnpackV1.Decode;

namespace SharpCompress.Compressors.Rar.UnpackV1;

internal partial class Unpack
{
    private readonly MultDecode[] MD = new[]
    {
        new MultDecode(),
        new MultDecode(),
        new MultDecode(),
        new MultDecode(),
    };

    private readonly byte[] UnpOldTable20 = new byte[PackDef.MC20 * 4];

    private int UnpAudioBlock,
        UnpChannels,
        UnpCurChannel,
        UnpChannelDelta;

    private readonly AudioVariables[] AudV = new AudioVariables[4];

    private readonly LitDecode LD = new();

    private readonly DistDecode DD = new();

    private readonly LowDistDecode LDD = new();

    private readonly RepDecode RD = new();

    private readonly BitDecode BD = new();

    private static readonly int[] LDecode =
    {
        0,
        1,
        2,
        3,
        4,
        5,
        6,
        7,
        8,
        10,
        12,
        14,
        16,
        20,
        24,
        28,
        32,
        40,
        48,
        56,
        64,
        80,
        96,
        112,
        128,
        160,
        192,
        224,
    };

    private static ReadOnlySpan<byte> LBits =>
        new byte[]
        {
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            1,
            1,
            1,
            1,
            2,
            2,
            2,
            2,
            3,
            3,
            3,
            3,
            4,
            4,
            4,
            4,
            5,
            5,
            5,
            5,
        };

    private static readonly int[] DDecode =
    {
        0,
        1,
        2,
        3,
        4,
        6,
        8,
        12,
        16,
        24,
        32,
        48,
        64,
        96,
        128,
        192,
        256,
        384,
        512,
        768,
        1024,
        1536,
        2048,
        3072,
        4096,
        6144,
        8192,
        12288,
        16384,
        24576,
        32768,
        49152,
        65536,
        98304,
        131072,
        196608,
        262144,
        327680,
        393216,
        458752,
        524288,
        589824,
        655360,
        720896,
        786432,
        851968,
        917504,
        983040,
    };

    private static readonly int[] DBits =
    {
        0,
        0,
        0,
        0,
        1,
        1,
        2,
        2,
        3,
        3,
        4,
        4,
        5,
        5,
        6,
        6,
        7,
        7,
        8,
        8,
        9,
        9,
        10,
        10,
        11,
        11,
        12,
        12,
        13,
        13,
        14,
        14,
        15,
        15,
        16,
        16,
        16,
        16,
        16,
        16,
        16,
        16,
        16,
        16,
        16,
        16,
        16,
        16,
    };

    private static readonly int[] SDDecode = { 0, 4, 8, 16, 32, 64, 128, 192 };

    private static readonly int[] SDBits = { 2, 2, 3, 4, 5, 6, 6, 6 };

    private void unpack20(bool solid)
    {
        int Bits;

        if (suspended)
        {
            unpPtr = wrPtr;
        }
        else
        {
            UnpInitData(solid);
            if (!unpReadBuf())
            {
                return;
            }
            if (!solid)
            {
                if (!ReadTables20())
                {
                    return;
                }
            }
            --destUnpSize;
        }

        while (destUnpSize >= 0)
        {
            unpPtr &= PackDef.MAXWINMASK;

            if (inAddr > readTop - 30)
            {
                if (!unpReadBuf())
                {
                    break;
                }
            }
            if (((wrPtr - unpPtr) & PackDef.MAXWINMASK) < 270 && wrPtr != unpPtr)
            {
                oldUnpWriteBuf();
                if (suspended)
                {
                    return;
                }
            }
            if (UnpAudioBlock != 0)
            {
                var AudioNumber = this.decodeNumber(MD[UnpCurChannel]);

                if (AudioNumber == 256)
                {
                    if (!ReadTables20())
                    {
                        break;
                    }
                    continue;
                }
                window[unpPtr++] = DecodeAudio(AudioNumber);
                if (++UnpCurChannel == UnpChannels)
                {
                    UnpCurChannel = 0;
                }
                --destUnpSize;
                continue;
            }

            var Number = this.decodeNumber(LD);
            if (Number < 256)
            {
                window[unpPtr++] = (byte)Number;
                --destUnpSize;
                continue;
            }
            if (Number > 269)
            {
                var Length = LDecode[Number -= 270] + 3;
                if ((Bits = LBits[Number]) > 0)
                {
                    Length += Utility.URShift(GetBits(), (16 - Bits));
                    AddBits(Bits);
                }

                var DistNumber = this.decodeNumber(DD);
                var Distance = DDecode[DistNumber] + 1;
                if ((Bits = DBits[DistNumber]) > 0)
                {
                    Distance += Utility.URShift(GetBits(), (16 - Bits));
                    AddBits(Bits);
                }

                if (Distance >= 0x2000)
                {
                    Length++;
                    if (Distance >= 0x40000L)
                    {
                        Length++;
                    }
                }

                CopyString20(Length, Distance);
                continue;
            }
            if (Number == 269)
            {
                if (!ReadTables20())
                {
                    break;
                }
                continue;
            }
            if (Number == 256)
            {
                CopyString20(lastLength, lastDist);
                continue;
            }
            if (Number < 261)
            {
                var Distance = oldDist[(oldDistPtr - (Number - 256)) & 3];
                var LengthNumber = this.decodeNumber(RD);
                var Length = LDecode[LengthNumber] + 2;
                if ((Bits = LBits[LengthNumber]) > 0)
                {
                    Length += Utility.URShift(GetBits(), (16 - Bits));
                    AddBits(Bits);
                }
                if (Distance >= 0x101)
                {
                    Length++;
                    if (Distance >= 0x2000)
                    {
                        Length++;
                        if (Distance >= 0x40000)
                        {
                            Length++;
                        }
                    }
                }
                CopyString20(Length, Distance);
                continue;
            }
            if (Number < 270)
            {
                var Distance = SDDecode[Number -= 261] + 1;
                if ((Bits = SDBits[Number]) > 0)
                {
                    Distance += Utility.URShift(GetBits(), (16 - Bits));
                    AddBits(Bits);
                }
                CopyString20(2, Distance);
            }
        }
        ReadLastTables();
        oldUnpWriteBuf();
    }

    private async System.Threading.Tasks.Task unpack20Async(
        bool solid,
        System.Threading.CancellationToken cancellationToken = default
    )
    {
        int Bits;

        if (suspended)
        {
            unpPtr = wrPtr;
        }
        else
        {
            UnpInitData(solid);
            if (!await unpReadBufAsync(cancellationToken).ConfigureAwait(false))
            {
                return;
            }
            if (!solid)
            {
                if (!await ReadTables20Async(cancellationToken).ConfigureAwait(false))
                {
                    return;
                }
            }
            --destUnpSize;
        }

        while (destUnpSize >= 0)
        {
            unpPtr &= PackDef.MAXWINMASK;

            if (inAddr > readTop - 30)
            {
                if (!await unpReadBufAsync(cancellationToken).ConfigureAwait(false))
                {
                    break;
                }
            }
            if (((wrPtr - unpPtr) & PackDef.MAXWINMASK) < 270 && wrPtr != unpPtr)
            {
                await oldUnpWriteBufAsync(cancellationToken).ConfigureAwait(false);
                if (suspended)
                {
                    return;
                }
            }
            if (UnpAudioBlock != 0)
            {
                var AudioNumber = this.decodeNumber(MD[UnpCurChannel]);

                if (AudioNumber == 256)
                {
                    if (!await ReadTables20Async(cancellationToken).ConfigureAwait(false))
                    {
                        break;
                    }
                    continue;
                }
                window[unpPtr++] = DecodeAudio(AudioNumber);
                if (++UnpCurChannel == UnpChannels)
                {
                    UnpCurChannel = 0;
                }
                --destUnpSize;
                continue;
            }

            var Number = this.decodeNumber(LD);
            if (Number < 256)
            {
                window[unpPtr++] = (byte)Number;
                --destUnpSize;
                continue;
            }
            if (Number > 269)
            {
                var Length = LDecode[Number -= 270] + 3;
                if ((Bits = LBits[Number]) > 0)
                {
                    Length += Utility.URShift(GetBits(), (16 - Bits));
                    AddBits(Bits);
                }

                var DistNumber = this.decodeNumber(DD);
                var Distance = DDecode[DistNumber] + 1;
                if ((Bits = DBits[DistNumber]) > 0)
                {
                    Distance += Utility.URShift(GetBits(), (16 - Bits));
                    AddBits(Bits);
                }

                if (Distance >= 0x2000)
                {
                    Length++;
                    if (Distance >= 0x40000L)
                    {
                        Length++;
                    }
                }

                CopyString20(Length, Distance);
                continue;
            }
            if (Number == 269)
            {
                if (!await ReadTables20Async(cancellationToken).ConfigureAwait(false))
                {
                    break;
                }
                continue;
            }
            if (Number == 256)
            {
                CopyString20(lastLength, lastDist);
                continue;
            }
            if (Number < 261)
            {
                var Distance = oldDist[(oldDistPtr - (Number - 256)) & 3];
                var LengthNumber = this.decodeNumber(RD);
                var Length = LDecode[LengthNumber] + 2;
                if ((Bits = LBits[LengthNumber]) > 0)
                {
                    Length += Utility.URShift(GetBits(), (16 - Bits));
                    AddBits(Bits);
                }
                if (Distance >= 0x101)
                {
                    Length++;
                    if (Distance >= 0x2000)
                    {
                        Length++;
                        if (Distance >= 0x40000)
                        {
                            Length++;
                        }
                    }
                }
                CopyString20(Length, Distance);
                continue;
            }
            if (Number < 270)
            {
                var Distance = SDDecode[Number -= 261] + 1;
                if ((Bits = SDBits[Number]) > 0)
                {
                    Distance += Utility.URShift(GetBits(), (16 - Bits));
                    AddBits(Bits);
                }
                CopyString20(2, Distance);
            }
        }
        ReadLastTables();
        await oldUnpWriteBufAsync(cancellationToken).ConfigureAwait(false);
    }

    private void CopyString20(int Length, int Distance)
    {
        lastDist = oldDist[oldDistPtr++ & 3] = Distance;
        lastLength = Length;
        destUnpSize -= Length;

        var DestPtr = unpPtr - Distance;
        if (DestPtr >= 0 && DestPtr < PackDef.MAXWINSIZE - 300 && unpPtr < PackDef.MAXWINSIZE - 300)
        {
            window[unpPtr++] = window[DestPtr++];
            window[unpPtr++] = window[DestPtr++];
            while (Length > 2)
            {
                Length--;
                window[unpPtr++] = window[DestPtr++];
            }
        }
        else
        {
            while ((Length--) != 0)
            {
                window[unpPtr] = window[DestPtr++ & PackDef.MAXWINMASK];
                unpPtr = (unpPtr + 1) & PackDef.MAXWINMASK;
            }
        }
    }

    private bool ReadTables20()
    {
        Span<byte> BitLength = stackalloc byte[PackDef.BC20];
        Span<byte> Table = stackalloc byte[PackDef.MC20 * 4];
        int TableSize,
            N,
            I;
        if (inAddr > readTop - 25)
        {
            if (!unpReadBuf())
            {
                return (false);
            }
        }
        var BitField = GetBits();
        UnpAudioBlock = (BitField & 0x8000);

        if (0 == (BitField & 0x4000))
        {
            // memset(UnpOldTable20,0,sizeof(UnpOldTable20));
            new Span<byte>(UnpOldTable20).Clear();
        }
        AddBits(2);

        if (UnpAudioBlock != 0)
        {
            UnpChannels = ((Utility.URShift(BitField, 12)) & 3) + 1;
            if (UnpCurChannel >= UnpChannels)
            {
                UnpCurChannel = 0;
            }
            AddBits(2);
            TableSize = PackDef.MC20 * UnpChannels;
        }
        else
        {
            TableSize = PackDef.NC20 + PackDef.DC20 + PackDef.RC20;
        }
        for (I = 0; I < PackDef.BC20; I++)
        {
            BitLength[I] = (byte)(Utility.URShift(GetBits(), 12));
            AddBits(4);
        }
        UnpackUtility.makeDecodeTables(BitLength, 0, BD, PackDef.BC20);
        I = 0;
        while (I < TableSize)
        {
            if (inAddr > readTop - 5)
            {
                if (!unpReadBuf())
                {
                    return (false);
                }
            }
            var Number = this.decodeNumber(BD);
            if (Number < 16)
            {
                Table[I] = (byte)((Number + UnpOldTable20[I]) & 0xf);
                I++;
            }
            else if (Number == 16)
            {
                N = (Utility.URShift(GetBits(), 14)) + 3;
                AddBits(2);
                while (N-- > 0 && I < TableSize)
                {
                    Table[I] = Table[I - 1];
                    I++;
                }
            }
            else
            {
                if (Number == 17)
                {
                    N = (Utility.URShift(GetBits(), 13)) + 3;
                    AddBits(3);
                }
                else
                {
                    N = (Utility.URShift(GetBits(), 9)) + 11;
                    AddBits(7);
                }
                while (N-- > 0 && I < TableSize)
                {
                    Table[I++] = 0;
                }
                // Nanook. Working port from Rar C code. Added when working on Audio Decode Fix. Seems equal to above, so commented it
                //byte v;
                //if (Number == 16)
                //{
                //    N = (Utility.URShift(GetBits(), 14)) + 3;
                //    AddBits(2);
                //    v = Table[I - 1];
                //}
                //else
                //{
                //    N = (Number - 17) * 4;
                //    int bits = 3 + N;
                //    N += N + 3 + (Utility.URShift(GetBits(), 16 - bits));
                //    AddBits(bits);
                //    v = 0;
                //}
                //N += I;
                //if (N > TableSize)
                //{
                //    N = TableSize; // original unRAR
                //}
                //do
                //{
                //    Table[I++] = v;
                //} while (I < N);
            }
        }
        if (inAddr > readTop)
        {
            return (true);
        }
        if (UnpAudioBlock != 0)
        {
            for (I = 0; I < UnpChannels; I++)
            {
                UnpackUtility.makeDecodeTables(Table, I * PackDef.MC20, MD[I], PackDef.MC20);
            }
        }
        else
        {
            UnpackUtility.makeDecodeTables(Table, 0, LD, PackDef.NC20);
            UnpackUtility.makeDecodeTables(Table, PackDef.NC20, DD, PackDef.DC20);
            UnpackUtility.makeDecodeTables(Table, PackDef.NC20 + PackDef.DC20, RD, PackDef.RC20);
        }

        // memcpy(UnpOldTable20,Table,sizeof(UnpOldTable20));
        for (var i = 0; i < UnpOldTable20.Length; i++)
        {
            UnpOldTable20[i] = Table[i];
        }
        return (true);
    }

    private async System.Threading.Tasks.Task<bool> ReadTables20Async(
        System.Threading.CancellationToken cancellationToken = default
    )
    {
        byte[] BitLength = new byte[PackDef.BC20];
        byte[] Table = new byte[PackDef.MC20 * 4];
        int TableSize,
            N,
            I;
        if (inAddr > readTop - 25)
        {
            if (!await unpReadBufAsync(cancellationToken).ConfigureAwait(false))
            {
                return (false);
            }
        }
        var BitField = GetBits();
        UnpAudioBlock = (BitField & 0x8000);

        if (0 == (BitField & 0x4000))
        {
            new Span<byte>(UnpOldTable20).Clear();
        }
        AddBits(2);

        if (UnpAudioBlock != 0)
        {
            UnpChannels = ((Utility.URShift(BitField, 12)) & 3) + 1;
            if (UnpCurChannel >= UnpChannels)
            {
                UnpCurChannel = 0;
            }
            AddBits(2);
            TableSize = PackDef.MC20 * UnpChannels;
        }
        else
        {
            TableSize = PackDef.NC20 + PackDef.DC20 + PackDef.RC20;
        }
        for (I = 0; I < PackDef.BC20; I++)
        {
            BitLength[I] = (byte)(Utility.URShift(GetBits(), 12));
            AddBits(4);
        }
        UnpackUtility.makeDecodeTables(BitLength, 0, BD, PackDef.BC20);
        I = 0;
        while (I < TableSize)
        {
            if (inAddr > readTop - 5)
            {
                if (!await unpReadBufAsync(cancellationToken).ConfigureAwait(false))
                {
                    return (false);
                }
            }
            var Number = this.decodeNumber(BD);
            if (Number < 16)
            {
                Table[I] = (byte)((Number + UnpOldTable20[I]) & 0xf);
                I++;
            }
            else if (Number == 16)
            {
                N = (Utility.URShift(GetBits(), 14)) + 3;
                AddBits(2);
                while (N-- > 0 && I < TableSize)
                {
                    Table[I] = Table[I - 1];
                    I++;
                }
            }
            else
            {
                if (Number == 17)
                {
                    N = (Utility.URShift(GetBits(), 13)) + 3;
                    AddBits(3);
                }
                else
                {
                    N = (Utility.URShift(GetBits(), 9)) + 11;
                    AddBits(7);
                }
                while (N-- > 0 && I < TableSize)
                {
                    Table[I++] = 0;
                }
            }
        }
        if (inAddr > readTop)
        {
            return (true);
        }
        if (UnpAudioBlock != 0)
        {
            for (I = 0; I < UnpChannels; I++)
            {
                UnpackUtility.makeDecodeTables(Table, I * PackDef.MC20, MD[I], PackDef.MC20);
            }
        }
        else
        {
            UnpackUtility.makeDecodeTables(Table, 0, LD, PackDef.NC20);
            UnpackUtility.makeDecodeTables(Table, PackDef.NC20, DD, PackDef.DC20);
            UnpackUtility.makeDecodeTables(Table, PackDef.NC20 + PackDef.DC20, RD, PackDef.RC20);
        }

        for (var i = 0; i < UnpOldTable20.Length; i++)
        {
            UnpOldTable20[i] = Table[i];
        }
        return (true);
    }

    private void unpInitData20(bool Solid)
    {
        if (!Solid)
        {
            UnpChannelDelta = UnpCurChannel = 0;
            UnpChannels = 1;

            // memset(AudV,0,sizeof(AudV));
            AudV[0] = new AudioVariables();
            AudV[1] = new AudioVariables();
            AudV[2] = new AudioVariables();
            AudV[3] = new AudioVariables();

            // memset(UnpOldTable20,0,sizeof(UnpOldTable20));
            new Span<byte>(UnpOldTable20).Clear();
        }
    }

    private void ReadLastTables()
    {
        if (readTop >= inAddr + 5)
        {
            if (UnpAudioBlock != 0)
            {
                if (this.decodeNumber(MD[UnpCurChannel]) == 256)
                {
                    ReadTables20();
                }
            }
            else
            {
                if (this.decodeNumber(LD) == 269)
                {
                    ReadTables20();
                }
            }
        }
    }

    private byte DecodeAudio(int Delta)
    {
        var v = AudV[UnpCurChannel];
        v.ByteCount++;
        v.D4 = v.D3;
        v.D3 = v.D2; // ->D3=V->D2;
        v.D2 = v.LastDelta - v.D1; // ->D2=V->LastDelta-V->D1;
        v.D1 = v.LastDelta; // V->D1=V->LastDelta;

        // int PCh=8*V->LastChar+V->K1*V->D1 +V->K2*V->D2 +V->K3*V->D3
        // +V->K4*V->D4+ V->K5*UnpChannelDelta;
        var PCh = (8 * v.LastChar) + (v.K1 * v.D1);
        PCh += (v.K2 * v.D2) + (v.K3 * v.D3);
        PCh += (v.K4 * v.D4) + (v.K5 * UnpChannelDelta);
        PCh = (Utility.URShift(PCh, 3)) & 0xFF;

        var Ch = PCh - Delta;
        var D = ((sbyte)Delta) << 3;

        v.Dif[0] += Math.Abs(D); // V->Dif[0]+=abs(D);
        v.Dif[1] += Math.Abs(D - v.D1); // V->Dif[1]+=abs(D-V->D1);
        v.Dif[2] += Math.Abs(D + v.D1); // V->Dif[2]+=abs(D+V->D1);
        v.Dif[3] += Math.Abs(D - v.D2); // V->Dif[3]+=abs(D-V->D2);
        v.Dif[4] += Math.Abs(D + v.D2); // V->Dif[4]+=abs(D+V->D2);
        v.Dif[5] += Math.Abs(D - v.D3); // V->Dif[5]+=abs(D-V->D3);
        v.Dif[6] += Math.Abs(D + v.D3); // V->Dif[6]+=abs(D+V->D3);
        v.Dif[7] += Math.Abs(D - v.D4); // V->Dif[7]+=abs(D-V->D4);
        v.Dif[8] += Math.Abs(D + v.D4); // V->Dif[8]+=abs(D+V->D4);
        v.Dif[9] += Math.Abs(D - UnpChannelDelta); // V->Dif[9]+=abs(D-UnpChannelDelta);
        v.Dif[10] += Math.Abs(D + UnpChannelDelta); // V->Dif[10]+=abs(D+UnpChannelDelta);

        v.LastDelta = (sbyte)(Ch - v.LastChar);
        UnpChannelDelta = v.LastDelta;
        v.LastChar = Ch; // V->LastChar=Ch;

        if ((v.ByteCount & 0x1F) == 0)
        {
            int MinDif = v.Dif[0],
                NumMinDif = 0;
            v.Dif[0] = 0; // ->Dif[0]=0;
            for (var I = 1; I < v.Dif.Length; I++)
            {
                if (v.Dif[I] < MinDif)
                {
                    MinDif = v.Dif[I];
                    NumMinDif = I;
                }
                v.Dif[I] = 0;
            }
            switch (NumMinDif)
            {
                case 1:
                    if (v.K1 >= -16)
                    {
                        v.K1--; // V->K1--;
                    }
                    break;

                case 2:
                    if (v.K1 < 16)
                    {
                        v.K1++; // V->K1++;
                    }
                    break;

                case 3:
                    if (v.K2 >= -16)
                    {
                        v.K2--; // V->K2--;
                    }
                    break;

                case 4:
                    if (v.K2 < 16)
                    {
                        v.K2++; // V->K2++;
                    }
                    break;

                case 5:
                    if (v.K3 >= -16)
                    {
                        v.K3--;
                    }
                    break;

                case 6:
                    if (v.K3 < 16)
                    {
                        v.K3++;
                    }
                    break;

                case 7:
                    if (v.K4 >= -16)
                    {
                        v.K4--;
                    }
                    break;

                case 8:
                    if (v.K4 < 16)
                    {
                        v.K4++;
                    }
                    break;

                case 9:
                    if (v.K5 >= -16)
                    {
                        v.K5--;
                    }
                    break;

                case 10:
                    if (v.K5 < 16)
                    {
                        v.K5++;
                    }
                    break;
            }
        }
        return ((byte)Ch);
    }
}
