//! \file       ImageI24.cs
//! \date       2019 Jun 22
//! \brief      HyperWorks image format.
//
// Copyright (C) 2019 by morkt
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
//

using System;
using System.ComponentModel.Composition;
using System.IO;
using System.Windows.Media;

// [970905][Ucom] Winter Kiss
// [980626][Love Gun] ACE OF SPADES 2

namespace GameRes.Formats.HyperWorks
{
    internal class I24MetaData : ImageMetaData
    {
        public byte Version;
    }

    [Export(typeof(ImageFormat))]
    public class I24Format : ImageFormat
    {
        public override string         Tag => "I24";
        public override string Description => "HyperWorks RGB image format";
        public override uint     Signature => 0x41343249; // 'I24A'

        public I24Format ()
        {
            Signatures = new[] { 0x41343249u, 0x20343249u };
        }

        public override ImageMetaData ReadMetaData (IBinaryStream file)
        {
            var header = file.ReadHeader (0x18);
            int bpp = header.ToInt16 (0x10);
            if (bpp != 24)
                return null;
            return new I24MetaData {
                Width  = header.ToUInt16 (0xC),
                Height = header.ToUInt16 (0xE),
                BPP    = bpp,
                Version = header[3],
            };
        }

        public override ImageData Read (IBinaryStream file, ImageMetaData info)
        {
            var reader = new I24Decoder (file, (I24MetaData)info);
            return reader.Unpack();
        }

        public override void Write (Stream file, ImageData image)
        {
            throw new System.NotImplementedException ("I24Format.Write not implemented");
        }
    }

    internal class I24Decoder
    {
        IBinaryStream   m_input;
        I24MetaData     m_info;

        public I24Decoder (IBinaryStream input, I24MetaData info)
        {
            m_input = input;
            m_info = info;
        }

        static readonly short[] shiftTable = new short[] {
            -1, 0, 0, 1, 1, 1, -1, 1, 2, 1, -2, 1, -2, 0, 0, 2, 1, 2, -1, 2, -3, 0
        };

        bool cacheEmpty = true;
        int byteCount = 0;
        int bits = 0;
        int bitCount = 0;

        class Node
        {
            public Node     next;
            public int      depth;
            public int      token;
        }

        struct DictRec
        {
            public Link     link;
            public int      bitSize;
            public int      token;
        }

        class Link
        {
            public Link[]   children = new Link[2];
            public int      token;
        }

        DictRec[] dTable_1 = new DictRec[256];
        DictRec[] dTable_2 = new DictRec[256];
        DictRec[] dTable_3 = new DictRec[256];

        Link[] tBuffer_1 = InitNodeList<Link> (684);
        Link[] tBuffer_2 = InitNodeList<Link> (22);
        Link[] tBuffer_3 = InitNodeList<Link> (502);

        Node[] bTable_1 = InitNodeList<Node> (342);
        Node[] bTable_2 = InitNodeList<Node> (11);
        Node[] bTable_3 = InitNodeList<Node> (251);

        static T[] InitNodeList<T>(int count) where T : new()
        {
            var list = new T[count];
            for (int i = 0; i < count; ++i)
                list[i] = new T();
            return list;
        }

        public ImageData Unpack ()
        {
            m_input.Position = 0x18;
            int stride = m_info.iWidth * 4;
            var pixels = new byte[stride * m_info.iHeight];
            byte[][] line_buffer = new[] { new byte[stride], new byte[stride], new byte[stride] };
            int dst = 0;
            var shift_table = shiftTable.Clone() as short[];
            for (int y = 0; y < m_info.iHeight; ++y)
            {
                var line = line_buffer[2];
                line_buffer[2] = line_buffer[1];
                line_buffer[1] = line_buffer[0];
                line_buffer[0] = line;
                int x = 0;
                int p = 0;
                while (x < m_info.iWidth)
                {
                    if (byteCount-- == 0)
                    {
                        if (cacheEmpty)
                        {
                            cacheEmpty = false;
                            int val = ReadUInt8() << 8;
                            val |= ReadUInt8();
                            bits = val;
                            bitCount = 8;
                        }
                        InitTree (bTable_1, 342);
                        InitTree (bTable_2, 11);
                        InitTree (bTable_3, 251);

                        RebuildTree(bTable_1, dTable_1, tBuffer_1, 342);
                        RebuildTree(bTable_2, dTable_2, tBuffer_2, 11);
                        RebuildTree(bTable_3, dTable_3, tBuffer_3, 251);
                        byteCount = 0x3FFF;
                    }
                    int color_token = GetToken (dTable_1);
                    int shift_token = GetToken (dTable_2);

                    int shift_idx = 2 * shift_token;
                    short s1 = shift_table[shift_idx];
                    short s2 = shift_table[shift_idx + 1];
                    if (shift_token != 0)
                    {
                        if (m_info.Version == 'A')
                        {
                            while (shift_idx > 0)
                            {
                                shift_table[shift_idx]   = shift_table[shift_idx - 2];
                                shift_table[shift_idx+1] = shift_table[shift_idx - 1];
                                shift_idx -= 2;
                            }
                            shift_table[0] = s1;
                            shift_table[1] = s2;
                        }
                        else
                        {
                            shift_table[shift_idx    ] = shift_table[shift_idx - 2];
                            shift_table[shift_idx + 1] = shift_table[shift_idx - 1];
                            shift_table[shift_idx - 2] = s1;
                            shift_table[shift_idx - 1] = s2;
                        }
                    }
                    int src = 4 * (x + s1);
                    if (color_token >= 216)
                    {
                        int count = color_token - 214;
                        x += count;
                        while (count --> 0)
                        {
                            line[p++] = line_buffer[s2][src++];
                            line[p++] = line_buffer[s2][src++];
                            line[p++] = line_buffer[s2][src++];
                            line[p++] = line_buffer[s2][src++];
                        }
                    }
                    else
                    {
                        sbyte r = shift_R[color_token];
                        if (r == -3)
                            r = (sbyte)(GetToken (dTable_3) + 3);
                        line[p+2] = (byte)(line_buffer[s2][src+2] - r);

                        sbyte g = shift_G[color_token];
                        if (g == -3)
                            g = (sbyte)(GetToken(dTable_3) + 3);
                        line[p+1] = (byte)(line_buffer[s2][src+1] - g);

                        sbyte b = shift_B[color_token];
                        if (b == -3)
                            b = (sbyte)(GetToken(dTable_3) + 3);
                        line[p] = (byte)(line_buffer[s2][src] - b);
                        line[p+3] = 0;
                        p += 4;
                        ++x;
                    }
                }
                Buffer.BlockCopy (line_buffer[0], 0, pixels, dst, stride);
                dst += stride;
            }
            return ImageData.Create (m_info, PixelFormats.Bgr32, null, pixels, stride);
        }

        private void InitTree (Node[] tree, int count) // sub_408FB0
        {
            for (int i = 0; i < count; ++i)
            {
                tree[i].next = null;
                tree[i].depth = 0;
                tree[i].token = i;
            }
            int length = GetBitLength();
            if (length <= 1)
                return;
            length -= 1;
            int fieldWidth = GetBits (3);
            int tIdx = 0;
            while (length > 0)
            {
                if (GetNextBit() != 0)
                {
                    tree[tIdx++].depth = GetBits (fieldWidth);
                    --length;
                }
                else
                {
                    int step = GetBitLength();
                    if (0 == step)
                        tIdx++;
                    else
                        tIdx += step;
                }
            }
        }

        private void RebuildTree (Node[] tree, DictRec[] dict, Link[] links, int count)
        {
            for (int i = 0; i < 256; ++i)
            {
                dict[i].token = 0;
                dict[i].link = null;
            }
            int next_idx = 0; // var node = tree;
            count -= 1;
            while (0 == tree[next_idx].depth)
            {
                if (--count <= 0)
                    break;
                ++next_idx;
            }
            if (0 == count)
                return;

            var node = tree[next_idx++];
            while (count --> 0)
            {
                int depth = tree[next_idx].depth;
                if (depth != 0)
                {
                    if (node != null)
                    {
                        if (node.depth <= depth)
                        {
                            var prev = node;
                            var ptr = node.next;
                            while (ptr != null)
                            {
                                if (ptr.depth > depth)
                                    break;
                                prev = ptr;
                                ptr = ptr.next;
                            }
                            prev.next = tree[next_idx];
                            tree[next_idx].next = ptr;
                        }
                        else
                        {
                            tree[next_idx].next = node;
                            node = tree[next_idx];
                        }
                    }
                }
                ++next_idx;
            }

            int bit_size = 0;
            int t3_i = 0;
            int dict_idx = 0;
            while (node != null)
            {
                if (node.depth > bit_size)
                {
                    dict_idx <<= node.depth - bit_size;
                    bit_size = node.depth;
                }
                if (bit_size >= 8)
                {
                    if (bit_size == 8)
                    {
                        dict[dict_idx].bitSize = 8;
                        dict[dict_idx].token = node.token;
                        dict[dict_idx].link = null;
                    }
                    else
                    {
                        int d1 = bit_size - 8;
                        int d2 = dict_idx >> d1;
                        int d3 = dict_idx << (32 - (bit_size - 8));
                        dict[d2].bitSize = 0;
                        var ptr_t2 = d2; // &dict[d2];
                        var link = dict[ptr_t2].link;
                        if (null == link)
                        {
                            var t3_ptr = links[t3_i];
                            t3_ptr.children[0] = null;
                            t3_ptr.children[1] = null;
                            t3_ptr.token = 0;
                            link = t3_ptr;
                            dict[ptr_t2].link = t3_ptr;
                            t3_i++;
                        }
                        while (d1 --> 0)
                        {
                            int v26 = (d3 >> 31) & 1;
                            d3 <<= 1;
                            if (null == link.children[v26])
                            {
                                var t3_ptr = links[t3_i];
                                t3_ptr.children[0] = null;
                                t3_ptr.children[1] = null;
                                t3_ptr.token = 0;
                                link.children[v26] = t3_ptr;
                                t3_i++;
                            }
                            link = link.children[v26];
                        }
                        link.token = node.token;
                    }
                }
                else
                {
                    int d1 = dict_idx << (8 - bit_size);
                    int d2 = 1 << (8 - bit_size);
                    while (d2 --> 0)
                    {
                        dict[d1].bitSize = bit_size;
                        dict[d1].token = node.token;
                        d1++;
                    }
                }
                ++dict_idx;
                node = node.next;
            }
        }

        int GetToken (DictRec[] table)
        {
            var table_ptr = table[(bits >> 8) & 0xFF];
            int count = table_ptr.bitSize;
            if (count != 0)
            {
                if (count >= bitCount)
                {
                    bits <<= bitCount;
                    count -= bitCount;
                    bits |= ReadUInt8();
                    bitCount = 8;
                }
                bits <<= count;
                bitCount -= count;
                return table_ptr.token;
            }
            else
            {
                bits = ReadUInt8() | bits << bitCount;
                bits <<= 8 - bitCount;
                Link link = table_ptr.link;
                do
                {
                    int path = GetNextBit() & 1;
                    link = link.children[path];
                    if (null == link)
                        throw new InvalidFormatException ("Invalid tree path");
                }
                while (link.children[0] != null);
                return link.token;
            }
        }

        private byte ReadUInt8()
        {
            return (byte)m_input.ReadByte();
        }

        private int GetNextBit ()
        {
            bits <<= 1;
            if (0 == --bitCount)
            {
                bits |= ReadUInt8();
                bitCount = 8;
            }
            return (bits >> 16) & 1;
        }

        private int GetBitLength ()
        {
            if (GetNextBit() != 0)
                return 0;
            int i = 0;
            do
            {
                ++i;
            }
            while (0 == GetNextBit());
            return (1 << i) | GetBits (i);
        }

        private int GetBits (int count)
        {
            int n = count;
            if (count >= bitCount)
            {
                bits <<= bitCount;
                n = count - bitCount;
                bits |= ReadUInt8();
                bitCount = 8;
            }
            if (n >= 8)
            {
                bits <<= 8;
                bits |= ReadUInt8();
                n -= 8;
            }
            bits <<= n;
            bitCount -= n;
            return (bits >> 16) & bitMask[count];
        }

        static readonly int[] bitMask = new[] {
            0x00000000, 0x00000001, 0x00000003, 0x00000007, 0x0000000F, 0x0000001F, 0x0000003F, 0x0000007F,
            0x000000FF, 0x000001FF, 0x000003FF, 0x000007FF, 0x00000FFF, 0x00001FFF, 0x00003FFF, 0x00007FFF,
            0x0000FFFF, 0x0001FFFF, 0x0003FFFF, 0x0007FFFF, 0x000FFFFF, 0x001FFFFF, 0x003FFFFF, 0x007FFFFF,
            0x00FFFFFF, 0x01FFFFFF, 0x03FFFFFF, 0x07FFFFFF, 0x0FFFFFFF, 0x1FFFFFFF, 0x3FFFFFFF, 0x7FFFFFFF
        };

        static readonly sbyte[] shift_R = new sbyte[] {
            -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3,
            -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -2, -2, -2, -2,
            -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
            -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, -1,
            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
            1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
            2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
        };
        static readonly sbyte[] shift_G = new sbyte[] {
            -3, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0,
            0, 0, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, -3, -3, -3, -3, -3, -3, -2, -2, -2,
            -2, -2, -2, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2,
            2, 2, 2, 2, -3, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -2, -1, -1, -1, -1,
            -1, -1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, -3, -3, -3, -3,
            -3, -3, -2, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 1,
            1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, -3, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2,
            -2, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2,
            2, 2, -3, -3, -3, -3, -3, -3, -2, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, -1,
            0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2,
        };
        static readonly sbyte[] shift_B = new sbyte[] {
            -3, -2, -1, 0, 1, 2, -3, -2, -1, 0, 1, 2, -3, -2, -1, 0, 1, 2, -3, -2, -1, 0, 1, 2,
            -3, -2, -1, 0, 1, 2, -3, -2, -1, 0, 1, 2, -3, -2, -1, 0, 1, 2, -3, -2, -1, 0,
            1, 2, -3, -2, -1, 0, 1, 2, -3, -2, -1, 0, 1, 2, -3, -2, -1, 0, 1, 2, -3, -2,
            -1, 0, 1, 2, -3, -2, -1, 0, 1, 2, -3, -2, -1, 0, 1, 2, -3, -2, -1, 0, 1, 2,
            -3, -2, -1, 0, 1, 2, -3, -2, -1, 0, 1, 2, -3, -2, -1, 0, 1, 2, -3, -2, -1, 0,
            1, 2, -3, -2, -1, 0, 1, 2, -3, -2, -1, 0, 1, 2, -3, -2, -1, 0, 1, 2, -3, -2,
            -1, 0, 1, 2, -3, -2, -1, 0, 1, 2, -3, -2, -1, 0, 1, 2, -3, -2, -1, 0, 1, 2,
            -3, -2, -1, 0, 1, 2, -3, -2, -1, 0, 1, 2, -3, -2, -1, 0, 1, 2, -3, -2, -1, 0,
            1, 2, -3, -2, -1, 0, 1, 2, -3, -2, -1, 0, 1, 2, -3, -2, -1, 0, 1, 2, -3, -2,
            -1, 0, 1, 2, -3, -2, -1, 0, 1, 2, -3, -2, -1, 0, 1, 2, 
        };
    }
}
