using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using AyCode.Core.Helpers;
using AyCode.Core.Serializers;
using static AyCode.Core.Helpers.JsonUtilities;

namespace AyCode.Core.Serializers.Binaries;

public static partial class AcBinaryDeserializer
{
    #region Helper Methods
    
    // ReadAndRegisterHashcodeIfNeeded removed - new format uses position-based footer for all reference tracking.
    // No hashcode prefix in wire format anymore.
    
    #endregion

    /// <summary>
    /// Loop-invariant state for PopulatePropertyWithMarker. Built once per object, passed by ref.
    /// Reduces call-site overhead from 9 parameters to 3 (ref state + propInfo + propertyIndex).
    /// </summary>
    private ref struct PopulateState<TInput> where TInput : struct, IBinaryInputBase
    {
        public BinaryDeserializationContext<TInput> Context;
        public object Target;
        public TypeMetadataWrapper<BinaryDeserializeTypeMetadata> ParentWrapper;
        public BinaryDeserializeTypeMetadata Metadata;
        public int NextDepth;
        public int Depth;
        public bool IsMergeMode;
        public bool SkipDefaultWrite;
    }

    #region Populate Object Methods

    /// <summary>
    /// Resolves a property type wrapper using PropertyTypeWrappers cache.
    /// Falls back to GetWrapper on cache miss and populates the cache.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static TypeMetadataWrapper<BinaryDeserializeTypeMetadata> ResolvePropertyWrapper<TInput>(
        TypeMetadataWrapper<BinaryDeserializeTypeMetadata> parentWrapper,
        int complexPropertyIndex,
        Type propertyType,
        BinaryDeserializationContext<TInput> context)
        where TInput : struct, IBinaryInputBase
    {
        var cached = parentWrapper.GetPropertyTypeWrapper(complexPropertyIndex, propertyType);
        if (cached != null)
            return cached;

        var resolved = context.GetWrapper(propertyType);
        parentWrapper.SetPropertyTypeWrapper(complexPropertyIndex, resolved);
        return resolved;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static BinaryDeserializeTypeMetadata GetTypeMetadata(Type type)
        => null;//MetadataCache.GetOrAdd(type, static t => new BinaryDeserializeTypeMetadata(t));

    /// <summary>
    /// Populate object with automatic mode detection from context.
    /// Uses IsMergeMode to determine merge behavior for IId collections.
    /// Wire format:
    /// - IId types: [Object][props 0-t�l...] - no refId prefix, Id is in props
    /// - Non-IId + All: [Object][hashcode][props 0-t�l...] - hashcode prefix
    /// - Ref=Off: [Object][props 0-t�l...] - no prefix
    /// </summary>
    private static void PopulateObject<TInput>(BinaryDeserializationContext<TInput> context, object target, Type targetType, int depth)
    where TInput : struct, IBinaryInputBase
    {
        var wrapper = context.GetWrapper(targetType);

        // UseMetadata: CacheMap is built in ReadObjectWithMetadata or ReadInlineMetadataForPopulate
        PopulateObjectCore(context, target, wrapper, depth, skipDefaultWrite: false);
    }

    /// <summary>
    /// Core populate logic shared by all populate paths.
    /// Wire format: All properties are written WITH type markers (including Id for IId types).
    /// No hashcode prefix - position-based footer handles reference tracking.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void PopulateObjectCore<TInput>(
        BinaryDeserializationContext<TInput> context,
        object target,
        TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper,
        int depth,
        bool skipDefaultWrite)
    where TInput : struct, IBinaryInputBase
    {
        PopulateObjectPropertiesIndexed(context, target, wrapper, depth, skipDefaultWrite);
    }

    /// <summary>
    /// Unified property populate for both UseMetadata and non-UseMetadata modes.
    /// UseMetadata=true: cacheMap[i] gives the setter (null → skip).
    /// UseMetadata=false: properties[i] gives the setter directly.
    /// </summary>
    private static void PopulateObjectPropertiesIndexed<TInput>(BinaryDeserializationContext<TInput> context, object target, TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper, int depth, bool skipDefaultWrite)
    where TInput : struct, IBinaryInputBase
    {
        // No depth safety net on deserialize: wire format is linear + finite, the serializer-side counter
        // already prevents pathological depth in well-formed payloads. Malicious wire is out of scope.

        var metadata = wrapper.Metadata;
        var properties = metadata.PropertiesArray;
        var cacheMap = wrapper.CacheMap;

        // UseMetadata: cacheMap.Length a source property-k száma
        // Non-UseMetadata: properties.Length a target property-k száma (source == target)
        var propCount = cacheMap?.Length ?? properties.Length;

        var state = new PopulateState<TInput>
        {
            Context = context,
            Target = target,
            ParentWrapper = wrapper,
            Metadata = metadata,
            NextDepth = depth + 1,
            Depth = depth,
            IsMergeMode = context.IsMergeMode,
            SkipDefaultWrite = skipDefaultWrite
        };

        if (!context.HasMetadata || !metadata.EnableMetadataFeature)
        {
            // Markerless loop: properties with ExpectedTypeCode read raw values directly.
            // Properties without ExpectedTypeCode (bool, enum, string, object) use standard marker path.
            // Also used when EnableMetadataFeature=false on the type (per-type metadata opt-out).
            for (int i = 0; i < propCount; i++)
            {
                var propInfo = properties[i];

                if (propInfo.ExpectedTypeCode.HasValue)
                {
                    ReadAndSetMarkerlessValue(context, target, propInfo);
                    continue;
                }

                // Non-markerless properties: standard marker-based read
                PopulatePropertyWithMarker(ref state, propInfo, i);
            }
        }
        else
        {
            // UseMetadata=true loop — UNCHANGED, zero extra overhead
            for (int i = 0; i < propCount; i++)
            {
                var propInfo = cacheMap != null ? cacheMap[i] : properties[i];

                PopulatePropertyWithMarker(ref state, propInfo, i);
            }
        }
    }

    /// <summary>
    /// Standard marker-based property read. Extracted to avoid duplicating logic in both loops.
    /// Loop-invariant state is passed via ref PopulateState to reduce call-site overhead.
    /// </summary>
    private static void PopulatePropertyWithMarker<TInput>(
        ref PopulateState<TInput> state,
        BinaryPropertySetterBase? propInfo,
        int propertyIndex)
    where TInput : struct, IBinaryInputBase
    {
        var context = state.Context;
        var target = state.Target;

        // Nincs megfelelő target property → skip (SkipValue reads its own marker byte)
        if (propInfo == null)
        {
            SkipValue(context, state.Metadata);
            return;
        }

        // FastWire markerless string-property fast-path — int32 sentinel header (-1 null / 0 empty / N>0
        // content). Wire-symmetric with `WriteStringGenerated` / `WritePropertyOrSkip` String case via
        // `WriteStringUtf16Markerless`. Skips the typeCode-read entirely; reader-writer pair eliminates
        // 1 byte per content string in FastWire mode. Condition order: bool field-load (`FastWire`)
        // first → cheap short-circuit in Compact mode (most-common case in many deployments) and
        // branch-predictor-stable in FastWire mode (constant for the entire Deserialize). The
        // `AccessorType == String` enum-compare (2 instructions: load + cmp) only runs when needed.
        if (context.FastWire && propInfo.AccessorType == PropertyAccessorType.String)
        {
            propInfo.SetValue(target, context.ReadStringUtf16Markerless());
            return;
        }

        // Read marker once — eliminates redundant PeekByte + ReadByte boundary checks.
        // All branches below receive the already-consumed typeCode.
        var typeCode = context.ReadByte();

        // Skip marker - property has default/null value
        if (typeCode == BinaryTypeCode.PropertySkip)
        {
            // Populate mode: overwrite with default (existing object may have non-default values)
            // Deserialize mode: skip write (new object already has defaults from CreateInstance)
            if (!state.SkipDefaultWrite)
            {
                SetPropertyToDefault(target, propInfo);
            }
            return;
        }

        // Null values - always set
        if (typeCode == BinaryTypeCode.Null)
        {
            propInfo.SetValue(target, null);
            return;
        }

        var nextDepth = state.NextDepth;

        // Handle collections
        if (typeCode == BinaryTypeCode.Array && propInfo.IsCollection)
        {
            var existingCollection = propInfo.GetValue(target);
            if (existingCollection is IList existingList)
            {
                // Merge mode with IId collection: use merge logic
                if (state.IsMergeMode && propInfo.IsIIdCollection)
                {
                    MergeIIdCollection(context, existingList, propInfo, nextDepth);
                }
                else
                {
                    // Normal populate: replace collection contents
                    PopulateListOptimized(context, existingList, propInfo, nextDepth);
                }
                return;
            }
        }

        // Handle nested complex objects - reuse existing if available
        if ((typeCode == BinaryTypeCode.Object || typeCode == BinaryTypeCode.ObjectWithMetadata
            || typeCode == BinaryTypeCode.ObjectRefFirst || typeCode == BinaryTypeCode.ObjectWithMetadataRefFirst) && propInfo.IsComplexType)
        {
            var existingObj = propInfo.GetValue(target);
            if (existingObj != null)
            {
                // Marker already consumed → rewind so ReadValue can read it
                context._position--;
                var nestedValue = ReadValue(context, propInfo.PropertyType);
                if (nestedValue != null)
                {
                    var complexIdx = propInfo.ComplexPropertyIndex;
                    var parentWrapper = state.ParentWrapper;
                    var nestedWrapper = complexIdx >= 0
                        ? ResolvePropertyWrapper(parentWrapper, complexIdx, propInfo.PropertyType, context)
                        : context.GetWrapper(propInfo.PropertyType);
                    CopyProperties(nestedValue, existingObj, nestedWrapper.Metadata);
                }
                return;
            }
        }

        // Default: read value and set (for primitives, strings, new objects)
        var positionBeforeRead = context.Position - 1; // marker already consumed
        try
        {
            // Use typed setters for primitives and strings to avoid ReadValue dispatch.
            // typeCode is already consumed — TryReadAndSetTypedValue skips its internal ReadByte.
            if (propInfo.AccessorType != PropertyAccessorType.Object &&
                TryReadAndSetTypedValue(context, target, propInfo, typeCode))
                return;

            // Complex property with Object marker: use pre-cached wrapper to skip GetWrapper lookup
            var complexIdx = propInfo.ComplexPropertyIndex;
            if (complexIdx >= 0 && typeCode == BinaryTypeCode.Object)
            {
                // Marker already consumed — go straight to ReadObjectCoreWithWrapper
                var propWrapper = ResolvePropertyWrapper(state.ParentWrapper, complexIdx, propInfo.PropertyType, context);
                var value = ReadObjectCoreWithWrapper(context, propWrapper, cacheIndex: -1);
                propInfo.SetValue(target, value);
            }
            else
            {
                // Marker already consumed → rewind so ReadValue can read it
                context._position--;
                var value = ReadValue(context, propInfo.PropertyType);
                propInfo.SetValue(target, value);
            }
        }
        catch (InvalidCastException ex)
        {
            var targetType = target.GetType();
            throw new AcBinaryDeserializationException(
                $"Type mismatch for property '{propInfo.Name}' (index {propertyIndex}) on '{targetType.Name}'. " +
                $"Expected type: '{propInfo.PropertyType.FullName}'. " +
                $"TypeCode read: {typeCode} (0x{typeCode:X2}). " +
                $"Position before read: {positionBeforeRead}, current: {context.Position}. " +
                $"Depth: {state.Depth}. " +
                $"Target type: {targetType.FullName}, Assembly: {targetType.Assembly.GetName().Name} v{targetType.Assembly.GetName().Version}. " +
                $"All target properties: [{string.Join(", ", state.Metadata.PropertiesArray.Select(p => $"{p.Name}:{p.PropertyType.Name}"))}]. " +
                $"Error: {ex.Message}",
                positionBeforeRead,
                propInfo.PropertyType,
                ex);
        }
    }

    /// <summary>
    /// Reads a raw value without type marker from stream (markerless mode, UseMetadata=false).
    /// The property's type is known from metadata — no type code in the stream.
    /// Only called for non-nullable value types with ExpectedTypeCode set.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void ReadAndSetMarkerlessValue<TInput>(BinaryDeserializationContext<TInput> context, object target, BinaryPropertySetterBase propInfo)
    where TInput : struct, IBinaryInputBase
    {
        switch (propInfo.AccessorType)
        {
            case PropertyAccessorType.Int32:
                propInfo.SetInt32(target, context.ReadVarInt());
                return;
            case PropertyAccessorType.Int64:
                propInfo.SetInt64(target, context.ReadVarLong());
                return;
            case PropertyAccessorType.Double:
                propInfo.SetDouble(target, context.ReadDoubleUnsafe());
                return;
            case PropertyAccessorType.Single:
                propInfo.SetSingle(target, context.ReadSingleUnsafe());
                return;
            case PropertyAccessorType.Decimal:
                propInfo.SetDecimal(target, context.ReadDecimalUnsafe());
                return;
            case PropertyAccessorType.DateTime:
                propInfo.SetDateTime(target, context.ReadDateTimeUnsafe());
                return;
            case PropertyAccessorType.Guid:
                propInfo.SetGuid(target, context.ReadGuidUnsafe());
                return;
            case PropertyAccessorType.Byte:
                propInfo.SetByte(target, context.ReadByte());
                return;
            case PropertyAccessorType.Int16:
                propInfo.SetInt16(target, context.ReadInt16Unsafe());
                return;
            case PropertyAccessorType.UInt16:
                propInfo.SetUInt16(target, context.ReadUInt16Unsafe());
                return;
            case PropertyAccessorType.UInt32:
                propInfo.SetUInt32(target, context.ReadVarUInt());
                return;
            case PropertyAccessorType.UInt64:
                propInfo.SetUInt64(target, context.ReadVarULong());
                return;
            case PropertyAccessorType.Boolean:
                propInfo.SetBoolean(target, context.ReadByte() != 0);
                return;
            case PropertyAccessorType.Enum:
                propInfo.SetEnumAsInt32(target, context.ReadVarInt());
                return;
        }
    }

    /// <summary>
    /// Called from ReadObject/ReadObjectWithMetadata for new instances.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void PopulateObject<TInput>(BinaryDeserializationContext<TInput> context, object target, TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper, bool skipDefaultWrite)
    where TInput : struct, IBinaryInputBase
    {
        // Bridge: PopulateObjectPropertiesIndexed still has int depth in signature (other callers use it).
        // Pass 0 as placeholder; depth-cleanup of PopulateObjectPropertiesIndexed + its other callers is pending.
        PopulateObjectPropertiesIndexed(context, target, wrapper, 0, skipDefaultWrite);
    }

    #endregion

    #region Populate List Methods

    private static void PopulateList<TInput>(BinaryDeserializationContext<TInput> context, IList targetList, Type listType, int depth)
    where TInput : struct, IBinaryInputBase
    {
        var elementType = GetCollectionElementType(listType) ?? typeof(object);

        var acObservable = targetList as IAcObservableCollection;
        acObservable?.BeginUpdate();

        try
        {
            targetList.Clear();

            var count = (int)context.ReadVarUInt();
            var nextDepth = depth + 1;

            for (int i = 0; i < count; i++)
            {
                // ReadValue handles ChainMode internally (ReadObject returns cached instance)
                var value = ReadValue(context, elementType);
                targetList.Add(value);
            }
        }
        finally
        {
            acObservable?.EndUpdate();
        }
    }

    /// <summary>
    /// Optimized list populate that reuses existing items when possible.
    /// </summary>
    private static void PopulateListOptimized<TInput>(BinaryDeserializationContext<TInput> context, IList existingList, BinaryPropertySetterBase propInfo, int depth)
    where TInput : struct, IBinaryInputBase
    {
        var elementType = propInfo.ElementType ?? typeof(object);
        var count = (int)context.ReadVarUInt();
        var nextDepth = depth + 1;

        var acObservable = existingList as IAcObservableCollection;
        acObservable?.BeginUpdate();

        try
        {
            var existingCount = existingList.Count;
            
            // Early exit if empty source - just clear destination
            if (count == 0)
            {
                existingList.Clear();
                return;
            }
            
            var wrapper = context.GetWrapper(elementType);
            var elementMetadata = wrapper.Metadata.IsComplexType ? wrapper.Metadata : null;

            for (int i = 0; i < count; i++)
            {
                // Read marker once — eliminates redundant PeekByte + ReadByte boundary checks
                var typeCode = context.ReadByte();

                // ObjectRefFirst: read cacheIndex before processing the object body
                var cacheIndex = -1;
                if (typeCode == BinaryTypeCode.ObjectRefFirst && elementMetadata != null)
                {
                    cacheIndex = (int)context.ReadVarUInt();
                    // Treat as Object from here on — same wire body, just with cacheIndex prefix
                    typeCode = BinaryTypeCode.Object;
                }

                // If we have an existing item at this index and the incoming is an object, reuse it
                if (i < existingCount && typeCode == BinaryTypeCode.Object && elementMetadata != null)
                {
                    var existingItem = existingList[i];
                    if (existingItem != null)
                    {
                        if (cacheIndex >= 0)
                            context.RegisterInternedValueAt(cacheIndex, existingItem);
                        PopulateObjectPropertiesIndexed(context, existingItem, wrapper, nextDepth, skipDefaultWrite: false);
                        continue;
                    }
                }

                // Read new value — use pre-resolved wrapper for Object elements to skip GetWrapper dictionary lookup
                object? value;
                if (typeCode == BinaryTypeCode.Object && elementMetadata != null)
                {
                    value = ReadObjectCoreWithWrapper(context, wrapper, cacheIndex: cacheIndex);
                }
                else
                {
                    // Marker already consumed → rewind so ReadValue can read it
                    context._position--;
                    value = ReadValue(context, elementType);
                }

                if (i < existingCount)
                {
                    existingList[i] = value;
                }
                else
                {
                    existingList.Add(value);
                }
            }

            // Remove extra items if new list is shorter
            while (existingList.Count > count)
            {
                existingList.RemoveAt(existingList.Count - 1);
            }
        }
        finally
        {
            acObservable?.EndUpdate();
        }
    }

    #endregion

    #region Merge Methods

    /// <summary>
    /// IId collection merge using cached property info.
    /// Matches items by Id, updates existing, adds new, optionally removes orphans.
    /// </summary>
    private static void MergeIIdCollection<TInput>(BinaryDeserializationContext<TInput> context, IList existingList, BinaryPropertySetterBase propInfo, int depth)
    where TInput : struct, IBinaryInputBase
    {
        var elementType = propInfo.ElementType!;
        var idGetter = propInfo.ElementIdGetter!;
        var idType = propInfo.ElementIdType!;

        var count = existingList.Count;
        var acObservable = existingList as IAcObservableCollection;
        acObservable?.BeginUpdate();

        try
        {
            // Build lookup dictionary with capacity hint
            Dictionary<object, object>? existingById = null;
            if (count > 0)
            {
                existingById = new Dictionary<object, object>(count);
                for (var idx = 0; idx < count; idx++)
                {
                    var item = existingList[idx];
                    if (item != null)
                    {
                        var id = idGetter(item);
                        if (id != null && !IsDefaultValue(id, idType))
                            existingById[id] = item;
                    }
                }
            }

            var arrayCount = (int)context.ReadVarUInt();
            var nextDepth = depth + 1;
            var wrapper = context.GetWrapper(elementType);
            var elementMetadata = wrapper.Metadata;
            
            // Track which IDs we see in source (for orphan removal)
            HashSet<object>? sourceIds = context.RemoveOrphanedItems && existingById != null 
                ? new HashSet<object>(arrayCount) 
                : null;

            for (int i = 0; i < arrayCount; i++)
            {
                // Read marker once — eliminates redundant PeekByte + ReadByte boundary checks
                var itemCode = context.ReadByte();

                // Read or create the new item
                object? newItem;
                if (itemCode == BinaryTypeCode.Object)
                {
                    // Fast path: use pre-resolved wrapper, skip GetWrapper dictionary lookup
                    newItem = CreateInstance(elementType, elementMetadata);
                    if (newItem == null) continue;
                    PopulateObjectPropertiesIndexed(context, newItem, wrapper, nextDepth, skipDefaultWrite: true);
                }
                else
                {
                    // Marker already consumed → rewind so ReadValue can read it
                    context._position--;
                    newItem = ReadValue(context, elementType);
                    if (newItem == null) continue;
                }

                var itemId = idGetter(newItem);
                if (itemId != null && !IsDefaultValue(itemId, idType))
                {
                    // Track this ID as seen in source
                    sourceIds?.Add(itemId);

                    if (existingById != null && existingById.TryGetValue(itemId, out var existingItem))
                    {
                        // Copy properties to existing item (preserves reference)
                        CopyProperties(newItem, existingItem, elementMetadata);
                        continue;
                    }
                }

                existingList.Add(newItem);
            }

            // Remove orphaned items (items in destination but not in source)
            if (context.RemoveOrphanedItems && existingById != null && sourceIds != null)
            {
                var itemsToRemove = new List<object>();
                foreach (var kvp in existingById)
                {
                    if (!sourceIds.Contains(kvp.Key))
                    {
                        itemsToRemove.Add(kvp.Value);
                    }
                }

                foreach (var item in itemsToRemove)
                {
                    existingList.Remove(item);
                }
            }
        }
        finally
        {
            acObservable?.EndUpdate();
        }
    }

    /// <summary>
    /// IId collection merge using type metadata (for top-level list merge).
    /// </summary>
    private static void MergeIIdCollectionWithMetadata<TInput>(
        BinaryDeserializationContext<TInput> context, 
        IList existingList, 
        Type elementType,
        TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper,
        int depth)
    where TInput : struct, IBinaryInputBase
    {
        var elementMetadata = wrapper.Metadata;
        var idGetter = elementMetadata.IdGetter!;
        var idType = elementMetadata.IdType!;

        var count = existingList.Count;
        var acObservable = existingList as IAcObservableCollection;
        acObservable?.BeginUpdate();

        try
        {
            // Build lookup dictionary with capacity hint
            Dictionary<object, object>? existingById = null;
            if (count > 0)
            {
                existingById = new Dictionary<object, object>(count);
                for (var idx = 0; idx < count; idx++)
                {
                    var item = existingList[idx];
                    if (item != null)
                    {
                        var id = idGetter(item);
                        if (id != null && !IsDefaultValue(id, idType))
                            existingById[id] = item;
                    }
                }
            }

            var arrayCount = (int)context.ReadVarUInt();
            var nextDepth = depth + 1;

            // Track which IDs we see in source (for orphan removal)
            HashSet<object>? sourceIds = context.RemoveOrphanedItems && existingById != null 
                ? new HashSet<object>(arrayCount) 
                : null;

            for (int i = 0; i < arrayCount; i++)
            {
                // Read marker once — eliminates redundant PeekByte + ReadByte boundary checks
                var itemCode = context.ReadByte();

                // Read or create the new item
                object? newItem;
                if (itemCode == BinaryTypeCode.Object)
                {
                    // Fast path: use pre-resolved wrapper, skip GetWrapper dictionary lookup
                    newItem = CreateInstance(elementType, elementMetadata);
                    if (newItem == null) continue;
                    PopulateObjectPropertiesIndexed(context, newItem, wrapper, nextDepth, skipDefaultWrite: true);
                }
                else
                {
                    // Marker already consumed → rewind so ReadValue can read it
                    context._position--;
                    newItem = ReadValue(context, elementType);
                    if (newItem == null) continue;
                }

                var itemId = idGetter(newItem);
                if (itemId != null && !IsDefaultValue(itemId, idType))
                {
                    // Track this ID as seen in source
                    sourceIds?.Add(itemId);

                    if (existingById != null && existingById.TryGetValue(itemId, out var existingItem))
                    {
                        // Copy properties to existing item (preserves reference)
                        CopyProperties(newItem, existingItem, elementMetadata);
                        continue;
                    }
                }

                existingList.Add(newItem);
            }

            // Remove orphaned items (items in destination but not in source)
            if (context.RemoveOrphanedItems && existingById != null && sourceIds != null)
            {
                var itemsToRemove = new List<object>();
                foreach (var kvp in existingById)
                {
                    if (!sourceIds.Contains(kvp.Key))
                    {
                        itemsToRemove.Add(kvp.Value);
                    }
                }

                foreach (var item in itemsToRemove)
                {
                    existingList.Remove(item);
                }
            }
        }
        finally
        {
            acObservable?.EndUpdate();
        }
    }

    private static void CopyProperties(object source, object target, BinaryDeserializeTypeMetadata metadata)
    {
        var props = metadata.PropertiesArray;
        for (var i = 0; i < props.Length; i++)
        {
            var prop = props[i];
            var value = prop.GetValue(source);
            if (value != null)
                prop.SetValue(target, value);
        }
    }
    
    /// <summary>
    /// Reads Id value without type marker. The serializer didn't write a marker for IId types.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void ReadIdValueWithoutMarker<TInput>(BinaryDeserializationContext<TInput> context, object target, BinaryPropertySetterInfo propInfo, IdAccessorType idType)
    where TInput : struct, IBinaryInputBase
    {
        switch (idType)
        {
            case IdAccessorType.Int32:
                propInfo.SetInt32(target, context.ReadVarInt());
                break;
            case IdAccessorType.Int64:
                propInfo.SetInt64(target, context.ReadVarLong());
                break;
            case IdAccessorType.Guid:
                propInfo.SetGuid(target, context.ReadGuidUnsafe());
                break;
        }
    }

    /// <summary>
    /// Sets a property to its default value using typed setters to avoid boxing.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void SetPropertyToDefault(object target, BinaryPropertySetterBase propInfo)
        => propInfo.SetToDefault(target);

    /// <summary>
    /// Determines if a type is a complex type (not primitive, string, or simple value type).
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static bool IsComplexType5(Type type)
    {
        if (type.IsPrimitive) return false;
        if (ReferenceEquals(type, StringType)) return false;
        if (type.IsEnum) return false;
        if (ReferenceEquals(type, GuidType)) return false;
        if (ReferenceEquals(type, DateTimeType)) return false;
        if (ReferenceEquals(type, DecimalType)) return false;
        if (ReferenceEquals(type, TimeSpanType)) return false;
        if (ReferenceEquals(type, DateTimeOffsetType)) return false;
        if (Nullable.GetUnderlyingType(type) != null) return false;
        return true;
    }

    #endregion
}
