using System;
using System.Buffers;
using System.Buffers.Binary;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using AyCode.Core.Helpers;
using AyCode.Core.Serializers.Expressions;
using static AyCode.Core.Helpers.JsonUtilities;

namespace AyCode.Core.Serializers.Binaries;

/// <summary>
/// High-performance binary deserializer matching AcBinarySerializer.
/// Features:
/// - VarInt decoding for compact integers
/// - String intern table lookup
/// - Property name table for fast property resolution
/// - Reference resolution for circular/shared references
/// - Populate/Merge mode support
/// - Optimized with FrozenDictionary for type dispatch
/// - Zero-allocation hot paths using Span and MemoryMarshal
/// </summary>
public static partial class AcBinaryDeserializer
{
    /// <summary>
    /// Diagnostic logger for deserializer-level debugging (DEBUG builds only).
    /// Set to non-null to log SequenceBinaryInput vs ArrayBinaryInput verification results.
    /// </summary>
    public static Action<string>? DiagnosticLogger { get; set; }

    private static readonly ConcurrentDictionary<Type, TypeConversionInfo> TypeConversionCache = new();

    /// <summary>
    /// Thread-safe registry for generated readers. Looked up in ReadObjectCore to bypass runtime path.
    /// </summary>
    internal static class GeneratedReaderRegistry
    {
        private static readonly ConcurrentDictionary<Type, IGeneratedBinaryReader> Readers = new();

        internal static void Register(Type type, IGeneratedBinaryReader reader) => Readers[type] = reader;

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static IGeneratedBinaryReader? TryGet(Type type) => Readers.GetValueOrDefault(type);
    }

    /// <summary>
    /// Registers a source-generated binary reader for the specified type.
    /// Once registered, ReadObjectCore bypasses the runtime wrapper/property loop
    /// and calls the generated reader directly.
    /// </summary>
    internal static void RegisterGeneratedReader(Type type, IGeneratedBinaryReader reader)
    {
        ArgumentNullException.ThrowIfNull(type);
        ArgumentNullException.ThrowIfNull(reader);
        GeneratedReaderRegistry.Register(type, reader);
    }

    /// <summary>
    /// ThreadLocal cache for type conversion info.
    /// </summary>
    [ThreadStatic]
    private static Dictionary<Type, TypeConversionInfo>? t_typeConversionLocalCache;

    // Type dispatch table for fast ReadValue — generic per TInput, JIT specializes each
    private delegate object? TypeReader<TInput>(BinaryDeserializationContext<TInput> context, Type targetType)
        where TInput : struct, IBinaryInputBase;

    private static class TypeReaderTable<TInput> where TInput : struct, IBinaryInputBase
    {
        public static readonly TypeReader<TInput>?[] Readers = InitReaders();

        private static TypeReader<TInput>?[] InitReaders()
        {
            var readers = new TypeReader<TInput>?[byte.MaxValue + 1];
            readers[BinaryTypeCode.Null] = static (_, _) => null;
            readers[BinaryTypeCode.True] = static (_, _) => true;
            readers[BinaryTypeCode.False] = static (_, _) => false;
            readers[BinaryTypeCode.Int8] = static (ctx, _) => (sbyte)ctx.ReadByte();
            readers[BinaryTypeCode.UInt8] = static (ctx, _) => ctx.ReadByte();
            readers[BinaryTypeCode.Int16] = static (ctx, _) => ctx.ReadInt16Unsafe();
            readers[BinaryTypeCode.UInt16] = static (ctx, _) => ctx.ReadUInt16Unsafe();
            readers[BinaryTypeCode.Int32] = static (ctx, type) => ReadInt32Value(ctx, type);
            readers[BinaryTypeCode.UInt32] = static (ctx, _) => ctx.ReadVarUInt();
            readers[BinaryTypeCode.Int64] = static (ctx, _) => ctx.ReadVarLong();
            readers[BinaryTypeCode.UInt64] = static (ctx, _) => ctx.ReadVarULong();
            readers[BinaryTypeCode.Float32] = static (ctx, _) => ctx.ReadSingleUnsafe();
            readers[BinaryTypeCode.Float64] = static (ctx, _) => ctx.ReadDoubleUnsafe();
            readers[BinaryTypeCode.Decimal] = static (ctx, _) => ctx.ReadDecimalUnsafe();
            readers[BinaryTypeCode.Char] = static (ctx, _) => ctx.ReadCharUnsafe();
            // Marker 91 now represents StringUtf16. Decoding is centralized in context.ReadStringUtf16Marker()
            // which keeps FastWire payload and compact legacy-compat behavior in one place.
            readers[BinaryTypeCode.StringUtf16] = static (ctx, _) => ctx.ReadStringUtf16Marker();
            readers[BinaryTypeCode.StringInterned] = static (ctx, _) => ctx.GetInternedString((int)ctx.ReadVarUInt());
            readers[BinaryTypeCode.StringEmpty] = static (_, _) => string.Empty;
            // H2Q6 interning tier readers (Compact mode only — Big tier never engages on interning path)
            readers[BinaryTypeCode.StringInternFirstSmall] = static (ctx, _) => ctx.ReadAndRegisterInternedStringSmall();
            readers[BinaryTypeCode.StringInternFirstMedium] = static (ctx, _) => ctx.ReadAndRegisterInternedStringMedium();
            readers[BinaryTypeCode.String] = static (ctx, _) => ctx.ReadUniversalLongString();
            readers[BinaryTypeCode.DateTime] = static (ctx, _) => ctx.ReadDateTimeUnsafe();
            readers[BinaryTypeCode.DateTimeOffset] = static (ctx, _) => ctx.ReadDateTimeOffsetUnsafe();
            readers[BinaryTypeCode.TimeSpan] = static (ctx, _) => ctx.ReadTimeSpanUnsafe();
            readers[BinaryTypeCode.Guid] = static (ctx, _) => ctx.ReadGuidUnsafe();
            readers[BinaryTypeCode.Enum] = static (ctx, type) => ReadEnumValue(ctx, type);
            readers[BinaryTypeCode.Object] = ReadObject;
            readers[BinaryTypeCode.ObjectRefFirst] = ReadObjectRefFirst;
            readers[BinaryTypeCode.ObjectWithMetadata] = ReadObjectWithMetadata;
            readers[BinaryTypeCode.ObjectWithMetadataRefFirst] = ReadObjectWithMetadataRefFirst;
            readers[BinaryTypeCode.ObjectRef] = ReadObjectRef;
            readers[BinaryTypeCode.ObjectWithTypeName] = ReadObjectWithTypeName;
            readers[BinaryTypeCode.ObjectWithTypeNameRefFirst] = ReadObjectWithTypeNameRefFirst;
            readers[BinaryTypeCode.ObjectWithTypeIndex] = ReadObjectWithTypeIndex;
            readers[BinaryTypeCode.ObjectWithTypeIndexRefFirst] = ReadObjectWithTypeIndexRefFirst;
            readers[BinaryTypeCode.Array] = ReadArray;
            readers[BinaryTypeCode.Dictionary] = ReadDictionary;
            readers[BinaryTypeCode.ByteArray] = static (ctx, _) => ReadByteArray(ctx);

            // V4N5 cleanup (2026-05-06): FixStr (UTF-8 short non-ASCII, 103..134) range REMOVED.
            // Non-ASCII short strings now use StringSmall tier marker (registered above).

            // Register FixStr readers (135..166) — universal short-form markers in the new
            // signed-excess layout; the per-marker charLength lives in the marker nibble/range.
            for (var code = BinaryTypeCode.FixStrBase; code <= BinaryTypeCode.FixStrMax; code++)
            {
                var length = BinaryTypeCode.DecodeFixStrLength(code);
                readers[code] = CreateFixStrReader<TInput>(code, length);
            }

            // Register FixObj slot readers (0..SlotCount-1)
            for (var slot = 0; slot < BinaryTypeCode.SlotCount; slot++) readers[slot] = CreateFixObjReader<TInput>(slot);
            
            return readers;
        }
    }


    // V4N5 cleanup note updated: compact string payloads now route through FixStr/String universals;
    // marker 91 is StringUtf16 (FastWire) with compact legacy compatibility.

    /// <summary>
    /// Creates a reader for the short-form universal string marker. Char length comes from the marker;
    /// payload codec is selected by the signed excess slot read by ReadUniversalFixStr.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static TypeReader<TInput> CreateFixStrReader<TInput>(byte marker, int length) where TInput : struct, IBinaryInputBase
    {
        if (length == 0) return static (_, _) => string.Empty;

        return (ctx, _) => ctx.ReadUniversalFixStr(marker);
    }

    /// <summary>
    /// Creates a reader for FixObj slot (0..SlotCount-1).
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static TypeReader<TInput> CreateFixObjReader<TInput>(int slot) where TInput : struct, IBinaryInputBase
    {
        return (ctx, targetType) => ReadObjectFromSlot(ctx, slot, targetType);
    }

    //private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);

    #region Public API

    /// <summary>
    /// Deserialize binary data to object of type T.
    /// </summary>
    /// <remarks>
    /// NativeAOT contract: <typeparamref name="T"/> is annotated with DAMs <c>PublicParameterlessConstructor | PublicProperties</c>
    /// so the trimmer preserves the constructor + property reflection metadata required by the runtime
    /// reflection path (<c>Activator.CreateInstance</c>, <c>Type.GetConstructor</c>, <c>PropertyInfo.GetValue/SetValue</c>).
    /// Without this annotation the runtime path throws <see cref="MissingMethodException"/> under AOT publish.
    /// The SGen path (when <typeparamref name="T"/> has <c>[AcBinarySerializable]</c>) does not rely on
    /// reflection metadata and works regardless — but the annotation is harmless and uniform.
    /// </remarks>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static T? Deserialize<[DynamicallyAccessedMembers(TypeMetadataBase.RequiredMembers)] T>(byte[] data)
        => Deserialize<T>(data, AcBinarySerializerOptions.Default);

    /// <summary>
    /// Deserialize binary data to object of type T with options.
    /// Zero-copy: ArrayBinaryInput references the byte[] directly.
    /// </summary>
    /// <inheritdoc cref="Deserialize{T}(byte[])" path="/remarks"/>
    public static T? Deserialize<[DynamicallyAccessedMembers(TypeMetadataBase.RequiredMembers)] T>(byte[] data, AcBinarySerializerOptions options)
    {
        if (data.Length == 0) return default;
        if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return default;

        var targetType = typeof(T);
        if (AcSerializerCommon.IsExpressionType(targetType))
            return (T?)(object?)DeserializeExpression(data, targetType, options);

        var context = DeserializationContextPool<ArrayBinaryInput>.Get(options);
        context.InitInput(new ArrayBinaryInput(data));

        try { return (T?)DeserializeCore(context, targetType); }
        finally { DeserializationContextPool<ArrayBinaryInput>.Return(context); }
    }

    /// <summary>
    /// Deserialize binary data to object of type T from a sub-range of a byte[].
    /// Zero-copy: ArrayBinaryInput references the byte[] directly with offset.
    /// </summary>
    /// <inheritdoc cref="Deserialize{T}(byte[])" path="/remarks"/>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static T? Deserialize<[DynamicallyAccessedMembers(TypeMetadataBase.RequiredMembers)] T>(byte[] data, int offset, int length)
        => Deserialize<T>(data, offset, length, AcBinarySerializerOptions.Default);

    /// <summary>
    /// Deserialize binary data to object of type T from a sub-range with options.
    /// Zero-copy: ArrayBinaryInput references the byte[] directly with offset.
    /// </summary>
    /// <inheritdoc cref="Deserialize{T}(byte[])" path="/remarks"/>
    public static T? Deserialize<[DynamicallyAccessedMembers(TypeMetadataBase.RequiredMembers)] T>(byte[] data, int offset, int length, AcBinarySerializerOptions options)
    {
        if (length == 0) return default;
        if (length == 1 && data[offset] == BinaryTypeCode.Null) return default;

        var targetType = typeof(T);
        if (AcSerializerCommon.IsExpressionType(targetType)) return (T?)(object?)DeserializeExpression(data, offset, length, targetType, options);

        var context = DeserializationContextPool<ArrayBinaryInput>.Get(options);
        context.InitInput(new ArrayBinaryInput(data, offset, length));

        try { return (T?)DeserializeCore(context, targetType); }
        finally { DeserializationContextPool<ArrayBinaryInput>.Return(context); }
    }

    /// <summary>
    /// Deserialize binary data to specified type.
    /// </summary>
    /// <inheritdoc cref="Deserialize{T}(byte[])" path="/remarks"/>
    public static object? Deserialize(byte[] data, [DynamicallyAccessedMembers(TypeMetadataBase.RequiredMembers)] Type targetType)
        => Deserialize(data, 0, data.Length, targetType, AcBinarySerializerOptions.Default);

    /// <summary>
    /// Deserialize binary data to specified type with options.
    /// </summary>
    /// <inheritdoc cref="Deserialize{T}(byte[])" path="/remarks"/>
    public static object? Deserialize(byte[] data, [DynamicallyAccessedMembers(TypeMetadataBase.RequiredMembers)] Type targetType, AcBinarySerializerOptions options)
        => Deserialize(data, 0, data.Length, targetType, options);

    /// <summary>
    /// Deserialize binary data to specified type from a sub-range.
    /// </summary>
    /// <inheritdoc cref="Deserialize{T}(byte[])" path="/remarks"/>
    public static object? Deserialize(byte[] data, int offset, int length, [DynamicallyAccessedMembers(TypeMetadataBase.RequiredMembers)] Type targetType)
        => Deserialize(data, offset, length, targetType, AcBinarySerializerOptions.Default);

    /// <summary>
    /// Deserialize binary data to specified type from a sub-range with options.
    /// Zero-copy: ArrayBinaryInput references the byte[] directly with offset.
    /// </summary>
    /// <inheritdoc cref="Deserialize{T}(byte[])" path="/remarks"/>
    public static object? Deserialize(byte[] data, int offset, int length, [DynamicallyAccessedMembers(TypeMetadataBase.RequiredMembers)] Type targetType, AcBinarySerializerOptions options)
    {
        if (length == 0) return null;
        if (length == 1 && data[offset] == BinaryTypeCode.Null) return null;

        if (AcSerializerCommon.IsExpressionType(targetType))
            return DeserializeExpression(data, offset, length, targetType, options);

        var context = DeserializationContextPool<ArrayBinaryInput>.Get(options);
        context.InitInput(new ArrayBinaryInput(data, offset, length));
        try { return DeserializeCore(context, targetType); }
        finally { DeserializationContextPool<ArrayBinaryInput>.Return(context); }
    }

    /// <summary>
    /// Deserialize binary data from a ReadOnlySequence (e.g., from SignalR/Pipes).
    /// Single-segment: zero-copy via FirstSpan. Multi-segment: linearize into pooled buffer.
    /// </summary>
    /// <inheritdoc cref="Deserialize{T}(byte[])" path="/remarks"/>
    public static T? Deserialize<[DynamicallyAccessedMembers(TypeMetadataBase.RequiredMembers)] T>(ReadOnlySequence<byte> data)
        => Deserialize<T>(data, AcBinarySerializerOptions.Default);

    /// <summary>
    /// Deserialize binary data from a ReadOnlySequence with options.
    /// Single-segment: zero-copy via FirstSpan. Multi-segment: uses SequenceBinaryInput for true streaming.
    /// </summary>
    /// <inheritdoc cref="Deserialize{T}(byte[])" path="/remarks"/>
    public static T? Deserialize<[DynamicallyAccessedMembers(TypeMetadataBase.RequiredMembers)] T>(ReadOnlySequence<byte> data, AcBinarySerializerOptions options)
    {
        if (data.Length == 0) return default;

        if (data.IsSingleSegment && MemoryMarshal.TryGetArray(data.First, out var seg))
            return Deserialize<T>(seg.Array!, seg.Offset, seg.Count, options);

        VerifyAgainstLinearized(data, typeof(T), options);
        return DeserializeSequence<T, SequenceBinaryInput>(new SequenceBinaryInput(data), typeof(T), options);
    }

    /// <summary>
    /// Deserialize binary data from a ReadOnlySequence to specified type.
    /// </summary>
    /// <inheritdoc cref="Deserialize{T}(byte[])" path="/remarks"/>
    public static object? Deserialize(ReadOnlySequence<byte> data, [DynamicallyAccessedMembers(TypeMetadataBase.RequiredMembers)] Type targetType)
        => Deserialize(data, targetType, AcBinarySerializerOptions.Default);

    /// <summary>
    /// Deserialize binary data from a ReadOnlySequence to specified type with options.
    /// </summary>
    /// <inheritdoc cref="Deserialize{T}(byte[])" path="/remarks"/>
    public static object? Deserialize(ReadOnlySequence<byte> data, [DynamicallyAccessedMembers(TypeMetadataBase.RequiredMembers)] Type targetType, AcBinarySerializerOptions options)
    {
        if (data.Length == 0) return null;

        if (data.IsSingleSegment && MemoryMarshal.TryGetArray(data.First, out var seg2))
            return Deserialize(seg2.Array!, seg2.Offset, seg2.Count, targetType, options);

        VerifyAgainstLinearized(data, targetType, options);
        return DeserializeSequence(new SequenceBinaryInput(data), targetType, options);
    }

    /// <summary>
    /// Deserialize from an <see cref="AsyncPipeReaderInput"/> with streaming pipeline parallelism.
    /// The producer thread feeds chunk data via <see cref="AsyncPipeReaderInput.Feed"/>,
    /// while this method (running on a background thread) deserializes incrementally,
    /// blocking on <see cref="System.Threading.ManualResetEventSlim"/> when data is exhausted.
    ///
    /// <para>Use these overloads for SignalR <c>AsyncSegment</c>-style chunked streaming,
    /// NamedPipe IPC, FileStream, or any other transport that produces an
    /// <see cref="AsyncPipeReaderInput"/>. The internal <see cref="AsyncPipeReaderInputAdapter"/>
    /// struct satisfies the JIT-specialization constraint of the generic deserialization path
    /// without exposing a value-type wrapper to the public API.</para>
    /// </summary>
    public static T? Deserialize<[DynamicallyAccessedMembers(TypeMetadataBase.RequiredMembers)] T>(AsyncPipeReaderInput input)
        => Deserialize<T>(input, AcBinarySerializerOptions.Default);

    /// <inheritdoc cref="Deserialize{T}(AsyncPipeReaderInput)"/>
    public static T? Deserialize<[DynamicallyAccessedMembers(TypeMetadataBase.RequiredMembers)] T>(AsyncPipeReaderInput input, AcBinarySerializerOptions options)
    {
        try
        {
            return DeserializeSequence<T, AsyncPipeReaderInputAdapter>(new AsyncPipeReaderInputAdapter(input), typeof(T), options);
        }
        finally
        {
            // Consumer-side "I'm done with this message" signal — arms _readPos = -1 sentinel so the
            // next AppendToBuffer recycles the buffer to 0 for the next message on the long-lived input.
            // Race-free: the deserializer has finished reading by this point (try block returned),
            // so the producer is free to mutate _writePos / _readPos.
            input.MessageDone();
        }
    }

    /// <inheritdoc cref="Deserialize{T}(AsyncPipeReaderInput)"/>
    public static object? Deserialize(AsyncPipeReaderInput input, [DynamicallyAccessedMembers(TypeMetadataBase.RequiredMembers)] Type targetType, AcBinarySerializerOptions options)
    {
        try
        {
            return DeserializeSequence(new AsyncPipeReaderInputAdapter(input), targetType, options);
        }
        finally
        {
            input.MessageDone();
        }
    }

    /// <summary>
    /// Internal: Deserialize with any TInput (multi-segment or other future input types).
    /// </summary>
    private static T? DeserializeSequence<T, TInput>(TInput input, Type targetType, AcBinarySerializerOptions options)
        where TInput : struct, IBinaryInputBase
    {
        if (AcSerializerCommon.IsExpressionType(targetType))
        {
            var (buf, off, len) = LinearizeSequence(input);
            return Deserialize<T>(buf, off, len, options);
        }

        var context = DeserializationContextPool<TInput>.Get(options);
        context.InitInput(input);
        try { return (T?)DeserializeCore(context, targetType); }
        finally
        {
            context.Input.Release();
            DeserializationContextPool<TInput>.Return(context);
        }
    }

    /// <summary>
    /// Internal: Deserialize with any TInput (multi-segment, non-generic result).
    /// </summary>
    private static object? DeserializeSequence<TInput>(TInput input, Type targetType, AcBinarySerializerOptions options)
        where TInput : struct, IBinaryInputBase
    {
        if (AcSerializerCommon.IsExpressionType(targetType))
        {
            var (buf, off, len) = LinearizeSequence(input);
            return Deserialize(buf, off, len, targetType, options);
        }

        var context = DeserializationContextPool<TInput>.Get(options);
        context.InitInput(input);
        try { return DeserializeCore(context, targetType); }
        finally
        {
            context.Input.Release();
            DeserializationContextPool<TInput>.Return(context);
        }
    }

    /// <summary>
    /// Fallback: linearize a TInput into a contiguous byte[] range (for Expression deserialization).
    /// </summary>
    private static (byte[] buffer, int offset, int length) LinearizeSequence<TInput>(TInput input)
        where TInput : struct, IBinaryInputBase
    {
        input.Initialize(out var buffer, out var position, out var bufferLength);
        return (buffer, position, bufferLength - position);
    }

    /// <summary>
    /// Core deserialization: ReadHeader + ReadValue with unified error handling.
    /// All public Deserialize overloads delegate here after pool/init setup.
    /// </summary>
    private static object? DeserializeCore<TInput>(BinaryDeserializationContext<TInput> context, Type targetType)
        where TInput : struct, IBinaryInputBase
    {
        try
        {
            context.ReadHeader();
            return ReadValue(context, targetType);
        }
        catch (AcBinaryDeserializationException) { throw; }
        catch (Exception ex)
        {
            throw new AcBinaryDeserializationException(
                $"Failed to deserialize binary data to type '{targetType.Name}': {ex.Message}",
                context.Position, targetType, ex);
        }
    }

    /// <summary>
    /// DEBUG-only verification: linearizes multi-segment data to byte[] and deserializes
    /// with ArrayBinaryInput to determine if drift is caused by SequenceBinaryInput or by
    /// the serialized data itself (generated reader / serializer bug).
    ///
    /// Result: VERIFY_OK → SequenceBinaryInput is the culprit.
    ///         VERIFY_FAIL → bug is in serializer or generated reader (data itself is bad).
    /// </summary>
    [Conditional("DEBUG")]
    private static void VerifyAgainstLinearized(ReadOnlySequence<byte> data, Type targetType, AcBinarySerializerOptions options)
    {
        if (DiagnosticLogger == null) return;

        var bytes = data.ToArray();
        var segmentCount = 0;
        foreach (var _ in data) segmentCount++;

        try
        {
            var result = Deserialize(bytes, targetType, options);
            DiagnosticLogger($"[VERIFY_OK] ArrayBinaryInput succeeded for {targetType.Name}, " +
                $"{bytes.Length} bytes, {segmentCount} segments → SequenceBinaryInput is suspect");
        }
        catch (Exception ex)
        {
            DiagnosticLogger($"[VERIFY_FAIL] ArrayBinaryInput ALSO FAILED for {targetType.Name}, " +
                $"{bytes.Length} bytes, {segmentCount} segments: {ex.Message}");
        }
    }

    /// <summary>
    /// Deserialize Expression from binary data.
    /// </summary>
    private static Expression? DeserializeExpression(byte[] data, Type targetExpressionType, AcBinarySerializerOptions options)
        => DeserializeExpression(data, 0, data.Length, targetExpressionType, options);

    /// <summary>
    /// Deserialize Expression from binary data sub-range.
    /// </summary>
    private static Expression? DeserializeExpression(byte[] data, int offset, int length, Type targetExpressionType, AcBinarySerializerOptions options)
    {
        var context = DeserializationContextPool<ArrayBinaryInput>.Get(options);
        context.InitInput(new ArrayBinaryInput(data, offset, length));
        try
        {
            var node = (AcExpressionNode?)DeserializeCore(context, typeof(AcExpressionNode));
            if (node == null) return null;

            var entityType = AcSerializerCommon.GetExpressionEntityType(targetExpressionType);
            return AcExpressionRebuilder.FromNode(node, entityType);
        }
        finally
        {
            DeserializationContextPool<ArrayBinaryInput>.Return(context);
        }
    }

    /// <summary>
    /// Core populate: ReadHeader + dispatch by typeCode. Used by Populate, PopulateMerge, ThenPopulate.
    /// </summary>
    private static void PopulateCore<TInput>(BinaryDeserializationContext<TInput> context, object target)
        where TInput : struct, IBinaryInputBase
    {
        var targetType = target.GetType();
        try
        {
            context.ReadHeader();
            var typeCode = context.PeekByte();

            if (typeCode < BinaryTypeCode.SlotCount)
            {
                // FixObj slot: marker byte is the slot index
                context.ReadByte();
                context.GetWrapper(targetType, typeCode);
                if (typeCode >= context._nextRuntimeSlot)
                    context._nextRuntimeSlot = typeCode + 1;
                PopulateObject(context, target, targetType, 0);
            }
            else if (typeCode == BinaryTypeCode.Object)
            {
                context.ReadByte();
                PopulateObject(context, target, targetType, 0);
            }
            else if (typeCode == BinaryTypeCode.ObjectRefFirst)
            {
                // ObjectRefFirst: [marker][VarUInt cacheIndex][props...]
                // Read marker + cacheIndex, register target in intern cache, then populate
                context.ReadByte();
                var cacheIndex = (int)context.ReadVarUInt();
                context.RegisterInternedValueAt(cacheIndex, target);
                PopulateObject(context, target, targetType, 0);
            }
            else if (typeCode == BinaryTypeCode.ObjectWithMetadata)
            {
                context.ReadByte();
                ReadInlineMetadataForPopulate(context, targetType);
                PopulateObject(context, target, targetType, 0);
            }
            else if (typeCode == BinaryTypeCode.ObjectWithMetadataRefFirst)
            {
                // ObjectWithMetadataRefFirst: [marker][VarUInt cacheIndex][metadata...][props...]
                context.ReadByte();
                var cacheIndex = (int)context.ReadVarUInt();
                context.RegisterInternedValueAt(cacheIndex, target);
                ReadInlineMetadataForPopulate(context, targetType);
                PopulateObject(context, target, targetType, 0);
            }
            else if (typeCode == BinaryTypeCode.Array && target is IList targetList)
            {
                context.ReadByte();
                PopulateList(context, targetList, targetType, 0);
            }
            else
            {
                throw new AcBinaryDeserializationException(
                    $"Cannot populate type '{targetType.Name}' from binary type code {typeCode}",
                    context.Position, targetType);
            }
        }
        catch (AcBinaryDeserializationException) { throw; }
        catch (Exception ex)
        {
            throw new AcBinaryDeserializationException(
                $"Failed to populate object of type '{targetType.Name}': {ex.Message}",
                context.Position, targetType, ex);
        }
    }

    /// <summary>
    /// Populate existing object from binary data.
    /// </summary>
    public static void Populate<T>(byte[] data, T target) where T : class
    {
        ArgumentNullException.ThrowIfNull(target);
        if (data.Length == 0) return;
        if (data.Length == 1 && data[0] == BinaryTypeCode.Null) return;

        var context = DeserializationContextPool<ArrayBinaryInput>.Get(AcBinarySerializerOptions.Default);
        context.InitInput(new ArrayBinaryInput(data));
        try { PopulateCore(context, target); }
        finally { DeserializationContextPool<ArrayBinaryInput>.Return(context); }
    }

    /// <summary>
    /// Populate existing object from binary data with options.
    /// </summary>
    public static void Populate<T>(byte[] data, T target, AcBinarySerializerOptions options) where T : class
        => Populate(data, 0, data.Length, target, options);

    /// <summary>
    /// Populate existing object from binary data sub-range.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void Populate<T>(byte[] data, int offset, int length, T target) where T : class
        => Populate(data, offset, length, target, AcBinarySerializerOptions.Default);

    /// <summary>
    /// Populate existing object from binary data sub-range with options.
    /// Zero-copy: ArrayBinaryInput references the byte[] directly with offset.
    /// </summary>
    public static void Populate<T>(byte[] data, int offset, int length, T target, AcBinarySerializerOptions options) where T : class
    {
        ArgumentNullException.ThrowIfNull(target);
        if (length == 0) return;
        if (length == 1 && data[offset] == BinaryTypeCode.Null) return;

        var context = DeserializationContextPool<ArrayBinaryInput>.Get(options);
        context.InitInput(new ArrayBinaryInput(data, offset, length));
        try { PopulateCore(context, target); }
        finally { DeserializationContextPool<ArrayBinaryInput>.Return(context); }
    }

    /// <summary>
    /// Populate with merge semantics for IId collections from byte[] (zero-copy).
    /// </summary>
    public static void PopulateMerge<T>(byte[] data, T target) where T : class
        => PopulateMerge(data, 0, data.Length, target, AcBinarySerializerOptions.Default);

    /// <summary>
    /// Populate with merge semantics for IId collections from byte[] with options (zero-copy).
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void PopulateMerge<T>(byte[] data, T target, AcBinarySerializerOptions options) where T : class
        => PopulateMerge(data, 0, data.Length, target, options);

    /// <summary>
    /// Populate with merge semantics for IId collections from a sub-range of byte[].
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void PopulateMerge<T>(byte[] data, int offset, int length, T target) where T : class
        => PopulateMerge(data, offset, length, target, AcBinarySerializerOptions.Default);

    /// <summary>
    /// Populate with merge semantics for IId collections from a sub-range with options.
    /// Zero-copy: ArrayBinaryInput references the byte[] directly with offset.
    /// </summary>
    public static void PopulateMerge<T>(byte[] data, int offset, int length, T target, AcBinarySerializerOptions? options) where T : class
    {
        ArgumentNullException.ThrowIfNull(target);
        if (length == 0) return;
        if (length == 1 && data[offset] == BinaryTypeCode.Null) return;

        var opts = options ?? AcBinarySerializerOptions.Default;
        var context = DeserializationContextPool<ArrayBinaryInput>.Get(opts);
        context.InitInput(new ArrayBinaryInput(data, offset, length));
        context.IsMergeMode = true;
        context.RemoveOrphanedItems = opts.RemoveOrphanedItems;

        try
        {
            PopulateMergeCore(context, target);
        }
        finally
        {
            DeserializationContextPool<ArrayBinaryInput>.Return(context);
        }
    }

    /// <summary>
    /// Core merge populate: handles IId collection merge for top-level arrays.
    /// </summary>
    private static void PopulateMergeCore<TInput>(BinaryDeserializationContext<TInput> context, object target)
        where TInput : struct, IBinaryInputBase
    {
        var targetType = target.GetType();
        try
        {
            context.ReadHeader();
            var typeCode = context.PeekByte();

            if (typeCode < BinaryTypeCode.SlotCount)
            {
                // FixObj slot: marker byte is the slot index
                context.ReadByte();
                context.GetWrapper(targetType, typeCode);
                if (typeCode >= context._nextRuntimeSlot)
                    context._nextRuntimeSlot = typeCode + 1;
                PopulateObject(context, target, targetType, 0);
            }
            else if (typeCode == BinaryTypeCode.Object)
            {
                context.ReadByte();
                PopulateObject(context, target, targetType, 0);
            }
            else if (typeCode == BinaryTypeCode.ObjectRefFirst)
            {
                context.ReadByte();
                var cacheIndex = (int)context.ReadVarUInt();
                context.RegisterInternedValueAt(cacheIndex, target);
                PopulateObject(context, target, targetType, 0);
            }
            else if (typeCode == BinaryTypeCode.ObjectWithMetadata)
            {
                context.ReadByte();
                ReadInlineMetadataForPopulate(context, targetType);
                PopulateObject(context, target, targetType, 0);
            }
            else if (typeCode == BinaryTypeCode.ObjectWithMetadataRefFirst)
            {
                context.ReadByte();
                var cacheIndex = (int)context.ReadVarUInt();
                context.RegisterInternedValueAt(cacheIndex, target);
                ReadInlineMetadataForPopulate(context, targetType);
                PopulateObject(context, target, targetType, 0);
            }
            else if (typeCode == BinaryTypeCode.Array && target is IList targetList)
            {
                context.ReadByte();
                var elementType = GetCollectionElementType(targetType);
                if (elementType != null)
                {
                    var wrapper = context.GetWrapper(elementType);
                    var elementMetadata = wrapper.Metadata;

                    if (elementMetadata.IsComplexType && elementMetadata.IsIId && elementMetadata.IdGetter != null)
                    {
                        MergeIIdCollectionWithMetadata(context, targetList, elementType, wrapper, 0);
                        return;
                    }
                }

                PopulateList(context, targetList, targetType, 0);
            }
            else
            {
                throw new AcBinaryDeserializationException(
                    $"Cannot populate type '{targetType.Name}' from binary type code {typeCode}",
                    context.Position, targetType);
            }
        }
        catch (AcBinaryDeserializationException) { throw; }
        catch (Exception ex)
        {
            throw new AcBinaryDeserializationException(
                $"Failed to populate/merge object of type '{targetType.Name}': {ex.Message}",
                context.Position, targetType, ex);
        }
    }

    #endregion

    #region Chain API

    /// <summary>
    /// Create a deserialize chain that parses binary data once and allows multiple deserializations.
    /// Maintains reference identity for IId objects across chain operations.
    /// </summary>
    public static IDeserializeChain<T> CreateDeserializeChain<T>(byte[] data)
        => CreateDeserializeChain<T>(data, AcBinarySerializerOptions.Default);

    /// <summary>
    /// Create a deserialize chain with options.
    /// </summary>
    public static IDeserializeChain<T> CreateDeserializeChain<T>(byte[] data, AcBinarySerializerOptions options)
    {
        if (data.Length == 0 || (data.Length == 1 && data[0] == BinaryTypeCode.Null))
            return EmptyDeserializeChain<T>.Instance;

        var chainTracker = new AcSerializerCommon.ChainReferenceTracker();
        var context = DeserializationContextPool<ArrayBinaryInput>.Get(options);
        context.InitInput(new ArrayBinaryInput(data));
        context.ChainTracker = chainTracker;

        try
        {
            var result = (T?)DeserializeCore(context, typeof(T));
            return new BinaryDeserializeChain<T>(data, options, chainTracker, result);
        }
        finally
        {
            DeserializationContextPool<ArrayBinaryInput>.Return(context);
        }
    }

    #endregion

    #region Chain Implementations

    /// <summary>
    /// Binary implementation of deserialize chain.
    /// Maintains reference identity for IId objects across chain operations.
    /// </summary>
    private sealed class BinaryDeserializeChain<T> : IDeserializeChain<T>
    {
        private readonly byte[] _data;
        private readonly AcBinarySerializerOptions _options;
        private readonly AcSerializerCommon.ChainReferenceTracker _chainTracker;
        private bool _isDisposed;

        public T? Value { get; }

        public BinaryDeserializeChain(byte[] data, AcBinarySerializerOptions options, AcSerializerCommon.ChainReferenceTracker chainTracker, T? value)
        {
            _data = data;
            _options = options;
            _chainTracker = chainTracker;
            Value = value;
        }

        public TResult? ThenDeserialize<TResult>()
        {
            ThrowIfDisposed();

            var context = DeserializationContextPool<ArrayBinaryInput>.Get(_options);
            context.InitInput(new ArrayBinaryInput(_data));
            context.ChainTracker = _chainTracker;
            try { return (TResult?)DeserializeCore(context, typeof(TResult)); }
            finally { DeserializationContextPool<ArrayBinaryInput>.Return(context); }
        }

        public IDeserializeChain<T> ThenPopulate(object target)
        {
            ArgumentNullException.ThrowIfNull(target);
            ThrowIfDisposed();

            var context = DeserializationContextPool<ArrayBinaryInput>.Get(_options);
            context.InitInput(new ArrayBinaryInput(_data));
            context.ChainTracker = _chainTracker;
            try
            {
                PopulateCore(context, target);
                return this;
            }
            finally { DeserializationContextPool<ArrayBinaryInput>.Return(context); }
        }

        private void ThrowIfDisposed()
        {
            ObjectDisposedException.ThrowIf(_isDisposed, this);
        }

        public void Dispose()
        {
            if (_isDisposed) return;
            _isDisposed = true;
            _chainTracker.Clear();
        }
    }

    #endregion

    #region Value Reading

    /// <summary>
    /// Tries to read and set a primitive value directly using typed setters to avoid boxing.
    /// The type code marker byte is already consumed by the caller.
    /// Returns true if handled, false if should fall back to generic path.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static bool TryReadAndSetTypedValue<TInput>(BinaryDeserializationContext<TInput> context, object target, BinaryPropertySetterBase propInfo, byte typeCode)
    where TInput : struct, IBinaryInputBase
    {
        // Only handle if we have a typed setter
        if (propInfo.AccessorType == PropertyAccessorType.Object)
            return false;

        // Handle based on property setter type and incoming data type.
        // The marker byte (typeCode) is already consumed — no ReadByte() needed.
        switch (propInfo.AccessorType)
        {
            case PropertyAccessorType.Int32:
                if (BinaryTypeCode.IsTinyInt(typeCode))
                {
                    propInfo.SetInt32(target, BinaryTypeCode.DecodeTinyInt(typeCode));
                    return true;
                }
                if (typeCode == BinaryTypeCode.Int32)
                {
                    propInfo.SetInt32(target, context.ReadVarInt());
                    return true;
                }
                break;

            case PropertyAccessorType.Int64:
                if (BinaryTypeCode.IsTinyInt(typeCode))
                {
                    propInfo.SetInt64(target, BinaryTypeCode.DecodeTinyInt(typeCode));
                    return true;
                }
                if (typeCode == BinaryTypeCode.Int32)
                {
                    propInfo.SetInt64(target, context.ReadVarInt());
                    return true;
                }
                if (typeCode == BinaryTypeCode.Int64)
                {
                    propInfo.SetInt64(target, context.ReadVarLong());
                    return true;
                }
                break;

            case PropertyAccessorType.Boolean:
                if (typeCode == BinaryTypeCode.True)
                {
                    propInfo.SetBoolean(target, true);
                    return true;
                }
                if (typeCode == BinaryTypeCode.False)
                {
                    propInfo.SetBoolean(target, false);
                    return true;
                }
                break;

            case PropertyAccessorType.Double:
                if (typeCode == BinaryTypeCode.Float64)
                {
                    propInfo.SetDouble(target, context.ReadDoubleUnsafe());
                    return true;
                }
                break;

            case PropertyAccessorType.Single:
                if (typeCode == BinaryTypeCode.Float32)
                {
                    propInfo.SetSingle(target, context.ReadSingleUnsafe());
                    return true;
                }
                break;

            case PropertyAccessorType.Decimal:
                if (typeCode == BinaryTypeCode.Decimal)
                {
                    propInfo.SetDecimal(target, context.ReadDecimalUnsafe());
                    return true;
                }
                break;

            case PropertyAccessorType.DateTime:
                if (typeCode == BinaryTypeCode.DateTime)
                {
                    propInfo.SetDateTime(target, context.ReadDateTimeUnsafe());
                    return true;
                }
                break;

            case PropertyAccessorType.Guid:
                if (typeCode == BinaryTypeCode.Guid)
                {
                    propInfo.SetGuid(target, context.ReadGuidUnsafe());
                    return true;
                }
                break;

            case PropertyAccessorType.Byte:
                if (BinaryTypeCode.IsTinyInt(typeCode))
                {
                    propInfo.SetByte(target, (byte)BinaryTypeCode.DecodeTinyInt(typeCode));
                    return true;
                }
                if (typeCode == BinaryTypeCode.UInt8)
                {
                    propInfo.SetByte(target, context.ReadByte());
                    return true;
                }
                break;

            case PropertyAccessorType.Int16:
                if (BinaryTypeCode.IsTinyInt(typeCode))
                {
                    propInfo.SetInt16(target, (short)BinaryTypeCode.DecodeTinyInt(typeCode));
                    return true;
                }
                if (typeCode == BinaryTypeCode.Int16)
                {
                    propInfo.SetInt16(target, context.ReadInt16Unsafe());
                    return true;
                }
                break;

            case PropertyAccessorType.UInt16:
                if (BinaryTypeCode.IsTinyInt(typeCode))
                {
                    propInfo.SetUInt16(target, (ushort)BinaryTypeCode.DecodeTinyInt(typeCode));
                    return true;
                }
                if (typeCode == BinaryTypeCode.UInt16)
                {
                    propInfo.SetUInt16(target, context.ReadUInt16Unsafe());
                    return true;
                }
                break;

            case PropertyAccessorType.UInt32:
                if (BinaryTypeCode.IsTinyInt(typeCode))
                {
                    propInfo.SetUInt32(target, (uint)BinaryTypeCode.DecodeTinyInt(typeCode));
                    return true;
                }
                if (typeCode == BinaryTypeCode.UInt32)
                {
                    propInfo.SetUInt32(target, context.ReadVarUInt());
                    return true;
                }
                break;

            case PropertyAccessorType.UInt64:
                if (BinaryTypeCode.IsTinyInt(typeCode))
                {
                    propInfo.SetUInt64(target, (ulong)BinaryTypeCode.DecodeTinyInt(typeCode));
                    return true;
                }
                if (typeCode == BinaryTypeCode.UInt64)
                {
                    propInfo.SetUInt64(target, context.ReadVarULong());
                    return true;
                }
                break;

            case PropertyAccessorType.Enum:
                if (typeCode == BinaryTypeCode.Enum)
                {
                    var enumByte = context.ReadByte();
                    int enumValue;
                    if (BinaryTypeCode.IsTinyInt(enumByte))
                        enumValue = BinaryTypeCode.DecodeTinyInt(enumByte);
                    else if (enumByte == BinaryTypeCode.Int32)
                        enumValue = context.ReadVarInt();
                    else
                        return false;
                    propInfo.SetEnumAsInt32(target, enumValue);
                    return true;
                }
                // Enum can also be encoded as TinyInt directly
                if (BinaryTypeCode.IsTinyInt(typeCode))
                {
                    propInfo.SetEnumAsInt32(target, BinaryTypeCode.DecodeTinyInt(typeCode));
                    return true;
                }
                break;

            case PropertyAccessorType.String:
                // FixStr is a range (135-166), can't go in switch — keep as range-check first.
                // Universal short-form marker: marker carries char length and slot selects payload path.
                if (BinaryTypeCode.IsFixStr(typeCode))
                {
                    propInfo.SetValue(target, context.ReadUniversalFixStr(typeCode));
                    return true;
                }
                // Single-value markers — switch lowers to a JIT/AOT jump table for O(1) dispatch
                // (vs. sequential if-chain that branches per non-matching marker).
                switch (typeCode)
                {
                    case BinaryTypeCode.StringUtf16:
                        propInfo.SetValue(target, context.ReadStringUtf16Marker());
                        return true;
                    case BinaryTypeCode.String:
                        propInfo.SetValue(target, context.ReadUniversalLongString());
                        return true;
                    case BinaryTypeCode.StringEmpty:
                        propInfo.SetValue(target, string.Empty);
                        return true;
                    case BinaryTypeCode.StringInterned:
                        propInfo.SetValue(target, context.GetInternedString((int)context.ReadVarUInt()));
                        return true;
                    case BinaryTypeCode.StringInternFirstSmall:
                        propInfo.SetValue(target, context.ReadAndRegisterInternedStringSmall());
                        return true;
                    case BinaryTypeCode.StringInternFirstMedium:
                        propInfo.SetValue(target, context.ReadAndRegisterInternedStringMedium());
                        return true;
                }
                break;
        }

        return false;
    }

    /// <summary>
    /// Bridge for generated readers to call ReadValue for unknown/complex/collection property types.
    /// Reads typeCode + dispatches via TypeReaderTable — same as runtime ReadValue.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    internal static object? ReadValueGenerated<TInput>(BinaryDeserializationContext<TInput> context, Type targetType)
    where TInput : struct, IBinaryInputBase
    {
        return ReadValue(context, targetType);
    }

    /// <summary>
    /// Optimized value reader using FrozenDictionary dispatch table.
    /// </summary>
    private static object? ReadValue<TInput>(BinaryDeserializationContext<TInput> context, Type targetType)
    where TInput : struct, IBinaryInputBase
    {
        if (context.IsAtEnd) return null;

        // FastWire markerless string fast-path — when the dispatch target is a string, the wire is
        // int32 sentinel (no marker byte). Skips the typeCode-read; companion to `WriteStringWithDispatch`
        // FastWire ag (which writes via `WriteStringUtf16Markerless`). Condition order: bool field-load
        // (`FastWire`) first — branch-predictor-stable; `targetType == typeof(string)` ref-equality check
        // (load + cmp) only runs when FastWire is true.
        if (context.FastWire && targetType == typeof(string))
        {
            return context.ReadStringUtf16Markerless();
        }

        var typeCode = context.ReadByte();

        // Handle tiny int first (most common case for small integers, >= 192)
        if (BinaryTypeCode.IsTinyInt(typeCode))
        {
            var intValue = BinaryTypeCode.DecodeTinyInt(typeCode);
            return ConvertToTargetType(intValue, targetType);
        }

        // Handle null
        if (typeCode == BinaryTypeCode.Null) return null;

        // Handle short-form universal marker (FixStr range reused with signed excess slot).
        if (BinaryTypeCode.IsFixStr(typeCode))
        {
            return context.ReadUniversalFixStr(typeCode);
        }
        // H2Q6: non-ASCII short strings now use StringSmall tier (handled below via TypeReaderTable dispatch).

        var reader = TypeReaderTable<TInput>.Readers[typeCode];
        return reader != null ? reader(context, targetType) : throw new AcBinaryDeserializationException($"Unknown type code: {typeCode}", context.Position, targetType);
    }

    /// <summary>
    /// Body-only string read for marker-less paths (polymorphism: assembly-qualified type-name).
    /// Wire format: <c>[VarUInt utf8Len][UTF-8 bytes]</c> — caller already consumed any marker.
    /// Used by <c>ReadObjectWithTypeName</c> / <c>ReadObjectWithTypeNameRefFirst</c> after their
    /// outer marker has been read; symmetric to <see cref="BinaryDeserializationContext{T}.WriteStringUtf8"/>
    /// on the writer side.
    /// </summary>
    [MethodImpl(MethodImplOptions.NoInlining)]
    private static string ReadPlainString<TInput>(BinaryDeserializationContext<TInput> context)
    where TInput : struct, IBinaryInputBase
    {
        var length = (int)context.ReadVarUInt();

        return length == 0 ? string.Empty : context.ReadStringUtf8(length);
    }

    // StringUtf16 / ReadUniversalLongString and ReadAndRegisterInternedStringSmall / Medium
    // (+ the cold ThrowCorruptedBigWire helper) all moved to BinaryDeserializationContext as instance
    // methods — single source of wire-decode shared by TypeReaderTable dispatch, PopulateProperty
    // cross-type path, and the SGen-emitted string-property switch. See
    // `BinaryDeserializationContext.Read.cs` for the implementations.

    ///// <summary>
    ///// Read a string and register it in the intern table for future references.
    ///// </summary>
    //[MethodImpl(MethodImplOptions.AggressiveInlining)]
    //private static string ReadAndInternString<TInput>(BinaryDeserializationContext<TInput> context, int streamPosition)
    //{
    //    var length = (int)context.ReadVarUInt();
    //    if (length == 0) return string.Empty;
    //    var str = context.ReadStringUtf8(length);
    //    // Always register strings that meet the minimum intern length threshold
    //    if (str.Length >= context.MinStringInternLength)
    //    {
    //        context.RegisterInternedString(str, streamPosition);
    //    }

    //    return str;
    //}

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static object ReadInt32Value<TInput>(BinaryDeserializationContext<TInput> context, Type targetType)
    where TInput : struct, IBinaryInputBase
    {
        var value = context.ReadVarInt();
        return ConvertToTargetType(value, targetType);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static object ConvertToTargetType(int value, Type targetType)
    {
        var info = GetConversionInfo(targetType);
        if (info.IsEnum) return Enum.ToObject(info.UnderlyingType, value);

        return info.TypeCode switch
        {
            TypeCode.Int32 => value,
            TypeCode.Int64 => (long)value,
            TypeCode.Int16 => (short)value,
            TypeCode.Byte => (byte)value,
            TypeCode.SByte => (sbyte)value,
            TypeCode.UInt16 => (ushort)value,
            TypeCode.UInt32 => (uint)value,
            TypeCode.UInt64 => (ulong)value,
            TypeCode.Double => (double)value,
            TypeCode.Single => (float)value,
            TypeCode.Decimal => (decimal)value,
            _ => value
        };
    }

    /// <summary>
    /// Gets type conversion info with ThreadLocal caching.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static TypeConversionInfo GetConversionInfo(Type targetType)
    {
        // Fast path: check ThreadLocal cache first
        var localCache = t_typeConversionLocalCache;
        if (localCache != null && localCache.TryGetValue(targetType, out var cached))
        {
            return cached;
        }
        
        // Slow path
        return GetConversionInfoSlow(targetType);
    }
    
    [MethodImpl(MethodImplOptions.NoInlining)]
    private static TypeConversionInfo GetConversionInfoSlow(Type targetType)
    {
        var info = TypeConversionCache.GetOrAdd(targetType, static type =>
        {
            var underlying = Nullable.GetUnderlyingType(type) ?? type;
            return new TypeConversionInfo(underlying, Type.GetTypeCode(underlying), underlying.IsEnum);
        });
        
        // Get or create local cache
        var localCache = t_typeConversionLocalCache ??= new Dictionary<Type, TypeConversionInfo>();

        // Clear when full - reuses internal array, better than new Dictionary()
        if (localCache.Count >= TypeMetadataBase.MaxLocalCacheSize)
        {
            localCache.Clear();
        }
        
        localCache[targetType] = info;
        return info;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static object ReadEnumValue<TInput>(BinaryDeserializationContext<TInput> context, Type targetType)
    where TInput : struct, IBinaryInputBase
    {
        var info = GetConversionInfo(targetType);
        var nextByte = context.ReadByte();

        int intValue;
        if (BinaryTypeCode.IsTinyInt(nextByte))
        {
            intValue = BinaryTypeCode.DecodeTinyInt(nextByte);
        }
        else if (nextByte == BinaryTypeCode.Int32)
        {
            intValue = context.ReadVarInt();
        }
        else
        {
            throw new AcBinaryDeserializationException(
                $"Invalid enum encoding: {nextByte}",
                context.Position, targetType);
        }

        return info.IsEnum ? Enum.ToObject(info.UnderlyingType, intValue) : intValue;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static byte[] ReadByteArray<TInput>(BinaryDeserializationContext<TInput> context)
    where TInput : struct, IBinaryInputBase
    {
        var length = (int)context.ReadVarUInt();

        return length == 0 ? [] : context.ReadBytes(length);
    }

    #endregion

    #region Object Reading

    /// <summary>
    /// Reads an ObjectRef - looks up previously registered object from shared intern cache.
    /// Wire format: [ObjectRef][VarUInt cacheIndex]
    ///
    /// IMPORTANT / BUG FIX TODO:
    /// Cross-type deserialization esetén ha egy objektum SkipObject-tel lett átugorva
    /// (mert a target típuson nincs megfelelő property), de később ObjectRef hivatkozik rá,
    /// az intern cache-ben nincs objektum → exception.
    ///
    /// Megoldás: SkipObject során a stream pozíciót kell beírni a cache-be (boxolt int).
    /// Itt az "is int" check-kel meg kell különböztetni:
    ///   - Ha a cached value valódi objektum → visszaadjuk (jelenlegi működés)
    ///   - Ha a cached value boxolt int (stream pozíció) → reposition + ReadObject a target type-ra,
    ///     majd az eredményt visszaírjuk a cache-be (hogy a következő ref ne olvasson újra)
    ///
    /// Az "is int" check biztonságos, mert az intern cache-be csak string és class instance
    /// kerülhet — egyik sem matchel az "is int"-re. A check az ObjectRef path-ban van,
    /// ami ritka eset (2+ referencia), tehát nem lassítja a hot path-ot.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static object? ReadObjectRef<TInput>(BinaryDeserializationContext<TInput> context, Type targetType)
    where TInput : struct, IBinaryInputBase
    {
        var cacheIndex = (int)context.ReadVarUInt();
        return context.GetInternedObject(cacheIndex);
    }

    /// <summary>
    /// FixObj slot read: marker byte (0..SlotCount-1) is the slot index.
    /// First occurrence: wrapper is null in slot → resolve from targetType, cache in slot.
    /// Subsequent: direct array access (~1-2ns).
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static object? ReadObjectFromSlot<TInput>(BinaryDeserializationContext<TInput> context, int slot, Type targetType) where TInput : struct, IBinaryInputBase
    {
        var wrapper = context.GetWrapper(targetType, slot);

        // Track highest slot used for Clear()
        if (slot >= context._nextRuntimeSlot)
            context._nextRuntimeSlot = slot + 1;

        // SGen fast path (same as ReadObjectCore)
        if (!context.HasMetadata && !context.IsChainMode && context.Options.UseGeneratedCode)
        {
            var generatedReader = wrapper.GeneratedReader;
            if (generatedReader != null)
                return generatedReader.ReadObject(context, cacheIndex: -1);
        }

        return ReadObjectCoreWithWrapper(context, wrapper, cacheIndex: -1);
    }

    /// <summary>
    /// Object olvasása (nem tracked, vagy UseMetadata nélkül).
    /// Wire format: [Object][props...]
    /// </summary>
    private static object? ReadObject<TInput>(BinaryDeserializationContext<TInput> context, Type targetType)
    where TInput : struct, IBinaryInputBase
    {
        return ReadObjectCore(context, targetType, cacheIndex: -1);
    }

    /// <summary>
    /// Object olvasása első előforduláskor (ObjectRefFirst marker).
    /// Wire format: [ObjectRefFirst][VarUInt cacheIndex][props...]
    /// Az objektumot regisztráljuk a cache-be a megadott index-re.
    /// </summary>
    private static object? ReadObjectRefFirst<TInput>(BinaryDeserializationContext<TInput> context, Type targetType)
    where TInput : struct, IBinaryInputBase
    {
        var cacheIndex = (int)context.ReadVarUInt();
        return ReadObjectCore(context, targetType, cacheIndex: cacheIndex);
    }

    /// <summary>
    /// Polymorphic PREFIX marker: declared type ≠ runtime type.
    /// Wire format: [ObjectWithTypeName (68)] [TypeName string] [inner marker: Object/Array/Dict/...] [body...]
    /// Reads the runtime type name, resolves it, registers wrapper in poly slot cache,
    /// then reads the inner marker via ReadValue.
    /// </summary>
    private static object? ReadObjectWithTypeName<TInput>(BinaryDeserializationContext<TInput> context, Type targetType)
    where TInput : struct, IBinaryInputBase
    {
        var typeName = ReadPlainString(context);
        var resolvedType = AcSerializerCommon.ResolveTypeName(typeName)
            ?? throw new AcBinaryDeserializationException($"Cannot resolve type '{typeName}' for ObjectWithTypeName at position {context.Position}.", context.Position, null);

        var wrapper = context.GetWrapper(resolvedType);
        context.RegisterPolymorphicWrapper(wrapper);
        // Next byte is the actual inner marker (Object/Array/Dict/etc.) — read it via ReadValue
        return ReadValue(context, resolvedType);
    }

    /// <summary>
    /// Polymorphic COMBINED marker: first type occurrence + ref tracking first occurrence.
    /// Wire format: [ObjectWithTypeNameRefFirst (69)] [TypeName string] [VarUInt refCacheIndex] [properties...]
    /// Object body follows directly — no inner Object/ObjectRefFirst marker.
    /// </summary>
    private static object? ReadObjectWithTypeNameRefFirst<TInput>(BinaryDeserializationContext<TInput> context, Type targetType)
    where TInput : struct, IBinaryInputBase
    {
        var typeName = ReadPlainString(context);
        var resolvedType = AcSerializerCommon.ResolveTypeName(typeName)
            ?? throw new AcBinaryDeserializationException(
                $"Cannot resolve type '{typeName}' for ObjectWithTypeNameRefFirst at position {context.Position}.",
                context.Position, null);
        var wrapper = context.GetWrapper(resolvedType);
        context.RegisterPolymorphicWrapper(wrapper);
        var cacheIndex = (int)context.ReadVarUInt();
        return ReadObjectCoreWithWrapper(context, wrapper, cacheIndex);
    }

    /// <summary>
    /// Polymorphic PREFIX marker with cached type index.
    /// Wire format: [ObjectWithTypeIndex (70)] [VarUInt typeIndex] [inner marker: Object/Array/Dict/...] [body...]
    /// Looks up the previously registered wrapper by index (~1-2ns array access),
    /// then reads the inner marker via ReadValue.
    /// </summary>
    private static object? ReadObjectWithTypeIndex<TInput>(BinaryDeserializationContext<TInput> context, Type targetType)
    where TInput : struct, IBinaryInputBase
    {
        var typeIndex = (int)context.ReadVarUInt();
        var wrapper = context.GetPolymorphicWrapper(typeIndex);
        return ReadValue(context, wrapper.Metadata.SourceType);
    }

    /// <summary>
    /// Polymorphic COMBINED marker: cached type index + ref tracking first occurrence.
    /// Wire format: [ObjectWithTypeIndexRefFirst (71)] [VarUInt typeIndex] [VarUInt refCacheIndex] [properties...]
    /// Object body follows directly — no inner Object/ObjectRefFirst marker. 0 dictionary lookup.
    /// </summary>
    private static object? ReadObjectWithTypeIndexRefFirst<TInput>(BinaryDeserializationContext<TInput> context, Type targetType)
    where TInput : struct, IBinaryInputBase
    {
        var typeIndex = (int)context.ReadVarUInt();
        var wrapper = context.GetPolymorphicWrapper(typeIndex);
        var cacheIndex = (int)context.ReadVarUInt();
        return ReadObjectCoreWithWrapper(context, wrapper, cacheIndex);
    }

    /// <summary>
    /// Object olvasás core implementáció.
    /// </summary>
    /// <param name="context"></param>
    /// <param name="targetType"></param>
    /// <param name="cacheIndex">-1 = not cached, 0+ = register at this cache index</param>
    private static object? ReadObjectCore<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int cacheIndex)
    where TInput : struct, IBinaryInputBase
    {
        // Handle dictionary types
        if (IsDictionaryType(targetType, out var keyType, out var valueType))
        {
            return ReadDictionaryAsObject(context, keyType!, valueType!);
        }

        var wrapper = context.GetWrapper(targetType);

        // SGen fast path: generated reader bypasses GetWrapper + CreateInstance + PopulateObject entirely.
        // Only when not in UseMetadata mode (cross-type CacheMap not known at compile time),
        // not in ChainMode (needs post-read identity tracking),
        // and UseGeneratedCode is enabled (matching serializer-side check).
        if (!context.HasMetadata && !context.IsChainMode && context.Options.UseGeneratedCode)
        {
            var generatedReader = wrapper.GeneratedReader;
            if (generatedReader != null)
                return generatedReader.ReadObject(context, cacheIndex);
        }

        return ReadObjectCoreWithWrapper(context, wrapper, cacheIndex);
    }

    /// <summary>
    /// Object olvasás with pre-resolved wrapper (eliminates GetWrapper dictionary lookup).
    /// </summary>
    private static object? ReadObjectCoreWithWrapper<TInput>(BinaryDeserializationContext<TInput> context, TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper, int cacheIndex)
    where TInput : struct, IBinaryInputBase
    {
        var metadata = wrapper.Metadata;
        var targetType = metadata.SourceType;

        var instance = CreateInstance(targetType, metadata);
        if (instance == null) return null;

        if (cacheIndex >= 0)
        {
            context.RegisterInternedValueAt(cacheIndex, instance);
        }

        PopulateObject(context, instance, wrapper, skipDefaultWrite: true);

        // ChainMode kezelés
        if (context.IsChainMode && metadata.IsIId && metadata.IdType != null)
        {
            var id = GetIdBoxed(instance, metadata);
            if (id != null && !IsDefaultValue(id, metadata.IdType))
            {
                if (context.ChainTracker!.TryGetObject(targetType, id, out var existingObj))
                {
                    CopyProperties(instance, existingObj!, metadata);
                    return existingObj;
                }
                context.ChainTracker.TryRegisterIIdObject(instance);
            }
        }

        return instance;
    }

    /// <summary>
    /// Object olvasása UseMetadata módban (nem tracked).
    /// Wire format:
    ///   Első előfordulás: [ObjectWithMetadata][propNameHash (4b)][propCount (VarUInt)][hash0..N][props...]
    ///   Ismételt:         [ObjectWithMetadata][propNameHash (4b)][props...]
    /// </summary>
    private static object? ReadObjectWithMetadata<TInput>(BinaryDeserializationContext<TInput> context, Type targetType)
    where TInput : struct, IBinaryInputBase
    {
        return ReadObjectWithMetadataCore(context, targetType, cacheIndex: -1);
    }

    /// <summary>
    /// Object olvasása UseMetadata módban, első tracked előfordulás (ObjectWithMetadataRefFirst marker).
    /// Wire format: [ObjectWithMetadataRefFirst][VarUInt cacheIndex][propNameHash (4b)][...][props...]
    /// Az objektumot regisztráljuk a cache-be a megadott index-re.
    /// </summary>
    private static object? ReadObjectWithMetadataRefFirst<TInput>(BinaryDeserializationContext<TInput> context, Type targetType)
    where TInput : struct, IBinaryInputBase
    {
        var cacheIndex = (int)context.ReadVarUInt();
        return ReadObjectWithMetadataCore(context, targetType, cacheIndex: cacheIndex);
    }

    /// <summary>
    /// ObjectWithMetadata olvasás core implementáció.
    /// </summary>
    /// <param name="context"></param>
    /// <param name="targetType"></param>
    /// <param name="cacheIndex">-1 = not cached, 0+ = register at this cache index</param>
    private static object? ReadObjectWithMetadataCore<TInput>(BinaryDeserializationContext<TInput> context, Type targetType, int cacheIndex)
    where TInput : struct, IBinaryInputBase
    {
        // Inline metadata: propNameHash mindig jön
        var propNameHash = context.ReadInt32Raw();

        // Source hash-ek keresése — ha nincs, első előfordulás → olvasás a stream-ből
        var sourceHashes = context.FindSourceHashes(propNameHash);
        if (sourceHashes == null)
        {
            var propCount = (int)context.ReadVarUInt();
            sourceHashes = new int[propCount];
            for (var i = 0; i < propCount; i++)
            {
                sourceHashes[i] = context.ReadInt32Raw();
            }
            context.RegisterInlineMetadata(propNameHash, sourceHashes);
        }

        // Handle dictionary types
        if (IsDictionaryType(targetType, out var keyType, out var valueType))
        {
            return ReadDictionaryAsObject(context, keyType!, valueType!);
        }

        var wrapper = context.GetWrapper(targetType);
        var metadata = wrapper.Metadata;

        var instance = CreateInstance(targetType, metadata);
        if (instance == null) return null;

        if (cacheIndex >= 0)
        {
            context.RegisterInternedValueAt(cacheIndex, instance);
        }

        // CacheMap felépítése ha még nincs
        if (wrapper.CacheMap == null)
            BuildCacheMap(wrapper, sourceHashes);

        PopulateObject(context, instance, wrapper, skipDefaultWrite: true);

        // ChainMode kezelés
        if (context.IsChainMode && metadata.IsIId && metadata.IdType != null)
        {
            var id = GetIdBoxed(instance, metadata);
            if (id != null && !IsDefaultValue(id, metadata.IdType))
            {
                if (context.ChainTracker!.TryGetObject(targetType, id, out var existingObj))
                {
                    CopyProperties(instance, existingObj!, metadata);
                    return existingObj;
                }
                context.ChainTracker.TryRegisterIIdObject(instance);
            }
        }

        return instance;
    }

    /// <summary>
    /// Inline metadata olvasása a Populate path-hoz.
    /// Az ObjectWithMetadata marker már consume-álva van.
    /// Beolvassa a propNameHash-t + hash-eket (első előfordulásnál), és felépíti a cacheMap-et.
    /// </summary>
    private static void ReadInlineMetadataForPopulate<TInput>(BinaryDeserializationContext<TInput> context, Type targetType)
    where TInput : struct, IBinaryInputBase
    {
        var propNameHash = context.ReadInt32Raw();

        var sourceHashes = context.FindSourceHashes(propNameHash);
        if (sourceHashes == null)
        {
            var propCount = (int)context.ReadVarUInt();
            sourceHashes = new int[propCount];
            for (var i = 0; i < propCount; i++)
            {
                sourceHashes[i] = context.ReadInt32Raw();
            }
            context.RegisterInlineMetadata(propNameHash, sourceHashes);
        }

        var wrapper = context.GetWrapper(targetType);
        if (wrapper.CacheMap == null)
            BuildCacheMap(wrapper, sourceHashes);
    }

    /// <summary>
    /// CacheMap felépítése: source property hash-ek → target PropertySetter mapping.
    /// Fast path: same-type esetén props[index+1] egyezik. Cross-type: keresés index+1-től.
    /// </summary>
    private static void BuildCacheMap(TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper, int[] sourceHashes)
    {
        var properties = wrapper.Metadata.PropertiesArray;
        var cacheMap = new BinaryPropertySetterBase?[sourceHashes.Length];
        var index = -1;
        for (var i = 0; i < sourceHashes.Length; i++)
        {
            var hash = sourceHashes[i];
            var nextIdx = index + 1;
            if (nextIdx < properties.Length && properties[nextIdx].PropertyNameHash == hash)
            {
                cacheMap[i] = properties[nextIdx];
                index = nextIdx;
            }
            else
            {
                for (var j = nextIdx; j < properties.Length; j++)
                {
                    if (properties[j].PropertyNameHash == hash)
                    {
                        cacheMap[i] = properties[j];
                        index = j;
                        break;
                    }
                }
            }
        }
        wrapper.CacheMap = cacheMap;
    }

    #endregion

    #region Array Reading

    private static object? ReadArray<TInput>(BinaryDeserializationContext<TInput> context, Type targetType)
    where TInput : struct, IBinaryInputBase
    {
        var elementType = GetCollectionElementType(targetType);
        if (elementType == null) elementType = typeof(object);

        var count = (int)context.ReadVarUInt();
        
        // Optimized path for primitive arrays
        if (targetType.IsArray && count > 0)
        {
            var result = TryReadPrimitiveArray(context, elementType, count);
            if (result != null) return result;
        }

        if (targetType.IsArray)
        {
            var array = Array.CreateInstance(elementType, count);
            for (var i = 0; i < count; i++)
            {
                var value = ReadValue(context, elementType);
                array.SetValue(value, i);
            }

            return array;
        }

        IList? list = null;
        try
        {
            var instance = Activator.CreateInstance(targetType);
            if (instance is IList l) list = l;
        }
        catch
        {
            /* Fallback to List<T> */
        }

        list ??= GetOrCreateListFactory(elementType)(count);

        var acObservable = list as IAcObservableCollection;
        acObservable?.BeginUpdate();

        try
        {
            for (var i = 0; i < count; i++)
            {
                var value = ReadValue(context, elementType);
                list.Add(value);
            }
        }
        finally
        {
            acObservable?.EndUpdate();
        }

        return list;
    }

    /// <summary>
    /// Optimized primitive array reader using bulk operations.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static Array? TryReadPrimitiveArray<TInput>(BinaryDeserializationContext<TInput> context, Type elementType, int count)
    where TInput : struct, IBinaryInputBase
    {
        // Int32 array
        if (ReferenceEquals(elementType, IntType))
        {
            var array = new int[count];
            for (var i = 0; i < count; i++)
            {
                var typeCode = context.ReadByte();
                if (BinaryTypeCode.IsTinyInt(typeCode))
                    array[i] = BinaryTypeCode.DecodeTinyInt(typeCode);
                else if (typeCode == BinaryTypeCode.Int32)
                    array[i] = context.ReadVarInt();
                else
                    return null; // Fall back to generic path
            }

            return array;
        }

        // Double array
        if (ReferenceEquals(elementType, DoubleType))
        {
            var array = new double[count];
            for (var i = 0; i < count; i++)
            {
                var typeCode = context.ReadByte();
                if (typeCode != BinaryTypeCode.Float64) return null;
                array[i] = context.ReadDoubleUnsafe();
            }

            return array;
        }

        // Long array
        if (ReferenceEquals(elementType, LongType))
        {
            var array = new long[count];
            for (var i = 0; i < count; i++)
            {
                var typeCode = context.ReadByte();
                if (BinaryTypeCode.IsTinyInt(typeCode))
                    array[i] = BinaryTypeCode.DecodeTinyInt(typeCode);
                else if (typeCode == BinaryTypeCode.Int32)
                    array[i] = context.ReadVarInt();
                else if (typeCode == BinaryTypeCode.Int64)
                    array[i] = context.ReadVarLong();
                else
                    return null;
            }

            return array;
        }

        // Bool array
        if (ReferenceEquals(elementType, BoolType))
        {
            var array = new bool[count];
            for (var i = 0; i < count; i++)
            {
                var typeCode = context.ReadByte();
                if (typeCode == BinaryTypeCode.True) array[i] = true;
                else if (typeCode == BinaryTypeCode.False) array[i] = false;
                else return null;
            }

            return array;
        }

        // Guid array
        if (ReferenceEquals(elementType, GuidType))
        {
            var array = new Guid[count];
            for (var i = 0; i < count; i++)
            {
                var typeCode = context.ReadByte();
                if (typeCode != BinaryTypeCode.Guid) return null;
                array[i] = context.ReadGuidUnsafe();
            }

            return array;
        }

        // Decimal array
        if (ReferenceEquals(elementType, DecimalType))
        {
            var array = new decimal[count];
            for (var i = 0; i < count; i++)
            {
                var typeCode = context.ReadByte();
                if (typeCode != BinaryTypeCode.Decimal) return null;
                array[i] = context.ReadDecimalUnsafe();
            }

            return array;
        }

        // DateTime array
        if (ReferenceEquals(elementType, DateTimeType))
        {
            var array = new DateTime[count];
            for (var i = 0; i < count; i++)
            {
                var typeCode = context.ReadByte();
                if (typeCode != BinaryTypeCode.DateTime) return null;
                array[i] = context.ReadDateTimeUnsafe();
            }

            return array;
        }

        // Float array
        if (ReferenceEquals(elementType, FloatType))
        {
            var array = new float[count];
            for (var i = 0; i < count; i++)
            {
                var typeCode = context.ReadByte();
                if (typeCode != BinaryTypeCode.Float32) return null;
                array[i] = context.ReadSingleUnsafe();
            }

            return array;
        }

        // Short (Int16) array
        if (ReferenceEquals(elementType, ShortType))
        {
            var array = new short[count];
            for (var i = 0; i < count; i++)
            {
                var typeCode = context.ReadByte();
                if (typeCode != BinaryTypeCode.Int16) return null;
                array[i] = context.ReadInt16Unsafe();
            }

            return array;
        }

        // UShort (UInt16) array
        if (ReferenceEquals(elementType, UShortType))
        {
            var array = new ushort[count];
            for (var i = 0; i < count; i++)
            {
                var typeCode = context.ReadByte();
                if (typeCode != BinaryTypeCode.UInt16) return null;
                array[i] = context.ReadUInt16Unsafe();
            }

            return array;
        }

        // UInt32 array
        if (ReferenceEquals(elementType, UIntType))
        {
            var array = new uint[count];
            for (var i = 0; i < count; i++)
            {
                var typeCode = context.ReadByte();
                if (typeCode != BinaryTypeCode.UInt32) return null;
                array[i] = context.ReadVarUInt();
            }

            return array;
        }

        // UInt64 array
        if (ReferenceEquals(elementType, ULongType))
        {
            var array = new ulong[count];
            for (var i = 0; i < count; i++)
            {
                var typeCode = context.ReadByte();
                if (typeCode != BinaryTypeCode.UInt64) return null;
                array[i] = context.ReadVarULong();
            }

            return array;
        }

        // SByte (Int8) array
        if (ReferenceEquals(elementType, SByteType))
        {
            var array = new sbyte[count];
            for (var i = 0; i < count; i++)
            {
                var typeCode = context.ReadByte();
                if (typeCode != BinaryTypeCode.Int8) return null;
                array[i] = unchecked((sbyte)context.ReadByte());
            }

            return array;
        }

        // Char array
        if (ReferenceEquals(elementType, CharType))
        {
            var array = new char[count];
            for (var i = 0; i < count; i++)
            {
                var typeCode = context.ReadByte();
                if (typeCode != BinaryTypeCode.Char) return null;
                array[i] = context.ReadCharUnsafe();
            }

            return array;
        }

        // DateTimeOffset array
        if (ReferenceEquals(elementType, DateTimeOffsetType))
        {
            var array = new DateTimeOffset[count];
            for (var i = 0; i < count; i++)
            {
                var typeCode = context.ReadByte();
                if (typeCode != BinaryTypeCode.DateTimeOffset) return null;
                array[i] = context.ReadDateTimeOffsetUnsafe();
            }

            return array;
        }

        // TimeSpan array
        if (ReferenceEquals(elementType, TimeSpanType))
        {
            var array = new TimeSpan[count];
            for (var i = 0; i < count; i++)
            {
                var typeCode = context.ReadByte();
                if (typeCode != BinaryTypeCode.TimeSpan) return null;
                array[i] = context.ReadTimeSpanUnsafe();
            }

            return array;
        }

        return null;
    }

    #endregion

    #region Dictionary Reading

    private static object? ReadDictionary<TInput>(BinaryDeserializationContext<TInput> context, Type targetType)
    where TInput : struct, IBinaryInputBase
    {
        if (!IsDictionaryType(targetType, out var keyType, out var valueType))
        {
            keyType = typeof(string);
            valueType = typeof(object);
        }

        return ReadDictionaryAsObject(context, keyType!, valueType!);
    }

    private static object ReadDictionaryAsObject<TInput>(BinaryDeserializationContext<TInput> context, Type keyType, Type valueType)
    where TInput : struct, IBinaryInputBase
    {
        var dictType = DictionaryGenericType.MakeGenericType(keyType, valueType);
        var count = (int)context.ReadVarUInt();
        var dict = (IDictionary)Activator.CreateInstance(dictType, count)!;

        for (var i = 0; i < count; i++)
        {
            var key = ReadValue(context, keyType);
            var value = ReadValue(context, valueType);
            if (key != null)
                dict.Add(key, value);
        }

        return dict;
    }

    #endregion

    #region Skip Value

    private static void SkipValue<TInput>(BinaryDeserializationContext<TInput> context, BinaryDeserializeTypeMetadata metaData)
    where TInput : struct, IBinaryInputBase
    {
        var typeCode = context.ReadByte();

        if (typeCode == BinaryTypeCode.Null) return;

        if (BinaryTypeCode.IsTinyInt(typeCode)) return;

        // Handle FixStr (short universal strings — marker carries char length)
        if (BinaryTypeCode.IsFixStr(typeCode))
        {
            var charLength = BinaryTypeCode.DecodeFixStrLength(typeCode);
            var excess = context.ReadByte();
            var byteLength = charLength + excess;
            if (byteLength > 0)
                context.Skip(byteLength);
            return;
        }
        // H2Q6: non-ASCII short strings now use StringSmall tier (handled in switch below).

        switch (typeCode)
        {
            case BinaryTypeCode.True:
            case BinaryTypeCode.False:
            case BinaryTypeCode.StringEmpty:
                return;
            case BinaryTypeCode.Int8:
            case BinaryTypeCode.UInt8:
                context.Skip(1);
                return;
            case BinaryTypeCode.Int16:
            case BinaryTypeCode.UInt16:
            case BinaryTypeCode.Char:
                context.Skip(2);
                return;
            case BinaryTypeCode.Int32:
                context.ReadVarInt();
                return;
            case BinaryTypeCode.UInt32:
                context.ReadVarUInt();
                return;
            case BinaryTypeCode.Float32:
                context.Skip(4);
                return;
            case BinaryTypeCode.Int64:
                context.ReadVarLong();
                return;
            case BinaryTypeCode.UInt64:
                context.ReadVarULong();
                return;
            case BinaryTypeCode.Float64:
            case BinaryTypeCode.TimeSpan:
                context.Skip(8);
                return;
            case BinaryTypeCode.DateTime:
                context.Skip(9);
                return;
            case BinaryTypeCode.DateTimeOffset:
                context.Skip(10);
                return;
            case BinaryTypeCode.Guid:
            case BinaryTypeCode.Decimal:
                context.Skip(16);
                return;
            case BinaryTypeCode.String:
                // Skip layout: [VarUInt(charLength - FixStrCount)][unsigned excess slot:1|2|4][bytes]
                {
                    var charLength = (int)context.ReadVarUInt() + BinaryTypeCode.FixStrCount;
                    var slotSize = BinaryTypeCode.GetUniversalStringExcessSlotSize(charLength);
                    var excess = slotSize switch
                    {
                        1 => context.ReadByte(),
                        2 => context.ReadUInt16Unsafe(),
                        _ => context.ReadUInt32Unsafe()
                    };
                    var byteLength = charLength + (int)excess;
                    if (byteLength > 0) context.Skip(byteLength);
                }
                return;
            case BinaryTypeCode.StringUtf16:
                // FastWire payload: [charLen:int32 LE][UTF-16 raw bytes].
                // For compact backward-compat payloads this marker may still carry legacy StringSmall shape;
                // skip-path remains dual-mode based on context.FastWire.
                {
                    if (context.FastWire)
                    {
                        var charLen = context.ReadInt32Unsafe();
                        if (charLen > 0) context.Skip(charLen * 2);
                    }
                    else
                    {
                        var header = context.ReadTwoBytesUnsafe();
                        var utf8Len = (byte)(header >> 8);
                        if (utf8Len > 0) context.Skip(utf8Len);
                    }
                }
                return;
            case BinaryTypeCode.StringInterned:
                context.ReadVarUInt();
                return;
            case BinaryTypeCode.StringInternFirstSmall:
                // H2Q6 interning Small: [cacheIdx:VarUInt][charLen:8][utf8Len:8][bytes] — register decoded string
                SkipAndRegisterInternedStringSmall(context);
                return;
            case BinaryTypeCode.StringInternFirstMedium:
                // H2Q6 interning Medium: [cacheIdx:VarUInt][charLen:16][utf8Len:16][bytes]
                SkipAndRegisterInternedStringMedium(context);
                return;
            case BinaryTypeCode.ByteArray:
                var byteLen = (int)context.ReadVarUInt();
                context.Skip(byteLen);
                return;
            case BinaryTypeCode.Enum:
                var enumByte = context.ReadByte();
                if (BinaryTypeCode.IsTinyInt(enumByte)) return;
                if (enumByte == BinaryTypeCode.Int32) context.ReadVarInt();
                return;
            case BinaryTypeCode.Object:
                SkipObject(context, metaData);
                return;
            case BinaryTypeCode.ObjectRefFirst:
                SkipObjectRefFirst(context, metaData);
                return;
            case BinaryTypeCode.ObjectWithMetadata:
                SkipObjectWithMetadata(context, metaData, cacheIndex: -1);
                return;
            case BinaryTypeCode.ObjectWithMetadataRefFirst:
            {
                var cacheIdx = (int)context.ReadVarUInt();
                SkipObjectWithMetadata(context, metaData, cacheIndex: cacheIdx);
                return;
            }
            case BinaryTypeCode.ObjectRef:
                context.ReadVarUInt();
                return;
            case BinaryTypeCode.Array:
                SkipArray(context, metaData);
                return;
            case BinaryTypeCode.Dictionary:
                SkipDictionary(context, metaData);
                return;
        }
    }

    /// <summary>
    /// Sima string kihagy�sa - NEM regisztr�l.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void SkipPlainString<TInput>(BinaryDeserializationContext<TInput> context)
    where TInput : struct, IBinaryInputBase
    {
        var byteLen = (int)context.ReadVarUInt();
        if (byteLen > 0)
        {
            context.Skip(byteLen);
        }
    }

    /// <summary>
    /// Skip an H2Q6 interning Small first-occurrence — still register in cache for subsequent ref reads.
    /// Wire format: <c>[StringInternFirstSmall][cacheIdx:VarUInt][charLen:8][utf8Len:8][UTF-8 bytes]</c>
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void SkipAndRegisterInternedStringSmall<TInput>(BinaryDeserializationContext<TInput> context)
    where TInput : struct, IBinaryInputBase
    {
        var cacheIndex = (int)context.ReadVarUInt();
        var header = context.ReadTwoBytesUnsafe();
        var charLen = (byte)header;
        var byteLen = (byte)(header >> 8);
        if (byteLen == 0)
        {
            context.RegisterInternedValueAt(cacheIndex, string.Empty);
            return;
        }
        var str = context.ReadStringUtf8WithCharLen(charLen, byteLen);
        context.RegisterInternedValueAt(cacheIndex, str);
    }

    /// <summary>
    /// Skip an H2Q6 interning Medium first-occurrence — still register in cache for subsequent ref reads.
    /// Wire format: <c>[StringInternFirstMedium][cacheIdx:VarUInt][charLen:16 LE][utf8Len:16 LE][UTF-8 bytes]</c>
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void SkipAndRegisterInternedStringMedium<TInput>(BinaryDeserializationContext<TInput> context)
    where TInput : struct, IBinaryInputBase
    {
        var cacheIndex = (int)context.ReadVarUInt();
        var packed = context.ReadUInt32Unsafe();
        var charLen = (ushort)packed;
        var byteLen = (ushort)(packed >> 16);
        if (byteLen == 0)
        {
            context.RegisterInternedValueAt(cacheIndex, string.Empty);
            return;
        }
        var str = context.ReadStringUtf8WithCharLen(charLen, byteLen);
        context.RegisterInternedValueAt(cacheIndex, str);
    }

    /// <summary>
    /// Skip ObjectRefFirst - must read cacheIndex and register placeholder in cache.
    /// Wire format: [ObjectRefFirst][VarUInt cacheIndex][props...]
    /// </summary>
    private static void SkipObjectRefFirst<TInput>(BinaryDeserializationContext<TInput> context, BinaryDeserializeTypeMetadata metaData)
    where TInput : struct, IBinaryInputBase
    {
        var cacheIndex = (int)context.ReadVarUInt();
        // Register placeholder (stream position as boxed int for potential lazy load)
        context.RegisterInternedValueAt(cacheIndex, context.Position);
        SkipObject(context, metaData);
    }

    ///// <summary>
    ///// Skip a string but still register it in the intern table if it meets the length threshold.
    ///// </summary>
    ///// <param name="context">Deserialization context</param>
    ///// <param name="streamPosition">Position before the type code was read</param>
    //[MethodImpl(MethodImplOptions.AggressiveInlining)]
    //private static void SkipAndInternString<TInput>(BinaryDeserializationContext<TInput> context, int streamPosition)
    //{
    //    var byteLen = (int)context.ReadVarUInt();
    //    if (byteLen == 0) return;

    //    var str = context.ReadStringUtf8(byteLen);
    //    if (str.Length >= context.MinStringInternLength)
    //    {
    //        context.RegisterInternedString(str, streamPosition);
    //    }
    //}

    /// <summary>
    /// Object kihagyása metadata nélkül — nem támogatott, mert nem tudjuk a property számot.
    /// </summary>
    private static void SkipObject<TInput>(BinaryDeserializationContext<TInput> context, BinaryDeserializeTypeMetadata metaData)
    where TInput : struct, IBinaryInputBase
    {
        throw new NotSupportedException(
            "SkipObject nem támogatott metadata nélkül. " +
            "A property szám nem határozható meg típus metadata nélkül.");
    }

    /// <summary>
    /// Skip ObjectWithMetadata/ObjectWithMetadataRefFirst.
    /// </summary>
    /// <param name="cacheIndex">-1 = not cached, 0+ = register at this cache index</param>
    private static void SkipObjectWithMetadata<TInput>(BinaryDeserializationContext<TInput> context, BinaryDeserializeTypeMetadata metaData, int cacheIndex)
    where TInput : struct, IBinaryInputBase
    {
        if (cacheIndex >= 0)
        {
            // Register placeholder for potential lazy load
            context.RegisterInternedValueAt(cacheIndex, context.Position);
        }

        var propNameHash = context.ReadInt32Raw();

        var sourceHashes = context.FindSourceHashes(propNameHash);
        if (sourceHashes == null)
        {
            var propCount = (int)context.ReadVarUInt();
            sourceHashes = new int[propCount];
            for (var i = 0; i < propCount; i++)
            {
                sourceHashes[i] = context.ReadInt32Raw();
            }
            context.RegisterInlineMetadata(propNameHash, sourceHashes);
        }

        // Skip all properties
        for (var i = 0; i < sourceHashes.Length; i++)
        {
            SkipValue(context, metaData);
        }
    }

    private static void SkipArray<TInput>(BinaryDeserializationContext<TInput> context, BinaryDeserializeTypeMetadata metaData)
    where TInput : struct, IBinaryInputBase
    {
        var count = (int)context.ReadVarUInt();
        for (var i = 0; i < count; i++)
        {
            SkipValue(context, metaData);
        }
    }

    private static void SkipDictionary<TInput>(BinaryDeserializationContext<TInput> context, BinaryDeserializeTypeMetadata metaData)
    where TInput : struct, IBinaryInputBase
    {
        var count = (int)context.ReadVarUInt();
        for (var i = 0; i < count; i++)
        {
            SkipValue(context, metaData); // key
            SkipValue(context, metaData); // value
        }
    }

    #endregion

    #region IId Registration Helpers

    /// <summary>
    /// Gets Id as boxed object - only used for ChainMode (rare path).
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static object? GetIdBoxed(object instance, BinaryDeserializeTypeMetadata metadata)
    {
        return metadata.IdAccessorType switch
        {
            IdAccessorType.Int32 => metadata.GetIdInt32(instance),
            IdAccessorType.Int64 => metadata.GetIdInt64(instance),
            IdAccessorType.Guid => metadata.GetIdGuid(instance),
            _ => null
        };
    }

    #endregion

    #region Type Metadata

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static object? CreateInstance(Type type, BinaryDeserializeTypeMetadata metadata)
    {
        if (metadata.CompiledConstructor != null)
            return metadata.CompiledConstructor();

        try
        {
            return Activator.CreateInstance(type);
        }
        catch (MissingMethodException ex)
        {
            throw new AcBinaryDeserializationException(
                $"Cannot create instance of type '{type.FullName}' because it does not have a parameterless constructor.",
                0, type, ex);
        }
    }

    private static Func<object, object?> CreateCompiledGetter(Type declaringType, PropertyInfo prop)
    {
        var objParam = Expression.Parameter(typeof(object), "obj");
        var castExpr = Expression.Convert(objParam, declaringType);
        var propAccess = Expression.Property(castExpr, prop);
        var boxed = Expression.Convert(propAccess, typeof(object));
        return Expression.Lambda<Func<object, object?>>(boxed, objParam).Compile();
    }

    #endregion
}

// Implementation moved to AcBinaryDeserializer.TypeConversionInfo.cs
