using System; using System.Collections.Generic; using System.Globalization; namespace Uigenx.Actions { /// /// Optional key/value bag carried by an action. The default value holds no dictionary, so an /// action without a payload costs nothing to create, copy or read. /// public readonly struct UgxActionPayload : IEquatable { static readonly Dictionary EmptyMap = new Dictionary(0); readonly Dictionary _values; public UgxActionPayload(Dictionary values) { _values = values; } public static UgxActionPayload Empty => default; public bool IsEmpty => _values == null || _values.Count == 0; public int Count => _values?.Count ?? 0; /// Never null. Callers must not mutate the result. public IReadOnlyDictionary Values => _values ?? EmptyMap; public Dictionary.Enumerator GetEnumerator() => (_values ?? EmptyMap).GetEnumerator(); public bool TryGet(string key, out object value) { if (_values == null || key == null) { value = null; return false; } return _values.TryGetValue(key, out value); } public bool Has(string key) => _values != null && key != null && _values.ContainsKey(key); public int GetInt(string key, int fallback = 0) => TryGet(key, out var raw) && TryToDouble(raw, out var d) ? (int)Math.Round(d) : fallback; public long GetLong(string key, long fallback = 0L) => TryGet(key, out var raw) && TryToDouble(raw, out var d) ? (long)Math.Round(d) : fallback; public float GetFloat(string key, float fallback = 0f) => TryGet(key, out var raw) && TryToDouble(raw, out var d) ? (float)d : fallback; public bool GetBool(string key, bool fallback = false) { if (!TryGet(key, out var raw)) return fallback; switch (raw) { case bool b: return b; case string s: if (bool.TryParse(s, out var parsed)) return parsed; return TryToDouble(s, out var n) ? n != 0d : fallback; default: return TryToDouble(raw, out var d) ? d != 0d : fallback; } } public string GetString(string key, string fallback = null) { if (!TryGet(key, out var raw) || raw == null) return fallback; if (raw is string s) return s; if (raw is IFormattable f) return f.ToString(null, CultureInfo.InvariantCulture); return raw.ToString(); } /// Returns a copy with one more entry. Allocates; use it off hot paths. public UgxActionPayload With(string key, object value) { var copy = _values == null ? new Dictionary(1) : new Dictionary(_values); copy[key] = value; return new UgxActionPayload(copy); } public static UgxActionPayload Of(string key, object value) => new UgxActionPayload(new Dictionary(1) { [key] = value }); public static UgxActionPayload Of(string k0, object v0, string k1, object v1) => new UgxActionPayload(new Dictionary(2) { [k0] = v0, [k1] = v1 }); static bool TryToDouble(object raw, out double value) { switch (raw) { case null: value = 0d; return false; case double d: value = d; return true; case float f: value = f; return true; case int i: value = i; return true; case long l: value = l; return true; case short sh: value = sh; return true; case byte by: value = by; return true; case decimal m: value = (double)m; return true; case bool b: value = b ? 1d : 0d; return true; case string s: return double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out value); default: value = 0d; return false; } } public bool Equals(UgxActionPayload other) => ReferenceEquals(_values, other._values); public override bool Equals(object obj) => obj is UgxActionPayload other && Equals(other); public override int GetHashCode() => _values?.GetHashCode() ?? 0; } }