using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace Tango.Telemetry
{
internal static class DateTimeUtcFixer
{
///
/// Recursively scans the object graph starting at and
/// sets Kind=Utc on any DateTime/DateTime? properties or collection elements that are Kind=Unspecified.
/// Uses DateTime.SpecifyKind(value, DateTimeKind.Utc) (no clock adjustment).
/// Notes:
/// - Only properties are updated (not fields).
/// - Collections: updates elements for IList/arrays and dictionary values; traverses other IEnumerable to reach nested objects.
/// - Read-only properties (no setter) are skipped.
/// - Root objects that are a DateTime value cannot be changed in place (struct); wrap them in a container if needed.
///
public static void EnsureDateTimeUTC(object obj)
{
var visited = new HashSet(ReferenceEqualityComparer.Instance);
EnsureUtcInternal(obj, visited);
}
private static void EnsureUtcInternal(object obj, HashSet visited)
{
if (obj == null) return;
var type = obj.GetType();
// Avoid revisiting reference objects (handle cycles, EF proxies, etc.)
if (!type.IsValueType) // only track reference types
{
if (!visited.Add(obj)) return;
}
// Handle dictionaries: update values, recurse into objects
if (obj is IDictionary dict)
{
var keys = new List();
foreach (var k in dict.Keys) keys.Add(k);
foreach (var key in keys)
{
var value = dict[key];
if (TrySpecifyUtcOnBoxedDateTime(ref value))
{
dict[key] = value;
}
else
{
EnsureUtcInternal(value, visited);
}
}
return;
}
// Handle lists/arrays: update elements, recurse into objects
if (obj is IList list)
{
for (int i = 0; i < list.Count; i++)
{
var element = list[i];
if (TrySpecifyUtcOnBoxedDateTime(ref element))
{
list[i] = element;
}
else
{
EnsureUtcInternal(element, visited);
}
}
return;
}
// Traverse other enumerables just to reach nested objects (cannot replace DateTime elements)
if (obj is IEnumerable enumerable && !(obj is string))
{
foreach (var element in enumerable)
{
EnsureUtcInternal(element, visited);
}
}
// Inspect properties
const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
foreach (var prop in type.GetProperties(flags))
{
// indexers not supported
if (prop.GetIndexParameters().Length != 0) continue;
// must be readable
if (!prop.CanRead) continue;
var pType = prop.PropertyType;
// DateTime
if (pType == typeof(DateTime))
{
var val = (DateTime)prop.GetValue(obj);
if (val.Kind == DateTimeKind.Unspecified)
{
var newVal = DateTime.SpecifyKind(val, DateTimeKind.Utc);
TrySetProperty(obj, prop, newVal);
}
continue;
}
// Nullable
var underlying = Nullable.GetUnderlyingType(pType);
if (underlying == typeof(DateTime))
{
var val = (DateTime?)prop.GetValue(obj);
if (val.HasValue && val.Value.Kind == DateTimeKind.Unspecified)
{
var newVal = (DateTime?)DateTime.SpecifyKind(val.Value, DateTimeKind.Utc);
TrySetProperty(obj, prop, newVal);
}
continue;
}
// Skip obvious simple types
if (IsSimple(pType)) continue;
// Recurse into complex objects
var child = prop.GetValue(obj);
EnsureUtcInternal(child, visited);
}
}
private static bool TrySpecifyUtcOnBoxedDateTime(ref object obj)
{
if (obj == null) return false;
var t = obj.GetType();
// Exact DateTime
if (t == typeof(DateTime))
{
var dt = (DateTime)obj;
if (dt.Kind == DateTimeKind.Unspecified)
{
obj = DateTime.SpecifyKind(dt, DateTimeKind.Utc);
}
return true;
}
// Nullable boxed
var underlying = Nullable.GetUnderlyingType(t);
if (underlying == typeof(DateTime))
{
// boxed Nullable can be unboxed to DateTime? safely
var ndt = (DateTime?)obj;
if (ndt.HasValue && ndt.Value.Kind == DateTimeKind.Unspecified)
{
obj = (DateTime?)DateTime.SpecifyKind(ndt.Value, DateTimeKind.Utc);
}
return true;
}
return false;
}
private static bool TrySetProperty(object target, PropertyInfo prop, object value)
{
try
{
// Prefer invoking the setter (even if non-public)
var setter = prop.GetSetMethod(nonPublic: true);
if (setter == null) return false;
setter.Invoke(target, new[] { value });
return true;
}
catch
{
// Fall back to PropertyInfo.SetValue (may still work in some runtimes)
try
{
prop.SetValue(target, value);
return true;
}
catch
{
return false;
}
}
}
private static bool IsSimple(Type t)
{
if (t.IsPrimitive || t.IsEnum) return true;
if (t == typeof(string) || t == typeof(decimal) || t == typeof(Guid) ||
t == typeof(Uri) || t == typeof(TimeSpan) || t == typeof(DateTimeOffset))
return true;
// Value types other than DateTime/Nullable are considered simple
return t.IsValueType;
}
/// Reference equality comparer for the visited set.
private sealed class ReferenceEqualityComparer : IEqualityComparer
{
public static readonly ReferenceEqualityComparer Instance = new ReferenceEqualityComparer();
public new bool Equals(object x, object y) => ReferenceEquals(x, y);
public int GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj);
}
}
}