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.

        /// <summary>
        /// Global recursion depth counter — final safety net against pathological graphs and non-IId cycles.
        /// Incremented at <see cref="EnterRecursion"/> (before marker write at each object-recursion entry),
        /// decremented at <see cref="ExitRecursion"/> (at WriteProperties/ScanObject body exit).
        /// Checked against <see cref="AcSerializerContextBase{TMetadata,TOptions}.MaxDepth"/> (byte, default 255)
        /// inside <see cref="TryHandleMaxDepth"/> which fires BEFORE any marker byte is written.
        /// </summary>
        private byte _recursionDepth;

        /// <summary>
        /// Pre-computed depth-check gate, set at <see cref="Reset"/>: <c>MaxDepthBehavior != Disable</c>.
        /// Only the explicit <see cref="MaxDepthBehavior.Disable"/> opts out. <see cref="MaxDepthBehavior.Throw"/>
        /// stays active even with <c>HasAllRefHandling=true</c>, because per-type <c>EnableRefHandling=false</c>
        /// attribute opt-outs leave gaps that ref-handling alone cannot cover (cycle through a non-tracked type).
        /// </summary>
        private bool _needsDepthCheck;

        /// <summary>
        /// Pre-computed action when depth reaches MaxDepth. Consulted on the rare depth-limit-hit cold path only.
        /// </summary>
        private MaxDepthBehavior _maxDepthAction;

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

            // Pre-compute depth-check gate. Only `MaxDepthBehavior.Disable` opts out:
            // - `Throw` (default): safety net. `HasAllRefHandling=true` is NOT enough on its own — per-type
            //   `EnableRefHandling=false` attribute opt-outs leave non-tracked types that can still form cycles.
            //   The safety net catches those + pathological-depth non-cyclic graphs.
            // - `Truncate`: explicit developer intent for intentional shallow serialization (delta updates,
            //   view-model projections). Always active when set, regardless of ref-handling mode.
            // - `Disable`: explicit opt-out — dev guarantees cycle-free + bounded-depth graph.
            // (MaxDepth value itself is NOT an opt-out — 0 means shallow-copy semantic when paired with Truncate.)
            _maxDepthAction = Options.MaxDepthBehavior;
            _needsDepthCheck = _maxDepthAction != MaxDepthBehavior.Disable;
            _recursionDepth = 0;
        }

        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;
            _recursionDepth = 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();
        }

        /// <summary>
        /// Hot wrapper: combined depth check + recursion enter. Gated by <c>_needsDepthCheck</c>.
        /// On limit hit: dispatches to <see cref="OnMaxDepthHit"/> (truncate writes Null or throw) and returns
        /// <c>true</c> — caller must skip marker write + recursive call. NO inc on this path.
        /// On miss: increments <c>_recursionDepth</c> and returns <c>false</c> — caller proceeds with marker write
        /// and recursive body. <see cref="ExitRecursion"/> at body exit balances the inc.
        /// Called BEFORE any marker write — wire-correct for any marker width.
        /// 2nd-occurrence ObjectRef paths (which don't actually recurse into a body) must call
        /// <see cref="ExitRecursion"/> to undo the inc that this method did on the success path.
        /// Single ctx field-read pattern: 2 ops merged into 1 method call vs the prior 3-method split.
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal bool TryEnterRecursion(bool hasTruncatePath)
        {
            if (!_needsDepthCheck) return false;
            if (_recursionDepth >= _maxDepth)
            {
                OnMaxDepthHit(hasTruncatePath);
                return true;
            }
            _recursionDepth++;
            return false;
        }

        /// <summary>
        /// Dec <c>_recursionDepth</c> if depth tracking is active. Called at WriteProperties/ScanObject body exit
        /// to balance the corresponding <see cref="TryEnterRecursion"/> call on its success path.
        /// Also called on 2nd-occurrence ObjectRef paths in <c>WriteObjectFullMarker*</c> / <c>WriteObjectRefMarker*</c>
        /// to undo the inc (2nd-occ writes ObjectRef without recursing — wrote no body, must dec back).
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal void ExitRecursion()
        {
            if (_needsDepthCheck) _recursionDepth--;
        }

        /// <summary>
        /// Cold path: dispatches on <c>_maxDepthAction</c> when the depth limit is hit.
        /// <list type="bullet">
        ///   <item><c>Throw</c>: throws <see cref="InvalidOperationException"/> (offending type name comes from the stack trace)</item>
        ///   <item><c>Truncate</c> + <paramref name="hasTruncatePath"/>=<c>true</c> (write pass): writes <c>Null</c> marker in place of the object — intentional shallow serialization. Wire-correct: no marker has been written yet, so no rewind needed</item>
        ///   <item><c>Truncate</c> + <paramref name="hasTruncatePath"/>=<c>false</c> (scan pass): no-op — caller's return skips children scan</item>
        /// </list>
        /// <c>NoInlining</c> keeps the throw / WriteByte body out of the hot caller (smaller i-cache footprint).
        /// </summary>
        [MethodImpl(MethodImplOptions.NoInlining)]
        private void OnMaxDepthHit(bool hasTruncatePath)
        {
            if (_maxDepthAction == MaxDepthBehavior.Throw)
                throw new InvalidOperationException(
                    $"AcBinary: recursion depth exceeded MaxDepth={_maxDepth} (depth={_recursionDepth}, position={Position})");
            // Truncate: write Null in place of the object. No rewind — check fires BEFORE any marker write.
            if (hasTruncatePath) WriteByte(BinaryTypeCode.Null);
        }

        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

        /// <summary>
        /// Unchecked <c>ref byte</c> into <see cref="_buffer"/> at <paramref name="position"/> — omits the
        /// JIT array bounds-check that a plain <c>_buffer[position]</c> index emits on every write.
        /// </summary>
        /// <remarks>
        /// Safe by the buffer invariant: every write primitive first guarantees <c>position &lt; _bufferEnd</c>
        /// (the <c>_position &lt; _bufferEnd</c> grow-guard or <see cref="EnsureCapacity"/>), and <c>_bufferEnd</c>
        /// is "one past the last writable index in <c>_buffer</c>" ⇒ <c>_bufferEnd &lt;= _buffer.Length</c>.
        /// So <c>position &lt; _buffer.Length</c> already holds — the capacity guard IS the bounds check.
        /// The JIT cannot see the <c>_bufferEnd &lt;= _buffer.Length</c> relation, so <c>_buffer[position]</c>
        /// would emit a second, redundant <c>cmp/jae</c> per write; this helper removes it. DEBUG builds keep
        /// an explicit guard so a misbehaving <c>IBinaryOutputBase</c> surfaces loudly, not as corruption.
        /// </remarks>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private ref byte BufferAt(int position)
        {
#if DEBUG
            if ((uint)position >= (uint)_buffer.Length)
                throw new InvalidOperationException(
                    $"BufferAt({position}) out of range — buffer invariant violated " +
                    $"(_buffer.Length={_buffer.Length}, _bufferEnd={_bufferEnd}).");
#endif
            return ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_buffer), (nint)(uint)position);
        }

        [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) GrowOne();
            BufferAt(_position++) = value;
        }

        /// <summary>
        /// Cold-path single-byte grow helper. Outlined from the hot-path 1-byte writers
        /// (<see cref="WriteByte"/>, <see cref="WriteVarUInt"/> / <see cref="WriteVarULong"/> fast-paths)
        /// so the inliner cost-models the hot path WITHOUT the 4-ref-arg <c>Output.Grow</c> call's
        /// argument-prep IL. This keeps the per-property hot-path tight enough for AOT to inline the
        /// write into the source-gen <c>WriteProperties</c> body across many call-sites — the cumulative
        /// gain shows up on graph-heavy cells where every leaf serialises ~6 primitive properties.
        /// </summary>
        [MethodImpl(MethodImplOptions.NoInlining)]
        private void GrowOne() => Output.Grow(ref _buffer, ref _position, ref _bufferEnd, 1);

        [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)
        {
            BufferAt(_position++) = b1;
            BufferAt(_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 BufferAt(_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
        {
            BufferAt(_position++) = typeCode;
            Unsafe.WriteUnaligned(ref BufferAt(_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) GrowOne();
                BufferAt(_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)
            {
                BufferAt(_position++) = (byte)value;
                return;
            }

            WriteVarUIntMultiByteUnsafe(value);
        }

        /// <summary>
        /// Prefix-tier VarUInt encoding (UTF-8-style: first byte's high bits encode total size).
        /// Compact path: <see cref="BitOperations.Log2"/> picks the tier (2/3/4) in O(1), then a single
        /// <see cref="Unsafe.WriteUnaligned{T}"/>&lt;<see cref="uint"/>&gt; stores [prefix-byte | value-bytes LE]
        /// in one machine instruction. 5-byte tier uses one byte + one uint32 store.
        /// Tier table:
        ///   0xxxxxxx                → 1 byte (handled inline by caller)
        ///   10xxxxxx + 1B           → 2 byte,  128..16 383                 (14 bit)
        ///   110xxxxx + 2B LE        → 3 byte,  16 384..2 097 151           (21 bit)
        ///   1110xxxx + 3B LE        → 4 byte,  2 097 152..268 435 455      (28 bit)
        ///   1111xxxx + 4B LE        → 5 byte,  268 435 456..uint.MaxValue  (32 bit; prefix nibble unused)
        /// Caller MUST ensure ≥5 bytes of buffer space (interface contract) — the uint32 store on the
        /// 2/3/4-byte tiers writes 4 bytes even though only `tier` bytes are advanced; the trailing
        /// 1-2 bytes get overwritten by the next encoded element. Little-endian host assumed (all
        /// shipping .NET 9 platforms).
        /// </summary>
        [MethodImpl(MethodImplOptions.NoInlining)]
        private void WriteVarUIntMultiByteUnsafe(uint value)
        {
            // Writes EXACTLY `tier` bytes per call — does NOT overrun into following buffer space.
            // (The earlier Unsafe.WriteUnaligned<uint> compact path wrote 4 bytes on 2/3-byte tiers
            // expecting the trailing 1-2 bytes to be overwritten by the next encoded element. That
            // assumption breaks for callers using a savedPos-rewind-then-prefix pattern — e.g.
            // WriteStringUtf8 where the rewinded position sits right before already-emitted UTF-8
            // bytes; a 4-byte uint store overwrites the first 1-2 bytes of the UTF-8 body with zero
            // padding, corrupting the string on the wire.)

            BufferAt(_position + 1) = (byte)value;
            if (value < 0x4000)
            {
                // 2-byte tier: 10xxxxxx (high 6 bits of value) + low 8 bits.
                BufferAt(_position) = (byte)(0x80 | (value >> 8));
                _position += 2;
                return;
            }

            BufferAt(_position + 2) = (byte)(value >> 8);
            if (value < 0x200000)
            {
                // 3-byte tier: 110xxxxx (high 5 bits) + 2 bytes LE (low 16 bits).
                BufferAt(_position) = (byte)(0xC0 | (value >> 16));
                _position += 3;
                return;
            }

            BufferAt(_position + 3) = (byte)(value >> 16);
            if (value < 0x10000000)
            {
                // 4-byte tier: 1110xxxx (high 4 bits) + 3 bytes LE (low 24 bits).
                BufferAt(_position) = (byte)(0xE0 | (value >> 24));
                
                _position += 4;
                return;
            }
            // 5-byte tier: 1111xxxx (low nibble unused) + 4 bytes LE (full uint32).
            BufferAt(_position) = 0xF0;
            BufferAt(_position + 4) = (byte)(value >> 24);
            _position += 5;
        }

        [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) GrowOne();

                BufferAt(_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)
            {
                BufferAt(_position++) = (byte)value;
                return;
            }

            WriteVarULongMultiByteUnsafe(value);
        }

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

            BufferAt(_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 BufferAt(_position), value.Ticks);
            BufferAt(_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 BufferAt(_position), value.UtcTicks);
            Unsafe.WriteUnaligned(ref BufferAt(_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(ref BufferAt(_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;
            // Tight UTF-8 upper bound for valid UTF-16 input: max 3 bytes per UTF-16 code unit.
            var maxBytes = charLength * 3;
            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 UTF-8 string in a shift-free layout with an unsigned excess slot.
        /// </summary>
        /// <remarks>
        /// Header is fully determined before encode:
        /// <list type="bullet">
        ///   <item><c>charLength &lt;= 31</c>: <c>[FixStr(marker carries charLength)][unsigned excess:1]</c></item>
        ///   <item><c>charLength &gt; 31</c>: <c>[String][VarUInt(charLength - FixStrCount)][unsigned excess:1|2|4]</c> — single marker with prefix-tier VarUInt charLength</item>
        /// </list>
        /// Body is UTF-8-encoded exactly once to the final destination (<c>encodeStart</c>) — no post-encode
        /// body shift/copy. For the current path, <c>excess = bytesWritten - charLength</c> is expected to be
        /// non-negative (ASCII=0, UTF-8=&gt;0). UTF-16 signed-negative slot usage remains on the existing FastWire
        /// path for now and is intentionally not activated in this method.
        ///
        /// <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>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void WriteStringWithDispatch(string value)
        {
            var charLength = value.Length;

#if DEBUG
            System.Diagnostics.Debug.Assert(charLength > 0, "WriteStringWithDispatch expects non-empty string; empty is handled by StringEmpty marker in WriteString.");
#endif

            // Overflow guard (O7G2) — predict-friendly (always false on realistic input). NoInlining throw helper.
            //if ((uint)charLength > BinaryTypeCode.MaxStringCharLength) ThrowStringTooLong(charLength);

            // Tight UTF-8 upper bound for valid UTF-16 input: max 3 bytes per UTF-16 code unit.
            var maxBytes = charLength * 3;

            // Single branch on FixStr vs long-form — replaces the previous 4 ternary-on-isFixStr cascade.
            // IMPORTANT: the slot VALUE (excess) is not known before UTF-8 encode, but the slot SIZE is.
            // We reserve the slot by width (1/2/4) from charLength, so encodeStart is final and no body shift is needed.
            int slotSize, headerSize, headerPos, slotPos, encodeStart;
            if (charLength <= BinaryTypeCode.FixStrMaxLength)
            {
                // FixStr: header = [marker:1][slot:1]
                slotSize = 1;
                headerSize = 2;

                EnsureCapacity(headerSize + maxBytes);

                headerPos = _position;
                slotPos = headerPos + 1;
                encodeStart = headerPos + 2;

                BufferAt(headerPos) = BinaryTypeCode.EncodeFixStr(charLength);
            }
            else
            {
                // Long-form: header = [marker:1][VarUInt(charLength - FixStrCount)][slot:1|2|4]
                // FixStr already covers 0..FixStrMaxLength, so wireLen = charLength - FixStrCount
                // keeps the small-band 1-byte VarUInt populated.
                slotSize = BinaryTypeCode.GetUniversalStringExcessSlotSize(charLength);
                var varUIntSize = VarUIntSize((uint)(charLength - BinaryTypeCode.FixStrCount));
                headerSize = 1 + varUIntSize + slotSize;

                EnsureCapacity(headerSize + maxBytes);

                headerPos = _position;
                slotPos = headerPos + 1 + varUIntSize;
                encodeStart = headerPos + headerSize;

                BufferAt(headerPos) = BinaryTypeCode.String;

                _position = headerPos + 1;
                WriteVarUIntUnsafe((uint)(charLength - BinaryTypeCode.FixStrCount));
                // _position now == slotPos. Slot write below uses Unsafe.WriteUnaligned at slotPos;
                // _position is finalized at the end via `_position = encodeStart + bytesWritten`.
            }

            var status = System.Text.Unicode.Utf8.FromUtf16(value.AsSpan(), _buffer.AsSpan(encodeStart, maxBytes), out _, out var bytesWritten, replaceInvalidSequences: false);
            var excess = bytesWritten - charLength;

            if (status != OperationStatus.Done) ThrowStringEncodingFailed(status);

#if DEBUG
            // With status==Done, UTF-8 path mathematically implies bytesWritten >= charLength.
            System.Diagnostics.Debug.Assert(excess >= 0, "WriteStringWithDispatch invariant broken: UTF-8 path produced negative excess.");
#endif

            // UTF16 branch remains on the existing FastWire path for now.
            // Current universal slot is unsigned (ASCII=0, UTF8>0). If UTF16-via-slot is introduced later,
            // the discriminator design must be revisited (separate flag/marker or signed slot variant).

            if (slotSize == 1) Unsafe.WriteUnaligned(ref BufferAt(slotPos), unchecked((byte)excess));
            else if (slotSize == 2) Unsafe.WriteUnaligned(ref BufferAt(slotPos), unchecked((ushort)excess));
            else Unsafe.WriteUnaligned(ref BufferAt(slotPos), excess);

            _position = encodeStart + bytesWritten;
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private static void ThrowStringEncodingFailed(OperationStatus status) =>
            throw new InvalidOperationException(
                $"String UTF-8 encode failed in WriteStringWithDispatch: status={status}. " +
                "This indicates an unexpected encoder failure (e.g. destination sizing or invalid input state)." );

        /// <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]
            BufferAt(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(ref BufferAt(_position), packed);
                _position += 2;
            }
            else
            {
                // Pack charLen:16 | utf8Len:16 → single uint store, LE
                var packed = (uint)charLength | ((uint)bytesWritten << 16);
                Unsafe.WriteUnaligned(ref BufferAt(_position), packed);
                _position += 4;
            }

            _position += bytesWritten;
        }

        /// <summary>
        /// Attempts to write <paramref name="value"/> through the string-interning protocol.
        /// Reads and immediately resets <see cref="StringInternEligible"/>; when the property is
        /// intern-eligible and the write-plan cursor yields an entry, emits the InternFirst /
        /// InternRef wire form and returns <c>true</c>. Returns <c>false</c> when the string must
        /// be written by the regular tier dispatch — the caller then invokes
        /// <see cref="WriteStringWithDispatch"/>.
        /// </summary>
        /// <remarks>
        /// Extracted from the runtime <c>WriteString</c> interning block (K9M3-style hoist) so the
        /// SGen string-property emit can call it directly — no <c>WriteStringGenerated</c> /
        /// <c>WriteString</c> hop. Caller guarantees non-null, non-empty content. The unconditional
        /// flag reset prevents the per-property <see cref="StringInternEligible"/> from leaking into
        /// subsequent string writes.
        /// </remarks>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal bool TryWriteInternedString(string value)
        {
            // Read and immediately reset — prevents the flag leaking to subsequent string writes
            // (TryWritePrimitive, WriteDictionary, or when IsValidForInterningString is false).
            var internEligible = StringInternEligible;
            StringInternEligible = false;

            if (internEligible && IsValidForInterningString(value.Length))
            {
                if (TryConsumeWritePlanEntry(out var planEntry))
                {
                    ValidateWritePlanString(in planEntry, value);
                    if (planEntry.IsFirst)
                    {
                        // H2Q6 v3 — StringInternFirst tier-marker dispatch (Small/Medium); charLen
                        // carried in header → 1-pass decode, no CountUtf8Chars Pass 1.
                        WriteStringInternFirstWithDispatch(planEntry.Value ?? value, planEntry.CacheMapIndex);
                    }
                    else
                    {
                        WriteStringInternRef(this, planEntry.CacheMapIndex);
                    }
                    return true;
                }
                // No plan entry → single occurrence, caller falls through to the tier dispatch.
#if DEBUG
                OnStringInterned?.Invoke(null, value);
#endif
            }
            return false;
        }

        // ─────────────────────────────────────────────────────────────────
        // 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++)
            {
                BufferAt(_position++) = BinaryTypeCode.Float64;
                Unsafe.WriteUnaligned(ref BufferAt(_position), span[i]);
                _position += 8;
            }
        }

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

        public void WriteGuidArrayBulk(ReadOnlySpan<Guid> span)
        {
            EnsureCapacity(span.Length * 17);
            for (var i = 0; i < span.Length; i++)
            {
                BufferAt(_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 |= 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
    }
}
