using System.Buffers;
using System.Collections.Concurrent;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
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 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) GrowAndValidate(additionalBytes);
        }

        /// <summary>
        /// Slow path for <see cref="EnsureCapacity"/>: invokes <c>Output.Grow</c> and revalidates that
        /// the request was actually satisfied. A chunk-limited output (e.g. <c>AsyncPipeWriterOutput</c>
        /// on a single-value write &gt; <c>MaxChunkSize</c> data bytes) may return WITHOUT growing the
        /// active buffer to the requested size — silent under-provisioning would cause downstream
        /// <c>ArgumentOutOfRangeException</c> in <c>AsSpan</c>/<c>CopyTo</c> at corrupt offsets.
        /// <c>NoInlining</c> keeps the hot path of <see cref="EnsureCapacity"/> tiny and inline-friendly.
        /// Related limit: see <c>BINARY_ASYNCPIPE_ISSUES.md#accore-bin-i-b7k9</c>.
        /// </summary>
        [MethodImpl(MethodImplOptions.NoInlining)]
        private void GrowAndValidate(int additionalBytes)
        {
            Output.Grow(ref _buffer, ref _position, ref _bufferEnd, additionalBytes);

            var available = _bufferEnd - _position;
            if (available < additionalBytes) ThrowGrowFailedToSatisfy(additionalBytes, available);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void WriteByte(byte value)
        {
            if (_position >= _bufferEnd)
                Output.Grow(ref _buffer, ref _position, ref _bufferEnd, 1);
            _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)
        {
            return value switch
            {
                < 0x80 => 1,
                < 0x4000 => 2,
                < 0x200000 => 3,
                < 0x10000000 => 4,
                _ => 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

        /// <summary>
        /// FastWire markerless string write — int32 sentinel header. Self-contained: handles all three
        /// states (null / empty / content) via int32 dispatch. <c>-1</c> = null, <c>0</c> = empty,
        /// <c>N &gt; 0</c> = content (followed by N×2 UTF-16 raw bytes). Saves 1 byte per content string vs
        /// the markered <see cref="WriteStringWithDispatch"/> StringSmall scheme; null/empty pay +3 bytes
        /// (4-byte int32 vs 1-byte marker), but null/empty are rare in typical workloads → net wire-size win.
        /// Companion reader is <see cref="BinaryDeserializationContext{TInput}.ReadStringUtf16Markerless"/>.
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void WriteStringUtf16Markerless(string? value)
        {
            if (value == null) { WriteRaw(-1); return; }
            var charLength = value.Length;
            if (charLength == 0) { WriteRaw(0); return; }

            var byteLenF = charLength * 2;
            EnsureCapacity(4 + byteLenF);
            Unsafe.WriteUnaligned<int>(ref _buffer[_position], charLength);
            _position += 4;
            MemoryMarshal.AsBytes(value.AsSpan()).CopyTo(_buffer.AsSpan(_position, byteLenF));
            _position += byteLenF;
        }

        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;
            System.Text.Unicode.Utf8.FromUtf16(value.AsSpan(), _buffer.AsSpan(encodeStart, maxBytes), out _, out var bytesWritten, replaceInvalidSequences: false);

            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 non-ASCII in-place from
        /// the encoder's byte count, then emits the appropriate wire marker:
        /// <list type="bullet">
        ///   <item>ASCII ≤ 31 byte → <c>FixStrAscii</c> (1-byte header, length in marker)</item>
        ///   <item>ASCII &gt; 31 byte → <c>StringAscii</c> (1+VarUInt header)</item>
        ///   <item>Non-ASCII utf8Len ≤ 255 → <c>StringSmall</c> (3-byte header: marker + charLen:8 + utf8Len:8)</item>
        ///   <item>Non-ASCII utf8Len ≤ 65535 → <c>StringMedium</c> (5-byte header: marker + charLen:16 + utf8Len:16)</item>
        ///   <item>Non-ASCII utf8Len &gt; 65535 → <c>StringBig</c> (9-byte header: marker + charLen:32 + utf8Len:32)</item>
        /// </list>
        /// </summary>
        /// <remarks>
        /// H2Q6 wire format v3 — non-ASCII tiers carry both <c>charLen</c> and <c>utf8Len</c> in the header,
        /// enabling 1-pass deserialize (no <c>CountUtf8Chars</c> Pass 1). ASCII path unchanged from M3R7.
        ///
        /// <para>Optimistic encode position is chosen by tier-prediction from <c>charLength</c>
        /// (worst-case 4 byte/char): ≤ 63 char → Small (3-byte header reserved); ≤ 16383 char → Medium
        /// (5-byte header reserved); else Big (9-byte). After encoding, <c>bytesWritten</c> determines
        /// the actual tier and the body is left-shifted only if the actual header is smaller than
        /// reserved (rare on Magyar text — short Hungarian content stays in Small tier with 0 shift).</para>
        ///
        /// <para>FastWire mode: re-uses the <c>StringSmall</c> marker value (91) as a generic
        /// "string marker" — body layout differs (UTF-16 raw + VarUInt charCount) and the reader
        /// dispatches by serializer mode, NOT by re-interpreting the marker. The 91 value is
        /// mode-shared because the wire envelope is mode-tagged at the header level.</para>
        ///
        /// <para>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.</para>
        /// </remarks>
        // V4N4 method-split reverted (2026-05-07): the split (Writer dispatcher + SmallFast + DispatchLong
        // + FastWire) was tested 2026-05-07 in two configurations (15:13:39 AggressiveInlining → regression;
        // 15:29:21 NoInlining-on-SmallFast → marginal/inconsistent). Bench-to-bench variance proved
        // unmeasurable on the available hardware — the optimization-value signal is below the noise floor.
        // Reverted to the single-method state (matches 09:39:09 baseline). The A-direction packed-header
        // store optimization (Unsafe.WriteUnaligned ushort/uint/ulong) is preserved — it was already in the
        // 09:39:09 baseline and is instruction-level, not affected by AOT inline-pressure variance.
        public void WriteStringWithDispatch(string value)
        {
            var charLength = value.Length;
            // Overflow guard (O7G2) — predict-friendly (always false on realistic input). NoInlining throw helper.
            if ((uint)charLength > BinaryTypeCode.MaxStringCharLength) ThrowStringTooLong(charLength);

            if (FastWire)
            {
                // FastWire markerless: int32 sentinel (-1 = null, 0 = empty, N > 0 = content + N*2 UTF-16 bytes).
                // All FastWire string writes funnel through here (WriteStringGenerated → WriteString →
                // WriteStringWithDispatch + WritePropertyOrSkip String case + TryWritePrimitive String case),
                // so a single change here propagates markerless wire to property + collection + dictionary +
                // runtime paths. Caller (WriteString) guarantees value is non-empty content; null/empty
                // sentinel encoding lives inside `WriteStringUtf16Markerless` for direct callers.
                WriteStringUtf16Markerless(value);
                return;
            }

            // Compact mode — H2Q6 post-encode tier dispatch (wire-optimal).
            //
            // Two-step tier logic:
            //   1. reserveHeader (from charLength, worst-case 4 byte/char): bounds buffer allocation
            //      AND encode position. Tight reserve (3/5/9) avoids large memmove on the hot path.
            //   2. actualHeader (from bytesWritten after encode): chooses the smallest fitting tier.
            //      A mostly-ASCII string in the 64-16383 char band gets Small (3 byte header) even though
            //      reserve was Medium (5 byte) — body is left-shifted by 2 bytes to compact.
            var maxBytes = charLength * 4;

            int reserveHeader = charLength switch
            {
                <= 63 => 3,
                <= 16383 => 5,
                _ => 9
            };

            EnsureCapacity(reserveHeader + maxBytes);

            var savedPos = _position;
            var encodeStart = savedPos + reserveHeader;
            System.Text.Unicode.Utf8.FromUtf16(value.AsSpan(), _buffer.AsSpan(encodeStart, maxBytes), out _, out var bytesWritten, replaceInvalidSequences: false);

            if (bytesWritten == charLength)
            {
                // ASCII override — FixStrAscii (≤31) or StringAscii (>31) with compact header
                if (bytesWritten <= BinaryTypeCode.FixStrAsciiMaxLength)
                {
                    _buffer.AsSpan(encodeStart, bytesWritten).CopyTo(_buffer.AsSpan(savedPos + 1, bytesWritten));
                    _buffer[savedPos] = BinaryTypeCode.EncodeFixStrAscii(bytesWritten);
                    _position = savedPos + 1 + bytesWritten;
                }
                else
                {
                    var actualVarUIntSize = VarUIntSize((uint)bytesWritten);
                    var asciiHeader = 1 + actualVarUIntSize;
                    var shift = reserveHeader - asciiHeader;
                    if (shift > 0)
                        _buffer.AsSpan(encodeStart, bytesWritten).CopyTo(_buffer.AsSpan(encodeStart - shift, bytesWritten));
                    _buffer[savedPos] = BinaryTypeCode.StringAscii;
                    _position = savedPos + 1;
                    WriteVarUIntUnsafe((uint)bytesWritten);
                    _position += bytesWritten;
                }

                return;
            }

            switch (bytesWritten)
            {
                // Non-ASCII — post-encode tier choice (smallest fitting tier wins). One if-else chain
                // per tier; each branch handles shift + header-store + position update inline.
                //
                // Combined header-store optimization (shift > 0 only):
                //   When the actual tier downgrades from the predicted reserve (e.g. Medium predicted
                //   from charLength but Small actual from bytesWritten), the body needs a left-shift
                //   memcpy. We write the FULL combined header (uint for Small, ulong for Medium) at
                //   savedPos BEFORE the body memcpy — the slack byte(s) at the high end of the store
                //   get overwritten by the subsequent memcpy → 1 store instead of 1+N for the header.
                //   When shift == 0 (predicted tier matches actual), the body is already at its final
                //   position; a combined store would corrupt the body's first byte(s), so we fall
                //   back to separate marker + packed-header stores.
                //
                // Big tier (9-byte header) always has shift == 0 (reserveHeader == actualHeader == 9)
                // because Big is the largest tier — no downgrade path possible. The 1-byte marker +
                // 8-byte ulong-packed pattern is already minimal (no slack issue, 9 dedicated bytes).
                case <= 255:
                {
                    // Small tier: 3-byte header [marker:1][charLen:8][utf8Len:8]
                    var shift = reserveHeader - 3;
                    if (shift > 0)
                    {
                        // Combined uint store: 4 bytes physical, 3 bytes meaningful, 1 byte slack
                        // at savedPos+3 — overwritten by body memcpy below.
                        var packedFull = (uint)BinaryTypeCode.StringSmall
                                         | ((uint)charLength << 8)
                                         | ((uint)bytesWritten << 16);
                        Unsafe.WriteUnaligned<uint>(ref _buffer[savedPos], packedFull);
                        _buffer.AsSpan(encodeStart, bytesWritten).CopyTo(_buffer.AsSpan(savedPos + 3, bytesWritten));
                    }
                    else
                    {
                        // shift == 0: body in place at savedPos+3 = encodeStart. Combined uint store
                        // would corrupt body's first byte; use separate 1-byte marker + 2-byte packed.
                        _buffer[savedPos] = BinaryTypeCode.StringSmall;
                        var packedHl = (ushort)(charLength | (bytesWritten << 8));
                        Unsafe.WriteUnaligned<ushort>(ref _buffer[savedPos + 1], packedHl);
                    }
                    _position = savedPos + 3 + bytesWritten;
                    break;
                }
                case <= 65535:
                {
                    // Medium tier: 5-byte header [marker:1][charLen:16][utf8Len:16]
                    var shift = reserveHeader - 5;
                    if (shift > 0)
                    {
                        // Combined ulong store: 8 bytes physical, 5 bytes meaningful, 3 bytes slack
                        // at savedPos+5..7 — overwritten by body memcpy below.
                        var packedFull = (ulong)BinaryTypeCode.StringMedium
                                         | ((ulong)(uint)charLength << 8)
                                         | ((ulong)(uint)bytesWritten << 24);
                        Unsafe.WriteUnaligned<ulong>(ref _buffer[savedPos], packedFull);
                        _buffer.AsSpan(encodeStart, bytesWritten).CopyTo(_buffer.AsSpan(savedPos + 5, bytesWritten));
                    }
                    else
                    {
                        // shift == 0: separate 1-byte marker + 4-byte packed.
                        _buffer[savedPos] = BinaryTypeCode.StringMedium;
                        var packedHl = (uint)charLength | ((uint)bytesWritten << 16);
                        Unsafe.WriteUnaligned<uint>(ref _buffer[savedPos + 1], packedHl);
                    }
                    _position = savedPos + 5 + bytesWritten;
                    break;
                }
                default:
                {
                    // Big tier: 9-byte header [marker:1][charLen:32][utf8Len:32]. shift always 0.
                    _buffer[savedPos] = BinaryTypeCode.StringBig;
                    var packed = (ulong)(uint)charLength | ((ulong)(uint)bytesWritten << 32);
                    Unsafe.WriteUnaligned<ulong>(ref _buffer[savedPos + 1], packed);
                    _position = savedPos + 9 + bytesWritten;
                    break;
                }
            }
        }

        /// <summary>
        /// Writes the first-occurrence body of an interned string with H2Q6 tier-marker dispatch.
        /// Used by the runtime/SGen string-intern write path; subsequent occurrences use cache-index ref.
        /// </summary>
        /// <remarks>
        /// Wire layout per tier:
        /// <list type="bullet">
        ///   <item><c>StringInternFirstSmall</c>: <c>[marker:1][cacheIdx:VarUInt][charLen:8][utf8Len:8][bytes]</c> — utf8Len ≤ 255</item>
        ///   <item><c>StringInternFirstMedium</c>: <c>[marker:1][cacheIdx:VarUInt][charLen:16][utf8Len:16][bytes]</c> — utf8Len ≤ 65535</item>
        /// </list>
        ///
        /// <para>Big tier never engages — <c>MaxStringInternLength</c> is byte-typed in
        /// <c>AcBinarySerializerOptions</c> (abszolút max 255 char × 4 byte/char = 1020 byte fits in Medium).</para>
        ///
        /// <para>Tier prediction by <c>charLength</c>: ≤ 63 char → Small (worst-case 252 byte ≤ 255);
        /// &gt; 63 char → Medium. Body is left-shifted by 2 bytes only when a long mostly-ASCII interning
        /// string drops back into Small tier (rare).</para>
        /// </remarks>
        public void WriteStringInternFirstWithDispatch(string value, int cacheMapIndex)
        {
            // Post-encode tier choice (wire-optimal): mostly-ASCII interning string in the 64+ char band
            // emits Small tier (3 byte) when bytesWritten ≤ 255, instead of Medium (5 byte). Big tier
            // never engages — MaxStringInternLength byte-typed (max 255 char × 4 byte = 1020 byte fits in Medium).
            var charLength = value.Length;
            // Overflow guard (defensive — interning length is byte-typed so this should never trigger,
            // but stays consistent with WriteStringWithDispatch and protects against future refactors).
            if ((uint)charLength > BinaryTypeCode.MaxStringCharLength) ThrowStringTooLong(charLength);

            var maxBytes = charLength * 4;
            var cacheIdxSize = VarUIntSize((uint)cacheMapIndex);

            // reserveHeader: charLength ≤ 63 → guaranteed Small (252 byte ≤ 255); else Medium-reserve.
            var reserveHeader = charLength <= 63 ? 3 : 5;

            EnsureCapacity(cacheIdxSize + reserveHeader + maxBytes);

            var savedPos = _position;
            var encodeStart = savedPos + cacheIdxSize + reserveHeader;
            System.Text.Unicode.Utf8.FromUtf16(value.AsSpan(), _buffer.AsSpan(encodeStart, maxBytes), out _, out var bytesWritten, replaceInvalidSequences: false);

            // Choose tier from actual bytesWritten (smallest fits)
            var actualHeader = bytesWritten <= 255 ? 3 : 5;
            var tierMarker = actualHeader == 3 ? BinaryTypeCode.StringInternFirstSmall : BinaryTypeCode.StringInternFirstMedium;

            var shift = reserveHeader - actualHeader;
            if (shift > 0)
                _buffer.AsSpan(encodeStart, bytesWritten).CopyTo(_buffer.AsSpan(encodeStart - shift, bytesWritten));

            // Write [marker][cacheIdx VarUInt][charLen + utf8Len header][bytes]
            _buffer[savedPos] = tierMarker;
            _position = savedPos + 1;

            WriteVarUIntUnsafe((uint)cacheMapIndex);

            if (actualHeader == 3)
            {
                // Pack charLen:8 | utf8Len:8 → single ushort store
                var packed = (ushort)(charLength | (bytesWritten << 8));
                Unsafe.WriteUnaligned<ushort>(ref _buffer[_position], packed);
                _position += 2;
            }
            else
            {
                // Pack charLen:16 | utf8Len:16 → single uint store, LE
                var packed = (uint)charLength | ((uint)bytesWritten << 16);
                Unsafe.WriteUnaligned<uint>(ref _buffer[_position], packed);
                _position += 4;
            }

            _position += bytesWritten;
        }

        // ─────────────────────────────────────────────────────────────────
        // V4N5 dead-code cleanup (2026-05-06): WriteFixStr, WriteFixStrDirect, WriteFixStrBytes,
        // WritePreencodedPropertyName, and WriteStringUtf8Internal removed — these were unreachable
        // (no core call site, no SourceGenerator template hit, no test, no reflection path).
        // The hot-path string writes go through WriteStringWithDispatch (M3R7 + H2Q6 marker dispatch).
        // ─────────────────────────────────────────────────────────────────

        /// <summary>
        /// Throw helper for the overflow guard in <see cref="WriteStringWithDispatch"/> and
        /// <see cref="WriteStringInternFirstWithDispatch"/>. Marked <c>NoInlining</c> so the hot path
        /// stays compact — the JIT/AOT keeps the throw-site out of the inlined caller body.
        /// </summary>
        [MethodImpl(MethodImplOptions.NoInlining)]
        private static void ThrowStringTooLong(int charLength) =>
            throw new InvalidOperationException(
                $"String too long for binary serialization: {charLength} chars exceeds {BinaryTypeCode.MaxStringCharLength}. " +
                $"This limit is dictated by the writer's worst-case 'charLength * 4' UTF-8 byte allocation; " +
                $"larger inputs would silently overflow int arithmetic.");

        /// <summary>
        /// Throw helper for <see cref="GrowAndValidate"/>: invoked when <c>Output.Grow</c> returns
        /// without satisfying the requested capacity (chunk-limited output, single value larger than
        /// the per-chunk data capacity). Marked <c>NoInlining</c> so the throw-site stays out of the
        /// inlined caller body. See <c>BINARY_ASYNCPIPE_ISSUES.md#accore-bin-i-b7k9</c> for fix direction.
        /// </summary>
        [MethodImpl(MethodImplOptions.NoInlining)]
        private static void ThrowGrowFailedToSatisfy(int requested, int available) =>
            throw new InvalidOperationException(
                $"Output.Grow did not satisfy the requested capacity: {requested} bytes requested, only {available} bytes available after Grow. " +
                $"This typically indicates a chunk-limited output (e.g. AsyncPipeWriterOutput) with a single serialized value exceeding the per-chunk data capacity. " +
                $"See BINARY_ASYNCPIPE_ISSUES.md#accore-bin-i-b7k9.");

        #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
    }
}
