Editor/HumanoidRetargeter/Embedded/ValveResourceFormat/Serialization/KeyValues/KV3TextSerializer.cs
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using HumanoidRetargeterVrf.Utils;
using System.Globalization;
using System.Runtime.CompilerServices;
using KVValueType = HumanoidRetargeterKeyValue.KVValueType;

namespace HumanoidRetargeterVrf.Serialization.KeyValues;

internal static class KV3TextSerializer
{
    public static void WriteValue(KVValue value, IndentedTextWriter writer)
    {
        var Type = value.Type;
        var Flag = value.Flag;
        var Value = value.Value;

        if (Flag != KVFlag.None)
        {
            switch (Flag)
            {
                case KVFlag.Resource:
                    writer.Write("resource:");
                    break;
                case KVFlag.ResourceName:
                    writer.Write("resource_name:");
                    break;
                case KVFlag.Panorama:
                    writer.Write("panorama:");
                    break;
                case KVFlag.SoundEvent:
                    writer.Write("soundevent:");
                    break;
                case KVFlag.SubClass:
                    writer.Write("subclass:");
                    break;
                case KVFlag.EntityName:
                    writer.Write("entity_name:");
                    break;
                default:
                    throw new InvalidOperationException($"Trying to print unknown keyvalues flag ({Flag})");
            }
        }

        switch (Type)
        {
            case KVValueType.Collection:
            case KVValueType.Array:
                ((KVObject)Value!).Serialize(writer);
                break;
            case KVValueType.String:
                {
                    var text = (string)Value!;
                    var isMultiline = text.Contains('\n', StringComparison.Ordinal);

                    if (isMultiline)
                    {
                        writer.Write("\"\"\"\n");
                        writer.Write(text);
                        writer.Write("\n\"\"\"");
                    }
                    else
                    {
                        WriteEscapedString(text, writer);
                    }
                    break;
                }
            case KVValueType.Boolean:
                writer.Write((bool)Value! ? "true" : "false");
                break;
            case KVValueType.FloatingPoint:
                writer.Write(Convert.ToSingle(Value, CultureInfo.InvariantCulture).ToString("#0.000000", CultureInfo.InvariantCulture));
                break;
            case KVValueType.FloatingPoint64:
                writer.Write(Convert.ToDouble(Value, CultureInfo.InvariantCulture).ToString("#0.000000", CultureInfo.InvariantCulture));
                break;
            case KVValueType.Int64:
                writer.Write(Convert.ToInt64(Value, CultureInfo.InvariantCulture));
                break;
            case KVValueType.UInt64:
                writer.Write(Convert.ToUInt64(Value, CultureInfo.InvariantCulture));
                break;
            case KVValueType.Int32:
                writer.Write(Convert.ToInt32(Value, CultureInfo.InvariantCulture));
                break;
            case KVValueType.UInt32:
                writer.Write(Convert.ToUInt32(Value, CultureInfo.InvariantCulture));
                break;
            case KVValueType.Int16:
                writer.Write(Convert.ToInt16(Value, CultureInfo.InvariantCulture));
                break;
            case KVValueType.UInt16:
                writer.Write(Convert.ToUInt16(Value, CultureInfo.InvariantCulture));
                break;
            case KVValueType.Null:
                writer.Write("null");
                break;
            case KVValueType.BinaryBlob:
                var byteArray = (byte[])Value!;
                var count = 0;

                {
                    // This might be longer than required
                    var lines = byteArray.Length / 32;
                    var size = 12 + byteArray.Length * 3;
                    size += (writer.Indent + 1) * lines;
                    size += Environment.NewLine.Length * lines;
                    writer.Grow(size);
                }

                writer.WriteLine();
                writer.WriteLine("#[");
                writer.Indent++;

                foreach (var oneByte in byteArray)
                {
                    writer.Write(HexToCharUpper(oneByte >> 4));
                    writer.Write(HexToCharUpper(oneByte));

                    if (++count % 32 == 0)
                    {
                        writer.WriteLine();
                    }
                    else
                    {
                        writer.Write(' ');
                    }
                }

                writer.Indent--;

                if (count % 32 != 0)
                {
                    writer.WriteLine();
                }

                writer.Write("]");
                break;
            default:
                throw new InvalidOperationException($"Trying to print unknown type '{Type}'");
        }
    }

    private static void WriteEscapedString(string input, IndentedTextWriter writer)
    {
        writer.Grow(input.Length + 2);
        writer.Write("\"");

        foreach (var c in input)
        {
            switch (c)
            {
                case '\n':
                    writer.Write("\\n");
                    break;
                case '\t':
                    writer.Write("\\t");
                    break;
                case '\\':
                    writer.Write("\\\\");
                    break;
                case '"':
                    writer.Write("\\\"");
                    break;
                default:
                    writer.Write(c);
                    break;
            }
        }

        writer.Write("\"");
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static char HexToCharUpper(int value)
    {
        value &= 0xF;
        value += '0';

        if (value > '9')
        {
            value += ('A' - ('9' + 1));
        }

        return (char)value;
    }
}