using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Reflection;
using UnityEngine;
using UnityEngine.Analytics;
using UnityEngine.Pool;
namespace UnityEditor.Rendering
{
///
/// Set of utilities for analytics
///
public static class AnalyticsUtils
{
const string k_VendorKey = "unity.srp";
internal static void SendData(IAnalytic analytic)
{
EditorAnalytics.SendAnalytic(analytic);
}
///
/// Gets a list of the serializable fields of the given type
///
/// The type to get fields that are serialized.
/// If obsolete fields are taken into account
/// The collection of that are serialized for this type
public static IEnumerable GetSerializableFields(this Type type, bool removeObsolete = false)
{
var members = type.GetMembers(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic);
if (type.BaseType != null && type.BaseType != typeof(object))
{
foreach (FieldInfo field in type.BaseType.GetSerializableFields())
{
yield return field;
}
}
foreach (var member in members)
{
if (member.MemberType != MemberTypes.Field && member.MemberType != MemberTypes.Property)
{
continue;
}
if (member.DeclaringType != type || member is not FieldInfo field)
{
continue;
}
if (removeObsolete && member.GetCustomAttribute() != null)
continue;
if (field.IsPublic)
{
if (member.GetCustomAttribute() != null)
continue;
yield return field;
}
else
{
if (member.GetCustomAttribute() != null)
yield return field;
}
}
}
static bool AreArraysDifferent(IList a, IList b)
{
if ((a == null) && (b == null))
return false;
if ((a == null) ^ (b == null))
return true;
if (a.Count != b.Count)
return true;
for (int i = 0; i < a.Count; i++)
{
if (!a[i].Equals(b[i]))
return true;
}
return false;
}
static string DumpValues(this IList list)
{
using (ListPool.Get(out var tempList))
{
for (int i = 0; i < list.Count; i++)
{
tempList.Add(list[i] != null ? list[i].ToString() : "null");
}
var arrayValues = string.Join(",", tempList);
return $"[{arrayValues}]";
}
}
static Dictionary DumpValues(Type type, object current)
{
var diff = new Dictionary();
foreach (var field in type.GetSerializableFields(removeObsolete: true))
{
var t = field.FieldType;
try
{
if (typeof(ScriptableObject).IsAssignableFrom(t))
continue;
var valueCurrent = current != null ? field.GetValue(current) : null;
if (t == typeof(string))
{
var stringCurrent = (string)valueCurrent;
diff[field.Name] = stringCurrent;
}
else if (t.IsPrimitive || t.IsEnum)
{
diff[field.Name] = ConvertPrimitiveWithInvariants(valueCurrent);
}
else if (t.IsArray && valueCurrent is IList valueCurrentList)
{
diff[field.Name] = valueCurrentList.DumpValues();
}
else if (t.IsClass || t.IsValueType)
{
if (valueCurrent is IEnumerable ea)
continue; // List not supported
var subDiff = DumpValues(t, valueCurrent);
foreach (var d in subDiff)
{
diff[field.Name + "." + d.Key] = d.Value;
}
}
}
catch (Exception ex)
{
Debug.LogError($"Exception found while parsing {field}, {ex}");
}
}
return diff;
}
static Dictionary GetDiffAsDictionary(Type type, object current, object defaults)
{
var diff = new Dictionary();
foreach (var field in type.GetSerializableFields())
{
var fieldType = field.FieldType;
if (!IsFieldIgnored(fieldType))
AddDiff(current, defaults, field, fieldType, diff);
}
return diff;
}
private static void AddDiff(object current, object defaults, FieldInfo field, Type fieldType, Dictionary diff)
{
try
{
var valueCurrent = current != null ? field.GetValue(current) : null;
var valueDefault = defaults != null ? field.GetValue(defaults) : null;
AddIfDifferent(field, fieldType, diff, valueCurrent, valueDefault);
}
catch (Exception ex)
{
Debug.LogError($"Exception found while parsing {field}, {ex}");
}
}
private static void AddIfDifferent(FieldInfo field, Type fieldType, Dictionary diff, object valueCurrent, object valueDefault)
{
if (!AreValuesEqual(fieldType, valueCurrent, valueDefault))
{
if (IsComplexType(fieldType))
{
var subDiff = GetDiffAsDictionary(fieldType, valueCurrent, valueDefault);
foreach (var d in subDiff)
{
diff[$"{field.Name}.{d.Key}"] = d.Value;
}
}
else
{
diff[field.Name] = ConvertValueToString(valueCurrent);
}
}
}
static bool IsFieldIgnored(Type fieldType)
{
return fieldType.GetCustomAttribute() != null || typeof(ScriptableObject).IsAssignableFrom(fieldType);
}
internal static bool AreValuesEqual(Type fieldType, object valueCurrent, object valueDefault)
{
if (fieldType == typeof(string))
return (string)valueCurrent == (string)valueDefault;
if (fieldType.IsPrimitive || fieldType.IsEnum)
return valueCurrent.Equals(valueDefault);
if (fieldType.IsArray && valueCurrent is IList currentList)
return !AreArraysDifferent(currentList, valueDefault as IList);
if (valueCurrent == null && valueDefault == null)
return true;
return valueDefault?.Equals(valueCurrent) ?? valueCurrent?.Equals(null) ?? false;
}
internal static bool IsComplexType(Type fieldType)
{
// Primitive types and enums are not considered complex
if (fieldType.IsPrimitive || fieldType.IsEnum)
return false;
// String is considered a primitive type for our purposes
if (fieldType == typeof(string))
return false;
// Arrays can be converted to string easy without sub-elements
if (fieldType.IsArray)
return false;
// Value types (structs) that are not primitive are considered complex
// Classes are considered complex types
return fieldType.IsValueType || fieldType.IsClass;
}
static string ConvertValueToString(object value)
{
if (value == null) return null;
if (value is IList list) return list.DumpValues();
return ConvertPrimitiveWithInvariants(value);
}
static string ConvertPrimitiveWithInvariants(object obj)
{
if (obj is IConvertible convertible)
return convertible.ToString(CultureInfo.InvariantCulture);
return obj.ToString();
}
static string[] ToStringArray(Dictionary diff, string format = null)
{
var changedSettings = new string[diff.Count];
if (string.IsNullOrEmpty(format))
format = @"{{""{0}"":""{1}""}}";
int i = 0;
foreach (var d in diff)
changedSettings[i++] = string.Format(format, d.Key, d.Value);
return changedSettings;
}
private static string[] EnumerableToNestedColumn([DisallowNull] this IEnumerable collection)
{
using (ListPool.Get(out var tmp))
{
foreach (var element in collection)
{
string[] elementColumns = ToStringArray(DumpValues(element.GetType(), element), @"""{0}"":""{1}""");
tmp.Add("{" + string.Join(", ", elementColumns) + "}");
}
return tmp.ToArray();
}
}
private static string[] ToNestedColumnSimplify([DisallowNull] this T current)
where T : new()
{
var type = current.GetType();
if (typeof(UnityEngine.Object).IsAssignableFrom(typeof(T)))
{
var instance = ScriptableObject.CreateInstance(type);
ToStringArray(GetDiffAsDictionary(type, current, instance));
ScriptableObject.DestroyImmediate(instance);
}
return ToStringArray(GetDiffAsDictionary(type, current, new T()));
}
///
/// Obtains the Serialized fields and values in form of nested columns for BigQuery
/// https://cloud.google.com/bigquery/docs/nested-repeated
///
/// The given type
/// The current object to obtain the fields and values.
/// If a comparison against the default value must be done.
/// The nested columns in form of {key.nestedKey : value}
/// Throws an exception if current parameter is null.
public static string[] ToNestedColumn([DisallowNull] this T current, bool compareAndSimplifyWithDefault = false)
where T : new()
{
if (current == null)
throw new ArgumentNullException(nameof(current));
if (current is IEnumerable currentAsEnumerable)
return EnumerableToNestedColumn(currentAsEnumerable);
if (compareAndSimplifyWithDefault)
return ToNestedColumnSimplify(current);
return ToStringArray(DumpValues(current.GetType(), current));
}
///
/// Obtains the Serialized fields and values in form of nested columns for BigQuery
/// https://cloud.google.com/bigquery/docs/nested-repeated
///
/// The given type
/// The current object to obtain the fields and values.
/// The default instance to compare values
/// The nested columns in form of {key.nestedKey : value}
/// Throws an exception if the current or defaultInstance parameters are null.
public static string[] ToNestedColumn([DisallowNull] this T current, T defaultInstance)
{
if (current == null)
throw new ArgumentNullException(nameof(current));
if (defaultInstance == null)
throw new ArgumentNullException(nameof(defaultInstance));
var type = current.GetType();
Dictionary diff = GetDiffAsDictionary(type, current, defaultInstance);
return ToStringArray(diff);
}
///
/// Obtains the Serialized fields and values in form of nested columns for BigQuery
/// https://cloud.google.com/bigquery/docs/nested-repeated
///
/// The given type
/// The current object to obtain the fields and values.
/// The default object
/// If a comparison against the default value must be done.
/// The nested columns in form of {key.nestedKey : value}
/// Throws an exception if the current parameter is null.
public static string[] ToNestedColumnWithDefault([DisallowNull] this T current, [DisallowNull] T defaultObject, bool compareAndSimplifyWithDefault = false)
{
if (current == null)
throw new ArgumentNullException(nameof(current));
var type = current.GetType();
Dictionary diff = (compareAndSimplifyWithDefault) ?
GetDiffAsDictionary(type, current, defaultObject) : DumpValues(type, current);
return ToStringArray(diff);
}
}
}