Editor/HumanoidRetargeter/Embedded/ValveResourceFormat/Compression/BlockCompress.cs
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using HumanoidRetargeterVrf.Utils;
using System.IO;

namespace HumanoidRetargeterVrf.Compression
{
    /// <summary>
    /// Provides methods for block compression and decompression.
    /// </summary>
    public static class BlockCompress
    {
        /// <summary>
        /// Contains information about compression state and size.
        /// </summary>
        public record struct CompressionInfo(bool IsCompressed, int Size);

        /// <summary>
        /// Gets the decompressed size from a binary reader.
        /// </summary>
        public static CompressionInfo GetDecompressedSize(BinaryReader reader)
        {
            var decompressedSize = reader.ReadUInt32();

            // Valve sets fourth byte in the compressed buffer to 0x80 to indicate that the data is uncompressed,
            // 0x80000000 is 2147483648 which automatically makes any number higher than max signed 32-bit integer.
            if (decompressedSize > int.MaxValue)
            {
                return new(false, (int)(decompressedSize & 0x7FFFFFFF));
            }

            return new(true, (int)decompressedSize);
        }

        /// <summary>
        /// Performs fast decompression of block-compressed data into the result buffer.
        /// </summary>
        public static void FastDecompress(CompressionInfo info, BinaryReader reader, Span<byte> result)
        {
            ArgumentOutOfRangeException.ThrowIfLessThan(result.Length, info.Size);

            if (!info.IsCompressed)
            {
                reader.Read(result[0..info.Size]);
                return;
            }

            var position = 0;
            ushort blockMask = 0;
            var i = 0;

            while (position < info.Size)
            {
                if (i == 0)
                {
                    blockMask = reader.ReadUInt16();
                    i = 16;
                }

                if ((blockMask & 1) > 0)
                {
                    var offsetSize = reader.ReadUInt16();
                    var offset = (offsetSize >> 4) + 1;
                    var size = (offsetSize & 0xF) + 3;
                    var positionSource = position - offset;

                    if (offset == 1)
                    {
                        while (size-- > 0)
                        {
                            result[position++] = result[positionSource];
                        }
                    }
                    else
                    {
                        while (size-- > 0)
                        {
                            result[position++] = result[positionSource++];
                        }
                    }
                }
                else
                {
                    result[position++] = reader.ReadByte();
                }

                blockMask >>= 1;
                i--;
            }
        }
    }
}