using System;
using System.Collections.Generic;
using System.Net.NetworkInformation;
using System.Text;

namespace PspCrypto
{
    public class Lzrc
    {
        private bool np9660;

        private byte[] input;
        private int in_ptr;
        private int in_len;

        private byte[] output;
        private int out_ptr;
        private int out_len;

        private uint range;
        private uint code;
        private uint out_code;
        private byte lc;
        private byte[][] bm_literal;
        private byte[][] bm_dist_bits;
        private byte[][] bm_dist;
        private byte[][] bm_match;
        private byte[][] bm_len;

        const int MAX_WIN_SZ = 16384;
        const int MAX_TBL_SZ = 65280;
        const int TBL_SZ = 65536;

        private byte[] text_buf = new byte[TBL_SZ];
        private int t_start, t_end, t_fill, sp_fill;
        private int t_len, t_pos;

        private int[] prev = new int[TBL_SZ], next = new int[TBL_SZ];
        private int[] root = new int[TBL_SZ];

        public Lzrc(bool np9660 = false)
        {
            this.np9660 = np9660;

            if (np9660)
            {
                this.bm_literal = new byte[8][];
                this.bm_dist_bits = new byte[8][];
                this.bm_dist = new byte[18][];
                this.bm_match = new byte[8][];
                this.bm_len = new byte[8][];
            }
            else
            {
                this.bm_literal = new byte[8][];
                this.bm_dist_bits = new byte[8][];
                this.bm_dist = new byte[16][];
                this.bm_match = new byte[8][];
                this.bm_len = new byte[8][];
            }
        }

        static void Init(byte[][] arr, byte value, int length)
        {
            for (int i = 0; i < arr.Length; i++)
            {
                arr[i] = new byte[length];
                for (int j = 0; j < length; j++)
                {
                    arr[i][j] = value;
                }
            }
        }

        private byte rc_getbyte()
        {
            if (in_ptr == in_len)
            {
                throw new Exception("End of input!");
            }

            return input[in_ptr++];
        }

        void rc_putbyte(byte b)
        {
            if (out_ptr == out_len)
            {
                throw new Exception("Output overflow!");
            }

            output[out_ptr++] = b;
        }

        void re_init(ref byte[] in_buf, int in_len)
        {
            input = in_buf;
            this.in_len = in_len;
            in_ptr = 0;

            this.output = new byte[in_len];
            this.out_len = in_len;
            out_ptr = 0;

            range = 0xffffffff;
            code = 0x00000000;
            lc = 5;
            out_code = 0xffffffff;

            re_putbyte(lc);


            if (this.np9660)
            {
                Init(bm_literal, 0x80, 256);
                Init(bm_dist_bits, 0x80, 39);
                Init(bm_dist, 0x80, 8);
                Init(bm_match, 0x80, 8);
                Init(bm_len, 0x80, 31);
            }
            else
            {
                Init(bm_literal, 0x80, 256); // 2048  2680 2656
                Init(bm_dist_bits, 0x80, 23); // 184
                Init(bm_dist, 0x80, 8); // 128
                Init(bm_match, 0x80, 8); // 64
                Init(bm_len, 0x80, 32); // 256 

            }
            //memset(re->bm_literal, 0x80, 2048);
            //memset(re->bm_dist_bits, 0x80, 312);
            //memset(re->bm_dist, 0x80, 144);
            //memset(re->bm_match, 0x80, 64);
            //memset(re->bm_len, 0x80, 248);
        }

        void rc_init(byte[] out_buf, int out_len, byte[] in_buf, int in_len)
        {
            input = in_buf;
            this.in_len = in_len;
            in_ptr = 0;

            output = out_buf;
            this.out_len = out_len;
            out_ptr = 0;

            range = 0xffffffff;
            lc = rc_getbyte();
            code = (uint)((rc_getbyte() << 24) |
                   (rc_getbyte() << 16) |
                   (rc_getbyte() << 8) |
                   rc_getbyte());
            out_code = 0xffffffff;
            
            if (this.np9660)
            {
                Init(bm_literal, 0x80, 256);
                Init(bm_dist_bits, 0x80, 39);
                Init(bm_dist, 0x80, 8);
                Init(bm_match, 0x80, 8);
                Init(bm_len, 0x80, 31);
            }
            else
            {
                Init(bm_literal, 0x80, 256); // 2048  2680 2656
                Init(bm_dist_bits, 0x80, 23); // 184
                Init(bm_dist, 0x80, 8); // 128
                Init(bm_match, 0x80, 8); // 64
                Init(bm_len, 0x80, 32); // 256 

            }

        }

        void normalize()
        {
            if (range < 0x01000000)
            {
                range <<= 8;
                code = (code << 8) + input[in_ptr];
                in_ptr++;
            }
        }

        int rc_bit(byte[] probs, int index)
        {
            uint bound;

            normalize();

            bound = (range >> 8) * probs[index];
            probs[index] -= (byte)(probs[index] >> 3);

            if (code < bound)
            {
                range = bound;
                probs[index] += 31;
                return 1;
            }
            else
            {
                code -= bound;
                range -= bound;
                return 0;
            }
        }

        int rc_bittree(byte[] probs, int index, int limit)
        {
            int number = 1;
            do
            {
                number = (number << 1) + rc_bit(probs, index + number);
            } while (number < limit);

            number -= limit;

            return number;
        }

        int rc_number(byte[] prob, int index, int n)
        {
            int i, number = 1;

            if (n > 3)
            {
                number = (number << 1) + rc_bit(prob, index + 3);
                if (n > 4)
                {
                    number = (number << 1) + rc_bit(prob, index + 3);
                }

                if (n > 5)
                {
                    normalize();
                    for (i = 0; i < n - 5; i++)
                    {
                        range >>= 1;
                        number <<= 1;
                        if (code < range)
                        {
                            number += 1;
                        }
                        else
                        {
                            code -= range;
                        }
                    }
                }
            }

            if (n > 0)
            {
                number = (number << 1) + rc_bit(prob, index + 0);
                if (n > 1)
                {
                    number = (number << 1) + rc_bit(prob, index + 1);
                    if (n > 2)
                    {
                        number = (number << 1) + rc_bit(prob, index + 2);
                    }
                }
            }

            return number;
        }

        public void init_tree()
        {
            int i;

            for (i = 0; i < TBL_SZ; i++)
            {
                root[i] = -1;
                prev[i] = -1;
                next[i] = -1;
            }

            t_start = 0;
            t_end = 0;
            t_fill = 0;
            sp_fill = 0;
        }
        
        void fill_buffer()
        {
            //void *memcpy(void *dest, const void * src, size_t n)

            int content_size, back_size, front_size;

            if (sp_fill == in_len)
                return;

            content_size = (t_fill < t_end) ? (MAX_TBL_SZ + t_fill - t_end) : (t_fill - t_end);
            if (content_size >= 509)
                return;

            if (t_fill < t_start)
            {
                back_size = t_start - t_fill - 1;
                if (sp_fill + back_size > in_len)
                    back_size = in_len - sp_fill;

                Array.ConstrainedCopy(input, sp_fill, text_buf, t_fill, back_size);
                // memcpy(text_buf + t_fill, re->input + sp_fill, back_size);

                sp_fill += back_size;
                t_fill += back_size;
            }
            else
            {
                back_size = MAX_TBL_SZ - t_fill;
                if (t_start == 0)
                    back_size -= 1;
                if (sp_fill + back_size > in_len)
                    back_size = in_len - sp_fill;

                Array.ConstrainedCopy(input, sp_fill, text_buf, t_fill, back_size);
                //memcpy(text_buf + t_fill, re->input + sp_fill, back_size);

                sp_fill += back_size;
                t_fill += back_size;

                front_size = t_start;
                if (t_start != 0)
                    front_size -= 1;
                if (sp_fill + front_size > in_len)
                    front_size = in_len - sp_fill;

                Array.ConstrainedCopy(input, sp_fill, text_buf, 0, front_size);
                //memcpy(text_buf, re->input + sp_fill, front_size);

                sp_fill += front_size;

                Array.ConstrainedCopy(text_buf, 255, text_buf, MAX_TBL_SZ, front_size);
                //memcpy(text_buf + max_tbl_sz, text_buf, 255);

                t_fill += front_size;
                if (t_fill >= MAX_TBL_SZ)
                    t_fill -= MAX_TBL_SZ;
            }
        }
        void remove_node(int p)
        {
            int t, q;

            if (prev[p] == -1)
                return;

            t = text_buf[p + 0];
            t = (t << 8) | text_buf[p + 1];

            q = next[p];
            if (q != -1)
                prev[q] = prev[p];

            if (prev[p] == -2)
                root[t] = q;
            else
                next[prev[p]] = q;

            prev[p] = -1;
            next[p] = -1;
        }

        int insert_node(int pos, out int match_len, out int match_dist, int do_cmp)
        {
            //Span<byte> src, win;
            int i, t, p;
            int content_size;

            //src = text_buf[pos..];
            //win = text_buf[t_start..];
            content_size = (t_fill < pos) ? (MAX_TBL_SZ + t_fill - pos) : (t_fill - pos);
            t_len = 1;
            t_pos = 0;
            match_len = t_len;
            match_dist = t_pos;

            if (in_ptr >= in_len)
            {
                match_len = 256;
                return 0;
            }

            if (in_ptr >= (in_len - 1))
                return 0;

            t = text_buf[pos];
            t = (t << 8) | text_buf[pos+1];
            if (root[t] == -1)
            {
                root[t] = pos;
                prev[pos] = -2;
                next[pos] = -1;
                return 0;
            }

            p = root[t];
            root[t] = pos;
            prev[pos] = -2;
            next[pos] = p;

            if (p != -1)
                prev[p] = pos;

            while (do_cmp == 1 && p != -1)
            {
                for (i = 0; (i < 255 && i < content_size); i++)
                {
                    if (text_buf[pos+i] != text_buf[p + i])
                        break;
                }

                if (i > t_len)
                {
                    t_len = i;
                    t_pos = pos - p;
                }
                else if (i == t_len)
                {
                    int mp = pos - p;
                    if (mp < 0)
                        mp += MAX_TBL_SZ;
                    if (mp < t_pos)
                    {
                        t_len = i;
                        t_pos = pos - p;
                    }
                }
                if (i == 255)
                {
                    remove_node(p);
                    break;
                }

                p = next[p];
            }

            if (this.np9660) 
            {
                // have we calculated match_dist of 256 when its not the end?
                if (t_len == 256 && in_ptr < in_len)
                    return 1;
            }
            if (t_pos < 0) throw new Exception("t_pos was < 0 :?"); // TODO: figure out why this happens on np9660.

            match_len = t_len;
            match_dist = t_pos;

            return 1;
        }
        void update_tree(int length)
        {
            int i, win_size;
            int tmp_len, tmp_pos;

            win_size = (t_end >= t_start) ? (t_end - t_start) : (MAX_TBL_SZ + t_end - t_start);

            for (i = 0; i < length; i++)
            {
                if (win_size == MAX_WIN_SZ)
                {
                    remove_node(t_start);
                    t_start += 1;
                    if (t_start == MAX_TBL_SZ)
                        t_start = 0;
                }
                else
                {
                    win_size += 1;
                }

                if (i > 0)
                {
                    insert_node(t_end, out tmp_len, out tmp_pos, 0);
                }
                t_end += 1;
                if (t_end >= MAX_TBL_SZ)
                    t_end -= MAX_TBL_SZ;
            }
        }
        void re_bittree(ref byte[] probs,int index, int limit, int number)
        {
            int n, tmp, bit;

            number += limit;

            // Get total bits used by number
            tmp = number;
            n = 0;
            while (tmp > 1)
            {
                tmp >>= 1;
                n++;
            }

            do
            {
                tmp = number >> n;
                bit = (number >> (n - 1)) & 1;
                re_bit(ref probs, index + tmp, bit);
                n -= 1;
            } while (n > 0);
        }

        void re_bit(ref byte[] prob, int index, int bit)
        {
            uint bound;
            uint old_r, old_c;
            byte old_p;

            re_normalize();

            old_r = range;
            old_c = code;
            old_p = prob[index];

            var pProb = prob[index];

            bound = (range >> 8) * (pProb);
            pProb -= (byte)(pProb >> 3);

            if (bit != 0)
            {
                range = bound;
                pProb += 31;
            }
            else
            {
                code += bound;
                if (code < old_c)
                    out_code += 1;
                range -= bound;
            }

            prob[index] = pProb;
        }

        void re_normalize()
        {
            if (range < 0x01000000)
            {
                if (out_code != 0xffffffff)
                {
                    if (out_code > 255)
                    {
                        int p, old_c;
                        p = out_ptr - 1;
                        do
                        {
                            old_c = output[p];
                            output[p] += 1;
                            p -= 1;
                        } while (old_c == 0xff);
                    }

                    re_putbyte((byte)(out_code & 0xff));
                }
                out_code = (code >> 24) & 0xff;
                range <<= 8;
                code <<= 8;
            }
        }

        void re_putbyte(byte out_byte)
        {
            if (out_ptr == out_len)
            {
                out_len += 0x1000;
                Array.Resize(ref output, out_len);
            }

            output[out_ptr++] = out_byte;
        }
        byte re_getbyte()
        {
            if (in_ptr == in_len)
            {
                throw new Exception("End of input!");
            }

            return input[in_ptr++];
        }

        void re_number(ref byte[] prob, int index, int n, int number)
        {
            int i;
            UInt32 old_c;

            i = 1;

            if (n > 3)
            {
                re_bit(ref prob, index + 3,(number >> (n - i)) & 1);
                i += 1;
                if (n > 4)
                {
                    re_bit(ref prob, index + 3, (number >> (n - i)) & 1);
                    i += 1;
                    if (n > 5)
                    {
                        re_normalize();
                        for (i = 3; i < n - 2; i++)
                        {
                            range >>= 1;
                            if (((number >> (n - i)) & 1) == 0)
                            {
                                old_c = code;
                                code += range;
                                if (code < old_c)
                                    out_code += 1;
                            }
                        }
                    }
                }
            }

            if (n > 0)
            {
                re_bit(ref prob, index + 0, (number >> (n - i - 0)) & 1);
                if (n > 1)
                {
                    re_bit(ref prob, index + 1, (number >> (n - i - 1)) & 1);
                    if (n > 2)
                    {
                        re_bit(ref prob, index + 2, (number >> (n - i - 2)) & 1);
                    }
                }
            }
        }
        void re_flush()
        {
            re_putbyte((byte)((out_code) & 0xff));
            re_putbyte((byte)((code >> 24) & 0xff));
            re_putbyte((byte)((code >> 16) & 0xff));
            re_putbyte((byte)((code >> 8) & 0xff));
            re_putbyte((byte)((code >> 0) & 0xff));
        }

        private byte[] re_trunc()
        {
            Array.Resize(ref output, out_ptr);
            return output;
        }

        public byte[] lzrc_compress(byte[] in_buf, int in_len)
        {
            int match_step, re_state, len_state, dist_state;
            int i, cur_byte, last_byte;
            int match_len, len_bits;
            int match_dist, dist_bits, limit;
            int round = -1;

            // initalize buffers to all 0x80
            re_init(ref in_buf, in_len);

            // initalize the tree
            init_tree();

            // initalize variable to 0 ...
            re_state = 0;
            last_byte = 0;
            match_len = 0;
            match_dist = 0;
            
            bool flg = false;

            while (true)
            {
                round += 1;
                match_step = 0;

                fill_buffer();
                insert_node(t_end, out match_len, out match_dist, 1);

                if (match_len < 256)
                {
                    // condition is different if np9660 vs pops
                    if (this.np9660)
                        flg = (match_len < 4 && match_dist > 255);
                    else
                        flg = (match_len <= 4 && match_dist > 255);

                    if(flg) // if (condition)
                        match_len = 1;
                    update_tree(match_len);
                }

                // condition is different if np9660 vs pops
                if (this.np9660)
                    flg = (match_len == 1 || (match_len < 4 && match_dist > 255));
                else
                    flg = (match_len == 1 || (match_len <= 4 && match_dist > 255));

                if (flg)
                {
                    re_bit(ref bm_match[re_state], match_step, 0);

                    if (re_state > 0)
                        re_state -= 1;

                    cur_byte = re_getbyte();
                    re_bittree(ref bm_literal[((last_byte >> lc) & 0x07)], 0, 0x100, cur_byte);

                    if (!this.np9660 && in_ptr >= in_len)
                    {
                        re_normalize();
                        re_flush();
                        return re_trunc();
                    }
                }
                else
                {
                    re_bit(ref bm_match[re_state], match_step, 1);

                    // write bitstream length (8 bits, where 0 marks the end of it)
                    len_bits = 0;
                    for (i = 1; i < 8; i++)
                    {
                        match_step += 1;
                        if ((match_len - 1) < (1 << i))
                            break;
                        re_bit(ref bm_match[re_state], match_step, 1);
                        len_bits += 1;
                    }
                    if (i != 8)
                        re_bit(ref bm_match[re_state], match_step, 0);

                    if (len_bits > 0)
                    {
                        len_state = ((len_bits - 1) << 2) + ((in_ptr << (len_bits - 1)) & 0x03);
                        re_number(ref bm_len[re_state], len_state, len_bits, (match_len - 1));

                        if (this.np9660)
                            flg = (match_len == 256);
                        else
                            flg = (in_ptr >= in_len);

                        if (flg)
                        {
                            re_normalize();
                            re_flush();
                            return re_trunc();
                        }
                    }
                    
                    // determine limit ...
                    dist_state = 0;
                    limit = 8;

                    // condition is different if np9660 vs pops
                    if (this.np9660)
                        flg = (match_len > 3);
                    else
                        flg = (match_len > 4);


                    if (flg) // if (condition)
                    {
                        dist_state += 7;

                        if (this.np9660)
                        {
                            limit = 44;
                        }
                        else
                        {
                            limit = 16;
                        }

                    }

                    // find total 1s in the match_dist
                    dist_bits = 0;
                    if(match_dist > 0) {
                        while ((match_dist >> dist_bits) != 1)
                        {
                            dist_bits += 1;
                        }
                    }
                    else
                    {
                        throw new Exception("Match dist is 0.. uhh cant match with yourself..");
                    }

                    re_bittree(ref bm_dist_bits[len_bits], dist_state, limit, dist_bits);

                    if (dist_bits > 0)
                        re_number(ref bm_dist[dist_bits], 0, dist_bits, match_dist);
                    

                    in_ptr += match_len;
                    re_state = 6 + ((in_ptr + 1) & 1);
                }
                last_byte = input[in_ptr - 1];
            } 
            
        }

        public int lzrc_decompress(byte[] out_buf, int out_len, byte[] in_buf, int in_len)
        {
            int match_step, rc_state, len_state, dist_state = 0;
            int i, bit, cur_byte, last_byte;
            int match_len, len_bits;
            int match_dist, dist_bits, limit;
            int match_src;
            int round = -1;

            bool flg = false;
            len_state = 0;

            rc_init(out_buf, out_len, in_buf, in_len);

            /*if ((lc & 0x80) != 0)
            {
                Buffer.BlockCopy(in_buf, 5, out_buf, 0, (int)code);
                return (int)code;
            }*/

            rc_state = 0;
            last_byte = 0;

            while (true)
            {
                round += 1;
                match_step = 0;
                bit = rc_bit(bm_match[rc_state], match_step);
                // if bit is 0, just copy from the bit tree into the output ?
                if (bit == 0) 
                {
                    if (rc_state > 0)
                        rc_state -= 1;

                    cur_byte = rc_bittree(bm_literal[(((out_ptr & 7) << 8) + last_byte >> lc) & 0x07], 0, 0x100);

                    rc_putbyte((byte)cur_byte);
                    if (out_ptr == out_len) return out_ptr;
                }
                else // This essentially goes; "hey the bytes that go here, are the same ones that were already *here* 
                {
                    // Determine bit length?
                    len_bits = 0;
                    for (i = 0; i < 7; i++)
                    {
                        match_step += 1;
                        bit = rc_bit(bm_match[rc_state], match_step);
                        if (bit == 0)
                            break;
                        len_bits += 1;
                    }

                    // Get the match length ..
                    if (len_bits == 0)
                    {
                        match_len = 1;
                    }
                    else
                    {
                        len_state = ((len_bits - 1) << 2) + ((out_ptr << (len_bits - 1)) & 0x03);
                        match_len = rc_number(bm_len[rc_state], len_state, len_bits);
                        if (this.np9660 && match_len == 0xFF)
                            return out_ptr;
                    }

                    dist_state = 0;
                    limit = 8;
                    if (this.np9660)
                        flg = (match_len > 2);
                    else
                        flg = (match_len > 3);

                    if (flg)
                    {
                        dist_state += 7;
                        if (this.np9660) 
                            limit = 44;
                        else
                            limit = 16;
                    }
                    dist_bits = rc_bittree(bm_dist_bits[len_bits], dist_state, limit);

                    if (dist_bits > 0)
                        match_dist = rc_number(bm_dist[dist_bits], 0, dist_bits);
                    else
                        match_dist = 1;

                    match_src = out_ptr - match_dist;
                    if (match_dist > out_ptr || match_dist < 0)
                    {
                        //Console.WriteLine($"match_dist out of range! 0x{match_dist:x8}");
                        //return -1; // test
                        throw new Exception($"match_dist out of range! 0x{match_dist:x8}");
                    }


                    for (i = 0; i < match_len + 1; i++)
                    {
                        rc_putbyte(output[match_src++]);
                    }
                    rc_state = 6 + ((out_ptr + 1) & 1);
                    if (out_ptr == out_len) return out_ptr;
                }
                last_byte = output[out_ptr - 1];
            }
        }
    }
}
