using System.Collections;
using System.Runtime.CompilerServices;

namespace AyCode.Core.Serializers.Binaries;

public static partial class AcBinarySerializer
{
    /// <summary>
    /// First pass: scans object graph to identify duplicates (strings + objects).
    /// Only traverses reference properties (complex types + strings).
    /// Stops traversing children after 2nd occurrence of an IId object:
    /// - Prevents infinite recursion on circular references
    /// - Consistent with write pass which writes ObjectRef (no children) for 2nd occurrence
    /// - Strings/objects skipped here are never written anyway (parent is ObjectRef)
    /// CacheIndex is assigned immediately on 2nd occurrence (no post-processing needed).
    /// Uses wrapper.GeneratedWriter when available — the wrapper is already cached,
    /// so no dictionary lookup overhead. SGen types call generated ScanForDuplicates
    /// which bypasses the entire runtime scan path.
    /// </summary>
    private static void ScanForDuplicates<TOutput>(object value, Type type, BinarySerializationContext<TOutput> context, TypeMetadataWrapper<BinarySerializeTypeMetadata>? wrapper = null)
        where TOutput : struct, IBinaryOutputBase
    {
        if (!context.HasCaching) return;

        wrapper ??= context.GetWrapper(type);

        // SGen path: wrapper.GeneratedWriter is cached (no registry lookup per call).
        // Generated ScanForDuplicates handles HasCaching + ScanObject + SortWritePlan.
        var genWriter = wrapper.GeneratedWriter;

        if (genWriter != null && context.Options.UseGeneratedCode) genWriter.ScanObject(value, context);
        else ScanValue(value, wrapper, context);

        context.SortWritePlan();
    }

    private static void ScanValue<TOutput>(object? value, TypeMetadataWrapper<BinarySerializeTypeMetadata> wrapper, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        if (value == null)
            return;

        var metadata = wrapper.Metadata;
        
        // Skip primitives (pre-computed field, no Type.GetTypeCode() call)
        if (metadata.IsPrimitiveType)
            return;

        // Collection → iterate elements using IList fast path (no IEnumerator alloc)
        if (metadata.IsCollection)
        {
            var isStringCollectionElementType = false;

            // Per-property interning: use CurrentProperty (set in property loop) when available,
            // fallback to global UseStringInterning for root-level collections.
           
            //TODO: A collection esetén is vizsgálni kéne hogy UseStringPropertyInterning - J.
            if (metadata.ElementNeedsScan && 
                !((isStringCollectionElementType = ReferenceEquals(metadata.CollectionElementType, StringType)) && !context.UseStringInterning))
            {
                if (isStringCollectionElementType)
                {
                    ScanStringCollection(value, context);
                    return;
                }

                // Pre-cache element wrapper for non-string collections (string fast path in ScanItem doesn't need it)
                var elementWrapper = context.GetWrapper(metadata.CollectionElementType!);

                if (value is IList list)
                {
                    for (var i = 0; i < list.Count; i++)
                    {
                        var item = list[i];
                        if (item != null)
                            ScanItem(item, elementWrapper, context);
                    }
                }
                else if (value is IEnumerable enumerable)
                {
                    foreach (var item in enumerable)
                    {
                        if (item != null)
                            ScanItem(item, elementWrapper, context);
                    }
                }
            }

            return;
        }

        // Object → ref tracking + recursive scan

        // SGen path: ScanObject handles its own ref tracking (ScanTrackObjectXxx)
        // Must be checked BEFORE runtime ref tracking to avoid double ScanVisitIndex++
        var genWriter = wrapper.GeneratedWriter;
        if (genWriter != null && context.Options.UseGeneratedCode)
        {
            genWriter.ScanObject(value, context);
            return;
        }

        // Reference tracking for IId types (or all types when ReferenceHandling == All)
        // 2nd occurrence → skip children because:
        // 1. Write pass writes ObjectRef (no children) → strings/objects here are never in output
        // 2. Prevents infinite recursion on circular references (A→B→A→...)
        // 3. Nested objects reachable from other paths are scanned through those paths
        if (context.UseTypeReferenceHandling(metadata))
        {
            var visitIndex = context.ScanVisitIndex++;

            // Direct tracking call - avoid extra indirection through context
            bool isFirst;
            int cacheIndex;
            int firstVisitIndex;
            switch (metadata.IdAccessorType)
            {
                case IdAccessorType.Int32:
                    var id32 = wrapper.RefIdGetterInt32!(value);
                    isFirst = wrapper.TryTrackInt32(id32, visitIndex, ref context.NextCacheIndexRef, out cacheIndex, out firstVisitIndex);
                    break;
                case IdAccessorType.Int64:
                    var id64 = wrapper.RefIdGetterInt64!(value);
                    isFirst = wrapper.TryTrackInt64(id64, visitIndex, ref context.NextCacheIndexRef, out cacheIndex, out firstVisitIndex);
                    break;
                case IdAccessorType.Guid:
                    var idGuid = wrapper.RefIdGetterGuid!(value);
                    isFirst = wrapper.TryTrackGuid(idGuid, visitIndex, ref context.NextCacheIndexRef, out cacheIndex, out firstVisitIndex);
                    break;
                default:
                    isFirst = true;
                    cacheIndex = -1;
                    firstVisitIndex = -1;
                    break;
            }

            if (!isFirst)
            {
                // Build WriteDuplicateEntry for IId references
                if (firstVisitIndex >= 0)
                {
                    // Exact 2nd occurrence: add RefFirst entry at first visit position
                    context.AddWriteDuplicateEntry(firstVisitIndex, cacheIndex, isFirst: true, value: null);
                }
                // 2nd+ occurrence: add ObjRef at current position
                context.AddWriteDuplicateEntry(visitIndex, cacheIndex, isFirst: false, value: null);

                return; // 2nd occurrence → skip children (symmetric with write pass ObjectRef)
            }
        }

        // Fallback: runtime property loop (no SGen writer for this type)
        // Combined check+inc — gated inside TryEnterRecursion (checks NeedsDepthCheck).
        // On limit hit: Throw or Truncate=return (write pass writes Null at the same boundary).
        if (context.TryEnterRecursion(hasTruncatePath: false)) return;

        var refProperties = metadata.ReferenceProperties;
        var hasPropertyFilter = context.HasPropertyFilter;

        for (var i = 0; i < refProperties.Length; i++)
        {
            var prop = refProperties[i];

            // Must match write pass: filtered properties write PropertySkip (no value) →
            // scanning them would assign CacheIndex for strings/objects never in output
            if (hasPropertyFilter && !context.ShouldSerializeProperty(value, prop))
                continue;

            if (prop.AccessorType == PropertyAccessorType.String)
            {
                if (!prop.UseStringPropertyInterning(context.InternBit)) continue;

                var str2 = prop.GetString(value);
                if (str2 != null && context.IsValidForInterningString(str2.Length))
                    context.ScanInternString(str2);
            }
            else if (prop.IsStringCollectionProperty)
            {
                if (!prop.UseStringPropertyInterning(context.InternBit)) continue;

                var propValue = prop.GetValue(value);
                if (propValue != null)
                    ScanStringCollection(propValue, context);
            }
            else
            {
                var propValue = prop.GetValue(value);
                if (propValue != null)
                {
                    var runtimeType = propValue.GetType();
                    var propWrapper = wrapper.GetPropertyTypeWrapper(prop.ComplexPropertyIndex, runtimeType);
                    if (propWrapper == null)
                    {
                        propWrapper = context.GetWrapper(runtimeType);
                        wrapper.SetPropertyTypeWrapper(prop.ComplexPropertyIndex, propWrapper);
                    }
                    ScanValue(propValue, propWrapper, context);
                }
            }
        }

        context.ExitRecursion();
    }

    /// <summary>
    /// Public entry point for SGen-generated ScanProperties to call back into runtime ScanValue
    /// for child types that don't have a generated writer (fallback to runtime path with wrapper lookup).
    /// </summary>
    /// <remarks>
    /// SGen-generated callers always pass a statically-known type (e.g. <c>typeof(MetadataInfo)</c>),
    /// so the DAMs annotation propagates correctly: the trimmer sees the static <c>typeof</c> reference,
    /// preserves the type's <c>PublicProperties</c> metadata, and <see cref="AcSerializerContextBase{TMetadata,TOptions}.GetWrapper"/>
    /// is satisfied.
    /// </remarks>
    internal static void ScanValueGenerated<TOutput>(
        object value,
        [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(TypeMetadataBase.RequiredMembers)] Type type,
        BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        // Bridge: ScanValue still has int depth in its signature (other internal callers use it).
        // Pass 0 placeholder; full depth-cleanup of ScanValue chain is pending.
        var wrapper = context.GetWrapper(type);
        ScanValue(value, wrapper, context);
    }

    /// <summary>
    /// Scans string elements of a collection for interning. Uses IList fast path when available.
    /// </summary>
    private static void ScanStringCollection<TOutput>(object collection, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        if (collection is IList list)
        {
            for (var i = 0; i < list.Count; i++)
            {
                if (list[i] is string str && context.IsValidForInterningString(str.Length))
                    context.ScanInternString(str);
            }
        }
        else if (collection is IEnumerable enumerable)
        {
            foreach (var item in enumerable)
            {
                if (item is string str && context.IsValidForInterningString(str.Length))
                    context.ScanInternString(str);
            }
        }
    }

    /// <summary>
    /// Scans a collection item. Uses pre-cached element wrapper when runtime type matches,
    /// falls back to GetWrapper for polymorphic items.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void ScanItem<TOutput>(object item, TypeMetadataWrapper<BinarySerializeTypeMetadata>? elementWrapper, BinarySerializationContext<TOutput> context)
        where TOutput : struct, IBinaryOutputBase
    {
        var itemType = item.GetType();
        var itemWrapper = itemType == elementWrapper!.Metadata.SourceType ? elementWrapper : context.GetWrapper(itemType);

        ScanValue(item, itemWrapper, context);
    }
}
