Editor/HumanoidRetargeter/Embedded/Resource.cs
// Adapted from ValveResourceFormat 17.0 Resource.cs (MIT); see ValveResourceFormat/LICENSE.
// Only the resource blocks needed for model and animgraph recovery are dispatched here.
#nullable enable
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using HumanoidRetargeterVrf.Blocks;
using HumanoidRetargeterVrf.ResourceTypes;

namespace HumanoidRetargeterVrf;

public sealed class Resource : IDisposable
{
    public BinaryReader Reader { get; private set; } = null!;
    public string FileName { get; set; } = "";
    public ushort Version { get; private set; }
    public List<Block> Blocks { get; } = new();
    public ResourceEditInfo? EditInfo => Blocks.OfType<ResourceEditInfo>().FirstOrDefault();
    public ResourceExtRefList? ExternalReferences => GetBlockByType(BlockType.RERL) as ResourceExtRefList;
    public Block? DataBlock => GetBlockByType(BlockType.DATA);
    public Block? GetBlockByType(BlockType type) => Blocks.FirstOrDefault(b => b.Type == type);
    public Block GetBlockByIndex(int index) => Blocks[index];
    public bool ContainsBlockType(BlockType type) => Blocks.Any(b => b.Type == type);

    public void Read(string file)
    {
        if (string.IsNullOrEmpty(FileName)) FileName = file;
        Reader = new BinaryReader(File.OpenRead(file));
        var size = Reader.ReadUInt32();
        if (size > Reader.BaseStream.Length || size < 16 || Reader.ReadUInt16() != 12)
            throw new InvalidDataException("Not a supported Source 2 resource header.");
        Version = Reader.ReadUInt16();
        var offset = Reader.ReadUInt32();
        var count = Reader.ReadUInt32();
        if (count > 4096 || offset + 8L + count * 12L > size) throw new InvalidDataException("Invalid resource block table.");
        Reader.BaseStream.Position = offset + 8;
        for (var i = 0; i < count; i++)
        {
            var name = System.Text.Encoding.ASCII.GetString(Reader.ReadBytes(4));
            var start = Reader.BaseStream.Position;
            var relative = Reader.ReadUInt32();
            var length = Reader.ReadUInt32();
            if (start + relative + length > size) throw new InvalidDataException("Resource block is out of bounds.");
            if (!Enum.TryParse<BlockType>(name, out var type)) type = BlockType.Undefined;
            Block block = type switch
            {
                BlockType.RERL => new ResourceExtRefList(),
                BlockType.NTRO => new ResourceIntrospectionManifest(),
                BlockType.REDI => new ResourceEditInfo(),
                BlockType.RED2 => new ResourceEditInfo2(),
                BlockType.VBIB => new VBIB(),
                BlockType.MBUF => new MBUF(),
                BlockType.MDAT => new Mesh(BlockType.MDAT),
                BlockType.PHYS => new PhysAggregateData(BlockType.PHYS),
                BlockType.MRPH => new Morph(BlockType.MRPH),
                BlockType.CTRL or BlockType.INSG => new BinaryKV3(type),
                BlockType.ANIM => new KeyValuesOrNTRO(type, "AnimationResourceData_t"),
                BlockType.AGRP => new KeyValuesOrNTRO(type, "AnimationGroupResourceData_t"),
                BlockType.ASEQ => new KeyValuesOrNTRO(type, "SequenceGroupResourceData_t"),
                BlockType.DATA when FileName.EndsWith(".vmdl", StringComparison.OrdinalIgnoreCase) || FileName.EndsWith(".vmdl_c", StringComparison.OrdinalIgnoreCase) => new Model(),
                BlockType.DATA when FileName.EndsWith(".vmesh", StringComparison.OrdinalIgnoreCase) || FileName.EndsWith(".vmesh_c", StringComparison.OrdinalIgnoreCase) => new Mesh(BlockType.DATA),
                BlockType.DATA when FileName.EndsWith(".vmat", StringComparison.OrdinalIgnoreCase) || FileName.EndsWith(".vmat_c", StringComparison.OrdinalIgnoreCase) => new Material(),
                BlockType.DATA when FileName.EndsWith(".vphys", StringComparison.OrdinalIgnoreCase) || FileName.EndsWith(".vphys_c", StringComparison.OrdinalIgnoreCase) => new PhysAggregateData(),
                BlockType.DATA => new KeyValuesOrNTRO(),
                _ => new OpaqueBlock(type)
            };
            block.Offset = checked((uint)(start + relative)); block.Size = length; block.Resource = this;
            Blocks.Add(block);
        }
        // Introspection and dependencies must be available before interpreting DATA.
        foreach (var block in Blocks.Where(b => b.Type is BlockType.NTRO or BlockType.RERL or BlockType.REDI or BlockType.RED2)) block.Read(Reader);
        foreach (var block in Blocks.Where(b => b.Type is not (BlockType.NTRO or BlockType.RERL or BlockType.REDI or BlockType.RED2 or BlockType.DATA))) block.Read(Reader);
        DataBlock?.Read(Reader);
    }

    public void Dispose() => Reader?.Dispose();

    sealed class OpaqueBlock(BlockType type) : Block
    {
        public override BlockType Type => type;
        public override void Read(BinaryReader reader) { }
        public override void WriteText(IndentedTextWriter writer) => throw new NotSupportedException();
        public override void Serialize(Stream stream) => throw new NotSupportedException();
    }
}