using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
namespace Uigenx.Binding
{
/// One bindable path and its studio data type.
public readonly struct UgxSchemaEntry
{
public readonly string Path;
/// One of the studio data types: string, int, float, bool, color, vector2, vector3, sprite, enum, object, array.
public readonly string Type;
public readonly bool Writable;
public UgxSchemaEntry(string path, string type, bool writable)
{
Path = path;
Type = type;
Writable = writable;
}
}
///
/// Reflects a data source type into the flat path list the studio binds against. Shared by
/// and the editor schema reflector so both report the same
/// shape. Bounded by depth, entry count and a cycle guard.
///
public static class UgxSchemaWalker
{
public const int DefaultMaxDepth = 6;
public const int DefaultMaxEntries = 2048;
const BindingFlags Members = BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly;
public static List Collect(Type type, int maxDepth = DefaultMaxDepth,
int maxEntries = DefaultMaxEntries)
{
var result = new List(32);
if (type == null) return result;
var seen = new HashSet();
Walk(type, null, result, seen, maxDepth, maxEntries);
return result;
}
/// Studio data type name for a CLR type. Unknown reference types become `object`.
public static string TypeName(Type type)
{
if (type == null) return "object";
var underlying = Nullable.GetUnderlyingType(type) ?? type;
if (underlying.IsEnum) return "enum";
if (underlying == typeof(string)) return "string";
if (underlying == typeof(bool)) return "bool";
if (underlying == typeof(float) || underlying == typeof(double) || underlying == typeof(decimal))
return "float";
if (underlying == typeof(byte) || underlying == typeof(sbyte) || underlying == typeof(short) ||
underlying == typeof(ushort) || underlying == typeof(int) || underlying == typeof(uint) ||
underlying == typeof(long) || underlying == typeof(ulong)) return "int";
if (underlying == typeof(Color) || underlying == typeof(Color32)) return "color";
if (underlying == typeof(Vector2) || underlying == typeof(Vector2Int)) return "vector2";
if (underlying == typeof(Vector3) || underlying == typeof(Vector3Int)) return "vector3";
if (typeof(Sprite).IsAssignableFrom(underlying) || typeof(Texture).IsAssignableFrom(underlying))
return "sprite";
if (IsCollection(underlying)) return "array";
return "object";
}
public static bool IsCollection(Type type)
=> type != null && type != typeof(string) && typeof(IEnumerable).IsAssignableFrom(type);
public static Type ElementType(Type type)
{
if (type == null) return null;
if (type.IsArray) return type.GetElementType();
foreach (var i in type.GetInterfaces())
{
if (i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>))
return i.GetGenericArguments()[0];
}
return typeof(object);
}
static void Walk(Type type, string prefix, List result, HashSet stack, int depth,
int maxEntries)
{
if (depth <= 0 || result.Count >= maxEntries) return;
if (!stack.Add(type)) return;
foreach (var member in EnumerateMembers(type))
{
if (result.Count >= maxEntries) break;
var attribute = member.Info.GetCustomAttribute();
if (attribute != null && attribute.Ignore) continue;
var segment = attribute?.Path ?? Camel(member.Info.Name);
var path = prefix == null ? segment : prefix + "." + segment;
var typeName = TypeName(member.ValueType);
result.Add(new UgxSchemaEntry(path, typeName, member.Writable));
if (typeName == "array")
{
var element = ElementType(member.ValueType);
var elementTypeName = TypeName(element);
var indexed = path + "[0]";
result.Add(new UgxSchemaEntry(indexed, elementTypeName, false));
if (elementTypeName == "object") Walk(element, indexed, result, stack, depth - 1, maxEntries);
}
else if (typeName == "object")
{
Walk(member.ValueType, path, result, stack, depth - 1, maxEntries);
}
}
stack.Remove(type);
}
readonly struct Member
{
public readonly MemberInfo Info;
public readonly Type ValueType;
public readonly bool Writable;
public Member(MemberInfo info, Type valueType, bool writable)
{
Info = info;
ValueType = valueType;
Writable = writable;
}
}
static IEnumerable EnumerateMembers(Type type)
{
for (var current = type; current != null && current != typeof(object); current = current.BaseType)
{
// The source's own members are always walked, but climbing into MonoBehaviour or
// ScriptableObject would report name, tag, transform and gameObject and then
// recurse through the whole Unity object graph, burying the real fields.
if (current != type && typeof(UnityEngine.Object).IsAssignableFrom(current)) yield break;
foreach (var field in current.GetFields(Members))
{
if (field.IsStatic || field.IsInitOnly) continue;
yield return new Member(field, field.FieldType, true);
}
foreach (var property in current.GetProperties(Members))
{
if (property.GetIndexParameters().Length > 0) continue;
var getter = property.GetGetMethod(false);
if (getter == null || getter.IsStatic) continue;
yield return new Member(property, property.PropertyType, property.GetSetMethod(false) != null);
}
}
}
static string Camel(string name)
{
if (string.IsNullOrEmpty(name)) return name;
if (name.Length > 2 && name[0] == 'm' && name[1] == '_') name = name.Substring(2);
if (name.Length == 0 || char.IsLower(name[0])) return name;
return char.ToLowerInvariant(name[0]) + name.Substring(1);
}
}
}