using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Text;
using System.Threading;
using static AyCode.Core.Helpers.JsonUtilities;

namespace AyCode.Core.Serializers.Binaries;

public static partial class AcBinarySerializer
{
    private static class BinarySerializationContextPool<TOutput> where TOutput : struct, IBinaryOutputBase
    {
        private static readonly ConcurrentQueue<BinarySerializationContext<TOutput>> Pool = new();

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static BinarySerializationContext<TOutput> Get(AcBinarySerializerOptions options)
        {
            if (Pool.TryDequeue(out var context))
            {
                context.Reset(options);
                return context;
            }

            return new BinarySerializationContext<TOutput>(options);
        }

        public static void ReturnAsync(BinarySerializationContext<TOutput> context)
        {
            // 🔥 FIRE-AND-FORGET: cleanup háttérben
            ThreadPool.UnsafeQueueUserWorkItem(Return, context, preferLocal: true);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static void Return(BinarySerializationContext<TOutput> context)
        {
            if (Pool.Count < context.Options.MaxContextPoolSize)
            {
                context.Clear();
                Pool.Enqueue(context);
            }
            else
            {
                context.Dispose();
            }
        }
    }

    /// <summary>
    /// Binary serialization context. Generic on TOutput for output strategy selection.
    /// Owns _buffer/_position for zero virtual dispatch on the hot path.
    /// All write operations (WriteByte, WriteVarUInt, etc.) are inline methods here.
    /// TOutput Output handles only cold-path buffer management (Grow/Initialize) and finalization.
    /// </summary>
    internal sealed partial class BinarySerializationContext<TOutput>
        : SerializationContextBase<BinarySerializeTypeMetadata, AcBinarySerializerOptions>, IDisposable
        where TOutput : struct, IBinaryOutputBase
    {
        private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);

        private const int PropertyIndexBufferMaxCache = 512;
        private const int PropertyStateBufferMaxCache = 512;

        /// <summary>
        /// Output target — handles only Grow (cold path) and finalization (AsSpan/ToArray/Flush).
        /// </summary>
        public TOutput Output;

        /// <summary>
        /// True if Output has been assigned (struct can't be null-checked).
        /// </summary>
        public bool OutputInitialized;

        #region Buffer State — owned by context for zero virtual dispatch

        /// <summary>Current writable buffer (from ArrayPool or IBufferWriter chunk).</summary>
        internal byte[] _buffer = null!;

        /// <summary>Current write position within _buffer.</summary>
        internal int _position;

        /// <summary>One past the last writable index in _buffer. Write must satisfy _position &lt; _bufferEnd.</summary>
        internal int _bufferEnd;

        #endregion

        private IdentityMap<string, InternEntry>? _stringInternMap;
        private int _nextCacheIndex;
        public int NextFirstIndex;    // Next first occurrence index for scan pass. Direct access for performance.

        #region WriteDuplicateEntry — scan pass output for write pass cursor

        private WriteDuplicateEntry[]? _writePlan;
        private int _writePlanCount;

        /// <summary>Unified scan visit counter. Increments on every IId object and internable string visit.</summary>
        public int ScanVisitIndex;

        /// <summary>Write plan entry count for write pass cursor.</summary>
        internal int WritePlanCount => _writePlanCount;

        /// <summary>Write plan array for write pass cursor. Sorted by VisitIndex after scan pass.</summary>
        internal WriteDuplicateEntry[]? WritePlan => _writePlan;

        /// <summary>
        /// Adds a pre-computed write instruction for a duplicate string or IId object reference.
        /// </summary>
        public void AddWriteDuplicateEntry(int visitIndex, int cacheMapIndex, bool isFirst, string? value)
        {
            if (_writePlan == null)
            {
                _writePlan = ArrayPool<WriteDuplicateEntry>.Shared.Rent(16);
            }
            else if (_writePlanCount >= _writePlan.Length)
            {
                var newArray = ArrayPool<WriteDuplicateEntry>.Shared.Rent(_writePlan.Length * 2);
                _writePlan.AsSpan(0, _writePlanCount).CopyTo(newArray);
                ArrayPool<WriteDuplicateEntry>.Shared.Return(_writePlan, clearArray: true);
                _writePlan = newArray;
            }

            ref var entry = ref _writePlan[_writePlanCount++];
            entry.VisitIndex = visitIndex;
            entry.CacheMapIndex = cacheMapIndex;
            entry.IsFirst = isFirst;
            entry.Value = value;
        }

        /// <summary>
        /// Sorts write plan by VisitIndex for sequential cursor consumption in write pass.
        /// Called once after scan pass completes.
        /// </summary>
        internal void SortWritePlan()
        {
            if (_writePlanCount > 1)
                _writePlan.AsSpan(0, _writePlanCount).Sort(static (a, b) => a.VisitIndex.CompareTo(b.VisitIndex));

            _nextWritePlanVisitIndex = _writePlanCount > 0 ? _writePlan![0].VisitIndex : int.MaxValue;
        }

        /// <summary>Write pass cursor index into sorted _writePlan array.</summary>
        internal int WritePlanCursor;

        /// <summary>Write pass visit counter. Mirrors ScanVisitIndex ordering.</summary>
        internal int WriteVisitIndex;

        /// <summary>
        /// Pre-cached VisitIndex of the next write plan entry, or int.MaxValue when exhausted.
        /// </summary>
        private int _nextWritePlanVisitIndex = int.MaxValue;

        /// <summary>
        /// Set per-property in WritePropertyOrSkip before calling WriteString.
        /// Controls whether the current string property participates in the cursor-based interning.
        /// Must mirror scan pass's prop.UseStringPropertyInterning() check.
        /// </summary>
        internal bool StringInternEligible;

        /// <summary>
        /// Next polymorphic type cache index. Assigned sequentially on first polymorphic write per runtime type.
        /// Used together with TypeMetadataWrapper.PolymorphicSeen/PolymorphicCacheIndex.
        /// </summary>
        internal int _nextTypeSlot;

        /// <summary>
        /// Tries to consume the next write plan entry at the current WriteVisitIndex.
        /// Returns true if the entry matches (duplicate exists at this visit point).
        /// Always increments WriteVisitIndex.
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal bool TryConsumeWritePlanEntry(out WriteDuplicateEntry entry)
        {
            var visitIndex = WriteVisitIndex++;
            if (visitIndex == _nextWritePlanVisitIndex)
            {
                entry = _writePlan![WritePlanCursor++];
                _nextWritePlanVisitIndex = WritePlanCursor < _writePlanCount
                    ? _writePlan[WritePlanCursor].VisitIndex
                    : int.MaxValue;
                return true;
            }
            entry = default;
            return false;
        }

        #endregion

        /// <summary>
        /// Next cache index reference for scan pass. Direct ref access for TryTrack methods.
        /// </summary>
        public ref int NextCacheIndexRef
        {
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            get => ref _nextCacheIndex;
        }

        private int[]? _propertyIndexBuffer;
        private byte[]? _propertyStateBuffer;

        /// <summary>
        /// Current property being serialized. Set in WriteObject property loops.
        /// Used by WriteString for per-property interning control (UseStringPropertyInterning).
        /// null when writing non-property values (arrays, dictionaries, root value).
        /// </summary>
        //internal BinaryPropertyAccessorBase? CurrentProperty;

#if DEBUG
        /// <summary>
        /// DEBUG ONLY: Callback invoked when a string is registered for interning.
        /// Parameters: (propertyPath, stringValue)
        /// Use this to analyze which properties have repeated string values.
        /// </summary>
        internal Action<string?, string>? OnStringInterned;
#endif

        // These properties delegate to Options for convenience
        internal bool HasStringInterning { get; private set; }
        internal bool UseStringInterning => HasStringInterning;

        /// <summary>
        /// Pre-computed <c>1 &lt;&lt; (int)Options.UseStringInterning</c>.
        /// Used by sgen scan (per-object intern check) and sgen write (per-property StringInternEligible).
        /// Avoids repeated field chain traversal (context.Options.UseStringInterning) + shift per call site.
        /// Value: None=1, Attribute=2, All=4.
        /// </summary>
        public int InternBit { get; private set; }

        public bool IsValidForInterningString(int strLength)
        {
            return strLength >= MinStringInternLength && (MaxStringInternLength == 0 || strLength <= MaxStringInternLength);
        }

        /// <summary>
        /// True if we have interning/ref tracking (cache count needed in header).
        /// </summary>
        public bool HasCaching => HasStringInterning || HasRefHandling;
        public bool UseMetadata => Options.UseMetadata;
        public bool UseGeneratedCode => Options.UseGeneratedCode;

        /// <summary>
        /// True when generated writers can bypass WriteObject entirely and write markers + properties inline.
        /// Requires: no UseMetadata (no inline metadata tracking).
        /// PropertyFilter is handled by generated code's per-property filter checks.
        /// Reference handling is safe because generated code inlines TryConsumeWritePlanEntry for IId types.
        /// </summary>
        public bool IsDirectObjectWrite => !UseMetadata;
        public bool FastWire { get; private set; }
        public byte MinStringInternLength => Options.MinStringInternLength;
        public byte MaxStringInternLength => Options.MaxStringInternLength;
        public BinaryPropertyFilter? PropertyFilter => Options.PropertyFilter;

        /// <summary>
        /// Cached check for PropertyFilter != null. Set in Reset() to avoid property getter in hot loop.
        /// </summary>
        public bool HasPropertyFilter { get; private set; }

        /// <summary>
        /// Current output position (total bytes written so far).
        /// Cold path — uses virtual dispatch through Output.GetTotalPosition.
        /// </summary>
        public int Position
        {
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            get => Output.GetTotalPosition(_position);
        }

        public BinarySerializationContext(AcBinarySerializerOptions options)
        {
            Reset(options);
            InitializeWrapperSlots(Volatile.Read(ref s_nextWrapperSlot));
        }

        /// <summary>
        /// Factory for creating BinarySerializeTypeMetadata instances.
        /// </summary>
        /// <remarks>
        /// The lambda's <c>t</c> parameter explicitly declares DAMs to match the
        /// <see cref="MetadataFactoryDelegate"/> signature — DAMs do not auto-propagate from the
        /// delegate type to lambda parameters.
        /// </remarks>
        protected override MetadataFactoryDelegate MetadataFactory
            => static ([System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(TypeMetadataBase.RequiredMembers)] t)
                => new BinarySerializeTypeMetadata(t, HasJsonIgnoreAttribute);

        public override void Reset(AcBinarySerializerOptions options)
        {
            // IMPORTANT: base.Reset sets Options first, so derived code can use Options-derived properties
            base.Reset(options);

            HasPropertyFilter = Options.PropertyFilter != null;

            InternBit = 1 << (int)Options.UseStringInterning;
            HasStringInterning = Options.UseStringInterning != StringInterningMode.None;

            FastWire = Options.WireMode == WireMode.Fast;
        }

        public override void Clear()
        {
            // Reset output buffer (large buffers → return to pool, rent halved)
            if (OutputInitialized)
            {
                Output.Reset();
            }

            _stringInternMap?.Reset();

            _nextCacheIndex = 0;
            NextFirstIndex = 0;
            ScanVisitIndex = 0;
            WritePlanCursor = 0;
            WriteVisitIndex = 0;
            _nextWritePlanVisitIndex = int.MaxValue;
            StringInternEligible = false;
            _nextTypeSlot = 0;

            // Clear write plan string references to avoid GC pinning, keep array if small enough
            if (_writePlan != null)
            {
                _writePlan.AsSpan(0, _writePlanCount).Clear();
                if (_writePlan.Length > PropertyIndexBufferMaxCache)
                {
                    ArrayPool<WriteDuplicateEntry>.Shared.Return(_writePlan);
                    _writePlan = null;
                }
            }
            _writePlanCount = 0;

            if (_propertyIndexBuffer != null && _propertyIndexBuffer.Length > PropertyIndexBufferMaxCache)
            {
                ArrayPool<int>.Shared.Return(_propertyIndexBuffer);
                _propertyIndexBuffer = null;
            }

            if (_propertyStateBuffer != null && _propertyStateBuffer.Length > PropertyStateBufferMaxCache)
            {
                ArrayPool<byte>.Shared.Return(_propertyStateBuffer);
                _propertyStateBuffer = null;
            }

            // Clear wrapper tracking - returns IdentityMap arrays to pool
            base.Clear();
        }

        public void Dispose()
        {
            if (_propertyIndexBuffer != null)
            {
                ArrayPool<int>.Shared.Return(_propertyIndexBuffer);
                _propertyIndexBuffer = null;
            }

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

            // Dispose the output if it implements IDisposable (e.g. ArrayBinaryOutput returns buffer to pool)
            if (Output is IDisposable disposableOutput)
                disposableOutput.Dispose();
        }

        #region Write Methods — inline, zero virtual dispatch

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private void EnsureCapacity(int additionalBytes)
        {
            if (_position + additionalBytes > _bufferEnd)
                Output.Grow(ref _buffer, ref _position, ref _bufferEnd, additionalBytes);
        }

        /// <summary>
        /// Ensures the buffer has enough space for the specified number of bytes.
        /// Called before property writes to avoid mid-object Grow() calls.
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void ReserveCapacity(int bytes)
        {
            EnsureCapacity(bytes);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void WriteByte(byte value)
        {
            if (_position >= _bufferEnd)
                Output.Grow(ref _buffer, ref _position, ref _bufferEnd, 1);
            _buffer[_position++] = value;
        }

        /// <summary>Writes a single byte without capacity check. Caller must ensure buffer space.</summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void WriteByteUnsafe(byte value)
        {
            _buffer[_position++] = value;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void WriteTwoBytes(byte b1, byte b2)
        {
            EnsureCapacity(2);
            WriteTwoBytesUnsafe(b1, b2);
        }

        /// <summary>Writes two bytes without capacity check. Caller must ensure buffer space.</summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void WriteTwoBytesUnsafe(byte b1, byte b2)
        {
            _buffer[_position++] = b1;
            _buffer[_position++] = b2;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void WriteBytes(ReadOnlySpan<byte> data)
        {
            EnsureCapacity(data.Length);
            WriteBytesUnsafe(data);
        }

        /// <summary>Writes a span of bytes without capacity check. Caller must ensure buffer space.</summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void WriteBytesUnsafe(ReadOnlySpan<byte> data)
        {
            data.CopyTo(_buffer.AsSpan(_position));
            _position += data.Length;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void WriteRaw<T>(T value) where T : unmanaged
        {
            EnsureCapacity(Unsafe.SizeOf<T>());
            WriteRawUnsafe(value);
        }

        /// <summary>Writes an unmanaged value without capacity check. Caller must ensure buffer space.</summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void WriteRawUnsafe<T>(T value) where T : unmanaged
        {
            Unsafe.WriteUnaligned(ref _buffer[_position], value);
            _position += Unsafe.SizeOf<T>();
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void WriteTypeCodeAndRaw<T>(byte typeCode, T value) where T : unmanaged
        {
            EnsureCapacity(1 + Unsafe.SizeOf<T>());
            WriteTypeCodeAndRawUnsafe(typeCode, value);
        }

        /// <summary>Writes a type code + unmanaged value without capacity check. Caller must ensure buffer space.</summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void WriteTypeCodeAndRawUnsafe<T>(byte typeCode, T value) where T : unmanaged
        {
            _buffer[_position++] = typeCode;
            Unsafe.WriteUnaligned(ref _buffer[_position], value);
            _position += Unsafe.SizeOf<T>();
        }

        #endregion

        #region VarInt Encoding — inline

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void WriteVarUInt(uint value)
        {
            //if (FastWire) { WriteRaw(value); return; }
            if (value < 0x80)
            {
                if (_position >= _bufferEnd)
                    Output.Grow(ref _buffer, ref _position, ref _bufferEnd, 1);
                _buffer[_position++] = (byte)value;
                return;
            }
            EnsureCapacity(5);
            WriteVarUIntMultiByteUnsafe(value);
        }

        /// <summary>Writes a VarUInt without capacity check. Caller must ensure at least 5 bytes of buffer space.</summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void WriteVarUIntUnsafe(uint value)
        {
            if (value < 0x80)
            {
                _buffer[_position++] = (byte)value;
                return;
            }
            WriteVarUIntMultiByteUnsafe(value);
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private void WriteVarUIntMultiByteUnsafe(uint value)
        {
            while (value >= 0x80)
            {
                _buffer[_position++] = (byte)(value | 0x80);
                value >>= 7;
            }
            _buffer[_position++] = (byte)value;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static int VarUIntSize(uint value)
        {
            if (value < 0x80) return 1;
            if (value < 0x4000) return 2;
            if (value < 0x200000) return 3;
            if (value < 0x10000000) return 4;
            return 5;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void WriteVarInt(int value)
        {
            //if (FastWire) { WriteRaw(value); return; }
            var encoded = (uint)((value << 1) ^ (value >> 31));
            WriteVarUInt(encoded);
        }

        /// <summary>Writes a zigzag-encoded VarInt without capacity check. Caller must ensure at least 5 bytes of buffer space.</summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void WriteVarIntUnsafe(int value)
        {
            var encoded = (uint)((value << 1) ^ (value >> 31));
            WriteVarUIntUnsafe(encoded);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void WriteVarULong(ulong value)
        {
            //if (FastWire) { WriteRaw(value); return; }
            if (value < 0x80)
            {
                if (_position >= _bufferEnd)
                    Output.Grow(ref _buffer, ref _position, ref _bufferEnd, 1);
                _buffer[_position++] = (byte)value;
                return;
            }
            EnsureCapacity(10);
            WriteVarULongMultiByteUnsafe(value);
        }

        /// <summary>Writes a VarULong without capacity check. Caller must ensure at least 10 bytes of buffer space.</summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void WriteVarULongUnsafe(ulong value)
        {
            if (value < 0x80)
            {
                _buffer[_position++] = (byte)value;
                return;
            }
            WriteVarULongMultiByteUnsafe(value);
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private void WriteVarULongMultiByteUnsafe(ulong value)
        {
            while (value >= 0x80)
            {
                _buffer[_position++] = (byte)(value | 0x80);
                value >>= 7;
            }
            _buffer[_position++] = (byte)value;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void WriteVarLong(long value)
        {
            //if (FastWire) { WriteRaw(value); return; }
            var encoded = (ulong)((value << 1) ^ (value >> 63));
            WriteVarULong(encoded);
        }

        /// <summary>Writes a zigzag-encoded VarLong without capacity check. Caller must ensure at least 10 bytes of buffer space.</summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void WriteVarLongUnsafe(long value)
        {
            var encoded = (ulong)((value << 1) ^ (value >> 63));
            WriteVarULongUnsafe(encoded);
        }

        #endregion

        #region Specialized Types — inline

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void WriteDecimalBits(decimal value)
        {
            EnsureCapacity(16);
            WriteDecimalBitsUnsafe(value);
        }

        /// <summary>Writes 16-byte decimal bits without capacity check. Caller must ensure buffer space.</summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void WriteDecimalBitsUnsafe(decimal value)
        {
            Span<int> bits = stackalloc int[4];
            decimal.TryGetBits(value, bits, out _);
            MemoryMarshal.AsBytes(bits).CopyTo(_buffer.AsSpan(_position, 16));
            _position += 16;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void WriteDateTimeBits(DateTime value)
        {
            EnsureCapacity(9);
            WriteDateTimeBitsUnsafe(value);
        }

        /// <summary>Writes 9-byte DateTime (Ticks + Kind) without capacity check. Caller must ensure buffer space.</summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void WriteDateTimeBitsUnsafe(DateTime value)
        {
            Unsafe.WriteUnaligned(ref _buffer[_position], value.Ticks);
            _buffer[_position + 8] = (byte)value.Kind;
            _position += 9;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void WriteGuidBits(Guid value)
        {
            EnsureCapacity(16);
            WriteGuidBitsUnsafe(value);
        }

        /// <summary>Writes 16-byte Guid without capacity check. Caller must ensure buffer space.</summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void WriteGuidBitsUnsafe(Guid value)
        {
            value.TryWriteBytes(_buffer.AsSpan(_position, 16));
            _position += 16;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void WriteDateTimeOffsetBits(DateTimeOffset value)
        {
            EnsureCapacity(10);
            WriteDateTimeOffsetBitsUnsafe(value);
        }

        /// <summary>Writes 10-byte DateTimeOffset (UtcTicks + Offset) without capacity check. Caller must ensure buffer space.</summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void WriteDateTimeOffsetBitsUnsafe(DateTimeOffset value)
        {
            Unsafe.WriteUnaligned(ref _buffer[_position], value.UtcTicks);
            Unsafe.WriteUnaligned(ref _buffer[_position + 8], (short)value.Offset.TotalMinutes);
            _position += 10;
        }

        #endregion

        #region String Writes — inline

        public void WriteStringUtf8(string value)
        {
            if (FastWire)
            {
                // UTF-16: char count (VarUInt) + raw char data (zero-encoding memcopy)
                var charLen = value.Length;
                var byteLen = charLen * 2;

                WriteVarUInt((uint)charLen);
                EnsureCapacity(byteLen);

                MemoryMarshal.AsBytes(value.AsSpan()).CopyTo(_buffer.AsSpan(_position, byteLen));
                _position += byteLen;

                return;
            }

            // D-2 + tight reserve: single-pass UTF-8 encode with input-bound-aware VarUInt slot.
            // Replaces the prior try-ASCII-then-rewind-and-encode-UTF-8 pattern (1 scan ASCII / 3 scans
            // non-ASCII) with a single GetBytes call that works identically for both content classes.
            //
            // Layout: [reserved N bytes for VarUInt][UTF-8 bytes...]
            //   1. Compute worst-case byte count from charLength (UTF-8 max = 4 bytes/char) and the
            //      VarUInt size needed for that upper bound. For charLength ≤ 31, reserveSize = 1
            //      (since 4*31 = 124 < 128 ⇒ VarUInt(124) = 1 byte). Most short strings hit this.
            //   2. EnsureCapacity for reserveSize + maxBytes.
            //   3. GetBytes directly into buffer at savedPos+reserveSize → returns exact byteCount.
            //   4. If actual VarUInt < reserveSize (rare), memmove encoded bytes left to compact.
            //   5. WriteVarUInt at savedPos and advance.
            //
            // Win vs the prior fixed-5-byte reserve: short strings (the common case) skip the memmove
            // entirely. For 32-char strings the reserve is 2 bytes; if actual byteCount < 128 we
            // memmove a smaller distance (1 byte) than the prior fixed approach (4 bytes).
            //
            // Span<byte>.CopyTo is overlap-safe via Buffer.Memmove on byte arrays.
            var charLength = value.Length;
            var maxBytes = charLength * 4;
            var reserveSize = VarUIntSize((uint)maxBytes);

            EnsureCapacity(reserveSize + maxBytes);

            var savedPos = _position;
            var encodeStart = savedPos + reserveSize;
            var bytesWritten = EncodeUtf8SinglePass(value.AsSpan(), _buffer.AsSpan(encodeStart, maxBytes));

            var actualVarUIntSize = VarUIntSize((uint)bytesWritten);
            if (actualVarUIntSize < reserveSize)
            {
                var shift = reserveSize - actualVarUIntSize;
                _buffer.AsSpan(encodeStart, bytesWritten).CopyTo(_buffer.AsSpan(encodeStart - shift, bytesWritten));
            }

            _position = savedPos;

            WriteVarUIntUnsafe((uint)bytesWritten);  // advances _position by actualVarUIntSize
            _position += bytesWritten;
        }

        /// <summary>
        /// Writes a non-empty string with marker-dispatch: detects ASCII vs UTF-8 in-place from the
        /// encoder's byte count and emits the appropriate wire marker (<c>FixStrAscii</c>,
        /// <c>FixStr</c>, <c>StringAscii</c>, or <c>String</c>). The reader uses the marker as an
        /// ASCII-validity contract — pure-ASCII payloads skip UTF-8 decode entirely (byte→char widen).
        /// </summary>
        /// <remarks>
        /// Layout (Compact wire): <c>[marker: 1 byte][optional VarUInt byteCount][encoded bytes]</c>
        /// — VarUInt is omitted for FixStr/FixStrAscii (length is encoded in the marker).
        ///
        /// ASCII detection is free: <c>bytesWritten == charLength</c> after a UTF-8 encode is a
        /// necessary AND sufficient condition for the input being pure ASCII (every UTF-16 char
        /// &lt; 0x80 produces exactly 1 UTF-8 byte; non-ASCII chars always produce 2-4 bytes).
        ///
        /// Caller MUST guarantee non-empty input (<c>value.Length &gt; 0</c>) — empty strings are
        /// handled by the higher-level <c>WriteString</c> via the <c>StringEmpty</c> marker.
        /// </remarks>
        public void WriteStringWithDispatch(string value)
        {
            if (FastWire)
            {
                // FastWire: char count (VarUInt) + raw UTF-16 memcopy. ASCII detection adds no value
                // here — the wire size is identical (2 bytes/char) and the read path is memcpy-based,
                // so the encoder/decoder UTF-8 cost (which the ASCII marker would skip) doesn't apply.
                WriteByte(BinaryTypeCode.String);
                var charLenF = value.Length;
                var byteLenF = charLenF * 2;
                WriteVarUInt((uint)charLenF);
                EnsureCapacity(byteLenF);
                MemoryMarshal.AsBytes(value.AsSpan()).CopyTo(_buffer.AsSpan(_position, byteLenF));
                _position += byteLenF;
                return;
            }

            var charLength = value.Length;

            // Hot-path split: encode position is chosen to MINIMIZE post-encode shifts.
            //
            // • charLength ≤ 31 → MIGHT be FixStr (bytesWritten ≤ 31) or long String (multibyte
            //   expansion). Encode optimistically at savedPos+1 (FixStr position). FixStr hit ⇒ 0 shift,
            //   only marker byte write. Long-fallback (rare, requires Hungarian/CJK chars in a
            //   short-char string AND post-expand size > 31) ⇒ shift bytes RIGHT by 1 (since the
            //   long lane needs 1 VarUInt byte after the marker; charLength ≤ 31 ⇒ maxBytes ≤ 124
            //   ⇒ VarUInt size = 1).
            //
            // • charLength > 31 → ALWAYS long String (bytesWritten ≥ charLength > 31). Use full
            //   D-2 layout [marker][reserveVarUInt][bytes], encode at savedPos+1+reserveVarUInt.
            //   Backfill compacts only when actual VarUInt size < reserved (rare).
            if (charLength <= BinaryTypeCode.FixStrMaxLength)
            {
                var maxBytesShort = charLength * 4;  // ≤ 124, fits in 1-byte VarUInt
                EnsureCapacity(2 + maxBytesShort);   // marker + 1-byte VarUInt + bytes (worst case)

                var savedPosShort = _position;
                var bytesWrittenShort = EncodeUtf8SinglePass(
                    value.AsSpan(),
                    _buffer.AsSpan(savedPosShort + 1, maxBytesShort));
                var isAsciiShort = bytesWrittenShort == charLength;

                if (bytesWrittenShort <= BinaryTypeCode.FixStrMaxLength)
                {
                    // Hot path: FixStr hit → bytes already at savedPos+1, no shift.
                    _buffer[savedPosShort] = isAsciiShort
                        ? BinaryTypeCode.EncodeFixStrAscii(bytesWrittenShort)
                        : BinaryTypeCode.EncodeFixStr(bytesWrittenShort);
                    _position = savedPosShort + 1 + bytesWrittenShort;
                }
                else
                {
                    // Cold: multibyte expansion pushed bytes > 31 → become long String/StringAscii.
                    // Shift bytes right by 1 to insert the 1-byte VarUInt slot.
                    _buffer.AsSpan(savedPosShort + 1, bytesWrittenShort)
                        .CopyTo(_buffer.AsSpan(savedPosShort + 2, bytesWrittenShort));
                    _buffer[savedPosShort] = isAsciiShort ? BinaryTypeCode.StringAscii : BinaryTypeCode.String;
                    _position = savedPosShort + 1;
                    WriteVarUIntUnsafe((uint)bytesWrittenShort);
                    _position += bytesWrittenShort;
                }

                return;
            }

            // Long path: charLength > 31 ⇒ bytesWritten > 31 ⇒ always String / StringAscii.
            // D-2 layout [marker:1][VarUInt slot:reserveVarUInt][bytes], encode at savedPos+1+reserveVarUInt.
            var maxBytes = charLength * 4;
            var reserveVarUInt = VarUIntSize((uint)maxBytes);

            EnsureCapacity(1 + reserveVarUInt + maxBytes);

            var savedPos = _position;
            var encodeStart = savedPos + 1 + reserveVarUInt;
            var bytesWritten = EncodeUtf8SinglePass(value.AsSpan(), _buffer.AsSpan(encodeStart, maxBytes));
            var isAscii = bytesWritten == charLength;

            _buffer[savedPos] = isAscii ? BinaryTypeCode.StringAscii : BinaryTypeCode.String;
            var actualVarUIntSize = VarUIntSize((uint)bytesWritten);
            if (actualVarUIntSize < reserveVarUInt)
            {
                var shift = reserveVarUInt - actualVarUIntSize;
                _buffer.AsSpan(encodeStart, bytesWritten).CopyTo(_buffer.AsSpan(encodeStart - shift, bytesWritten));
            }
            _position = savedPos + 1;
            WriteVarUIntUnsafe((uint)bytesWritten);
            _position += bytesWritten;
        }

        public void WriteFixStr(string value)
        {
            var length = value.Length;
            EnsureCapacity(1 + length);
            _buffer[_position++] = BinaryTypeCode.EncodeFixStr(length);
            Ascii.FromUtf16(value.AsSpan(), _buffer.AsSpan(_position, length), out _);
            _position += length;
        }

        public void WriteFixStrDirect(string value)
        {
            var length = value.Length;
            EnsureCapacity(1 + length);

            var destSpan = _buffer.AsSpan(_position + 1, length);
            var status = Ascii.FromUtf16(value.AsSpan(), destSpan, out var bytesWritten);

            if (status == OperationStatus.Done && bytesWritten == length)
            {
                _buffer[_position] = BinaryTypeCode.EncodeFixStr(length);
                _position += 1 + length;
            }
            else
            {
                _buffer[_position++] = BinaryTypeCode.String;
                WriteStringUtf8Internal(value);
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void WriteFixStrBytes(ReadOnlySpan<byte> utf8Bytes)
        {
            var length = utf8Bytes.Length;
            EnsureCapacity(1 + length);
            _buffer[_position++] = BinaryTypeCode.EncodeFixStr(length);
            utf8Bytes.CopyTo(_buffer.AsSpan(_position, length));
            _position += length;
        }

        public void WritePreencodedPropertyName(ReadOnlySpan<byte> utf8Name)
        {
            WriteByte(BinaryTypeCode.String);
            WriteVarUInt((uint)utf8Name.Length);
            WriteBytes(utf8Name);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private void WriteStringUtf8Internal(string value)
        {
            var byteCount = Utf8NoBom.GetByteCount(value);
            WriteVarUInt((uint)byteCount);
            EnsureCapacity(byteCount);
            Utf8NoBom.GetBytes(value.AsSpan(), _buffer.AsSpan(_position, byteCount));
            _position += byteCount;
        }

        /// <summary>
        /// Custom UTF-16 → UTF-8 single-pass encoder. Symmetric with the deserializer's custom decoder
        /// (<see cref="AcBinaryDeserializer"/>'s <c>DecodeUtf8SinglePass</c>).
        /// </summary>
        /// <remarks>
        /// Bypasses <see cref="System.Text.Encoding.UTF8"/>.GetBytes virtual-dispatch + encoder-fallback
        /// overhead. Trusted-input encoder — no validation (writer side, the input is a .NET string
        /// which always has valid UTF-16 surrogate pairs).
        ///
        /// Layered for max throughput on mixed content:
        /// • <b>Phase 1 — Vector256 ASCII narrow:</b> 16 chars/iter. Loads <c>Vector256&lt;ushort&gt;</c>,
        ///   tests <c>(v &amp; 0xFF80) == 0</c> for all-ASCII; on hit, narrows to <c>Vector128&lt;byte&gt;</c>
        ///   via <c>Vector128.Narrow(GetLower, GetUpper)</c> = 16 bytes per iter.
        /// • <b>Phase 2 — DWORD ASCII batch:</b> 4 chars/iter. OR-mask test
        ///   <c>(c0 | c1 | c2 | c3) &amp; 0xFF80 == 0</c>; on hit, 4 byte writes per iter.
        /// • <b>Phase 3 — Scalar multi-byte encode:</b> 1-byte (ASCII), 2-byte (Latin extended,
        ///   Cyrillic, Greek), 3-byte (CJK BMP), 4-byte (supplementary plane via UTF-16 surrogate pair).
        ///
        /// Returns actual byte count written. Caller must ensure <paramref name="dst"/> has at least
        /// <c>src.Length * 4</c> capacity (UTF-8 worst case).
        /// </remarks>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static int EncodeUtf8SinglePass(ReadOnlySpan<char> src, Span<byte> dst)
        {
            int srcIdx = 0, dstIdx = 0;
            ref char srcRefChar = ref MemoryMarshal.GetReference(src);
            ref ushort srcRefU16 = ref Unsafe.As<char, ushort>(ref srcRefChar);
            ref byte dstRef = ref MemoryMarshal.GetReference(dst);

            // Phase 1 — Vector256 ASCII narrow (16 chars/iter, falls out on first non-ASCII)
            if (Vector256.IsHardwareAccelerated)
            {
                var asciiMask = Vector256.Create((ushort)0xFF80);
                while (src.Length - srcIdx >= Vector256<ushort>.Count)  // 16 chars per Vector256<ushort>
                {
                    var v = Vector256.LoadUnsafe(ref srcRefU16, (uint)srcIdx);
                    // ASCII detect: any char's high bits set (>= 0x80)?
                    if ((v & asciiMask) != Vector256<ushort>.Zero) break;
                    // Narrow 16 ushorts (Vector256) → 16 bytes (Vector128) via two halves
                    var bytes = Vector128.Narrow(v.GetLower(), v.GetUpper());
                    bytes.StoreUnsafe(ref dstRef, (uint)dstIdx);
                    srcIdx += Vector256<ushort>.Count;
                    dstIdx += Vector256<ushort>.Count;  // 16 chars → 16 bytes (1:1 for ASCII)
                }
            }

            // Phase 2/3 — scalar with DWORD ASCII batch
            while (srcIdx < src.Length)
            {
                // DWORD ASCII batch: 4 chars → 4 bytes when all ASCII
                if (src.Length - srcIdx >= 4)
                {
                    var c0 = Unsafe.Add(ref srcRefChar, srcIdx);
                    var c1 = Unsafe.Add(ref srcRefChar, srcIdx + 1);
                    var c2 = Unsafe.Add(ref srcRefChar, srcIdx + 2);
                    var c3 = Unsafe.Add(ref srcRefChar, srcIdx + 3);
                    if (((c0 | c1 | c2 | c3) & 0xFF80) == 0)
                    {
                        Unsafe.Add(ref dstRef, dstIdx)     = (byte)c0;
                        Unsafe.Add(ref dstRef, dstIdx + 1) = (byte)c1;
                        Unsafe.Add(ref dstRef, dstIdx + 2) = (byte)c2;
                        Unsafe.Add(ref dstRef, dstIdx + 3) = (byte)c3;
                        srcIdx += 4;
                        dstIdx += 4;
                        continue;
                    }
                }

                // Scalar single-char encode
                var c = Unsafe.Add(ref srcRefChar, srcIdx);
                if (c < 0x80)
                {
                    // 1-byte ASCII (U+0000–U+007F)
                    Unsafe.Add(ref dstRef, dstIdx++) = (byte)c;
                    srcIdx += 1;
                }
                else if (c < 0x800)
                {
                    // 2-byte: 110xxxxx 10xxxxxx → U+0080–U+07FF
                    // Latin extended (Hungarian, Polish, Czech, Spanish, French, German diacritics),
                    // Greek, Cyrillic, Hebrew, Arabic.
                    Unsafe.Add(ref dstRef, dstIdx)     = (byte)(0xC0 | (c >> 6));
                    Unsafe.Add(ref dstRef, dstIdx + 1) = (byte)(0x80 | (c & 0x3F));
                    dstIdx += 2;
                    srcIdx += 1;
                }
                else if ((c & 0xF800) != 0xD800)
                {
                    // 3-byte BMP: 1110xxxx 10xxxxxx 10xxxxxx → U+0800–U+FFFF (excluding surrogate range)
                    // CJK BMP, various other BMP scripts.
                    Unsafe.Add(ref dstRef, dstIdx)     = (byte)(0xE0 | (c >> 12));
                    Unsafe.Add(ref dstRef, dstIdx + 1) = (byte)(0x80 | ((c >> 6) & 0x3F));
                    Unsafe.Add(ref dstRef, dstIdx + 2) = (byte)(0x80 | (c & 0x3F));
                    dstIdx += 3;
                    srcIdx += 1;
                }
                else
                {
                    // 4-byte: surrogate pair → supplementary plane codepoint (U+10000–U+10FFFF)
                    // High surrogate (0xD800–0xDBFF) followed by low surrogate (0xDC00–0xDFFF).
                    var c2 = Unsafe.Add(ref srcRefChar, srcIdx + 1);
                    var codepoint = 0x10000 + ((c - 0xD800) << 10) + (c2 - 0xDC00);
                    Unsafe.Add(ref dstRef, dstIdx)     = (byte)(0xF0 | (codepoint >> 18));
                    Unsafe.Add(ref dstRef, dstIdx + 1) = (byte)(0x80 | ((codepoint >> 12) & 0x3F));
                    Unsafe.Add(ref dstRef, dstIdx + 2) = (byte)(0x80 | ((codepoint >> 6) & 0x3F));
                    Unsafe.Add(ref dstRef, dstIdx + 3) = (byte)(0x80 | (codepoint & 0x3F));
                    dstIdx += 4;
                    srcIdx += 2;  // consumed 2 chars (surrogate pair)
                }
            }

            return dstIdx;
        }

        #endregion

        #region Bulk Array Writes — inline

        public void WriteDoubleArrayBulk(ReadOnlySpan<double> span)
        {
            EnsureCapacity(span.Length * 9);
            for (var i = 0; i < span.Length; i++)
            {
                _buffer[_position++] = BinaryTypeCode.Float64;
                Unsafe.WriteUnaligned(ref _buffer[_position], span[i]);
                _position += 8;
            }
        }

        public void WriteFloatArrayBulk(ReadOnlySpan<float> span)
        {
            EnsureCapacity(span.Length * 5);
            for (var i = 0; i < span.Length; i++)
            {
                _buffer[_position++] = BinaryTypeCode.Float32;
                Unsafe.WriteUnaligned(ref _buffer[_position], span[i]);
                _position += 4;
            }
        }

        public void WriteGuidArrayBulk(ReadOnlySpan<Guid> span)
        {
            EnsureCapacity(span.Length * 17);
            for (var i = 0; i < span.Length; i++)
            {
                _buffer[_position++] = BinaryTypeCode.Guid;
                span[i].TryWriteBytes(_buffer.AsSpan(_position, 16));
                _position += 16;
            }
        }

        public void WriteInt32ArrayOptimized(ReadOnlySpan<int> span)
        {
            for (var i = 0; i < span.Length; i++)
            {
                var value = span[i];
                if (BinaryTypeCode.TryEncodeTinyInt(value, out var tiny))
                {
                    WriteByte(tiny);
                }
                else
                {
                    WriteByte(BinaryTypeCode.Int32);
                    WriteVarInt(value);
                }
            }
        }

        public void WriteLongArrayOptimized(ReadOnlySpan<long> span)
        {
            for (var i = 0; i < span.Length; i++)
            {
                var value = span[i];
                if (value >= int.MinValue && value <= int.MaxValue)
                {
                    var intValue = (int)value;
                    if (BinaryTypeCode.TryEncodeTinyInt(intValue, out var tiny))
                    {
                        WriteByte(tiny);
                    }
                    else
                    {
                        WriteByte(BinaryTypeCode.Int32);
                        WriteVarInt(intValue);
                    }
                }
                else
                {
                    WriteByte(BinaryTypeCode.Int64);
                    WriteVarLong(value);
                }
            }
        }

        public void WriteBytesSimd(ReadOnlySpan<byte> source)
        {
            EnsureCapacity(source.Length);
            var destination = _buffer.AsSpan(_position, source.Length);

            if (Vector.IsHardwareAccelerated && source.Length >= Vector<byte>.Count * 2)
            {
                var vectorSize = Vector<byte>.Count;
                var i = 0;
                var length = source.Length;
                var vectorCount = length / vectorSize;
                for (var v = 0; v < vectorCount; v++)
                {
                    var vec = new Vector<byte>(source.Slice(i, vectorSize));
                    vec.CopyTo(destination.Slice(i, vectorSize));
                    i += vectorSize;
                }
                if (i < length)
                    source.Slice(i).CopyTo(destination.Slice(i));
            }
            else
            {
                source.CopyTo(destination);
            }

            _position += source.Length;
        }

        #endregion

        #region String Interning

        /// <summary>
        /// Serialize pass: looks up interned string state.
        /// Returns the entry ref for caller to check IsFirstWrite and update it.
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public ref InternEntry GetInternedStringEntry(string value, out bool found)
        {
            if (_stringInternMap == null)
            {
                found = false;
                return ref Unsafe.NullRef<InternEntry>();
            }

            if (_stringInternMap.TryAdd(value, out var slotIndex))
            {
                // Not in map (shouldn't happen after scan pass for cached strings)
                found = false;
                return ref _stringInternMap.GetValueRef(slotIndex);
            }

            found = true;
            return ref _stringInternMap.GetValueRef(slotIndex);
        }

        /// <summary>
        /// Scan pass: tracks a string for interning. Assigns CacheIndex immediately on 2nd occurrence.
        /// Builds WriteDuplicateEntry for all duplicate occurrences.
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void ScanInternString(string value)
        {
            var visitIndex = ScanVisitIndex++;

            _stringInternMap ??= new IdentityMap<string, InternEntry>();

            if (!_stringInternMap.TryAdd(value, out var slotIndex))
            {
                // 2+ occurrence: assign CacheIndex immediately
                ref var entry = ref _stringInternMap.GetValueRef(slotIndex);
                if (entry.CacheIndex == -1)
                {
                    // 2nd occurrence: assign CacheIndex + add StringFirst entry at first visit position
                    entry.CacheIndex = _nextCacheIndex++;
                    entry.IsFirstWrite = true;
                    AddWriteDuplicateEntry(entry.FirstIndex, entry.CacheIndex, isFirst: true, value);
                }
                // 2nd+ occurrence: add StringRef entry at current position
                AddWriteDuplicateEntry(visitIndex, entry.CacheIndex, isFirst: false, value: null);
                return;
            }

            // 1st occurrence: store scan visit index, CacheIndex = -1 (not cached yet)
            ref var newEntry = ref _stringInternMap.GetValueRef(slotIndex);
            newEntry.FirstIndex = visitIndex;
            newEntry.CacheIndex = -1;
        }

        /// <summary>
        /// Returns true if there are any interned strings that occurred more than once.
        /// </summary>
        public bool HasInternedStrings => _stringInternMap != null && _stringInternMap.Count > 0;

        /// <summary>
        /// Gets the count of cached values (string intern + object ref that occurred more than once).
        /// </summary>
        public int GetCacheCount() => _nextCacheIndex;

        #endregion

        #region Polymorphic Type Prefix

        /// <summary>
        /// Writes a polymorphic type prefix when the runtime type differs from the declared property type.
        /// <para>
        /// When <paramref name="cachedObjectCacheIndex"/> is -1 (default): PREFIX markers.
        /// An inner Object/Array/Dict marker follows.
        ///   First type occurrence: ObjectWithTypeName (68) + typename
        ///   Cached type: ObjectWithTypeIndex (70) + typeIndex
        /// </para>
        /// <para>
        /// When <paramref name="cachedObjectCacheIndex"/> >= 0: COMBINED markers.
        /// Object body follows directly (no inner Object/ObjectRefFirst marker).
        ///   First type occurrence: ObjectWithTypeNameRefFirst (69) + typename + refCacheIndex
        ///   Cached type: ObjectWithTypeIndexRefFirst (71) + typeIndex + refCacheIndex
        /// </para>
        /// </summary>
        //[MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void WritePolymorphicPrefix(Type runtimeType, int cachedObjectCacheIndex = -1)
        {
            var rtWrapper = GetWrapper(runtimeType);
            if (!rtWrapper.PolymorphicSeen)
            {
                rtWrapper.PolymorphicSeen = true;
                rtWrapper.PolymorphicCacheIndex = _nextTypeSlot++;
                if (cachedObjectCacheIndex >= 0)
                {
                    WriteByte(BinaryTypeCode.ObjectWithTypeNameRefFirst);
                    WriteStringUtf8(runtimeType.FullName!);
                    WriteVarUInt((uint)cachedObjectCacheIndex);
                }
                else
                {
                    WriteByte(BinaryTypeCode.ObjectWithTypeName);
                    WriteStringUtf8(runtimeType.FullName!);
                }
            }
            else
            {
                if (cachedObjectCacheIndex >= 0)
                {
                    WriteByte(BinaryTypeCode.ObjectWithTypeIndexRefFirst);
                    WriteVarUInt((uint)rtWrapper.PolymorphicCacheIndex);
                    WriteVarUInt((uint)cachedObjectCacheIndex);
                }
                else
                {
                    WriteByte(BinaryTypeCode.ObjectWithTypeIndex);
                    WriteVarUInt((uint)rtWrapper.PolymorphicCacheIndex);
                }
            }
        }

        #endregion

        #region UseMetadata Type Tracking

        /// <summary>
        /// Registers a type for UseMetadata first/repeated tracking.
        /// Returns true on first occurrence (caller should write inline property hashes),
        /// false on repeated (caller writes only typeNameHash).
        /// Used by both runtime and SGen paths via wrapper.
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static bool RegisterMetadataType(TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper)
        {
            if (wrapper.MetadataSeen)
                return false;

            wrapper.MetadataSeen = true;
            return true;
        }

        /// <summary>
        /// Inline metadata kiírása az ObjectWithMetadata marker után.
        /// Első előfordulás: [propNameHash (4b)][propCount (VarUInt)][hash0 (4b)][hash1 (4b)]...
        /// Ismételt:         [propNameHash (4b)]
        /// </summary>
        //[MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void WriteInlineMetadata(BinarySerializeTypeMetadata metadata, bool isFirstOccurrence)
        {
            WriteRaw(metadata.PropNameHash);

            if (isFirstOccurrence)
            {
                var hashes = metadata.MetadataPropertyHashes;
                WriteVarUInt((uint)hashes.Length);
                for (var i = 0; i < hashes.Length; i++)
                {
                    WriteRaw(hashes[i]);
                }
            }
        }

        #endregion

        #region Property State Buffer

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public byte[] RentPropertyStateBuffer(int size)
        {
            if (_propertyStateBuffer != null && _propertyStateBuffer.Length >= size)
            {
                return _propertyStateBuffer;
            }

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

            _propertyStateBuffer = ArrayPool<byte>.Shared.Rent(size);
            return _propertyStateBuffer;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void ReturnPropertyStateBuffer(byte[] buffer)
        {
            // Buffer stays cached for reuse.
        }

        #endregion

        #region Property Filtering

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public bool ShouldSerializeProperty(object instance, BinaryPropertyAccessor property)
        {
            if (PropertyFilter == null)
            {
                return true;
            }

            var context = new BinaryPropertyFilterContext(
                instance,
                property.DeclaringType,
                property.Name,
                property.PropertyType,
                property.DynamicGetter);

            return PropertyFilter(context);
        }

        public bool CheckDuplicatePropName => Options.CheckDuplicatePropName;

        #endregion

        #region Header

        /// <summary>
        /// Writes the binary header directly. Call AFTER ScanForDuplicates (cacheCount is known).
        /// No placeholder, no shift — single forward write.
        /// Layout: [version (1b)][flags (1b)][cacheCount (VarUInt, if caching)]
        /// </summary>
        public void WriteHeader()
        {
            var flags = BinaryTypeCode.HeaderFlagsBase;
            if (UseMetadata)
                flags |= BinaryTypeCode.HeaderFlag_Metadata;
            if (ReferenceHandling == ReferenceHandlingMode.OnlyId)
                flags |= BinaryTypeCode.HeaderFlag_RefHandling_OnlyId;
            else if (ReferenceHandling == ReferenceHandlingMode.All)
                flags |= (byte)(BinaryTypeCode.HeaderFlag_RefHandling_OnlyId | BinaryTypeCode.HeaderFlag_RefHandling_All);
            if (HasCaching)
                flags |= BinaryTypeCode.HeaderFlag_HasCacheCount;

            WriteByte(AcBinarySerializerOptions.FormatVersion);
            WriteByte(flags);

            if (HasCaching)
            {
                WriteVarUInt((uint)GetCacheCount());
            }
        }

        #endregion

        #region Helpers

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static void ClearAndTrimIfNeeded<TKey, TValue>(Dictionary<TKey, TValue>? dict, int maxCapacity)
            where TKey : notnull
        {
            if (dict == null)
            {
                return;
            }

            dict.Clear();
            if (dict.EnsureCapacity(0) > maxCapacity)
            {
                dict.TrimExcess();
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static void ClearAndTrimIfNeeded<T>(HashSet<T>? set, int maxCapacity)
        {
            if (set == null)
            {
                return;
            }

            set.Clear();
            if (set.EnsureCapacity(0) > maxCapacity)
            {
                set.TrimExcess();
            }
        }

        #endregion
    }
}
