AyCode.Core/AyCode.Core/Serializers/Binaries/AcBinaryDeserializer.Popula...

650 lines
24 KiB
C#
Raw Blame History

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
#region Populate Object Methods
[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(BinaryDeserializationContext context, object target, Type targetType, int depth)
{
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>
private static void PopulateObjectCore(
BinaryDeserializationContext context,
object target,
TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper,
int depth,
bool skipDefaultWrite)
{
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(
BinaryDeserializationContext context,
object target,
TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper,
int depth,
bool skipDefaultWrite)
{
var metadata = wrapper.Metadata;
var properties = metadata.PropertiesArray;
var cacheMap = wrapper.CacheMap;
var nextDepth = depth + 1;
var isMergeMode = context.IsMergeMode;
// 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;
if (!context.HasMetadata)
{
// Markerless loop: properties with ExpectedTypeCode read raw values directly.
// Properties without ExpectedTypeCode (bool, enum, string, object) use standard marker path.
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(context, target, propInfo, metadata, nextDepth, isMergeMode, skipDefaultWrite, i, depth);
}
}
else
{
// UseMetadata=true loop — UNCHANGED, zero extra overhead
for (int i = 0; i < propCount; i++)
{
var propInfo = cacheMap != null ? cacheMap[i] : properties[i];
PopulatePropertyWithMarker(context, target, propInfo, metadata, nextDepth, isMergeMode, skipDefaultWrite, i, depth);
}
}
}
/// <summary>
/// Standard marker-based property read. Extracted to avoid duplicating logic in both loops.
/// </summary>
private static void PopulatePropertyWithMarker(
BinaryDeserializationContext context,
object target,
BinaryPropertySetterBase? propInfo,
BinaryDeserializeTypeMetadata metadata,
int nextDepth,
bool isMergeMode,
bool skipDefaultWrite,
int propertyIndex,
int depth)
{
var peekCode = context.PeekByte();
// Nincs megfelelő target property → skip
if (propInfo == null)
{
SkipValue(context, metadata);
return;
}
// Skip marker - property has default/null value
if (peekCode == BinaryTypeCode.PropertySkip)
{
context.ReadByte(); // consume Skip marker
// Populate mode: overwrite with default (existing object may have non-default values)
// Deserialize mode: skip write (new object already has defaults from CreateInstance)
if (!skipDefaultWrite)
{
SetPropertyToDefault(target, propInfo);
}
return;
}
// Null values - always set
if (peekCode == BinaryTypeCode.Null)
{
context.ReadByte(); // consume Null marker
propInfo.SetValue(target, null);
return;
}
// Handle collections
if (peekCode == BinaryTypeCode.Array && propInfo.IsCollection)
{
var existingCollection = propInfo.GetValue(target);
if (existingCollection is IList existingList)
{
context.ReadByte(); // consume Array marker
// Merge mode with IId collection: use merge logic
if (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 ((peekCode == BinaryTypeCode.Object || peekCode == BinaryTypeCode.ObjectWithMetadata) && propInfo.IsComplexType)
{
var existingObj = propInfo.GetValue(target);
if (existingObj != null)
{
// ReadValue kezeli mindkét markert
var nestedValue = ReadValue(context, propInfo.PropertyType, nextDepth);
if (nestedValue != null)
{
var nestedMeta = context.GetWrapper(propInfo.PropertyType).Metadata;
CopyProperties(nestedValue, existingObj, nestedMeta);
}
return;
}
}
// Default: read value and set (for primitives, strings, new objects)
var positionBeforeRead = context.Position;
try
{
// Use typed setters for primitives and strings to avoid ReadValue dispatch
if (propInfo.AccessorType != PropertyAccessorType.Object &&
TryReadAndSetTypedValue(context, target, propInfo, peekCode))
return;
var value = ReadValue(context, propInfo.PropertyType, nextDepth);
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}'. " +
$"PeekCode before read: {peekCode} (0x{peekCode:X2}). " +
$"Position before read: {positionBeforeRead}, current: {context.Position}. " +
$"Depth: {depth}. " +
$"Target type: {targetType.FullName}, Assembly: {targetType.Assembly.GetName().Name} v{targetType.Assembly.GetName().Version}. " +
$"All target properties: [{string.Join(", ", 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(BinaryDeserializationContext context, object target, BinaryPropertySetterBase propInfo)
{
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;
}
}
/// <summary>
/// Called from ReadObject/ReadObjectWithMetadata for new instances.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void PopulateObject(BinaryDeserializationContext context, object target, TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper, int depth, bool skipDefaultWrite)
{
PopulateObjectPropertiesIndexed(context, target, wrapper, depth, skipDefaultWrite);
}
#endregion
#region Populate List Methods
private static void PopulateList(BinaryDeserializationContext context, IList targetList, Type listType, int depth)
{
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, nextDepth);
targetList.Add(value);
}
}
finally
{
acObservable?.EndUpdate();
}
}
/// <summary>
/// Optimized list populate that reuses existing items when possible.
/// </summary>
private static void PopulateListOptimized(BinaryDeserializationContext context, IList existingList, BinaryPropertySetterBase propInfo, int depth)
{
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++)
{
var peekCode = context.PeekByte();
// If we have an existing item at this index and the incoming is an object, reuse it
if (i < existingCount && peekCode == BinaryTypeCode.Object && elementMetadata != null)
{
var existingItem = existingList[i];
if (existingItem != null)
{
context.ReadByte(); // consume Object marker
PopulateObjectCore(context, existingItem, wrapper, nextDepth, skipDefaultWrite: false);
continue;
}
}
// Read new value
var value = ReadValue(context, elementType, nextDepth);
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(BinaryDeserializationContext context, IList existingList, BinaryPropertySetterBase propInfo, int depth)
{
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++)
{
var itemCode = context.PeekByte();
if (itemCode != BinaryTypeCode.Object)
{
var value = ReadValue(context, elementType, nextDepth);
if (value != null)
existingList.Add(value);
continue;
}
context.ReadByte(); // consume Object marker
var newItem = CreateInstance(elementType, elementMetadata);
if (newItem == null) continue;
// PopulateObjectCore handles hashcode reading for Non-IId types
PopulateObjectCore(context, newItem, wrapper, nextDepth, skipDefaultWrite: true);
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(
BinaryDeserializationContext context,
IList existingList,
Type elementType,
TypeMetadataWrapper<BinaryDeserializeTypeMetadata> wrapper,
int depth)
{
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++)
{
var itemCode = context.PeekByte();
if (itemCode != BinaryTypeCode.Object)
{
var value = ReadValue(context, elementType, nextDepth);
if (value != null)
existingList.Add(value);
continue;
}
context.ReadByte(); // consume Object marker
var newItem = CreateInstance(elementType, elementMetadata);
if (newItem == null) continue;
// PopulateObjectCore handles hashcode reading for Non-IId types
PopulateObjectCore(context, newItem, wrapper, nextDepth, skipDefaultWrite: true);
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(BinaryDeserializationContext context, object target, BinaryPropertySetterInfo propInfo, IdAccessorType idType)
{
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
}