using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using UnityEngine;
using UnityEngine.UIElements;
namespace Uigenx.Binding
{
///
/// Path keyed value bag. Keys are full paths (`player.hp`, `inventory.items[0].count`), so
/// every get and set is O(1) with no tree walk. This is what the studio preview, the live data
/// stream and the tests drive.
///
public sealed class UgxDictionaryDataSource : IUgxDataSource, INotifyBindablePropertyChanged,
IDataSourceViewHashProvider
{
readonly Dictionary _values;
long _version;
public UgxDictionaryDataSource(int capacity = 16)
=> _values = new Dictionary(capacity, StringComparer.Ordinal);
public event EventHandler propertyChanged;
/// Raised with the path that changed, before .
public event Action PathChanged;
public long GetViewHashCode() => _version;
public int Count => _values.Count;
public IEnumerable Paths => _values.Keys;
public bool TryGet(string path, out object value)
{
if (string.IsNullOrEmpty(path))
{
value = null;
return false;
}
return _values.TryGetValue(path, out value);
}
public bool TrySet(string path, object value)
{
if (string.IsNullOrEmpty(path)) return false;
Set(path, value);
return true;
}
public void Set(string path, object value)
{
if (string.IsNullOrEmpty(path)) throw new ArgumentException("path is required", nameof(path));
if (_values.TryGetValue(path, out var existing) && Equals(existing, value)) return;
_values[path] = value;
Raise(path);
}
public void Remove(string path)
{
if (!_values.Remove(path)) return;
Raise(path);
}
public void Clear()
{
if (_values.Count == 0) return;
_values.Clear();
Raise(string.Empty);
}
public T Get(string path, T fallback = default)
{
if (!_values.TryGetValue(path, out var raw) || raw == null) return fallback;
if (raw is T typed) return typed;
try
{
return (T)Convert.ChangeType(raw, typeof(T), CultureInfo.InvariantCulture);
}
catch (Exception)
{
return fallback;
}
}
///
/// Flattens a nested tree (the shape the daemon sends in `preview.request.data`) into path
/// keys and merges it in. Cost is linear in the size of the tree.
///
public void Merge(IDictionary nested, string prefix = null)
{
if (nested == null) return;
foreach (var pair in nested) Flatten(Join(prefix, pair.Key), pair.Value);
}
/// Applies a flat path to value map, one change notification per changed path.
public void Apply(IReadOnlyDictionary flat)
{
if (flat == null) return;
foreach (var pair in flat) Set(pair.Key, pair.Value);
}
void Flatten(string path, object value)
{
switch (value)
{
case IDictionary map:
foreach (var pair in map) Flatten(Join(path, pair.Key), pair.Value);
return;
case IDictionary raw:
foreach (DictionaryEntry entry in raw) Flatten(Join(path, Convert.ToString(entry.Key, CultureInfo.InvariantCulture)), entry.Value);
return;
case string _:
Set(path, value);
return;
case IList list:
Set(path + ".length", list.Count);
for (int i = 0; i < list.Count; i++) Flatten(path + "[" + i.ToString(CultureInfo.InvariantCulture) + "]", list[i]);
return;
default:
Set(path, value);
return;
}
}
static string Join(string prefix, string key) => string.IsNullOrEmpty(prefix) ? key : prefix + "." + key;
void Raise(string path)
{
_version++;
// Listeners are foreign code: one throwing must not stop the others.
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);
}
}
}
}