using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace AyCode.Core.Serializers.SourceGenerator;

/// <summary>
/// Writer-side emit pass: generates the <c>IGeneratedBinaryWriter</c> implementation for each
/// <c>[AcBinarySerializable]</c> type. Emits <c>WriteProperties</c> (write pass), <c>ScanObject</c>
/// (scan pass for ref-handling + interning), and <c>ScanForDuplicates</c> (write-plan builder).
///
/// <para>Sub-passes:</para>
/// <list type="bullet">
///   <item><c>EmitProp</c> — per-property write emit (markered + markerless variants).</item>
///   <item><c>EmitScan*</c> — per-property scan emit (intern + ref tracking).</item>
///   <item><c>EmitDirect*Write</c> — inline collection / dictionary write loops.</item>
///   <item><c>EmitSkip</c> / <c>EmitVal</c> / <c>EmitMarkerless</c> — primitive write fragments.</item>
/// </list>
/// </summary>
public partial class AcBinarySourceGenerator
{
    private static string GenWriter(SerializableClassInfo ci)
    {
        var sb = new StringBuilder(4096);
        sb.AppendLine("// <auto-generated/>");
        sb.AppendLine("#nullable enable");
        sb.AppendLine("using System.Runtime.CompilerServices;");
        sb.AppendLine("using System.Runtime.InteropServices;");
        sb.AppendLine("using AyCode.Core.Serializers.Binaries;");
        // IGeneratedBinaryWriter and other serializer types
        sb.AppendLine("using AyCode.Core.Serializers;");
        sb.AppendLine();
        if (!string.IsNullOrEmpty(ci.Namespace))
            sb.AppendLine($"namespace {ci.Namespace};");
        sb.AppendLine();
        sb.AppendLine($"internal sealed class {ci.ClassName}_GeneratedWriter : IGeneratedBinaryWriter");
        sb.AppendLine("{");
        sb.AppendLine($"    internal static readonly {ci.ClassName}_GeneratedWriter Instance = new();");
        sb.AppendLine($"    internal static readonly int s_wrapperSlot = AcBinarySerializer.AllocateWrapperSlot();");
        sb.AppendLine($"    internal static readonly int s_typeNameHash = {ci.TypeNameHash};");
        sb.Append(    $"    internal static readonly int[] s_propertyHashes = new int[] {{ ");
        sb.Append(string.Join(", ", ci.PropertyNameHashes));
        sb.AppendLine(" };");
        sb.AppendLine();
        sb.AppendLine("    public void WriteProperties<TOutput>(object value, AcBinarySerializer.BinarySerializationContext<TOutput> context) where TOutput : struct, IBinaryOutputBase");
        sb.AppendLine("    {");
        sb.AppendLine("        // Depth check + EnterRecursion happens at the CALLER (before marker write).");
        sb.AppendLine("        // Body just runs property writes; ExitRecursion at the end balances the caller's EnterRecursion.");
        sb.AppendLine($"        var obj = Unsafe.As<{ci.FullTypeName}>(value);");

        foreach (var p in ci.Properties)
        {
            sb.AppendLine();
            EmitProp(sb, p, "        ", ci.FullTypeName, ci.EnableMetadata, ci.EnablePropertyFilter, ci.EnablePolymorphDetect);
        }

        sb.AppendLine();
        sb.AppendLine("        context.ExitRecursion();");
        sb.AppendLine("    }");
        sb.AppendLine();

        // ScanObject — full scan pass (null/depth + self ref tracking + property scan)
        GenScanProperties(sb, ci);

        sb.AppendLine();

        // ScanForDuplicates — instance method on IGeneratedBinaryWriter, called from Serialize
        sb.AppendLine("    public void ScanForDuplicates<TOutput>(object value, AcBinarySerializer.BinarySerializationContext<TOutput> context)");
        sb.AppendLine("        where TOutput : struct, IBinaryOutputBase");
        sb.AppendLine("    {");
        sb.AppendLine("        if (!context.HasCaching) return;");
        sb.AppendLine("        ScanObject(value, context);");
        sb.AppendLine("        context.SortWritePlan();");
        sb.AppendLine("    }");

        sb.AppendLine("}");
        return sb.ToString();
    }

    /// <summary>
    /// Generates the ScanObject method — full scan pass entry point for this type.
    /// Includes: null/depth check, self ref tracking (IId or All mode), property scan.
    /// Only emits code for reference properties (strings + complex types) — primitives are skipped.
    /// </summary>
    private static void GenScanProperties(StringBuilder sb, SerializableClassInfo ci)
    {
        sb.AppendLine("    public void ScanObject<TOutput>(object value, AcBinarySerializer.BinarySerializationContext<TOutput> context) where TOutput : struct, IBinaryOutputBase");
        sb.AppendLine("    {");

        // Compile-time proven: no scan work needed for this type
        if (!ci.NeedsScan)
        {
            sb.AppendLine("        // NeedsScan=false: no ref tracking, no string interning, no scannable children");
            sb.AppendLine("    }");
            return;
        }

        // Early return: skip scan when no active runtime feature matches this type's needs
        if (!ci.NeedsIdScan)
        {
            if (ci.NeedsAllRefScan && ci.NeedsInternScan)
                sb.AppendLine("        if (!context.HasAllRefHandling && !context.HasStringInterning) return;");
            else if (ci.NeedsAllRefScan)
                sb.AppendLine("        if (!context.HasAllRefHandling) return;");
            else if (ci.NeedsInternScan)
                sb.AppendLine("        if (!context.HasStringInterning) return;");
        }

        // Null guard — MaxDepth option removed (was: cycle protection via runtime depth check).
        // Cycle safety now comes from IId-tracking; future [AcBinaryCircular] attr will mark non-IId circular refs.
        sb.AppendLine("        if (value == null) return;");
        sb.AppendLine();
        sb.AppendLine($"        var obj = Unsafe.As<{ci.FullTypeName}>(value);");

        // Self ref tracking — inline TryTrack via wrapper (no bridge method overhead)
        // Only emitted when the corresponding feature flag is enabled.
        if (ci.IsIId)
        {
            var tryTrackMethod = ci.IdTypeName switch
            {
                "int" => "TryTrackInt32",
                "long" => "TryTrackInt64",
                "System.Guid" => "TryTrackGuid",
                _ => "TryTrackInt32"
            };
            sb.AppendLine();
            sb.AppendLine("        if (context.HasRefHandling)");
            sb.AppendLine("        {");
            sb.AppendLine($"            var wrapper = context.GetWrapper(typeof({ci.FullTypeName}), s_wrapperSlot);");
            sb.AppendLine("            var visitIndex = context.ScanVisitIndex++;");
            sb.AppendLine($"            if (!wrapper.{tryTrackMethod}(obj.Id, visitIndex, ref context.NextCacheIndexRef, out var cacheIndex, out var firstVisitIndex))");
            sb.AppendLine("            {");
            sb.AppendLine("                if (firstVisitIndex >= 0)");
            sb.AppendLine("                    context.AddWriteDuplicateEntry(firstVisitIndex, cacheIndex, isFirst: true, value: null);");
            sb.AppendLine("                context.AddWriteDuplicateEntry(visitIndex, cacheIndex, isFirst: false, value: null);");
            sb.AppendLine("                return;");
            sb.AppendLine("            }");
            sb.AppendLine("        }");
        }
        if (ci.EnableRefHandling && !ci.IsIId)
        {
            // Non-IId type: track via wrapper.TryTrackInt32 with RuntimeHelpers.GetHashCode
            sb.AppendLine();
            sb.AppendLine("        if (context.HasAllRefHandling)");
            sb.AppendLine("        {");
            sb.AppendLine($"            var wrapper = context.GetWrapper(typeof({ci.FullTypeName}), s_wrapperSlot);");
            sb.AppendLine("            var visitIndex = context.ScanVisitIndex++;");
            sb.AppendLine("            if (!wrapper.TryTrackInt32(System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(value), visitIndex, ref context.NextCacheIndexRef, out var cacheIndex, out var firstVisitIndex))");
            sb.AppendLine("            {");
            sb.AppendLine("                if (firstVisitIndex >= 0)");
            sb.AppendLine("                    context.AddWriteDuplicateEntry(firstVisitIndex, cacheIndex, isFirst: true, value: null);");
            sb.AppendLine("                context.AddWriteDuplicateEntry(visitIndex, cacheIndex, isFirst: false, value: null);");
            sb.AppendLine("                return;");
            sb.AppendLine("            }");
            sb.AppendLine("        }");
        }

        // Collect scannable properties
        var scanProps = ci.Properties.Where(p =>
            p.TypeKind == PropertyTypeKind.String ||
            p.TypeKind == PropertyTypeKind.Complex ||
            p.TypeKind == PropertyTypeKind.Collection ||
            p.TypeKind == PropertyTypeKind.Dictionary).ToList();

        // Hoist UseStringInterning + IsValidForInterningString checks if any string scanning needed
        var hasStringScan = scanProps.Any(p =>
            (p.TypeKind == PropertyTypeKind.String && p.InterningFlags != 0) ||
            (p.TypeKind == PropertyTypeKind.Collection && p.ElementKind == PropertyTypeKind.String && p.InterningFlags != 0) ||
            (p.TypeKind == PropertyTypeKind.Dictionary && (p.DictKeyKind == PropertyTypeKind.String || p.DictValueKind == PropertyTypeKind.String) && p.InterningFlags != 0));

        // Combined check+inc — gated inside TryEnterRecursion (checks NeedsDepthCheck).
        // Emitted AFTER all early returns (NeedsScan=false, feature-flag, null guard, IId 2nd-occurrence)
        // and BEFORE the property scan loop that recurses into children.
        // On limit hit: helper method (cold path, NoInlining) dispatches Throw or Truncate (return).
        sb.AppendLine();
        sb.AppendLine("        if (context.TryEnterRecursion(hasTruncatePath: false)) return; // scan: skip children");

        if (hasStringScan)
        {
            // Use pre-computed InternBit from context (avoids Options.UseStringInterning field chain + shift per object).
            // Per-property InterningFlags check uses internBit directly.
            // Cannot combine flags (OR) because different properties may have different flags
            // and Attribute mode must NOT scan All-only properties.
            sb.AppendLine();
            sb.AppendLine("        var internBit = context.InternBit;");
            sb.AppendLine("        int minIntern = 0, maxIntern = 0;");
            sb.AppendLine("        if (internBit > 1) { minIntern = context.MinStringInternLength; maxIntern = context.MaxStringInternLength; }");
        }

        var hasAnyScanProp = false;
        foreach (var p in scanProps)
        {
            sb.AppendLine();
            hasAnyScanProp = true;
            EmitScanProp(sb, p, "        ", ci.FullTypeName, ci.EnablePropertyFilter);
        }

        if (!hasAnyScanProp)
        {
            sb.AppendLine("        // No reference properties to scan");
        }

        sb.AppendLine();
        sb.AppendLine("        context.ExitRecursion();");
        sb.AppendLine("    }");
    }

    private static void EmitProp(StringBuilder sb, PropInfo p, string i, string fullTypeName, bool enableMetadata, bool enablePropertyFilter, bool enablePolymorphDetect)
    {
        var a = $"obj.{p.Name}";

        // Markerless types: write raw value only, no type marker, no PropertySkip
        // Matches runtime WritePropertyMarkerless — these have ExpectedTypeCode
        // NEVER filtered (runtime doesn't filter markerless properties either)
        // When EnableMetadataFeature=false: always markerless (no UseMetadata branch needed)
        // When EnableMetadataFeature=true: UseMetadata=true uses markered path (EmitSkip)
        if (IsMarkerless(p.TypeKind))
        {
            if (!enableMetadata)
                EmitMarkerless(sb, p.TypeKind, a, i);
            else
                EmitPropertyBridge(sb, p.TypeKind, a, i);
            return;
        }

        // All non-markerless properties: emit PropertyFilter guard
        // When filter returns false, write PropertySkip and skip the property write.
        // Gated by `[AcBinarySerializable(EnablePropertyFilterFeature = ...)]` — when false, the entire
        // filter-check block is omitted from emit (no `HasPropertyFilter` test, no filter-context allocation,
        // no lambda-call) → leaner generated code on hot-path types that never use a property-filter.
        if (enablePropertyFilter)
        {
            sb.AppendLine($"{i}if (context.HasPropertyFilter)");
            sb.AppendLine($"{i}{{");
            sb.AppendLine($"{i}    var fc_{p.Name} = new BinaryPropertyFilterContext(obj, typeof({fullTypeName}), \"{p.Name}\", typeof({p.TypeNameForTypeof}), static o => (({fullTypeName})o).{p.Name});");
            sb.AppendLine($"{i}    if (!context.PropertyFilter!(in fc_{p.Name}))");
            sb.AppendLine($"{i}    {{");
            sb.AppendLine($"{i}        context.WriteByte(BinaryTypeCode.PropertySkip);");
            sb.AppendLine($"{i}        goto skip_{p.Name};");
            sb.AppendLine($"{i}    }}");
            sb.AppendLine($"{i}}}");
        }

        // Nullable value types always use markered path (need Null marker)
        if (IsNullableVTKind(p.TypeKind))
        {
            sb.AppendLine($"{i}if ({a}.HasValue)");
            sb.AppendLine($"{i}{{");
            EmitVal(sb, Underlying(p.TypeKind), $"{a}.Value", p.TypeNameForTypeof, i + "    ");
            sb.AppendLine($"{i}}}");
            sb.AppendLine($"{i}else context.WriteByte(BinaryTypeCode.PropertySkip);");
            sb.AppendLine($"{i}skip_{p.Name}:;");
            return;
        }

        // Non-markerless types: write WITH type marker byte (markered path)
        switch (p.TypeKind)
        {
            case PropertyTypeKind.String:
                // Inlined string-property write -- streamlined chain (bypasses WriteStringGenerated /
                // WriteString): FastWire -> markerless UTF-16; else null -> PropertySkip, empty ->
                // StringEmpty, content -> interning attempt (eligible props) + WriteStringWithDispatch.
                // A local pins the single getter evaluation; the chain is small + branch-friendly so
                // the JIT folds it into WriteProperties (no per-string call frame).
                sb.AppendLine($"{i}var str_{p.Name} = {a};");
                sb.AppendLine($"{i}if (context.FastWire) context.WriteStringUtf16Markerless(str_{p.Name});");
                sb.AppendLine($"{i}else if (str_{p.Name} == null) context.WriteByte(BinaryTypeCode.PropertySkip);");
                sb.AppendLine($"{i}else if (str_{p.Name}.Length == 0) context.WriteByte(BinaryTypeCode.StringEmpty);");
                if (p.InterningFlags == 0)
                {
                    sb.AppendLine($"{i}else context.WriteStringWithDispatch(str_{p.Name});");
                }
                else
                {
                    sb.AppendLine($"{i}else");
                    sb.AppendLine($"{i}{{");
                    sb.AppendLine($"{i}    context.StringInternEligible = ({p.InterningFlags} & context.InternBit) != 0;");
                    sb.AppendLine($"{i}    if (!context.TryWriteInternedString(str_{p.Name})) context.WriteStringWithDispatch(str_{p.Name});");
                    sb.AppendLine($"{i}}}");
                }
                break;
            case PropertyTypeKind.Complex:
                // Complex object: direct write bypasses GetWrapper + WriteObject pipeline entirely
                // when the property type has a generated writer. Falls back to WriteObjectGenerated otherwise.
                if (p.HasGeneratedWriter)
                    EmitDirectObjectWrite(sb, p, a, i);
                else if (p.IsObjectDeclaredType)
                {
                    // System.Object property: runtime type unknown at compile time.
                    // Write ObjectWithTypeName prefix so deserializer can resolve the concrete type.
                    // Use value.GetType() for runtime type dispatch (not typeof(object)).
                    // Gated by `[AcBinarySerializable(enablePolymorphDetectFeature: ...)]` — `false`
                    // skips the type-name emit entirely. Misuse (false + non-null object prop at runtime)
                    // is caught at build time by ACBIN002 in DetectAndReportPolymorphicMisuse.
                    sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);");
                    sb.AppendLine($"{i}else");
                    sb.AppendLine($"{i}{{");
                    if (enablePolymorphDetect)
                    {
                        sb.AppendLine($"{i}    if (!context.UseMetadata)");
                        sb.AppendLine($"{i}    {{");
                        sb.AppendLine($"{i}        context.WriteByte(BinaryTypeCode.ObjectWithTypeName);");
                        sb.AppendLine($"{i}        context.WriteStringUtf8({a}.GetType().AssemblyQualifiedName!);");
                        sb.AppendLine($"{i}    }}");
                    }
                    sb.AppendLine($"{i}    AcBinarySerializer.WriteValueGenerated({a}, {a}.GetType(), context);");
                    sb.AppendLine($"{i}}}");
                }
                else
                {
                    // Reference type properties can always be null at runtime regardless of nullable
                    // annotation — runtime can violate the nullable-disabled contract via EF lazy-load,
                    // projection gaps, detached navigation properties, etc. Mirrors the EmitDirectObjectWrite
                    // (line ~828) and EmitDirectCollectionWrite (line ~877) defensive null-check.
                    // Reader-side compat: every markered property is wrapped in `if (tc != PropertySkip)`
                    // by EmitReadProperty (GenReader.cs line ~137), so the marker is uniformly handled.
                    sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);");
                    sb.AppendLine($"{i}else AcBinarySerializer.WriteObjectGenerated({a}, typeof({p.TypeNameForTypeof}), context);");
                }
                break;
            case PropertyTypeKind.Collection:
                // Direct collection write for List<T>/T[] with Complex element types that have generated writers
                if (p.ElementHasGeneratedWriter && p.CollectionKind != null)
                    EmitDirectCollectionWrite(sb, p, a, i);
                else
                {
                    // Reference type collections (with non-SGen elements, falling onto the
                    // WriteValueGenerated bridge) can always be null at runtime regardless of nullable
                    // annotation — runtime can violate the nullable-disabled contract via EF lazy-load,
                    // projection gaps, missing initializers, etc. Mirrors EmitDirectCollectionWrite
                    // (line ~877) and EmitDirectObjectWrite (line ~828) defensive null-check.
                    // Reader-side compat: every markered property is wrapped in `if (tc != PropertySkip)`
                    // by EmitReadProperty (GenReader.cs line ~137).
                    sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);");
                    sb.AppendLine($"{i}else AcBinarySerializer.WriteValueGenerated({a}, typeof({p.TypeNameForTypeof}), context);");
                }
                break;
            case PropertyTypeKind.Dictionary:
                EmitDirectDictionaryWrite(sb, p, a, i);
                break;
            default:
                EmitSkip(sb, p.TypeKind, a, p.TypeNameForTypeof, i);
                break;
        }

        sb.AppendLine($"{i}skip_{p.Name}:;");
    }

    // IsMarkerless moved to AcBinarySourceGenerator.TypeAnalysis.cs (shared writer/reader utility).

    /// <summary>
    /// Emits raw value only — no type marker, no PropertySkip.
    /// Matches runtime WritePropertyMarkerless exactly.
    /// </summary>
    private static void EmitMarkerless(StringBuilder sb, PropertyTypeKind k, string a, string i)
    {
        switch (k)
        {
            case PropertyTypeKind.Int32:    sb.AppendLine($"{i}context.WriteVarInt({a});"); break;
            case PropertyTypeKind.Int64:    sb.AppendLine($"{i}context.WriteVarLong({a});"); break;
            case PropertyTypeKind.Double:   sb.AppendLine($"{i}context.WriteRaw({a});"); break;
            case PropertyTypeKind.Single:   sb.AppendLine($"{i}context.WriteRaw({a});"); break;
            case PropertyTypeKind.Decimal:  sb.AppendLine($"{i}context.WriteDecimalBits({a});"); break;
            case PropertyTypeKind.DateTime: sb.AppendLine($"{i}context.WriteDateTimeBits({a});"); break;
            case PropertyTypeKind.Guid:     sb.AppendLine($"{i}context.WriteGuidBits({a});"); break;
            case PropertyTypeKind.Byte:     sb.AppendLine($"{i}context.WriteByte({a});"); break;
            case PropertyTypeKind.Int16:    sb.AppendLine($"{i}context.WriteRaw({a});"); break;
            case PropertyTypeKind.UInt16:   sb.AppendLine($"{i}context.WriteRaw({a});"); break;
            case PropertyTypeKind.UInt32:   sb.AppendLine($"{i}context.WriteVarUInt({a});"); break;
            case PropertyTypeKind.UInt64:   sb.AppendLine($"{i}context.WriteVarULong({a});"); break;
            case PropertyTypeKind.TimeSpan: sb.AppendLine($"{i}context.WriteRaw({a}.Ticks);"); break;
            case PropertyTypeKind.DateTimeOffset: sb.AppendLine($"{i}context.WriteDateTimeOffsetBits({a});"); break;
            case PropertyTypeKind.Boolean: sb.AppendLine($"{i}context.WriteByte({a} ? (byte)1 : (byte)0);"); break;
            case PropertyTypeKind.Enum:    sb.AppendLine($"{i}context.WriteVarInt((int){a});"); break;
        }
    }

    /// <summary>
    /// Emits a single bridge method call for markerless property types with enableMetadata=true.
    /// The bridge method on BinarySerializationContext handles both UseMetadata=true (markered+skip)
    /// and UseMetadata=false (markerless) paths internally. Replaces 7-11 lines of generated code with 1 line.
    /// </summary>
    private static void EmitPropertyBridge(StringBuilder sb, PropertyTypeKind k, string a, string i)
    {
        var call = k switch
        {
            PropertyTypeKind.Int32 => $"context.WriteInt32Property({a});",
            PropertyTypeKind.Int64 => $"context.WriteInt64Property({a});",
            PropertyTypeKind.Boolean => $"context.WriteBoolProperty({a});",
            PropertyTypeKind.Double => $"context.WriteFloat64Property({a});",
            PropertyTypeKind.Single => $"context.WriteFloat32Property({a});",
            PropertyTypeKind.Decimal => $"context.WriteDecimalProperty({a});",
            PropertyTypeKind.DateTime => $"context.WriteDateTimeProperty({a});",
            PropertyTypeKind.Guid => $"context.WriteGuidProperty({a});",
            PropertyTypeKind.Byte => $"context.WriteByteProperty({a});",
            PropertyTypeKind.Int16 => $"context.WriteInt16Property({a});",
            PropertyTypeKind.UInt16 => $"context.WriteUInt16Property({a});",
            PropertyTypeKind.UInt32 => $"context.WriteUInt32Property({a});",
            PropertyTypeKind.UInt64 => $"context.WriteUInt64Property({a});",
            PropertyTypeKind.Enum => $"context.WriteEnumInt32Property((int){a});",
            PropertyTypeKind.TimeSpan => $"context.WriteTimeSpanProperty({a});",
            PropertyTypeKind.DateTimeOffset => $"context.WriteDateTimeOffsetProperty({a});",
            _ => null
        };

        if (call != null)
            sb.AppendLine($"{i}{call}");
    }

    /// <summary>
    /// Emits direct object write — bypasses GetWrapper + WriteObject entirely.
    #region Scan Pass Code Generation

    /// <summary>
    /// Compile-time check: will EmitScanProp produce any scan work for this property?
    /// When false, the entire block (including PropertyFilter guard) is skipped.
    /// </summary>
    private static bool HasScanWork(PropInfo p) => p.TypeKind switch
    {
        PropertyTypeKind.String => p.InterningFlags != 0,
        PropertyTypeKind.Complex when p.HasGeneratedWriter => p.ChildNeedsScan,
        PropertyTypeKind.Complex => true,
        PropertyTypeKind.Collection => HasCollectionScanWork(p),
        PropertyTypeKind.Dictionary => HasDictionaryScanWork(p),
        _ => false
    };

    private static bool HasCollectionScanWork(PropInfo p) => p.ElementKind switch
    {
        PropertyTypeKind.String => p.InterningFlags != 0,
        PropertyTypeKind.Complex when p.ElementHasGeneratedWriter && p.CollectionKind != null => p.ElementNeedsScan,
        PropertyTypeKind.Complex => true,
        _ => false
    };

    private static bool HasDictionaryScanWork(PropInfo p)
    {
        if (p.DictKeyKind == PropertyTypeKind.String && p.InterningFlags != 0) return true;
        if (p.DictValueKind == PropertyTypeKind.String && p.InterningFlags != 0) return true;
        if (p.DictValueKind == PropertyTypeKind.Complex && p.DictValueHasGeneratedWriter) return p.DictValueNeedsScan;
        if (p.DictValueKind == PropertyTypeKind.Complex) return true;
        return false;
    }

    /// <summary>
    /// Emits scan pass code for a single property.
    /// String: interning check + ScanInternString.
    /// Complex (SGen): ref tracking via slot IdentityMap + recursive ScanProperties.
    /// Complex (no SGen): fallback to ScanValueGenerated (runtime wrapper lookup).
    /// Collection: iterate elements with same patterns.
    /// </summary>
    private static void EmitScanProp(StringBuilder sb, PropInfo p, string i, string fullTypeName, bool enablePropertyFilter)
    {
        // Compile-time proven: no scan work for this property — skip entirely (including PropertyFilter guard)
        if (!HasScanWork(p)) return;

        var a = $"obj.{p.Name}";

        // PropertyFilter: must match write pass — if filter skips property, scan must skip too
        // Only for non-markerless properties (matching EmitProp behavior).
        // Gated by `[AcBinarySerializable(EnablePropertyFilterFeature = ...)]` — same gate as the writer pass.
        if (enablePropertyFilter && !IsMarkerless(p.TypeKind))
        {
            sb.AppendLine($"{i}if (context.HasPropertyFilter)");
            sb.AppendLine($"{i}{{");
            sb.AppendLine($"{i}    var fc_{p.Name} = new BinaryPropertyFilterContext(obj, typeof({fullTypeName}), \"{p.Name}\", typeof({p.TypeNameForTypeof}), static o => (({fullTypeName})o).{p.Name});");
            sb.AppendLine($"{i}    if (!context.PropertyFilter!(in fc_{p.Name}))");
            sb.AppendLine($"{i}        goto scanskip_{p.Name};");
            sb.AppendLine($"{i}}}");
        }

        switch (p.TypeKind)
        {
            case PropertyTypeKind.String:
                EmitScanString(sb, p, a, i);
                break;

            case PropertyTypeKind.Complex:
                if (p.HasGeneratedWriter)
                    EmitScanComplexSGen(sb, p, a, i);
                else
                    EmitScanComplexRuntime(sb, p, a, i);
                break;

            case PropertyTypeKind.Collection:
                EmitScanCollection(sb, p, a, i);
                break;

            case PropertyTypeKind.Dictionary:
                EmitScanDictionary(sb, p, a, i);
                break;
        }

        if (!IsMarkerless(p.TypeKind))
            sb.AppendLine($"{i}scanskip_{p.Name}:;");
    }

    /// <summary>
    /// Emits scan pass code for a string property: interning flags check + ScanInternString.
    /// </summary>
    private static void EmitScanString(StringBuilder sb, PropInfo p, string a, string i)
    {
        if (p.InterningFlags == 0)
        {
            // Never interned (explicit [AcStringIntern(false)] or no flags) — skip entirely
            return;
        }

        // Per-property InterningFlags check with hoisted internBit (context.Options read once)
        sb.AppendLine($"{i}if (({p.InterningFlags} & internBit) != 0)");
        sb.AppendLine($"{i}{{");
        sb.AppendLine($"{i}    var str_{p.Name} = {a};");
        sb.AppendLine($"{i}    if (str_{p.Name} != null)");
        sb.AppendLine($"{i}    {{");
        sb.AppendLine($"{i}        var slen_{p.Name} = str_{p.Name}.Length;");
        sb.AppendLine($"{i}        if (slen_{p.Name} >= minIntern && (maxIntern == 0 || slen_{p.Name} <= maxIntern))");
        sb.AppendLine($"{i}            context.ScanInternString(str_{p.Name});");
        sb.AppendLine($"{i}    }}");
        sb.AppendLine($"{i}}}");
    }

    /// <summary>
    /// Emits scan pass code for a Complex property with SGen writer.
    /// No parent-side ref tracking — child ScanObject handles its own (ScanTrackObjectXxx).
    /// </summary>
    private static void EmitScanComplexSGen(StringBuilder sb, PropInfo p, string a, string i)
    {
        // Compile-time proven: child scan is no-op — skip entirely
        if (!p.ChildNeedsScan) return;

        var writer = p.WriterClassName;
        var childVar = $"sc_{p.Name}";

        // 3-axis guard: IId → always call, AllRef → guard All mode, Intern → guard UseStringInterning
        string? guard = null;
        if (!p.ChildNeedsIdScan)
        {
            if (p.ChildNeedsAllRefScan && p.ChildNeedsInternScan)
                guard = "context.HasAllRefHandling || context.HasStringInterning";
            else if (p.ChildNeedsAllRefScan)
                guard = "context.HasAllRefHandling";
            else if (p.ChildNeedsInternScan)
                guard = "context.HasStringInterning";
        }

        if (guard != null)
        {
            sb.AppendLine($"{i}if ({guard})");
            sb.AppendLine($"{i}{{");
            sb.AppendLine($"{i}    var {childVar} = {a};");
            sb.AppendLine($"{i}    if ({childVar} != null)");
            sb.AppendLine($"{i}        {writer}.Instance.ScanObject({childVar}, context);");
            sb.AppendLine($"{i}}}");
        }
        else
        {
            // IId in subtree — always call (active in OnlyId + All)
            sb.AppendLine($"{i}var {childVar} = {a};");
            sb.AppendLine($"{i}if ({childVar} != null)");
            sb.AppendLine($"{i}    {writer}.Instance.ScanObject({childVar}, context);");
        }
    }

    /// <summary>
    /// Emits scan pass code for a Complex property without SGen writer (runtime fallback).
    /// System.Object properties use value.GetType() for runtime type dispatch.
    /// </summary>
    private static void EmitScanComplexRuntime(StringBuilder sb, PropInfo p, string a, string i)
    {
        var childVar = $"sc_{p.Name}";
        sb.AppendLine($"{i}var {childVar} = {a};");
        sb.AppendLine($"{i}if ({childVar} != null)");
        if (p.IsObjectDeclaredType)
            sb.AppendLine($"{i}    AcBinarySerializer.ScanValueGenerated({childVar}, {childVar}.GetType(), context);");
        else
            sb.AppendLine($"{i}    AcBinarySerializer.ScanValueGenerated({childVar}, typeof({p.TypeNameForTypeof}), context);");
    }

    /// <summary>
    /// Emits scan pass code for a Collection property.
    /// Handles string collections (interning) and complex element collections (SGen or runtime fallback).
    /// </summary>
    private static void EmitScanCollection(StringBuilder sb, PropInfo p, string a, string i)
    {
        // String element collection
        if (p.ElementKind == PropertyTypeKind.String)
        {
            if (p.InterningFlags == 0) return; // never interned

            sb.AppendLine($"{i}var scol_{p.Name} = {a};");
            sb.AppendLine($"{i}if (scol_{p.Name} != null && ({p.InterningFlags} & internBit) != 0)");
            sb.AppendLine($"{i}{{");

            if (p.CollectionKind == "Array")
            {
                sb.AppendLine($"{i}    for (var si_{p.Name} = 0; si_{p.Name} < scol_{p.Name}.Length; si_{p.Name}++)");
                sb.AppendLine($"{i}    {{");
                sb.AppendLine($"{i}        var se_{p.Name} = scol_{p.Name}[si_{p.Name}];");
            }
            else if (p.CollectionKind == "List")
            {
                sb.AppendLine($"{i}    var span_{p.Name} = CollectionsMarshal.AsSpan(scol_{p.Name});");
                sb.AppendLine($"{i}    for (var si_{p.Name} = 0; si_{p.Name} < span_{p.Name}.Length; si_{p.Name}++)");
                sb.AppendLine($"{i}    {{");
                sb.AppendLine($"{i}        var se_{p.Name} = span_{p.Name}[si_{p.Name}];");
            }
            else if (p.CollectionKind == "IndexedCollection")
            {
                sb.AppendLine($"{i}    for (var si_{p.Name} = 0; si_{p.Name} < scol_{p.Name}.Count; si_{p.Name}++)");
                sb.AppendLine($"{i}    {{");
                sb.AppendLine($"{i}        var se_{p.Name} = scol_{p.Name}[si_{p.Name}];");
            }
            else
            {
                sb.AppendLine($"{i}    foreach (var se_{p.Name} in scol_{p.Name})");
                sb.AppendLine($"{i}    {{");
            }

            sb.AppendLine($"{i}        if (se_{p.Name} != null)");
            sb.AppendLine($"{i}        {{");
            sb.AppendLine($"{i}            var slen_{p.Name} = se_{p.Name}.Length;");
            sb.AppendLine($"{i}            if (slen_{p.Name} >= minIntern && (maxIntern == 0 || slen_{p.Name} <= maxIntern))");
            sb.AppendLine($"{i}                context.ScanInternString(se_{p.Name});");
            sb.AppendLine($"{i}        }}");
            sb.AppendLine($"{i}    }}");
            sb.AppendLine($"{i}}}");
            return;
        }

        // Complex element collection with SGen writer
        if (p.ElementKind == PropertyTypeKind.Complex && p.ElementHasGeneratedWriter && p.CollectionKind != null)
        {
            // Compile-time proven: element scan is no-op — skip entirely
            if (!p.ElementNeedsScan) return;

            var writer = p.ElementWriterClassName;

            // 3-axis guard: IId → always scan, AllRef → guard All mode, Intern → guard UseStringInterning
            string? elemGuard = null;
            if (!p.ElementNeedsIdScan)
            {
                if (p.ElementNeedsAllRefScan && p.ElementNeedsInternScan)
                    elemGuard = "context.HasAllRefHandling || context.HasStringInterning";
                else if (p.ElementNeedsAllRefScan)
                    elemGuard = "context.HasAllRefHandling";
                else if (p.ElementNeedsInternScan)
                    elemGuard = "context.HasStringInterning";
            }

            // Guard entire collection scan with runtime check when no IId in element subtree
            if (elemGuard != null)
                sb.AppendLine($"{i}if ({elemGuard})");

            sb.AppendLine($"{i}{{");
            sb.AppendLine($"{i}    var scol_{p.Name} = {a};");
            sb.AppendLine($"{i}    if (scol_{p.Name} != null)");
            sb.AppendLine($"{i}    {{");

            if (p.CollectionKind == "Array")
            {
                sb.AppendLine($"{i}    for (var si_{p.Name} = 0; si_{p.Name} < scol_{p.Name}.Length; si_{p.Name}++)");
                sb.AppendLine($"{i}    {{");
                sb.AppendLine($"{i}        var se_{p.Name} = scol_{p.Name}[si_{p.Name}];");
            }
            else if (p.CollectionKind == "List")
            {
                sb.AppendLine($"{i}    var span_{p.Name} = CollectionsMarshal.AsSpan(scol_{p.Name});");
                sb.AppendLine($"{i}    for (var si_{p.Name} = 0; si_{p.Name} < span_{p.Name}.Length; si_{p.Name}++)");
                sb.AppendLine($"{i}    {{");
                sb.AppendLine($"{i}        var se_{p.Name} = span_{p.Name}[si_{p.Name}];");
            }
            else if (p.CollectionKind == "IndexedCollection")
            {
                sb.AppendLine($"{i}    for (var si_{p.Name} = 0; si_{p.Name} < scol_{p.Name}.Count; si_{p.Name}++)");
                sb.AppendLine($"{i}    {{");
                sb.AppendLine($"{i}        var se_{p.Name} = scol_{p.Name}[si_{p.Name}];");
            }
            else // Counted (foreach)
            {
                sb.AppendLine($"{i}    foreach (var se_{p.Name} in scol_{p.Name})");
                sb.AppendLine($"{i}    {{");
            }

            var e = $"se_{p.Name}";
            // Null check only — ScanObject handles depth + ref tracking internally
            sb.AppendLine($"{i}        if ({e} == null) continue;");
            sb.AppendLine($"{i}        {writer}.Instance.ScanObject({e}, context);");
            sb.AppendLine($"{i}    }}");
            sb.AppendLine($"{i}    }}");
            sb.AppendLine($"{i}}}");
            return;
        }

        // Complex element collection without SGen writer — runtime fallback
        if (p.ElementKind == PropertyTypeKind.Complex)
        {
            sb.AppendLine($"{i}var scol_{p.Name} = {a};");
            sb.AppendLine($"{i}if (scol_{p.Name} != null)");
            sb.AppendLine($"{i}    AcBinarySerializer.ScanValueGenerated(scol_{p.Name}, typeof({p.TypeNameForTypeof}), context);");
            return;
        }

        // Primitive element collection — no scanning needed
    }

    /// <summary>
    /// Emits inline dictionary scan. Iterates entries and:
    /// - String keys: ScanInternString if interning flags match
    /// - String values: ScanInternString if interning flags match
    /// - Complex+SGen values: ScanObject on each value (handles ref tracking internally)
    /// Eliminates GetWrapper dictionary lookup for all inlineable dictionary types.
    /// </summary>
    private static void EmitScanDictionary(StringBuilder sb, PropInfo p, string a, string i)
    {
        var s = p.Name;
        var hasStringKeys = p.DictKeyKind == PropertyTypeKind.String && p.InterningFlags != 0;
        var hasStringValues = p.DictValueKind == PropertyTypeKind.String && p.InterningFlags != 0;
        var hasComplexValues = p.DictValueKind == PropertyTypeKind.Complex && p.DictValueHasGeneratedWriter;

        // No scanning needed for primitive-only dictionaries without internable strings or complex values
        if (!hasStringKeys && !hasStringValues && !hasComplexValues) return;

        // Complex+SGen values: compile-time proven scan is no-op → skip entirely
        if (hasComplexValues && !p.DictValueNeedsScan && !hasStringKeys && !hasStringValues) return;

        // Build guard expression for Complex+SGen values (3-axis: IId/AllRef/Intern)
        string? complexGuard = null;
        if (hasComplexValues && p.DictValueNeedsScan && !p.DictValueNeedsIdScan)
        {
            if (p.DictValueNeedsAllRefScan && p.DictValueNeedsInternScan)
                complexGuard = "context.HasAllRefHandling || context.HasStringInterning";
            else if (p.DictValueNeedsAllRefScan)
                complexGuard = "context.HasAllRefHandling";
            else if (p.DictValueNeedsInternScan)
                complexGuard = "context.HasStringInterning";
        }

        // For string-only scan (no complex values), use simple interning loop
        if (!hasComplexValues)
        {
            sb.AppendLine($"{i}var sd_{s} = {a};");
            sb.AppendLine($"{i}if (sd_{s} != null && ({p.InterningFlags} & internBit) != 0)");
            sb.AppendLine($"{i}{{");
            sb.AppendLine($"{i}    foreach (var sde_{s} in sd_{s})");
            sb.AppendLine($"{i}    {{");
            if (hasStringKeys)
            {
                sb.AppendLine($"{i}        if (sde_{s}.Key != null)");
                sb.AppendLine($"{i}        {{");
                sb.AppendLine($"{i}            var sklen_{s} = sde_{s}.Key.Length;");
                sb.AppendLine($"{i}            if (sklen_{s} >= minIntern && (maxIntern == 0 || sklen_{s} <= maxIntern))");
                sb.AppendLine($"{i}                context.ScanInternString(sde_{s}.Key);");
                sb.AppendLine($"{i}        }}");
            }
            if (hasStringValues)
            {
                sb.AppendLine($"{i}        if (sde_{s}.Value != null)");
                sb.AppendLine($"{i}        {{");
                sb.AppendLine($"{i}            var svlen_{s} = sde_{s}.Value.Length;");
                sb.AppendLine($"{i}            if (svlen_{s} >= minIntern && (maxIntern == 0 || svlen_{s} <= maxIntern))");
                sb.AppendLine($"{i}                context.ScanInternString(sde_{s}.Value);");
                sb.AppendLine($"{i}        }}");
            }
            sb.AppendLine($"{i}    }}");
            sb.AppendLine($"{i}}}");
            return;
        }

        // Complex+SGen values (with optional string key/value interning)
        var writer = p.DictValueWriterClassName!;

        // Guard entire scan block when no IId in value subtree
        if (complexGuard != null && !hasStringKeys && !hasStringValues)
            sb.AppendLine($"{i}if ({complexGuard})");

        sb.AppendLine($"{i}{{");
        sb.AppendLine($"{i}    var sd_{s} = {a};");
        sb.AppendLine($"{i}    if (sd_{s} != null)");
        sb.AppendLine($"{i}    {{");
        sb.AppendLine($"{i}        foreach (var sde_{s} in sd_{s})");
        sb.AppendLine($"{i}        {{");

        // String key interning
        if (hasStringKeys)
        {
            sb.AppendLine($"{i}            if (({p.InterningFlags} & internBit) != 0 && sde_{s}.Key != null)");
            sb.AppendLine($"{i}            {{");
            sb.AppendLine($"{i}                var sklen_{s} = sde_{s}.Key.Length;");
            sb.AppendLine($"{i}                if (sklen_{s} >= minIntern && (maxIntern == 0 || sklen_{s} <= maxIntern))");
            sb.AppendLine($"{i}                    context.ScanInternString(sde_{s}.Key);");
            sb.AppendLine($"{i}            }}");
        }

        // String value interning
        if (hasStringValues)
        {
            sb.AppendLine($"{i}            if (({p.InterningFlags} & internBit) != 0 && sde_{s}.Value != null)");
            sb.AppendLine($"{i}            {{");
            sb.AppendLine($"{i}                var svlen_{s} = sde_{s}.Value.Length;");
            sb.AppendLine($"{i}                if (svlen_{s} >= minIntern && (maxIntern == 0 || svlen_{s} <= maxIntern))");
            sb.AppendLine($"{i}                    context.ScanInternString(sde_{s}.Value);");
            sb.AppendLine($"{i}            }}");
        }

        // Complex value ScanObject
        if (hasComplexValues)
        {
            sb.AppendLine($"{i}            if (sde_{s}.Value != null)");
            if (complexGuard != null)
            {
                sb.AppendLine($"{i}            {{");
                sb.AppendLine($"{i}                if ({complexGuard})");
                sb.AppendLine($"{i}                    {writer}.Instance.ScanObject(sde_{s}.Value, context);");
                sb.AppendLine($"{i}            }}");
            }
            else
                sb.AppendLine($"{i}                {writer}.Instance.ScanObject(sde_{s}.Value, context);");
        }

        sb.AppendLine($"{i}        }}");
        sb.AppendLine($"{i}    }}");
        sb.AppendLine($"{i}}}");
    }

    #endregion

    /// <summary>
    /// Emits inline object write for a Complex property that has a generated writer.
    /// Compile-time ChildNeedsRefScan eliminates TryConsumeWritePlanEntry when scan never tracks child.
    /// !ChildNeedsRefScan + !ChildEnableMetadata → ZERO branches: just Object + WriteProperties.
    /// </summary>
    private static void EmitDirectObjectWrite(StringBuilder sb, PropInfo p, string a, string i)
    {
        var writer = p.WriterClassName;
        var refSuffix = p.IsIId ? "IId" : "All";

        // Reference type properties can always be null at runtime regardless of nullable annotation
        sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);");

        if (!p.ChildNeedsRefScan && !p.ChildEnableMetadata)
        {
            // Compile-time proven: no ref, no metadata. Combined check+inc BEFORE marker write so Truncate writes
            // Null wire-correctly. TryEnterRecursion inc'd on success; ExitRecursion at WriteProperties end.
            sb.AppendLine($"{i}else if (context.TryEnterRecursion(hasTruncatePath: true)) {{ /* truncated: Null written */ }}");
            sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Object); {writer}.Instance.WriteProperties({a}, context); }}");
        }
        else if (p.ChildNeedsRefScan && !p.ChildEnableMetadata)
        {
            sb.AppendLine($"{i}else if (context.WriteObjectRefMarker{refSuffix}()) {writer}.Instance.WriteProperties({a}, context);");
        }
        else if (!p.ChildNeedsRefScan && p.ChildEnableMetadata)
        {
            sb.AppendLine($"{i}else if (context.WriteObjectMetaMarker({a}, {writer}.s_wrapperSlot)) {writer}.Instance.WriteProperties({a}, context);");
        }
        else
        {
            sb.AppendLine($"{i}else if (context.WriteObjectFullMarker{refSuffix}({a}, {writer}.s_wrapperSlot)) {writer}.Instance.WriteProperties({a}, context);");
        }
    }

    /// <summary>
    /// Emits inline metadata write: typeNameHash + (if first) propCount + property hashes.
    /// All values are compile-time constants.
    /// </summary>
    private static void EmitInlineMetadata(StringBuilder sb, int typeNameHash, int[] propertyHashes, string isFirstVar, string i)
    {
        sb.AppendLine($"{i}context.WriteRaw({typeNameHash});");
        sb.AppendLine($"{i}if ({isFirstVar})");
        sb.AppendLine($"{i}{{");
        sb.AppendLine($"{i}    context.WriteVarUInt({(uint)propertyHashes.Length});");
        foreach (var hash in propertyHashes)
            sb.AppendLine($"{i}    context.WriteRaw({hash});");
        sb.AppendLine($"{i}}}");
    }

    /// <summary>
    /// Emits inline collection write for List&lt;T&gt; / T[] where T is a Complex type with generated writer.
    /// Bypasses GetWrapper + WriteArray + WriteValue per-element dispatch entirely.
    /// Wire format: [Array marker][VarUInt count][elem₁ marker+props][elem₂ marker+props]...
    /// Handles both UseMetadata=true and false inline — no fallback to WriteValueGenerated.
    /// </summary>
    private static void EmitDirectCollectionWrite(StringBuilder sb, PropInfo p, string a, string i)
    {
        var writer = p.ElementWriterClassName;

        // Reference type collections can always be null at runtime regardless of nullable annotation
        sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);");
        sb.AppendLine($"{i}else");
        sb.AppendLine($"{i}{{");

        sb.AppendLine($"{i}    context.WriteByte(BinaryTypeCode.Array);");

        // Get count and iteration based on collection kind
        if (p.CollectionKind == "Array")
        {
            sb.AppendLine($"{i}    var arr_{p.Name} = {a};");
            sb.AppendLine($"{i}    context.WriteVarUInt((uint)arr_{p.Name}.Length);");
            sb.AppendLine($"{i}    for (var i_{p.Name} = 0; i_{p.Name} < arr_{p.Name}.Length; i_{p.Name}++)");
            sb.AppendLine($"{i}    {{");
            sb.AppendLine($"{i}        var elem_{p.Name} = arr_{p.Name}[i_{p.Name}];");
        }
        else if (p.CollectionKind == "Counted")
        {
            sb.AppendLine($"{i}    var col_{p.Name} = {a};");
            sb.AppendLine($"{i}    context.WriteVarUInt((uint)col_{p.Name}.Count);");
            sb.AppendLine($"{i}    foreach (var elem_{p.Name} in col_{p.Name})");
            sb.AppendLine($"{i}    {{");
        }
        else if (p.CollectionKind == "IndexedCollection")
        {
            sb.AppendLine($"{i}    var col_{p.Name} = {a};");
            sb.AppendLine($"{i}    context.WriteVarUInt((uint)col_{p.Name}.Count);");
            sb.AppendLine($"{i}    for (var i_{p.Name} = 0; i_{p.Name} < col_{p.Name}.Count; i_{p.Name}++)");
            sb.AppendLine($"{i}    {{");
            sb.AppendLine($"{i}        var elem_{p.Name} = col_{p.Name}[i_{p.Name}];");
        }
        else // List — CollectionsMarshal.AsSpan for zero-overhead iteration
        {
            sb.AppendLine($"{i}    var span_{p.Name} = CollectionsMarshal.AsSpan({a});");
            sb.AppendLine($"{i}    context.WriteVarUInt((uint)span_{p.Name}.Length);");
            sb.AppendLine($"{i}    for (var i_{p.Name} = 0; i_{p.Name} < span_{p.Name}.Length; i_{p.Name}++)");
            sb.AppendLine($"{i}    {{");
            sb.AppendLine($"{i}        var elem_{p.Name} = span_{p.Name}[i_{p.Name}];");
        }

        // Per-element write
        var e = $"elem_{p.Name}";
        sb.AppendLine($"{i}        if ({e} == null) {{ context.WriteByte(BinaryTypeCode.Null); continue; }}");

        var elemRefSuffix = p.ElementIsIId ? "IId" : "All";

        // Ref-aware emit decision routed through RefAwareEmitPredicate — symmetric counterpart to the
        // reader-side EmitReadCollectionElement / EmitReadComplex guards. Single source of truth so the
        // writer-emit (which marker variants may appear on the wire) and the reader-emit (which marker
        // variants are decoded) NEVER drift apart on the same PropInfo.
        var elementEmitsRefMarker = RefAwareEmitPredicate.ElementEmitsRefMarker(p);

        if (!elementEmitsRefMarker && !p.ElementEnableMetadata)
        {
            // Compile-time proven: no ref, no metadata. Combined check+inc before marker write.
            sb.AppendLine($"{i}        if (context.TryEnterRecursion(hasTruncatePath: true)) continue;");
            sb.AppendLine($"{i}        context.WriteByte(BinaryTypeCode.Object);");
            sb.AppendLine($"{i}        {writer}.Instance.WriteProperties({e}, context);");
        }
        else if (elementEmitsRefMarker && !p.ElementEnableMetadata)
        {
            sb.AppendLine($"{i}        if (context.WriteObjectRefMarker{elemRefSuffix}()) {writer}.Instance.WriteProperties({e}, context);");
        }
        else if (!elementEmitsRefMarker && p.ElementEnableMetadata)
        {
            sb.AppendLine($"{i}        if (context.WriteObjectMetaMarker({e}, {writer}.s_wrapperSlot)) {writer}.Instance.WriteProperties({e}, context);");
        }
        else
        {
            sb.AppendLine($"{i}        if (context.WriteObjectFullMarker{elemRefSuffix}({e}, {writer}.s_wrapperSlot)) {writer}.Instance.WriteProperties({e}, context);");
        }

        sb.AppendLine($"{i}    }}");
        sb.AppendLine($"{i}}}");
    }

    /// <summary>
    /// Emits inline write of a primitive/string/enum value in non-property context (no PropertySkip).
    /// Matches runtime TryWritePrimitive wire format: TinyInt for small int, type code + value otherwise.
    /// Used for dictionary key/value writes.
    /// </summary>
    private static void EmitWritePrimitiveValue(StringBuilder sb, PropertyTypeKind kind, string a, string suffix, string i)
    {
        switch (kind)
        {
            case PropertyTypeKind.Int32:
                sb.AppendLine($"{i}if (BinaryTypeCode.TryEncodeTinyInt({a}, out var tk_{suffix})) context.WriteByte(tk_{suffix});");
                sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Int32); context.WriteVarInt({a}); }}");
                break;
            case PropertyTypeKind.Int64:
                sb.AppendLine($"{i}if ({a} >= int.MinValue && {a} <= int.MaxValue)");
                sb.AppendLine($"{i}{{");
                sb.AppendLine($"{i}    var iv_{suffix} = (int){a};");
                sb.AppendLine($"{i}    if (BinaryTypeCode.TryEncodeTinyInt(iv_{suffix}, out var tk_{suffix})) context.WriteByte(tk_{suffix});");
                sb.AppendLine($"{i}    else {{ context.WriteByte(BinaryTypeCode.Int32); context.WriteVarInt(iv_{suffix}); }}");
                sb.AppendLine($"{i}}}");
                sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Int64); context.WriteVarLong({a}); }}");
                break;
            case PropertyTypeKind.Boolean:
                sb.AppendLine($"{i}context.WriteByte({a} ? BinaryTypeCode.True : BinaryTypeCode.False);");
                break;
            case PropertyTypeKind.Double:
                sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Float64); context.WriteRaw({a});");
                break;
            case PropertyTypeKind.Single:
                sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Float32); context.WriteRaw({a});");
                break;
            case PropertyTypeKind.Decimal:
                sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Decimal); context.WriteDecimalBits({a});");
                break;
            case PropertyTypeKind.DateTime:
                sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.DateTime); context.WriteDateTimeBits({a});");
                break;
            case PropertyTypeKind.Guid:
                sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Guid); context.WriteGuidBits({a});");
                break;
            case PropertyTypeKind.TimeSpan:
                sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.TimeSpan); context.WriteRaw({a}.Ticks);");
                break;
            case PropertyTypeKind.DateTimeOffset:
                sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.DateTimeOffset); context.WriteDateTimeOffsetBits({a});");
                break;
            case PropertyTypeKind.Byte:
                sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.UInt8); context.WriteByte({a});");
                break;
            case PropertyTypeKind.Int16:
                sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Int16); context.WriteRaw({a});");
                break;
            case PropertyTypeKind.UInt16:
                sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.UInt16); context.WriteRaw({a});");
                break;
            case PropertyTypeKind.UInt32:
                sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.UInt32); context.WriteVarUInt({a});");
                break;
            case PropertyTypeKind.UInt64:
                sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.UInt64); context.WriteVarULong({a});");
                break;
            case PropertyTypeKind.Enum:
                sb.AppendLine($"{i}{{ var ev_{suffix} = (int){a}; context.WriteByte(BinaryTypeCode.Enum);");
                sb.AppendLine($"{i}  if (BinaryTypeCode.TryEncodeTinyInt(ev_{suffix}, out var te_{suffix})) context.WriteByte(te_{suffix});");
                sb.AppendLine($"{i}  else {{ context.WriteByte(BinaryTypeCode.Int32); context.WriteVarInt(ev_{suffix}); }} }}");
                break;
        }
    }

    /// <summary>
    /// Emits inline dictionary write. Wire format: [Dictionary][count][key₁ value₁ key₂ value₂ ...].
    /// Keys/values are written with type codes matching runtime TryWritePrimitive/WriteValue.
    /// Eliminates GetWrapper dictionary lookup for all inlineable key/value types.
    /// </summary>
    private static void EmitDirectDictionaryWrite(StringBuilder sb, PropInfo p, string a, string i)
    {
        var s = p.Name;
        var keyType = p.DictKeyTypeName ?? "object";
        var valType = p.DictValueTypeName ?? "object";

        // Reference type dictionaries can always be null at runtime regardless of nullable annotation
        sb.AppendLine($"{i}if ({a} == null) context.WriteByte(BinaryTypeCode.PropertySkip);");
        sb.AppendLine($"{i}else");
        sb.AppendLine($"{i}{{");

        sb.AppendLine($"{i}    context.WriteByte(BinaryTypeCode.Dictionary);");
        sb.AppendLine($"{i}    context.WriteVarUInt((uint){a}.Count);");
        sb.AppendLine($"{i}    foreach (var kvp_{s} in {a})");
        sb.AppendLine($"{i}    {{");

        var k = $"kvp_{s}.Key";
        var v = $"kvp_{s}.Value";
        var ii = i + "        ";

        // Write key
        if (p.DictKeyKind == PropertyTypeKind.String)
        {
            if (p.InterningFlags == 0)
                sb.AppendLine($"{ii}context.StringInternEligible = false;");
            else
                sb.AppendLine($"{ii}context.StringInternEligible = ({p.InterningFlags} & context.InternBit) != 0;");
            sb.AppendLine($"{ii}AcBinarySerializer.WriteStringGenerated({k}, context);");
        }
        else if (IsMarkerless(p.DictKeyKind) || p.DictKeyKind == PropertyTypeKind.Enum)
        {
            EmitWritePrimitiveValue(sb, p.DictKeyKind, k, $"dk_{s}", ii);
        }
        else
        {
            sb.AppendLine($"{ii}AcBinarySerializer.WriteValueGenerated({k}, typeof({keyType}), context);");
        }

        // Write value
        if (p.DictValueKind == PropertyTypeKind.String)
        {
            // String value: null → Null, non-null → WriteStringGenerated
            sb.AppendLine($"{ii}if ({v} == null) context.WriteByte(BinaryTypeCode.Null);");
            sb.AppendLine($"{ii}else");
            sb.AppendLine($"{ii}{{");
            if (p.InterningFlags == 0)
                sb.AppendLine($"{ii}    context.StringInternEligible = false;");
            else
                sb.AppendLine($"{ii}    context.StringInternEligible = ({p.InterningFlags} & context.InternBit) != 0;");
            sb.AppendLine($"{ii}    AcBinarySerializer.WriteStringGenerated({v}, context);");
            sb.AppendLine($"{ii}}}");
        }
        else if (IsMarkerless(p.DictValueKind) || p.DictValueKind == PropertyTypeKind.Enum)
        {
            EmitWritePrimitiveValue(sb, p.DictValueKind, v, $"dv_{s}", ii);
        }
        else if (p.DictValueKind == PropertyTypeKind.Complex && p.DictValueHasGeneratedWriter)
        {
            EmitDictValueComplexWrite(sb, p, v, s, ii);
        }
        else
        {
            // Fallback for non-inlineable value types
            sb.AppendLine($"{ii}AcBinarySerializer.WriteValueGenerated({v}, typeof({valType}), context);");
        }

        sb.AppendLine($"{i}    }}");
        sb.AppendLine($"{i}}}");
    }

    /// <summary>
    /// Emits inline write for a Complex+SGen dictionary value with ref tracking and metadata support.
    /// Delegates marker logic to runtime WriteObjectRefMarker/MetaMarker/FullMarker bridge.
    /// </summary>
    private static void EmitDictValueComplexWrite(StringBuilder sb, PropInfo p, string v, string s, string i)
    {
        var writer = p.DictValueWriterClassName!;

        sb.AppendLine($"{i}if ({v} == null) {{ context.WriteByte(BinaryTypeCode.Null); }}");

        var dvRefSuffix = p.DictValueIsIId ? "IId" : "All";

        if (!p.DictValueNeedsRefScan && !p.DictValueEnableMetadata)
        {
            // No ref, no metadata. Combined check+inc before marker write.
            sb.AppendLine($"{i}else if (context.TryEnterRecursion(hasTruncatePath: true)) {{ /* truncated: Null written */ }}");
            sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Object); {writer}.Instance.WriteProperties({v}, context); }}");
        }
        else if (p.DictValueNeedsRefScan && !p.DictValueEnableMetadata)
        {
            sb.AppendLine($"{i}else if (context.WriteObjectRefMarker{dvRefSuffix}()) {writer}.Instance.WriteProperties({v}, context);");
        }
        else if (!p.DictValueNeedsRefScan && p.DictValueEnableMetadata)
        {
            sb.AppendLine($"{i}else if (context.WriteObjectMetaMarker({v}, {writer}.s_wrapperSlot)) {writer}.Instance.WriteProperties({v}, context);");
        }
        else
        {
            sb.AppendLine($"{i}else if (context.WriteObjectFullMarker{dvRefSuffix}({v}, {writer}.s_wrapperSlot)) {writer}.Instance.WriteProperties({v}, context);");
        }
    }

    private static void EmitSkip(StringBuilder sb, PropertyTypeKind k, string a, string typeName, string i)
    {
        switch (k)
        {
            case PropertyTypeKind.Int32:
            {
                // Mirrors runtime WritePropertyOrSkip → WriteInt32 (TinyInt optimization)
                var s32 = a.Replace(".", "_");
                sb.AppendLine($"{i}if ({a} == 0) context.WriteByte(BinaryTypeCode.PropertySkip);");
                sb.AppendLine($"{i}else if (BinaryTypeCode.TryEncodeTinyInt({a}, out var ti_{s32})) context.WriteByte(ti_{s32});");
                sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Int32); context.WriteVarInt({a}); }}");
                break;
            }
            case PropertyTypeKind.Int64:
            {
                // Mirrors runtime WritePropertyOrSkip → WriteInt64 → WriteInt32 (int range + TinyInt)
                var s64 = a.Replace(".", "_");
                sb.AppendLine($"{i}if ({a} == 0L) context.WriteByte(BinaryTypeCode.PropertySkip);");
                sb.AppendLine($"{i}else if ({a} >= int.MinValue && {a} <= int.MaxValue)");
                sb.AppendLine($"{i}{{");
                sb.AppendLine($"{i}    var iv_{s64} = (int){a};");
                sb.AppendLine($"{i}    if (BinaryTypeCode.TryEncodeTinyInt(iv_{s64}, out var ti_{s64})) context.WriteByte(ti_{s64});");
                sb.AppendLine($"{i}    else {{ context.WriteByte(BinaryTypeCode.Int32); context.WriteVarInt(iv_{s64}); }}");
                sb.AppendLine($"{i}}}");
                sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Int64); context.WriteVarLong({a}); }}");
                break;
            }
            case PropertyTypeKind.Boolean:
                sb.AppendLine($"{i}if (!{a}) context.WriteByte(BinaryTypeCode.PropertySkip);");
                sb.AppendLine($"{i}else context.WriteByte(BinaryTypeCode.True);");
                break;
            case PropertyTypeKind.Double:
                sb.AppendLine($"{i}if ({a} == 0.0) context.WriteByte(BinaryTypeCode.PropertySkip);");
                sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Float64); context.WriteRaw({a}); }}");
                break;
            case PropertyTypeKind.Single:
                sb.AppendLine($"{i}if ({a} == 0f) context.WriteByte(BinaryTypeCode.PropertySkip);");
                sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Float32); context.WriteRaw({a}); }}");
                break;
            case PropertyTypeKind.Decimal:
                sb.AppendLine($"{i}if ({a} == 0m) context.WriteByte(BinaryTypeCode.PropertySkip);");
                sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Decimal); context.WriteDecimalBits({a}); }}");
                break;
            case PropertyTypeKind.DateTime:
                sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.DateTime); context.WriteDateTimeBits({a});");
                break;
            case PropertyTypeKind.Guid:
                sb.AppendLine($"{i}if ({a} == System.Guid.Empty) context.WriteByte(BinaryTypeCode.PropertySkip);");
                sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Guid); context.WriteGuidBits({a}); }}");
                break;
            case PropertyTypeKind.Byte:
                sb.AppendLine($"{i}if ({a} == 0) context.WriteByte(BinaryTypeCode.PropertySkip);");
                sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.UInt8); context.WriteByte({a}); }}");
                break;
            case PropertyTypeKind.Int16:
                sb.AppendLine($"{i}if ({a} == 0) context.WriteByte(BinaryTypeCode.PropertySkip);");
                sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Int16); context.WriteRaw({a}); }}");
                break;
            case PropertyTypeKind.UInt16:
                sb.AppendLine($"{i}if ({a} == 0) context.WriteByte(BinaryTypeCode.PropertySkip);");
                sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.UInt16); context.WriteRaw({a}); }}");
                break;
            case PropertyTypeKind.UInt32:
                sb.AppendLine($"{i}if ({a} == 0U) context.WriteByte(BinaryTypeCode.PropertySkip);");
                sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.UInt32); context.WriteVarUInt({a}); }}");
                break;
            case PropertyTypeKind.UInt64:
                sb.AppendLine($"{i}if ({a} == 0UL) context.WriteByte(BinaryTypeCode.PropertySkip);");
                sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.UInt64); context.WriteVarULong({a}); }}");
                break;
            case PropertyTypeKind.Enum:
                var s = a.Replace(".", "_");
                sb.AppendLine($"{i}var ev_{s} = (int){a};");
                sb.AppendLine($"{i}if (ev_{s} == 0) context.WriteByte(BinaryTypeCode.PropertySkip);");
                sb.AppendLine($"{i}else if (BinaryTypeCode.TryEncodeTinyInt(ev_{s}, out var te_{s})) {{ context.WriteByte(BinaryTypeCode.Enum); context.WriteByte(te_{s}); }}");
                sb.AppendLine($"{i}else {{ context.WriteByte(BinaryTypeCode.Enum); context.WriteByte(BinaryTypeCode.Int32); context.WriteVarInt(ev_{s}); }}");
                break;
            case PropertyTypeKind.TimeSpan:
                sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.TimeSpan); context.WriteRaw({a}.Ticks);");
                break;
            case PropertyTypeKind.DateTimeOffset:
                sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.DateTimeOffset); context.WriteDateTimeOffsetBits({a});");
                break;
            default:
                sb.AppendLine($"{i}AcBinarySerializer.WriteValueGenerated({a}, typeof({typeName}), context);");
                break;
        }
    }

    private static void EmitVal(StringBuilder sb, PropertyTypeKind k, string a, string typeName, string i)
    {
        switch (k)
        {
            case PropertyTypeKind.Int32: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Int32); context.WriteVarInt({a});"); break;
            case PropertyTypeKind.Int64: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Int64); context.WriteVarLong({a});"); break;
            case PropertyTypeKind.Boolean: sb.AppendLine($"{i}context.WriteByte({a} ? BinaryTypeCode.True : BinaryTypeCode.False);"); break;
            case PropertyTypeKind.Double: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Float64); context.WriteRaw({a});"); break;
            case PropertyTypeKind.Single: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Float32); context.WriteRaw({a});"); break;
            case PropertyTypeKind.Decimal: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Decimal); context.WriteDecimalBits({a});"); break;
            case PropertyTypeKind.DateTime: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.DateTime); context.WriteDateTimeBits({a});"); break;
            case PropertyTypeKind.Guid: sb.AppendLine($"{i}context.WriteByte(BinaryTypeCode.Guid); context.WriteGuidBits({a});"); break;
            default: EmitSkip(sb, k, a, typeName, i); break;
        }
    }
}
