using Google.Protobuf.WellKnownTypes;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;
///
/// Translates gateway-side instances into the boxed CLR objects
/// DataValueSnapshot.Value carries. Mirrors the seven Galaxy data types in
/// DataTypeMap (Boolean, Int32, Int64, Float32, Float64, String, DateTime), plus
/// the array variants exposed by . Unknown / awkward values fall
/// back to the raw_value bytes so a forward-compatible MXAccess deployment
/// doesn't lose data on the wire — the consumer can opt to deserialise the bytes.
///
///
/// ⚠️ Every method here must be total — it must never throw. The only caller is
/// 's dispatch loop, and an exception escaping into that loop
/// terminates it permanently: the pump keeps reading the gateway stream while nothing
/// is ever dispatched again. So a malformed payload returns false from
/// rather than raising, and every bound/shape check below is a
/// guard rather than an assertion.
///
internal static class MxValueDecoder
{
/// Decodes a gateway MxValue into a boxed CLR object.
/// The MxValue to decode, or null.
///
/// The decoded boxed CLR value, or null when is null, carries a
/// null MxValue, or cannot be represented. Prefer when the caller
/// needs to tell a genuine null apart from an unrepresentable value.
///
public static object? Decode(MxValue? value)
{
TryDecode(value, out var decoded);
return decoded;
}
///
/// Decodes a gateway MxValue, reporting whether it was representable at all.
///
/// The MxValue to decode, or null.
///
/// The decoded boxed CLR value. Null both for a genuine null and for a value this driver
/// cannot represent — alone cannot distinguish the two.
///
///
/// true when the value was decoded, including the genuine-null cases (a null
/// reference or ). false when the payload carries a
/// kind this driver has no CLR representation for — a null published under a
/// Good quality would otherwise be indistinguishable from a real null, which is
/// how a new wire type would go silently missing.
///
public static bool TryDecode(MxValue? value, out object? decoded)
{
decoded = null;
if (value is null) return true;
if (value.IsNull) return true;
switch (value.KindCase)
{
case MxValue.KindOneofCase.BoolValue: decoded = value.BoolValue; return true;
case MxValue.KindOneofCase.Int32Value: decoded = value.Int32Value; return true;
case MxValue.KindOneofCase.Int64Value: decoded = value.Int64Value; return true;
case MxValue.KindOneofCase.FloatValue: decoded = value.FloatValue; return true;
case MxValue.KindOneofCase.DoubleValue: decoded = value.DoubleValue; return true;
case MxValue.KindOneofCase.StringValue: decoded = value.StringValue; return true;
case MxValue.KindOneofCase.TimestampValue: decoded = DecodeTimestamp(value.TimestampValue); return true;
case MxValue.KindOneofCase.RawValue: decoded = value.RawValue.ToByteArray(); return true;
case MxValue.KindOneofCase.ArrayValue: return TryDecodeArray(value.ArrayValue, out decoded);
case MxValue.KindOneofCase.SparseArrayValue: return TryDecodeSparseArray(value.SparseArrayValue, out decoded);
default: return false;
}
}
private static DateTime? DecodeTimestamp(Timestamp? ts) => ts?.ToDateTime();
private static bool TryDecodeArray(MxArray? array, out object? decoded)
{
decoded = null;
if (array is null) return false;
switch (array.ValuesCase)
{
case MxArray.ValuesOneofCase.BoolValues: decoded = array.BoolValues.Values.ToArray(); return true;
case MxArray.ValuesOneofCase.Int32Values: decoded = array.Int32Values.Values.ToArray(); return true;
case MxArray.ValuesOneofCase.Int64Values: decoded = array.Int64Values.Values.ToArray(); return true;
case MxArray.ValuesOneofCase.FloatValues: decoded = array.FloatValues.Values.ToArray(); return true;
case MxArray.ValuesOneofCase.DoubleValues: decoded = array.DoubleValues.Values.ToArray(); return true;
case MxArray.ValuesOneofCase.StringValues: decoded = array.StringValues.Values.ToArray(); return true;
case MxArray.ValuesOneofCase.TimestampValues:
decoded = array.TimestampValues.Values.Select(t => t.ToDateTime()).ToArray();
return true;
default: return false;
}
}
///
/// Expands a sparse array into the dense CLR array it denotes: every index the payload
/// names carries its value, and every index it does not carries the element type's
/// default.
///
///
///
/// ⚠️ Per the gateway contract this cannot arrive inbound. MxSparseArray is
/// documented write-only — the gateway expands it into a full MxArray before
/// forwarding, and "the worker never receives or produces it". This branch therefore
/// exists as contract defence, not as an expected path: if a future gateway ever emits
/// one, the alternative is a silent null published under the event's own (likely Good)
/// quality, which loses the reading with nothing to show for it.
///
///
/// The fill semantics deliberately mirror the gateway's own SparseArrayExpander
/// rather than inventing a reading: unmentioned indices are reset to the element
/// default, not preserved (the gateway cannot read-modify-write without racing the
/// provider), Time defaults to the Unix epoch because the expander fills
/// Timestamp{0,0}, and Integer widens to long[] if any element
/// carries an int64 — matching the expander's own Int32/Int64 sub-array choice.
///
///
/// Malformed payloads the gateway would have rejected outright (zero/oversized length,
/// out-of-range index) are tolerated here rather than thrown, because this side is a
/// consumer: an out-of-range index is skipped, a duplicate index is last-wins.
///
///
private static bool TryDecodeSparseArray(MxSparseArray? sparse, out object? decoded)
{
decoded = null;
if (sparse is null) return false;
if (sparse.TotalLength == 0 || sparse.TotalLength > (uint)Array.MaxLength) return false;
var length = (int)sparse.TotalLength;
var elements = sparse.Elements;
switch (sparse.ElementDataType)
{
case MxDataType.Boolean:
return Fill(new bool[length], elements, v => v.BoolValue, out decoded);
// The expander picks its Integer sub-array width from the element kinds, since a
// sparse payload carries no CLR array to infer it from. Mirror that exactly, or a
// round-trip through the gateway would change the published CLR element type.
case MxDataType.Integer when UsesInt64(elements):
return Fill(new long[length], elements, ReadInt64, out decoded);
case MxDataType.Integer:
return Fill(new int[length], elements, v => v.Int32Value, out decoded);
case MxDataType.Float:
return Fill(new float[length], elements, v => v.FloatValue, out decoded);
case MxDataType.Double:
return Fill(new double[length], elements, v => v.DoubleValue, out decoded);
case MxDataType.String:
var strings = new string[length];
Array.Fill(strings, string.Empty);
return Fill(strings, elements, v => v.StringValue ?? string.Empty, out decoded);
case MxDataType.Time:
var times = new DateTime[length];
Array.Fill(times, DateTime.UnixEpoch);
return Fill(times, elements, v => v.TimestampValue?.ToDateTime() ?? DateTime.UnixEpoch, out decoded);
default:
// Structs, enums, references and the rest are not scalar element types the
// expander supports either — there is nothing meaningful to build.
return false;
}
}
private static bool Fill(
T[] target,
IReadOnlyList elements,
Func read,
out object? decoded)
{
foreach (var element in elements)
{
if (element.Index >= (uint)target.Length) continue; // out of range — skip, never throw
if (element.Value is null) continue; // leave the default in place
target[(int)element.Index] = read(element.Value);
}
decoded = target;
return true;
}
private static bool UsesInt64(IReadOnlyList elements)
{
foreach (var element in elements)
{
if (element.Value?.KindCase == MxValue.KindOneofCase.Int64Value) return true;
}
return false;
}
private static long ReadInt64(MxValue value) => value.KindCase switch
{
MxValue.KindOneofCase.Int64Value => value.Int64Value,
MxValue.KindOneofCase.Int32Value => value.Int32Value,
_ => 0L,
};
}