Editor/HumanoidRetargeter/Embedded/ValveResourceFormat/Resource/ResourceTypes/BinaryKV3.cs
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using HumanoidRetargeterVrf.Utils;
using System.Buffers;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using HumanoidRetargeterCompression;

using HumanoidRetargeterVrf.Serialization.KeyValues;
using KVValueType = HumanoidRetargeterKeyValue.KVValueType;

#nullable disable

namespace HumanoidRetargeterVrf.ResourceTypes
{
    /// <summary>
    /// Represents a binary KeyValues3 data block.
    /// </summary>
    public partial class BinaryKV3 : Block
    {
        private readonly BlockType KVBlockType;

        /// <inheritdoc/>
        public override BlockType Type => KVBlockType;

        /// <summary>
        /// Magic number for VKV3 format.
        /// </summary>
        public const int MAGIC0 = 0x03564B56; // VKV3 (3 isn't ascii, its 0x03)

        /// <summary>
        /// Magic number for KV3 version 1.
        /// </summary>
        public const int MAGIC1 = 0x4B563301; // KV3\x01

        /// <summary>
        /// Magic number for KV3 version 2.
        /// </summary>
        public const int MAGIC2 = 0x4B563302; // KV3\x02

        /// <summary>
        /// Magic number for KV3 version 3.
        /// </summary>
        public const int MAGIC3 = 0x4B563303; // KV3\x03

        /// <summary>
        /// Magic number for KV3 version 4.
        /// </summary>
        public const int MAGIC4 = 0x4B563304; // KV3\x04

        /// <summary>
        /// Magic number for KV3 version 5.
        /// </summary>
        public const int MAGIC5 = 0x4B563305; // KV3\x05

        /// <summary>
        /// Checks if the given magic number represents a binary KV3 format.
        /// </summary>
        /// <param name="magic">The magic number to check.</param>
        /// <returns>True if the magic number is a valid binary KV3 format.</returns>
        public static bool IsBinaryKV3(uint magic) => magic is MAGIC0 or MAGIC1 or MAGIC2 or MAGIC3 or MAGIC4 or MAGIC5;

        /// <summary>
        /// Gets the deserialized KeyValues3 data.
        /// </summary>
        public KVObject Data { get; private set; }

        /// <summary>
        /// Gets the encoding identifier for this KV3 data.
        /// </summary>
        public KV3ID? Encoding { get; private set; }

        /// <summary>
        /// Gets the format identifier for this KV3 data.
        /// </summary>
        public KV3ID Format { get; private set; }

        private class Buffers
        {
            public ArraySegment<byte> Bytes1;
            public ArraySegment<byte> Bytes2;
            public ArraySegment<byte> Bytes4;
            public ArraySegment<byte> Bytes8;
        }

        private class Context
        {
            public int Version;
            public ArraySegment<byte> Types;
            public ArraySegment<byte> ObjectLengths;
            public ArraySegment<byte> BinaryBlobs;
            public ArraySegment<byte> BinaryBlobLengths;
            public string[] Strings;
            public Buffers Buffer;
            public Buffers AuxiliaryBuffer;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="BinaryKV3"/> class with DATA block type.
        /// </summary>
        public BinaryKV3()
        {
            KVBlockType = BlockType.DATA;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="BinaryKV3"/> class with the specified block type.
        /// </summary>
        /// <param name="type">The block type.</param>
        public BinaryKV3(BlockType type)
        {
            KVBlockType = type;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="BinaryKV3"/> class with the specified data and format.
        /// </summary>
        /// <param name="data">The KeyValues3 data.</param>
        /// <param name="format">The format identifier.</param>
        /// <param name="blockType">The block type.</param>
        public BinaryKV3(KVObject data, KV3ID format, BlockType blockType = BlockType.Undefined)
        {
            KVBlockType = blockType;
            Data = data;
            Format = format;
        }

        /// <inheritdoc/>
        public override void Read(BinaryReader reader)
        {
            if (KVBlockType != BlockType.Undefined)
            {
                reader.BaseStream.Position = Offset;
            }

            var magic = reader.ReadUInt32();

            if (magic == MAGIC0)
            {
                ReadVersion0(reader);
                return;
            }

            var version = magic & 0xFF;
            magic &= 0xFFFFFF00;

            if (magic != 0x4B563300)
            {
                throw new UnexpectedMagicException("Unsupported KV3 signature", magic, nameof(magic));
            }

            if (version < 1 || version > 5)
            {
                throw new UnexpectedMagicException("Unsupported KV3 version", version, nameof(version));
            }

            ReadBuffer((int)version, reader);
        }

        private static void DecompressLZ4(BinaryReader reader, Span<byte> output, int compressedSize)
        {
            var inputBuf = ArrayPool<byte>.Shared.Rent(compressedSize);

            try
            {
                var input = inputBuf.AsSpan(0, compressedSize);
                reader.Read(input);

                var written = Lz4.Decode(input, output);

                if (written != output.Length)
                {
                    throw new InvalidDataException($"Failed to decompress LZ4 (expected {output.Length} bytes, got {written})");
                }
            }
            finally
            {
                ArrayPool<byte>.Shared.Return(inputBuf);
            }
        }

        private static void DecompressZSTD(HumanoidRetargeterZstd.Decompressor zstdDecompressor, BinaryReader reader, Span<byte> output, int compressedSize)
        {
            var inputBuf = ArrayPool<byte>.Shared.Rent(compressedSize);

            try
            {
                var input = inputBuf.AsSpan(0, compressedSize);
                reader.Read(input);

                if (!zstdDecompressor.TryUnwrap(input, output, out var written) || output.Length != written)
                {
                    throw new InvalidDataException($"Failed to decompress ZSTD (expected {output.Length} bytes, got {written})");
                }
            }
            finally
            {
                ArrayPool<byte>.Shared.Return(inputBuf);
            }
        }

        private void ReadBuffer(int version, BinaryReader reader)
        {
            var context = new Context
            {
                Version = version,
            };

            Format = KV3IDLookup.GetByValue(new Guid(reader.ReadBytes(16)));

            var compressionMethod = reader.ReadUInt32();
            ushort compressionDictionaryId = 0;
            ushort compressionFrameSize = 0;
            var countBytes1 = 0;
            var countBytes4 = 0;
            var countBytes8 = 0;
            var countTypes = 0;
            var countObjects = 0;
            var countArrays = 0;
            var sizeUncompressedTotal = 0;
            var sizeCompressedTotal = 0;
            var countBlocks = 0;
            var sizeBinaryBlobsBytes = 0;

            if (version == 1)
            {
                // Version 1 did not have extra compression data
                countBytes1 = reader.ReadInt32();
                countBytes4 = reader.ReadInt32();
                countBytes8 = reader.ReadInt32();
                sizeUncompressedTotal = reader.ReadInt32();

                sizeCompressedTotal = (int)(Size - (reader.BaseStream.Position - Offset));
            }
            else
            {
                compressionDictionaryId = reader.ReadUInt16();
                compressionFrameSize = reader.ReadUInt16();
                countBytes1 = reader.ReadInt32();
                countBytes4 = reader.ReadInt32();
                countBytes8 = reader.ReadInt32();
                countTypes = reader.ReadInt32();
                countObjects = reader.ReadUInt16();
                countArrays = reader.ReadUInt16();
                sizeUncompressedTotal = reader.ReadInt32();
                sizeCompressedTotal = reader.ReadInt32();
                countBlocks = reader.ReadInt32();
                sizeBinaryBlobsBytes = reader.ReadInt32();
            }

            var countBytes2 = 0;
            var sizeBlockCompressedSizesBytes = 0;

            if (version >= 4)
            {
                countBytes2 = reader.ReadInt32();
                sizeBlockCompressedSizesBytes = reader.ReadInt32();
            }

            var sizeUncompressedBuffer1 = 0;
            var sizeCompressedBuffer1 = 0;
            var sizeUncompressedBuffer2 = 0;
            var sizeCompressedBuffer2 = 0;
            var countBytes1_buffer2 = 0;
            var countBytes2_buffer2 = 0;
            var countBytes4_buffer2 = 0;
            var countBytes8_buffer2 = 0;
            var countObjects_buffer2 = 0;
            var countArrays_buffer2 = 0;

            if (version >= 5)
            {
                sizeUncompressedBuffer1 = reader.ReadInt32();
                sizeCompressedBuffer1 = reader.ReadInt32();
                sizeUncompressedBuffer2 = reader.ReadInt32();
                sizeCompressedBuffer2 = reader.ReadInt32();
                countBytes1_buffer2 = reader.ReadInt32();
                countBytes2_buffer2 = reader.ReadInt32();
                countBytes4_buffer2 = reader.ReadInt32();
                countBytes8_buffer2 = reader.ReadInt32();
                var unk13 = reader.ReadInt32();
                countObjects_buffer2 = reader.ReadInt32();
                countArrays_buffer2 = reader.ReadInt32();
                var unk16 = reader.ReadInt32();

                Debug.Assert(sizeUncompressedTotal == sizeUncompressedBuffer1 + sizeUncompressedBuffer2);
            }
            else
            {
                sizeCompressedBuffer1 = sizeCompressedTotal;
                sizeUncompressedBuffer1 = sizeUncompressedTotal;
            }

            var buffer1Raw = ArrayPool<byte>.Shared.Rent(version < 5 && compressionMethod == 2 ? sizeUncompressedBuffer1 + sizeBinaryBlobsBytes : sizeUncompressedBuffer1);
            byte[] buffer2Raw = null;
            byte[] binaryBlobsRaw = null;
            HumanoidRetargeterZstd.Decompressor zstdDecompressor = null;

            try
            {
                ArraySegment<byte> bufferWithBinaryBlobSizes = null;

                // Buffer 1
                {
                    var buffer1Span = new ArraySegment<byte>(buffer1Raw, 0, sizeUncompressedBuffer1);

                    if (compressionMethod == 0) // uncompressed
                    {
                        if (compressionDictionaryId != 0)
                        {
                            throw new UnexpectedMagicException("Unhandled", compressionDictionaryId, nameof(compressionDictionaryId));
                        }

                        if (compressionFrameSize != 0)
                        {
                            throw new UnexpectedMagicException("Unhandled", compressionFrameSize, nameof(compressionFrameSize));
                        }

                        if (version >= 5)
                        {
                            Debug.Assert(sizeCompressedBuffer1 == 0);
                        }
                        else
                        {
                            Debug.Assert(sizeCompressedBuffer1 == sizeUncompressedBuffer1);
                        }

                        reader.Read(buffer1Span);
                    }
                    else if (compressionMethod == 1) // LZ4
                    {
                        if (compressionDictionaryId != 0)
                        {
                            throw new UnexpectedMagicException("Unhandled", compressionDictionaryId, nameof(compressionDictionaryId));
                        }

                        if (compressionFrameSize != 16384 && version >= 2)
                        {
                            throw new UnexpectedMagicException("Unhandled", compressionFrameSize, nameof(compressionFrameSize));
                        }

                        Debug.Assert(sizeCompressedBuffer1 > 0);

                        DecompressLZ4(reader, buffer1Span, sizeCompressedBuffer1);
                    }
                    else if (compressionMethod == 2) // ZSTD
                    {
                        Debug.Assert(version >= 2);

                        if (compressionDictionaryId != 0)
                        {
                            throw new UnexpectedMagicException("Unhandled", compressionDictionaryId, nameof(compressionDictionaryId));
                        }

                        if (compressionFrameSize != 0)
                        {
                            throw new UnexpectedMagicException("Unhandled", compressionFrameSize, nameof(compressionFrameSize));
                        }

                        Debug.Assert(sizeCompressedBuffer1 > 0);

                        var outBufferLength = sizeUncompressedBuffer1;

                        // Before version 5, when using zstd, both the buffer and binary blobs were compressed together
                        if (version < 5)
                        {
                            outBufferLength += sizeBinaryBlobsBytes;
                        }

                        zstdDecompressor = new HumanoidRetargeterZstd.Decompressor();

                        DecompressZSTD(zstdDecompressor, reader, buffer1Raw.AsSpan(0, outBufferLength), sizeCompressedBuffer1);
                    }
                    else
                    {
                        throw new UnexpectedMagicException("Unknown compression method", compressionMethod, nameof(compressionMethod));
                    }

                    var buffer1 = new Buffers();

                    var offset = 0;

                    if (countBytes1 > 0)
                    {
                        var end = offset + countBytes1;
                        buffer1.Bytes1 = buffer1Span[offset..end];
                        offset = end;
                    }

                    if (countBytes2 > 0)
                    {
                        Align(ref offset, 2);

                        var end = offset + countBytes2 * 2;
                        buffer1.Bytes2 = buffer1Span[offset..end];
                        offset = end;
                    }

                    if (countBytes4 > 0)
                    {
                        Align(ref offset, 4);

                        var end = offset + countBytes4 * 4;
                        buffer1.Bytes4 = buffer1Span[offset..end];
                        offset = end;
                    }

                    if (countBytes8 > 0)
                    {
                        Align(ref offset, 8);

                        var end = offset + countBytes8 * 8;
                        buffer1.Bytes8 = buffer1Span[offset..end];
                        offset = end;
                    }
                    else if (version < 5)
                    {
                        // For some reason V5 does not align this when empty, but earlier versions did
                        Align(ref offset, 8);
                    }

                    Debug.Assert(countBytes4 > 0); // should be guaranteed to be at least 1 for the strings count

                    var countStrings = MemoryMarshal.Read<int>(buffer1.Bytes4);
                    buffer1.Bytes4 = buffer1.Bytes4[sizeof(int)..];
                    context.Strings = new string[countStrings];

                    if (version >= 5)
                    {
                        context.AuxiliaryBuffer = buffer1;

                        var readStringBytes = 0;

                        for (var i = 0; i < countStrings; i++)
                        {
                            context.Strings[i] = ReadNullTermUtf8String(ref buffer1.Bytes1, ref readStringBytes);
                        }

                        Debug.Assert(buffer1Span.Count == offset);
                    }
                    else
                    {
                        context.Buffer = buffer1;

                        var stringsBuffer = buffer1Span[offset..];
                        var stringsStartOffset = offset;

                        for (var i = 0; i < countStrings; i++)
                        {
                            context.Strings[i] = ReadNullTermUtf8String(ref stringsBuffer, ref offset);
                        }

                        // Types before v5
                        int typesLength;

                        if (version == 1)
                        {
                            typesLength = sizeUncompressedTotal - offset - 4;
                        }
                        else
                        {
                            typesLength = countTypes - offset + stringsStartOffset;
                        }

                        context.Types = buffer1Span[offset..(offset + typesLength)];
                        offset += typesLength;

                        if (countBlocks == 0)
                        {
                            var trailer = MemoryMarshal.Read<uint>(buffer1Span[offset..]);
                            offset += 4;
                            UnexpectedMagicException.Assert(trailer == 0xFFEEDD00, trailer);

                            Debug.Assert(buffer1Span.Count == offset);
                        }
                        else
                        {
                            bufferWithBinaryBlobSizes = buffer1Span[offset..];
                        }
                    }
                }

                // Buffer 2
                if (version >= 5)
                {
                    buffer2Raw = ArrayPool<byte>.Shared.Rent(sizeUncompressedBuffer2);
                    var buffer2Span = new ArraySegment<byte>(buffer2Raw, 0, sizeUncompressedBuffer2);

                    if (compressionMethod == 0) // uncompressed
                    {
                        Debug.Assert(sizeCompressedBuffer2 == 0);

                        reader.Read(buffer2Span);
                    }
                    else if (compressionMethod == 1) // LZ4
                    {
                        Debug.Assert(sizeCompressedBuffer2 > 0);

                        DecompressLZ4(reader, buffer2Span, sizeCompressedBuffer2);
                    }
                    else if (compressionMethod == 2) // ZSTD
                    {
                        Debug.Assert(sizeCompressedBuffer2 > 0);

                        zstdDecompressor ??= new HumanoidRetargeterZstd.Decompressor();

                        DecompressZSTD(zstdDecompressor, reader, buffer2Span, sizeCompressedBuffer2);
                    }
                    else
                    {
                        throw new UnexpectedMagicException("Unknown compression method", compressionMethod, nameof(compressionMethod));
                    }

                    var buffer2 = new Buffers();
                    context.Buffer = buffer2;

                    var end = countObjects_buffer2 * sizeof(int);
                    var offset = end;

                    context.ObjectLengths = buffer2Span[..end];

                    if (countBytes1_buffer2 > 0)
                    {
                        end = offset + countBytes1_buffer2;
                        buffer2.Bytes1 = buffer2Span[offset..end];
                        offset = end;
                    }

                    if (countBytes2_buffer2 > 0)
                    {
                        Align(ref offset, 2);

                        end = offset + countBytes2_buffer2 * 2;
                        buffer2.Bytes2 = buffer2Span[offset..end];
                        offset = end;
                    }

                    if (countBytes4_buffer2 > 0)
                    {
                        Align(ref offset, 4);

                        end = offset + countBytes4_buffer2 * 4;
                        buffer2.Bytes4 = buffer2Span[offset..end];
                        offset = end;
                    }

                    if (countBytes8_buffer2 > 0)
                    {
                        Align(ref offset, 8);

                        end = offset + countBytes8_buffer2 * 8;
                        buffer2.Bytes8 = buffer2Span[offset..end];
                        offset = end;
                    }

                    // Types in v5
                    context.Types = buffer2Span[offset..(offset + countTypes)];
                    offset += countTypes;

                    if (countBlocks == 0)
                    {
                        var trailer = MemoryMarshal.Read<uint>(buffer2Span[offset..]);
                        offset += 4;
                        UnexpectedMagicException.Assert(trailer == 0xFFEEDD00, trailer);
                    }
                    else
                    {
                        bufferWithBinaryBlobSizes = buffer2Span[offset..];
                    }
                }

                if (countBlocks > 0)
                {
                    Debug.Assert(version >= 2);
                    Debug.Assert(bufferWithBinaryBlobSizes != null);

                    {
                        var end = countBlocks * sizeof(int);
                        context.BinaryBlobLengths = bufferWithBinaryBlobSizes[..end];
                        bufferWithBinaryBlobSizes = bufferWithBinaryBlobSizes[end..];

                        var trailer = MemoryMarshal.Read<uint>(bufferWithBinaryBlobSizes);
                        bufferWithBinaryBlobSizes = bufferWithBinaryBlobSizes[sizeof(int)..];
                        UnexpectedMagicException.Assert(trailer == 0xFFEEDD00, trailer);
                    }

                    if (compressionMethod == 0) // Uncompressed
                    {
                        binaryBlobsRaw = ArrayPool<byte>.Shared.Rent(sizeBinaryBlobsBytes);
                        context.BinaryBlobs = new ArraySegment<byte>(binaryBlobsRaw, 0, sizeBinaryBlobsBytes);
                        reader.Read(context.BinaryBlobs);
                    }
                    else if (compressionMethod == 1) // LZ4
                    {
                        binaryBlobsRaw = ArrayPool<byte>.Shared.Rent(sizeBinaryBlobsBytes);
                        context.BinaryBlobs = new ArraySegment<byte>(binaryBlobsRaw, 0, sizeBinaryBlobsBytes);

                        using var lz4decoder = new Lz4Chain(compressionFrameSize, 0);

                        var decompressedOffset = 0;

                        while (bufferWithBinaryBlobSizes.Count > 0)
                        {
                            var compressedBlockLength = MemoryMarshal.Read<ushort>(bufferWithBinaryBlobSizes);
                            bufferWithBinaryBlobSizes = bufferWithBinaryBlobSizes[sizeof(ushort)..];

                            var inputBuf = ArrayPool<byte>.Shared.Rent(compressedBlockLength);

                            try
                            {
                                var decodedFrameSize = decompressedOffset + compressionFrameSize > sizeBinaryBlobsBytes ? sizeBinaryBlobsBytes - decompressedOffset : compressionFrameSize;
                                var output = context.BinaryBlobs.AsSpan(decompressedOffset, decodedFrameSize);

                                var input = inputBuf.AsSpan(0, compressedBlockLength);
                                reader.Read(input);

                                if (!lz4decoder.DecodeAndDrain(input, output, out var decoded) || decoded < 1)
                                {
                                    throw new InvalidOperationException("LZ4 decode drain failed, this is likely a bug.");
                                }

                                decompressedOffset += decoded;
                            }
                            finally
                            {
                                ArrayPool<byte>.Shared.Return(inputBuf);
                            }
                        }
                    }
                    else if (compressionMethod == 2) // ZSTD
                    {
                        if (version >= 5)
                        {
                            UnexpectedMagicException.Assert(sizeBlockCompressedSizesBytes == 0, sizeBlockCompressedSizesBytes);

                            var sizeCompressedBinaryBlobs = sizeCompressedTotal - sizeCompressedBuffer1 - sizeCompressedBuffer2;

                            binaryBlobsRaw = ArrayPool<byte>.Shared.Rent(sizeBinaryBlobsBytes);
                            context.BinaryBlobs = new ArraySegment<byte>(binaryBlobsRaw, 0, sizeBinaryBlobsBytes);

                            zstdDecompressor ??= new HumanoidRetargeterZstd.Decompressor();

                            DecompressZSTD(zstdDecompressor, reader, context.BinaryBlobs, sizeCompressedBinaryBlobs);
                        }
                        else
                        {
                            // This is supposed to be a streaming decompress using ZSTD_decompressStream,
                            // but as it turns out, zstd unwrap above already decompressed all of the blocks for us.
                            // It's possible that Valve's code needs extra decompress because they set ZSTD_d_stableOutBuffer parameter.
                            context.BinaryBlobs = new ArraySegment<byte>(buffer1Raw, sizeUncompressedBuffer1, sizeBinaryBlobsBytes);
                        }
                    }
                    else
                    {
                        throw new UnexpectedMagicException("Unsupported compression method in block decoder", compressionMethod, nameof(compressionMethod));
                    }

                    {
                        var trailer = reader.ReadUInt32();
                        UnexpectedMagicException.Assert(trailer == 0xFFEEDD00, trailer);
                    }
                }

                Data = ParseBinaryKV3(context, null, true);

                Debug.Assert(context.Types.Count == 0);
                Debug.Assert(context.ObjectLengths.Count == 0);
                Debug.Assert(context.BinaryBlobs.Count == 0);
                Debug.Assert(context.BinaryBlobLengths.Count == 0);
                Debug.Assert(context.Buffer.Bytes1.Count == 0);
                Debug.Assert(context.Buffer.Bytes2.Count == 0);
                Debug.Assert(context.Buffer.Bytes4.Count == 0);
                Debug.Assert(context.Buffer.Bytes8.Count == 0);

                if (version >= 5)
                {
                    Debug.Assert(context.AuxiliaryBuffer.Bytes1.Count == 0);
                    Debug.Assert(context.AuxiliaryBuffer.Bytes2.Count == 0);
                    Debug.Assert(context.AuxiliaryBuffer.Bytes4.Count == 0);
                    Debug.Assert(context.AuxiliaryBuffer.Bytes8.Count == 0);
                }
            }
            finally
            {
                ArrayPool<byte>.Shared.Return(buffer1Raw);

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

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

                zstdDecompressor?.Dispose();
            }
        }

        private static (KV3BinaryNodeType Type, KVFlag Flag) ReadType(Context context)
        {
            var databyte = context.Types[0];
            context.Types = context.Types[1..];
            var flagInfo = KVFlag.None;

            if (context.Version >= 3)
            {
                if ((databyte & 0x80) > 0)
                {
                    databyte &= 0x3F; // Remove the flag bit

                    flagInfo = (KVFlag)context.Types[0];
                    context.Types = context.Types[1..];

                    if (flagInfo > KVFlag.MaxPersistedFlag)
                    {
                        throw new UnexpectedMagicException("Unexpected kv3 flag", (int)flagInfo, nameof(flagInfo));
                    }
                }
            }
            else if ((databyte & 0x80) > 0) // TODO: Valve's new code also checks for 0x40 even for old kv3 version
            {
                databyte &= 0x7F; // Remove the flag bit

                flagInfo = (KVFlag)context.Types[0];
                context.Types = context.Types[1..];

                if (((int)flagInfo & 4) > 0) // Multiline string
                {
                    Debug.Assert(databyte == (int)KV3BinaryNodeType.STRING);
                    flagInfo ^= (KVFlag)4;
                }

                // Strictly speaking there could be more than one flag set, but in practice it was seemingly never.
                // Valve's new code just sets whichever flag is highest, new kv3 version does not support multiple flags at once.
                flagInfo = (int)flagInfo switch
                {
                    0 => KVFlag.None,
                    1 => KVFlag.Resource,
                    2 => KVFlag.ResourceName,
                    8 => KVFlag.Panorama,
                    16 => KVFlag.SoundEvent,
                    32 => KVFlag.SubClass,
                    _ => throw new UnexpectedMagicException("Unexpected kv3 flag", (int)flagInfo, nameof(flagInfo))
                };
            }

            return ((KV3BinaryNodeType)databyte, flagInfo);
        }

        private static KVObject ParseBinaryKV3(Context context, KVObject parent, bool inArray = false)
        {
            string name = null;
            if (!inArray)
            {
                var stringID = MemoryMarshal.Read<int>(context.Buffer.Bytes4);
                context.Buffer.Bytes4 = context.Buffer.Bytes4[sizeof(int)..];

                name = (stringID == -1) ? string.Empty : context.Strings[stringID];
            }

            var (datatype, flagInfo) = ReadType(context);

            return ReadBinaryValue(context, name, datatype, flagInfo, parent);
        }

        private static KVObject ReadBinaryValue(Context context, string name, KV3BinaryNodeType datatype, KVFlag flagInfo, KVObject parent)
        {
            // We don't support non-object roots properly, so this is a hack to handle "null" kv3
            if (datatype != KV3BinaryNodeType.OBJECT && parent == null)
            {
                name ??= "root";
                parent ??= new KVObject(name);
            }

            var buffer = context.Buffer;

            switch (datatype)
            {
                // Hardcoded values
                case KV3BinaryNodeType.NULL:
                    parent.AddProperty(name, MakeValue(datatype, null, flagInfo));
                    break;
                case KV3BinaryNodeType.BOOLEAN_TRUE:
                    parent.AddProperty(name, MakeValue(datatype, true, flagInfo));
                    break;
                case KV3BinaryNodeType.BOOLEAN_FALSE:
                    parent.AddProperty(name, MakeValue(datatype, false, flagInfo));
                    break;
                case KV3BinaryNodeType.INT64_ZERO:
                    parent.AddProperty(name, MakeValue(datatype, 0L, flagInfo));
                    break;
                case KV3BinaryNodeType.INT64_ONE:
                    parent.AddProperty(name, MakeValue(datatype, 1L, flagInfo));
                    break;
                case KV3BinaryNodeType.DOUBLE_ZERO:
                    parent.AddProperty(name, MakeValue(datatype, 0.0D, flagInfo));
                    break;
                case KV3BinaryNodeType.DOUBLE_ONE:
                    parent.AddProperty(name, MakeValue(datatype, 1.0D, flagInfo));
                    break;

                // 1 byte values
                case KV3BinaryNodeType.BOOLEAN:
                    {
                        var value = buffer.Bytes1[0] == 1;
                        buffer.Bytes1 = buffer.Bytes1[1..];

                        parent.AddProperty(name, MakeValue(datatype, value, flagInfo));
                    }
                    break;
                // TODO: 22 might be INT32_AS_BYTE, and 23 is UINT32_AS_BYTE
                case KV3BinaryNodeType.INT32_AS_BYTE:
                    {
                        Debug.Assert(context.Version >= 4);

                        var value = (int)buffer.Bytes1[0];
                        buffer.Bytes1 = buffer.Bytes1[1..];

                        parent.AddProperty(name, MakeValue(datatype, value, flagInfo));
                    }
                    break;

                // 2 byte values
                case KV3BinaryNodeType.INT16:
                    {
                        Debug.Assert(context.Version >= 4);

                        var value = MemoryMarshal.Read<short>(buffer.Bytes2);
                        buffer.Bytes2 = buffer.Bytes2[sizeof(short)..];

                        parent.AddProperty(name, MakeValue(datatype, value, flagInfo));
                    }
                    break;
                case KV3BinaryNodeType.UINT16:
                    {
                        Debug.Assert(context.Version >= 4);

                        var value = MemoryMarshal.Read<ushort>(buffer.Bytes2);
                        buffer.Bytes2 = buffer.Bytes2[sizeof(ushort)..];

                        parent.AddProperty(name, MakeValue(datatype, value, flagInfo));
                    }
                    break;

                // 4 byte values
                case KV3BinaryNodeType.INT32:
                    {
                        var value = MemoryMarshal.Read<int>(buffer.Bytes4);
                        buffer.Bytes4 = buffer.Bytes4[sizeof(int)..];

                        parent.AddProperty(name, MakeValue(datatype, value, flagInfo));
                    }
                    break;
                case KV3BinaryNodeType.UINT32:
                    {
                        var value = MemoryMarshal.Read<uint>(buffer.Bytes4);
                        buffer.Bytes4 = buffer.Bytes4[sizeof(uint)..];

                        parent.AddProperty(name, MakeValue(datatype, value, flagInfo));
                    }
                    break;
                case KV3BinaryNodeType.FLOAT:
                    {
                        Debug.Assert(context.Version >= 4);

                        var value = MemoryMarshal.Read<float>(buffer.Bytes4);
                        buffer.Bytes4 = buffer.Bytes4[sizeof(float)..];

                        parent.AddProperty(name, MakeValue(datatype, value, flagInfo));
                    }
                    break;

                // 8 byte values
                case KV3BinaryNodeType.INT64:
                    {
                        var value = MemoryMarshal.Read<long>(buffer.Bytes8);
                        buffer.Bytes8 = buffer.Bytes8[sizeof(long)..];

                        parent.AddProperty(name, MakeValue(datatype, value, flagInfo));
                    }
                    break;
                case KV3BinaryNodeType.UINT64:
                    {
                        var value = MemoryMarshal.Read<ulong>(buffer.Bytes8);
                        buffer.Bytes8 = buffer.Bytes8[sizeof(ulong)..];

                        parent.AddProperty(name, MakeValue(datatype, value, flagInfo));
                    }
                    break;
                case KV3BinaryNodeType.DOUBLE:
                    {
                        var value = MemoryMarshal.Read<double>(buffer.Bytes8);
                        buffer.Bytes8 = buffer.Bytes8[sizeof(double)..];

                        parent.AddProperty(name, MakeValue(datatype, value, flagInfo));
                    }
                    break;

                // Custom types
                case KV3BinaryNodeType.STRING:
                    {
                        var id = MemoryMarshal.Read<int>(buffer.Bytes4);
                        buffer.Bytes4 = buffer.Bytes4[sizeof(int)..];

                        parent.AddProperty(name, MakeValue(datatype, id == -1 ? string.Empty : context.Strings[id], flagInfo));
                    }
                    break;
                case KV3BinaryNodeType.BINARY_BLOB when context.Version < 2:
                    {
                        var blockLength = MemoryMarshal.Read<int>(buffer.Bytes4);
                        buffer.Bytes4 = buffer.Bytes4[sizeof(int)..];
                        byte[] output;

                        if (blockLength > 0)
                        {
                            output = [.. buffer.Bytes1[..blockLength]]; // explicit copy
                            buffer.Bytes1 = buffer.Bytes1[blockLength..];
                        }
                        else
                        {
                            output = [];
                        }

                        parent.AddProperty(name, MakeValue(datatype, output, flagInfo));
                    }
                    break;
                case KV3BinaryNodeType.BINARY_BLOB:
                    {
                        var blockLength = MemoryMarshal.Read<int>(context.BinaryBlobLengths);
                        context.BinaryBlobLengths = context.BinaryBlobLengths[sizeof(int)..];
                        byte[] output;

                        if (blockLength > 0)
                        {
                            output = [.. context.BinaryBlobs[..blockLength]]; // explicit copy
                            context.BinaryBlobs = context.BinaryBlobs[blockLength..];
                        }
                        else
                        {
                            output = [];
                        }

                        parent.AddProperty(name, MakeValue(datatype, output, flagInfo));
                    }
                    break;
                case KV3BinaryNodeType.ARRAY:
                    {
                        var arrayLength = MemoryMarshal.Read<int>(buffer.Bytes4);
                        buffer.Bytes4 = buffer.Bytes4[sizeof(int)..];

                        var array = new KVObject(name, isArray: true, capacity: arrayLength);

                        for (var i = 0; i < arrayLength; i++)
                        {
                            ParseBinaryKV3(context, array, true);
                        }

                        parent.AddProperty(name, MakeValue(datatype, array, flagInfo));
                    }
                    break;
                case KV3BinaryNodeType.ARRAY_TYPED:
                case KV3BinaryNodeType.ARRAY_TYPE_BYTE_LENGTH:
                    {
                        int arrayLength;

                        if (datatype == KV3BinaryNodeType.ARRAY_TYPE_BYTE_LENGTH)
                        {
                            arrayLength = buffer.Bytes1[0];
                            buffer.Bytes1 = buffer.Bytes1[1..];
                        }
                        else
                        {
                            arrayLength = MemoryMarshal.Read<int>(buffer.Bytes4);
                            buffer.Bytes4 = buffer.Bytes4[sizeof(int)..];
                        }

                        var (subType, subFlagInfo) = ReadType(context);
                        var typedArray = new KVObject(name, isArray: true, capacity: arrayLength);

                        for (var i = 0; i < arrayLength; i++)
                        {
                            ReadBinaryValue(context, name, subType, subFlagInfo, typedArray);
                        }

                        parent.AddProperty(name, MakeValue(datatype, typedArray, flagInfo));
                    }
                    break;
                case KV3BinaryNodeType.ARRAY_TYPE_AUXILIARY_BUFFER:
                    {
                        Debug.Assert(context.Version >= 5);

                        var arrayLength = buffer.Bytes1[0];
                        buffer.Bytes1 = buffer.Bytes1[1..];

                        var (subType, subFlagInfo) = ReadType(context);
                        var typedArray = new KVObject(name, isArray: true, capacity: arrayLength);

                        // Swap the buffers and simply call read again instead of reimplementing the switch here
                        (context.AuxiliaryBuffer, context.Buffer) = (context.Buffer, context.AuxiliaryBuffer);

                        for (var i = 0; i < arrayLength; i++)
                        {
                            ReadBinaryValue(context, name, subType, subFlagInfo, typedArray);
                        }

                        (context.AuxiliaryBuffer, context.Buffer) = (context.Buffer, context.AuxiliaryBuffer);

                        parent.AddProperty(name, MakeValue(datatype, typedArray, flagInfo));
                    }
                    break;

                case KV3BinaryNodeType.OBJECT:
                    {
                        int objectLength;

                        if (context.Version >= 5)
                        {
                            objectLength = MemoryMarshal.Read<int>(context.ObjectLengths);
                            context.ObjectLengths = context.ObjectLengths[sizeof(int)..];
                        }
                        else
                        {
                            objectLength = MemoryMarshal.Read<int>(buffer.Bytes4);
                            buffer.Bytes4 = buffer.Bytes4[sizeof(int)..];
                        }

                        var newObject = new KVObject(name, isArray: false, capacity: objectLength);

                        for (var i = 0; i < objectLength; i++)
                        {
                            ParseBinaryKV3(context, newObject, false);
                        }

                        if (parent == null)
                        {
                            parent = newObject;
                        }
                        else
                        {
                            parent.AddProperty(name, MakeValue(datatype, newObject, flagInfo));
                        }
                    }
                    break;
                default:
                    throw new UnexpectedMagicException($"Unknown KVType for field '{name}'", (int)datatype, nameof(datatype));
            }

            return parent;
        }

        private static KVValueType ConvertBinaryOnlyKVType(KV3BinaryNodeType type)
        {
            // TODO: Why we are upcasting (u)int32 to 64
#pragma warning disable IDE0066 // Convert switch statement to expression
            switch (type)
            {
                case KV3BinaryNodeType.BOOLEAN:
                case KV3BinaryNodeType.BOOLEAN_TRUE:
                case KV3BinaryNodeType.BOOLEAN_FALSE:
                    return KVValueType.Boolean;
                case KV3BinaryNodeType.INT16:
                    return KVValueType.Int16;
                case KV3BinaryNodeType.UINT16:
                    return KVValueType.UInt16;
                case KV3BinaryNodeType.INT64:
                case KV3BinaryNodeType.INT32:
                case KV3BinaryNodeType.INT64_ZERO:
                case KV3BinaryNodeType.INT64_ONE:
                case KV3BinaryNodeType.INT32_AS_BYTE:
                    return KVValueType.Int64;
                case KV3BinaryNodeType.UINT64:
                case KV3BinaryNodeType.UINT32:
                    return KVValueType.UInt64;
                case KV3BinaryNodeType.FLOAT:
                    return KVValueType.FloatingPoint;
                case KV3BinaryNodeType.DOUBLE:
                case KV3BinaryNodeType.DOUBLE_ZERO:
                case KV3BinaryNodeType.DOUBLE_ONE:
                    return KVValueType.FloatingPoint64;
                case KV3BinaryNodeType.ARRAY:
                case KV3BinaryNodeType.ARRAY_TYPED:
                case KV3BinaryNodeType.ARRAY_TYPE_BYTE_LENGTH:
                case KV3BinaryNodeType.ARRAY_TYPE_AUXILIARY_BUFFER:
                    return KVValueType.Array;
                case KV3BinaryNodeType.OBJECT:
                    return KVValueType.Collection;
                case KV3BinaryNodeType.STRING:
                    return KVValueType.String;
                case KV3BinaryNodeType.BINARY_BLOB:
                    return KVValueType.BinaryBlob;
                case KV3BinaryNodeType.NULL:
                    return KVValueType.Null;
                default:
                    throw new NotImplementedException($"Unknown type {type}");
            }
#pragma warning restore IDE0066 // Convert switch statement to expression
        }

        private static KVValue MakeValue(KV3BinaryNodeType type, object data, KVFlag flag = KVFlag.None)
        {
            var realType = ConvertBinaryOnlyKVType(type);
            return new KVValue(realType, flag, data);
        }

        /// <summary>
        /// Gets the KeyValues3 data as a KV3File object.
        /// </summary>
        /// <returns>A KV3File object containing the data and format.</returns>
#pragma warning disable CA1024 // Use properties where appropriate
        public KV3File GetKV3File()
#pragma warning restore CA1024 // Use properties where appropriate
        {
            return new KV3File(Data, format: Format);
        }

        /// <inheritdoc/>
        /// <remarks>
        /// Converts the binary KV3 data to text format and writes it.
        /// </remarks>
        public override void WriteText(IndentedTextWriter writer)
        {
            GetKV3File().WriteText(writer);
        }

        private static string ReadNullTermUtf8String(ref ArraySegment<byte> buffer, ref int offset)
        {
            var nullByte = buffer.AsSpan().IndexOf((byte)0);
            var str = buffer[..nullByte];
            buffer = buffer[(nullByte + 1)..];

            offset += nullByte + 1;

            return System.Text.Encoding.UTF8.GetString(str);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static void Align(ref int offset, int alignment)
        {
            alignment -= 1;
            offset += alignment;
            offset &= ~alignment;
        }

        /// <summary>
        /// Converts binary KV3 data to text format. This method is exposed for unmanaged callers.
        /// </summary>
        /// <param name="dataPtr">Pointer to the binary KV3 data.</param>
        /// <param name="dataLength">Length of the binary data.</param>
        /// <returns>Pointer to the text representation of the KV3 data.</returns>
    }
}