using AyCode.Core.Compression;
using AyCode.Core.Serializers.Expressions;
using System.Buffers;
using System.Collections;
using System.Collections.Concurrent;
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;

namespace AyCode.Core.Serializers.Binaries;

/// <summary>
/// High-performance binary serializer optimized for speed and memory efficiency.
/// Features:
/// - VarInt encoding for compact integers (MessagePack-style)
/// - String interning for repeated strings
/// - Property name table for fast lookup
/// - Reference handling for circular/shared references
/// - Optional metadata for schema evolution
/// - Optimized buffer management with ArrayPool
/// - Zero-allocation hot paths using Span and MemoryMarshal
/// - Automatic Expression to AcExpressionNode conversion
/// - Generic TOutput for output strategy selection (ArrayBinaryOutput / BufferWriterBinaryOutput)
/// - Buffer-in-context: _buffer/_position owned by context for zero virtual dispatch on hot path
/// </summary>
public static partial class AcBinarySerializer
{

    // Pre-computed UTF8 encoder for string operations
    private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
    private static readonly Type StringType = typeof(string);
    private static readonly Type GuidType = typeof(Guid);
    private static readonly Type DateTimeOffsetType = typeof(DateTimeOffset);
    private static readonly Type TimeSpanType = typeof(TimeSpan);
    private static readonly Type IntType = typeof(int);
    private static readonly Type LongType = typeof(long);
    private static readonly Type FloatType = typeof(float);
    private static readonly Type DoubleType = typeof(double);
    private static readonly Type DecimalType = typeof(decimal);
    private static readonly Type BoolType = typeof(bool);
    private static readonly Type DateTimeType = typeof(DateTime);
    private static readonly Type ShortType = typeof(short);
    private static readonly Type UShortType = typeof(ushort);
    private static readonly Type UIntType = typeof(uint);
    private static readonly Type ULongType = typeof(ulong);
    private static readonly Type ByteType = typeof(byte);
    private static readonly Type SByteType = typeof(sbyte);
    private static readonly Type CharType = typeof(char);

    #region Public API

#if DEBUG
    /// <summary>
    /// DEBUG ONLY: Analyzes which string properties have repeated values that would benefit from interning.
    /// Returns a dictionary where key is "TypeName.PropertyName" and value is the occurrence count.
    /// Only properties with count > 1 are good candidates for [StringIntern] attribute.
    /// </summary>
    /// <param name="value">The object graph to analyze.</param>
    /// <param name="options">Serializer options (UseStringInterning should be enabled).</param>
    /// <returns>Dictionary of property paths to their string occurrence counts.</returns>
    public static Dictionary<string, Dictionary<string, int>> AnalyzeStringInternCandidates<T>(T value, AcBinarySerializerOptions? options = null)
    {
        ArgumentNullException.ThrowIfNull(value);

        options ??= AcBinarySerializerOptions.Default;

        // For analysis, use the provided reference handling mode
        var analysisOptions = new AcBinarySerializerOptions
        {
            UseStringInterning = StringInterningMode.All,
            MinStringInternLength = options.MinStringInternLength,
            MaxStringInternLength = options.MaxStringInternLength,
            ReferenceHandling = options.ReferenceHandling
        };

        var result = new Dictionary<string, Dictionary<string, int>>();
        var runtimeType = value.GetType();

        // Create context without pooling (we need to set up callback)
        using var context = new BinarySerializationContext<ArrayBinaryOutput>(analysisOptions);
        context.Output = new ArrayBinaryOutput(options.BufferWriterChunkSize);
        context.OutputInitialized = true;
        context.Output.Initialize(out context._buffer, out context._position, out context._bufferEnd);

        // Set up tracking callbacks
        context.OnStringInterned = (propertyPath, stringValue) =>
        {
            propertyPath ??= "(unknown)";

            if (!result.TryGetValue(stringValue, out var properties))
            {
                properties = new Dictionary<string, int>();
                result[stringValue] = properties;
            }

            properties[propertyPath] = properties.GetValueOrDefault(propertyPath) + 1;
        };

        // Run serialization to trigger callbacks
        context.WriteHeader();
        WriteValue(value, runtimeType, context);

        return result;
    }

    public static StringBuilder GetAnalyzeStringInternCandidatesLog<T>(T value, AcBinarySerializerOptions? options = null)
    {
        var sb = new StringBuilder();
        options ??= AcBinarySerializerOptions.Default;
        var analysis = AnalyzeStringInternCandidates(value, options);

        // Transform: stringValue → properties  TO  propertyPath → (stringValue, count)
        var propertyStats = new Dictionary<string, List<(string StringValue, int Count, int ByteLength)>>();

        foreach (var (stringValue, properties) in analysis)
        {
            var byteLength = System.Text.Encoding.UTF8.GetByteCount(stringValue.AsSpan());
            foreach (var (propPath, count) in properties)
            {
                if (!propertyStats.TryGetValue(propPath, out var list))
                {
                    list = [];
                    propertyStats[propPath] = list;
                }
                list.Add((stringValue, count, byteLength));
            }
        }

        var refMode = options.ReferenceHandling;

        // Header
        sb.AppendLine("+==============================================================================+");
        sb.AppendLine($"|                     STRING INTERN ANALYSIS REPORT (RefMode: {refMode,-12})        |");
        sb.AppendLine("+==============================================================================+");
        sb.AppendLine();

        // Global summary
        var totalStrings = analysis.Values.Sum(p => p.Values.Sum());
        var uniqueStrings = analysis.Count;
        var repeatedStrings = analysis.Count(kv => kv.Value.Values.Sum() > 1);

        sb.AppendLine("+-----------------------------------------------------------------------------+");
        sb.AppendLine("| STRING SUMMARY                                                              |");
        sb.AppendLine("+-----------------------------------------------------------------------------+");
        sb.AppendLine($"| Total string occurrences: {totalStrings,-10} Unique strings: {uniqueStrings,-10} Repeated: {repeatedStrings,-8} |");
        sb.AppendLine("+-----------------------------------------------------------------------------+");
        sb.AppendLine();

        // Property-focused table
        // Calculate stats for each property first (for sorting by RepeatSum%)
        var propertyStatsCalculated = propertyStats.Select(kv =>
        {
            var propPath = kv.Key;
            var strings = kv.Value;
            var total = strings.Sum(s => s.Count);
            var unique = strings.Count;
            var repeated = strings.Count(s => s.Count > 1);
            var repeatSum = strings.Where(s => s.Count > 1).Sum(s => s.Count); // Sum of occurrences of repeated strings
            var repeatSumPct = total > 0 ? repeatSum * 100.0 / total : 0;

            // Calculate savings
            var totalBytes = strings.Sum(s => s.Count * s.ByteLength);
            var uniqueBytes = strings.Sum(s => s.ByteLength);
            var indexBytes = total * 2;
            var savings = totalBytes - (uniqueBytes + indexBytes);

            return (propPath, strings, total, unique, repeated, repeatSum, repeatSumPct, savings);
        }).ToList();

        sb.AppendLine("+--------------------------------+-------+---------+--------+---------+-----------+----------+-------------+");
        sb.AppendLine("| Property                       | Total | RepSum  | Unique | Repeated| RepSum %  | Savings  | Recommend   |");
        sb.AppendLine("+--------------------------------+-------+---------+--------+---------+-----------+----------+-------------+");

        foreach (var stat in propertyStatsCalculated.OrderByDescending(x => x.repeatSumPct))
        {
            var savingsStr = stat.savings > 0 ? $"+{stat.savings:N0}B" : $"{stat.savings:N0}B";
            var recommend = stat.savings > 100 ? "[INTERN]" : stat.savings > 0 ? " maybe  " : " skip   ";

            var propDisplay = stat.propPath.Length > 30 ? stat.propPath[..27] + "..." : stat.propPath;
            sb.AppendLine($"| {propDisplay,-30} | {stat.total,5} | {stat.repeatSum,7} | {stat.unique,6} | {stat.repeated,7} | {stat.repeatSumPct,8:F1}% | {savingsStr,8} | {recommend,-11} |");
        }

        sb.AppendLine("+--------------------------------+-------+---------+--------+---------+-----------+----------+-------------+");
        sb.AppendLine();

        // Detailed property breakdown (only for properties with significant savings)
        sb.AppendLine("+-----------------------------------------------------------------------------+");
        sb.AppendLine("| DETAILED BREAKDOWN (properties with savings > 100 bytes)                   |");
        sb.AppendLine("+-----------------------------------------------------------------------------+");

        foreach (var stat in propertyStatsCalculated
            .Where(x => x.savings > 100)
            .OrderByDescending(x => x.repeatSumPct))
        {
            sb.AppendLine();
            sb.AppendLine($"  {stat.propPath} (RepSum: {stat.repeatSum}/{stat.total} = {stat.repeatSumPct:F1}%):");

            foreach (var (strVal, count, _) in stat.strings.OrderByDescending(s => s.Count).Take(10))
            {
                var preview = strVal.Length > 40 ? strVal[..37] + "..." : strVal;
                var marker = count > 1 ? ">" : " ";
                sb.AppendLine($"    {marker} [{count,4}x] \"{preview}\"");
            }

            if (stat.strings.Count > 10)
            {
                sb.AppendLine($"    ... and {stat.strings.Count - 10} more unique values");
            }
        }

        sb.AppendLine();
        sb.AppendLine("===============================================================================");
        sb.AppendLine("Legend: Total=all occurrences, RepSum=sum of repeated string occurrences");
        sb.AppendLine("        Unique=distinct values, Repeated=count of values appearing 2+ times");
        sb.AppendLine("        RepSum%=percentage of occurrences that are repeated (higher=better for intern)");
        sb.AppendLine("        Savings=estimated bytes saved with interning (positive=good)");
        sb.AppendLine("        > = repeated string (benefits from interning)");

        return sb;
    }
#endif

    /// <summary>
    /// Registers a source-generated binary writer for the specified type.
    /// Once registered, WriteObject bypasses the runtime switch/delegate property loop
    /// and calls the generated writer directly — eliminating Func&lt;&gt;.Invoke() overhead.
    /// Call once at startup (e.g., in a static constructor or module initializer).
    /// </summary>
    /// <typeparam name="T">The type to register the writer for.</typeparam>
    /// <param name="writer">The generated writer instance (typically a singleton).</param>
    internal static void RegisterGeneratedWriter<T>(IGeneratedBinaryWriter writer)
    {
        ArgumentNullException.ThrowIfNull(writer);
        GeneratedWriterRegistry.Register(typeof(T), writer);
    }

    /// <summary>
    /// Registers a source-generated binary writer for the specified type.
    /// </summary>
    internal static void RegisterGeneratedWriter(Type type, IGeneratedBinaryWriter writer)
    {
        ArgumentNullException.ThrowIfNull(type);
        ArgumentNullException.ThrowIfNull(writer);
        GeneratedWriterRegistry.Register(type, writer);
    }

    #region SGen Slot Allocation

    /// <summary>
    /// Number of runtime wrapper slots reserved for polymorphic type cache (indices 0..RuntimeSlotCount-1).
    /// SGen compile-time slots start at RuntimeSlotCount and above.
    /// Easily modifiable — all code references this constant instead of literal values.
    /// </summary>
    internal const int RuntimeSlotCount = BinaryTypeCode.SlotCount;

    /// <summary>
    /// Next available wrapper slot index. Starts at RuntimeSlotCount so SGen slots
    /// don't collide with runtime polymorphic slots (0..RuntimeSlotCount-1).
    /// </summary>
    internal static int s_nextWrapperSlot = RuntimeSlotCount + 1;

    /// <summary>
    /// Allocates a unique slot index for SGen wrapper cache.
    /// Returns RuntimeSlotCount, RuntimeSlotCount+1, RuntimeSlotCount+2, ...
    /// </summary>
    internal static int AllocateWrapperSlot() => Interlocked.Increment(ref s_nextWrapperSlot) - 1;

    #endregion

    /// <summary>
    /// Thread-safe registry for generated writers. Looked up once per TypeMetadataWrapper creation.
    /// </summary>
    internal static class GeneratedWriterRegistry
    {
        private static readonly ConcurrentDictionary<Type, IGeneratedBinaryWriter> Writers = new();

        internal static void Register(Type type, IGeneratedBinaryWriter writer) => Writers[type] = writer;

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

    /// <summary>
    /// Serialize object to binary with default options.
    /// </summary>
    /// <remarks>
    /// NativeAOT contract: <typeparamref name="T"/> is annotated with DAMs <c>PublicProperties</c>
    /// so the trimmer preserves <see cref="System.Reflection.PropertyInfo"/> metadata required by the
    /// runtime reflection path (compiled getters fall back to <c>PropertyInfo.GetValue</c> when
    /// <see cref="System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported"/> is false).
    /// SGen path is unaffected — annotation is uniform with <see cref="AcBinaryDeserializer.Deserialize{T}(byte[])"/>.
    /// </remarks>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static byte[] Serialize<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(T value)
        => Serialize(value, AcBinarySerializerOptions.Default);

    /// <summary>
    /// Serialize object to an IBufferWriter with default options. Returns bytes written.
    /// </summary>
    /// <inheritdoc cref="Serialize{T}(T)" path="/remarks"/>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static int Serialize<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(T value, IBufferWriter<byte> writer)
        => Serialize(value, writer, AcBinarySerializerOptions.Default);

    /// <summary>
    /// Serialize object to binary with specified options.
    /// Uses ArrayBinaryOutput for byte[] result path.
    /// </summary>
    public static byte[] Serialize<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(T value, AcBinarySerializerOptions options)
        => Serialize(value, typeof(T), options);

    public static byte[] Serialize(object? value, AcBinarySerializerOptions options) 
        => value == null ? [BinaryTypeCode.Null] : Serialize(value, value.GetType(), options);

    /// <summary>
    /// Non-generic <c>Type</c>-based <see cref="Serialize{T}(T, AcBinarySerializerOptions)"/>. For
    /// runtime-typed scenarios (plugin frameworks, ASP.NET ModelBinding, MVC formatters). The
    /// <paramref name="type"/> parameter is the declared type used for dispatch — identical semantics
    /// to the generic version where <c>typeof(T)</c> is the dispatch type.
    /// </summary>
    public static byte[] Serialize(object? value, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type, AcBinarySerializerOptions options)
    {
        if (value == null) return [BinaryTypeCode.Null];

        var context = AcquireArrayOutputContext(options);

        try
        {
            // Full path: IQueryable/Expression conversion, primitive/collection dispatch
            var actualValue = ConvertExpressionValue(value, ref type);
            var wrapper = context.GetWrapper(type);

            ScanForDuplicates(actualValue, type, context, wrapper);
            context.WriteHeader();

            // SGen fast path: skip IQueryable/Expression check + WriteValue dispatch chain.
            // If root type has a GeneratedWriter it cannot be IQueryable/Expression/primitive/collection.
            if (wrapper.GeneratedWriter != null && options.UseGeneratedCode) WriteObject(actualValue, wrapper, context);
            else WriteValue(actualValue, type, context);

            return options.UseCompression != Lz4CompressionMode.None 
                ? Lz4.Compress(context.Output.AsSpan(context._buffer, context._position), options.UseCompression) 
                : context.Output.ToArray(context._buffer, context._position);
        }
        finally
        {
            ReturnContext(context, options);
        }
    }

    /// <summary>
    /// Runs only the scan pass (ScanForDuplicates) without writing.
    /// For benchmarking scan pass overhead in isolation.
    /// </summary>
    internal static void ScanOnly<T>(T value, AcBinarySerializerOptions options)
    {
        if (value == null) return;

        var runtimeType = typeof(T);
        var context = AcquireArrayOutputContext(options);

        try
        {
            ScanForDuplicates(value, runtimeType, context);
        }
        finally
        {
            ReturnContext(context, options);
        }
    }

    /// <summary>
    /// Serialize object to an IBufferWriter for zero-copy scenarios.
    /// Uses BufferWriterBinaryOutput — writes directly to the caller's buffer.
    /// Note: Compression is applied if enabled in options.
    /// </summary>
    public static int Serialize<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(T value, IBufferWriter<byte> writer, AcBinarySerializerOptions options)
    {
        if (value == null)
        {
            var span = writer.GetSpan(1);
            span[0] = BinaryTypeCode.Null;
            writer.Advance(1);
            return 1;
        }

        var runtimeType = typeof(T);
        var context = BinarySerializationContextPool<BufferWriterBinaryOutput>.Get(options);

        context.Output = new BufferWriterBinaryOutput(writer, options.BufferWriterChunkSize);
        context.Output.Initialize(out context._buffer, out context._position, out context._bufferEnd);

        try
        {
            // SGen fast path: skip IQueryable/Expression check + WriteValue dispatch chain.
            // If root type has a GeneratedWriter it cannot be IQueryable/Expression/primitive/collection.
            if (options.UseGeneratedCode)
            {
                var wrapper = context.GetWrapper(runtimeType);
                if (wrapper.GeneratedWriter != null)
                {
                    ScanForDuplicates(value, runtimeType, context);
                    context.WriteHeader();
                    WriteObject(value, wrapper, context);

                    if (options.UseCompression != Lz4CompressionMode.None)
                        ThrowCompressionNotSupportedWithBufferWriter(context);

                    var bytesWritten = context.Output.GetTotalPosition(context._position);
                    context.Output.Flush(context._buffer, context._position);
                    return bytesWritten;
                }
            }

            // Full path: IQueryable/Expression conversion, primitive/collection dispatch
            var actualValue = ConvertExpressionValue(value, ref runtimeType);
            ScanForDuplicates(actualValue, runtimeType, context);
            context.WriteHeader();
            WriteValue(actualValue, runtimeType, context);

            if (options.UseCompression != Lz4CompressionMode.None)
                ThrowCompressionNotSupportedWithBufferWriter(context);

            var totalBytesWritten = context.Output.GetTotalPosition(context._position);
            context.Output.Flush(context._buffer, context._position);
            return totalBytesWritten;
        }
        finally
        {
            context.Output = default;
            ReturnContext(context, options);
        }
    }

    /// <summary>
    /// Non-generic <c>Type</c>-based counterpart to
    /// <see cref="Serialize{T}(T, IBufferWriter{byte}, AcBinarySerializerOptions)"/>. Body identical
    /// — <paramref name="type"/> is a declared-type hint only; runtime polymorphism uses <c>value.GetType()</c>.
    /// </summary>
    public static int Serialize(object? value, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type, IBufferWriter<byte> writer, AcBinarySerializerOptions options)
    {
        if (value == null)
        {
            var span = writer.GetSpan(1);
            span[0] = BinaryTypeCode.Null;
            writer.Advance(1);
            return 1;
        }

        var runtimeType = type;
        var context = BinarySerializationContextPool<BufferWriterBinaryOutput>.Get(options);
        context.Output = new BufferWriterBinaryOutput(writer, options.BufferWriterChunkSize);
        context.Output.Initialize(out context._buffer, out context._position, out context._bufferEnd);

        try
        {
            if (options.UseGeneratedCode)
            {
                var wrapper = context.GetWrapper(runtimeType);
                if (wrapper.GeneratedWriter != null)
                {
                    ScanForDuplicates(value, runtimeType, context);
                    context.WriteHeader();
                    WriteObject(value, wrapper, context);

                    if (options.UseCompression != Lz4CompressionMode.None)
                        ThrowCompressionNotSupportedWithBufferWriter(context);

                    var bytesWritten = context.Output.GetTotalPosition(context._position);
                    context.Output.Flush(context._buffer, context._position);
                    return bytesWritten;
                }
            }

            var actualValue = ConvertExpressionValue(value, ref runtimeType);
            ScanForDuplicates(actualValue, runtimeType, context);
            context.WriteHeader();
            WriteValue(actualValue, runtimeType, context);

            if (options.UseCompression != Lz4CompressionMode.None)
                ThrowCompressionNotSupportedWithBufferWriter(context);

            var totalBytesWritten = context.Output.GetTotalPosition(context._position);
            context.Output.Flush(context._buffer, context._position);
            return totalBytesWritten;
        }
        finally
        {
            context.Output = default;
            ReturnContext(context, options);
        }
    }

    /// <summary>
    /// Serialize to a <see cref="System.IO.Pipelines.Pipe"/> as a chunked stream — pure AcBinary
    /// bytes are written via <see cref="AsyncPipeWriterOutput"/> in raw mode (no per-chunk header).
    /// The output is byte-compatible with <see cref="Serialize{T}(T, AcBinarySerializerOptions)"/>'s
    /// <c>byte[]</c> result; a consumer drains <c>pipe.Reader</c> in its own reader-task and pushes
    /// bytes via <see cref="AsyncPipeReaderInput.Feed"/>, then calls
    /// <see cref="AcBinaryDeserializer.Deserialize{T}(AsyncPipeReaderInput, AcBinarySerializerOptions)"/>
    /// — no extra parser, no special transport adapter.
    ///
    /// <para><b>Why <see cref="System.IO.Pipelines.Pipe"/> instead of <see cref="System.IO.Pipelines.PipeWriter"/>?</b>
    /// <c>Pipe.Writer</c> is always the BCL <c>PipeWriterImpl</c>, which is parallel-capable
    /// (no <c>_tailMemory</c> reset race like <c>StreamPipeWriter</c>). This overload exposes the
    /// <paramref name="flushPolicy"/> + <paramref name="flushTimeout"/> tuning safely.</para>
    /// </summary>
    /// <param name="value">The value to serialize; <c>null</c> writes a single null marker.</param>
    /// <param name="pipe">Target pipe — caller drains <c>pipe.Reader</c> elsewhere.</param>
    /// <param name="options">Serializer options (type wrappers, reference handling, interning, etc.).</param>
    /// <param name="flushPolicy">
    /// Per-chunk flush synchronization — see <see cref="FlushPolicy"/> for the three trade-off
    /// points. <see cref="FlushPolicy.PerChunk"/>: strictly bounded ~chunk × 1 peak memory, no
    /// producer/flush parallelism. <see cref="FlushPolicy.DoubleBuffered"/> (default): ~chunk × 2
    /// peak memory, max producer/flush parallelism. <see cref="FlushPolicy.Coalesced"/>: up to
    /// PauseWriterThreshold (~64 KB), highest throughput on bounded payloads.
    /// </param>
    /// <param name="flushTimeout">
    /// Per-flush timeout. <c>null</c> → wait forever. Positive value: throws
    /// <see cref="TimeoutException"/> on stuck consumers.
    /// </param>
    /// <returns>Total serialized bytes written.</returns>
    public static int SerializeChunked<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(T value, System.IO.Pipelines.Pipe pipe, AcBinarySerializerOptions options, FlushPolicy flushPolicy = FlushPolicy.DoubleBuffered, TimeSpan? flushTimeout = null)
        => SerializeToPipeWriterCore(value, typeof(T), pipe.Writer, options, flushPolicy, flushTimeout, multiMessage: false);

    // SerializeChunked non-generic
    public static int SerializeChunked(object? value, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type, System.IO.Pipelines.Pipe pipe, AcBinarySerializerOptions options, FlushPolicy flushPolicy = FlushPolicy.DoubleBuffered, TimeSpan? flushTimeout = null)
        => SerializeToPipeWriterCore(value, type, pipe.Writer, options, flushPolicy, flushTimeout, multiMessage: false);
    
    
    /// <summary>
    /// Serialize to any <see cref="System.IO.Pipelines.PipeWriter"/> as a chunked stream — pure
    /// AcBinary bytes, no per-chunk header. The output is byte-compatible with
    /// <see cref="Serialize{T}(T, AcBinarySerializerOptions)"/>'s <c>byte[]</c> result.
    ///
    /// <para><b>Flush strategy auto-selected by writer type</b>: <c>StreamPipeWriter</c>
    /// (<c>PipeWriter.Create(stream)</c> — NamedPipe / FileStream / NetworkStream / etc.) runs
    /// sequentially per chunk because the BCL impl resets <c>_tailMemory</c> on flush completion
    /// (race-incompatible with parallel send). Other PipeWriter implementations (Kestrel transport,
    /// custom impls) run with the safe <see cref="FlushPolicy.DoubleBuffered"/> default — max parallelism, zero-alloc.</para>
    ///
    /// <para><b>Need runtime tuning of the flush strategy?</b> Build a
    /// <see cref="System.IO.Pipelines.Pipe"/> instance and use
    /// <see cref="SerializeChunked{T}(T, System.IO.Pipelines.Pipe, AcBinarySerializerOptions, FlushPolicy, TimeSpan?)"/>
    /// — only Pipe-based writers can guarantee parallel-capable flush behavior.</para>
    ///
    /// <para><b>Need a multiplexed wire format with per-chunk frame headers?</b> See
    /// <see cref="SerializeChunkedFramed{T}(T, System.IO.Pipelines.PipeWriter, AcBinarySerializerOptions)"/>.</para>
    /// </summary>
    /// <param name="value">The value to serialize; <c>null</c> writes a single null marker.</param>
    /// <param name="pipeWriter">Target pipe writer.</param>
    /// <param name="options">Serializer options (type wrappers, reference handling, interning, etc.).</param>
    /// <returns>Total serialized bytes written.</returns>
    public static int SerializeChunked<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(T value, System.IO.Pipelines.PipeWriter pipeWriter, AcBinarySerializerOptions options)
        => SerializeToPipeWriterCore(value, typeof(T), pipeWriter, options, FlushPolicy.DoubleBuffered, flushTimeout: null, multiMessage: false);

    /// <summary>
    /// Non-generic <c>Type</c>-based counterpart to
    /// <see cref="SerializeChunked{T}(T, System.IO.Pipelines.PipeWriter, AcBinarySerializerOptions)"/>.
    /// For runtime-typed scenarios (MVC formatters, plugin frameworks).
    /// </summary>
    public static int SerializeChunked(object? value, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type, System.IO.Pipelines.PipeWriter pipeWriter, AcBinarySerializerOptions options)
        => SerializeToPipeWriterCore(value,type, pipeWriter, options, FlushPolicy.DoubleBuffered, flushTimeout: null, multiMessage: false);

    /// <summary>
    /// Serialize a value into a chunked stream where each chunk carries a self-describing
    /// frame header — <c>[201][UINT16 size][data]</c> per chunk, with a final <c>[202]</c>
    /// end-of-stream marker. The frame headers let the receiver detect chunk boundaries
    /// incrementally without knowing the total payload size up front, and let multiple
    /// independent messages share a single transport with reliable separation.
    ///
    /// <para><b>Use this when</b> building a multiplexed wire protocol where several logical
    /// messages are interleaved on one stream, when the receiver needs to start deserializing
    /// as bytes arrive (pipeline parallelism — serialize / network / deserialize overlap), or
    /// when the upper layer needs to dispatch each chunk independently. Typical scenarios:
    /// real-time RPC, custom Hub-style protocols, event stream multiplexing.</para>
    ///
    /// <para><b>Concrete example</b>: SignalR's <c>BinaryProtocolMode.AsyncSegment</c> uses
    /// this exact wire format to interleave many HubMessages over a single connection.</para>
    ///
    /// <para><b>Need a simpler streaming output without per-chunk metadata?</b> Use
    /// <see cref="SerializeChunked{T}(T, System.IO.Pipelines.Pipe, AcBinarySerializerOptions, FlushPolicy, TimeSpan?)"/>
    /// — bit-compatible with <see cref="Serialize{T}(T, AcBinarySerializerOptions)"/>'s
    /// <c>byte[]</c> output, no extra parser needed on the receive side.</para>
    /// </summary>
    /// <param name="value">The value to serialize; <c>null</c> writes a single null marker.</param>
    /// <param name="pipe">Target pipe — caller drains <c>pipe.Reader</c> elsewhere.</param>
    /// <param name="options">Serializer options.</param>
    /// <param name="flushPolicy">See <see cref="SerializeChunked{T}(T, System.IO.Pipelines.Pipe, AcBinarySerializerOptions, FlushPolicy, TimeSpan?)"/>.</param>
    /// <param name="flushTimeout">See <see cref="SerializeChunked{T}(T, System.IO.Pipelines.Pipe, AcBinarySerializerOptions, FlushPolicy, TimeSpan?)"/>.</param>
    /// <returns>Total serialized data bytes (excluding framing overhead).</returns>
    public static int SerializeChunkedFramed<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(T value, System.IO.Pipelines.Pipe pipe, AcBinarySerializerOptions options, FlushPolicy flushPolicy = FlushPolicy.DoubleBuffered, TimeSpan? flushTimeout = null)
        => SerializeToPipeWriterCore(value, typeof(T), pipe.Writer, options, flushPolicy, flushTimeout, multiMessage: true);
    

    /// <summary>
    /// Serialize to any <see cref="System.IO.Pipelines.PipeWriter"/> with per-chunk frame headers
    /// (multiplexed wire format). See
    /// <see cref="SerializeChunkedFramed{T}(T, System.IO.Pipelines.Pipe, AcBinarySerializerOptions, FlushPolicy, TimeSpan?)"/>
    /// for the wire format details and use-cases.
    ///
    /// <para><b>Flush strategy auto-selected by writer type</b> — see
    /// <see cref="SerializeChunked{T}(T, System.IO.Pipelines.PipeWriter, AcBinarySerializerOptions)"/>.</para>
    /// </summary>
    public static int SerializeChunkedFramed<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(T value, System.IO.Pipelines.PipeWriter pipeWriter, AcBinarySerializerOptions options)
        => SerializeToPipeWriterCore(value, typeof(T), pipeWriter, options, FlushPolicy.DoubleBuffered, flushTimeout: null, multiMessage: true);

    /// <summary>
    /// Non-generic <c>Type</c>-based counterpart to
    /// <see cref="SerializeChunkedFramed{T}(T, System.IO.Pipelines.PipeWriter, AcBinarySerializerOptions)"/>.
    /// </summary>
    public static int SerializeChunkedFramed(object? value, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type, System.IO.Pipelines.PipeWriter pipeWriter, AcBinarySerializerOptions options)
        => SerializeToPipeWriterCore(value, type, pipeWriter, options, FlushPolicy.DoubleBuffered, flushTimeout: null, multiMessage: true);

    /// <summary>
    /// Internal flush-tunable framed PipeWriter overload — used by <c>AyCode.Services</c>
    /// (SignalR hub protocol) on Kestrel transport output, which is parallel-capable. External
    /// callers should use the <see cref="System.IO.Pipelines.Pipe"/> overload to safely tune
    /// <paramref name="flushPolicy"/> on a guaranteed parallel-capable writer.
    /// </summary>
    internal static int SerializeChunkedFramed<T>(T value, System.IO.Pipelines.PipeWriter pipeWriter, AcBinarySerializerOptions options, FlushPolicy flushPolicy, TimeSpan? flushTimeout)
        => SerializeToPipeWriterCore(value, typeof(T), pipeWriter, options, flushPolicy, flushTimeout, multiMessage: true);

    /// <summary>
    /// Internal legacy alias for <see cref="SerializeChunkedFramed{T}(T, System.IO.Pipelines.PipeWriter, AcBinarySerializerOptions, FlushPolicy, TimeSpan?)"/>
    /// — kept until the SignalR hub protocol (<c>AcBinaryHubProtocol.cs</c>) is migrated to the
    /// new name in a separate, isolated step. Identical behavior to <c>SerializeChunkedFramed</c>
    /// (framed wire format with <c>[201][UINT16][data]</c> per chunk + <c>[202]</c> end marker).
    /// </summary>
    internal static int Serialize<T>(T value, System.IO.Pipelines.PipeWriter pipeWriter, AcBinarySerializerOptions options, FlushPolicy flushPolicy, TimeSpan? flushTimeout)
        => SerializeToPipeWriterCore(value, typeof(T), pipeWriter, options, flushPolicy, flushTimeout, multiMessage: true);

    /// <summary>
    /// Non-generic <see cref="Serialize{T}(T, System.IO.Pipelines.PipeWriter, AcBinarySerializerOptions, FlushPolicy, TimeSpan?)"/>
    /// overload with an explicit <paramref name="type"/> argument — used at heterogeneous <c>object?</c>
    /// call sites (e.g. the SignalR hub protocol's streamed-arg path) where the generic overload
    /// would infer <c>T = object</c> and emit an object-typed wire payload instead of the concrete
    /// runtime type. Caller passes <c>value.GetType()</c> (or a project-known concrete type).
    /// </summary>
    internal static int Serialize(object? value, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type, System.IO.Pipelines.PipeWriter pipeWriter, AcBinarySerializerOptions options, FlushPolicy flushPolicy, TimeSpan? flushTimeout)
        => SerializeToPipeWriterCore(value, type, pipeWriter, options, flushPolicy, flushTimeout, multiMessage: true);

    /// <summary>
    /// Common pipe-output serialization core. Same loop for both raw (<see cref="SerializeChunked{T}"/>)
    /// and framed (<see cref="SerializeChunkedFramed{T}"/>) modes — the only difference flows through
    /// <paramref name="multiMessage"/> into the <see cref="AsyncPipeWriterOutput"/> ctor.
    /// </summary>
    private static int SerializeToPipeWriterCore(object? value, Type type, System.IO.Pipelines.PipeWriter pipeWriter, AcBinarySerializerOptions options, FlushPolicy flushPolicy, TimeSpan? flushTimeout, bool multiMessage)
    {
        if (value == null)
        {
            if (!multiMessage)
            {
                // Raw single-message mode: null is just a [BinaryTypeCode.Null] byte on the wire.
                // No chunking needed, no [201]/[202] framing.
                var span = pipeWriter.GetSpan(1);
                span[0] = BinaryTypeCode.Null;
                pipeWriter.Advance(1);
                return 1;
            }

            // Framed mode (multiMessage=true): null still needs to flow through AsyncPipeWriterOutput
            // so the wire is [201][UINT16=1][BinaryTypeCode.Null][202] — well-formed chunked frame the
            // receiver can parse. Bypassing AsyncPipeWriterOutput here would emit a bare [Null] byte that
            // breaks framing for any chunked-stream consumer (e.g. AcBinaryHubProtocol AsyncSegment).
            var nullOutput = new AsyncPipeWriterOutput(pipeWriter, options.BufferWriterChunkSize, multiMessage: true, flushPolicy, flushTimeout);
            nullOutput.Initialize(out var nullBuf, out var nullPos, out _);
            nullBuf[nullPos++] = BinaryTypeCode.Null;
            nullOutput.Flush(nullBuf, nullPos);
            return 1;
        }

        var runtimeType = type;
        var context = BinarySerializationContextPool<AsyncPipeWriterOutput>.Get(options);

        context.Output = new AsyncPipeWriterOutput(pipeWriter, options.BufferWriterChunkSize, multiMessage, flushPolicy, flushTimeout);
        context.Output.Initialize(out context._buffer, out context._position, out context._bufferEnd);

        try
        {
            if (options.UseGeneratedCode)
            {
                var wrapper = context.GetWrapper(runtimeType);
                if (wrapper.GeneratedWriter != null)
                {
                    ScanForDuplicates(value, runtimeType, context);
                    context.WriteHeader();
                    WriteObject(value, wrapper, context);

                    if (options.UseCompression != Compression.Lz4CompressionMode.None)
                        ThrowCompressionNotSupportedWithPipeWriter(context);

                    var bytesWritten = context.Output.GetTotalPosition(context._position);
                    context.Output.Flush(context._buffer, context._position);
                    return bytesWritten;
                }
            }

            var actualValue = ConvertExpressionValue(value, ref runtimeType);
            ScanForDuplicates(actualValue, runtimeType, context);
            context.WriteHeader();
            WriteValue(actualValue, runtimeType, context);

            if (options.UseCompression != Compression.Lz4CompressionMode.None)
                ThrowCompressionNotSupportedWithPipeWriter(context);

            var totalBytesWritten = context.Output.GetTotalPosition(context._position);
            context.Output.Flush(context._buffer, context._position);
            return totalBytesWritten;
        }
        finally
        {
            context.Output = default;
            ReturnContext(context, options);
        }
    }

    /// <summary>
    /// Get the serialized size without allocating the final array.
    /// Useful for pre-allocating buffers.
    /// </summary>
    public static int GetSerializedSize<T>(T value, AcBinarySerializerOptions options)
    {
        if (value == null) return 1;

        var runtimeType = typeof(T);
        var context = AcquireArrayOutputContext(options);

        try
        {
            ScanForDuplicates(value, runtimeType, context);
            context.WriteHeader();
            WriteValue(value, runtimeType, context);
            return context.Position;
        }
        finally
        {
            ReturnContext(context, options);
        }
    }

    /// <summary>
    /// Serialize object and keep the pooled buffer for zero-copy consumers.
    /// Caller must dispose the returned result to release the buffer.
    /// Note: Compression is applied if enabled in options, result will be immutable (not pooled).
    /// </summary>
    public static BinarySerializationResult SerializeToPooledBuffer<T>(T value, AcBinarySerializerOptions options)
    {
        if (value == null) return BinarySerializationResult.FromImmutable([BinaryTypeCode.Null]);

        var runtimeType = typeof(T);
        var context = AcquireArrayOutputContext(options);

        try
        {
            ScanForDuplicates(value, runtimeType, context);
            context.WriteHeader();
            WriteValue(value, runtimeType, context);

            // If compression enabled, compress directly from buffer span (1 allocation)
            if (options.UseCompression != Lz4CompressionMode.None)
            {
                var compressed = Lz4.Compress(context.Output.AsSpan(context._buffer, context._position), options.UseCompression);
                return BinarySerializationResult.FromImmutable(compressed);
            }

            return context.Output.DetachResult(context._buffer, context._position);
        }
        finally
        {
            ReturnContext(context, options);
        }
    }

    #region Entry Point Helpers

    /// <summary>
    /// Acquires a pooled ArrayBinaryOutput context and initializes the output buffer.
    /// Reuses pooled ArrayBinaryOutput instance when available.
    /// AggressiveInlining: JIT must see buffer init in caller scope for register allocation.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static BinarySerializationContext<ArrayBinaryOutput> AcquireArrayOutputContext(AcBinarySerializerOptions options)
    {
        var context = BinarySerializationContextPool<ArrayBinaryOutput>.Get(options);
        if (!context.OutputInitialized)
        {
            context.Output = new ArrayBinaryOutput(options.InitialBufferCapacity);
            context.OutputInitialized = true;
        }
        context.Output.Initialize(out context._buffer, out context._position, out context._bufferEnd);
        return context;
    }

    /// <summary>
    /// Returns a serialization context to its pool. Uses async return when UseAsync is enabled.
    /// </summary>
    private static void ReturnContext<TOutput>(BinarySerializationContext<TOutput> context, AcBinarySerializerOptions options)
        where TOutput : struct, IBinaryOutputBase
    {
        if (options.UseAsync) BinarySerializationContextPool<TOutput>.ReturnAsync(context);
        else BinarySerializationContextPool<TOutput>.Return(context);
    }

    /// <summary>
    /// Converts IQueryable/Expression values to AcExpressionNode for serialization.
    /// Returns the converted value (or original if no conversion needed).
    /// </summary>
    [MethodImpl(MethodImplOptions.NoInlining)]
    private static object ConvertExpressionValue<T>(T value, ref Type runtimeType)
    {
        if (value is IQueryable queryable)
        {
            runtimeType = typeof(AcExpressionNode);
            return AcSerializerCommon.QueryableToNode(queryable);
        }
        if (AcSerializerCommon.IsExpressionType(runtimeType))
        {
            runtimeType = typeof(AcExpressionNode);
            return AcExpressionConverter.ToNode((Expression)(object)value!);
        }
        return value!;
    }

    /// <summary>
    /// Flushes output and throws NotSupportedException for compression with IBufferWriter.
    /// </summary>
    [MethodImpl(MethodImplOptions.NoInlining)]
    private static void ThrowCompressionNotSupportedWithBufferWriter(BinarySerializationContext<BufferWriterBinaryOutput> context)
    {
        context.Output.Flush(context._buffer, context._position);
        throw new NotSupportedException(
            "Compression is not supported with IBufferWriter output. " +
            "Use the byte[] overload or disable compression.");
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static void ThrowCompressionNotSupportedWithPipeWriter(BinarySerializationContext<AsyncPipeWriterOutput> context)
    {
        context.Output.Flush(context._buffer, context._position);
        throw new NotSupportedException(
            "Compression is not supported with PipeWriter output. " +
            "Use the byte[] overload or disable compression.");
    }

    #endregion

    #endregion

    #region Generated Writer Bridge Methods

    /// <summary>
    /// Bridge for generated writers: writes a non-null complex/collection value directly.
    /// Skips null check (caller handles it) and TryWritePrimitive (caller knows it's complex).
    /// Equivalent to the runtime's WriteValueNonPrimitive path.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    internal static void WriteValueGenerated<TOutput>(object value, Type type, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        WriteValueNonPrimitive(value, type, context);
    }

    /// <summary>
    /// Bridge for generated writers to call the runtime WriteString.
    /// <para>FastWire mode: markerless wire — delegates to <see cref="BinarySerializationContext{TOutput}.WriteStringUtf16Markerless"/>
    /// which handles all three states (null / empty / content) via int32 sentinel header.</para>
    /// <para>Compact mode: existing markerful path — null → <c>PropertySkip</c>, empty → <c>StringEmpty</c>,
    /// content → <see cref="WriteString{TOutput}"/> with marker dispatch.</para>
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    internal static void WriteStringGenerated<TOutput>(string? value, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        if (context.FastWire)
        {
            context.WriteStringUtf16Markerless(value);
            return;
        }

        if (string.IsNullOrEmpty(value))
        {
            context.WriteByte(value == null ? BinaryTypeCode.PropertySkip : BinaryTypeCode.StringEmpty);
            return;
        }

        WriteString(value, context);
    }

    /// <summary>
    /// Bridge for generated writers: writes a non-null complex OBJECT directly.
    /// Skips WriteValueNonPrimitive type dispatch (is byte[]?, is IDictionary?, is IEnumerable?, GetWrapper)
    /// because the SrcGen knows at compile-time that the property is a complex object.
    /// Uses pre-resolved wrapper type to avoid GetWrapper dictionary lookup.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    internal static void WriteObjectGenerated<TOutput>(object value, Type type, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        var wrapper = context.GetWrapper(type);
        WriteObject(value, wrapper, context);
    }

    /// <summary>
    /// Bridge for generated writers: writes a non-null complex OBJECT using slot-based wrapper lookup.
    /// SGen types pass their compile-time known slot index — avoids GetWrapper dictionary lookup.
    /// First call per slot per context: populates slot from GetWrapper. Subsequent calls: direct array index.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    internal static void WriteObjectGenerated<TOutput>(object value, Type type, int wrapperSlot, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        var wrapper = context.GetWrapper(type, wrapperSlot);
        WriteObject(value, wrapper, context);
    }

    #endregion

    #region Value Writing
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void WriteValue<TOutput>(object? value, Type type, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        if (value == null)
        {
            context.WriteByte(BinaryTypeCode.Null);
            return;
        }

        // Try writing as primitive first
        if (TryWritePrimitive(value, type, context))
            return;

        WriteValueNonPrimitive(value, type, context);
    }

    /// <summary>
    /// Writes a non-primitive value (collection, dictionary, byte[], or complex object).
    /// Skips null check and TryWritePrimitive — caller guarantees value is non-null and not a primitive type.
    /// Called from WritePropertyOrSkip default case (PropertyAccessorType.Object) and WriteValue fallback.
    /// </summary>
    private static void WriteValueNonPrimitive<TOutput>(object value, Type type, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        // Nullable<T> where T is a value type: boxed value may be a primitive.
        // Only Nullable<T> can be a value type in the Object accessor path.
        if (type.IsValueType)
        {
            if (TryWritePrimitive(value, type, context))
                return;
        }

        // Handle byte arrays specially (value-like, no reference tracking)
        if (value is byte[] byteArray)
        {
            WriteByteArray(byteArray, context);
            return;
        }

        // Handle dictionaries
        if (value is IDictionary dictionary)
        {
            WriteDictionary(dictionary, context);
            return;
        }

        // Get wrapper once — used by both WriteArray and WriteObject
        var wrapper = context.GetWrapper(type);

        // Handle collections/arrays
        if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType))
        {
            WriteArray(enumerable, wrapper, context);
            return;
        }

        // Handle complex objects with single-pass reference tracking
        WriteObject(value, wrapper, context);
    }

    /// <summary>
    /// Writes a non-primitive value with a pre-resolved wrapper (from PropertyTypeWrappers cache).
    /// Avoids GetWrapper dictionary lookup. Handles byte[], dictionary, collection, and complex objects.
    /// </summary>
    private static void WriteValueNonPrimitiveWithWrapper<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        var type = wrapper.Metadata.SourceType;

        // Nullable<T> where T is a value type: boxed value may be a primitive.
        if (type.IsValueType)
        {
            if (TryWritePrimitive(value, value.GetType(), context))
                return;
        }

        // Handle byte arrays specially (value-like, no reference tracking)
        if (value is byte[] byteArray)
        {
            WriteByteArray(byteArray, context);
            return;
        }

        // Handle dictionaries
        if (value is IDictionary dictionary)
        {
            WriteDictionary(dictionary, context);
            return;
        }

        // Handle collections/arrays
        if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType))
        {
            WriteArray(enumerable, wrapper, context);
            return;
        }

        // Handle complex objects
        WriteObject(value, wrapper, context);
    }

    /// <summary>
    /// Polymorphic variant of WriteValueNonPrimitiveWithWrapper.
    /// Cold path: polymorphism is rare. Writes poly prefix for non-object types,
    /// delegates to WriteObjectPolymorphic for combined poly+ref marker handling.
    /// </summary>
    [MethodImpl(MethodImplOptions.NoInlining)]
    private static void WriteValueNonPrimitiveWithWrapperPoly<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, Type polyRuntimeType)
        where TOutput : struct, IBinaryOutputBase
    {
        var type = wrapper.Metadata.SourceType;

        if (type.IsValueType)
        {
            if (TryWritePrimitive(value, value.GetType(), context))
                return;
        }

        if (value is byte[] byteArray)
        {
            context.WritePolymorphicPrefix(polyRuntimeType);
            WriteByteArray(byteArray, context);
            return;
        }

        if (value is IDictionary dictionary)
        {
            context.WritePolymorphicPrefix(polyRuntimeType);
            WriteDictionary(dictionary, context);
            return;
        }

        if (value is IEnumerable enumerable && !ReferenceEquals(type, StringType))
        {
            context.WritePolymorphicPrefix(polyRuntimeType);
            WriteArray(enumerable, wrapper, context);
            return;
        }

        // Complex object — handles combined poly+ref markers
        WriteObjectPolymorphic(value, wrapper, context, polyRuntimeType);
    }

    /// <summary>
    /// Optimized primitive writer using TypeCode dispatch.
    /// Avoids Nullable.GetUnderlyingType in hot path by using cached type info.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static bool TryWritePrimitive<TOutput>(object value, Type type, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        // Fast path: check TypeCode first (handles most primitives)
        var typeCode = Type.GetTypeCode(type);

        switch (typeCode)
        {
            case TypeCode.Int32:
                WriteInt32((int)value, context);
                return true;
            case TypeCode.Int64:
                WriteInt64((long)value, context);
                return true;
            case TypeCode.Boolean:
                context.WriteByte((bool)value ? BinaryTypeCode.True : BinaryTypeCode.False);
                return true;
            case TypeCode.Double:
                WriteFloat64Unsafe((double)value, context);
                return true;
            case TypeCode.String:
                WriteString((string)value, context);
                return true;
            case TypeCode.Single:
                WriteFloat32Unsafe((float)value, context);
                return true;
            case TypeCode.Decimal:
                WriteDecimalUnsafe((decimal)value, context);
                return true;
            case TypeCode.DateTime:
                WriteDateTimeUnsafe((DateTime)value, context);
                return true;
            case TypeCode.Byte:
                context.WriteByte(BinaryTypeCode.UInt8);
                context.WriteByte((byte)value);
                return true;
            case TypeCode.Int16:
                WriteInt16Unsafe((short)value, context);
                return true;
            case TypeCode.UInt16:
                WriteUInt16Unsafe((ushort)value, context);
                return true;
            case TypeCode.UInt32:
                WriteUInt32((uint)value, context);
                return true;
            case TypeCode.UInt64:
                WriteUInt64((ulong)value, context);
                return true;
            case TypeCode.SByte:
                context.WriteByte(BinaryTypeCode.Int8);
                context.WriteByte(unchecked((byte)(sbyte)value));
                return true;
            case TypeCode.Char:
                WriteCharUnsafe((char)value, context);
                return true;
        }

        // Handle special types by reference comparison (faster than type equality)
        if (ReferenceEquals(type, GuidType))
        {
            WriteGuidUnsafe((Guid)value, context);
            return true;
        }
        if (ReferenceEquals(type, DateTimeOffsetType))
        {
            WriteDateTimeOffsetUnsafe((DateTimeOffset)value, context);
            return true;
        }
        if (ReferenceEquals(type, TimeSpanType))
        {
            WriteTimeSpanUnsafe((TimeSpan)value, context);
            return true;
        }
        if (type.IsEnum)
        {
            WriteEnum(value, context);
            return true;
        }

        // Handle nullable types - use cached check instead of GetUnderlyingType
        // For nullable, value is already unwrapped when boxed, so we can use value.GetType()
        if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
        {
            // When boxed, nullable value types are unwrapped to their underlying type
            // So we can just call TryWritePrimitive with the actual runtime type
            return TryWritePrimitive(value, value.GetType(), context);
        }

        return false;
    }

    #endregion

    #region Optimized Primitive Writers using MemoryMarshal

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void WriteInt32<TOutput>(int value, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        if (BinaryTypeCode.TryEncodeTinyInt(value, out var tiny))
        {
            context.WriteByte(tiny);
            return;
        }
        context.WriteByte(BinaryTypeCode.Int32);
        context.WriteVarInt(value);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void WriteInt64<TOutput>(long value, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        if (value >= int.MinValue && value <= int.MaxValue)
        {
            WriteInt32((int)value, context);
            return;
        }
        context.WriteByte(BinaryTypeCode.Int64);
        context.WriteVarLong(value);
    }

    /// <summary>
    /// Optimized float64 writer using batched write.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void WriteFloat64Unsafe<TOutput>(double value, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        context.WriteTypeCodeAndRaw(BinaryTypeCode.Float64, value);
    }

    /// <summary>
    /// Optimized float32 writer using batched write.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void WriteFloat32Unsafe<TOutput>(float value, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        context.WriteTypeCodeAndRaw(BinaryTypeCode.Float32, value);
    }

    /// <summary>
    /// Optimized decimal writer using direct memory copy of bits.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void WriteDecimalUnsafe<TOutput>(decimal value, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        context.WriteByte(BinaryTypeCode.Decimal);
        context.WriteDecimalBits(value);
    }

    /// <summary>
    /// Optimized DateTime writer.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void WriteDateTimeUnsafe<TOutput>(DateTime value, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        context.WriteByte(BinaryTypeCode.DateTime);
        context.WriteDateTimeBits(value);
    }

    /// <summary>
    /// Optimized Guid writer using direct memory copy.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void WriteGuidUnsafe<TOutput>(Guid value, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        context.WriteByte(BinaryTypeCode.Guid);
        context.WriteGuidBits(value);
    }

    /// <summary>
    /// Optimized DateTimeOffset writer.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void WriteDateTimeOffsetUnsafe<TOutput>(DateTimeOffset value, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        context.WriteByte(BinaryTypeCode.DateTimeOffset);
        context.WriteDateTimeOffsetBits(value);
    }

    /// <summary>
    /// Optimized TimeSpan writer.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void WriteTimeSpanUnsafe<TOutput>(TimeSpan value, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        context.WriteTypeCodeAndRaw(BinaryTypeCode.TimeSpan, value.Ticks);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void WriteInt16Unsafe<TOutput>(short value, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        context.WriteTypeCodeAndRaw(BinaryTypeCode.Int16, value);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void WriteUInt16Unsafe<TOutput>(ushort value, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        context.WriteTypeCodeAndRaw(BinaryTypeCode.UInt16, value);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void WriteUInt32<TOutput>(uint value, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        context.WriteByte(BinaryTypeCode.UInt32);
        context.WriteVarUInt(value);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void WriteUInt64<TOutput>(ulong value, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        context.WriteByte(BinaryTypeCode.UInt64);
        context.WriteVarULong(value);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void WriteCharUnsafe<TOutput>(char value, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        context.WriteByte(BinaryTypeCode.Char);
        context.WriteRaw(value);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void WriteEnum<TOutput>(object value, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        // Use direct unboxing instead of Convert.ToInt32 to avoid NumberFormatInfo overhead
        var intValue = GetEnumAsInt32Fast(value);
        if (BinaryTypeCode.TryEncodeTinyInt(intValue, out var tiny))
        {
            context.WriteByte(BinaryTypeCode.Enum);
            context.WriteByte(tiny);
            return;
        }
        context.WriteByte(BinaryTypeCode.Enum);
        context.WriteByte(BinaryTypeCode.Int32);
        context.WriteVarInt(intValue);
    }

    /// <summary>
    /// Fast enum to int conversion avoiding Convert.ToInt32 overhead.
    /// Handles all common enum underlying types.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static int GetEnumAsInt32Fast(object enumValue)
    {
        var type = enumValue.GetType();
        var underlyingType = type.GetEnumUnderlyingType();

        if (ReferenceEquals(underlyingType, IntType))
            return (int)enumValue;
        if (ReferenceEquals(underlyingType, typeof(byte)))
            return (byte)enumValue;
        if (ReferenceEquals(underlyingType, typeof(sbyte)))
            return (sbyte)enumValue;
        if (ReferenceEquals(underlyingType, typeof(short)))
            return (short)enumValue;
        if (ReferenceEquals(underlyingType, typeof(ushort)))
            return (ushort)enumValue;
        if (ReferenceEquals(underlyingType, typeof(uint)))
            return unchecked((int)(uint)enumValue);

        // Fallback for rare cases (long, ulong)
        return Convert.ToInt32(enumValue);
    }

    /// <summary>
    /// Optimized string writer with FixStr for short strings.
    /// Uses pre-computed WriteDuplicateEntry cursor for interning (no IdentityMap lookup).
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void WriteString<TOutput>(string value, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        if (context.FastWire)
        {
            // FastWire: markerless UTF-16 — WriteStringUtf16Markerless handles null / empty / content
            // via the int32 sentinel header. Interning is intentionally skipped in FastWire today
            // (BINARY_ISSUES.md#accore-bin-i-k3w8 — to be reconciled separately).
            context.WriteStringUtf16Markerless(value);
            return;
        }

        if (value.Length == 0)
        {
            context.WriteByte(BinaryTypeCode.StringEmpty);
            return;
        }

        // Interning attempt — TryWriteInternedString reads + resets StringInternEligible and emits
        // the InternFirst / InternRef wire form when the write-plan cursor yields an entry. Returns
        // false (string not interned) → fall through to the tier dispatch below.
        if (context.TryWriteInternedString(value))
            return;

        // Marker-dispatch (ACCORE-BIN-T-M3R7): WriteStringWithDispatch encodes UTF-8 once, detects
        // ASCII via bytesWritten == charLength, and emits the optimal wire marker:
        //   • bytesWritten ≤ 31 + ASCII   → FixStrAscii (single byte marker + length, ASCII payload)
        //   • bytesWritten ≤ 31 + UTF-8   → FixStr      (single byte marker + length, UTF-8 payload)
        //   • bytesWritten > 31  + ASCII  → StringAscii (marker + VarUInt length + ASCII payload)
        //   • bytesWritten > 31  + UTF-8  → String      (marker + VarUInt length + UTF-8 payload)
        // Reader dispatches on the ASCII marker to skip UTF-8 decode (byte→char widen only).
        context.WriteStringWithDispatch(value);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void WriteByteArray<TOutput>(byte[] value, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        context.WriteByte(BinaryTypeCode.ByteArray);
        context.WriteVarUInt((uint)value.Length);
        context.WriteBytes(value);
    }

    /// <summary>
    /// String intern 2nd occurrence — cold path, just writes reference index.
    /// </summary>
    //[MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void WriteStringInternRef<TOutput>(BinarySerializationContext<TOutput> context, int cacheMapIndex)
        where TOutput : struct, IBinaryOutputBase
    {
        context.WriteByte(BinaryTypeCode.StringInterned);
        context.WriteVarUInt((uint)cacheMapIndex);
    }

    /// <summary>
    /// Object ref 2nd occurrence — cold path, just writes reference index.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void WriteObjectRef<TOutput>(BinarySerializationContext<TOutput> context, int cacheMapIndex)
        where TOutput : struct, IBinaryOutputBase
    {
        context.WriteByte(BinaryTypeCode.ObjectRef);
        context.WriteVarUInt((uint)cacheMapIndex);
    }

    #endregion

    #region Complex Type Writers

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void WriteObject<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        // Combined check+inc FIRST — fires BEFORE any marker write so Truncate writes Null wire-correctly.
        // TryEnterRecursion inc'd _recursionDepth on success.
        if (context.TryEnterRecursion(hasTruncatePath: true)) return;

        var metadata = wrapper.Metadata;
        var useMetaForType = context.UseMetadata && metadata.EnableMetadataFeature;

        // Ref-handling dispatch. The variants check write plan: on 2nd-occurrence ObjectRef they undo the inc
        // via ExitRecursion (we already inc'd above but no body recursion will happen for that path).
        if (context.UseTypeReferenceHandling(metadata))
        {
            if (useMetaForType)
                WriteObjectWithRefHandlingMeta(value, wrapper, context);
            else
                WriteObjectWithRefHandling(value, wrapper, context);
            return;
        }

        // Non-ref path: write marker + body. Inc already done by TryEnterRecursion; matched by ExitRecursion
        // at WriteObjectProperties body exit.

        if (useMetaForType)
        {
            // UseMetadata: típus regisztrálása (első vs ismételt előfordulás tracking)
            var isFirstMetadataOccurrence = BinarySerializationContext<TOutput>.RegisterMetadataType(wrapper);

            // Marker kiírása — no ref handling, no cachedObjectCacheIndex
            context.WriteByte(BinaryTypeCode.ObjectWithMetadata);
            context.WriteInlineMetadata(wrapper.Metadata, isFirstMetadataOccurrence);
        }
        else
        {
            // FixObj: assign slot on first occurrence this session
            if (!wrapper.PolymorphicSeen)
            {
                wrapper.PolymorphicSeen = true;
                wrapper.PolymorphicCacheIndex = context._nextTypeSlot++;
            }
            if (wrapper.PolymorphicCacheIndex < BinaryTypeCode.SlotCount)
                context.WriteByte((byte)wrapper.PolymorphicCacheIndex);
            else
                context.WriteByte(BinaryTypeCode.Object);
        }

        WriteObjectProperties(value, wrapper, context, useMetaForType);
    }

    /// <summary>
    /// WriteObject variant with reference handling, no metadata.
    /// Cold path: only IId types with ref tracking enabled.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void WriteObjectWithRefHandling<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        // Caller (WriteObject) already inc'd via TryEnterRecursion. 2nd-occurrence path undoes it.
        var cachedObjectCacheIndex = -1;
        if (context.TryConsumeWritePlanEntry(out var planEntry))
        {
            ValidateWritePlanObject(in planEntry, value, wrapper);
            if (planEntry.IsFirst)
            {
                cachedObjectCacheIndex = planEntry.CacheMapIndex;
            }
            else
            {
                // 2nd-occurrence: ObjectRef written, no body to recurse into → undo the caller's inc.
                context.ExitRecursion();
                WriteObjectRef(context, planEntry.CacheMapIndex);
                return;
            }
        }

        // Marker kiírása — no metadata (inc already done by caller's TryEnterRecursion)
        if (cachedObjectCacheIndex >= 0)
        {
            context.WriteByte(BinaryTypeCode.ObjectRefFirst);
            context.WriteVarUInt((uint)cachedObjectCacheIndex);
        }
        else
        {
            if (!wrapper.PolymorphicSeen)
            {
                wrapper.PolymorphicSeen = true;
                wrapper.PolymorphicCacheIndex = context._nextTypeSlot++;
            }
            if (wrapper.PolymorphicCacheIndex < BinaryTypeCode.SlotCount)
                context.WriteByte((byte)wrapper.PolymorphicCacheIndex);
            else
                context.WriteByte(BinaryTypeCode.Object);
        }

        WriteObjectProperties(value, wrapper, context, useMetaForType: false);
    }

    /// <summary>
    /// WriteObject variant with reference handling + metadata.
    /// Cold path: IId types with ref tracking + UseMetadata enabled.
    /// </summary>
    //[MethodImpl(MethodImplOptions.NoInlining)]
    private static void WriteObjectWithRefHandlingMeta<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        var isFirstMetadataOccurrence = BinarySerializationContext<TOutput>.RegisterMetadataType(wrapper);

        // Caller (WriteObject) already inc'd via TryEnterRecursion. 2nd-occurrence path undoes it.
        var cachedObjectCacheIndex = -1;
        if (context.TryConsumeWritePlanEntry(out var planEntry))
        {
            ValidateWritePlanObject(in planEntry, value, wrapper);
            if (planEntry.IsFirst)
            {
                cachedObjectCacheIndex = planEntry.CacheMapIndex;
            }
            else
            {
                // 2nd-occurrence: ObjectRef written, no body to recurse into → undo the caller's inc.
                context.ExitRecursion();
                WriteObjectRef(context, planEntry.CacheMapIndex);
                return;
            }
        }

        // Marker kiírása — with metadata (inc already done by caller's TryEnterRecursion)
        if (cachedObjectCacheIndex >= 0)
        {
            context.WriteByte(BinaryTypeCode.ObjectWithMetadataRefFirst);
            context.WriteVarUInt((uint)cachedObjectCacheIndex);
        }
        else
        {
            context.WriteByte(BinaryTypeCode.ObjectWithMetadata);
        }
        context.WriteInlineMetadata(wrapper.Metadata, isFirstMetadataOccurrence);

        WriteObjectProperties(value, wrapper, context, useMetaForType: true);
    }

    /// <summary>
    /// Shared property writing loop — used by WriteObject, WriteObjectWithRefHandling, WriteObjectPolymorphic.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void WriteObjectProperties<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, bool useMetaForType)
        where TOutput : struct, IBinaryOutputBase
    {
        if (context.UseGeneratedCode)
        {
            var generatedWriter = wrapper.GeneratedWriter;
            if (generatedWriter != null)
            {
                // SGen path: generated WriteProperties ends with its own ExitRecursion call.
                generatedWriter.WriteProperties(value, context);
                return;
            }
        }

        // Runtime path: depth-check + EnterRecursion was done by the caller (WriteObject or variants)
        // BEFORE marker write. Body just runs property writes; ExitRecursion at end balances EnterRecursion.
        if (!useMetaForType)
        {
            WritePropertiesMarkerless(value, wrapper, context);
        }
        else
        {
            WritePropertiesWithMeta(value, wrapper, context);
        }

        context.ExitRecursion();
    }

    private static void WritePropertiesWithMeta<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context) where TOutput : struct, IBinaryOutputBase
    {
        var properties = wrapper.Metadata.Properties;
        var propCount = properties.Length;
        // Combined runtime+attribute gate: if the type opted out of the filter feature via
        // [AcBinarySerializable(EnablePropertyFilterFeature = false)], the runtime PropertyFilter is
        // ignored for this type — short-circuit eliminates the per-property `ShouldSerializeProperty`
        // call entirely. Mirrors the SGen-side gate in `EmitProp`.
        var hasPropertyFilter = context.HasPropertyFilter && wrapper.Metadata.EnablePropertyFilterFeature;

        for (var i = 0; i < propCount; i++)
        {
            var prop = properties[i];

            if (hasPropertyFilter && !context.ShouldSerializeProperty(value, prop))
            {
                context.WriteByte(BinaryTypeCode.PropertySkip);
                continue;
            }

            WritePropertyOrSkip(value, prop, wrapper, context);
        }
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void WritePropertiesMarkerless<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context) where TOutput : struct, IBinaryOutputBase
    {
        var properties = wrapper.Metadata.Properties;
        var propCount = properties.Length;
        // Combined runtime+attribute gate — see WritePropertiesWithMeta for the rationale.
        var hasPropertyFilter = context.HasPropertyFilter && wrapper.Metadata.EnablePropertyFilterFeature;

        for (var i = 0; i < propCount; i++)
        {
            var prop = properties[i];

            if (!prop.ExpectedTypeCode.HasValue && hasPropertyFilter && !context.ShouldSerializeProperty(value, prop))
            {
                context.WriteByte(BinaryTypeCode.PropertySkip);
            }
            else
            {
                WritePropertyOrSkip(value, prop, wrapper, context);
            }
        }
    }

    /// <summary>
    /// Polymorphic marker writing — extracted from WriteObject to keep hot path small.
    /// Cold path: polymorphism is rare, NoInlining call overhead acceptable.
    /// </summary>
    [MethodImpl(MethodImplOptions.NoInlining)]
    private static void WritePolymorphicMarker<TOutput>(BinarySerializationContext<TOutput> context, Type polyRuntimeType, int cachedObjectCacheIndex)
        where TOutput : struct, IBinaryOutputBase
    {
        if (cachedObjectCacheIndex >= 0)
        {
            // Combined poly + RefFirst marker (69/71)
            context.WritePolymorphicPrefix(polyRuntimeType, cachedObjectCacheIndex);
        }
        else
        {
            var rtWrapper = context.GetWrapper(polyRuntimeType);
            if (rtWrapper.PolymorphicSeen && rtWrapper.PolymorphicCacheIndex < BinaryTypeCode.SlotCount)
            {
                // 2+ poly in this session → FixObj (1 byte)
                context.WriteByte((byte)rtWrapper.PolymorphicCacheIndex);
            }
            else
            {
                // First poly in this session → ObjectWithTypeName + assigns slot
                context.WritePolymorphicPrefix(polyRuntimeType);
                context.WriteByte(BinaryTypeCode.Object);
            }
        }
    }

    /// <summary>
    /// Polymorphic object writing — handles combined poly+ref markers.
    /// Cold path: polymorphism is rare, NoInlining acceptable.
    /// Poly always implies UseMetadata=false (checked in WritePropertyOrSkip).
    /// </summary>
    [MethodImpl(MethodImplOptions.NoInlining)]
    private static void WriteObjectPolymorphic<TOutput>(object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context, Type polyRuntimeType)
        where TOutput : struct, IBinaryOutputBase
    {
        // Combined check+inc FIRST — fires BEFORE any marker write so Truncate writes Null wire-correctly.
        if (context.TryEnterRecursion(hasTruncatePath: true)) return;

        var metadata = wrapper.Metadata;

        // Reference handling — 2nd-occurrence ObjectRef path undoes the inc done above.
        var cachedObjectCacheIndex = -1;
        if (context.UseTypeReferenceHandling(metadata))
        {
            if (context.TryConsumeWritePlanEntry(out var planEntry))
            {
                ValidateWritePlanObject(in planEntry, value, wrapper);
                if (planEntry.IsFirst)
                {
                    cachedObjectCacheIndex = planEntry.CacheMapIndex;
                }
                else
                {
                    // 2nd-occurrence: ObjectRef written, no body to recurse into → undo the inc.
                    context.ExitRecursion();
                    WriteObjectRef(context, planEntry.CacheMapIndex);
                    return;
                }
            }
        }

        // 1st-occurrence or non-tracked: writes marker + body (inc already done by TryEnterRecursion)
        // Poly marker (handles combined poly+ref)
        WritePolymorphicMarker(context, polyRuntimeType, cachedObjectCacheIndex);

        WriteObjectProperties(value, wrapper, context, false);
    }

    /// <summary>
    /// Checks if a property value is null or default without boxing for value types.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static bool IsPropertyDefaultOrNull(object obj, BinaryPropertyAccessor prop)
    {
        switch (prop.AccessorType)
        {
            case PropertyAccessorType.Int32:
                return prop.GetInt32(obj) == 0;
            case PropertyAccessorType.Int64:
                return prop.GetInt64(obj) == 0L;
            case PropertyAccessorType.Boolean:
                return !prop.GetBoolean(obj);
            case PropertyAccessorType.Double:
                return prop.GetDouble(obj) == 0.0;
            case PropertyAccessorType.Single:
                return prop.GetSingle(obj) == 0f;
            case PropertyAccessorType.Decimal:
                return prop.GetDecimal(obj) == 0m;
            case PropertyAccessorType.Byte:
                return prop.GetByte(obj) == 0;
            case PropertyAccessorType.Int16:
                return prop.GetInt16(obj) == 0;
            case PropertyAccessorType.UInt16:
                return prop.GetUInt16(obj) == 0;
            case PropertyAccessorType.UInt32:
                return prop.GetUInt32(obj) == 0;
            case PropertyAccessorType.UInt64:
                return prop.GetUInt64(obj) == 0;
            case PropertyAccessorType.Guid:
                return prop.GetGuid(obj) == Guid.Empty;
            case PropertyAccessorType.Enum:
                return prop.GetEnumAsInt32(obj) == 0;
            case PropertyAccessorType.DateTime:
                // DateTime default is not typically skipped
                return false;
            default:
                // Object type - use regular getter
                var value = prop.GetValue(obj);
                if (value == null) return true;
                if (prop.PropertyTypeCode == TypeCode.String) return string.IsNullOrEmpty((string)value);
                return false;
        }
    }

    /// <summary>
    /// Writes a property value using typed getters to avoid boxing.
    /// </summary>
    //[MethodImpl(MethodImplOptions.AggressiveInlining)]
    //private static void WritePropertyValue<TOutput>(object obj, BinaryPropertyAccessor prop, BinarySerializationContext<TOutput> context, int depth)
    //    where TOutput : struct, IBinaryOutputBase
    //{
    //    switch (prop.AccessorType)
    //    {
    //        case PropertyAccessorType.Int32:
    //            WriteInt32(prop.GetInt32(obj), context);
    //            return;
    //        case PropertyAccessorType.Int64:
    //            WriteInt64(prop.GetInt64(obj), context);
    //            return;
    //        case PropertyAccessorType.Boolean:
    //            context.WriteByte(prop.GetBoolean(obj) ? BinaryTypeCode.True : BinaryTypeCode.False);
    //            return;
    //        case PropertyAccessorType.Double:
    //            WriteFloat64Unsafe(prop.GetDouble(obj), context);
    //            return;
    //        case PropertyAccessorType.Single:
    //            WriteFloat32Unsafe(prop.GetSingle(obj), context);
    //            return;
    //        case PropertyAccessorType.Decimal:
    //            WriteDecimalUnsafe(prop.GetDecimal(obj), context);
    //            return;
    //        case PropertyAccessorType.DateTime:
    //            WriteDateTimeUnsafe(prop.GetDateTime(obj), context);
    //            return;
    //        case PropertyAccessorType.Byte:
    //            context.WriteByte(BinaryTypeCode.UInt8);
    //            context.WriteByte(prop.GetByte(obj));
    //            return;
    //        case PropertyAccessorType.Int16:
    //            WriteInt16Unsafe(prop.GetInt16(obj), context);
    //            return;
    //        case PropertyAccessorType.UInt16:
    //            WriteUInt16Unsafe(prop.GetUInt16(obj), context);
    //            return;
    //        case PropertyAccessorType.UInt32:
    //            WriteUInt32(prop.GetUInt32(obj), context);
    //            return;
    //        case PropertyAccessorType.UInt64:
    //            WriteUInt64(prop.GetUInt64(obj), context);
    //            return;
    //        case PropertyAccessorType.Guid:
    //            WriteGuidUnsafe(prop.GetGuid(obj), context);
    //            return;
    //        case PropertyAccessorType.Enum:
    //            var enumValue = prop.GetEnumAsInt32(obj);
    //            if (BinaryTypeCode.TryEncodeTinyInt(enumValue, out var tiny))
    //            {
    //                context.WriteByte(BinaryTypeCode.Enum);
    //                context.WriteByte(tiny);
    //            }
    //            else
    //            {
    //                context.WriteByte(BinaryTypeCode.Enum);
    //                context.WriteByte(BinaryTypeCode.Int32);
    //                context.WriteVarInt(enumValue);
    //            }
    //            return;
    //        case PropertyAccessorType.String:
    //            {
    //                // Fast path: typed getter, no boxing, no Type.GetTypeCode() call
    //                var strValue = prop.GetString(obj);
    //                if (strValue != null)
    //                    WriteString(strValue, context);
    //                else
    //                    context.WriteByte(BinaryTypeCode.Null);
    //                return;
    //            }
    //        default:
    //            // Fallback to object getter for reference types
    //            var value = prop.GetValue(obj);
    //            WriteValue(value, prop.PropertyType, context, depth);
    //            return;
    //    }
    //}

    /// <summary>
    /// Writes a property value OR a skip marker if the value is default/null.
    /// Delegates to PropertyWriter bridge methods which handle UseMetadata internally:
    /// UseMetadata=true: skip marker for defaults, type code + value for non-defaults.
    /// UseMetadata=false (markerless): raw value only, no skip markers.
    /// </summary>
    /// <remarks>
    /// NativeAOT note: the polymorphism path uses <c>value.GetType()</c> to obtain the runtime type,
    /// which inherently loses the DAMs annotation chain. Polymorphic concrete types must therefore
    /// be rooted by the consumer (either via <c>[AcBinarySerializable]</c> + SGen, or
    /// <c>&lt;TrimmerRootAssembly&gt;</c>). The IL2072 suppression below is bounded to this
    /// well-known trimmer blind spot — runtime polymorphism is a fundamental limitation, not a bug.
    /// </remarks>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2072",
        Justification = "Polymorphism via obj.GetType() is a documented trimmer blind spot. Consumers must root "
                      + "polymorphic concrete types via [AcBinarySerializable] (SGen) or TrimmerRootAssembly.")]
    private static void WritePropertyOrSkip<TOutput>(object obj, BinaryPropertyAccessor prop, TypeMetadataWrapper<BinarySerializeTypeMetadata> parentWrapper, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        switch (prop.AccessorType)
        {
            case PropertyAccessorType.Int32:
                context.WriteInt32Property(prop.GetInt32(obj));
                return;
            case PropertyAccessorType.Int64:
                context.WriteInt64Property(prop.GetInt64(obj));
                return;
            case PropertyAccessorType.Boolean:
                context.WriteBoolProperty(prop.GetBoolean(obj));
                return;
            case PropertyAccessorType.Double:
                context.WriteFloat64Property(prop.GetDouble(obj));
                return;
            case PropertyAccessorType.Single:
                context.WriteFloat32Property(prop.GetSingle(obj));
                return;
            case PropertyAccessorType.Decimal:
                context.WriteDecimalProperty(prop.GetDecimal(obj));
                return;
            case PropertyAccessorType.DateTime:
                context.WriteDateTimeProperty(prop.GetDateTime(obj));
                return;
            case PropertyAccessorType.Byte:
                context.WriteByteProperty(prop.GetByte(obj));
                return;
            case PropertyAccessorType.Int16:
                context.WriteInt16Property(prop.GetInt16(obj));
                return;
            case PropertyAccessorType.UInt16:
                context.WriteUInt16Property(prop.GetUInt16(obj));
                return;
            case PropertyAccessorType.UInt32:
                context.WriteUInt32Property(prop.GetUInt32(obj));
                return;
            case PropertyAccessorType.UInt64:
                context.WriteUInt64Property(prop.GetUInt64(obj));
                return;
            case PropertyAccessorType.Guid:
                context.WriteGuidProperty(prop.GetGuid(obj));
                return;
            case PropertyAccessorType.Enum:
                context.WriteEnumInt32Property(prop.GetEnumAsInt32(obj));
                return;
            case PropertyAccessorType.String:
                {
                    // Fast path: typed getter, no boxing, no Type.GetTypeCode() call.
                    // FastWire: markerless int32 sentinel via `WriteStringUtf16Markerless` — wire-symmetric
                    // with `WriteStringGenerated` (SGen) so cross-mode interop holds. Compact: existing markered.
                    string? value = prop.GetString(obj);
                    if (context.FastWire)
                    {
                        context.WriteStringUtf16Markerless(value);
                        return;
                    }
                    if (string.IsNullOrEmpty(value))
                    {
                        context.WriteByte(value == null ? BinaryTypeCode.PropertySkip : BinaryTypeCode.StringEmpty);
                    }
                    else
                    {
                        context.StringInternEligible = prop.UseStringPropertyInterning(context.InternBit);
                        WriteString(value, context);
                    }
                    return;
                }
            default:
                {
                    // Object type (collection, complex object, byte[], dictionary)
                    // Use pre-cached wrapper, fallback to GetWrapper on miss/polymorphism
                    // Set interning eligibility for string collection elements
                    context.StringInternEligible = prop.IsStringCollectionProperty && prop.UseStringPropertyInterning(context.InternBit);

                    #if DEBUG
                    var value = GetPropertyValueWithDiagnostics(obj, prop);
                    #else
                    var value = prop.GetValue(obj);
                    #endif

                    // SKIP marker only for null (reference types)
                    // Empty string, empty collections, etc. are valid values and must be written!
                    if (value == null)
                    {
                        context.WriteByte(BinaryTypeCode.PropertySkip);
                    }
                    else
                    {
                        var runtimeType = value.GetType();

                        var complexIdx = prop.ComplexPropertyIndex;
                        if (complexIdx >= 0)
                        {
                            var propWrapper = parentWrapper.GetPropertyTypeWrapper(complexIdx, runtimeType);
                            if (propWrapper == null)
                            {
                                propWrapper = context.GetWrapper(runtimeType);
                                parentWrapper.SetPropertyTypeWrapper(complexIdx, propWrapper);
                            }
                            if (!context.UseMetadata && prop.IsPolymorphicCandidate && runtimeType != prop.PropertyType)
                                WriteValueNonPrimitiveWithWrapperPoly(value, propWrapper, context, runtimeType);
                            else
                                WriteValueNonPrimitiveWithWrapper(value, propWrapper, context);
                        }
                        else
                        {
                            // Non-complex in default case (nullable value type, etc.)
                            if (!context.UseMetadata && prop.IsPolymorphicCandidate && runtimeType != prop.PropertyType)
                                context.WritePolymorphicPrefix(runtimeType);
                            WriteValueNonPrimitive(value, runtimeType, context);
                        }
                    }
                    return;
                }
        }
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static object? GetPropertyValueWithDiagnostics(object source, BinaryPropertyAccessor property)
    {
        try
        {
            return property.GetValue(source);
        }
        catch (Exception ex) when (ex is NullReferenceException || ex is TargetInvocationException)
        {
            var sourceType = source.GetType().FullName ?? source.GetType().Name;
            var propertyName = property.Name;
            var declaredType = property.PropertyType?.FullName ?? "<unknown>";

            throw new InvalidOperationException(
                $"AcBinary runtime property write failed at {sourceType}.{propertyName} (declaredType={declaredType}, accessor={property.AccessorType}). " +
                "This usually indicates a null-sensitive property getter or malformed source data.",
                ex);
        }
    }

    #endregion

    #region Specialized Array Writers

    /// <summary>
    /// Optimized array writer with specialized paths for primitive collections.
    /// </summary>
    private static void WriteArray<TOutput>(IEnumerable enumerable, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        context.WriteByte(BinaryTypeCode.Array);

        // Use pre-computed metadata — no GetWrapper or GetCollectionElementType needed
        var metadata = wrapper.Metadata;
        var elementType = metadata.CollectionElementType;

        // Optimized path for primitive T[] and List<T> — unified via ReadOnlySpan<T>
        if (elementType != null)
        {
            if (TryWritePrimitiveCollection(enumerable, elementType, context))
                return;
        }

        // For IList, we can write the count directly and use indexed access
        if (enumerable is IList list)
        {
            var count = list.Count;
            context.WriteVarUInt((uint)count);
            for (var i = 0; i < count; i++)
            {
                var item = list[i];
                var itemType = item?.GetType() ?? typeof(object);
                WriteValue(item, itemType, context);
            }
            return;
        }

        // For ICollection (HashSet<T>, Queue<T>, SortedSet<T>, etc.) — has Count, no indexer
        if (enumerable is ICollection collection)
        {
            context.WriteVarUInt((uint)collection.Count);
            foreach (var item in enumerable)
            {
                var itemType = item?.GetType() ?? typeof(object);
                WriteValue(item, itemType, context);
            }
            return;
        }

        // For other IEnumerable (rare: custom IEnumerable without ICollection), collect first
        var items = new List<object?>();
        foreach (var item in enumerable)
        {
            items.Add(item);
        }

        context.WriteVarUInt((uint)items.Count);
        foreach (var item in items)
        {
            var itemType = item?.GetType() ?? typeof(object);
            WriteValue(item, itemType, context);
        }
    }

    /// <summary>
    /// Unified primitive collection writer for both T[] and List&lt;T&gt;.
    /// Extracts ReadOnlySpan&lt;T&gt; from either source (zero-cost implicit conversion for arrays,
    /// CollectionsMarshal.AsSpan for lists) and writes using bulk context methods with EnsureCapacity.
    /// </summary>
    private static bool TryWritePrimitiveCollection<TOutput>(IEnumerable enumerable, Type elementType, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        if (ReferenceEquals(elementType, IntType))
        {
            ReadOnlySpan<int> span;
            if (enumerable is int[] arr) span = arr;
            else if (enumerable is List<int> list) span = CollectionsMarshal.AsSpan(list);
            else return false;

            context.WriteVarUInt((uint)span.Length);
            context.WriteInt32ArrayOptimized(span);
            return true;
        }

        if (ReferenceEquals(elementType, LongType))
        {
            ReadOnlySpan<long> span;
            if (enumerable is long[] arr) span = arr;
            else if (enumerable is List<long> list) span = CollectionsMarshal.AsSpan(list);
            else return false;

            context.WriteVarUInt((uint)span.Length);
            context.WriteLongArrayOptimized(span);
            return true;
        }

        if (ReferenceEquals(elementType, DoubleType))
        {
            ReadOnlySpan<double> span;
            if (enumerable is double[] arr) span = arr;
            else if (enumerable is List<double> list) span = CollectionsMarshal.AsSpan(list);
            else return false;

            context.WriteVarUInt((uint)span.Length);
            context.WriteDoubleArrayBulk(span);
            return true;
        }

        if (ReferenceEquals(elementType, FloatType))
        {
            ReadOnlySpan<float> span;
            if (enumerable is float[] arr) span = arr;
            else if (enumerable is List<float> list) span = CollectionsMarshal.AsSpan(list);
            else return false;

            context.WriteVarUInt((uint)span.Length);
            context.WriteFloatArrayBulk(span);
            return true;
        }

        if (ReferenceEquals(elementType, BoolType))
        {
            ReadOnlySpan<bool> span;
            if (enumerable is bool[] arr) span = arr;
            else if (enumerable is List<bool> list) span = CollectionsMarshal.AsSpan(list);
            else return false;

            context.WriteVarUInt((uint)span.Length);
            for (var i = 0; i < span.Length; i++)
                context.WriteByte(span[i] ? BinaryTypeCode.True : BinaryTypeCode.False);
            return true;
        }

        if (ReferenceEquals(elementType, GuidType))
        {
            ReadOnlySpan<Guid> span;
            if (enumerable is Guid[] arr) span = arr;
            else if (enumerable is List<Guid> list) span = CollectionsMarshal.AsSpan(list);
            else return false;

            context.WriteVarUInt((uint)span.Length);
            context.WriteGuidArrayBulk(span);
            return true;
        }

        if (ReferenceEquals(elementType, DecimalType))
        {
            ReadOnlySpan<decimal> span;
            if (enumerable is decimal[] arr) span = arr;
            else if (enumerable is List<decimal> list) span = CollectionsMarshal.AsSpan(list);
            else return false;

            context.WriteVarUInt((uint)span.Length);
            for (var i = 0; i < span.Length; i++)
                WriteDecimalUnsafe(span[i], context);
            return true;
        }

        if (ReferenceEquals(elementType, DateTimeType))
        {
            ReadOnlySpan<DateTime> span;
            if (enumerable is DateTime[] arr) span = arr;
            else if (enumerable is List<DateTime> list) span = CollectionsMarshal.AsSpan(list);
            else return false;

            context.WriteVarUInt((uint)span.Length);
            for (var i = 0; i < span.Length; i++)
                WriteDateTimeUnsafe(span[i], context);
            return true;
        }

        if (ReferenceEquals(elementType, StringType))
        {
            ReadOnlySpan<string?> span;
            if (enumerable is string?[] arr) span = arr;
            else if (enumerable is List<string?> list) span = CollectionsMarshal.AsSpan(list);
            else return false;

            context.WriteVarUInt((uint)span.Length);
            for (var i = 0; i < span.Length; i++)
            {
                var s = span[i];
                if (s == null)
                    context.WriteByte(BinaryTypeCode.Null);
                else
                    WriteString(s, context);
            }
            return true;
        }

        if (ReferenceEquals(elementType, ShortType))
        {
            ReadOnlySpan<short> span;
            if (enumerable is short[] arr) span = arr;
            else if (enumerable is List<short> list) span = CollectionsMarshal.AsSpan(list);
            else return false;

            context.WriteVarUInt((uint)span.Length);
            for (var i = 0; i < span.Length; i++)
                WriteInt16Unsafe(span[i], context);
            return true;
        }

        if (ReferenceEquals(elementType, UShortType))
        {
            ReadOnlySpan<ushort> span;
            if (enumerable is ushort[] arr) span = arr;
            else if (enumerable is List<ushort> list) span = CollectionsMarshal.AsSpan(list);
            else return false;

            context.WriteVarUInt((uint)span.Length);
            for (var i = 0; i < span.Length; i++)
                WriteUInt16Unsafe(span[i], context);
            return true;
        }

        if (ReferenceEquals(elementType, UIntType))
        {
            ReadOnlySpan<uint> span;
            if (enumerable is uint[] arr) span = arr;
            else if (enumerable is List<uint> list) span = CollectionsMarshal.AsSpan(list);
            else return false;

            context.WriteVarUInt((uint)span.Length);
            for (var i = 0; i < span.Length; i++)
                WriteUInt32(span[i], context);
            return true;
        }

        if (ReferenceEquals(elementType, ULongType))
        {
            ReadOnlySpan<ulong> span;
            if (enumerable is ulong[] arr) span = arr;
            else if (enumerable is List<ulong> list) span = CollectionsMarshal.AsSpan(list);
            else return false;

            context.WriteVarUInt((uint)span.Length);
            for (var i = 0; i < span.Length; i++)
                WriteUInt64(span[i], context);
            return true;
        }

        if (ReferenceEquals(elementType, ByteType))
        {
            // Note: top-level byte[] goes through WriteByteArray (BinaryTypeCode.ByteArray).
            // This handles List<byte> and byte[] when reached via WriteArray (element-level encoding).
            ReadOnlySpan<byte> span;
            if (enumerable is byte[] arr) span = arr;
            else if (enumerable is List<byte> list) span = CollectionsMarshal.AsSpan(list);
            else return false;

            context.WriteVarUInt((uint)span.Length);
            for (var i = 0; i < span.Length; i++)
            {
                context.WriteByte(BinaryTypeCode.UInt8);
                context.WriteByte(span[i]);
            }
            return true;
        }

        if (ReferenceEquals(elementType, SByteType))
        {
            ReadOnlySpan<sbyte> span;
            if (enumerable is sbyte[] arr) span = arr;
            else if (enumerable is List<sbyte> list) span = CollectionsMarshal.AsSpan(list);
            else return false;

            context.WriteVarUInt((uint)span.Length);
            for (var i = 0; i < span.Length; i++)
            {
                context.WriteByte(BinaryTypeCode.Int8);
                context.WriteByte(unchecked((byte)span[i]));
            }
            return true;
        }

        if (ReferenceEquals(elementType, CharType))
        {
            ReadOnlySpan<char> span;
            if (enumerable is char[] arr) span = arr;
            else if (enumerable is List<char> list) span = CollectionsMarshal.AsSpan(list);
            else return false;

            context.WriteVarUInt((uint)span.Length);
            for (var i = 0; i < span.Length; i++)
                WriteCharUnsafe(span[i], context);
            return true;
        }

        if (ReferenceEquals(elementType, DateTimeOffsetType))
        {
            ReadOnlySpan<DateTimeOffset> span;
            if (enumerable is DateTimeOffset[] arr) span = arr;
            else if (enumerable is List<DateTimeOffset> list) span = CollectionsMarshal.AsSpan(list);
            else return false;

            context.WriteVarUInt((uint)span.Length);
            for (var i = 0; i < span.Length; i++)
                WriteDateTimeOffsetUnsafe(span[i], context);
            return true;
        }

        if (ReferenceEquals(elementType, TimeSpanType))
        {
            ReadOnlySpan<TimeSpan> span;
            if (enumerable is TimeSpan[] arr) span = arr;
            else if (enumerable is List<TimeSpan> list) span = CollectionsMarshal.AsSpan(list);
            else return false;

            context.WriteVarUInt((uint)span.Length);
            for (var i = 0; i < span.Length; i++)
                WriteTimeSpanUnsafe(span[i], context);
            return true;
        }

        // Enum collections: iterate without GetWrapper/WriteValue dispatch
        if (elementType.IsEnum)
        {
            if (enumerable is IList enumList)
            {
                context.WriteVarUInt((uint)enumList.Count);
                for (var i = 0; i < enumList.Count; i++)
                    WriteEnum(enumList[i]!, context);
            }
            else if (enumerable is ICollection enumCol)
            {
                context.WriteVarUInt((uint)enumCol.Count);
                foreach (var item in enumerable)
                    WriteEnum(item!, context);
            }
            else
            {
                var items = new List<object>();
                foreach (var item in enumerable)
                    items.Add(item!);
                context.WriteVarUInt((uint)items.Count);
                for (var i = 0; i < items.Count; i++)
                    WriteEnum(items[i], context);
            }
            return true;
        }

        return false;
    }

    private static void WriteDictionary<TOutput>(IDictionary dictionary, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        context.WriteByte(BinaryTypeCode.Dictionary);
        context.WriteVarUInt((uint)dictionary.Count);

        foreach (DictionaryEntry entry in dictionary)
        {
            // Write key
            var keyType = entry.Key?.GetType() ?? typeof(object);
            WriteValue(entry.Key, keyType, context);

            // Write value
            var valueType = entry.Value?.GetType() ?? typeof(object);
            WriteValue(entry.Value, valueType, context);
        }
    }

    #endregion

    #region Serialization Result

    // Implementation moved to AcBinarySerializer.BinarySerializationResult.cs

    #endregion

    #region Context Pool

    // Implementation moved to AcBinarySerializer.BinarySerializationContext.cs

    #endregion

    #region Serialization Context

    // Implementation moved to AcBinarySerializer.BinarySerializationContext.cs

    #endregion

    #region Type Metadata

    private static Type? GetCollectionElementType(Type type)
    {
        if (type.IsArray)
        {
            return type.GetElementType();
        }

        if (type.IsGenericType)
        {
            var args = type.GetGenericArguments();
            if (args.Length == 1)
            {
                return args[0];
            }
        }

        foreach (var iface in type.GetInterfaces())
        {
            if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IEnumerable<>))
            {
                return iface.GetGenericArguments()[0];
            }
        }

        return null;
    }


    // Type metadata helpers moved to AcBinarySerializer.BinarySerializeTypeMetadata.cs

    #endregion

    #region WritePlan Debug Validation

    /// <summary>
    /// DEBUG ONLY: Validates that the WritePlan entry's string value matches the actual string being written.
    /// Catches scan/write pass visit index misalignment immediately.
    /// </summary>
    [Conditional("DEBUG")]
    private static void ValidateWritePlanString(in WriteDuplicateEntry planEntry, string actualValue)
    {
        if (planEntry.IsFirst && planEntry.Value != null && !string.Equals(planEntry.Value, actualValue, StringComparison.Ordinal))
        {
            throw new InvalidOperationException(
                $"WritePlan cursor mismatch at VisitIndex={planEntry.VisitIndex}: " +
                $"plan string=\"{planEntry.Value}\", actual=\"{actualValue}\". " +
                $"Scan/write pass visit order diverged.");
        }
    }

    /// <summary>
    /// DEBUG ONLY: Validates that the WritePlan entry's IId matches the actual object being written.
    /// Uses the typed Id getter to extract the Id and compares against the scan pass IdentityMap.
    /// </summary>
    [Conditional("DEBUG")]
    private static void ValidateWritePlanObject(in WriteDuplicateEntry planEntry, object value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper)
    {
        var metadata = wrapper.Metadata;
        switch (metadata.IdAccessorType)
        {
            case IdAccessorType.Int32:
            {
                var actualId = wrapper.RefIdGetterInt32!(value);
                if (!wrapper.TryGetEntryInt32(actualId, out var slotIndex))
                    throw new InvalidOperationException(
                        $"WritePlan cursor mismatch at VisitIndex={planEntry.VisitIndex}: " +
                        $"Int32 Id={actualId} not found in IdentityMap. Scan/write pass visit order diverged.");

                ref var entry = ref wrapper.GetEntryRefInt32(slotIndex);
                if (entry.CacheIndex != planEntry.CacheMapIndex)
                    throw new InvalidOperationException(
                        $"WritePlan cursor mismatch at VisitIndex={planEntry.VisitIndex}: " +
                        $"Int32 Id={actualId} CacheIndex={entry.CacheIndex}, plan CacheMapIndex={planEntry.CacheMapIndex}.");
                break;
            }
            case IdAccessorType.Int64:
            {
                var actualId = wrapper.RefIdGetterInt64!(value);
                if (!wrapper.TryGetEntryInt64(actualId, out var slotIndex))
                    throw new InvalidOperationException(
                        $"WritePlan cursor mismatch at VisitIndex={planEntry.VisitIndex}: " +
                        $"Int64 Id={actualId} not found in IdentityMap. Scan/write pass visit order diverged.");

                ref var entry = ref wrapper.GetEntryRefInt64(slotIndex);
                if (entry.CacheIndex != planEntry.CacheMapIndex)
                    throw new InvalidOperationException(
                        $"WritePlan cursor mismatch at VisitIndex={planEntry.VisitIndex}: " +
                        $"Int64 Id={actualId} CacheIndex={entry.CacheIndex}, plan CacheMapIndex={planEntry.CacheMapIndex}.");
                break;
            }
            case IdAccessorType.Guid:
            {
                var actualId = wrapper.RefIdGetterGuid!(value);
                if (!wrapper.TryGetEntryGuid(actualId, out var slotIndex))
                    throw new InvalidOperationException(
                        $"WritePlan cursor mismatch at VisitIndex={planEntry.VisitIndex}: " +
                        $"Guid Id={actualId} not found in IdentityMap. Scan/write pass visit order diverged.");

                ref var entry = ref wrapper.GetEntryRefGuid(slotIndex);
                if (entry.CacheIndex != planEntry.CacheMapIndex)
                    throw new InvalidOperationException(
                        $"WritePlan cursor mismatch at VisitIndex={planEntry.VisitIndex}: " +
                        $"Guid Id={actualId} CacheIndex={entry.CacheIndex}, plan CacheMapIndex={planEntry.CacheMapIndex}.");
                break;
            }
        }
    }

    #endregion
}
