using System;
using System.Collections.Generic;
using Unity.Properties;
using UnityEngine;
using UnityEngine.UIElements;
namespace Uigenx.Binding
{
///
/// Exposes a plain object through Unity.Properties. Unity builds a reflected property
/// bag for the target the first time a path resolves, so no attribute or code generation is
/// required for public members. Changes are found by polling, because a POCO cannot announce
/// them; a source that can should implement directly or derive
/// from .
///
public sealed class UgxReflectiveDataSource : IUgxDataSource, INotifyBindablePropertyChanged,
IDataSourceViewHashProvider
{
readonly object _target;
readonly List _watched = new List();
readonly Dictionary _last = new Dictionary(StringComparer.Ordinal);
double _nextPollTime;
long _version;
/// Polls per second. Zero or less polls on every call.
public float Hz { get; set; } = 20f;
public UgxReflectiveDataSource(object target)
{
_target = target ?? throw new ArgumentNullException(nameof(target));
}
public object Target => _target;
public event EventHandler propertyChanged;
public event Action PathChanged;
public long GetViewHashCode() => _version;
public IEnumerable Paths
{
get
{
foreach (var entry in UgxSchemaWalker.Collect(_target.GetType())) yield return entry.Path;
}
}
public bool TryGet(string path, out object value)
{
if (string.IsNullOrEmpty(path))
{
value = null;
return false;
}
var container = _target;
return PropertyContainer.TryGetValue(ref container, UgxPropertyPaths.Get(path), out value);
}
public bool TrySet(string path, object value)
{
if (string.IsNullOrEmpty(path)) return false;
var container = _target;
if (!PropertyContainer.TrySetValue(ref container, UgxPropertyPaths.Get(path), value)) return false;
Raise(path, value);
return true;
}
/// Adds a path to the polled set. Only watched paths raise change notifications.
public void Watch(string path)
{
if (string.IsNullOrEmpty(path) || _watched.Contains(path)) return;
_watched.Add(path);
if (TryGet(path, out var value)) _last[path] = value;
}
public void Unwatch(string path)
{
if (!_watched.Remove(path)) return;
_last.Remove(path);
}
public void UnwatchAll()
{
_watched.Clear();
_last.Clear();
}
///
/// Compares every watched path against its last value and raises the changed ones. Honours
/// so callers can drive it from an unconditional per frame update.
///
public void Poll(double now)
{
if (Hz > 0f)
{
if (now < _nextPollTime) return;
_nextPollTime = now + 1d / Hz;
}
for (int i = 0; i < _watched.Count; i++)
{
var path = _watched[i];
if (!TryGet(path, out var value)) continue;
if (_last.TryGetValue(path, out var previous) && Equals(previous, value)) continue;
_last[path] = value;
Raise(path, value);
}
}
public void Poll() => Poll(Time.unscaledTimeAsDouble);
void Raise(string path, object value)
{
_last[path] = value;
_version++;
try
{
PathChanged?.Invoke(path);
}
catch (Exception e)
{
Debug.LogError("[ugx] a data source listener threw for '" + path + "': " + e);
}
try
{
propertyChanged?.Invoke(this, new BindablePropertyChangedEventArgs(path));
}
catch (Exception e)
{
Debug.LogError("[ugx] a binding listener threw for '" + path + "': " + e);
}
}
}
}