using System;
using System.Globalization;
using System.Linq;
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
using MxGateway.Contracts.Proto;
namespace MxGateway.Worker.Conversion;
public sealed class VariantConverter
{
///
/// Converts an object value to an MxValue without a specified data type.
///
/// Value to convert.
/// Converted MxValue.
public MxValue Convert(object? value)
{
return Convert(value, MxDataType.Unspecified);
}
///
/// Converts an object value to an MxValue with an expected data type.
///
/// Value to convert.
/// Expected MXAccess data type.
/// Converted MxValue.
public MxValue Convert(
object? value,
MxDataType expectedDataType)
{
if (value is null || value is DBNull)
{
return CreateNullValue(value, expectedDataType);
}
if (value is Array array)
{
return new MxValue
{
DataType = MxDataType.Unspecified,
VariantType = CreateArrayVariantType(array),
ArrayValue = ConvertArray(array, expectedDataType),
};
}
return ConvertScalar(value, expectedDataType);
}
///
/// Converts a .NET array to an MxArray.
///
/// Array to convert.
/// Expected data type for array elements.
/// Converted MxArray.
public MxArray ConvertArray(
Array array,
MxDataType expectedElementDataType = MxDataType.Unspecified)
{
if (array is null)
{
throw new ArgumentNullException(nameof(array));
}
MxArray mxArray = new()
{
VariantType = CreateArrayVariantType(array),
};
for (int dimension = 0; dimension < array.Rank; dimension++)
{
mxArray.Dimensions.Add((uint)array.GetLength(dimension));
}
System.Type? elementType = array.GetType().GetElementType();
MxDataType elementDataType = ResolveArrayElementDataType(elementType, expectedElementDataType);
mxArray.ElementDataType = elementDataType;
switch (elementDataType)
{
case MxDataType.Boolean:
mxArray.BoolValues = ConvertBoolArray(array);
return mxArray;
case MxDataType.Integer:
if (elementType == typeof(long) || elementType == typeof(ulong))
{
mxArray.Int64Values = ConvertInt64Array(array);
}
else
{
mxArray.Int32Values = ConvertInt32Array(array);
}
return mxArray;
case MxDataType.Float:
mxArray.FloatValues = ConvertFloatArray(array);
return mxArray;
case MxDataType.Double:
mxArray.DoubleValues = ConvertDoubleArray(array);
return mxArray;
case MxDataType.String:
mxArray.StringValues = ConvertStringArray(array);
return mxArray;
case MxDataType.Time:
mxArray.TimestampValues = ConvertTimestampArray(array);
return mxArray;
default:
mxArray.ElementDataType = MxDataType.Unknown;
mxArray.RawElementDataType = (int)expectedElementDataType;
mxArray.RawDiagnostic = CreateRawDiagnostic(array);
mxArray.RawValues = ConvertRawArray(array);
return mxArray;
}
}
///
/// Converts an into a CLR object suitable for an
/// MXAccess COM write. The COM marshaler boxes the returned value into the
/// matching VARIANT, so this is the inverse of .
///
/// Protobuf value to convert.
/// A COM-marshalable value, or for an MXAccess null.
public object? ConvertToComValue(MxValue value)
{
if (value is null)
{
throw new ArgumentNullException(nameof(value));
}
if (value.IsNull)
{
return null;
}
return value.KindCase switch
{
MxValue.KindOneofCase.BoolValue => value.BoolValue,
MxValue.KindOneofCase.Int32Value => value.Int32Value,
MxValue.KindOneofCase.Int64Value => value.Int64Value,
MxValue.KindOneofCase.FloatValue => value.FloatValue,
MxValue.KindOneofCase.DoubleValue => value.DoubleValue,
MxValue.KindOneofCase.StringValue => value.StringValue,
// The COM marshaler renders a DateTime as VT_DATE; MXAccess accepts
// it as the timestamped-write time argument.
MxValue.KindOneofCase.TimestampValue => value.TimestampValue.ToDateTime(),
MxValue.KindOneofCase.ArrayValue => ConvertToComArray(value.ArrayValue),
MxValue.KindOneofCase.RawValue => throw new ArgumentException(
"MxValue raw payloads cannot be written to MXAccess.", nameof(value)),
_ => throw new ArgumentException(
"MxValue has no value kind set; nothing to write.", nameof(value)),
};
}
private static Array ConvertToComArray(MxArray array)
{
return array.ValuesCase switch
{
MxArray.ValuesOneofCase.BoolValues => array.BoolValues.Values.ToArray(),
MxArray.ValuesOneofCase.Int32Values => array.Int32Values.Values.ToArray(),
MxArray.ValuesOneofCase.Int64Values => array.Int64Values.Values.ToArray(),
MxArray.ValuesOneofCase.FloatValues => array.FloatValues.Values.ToArray(),
MxArray.ValuesOneofCase.DoubleValues => array.DoubleValues.Values.ToArray(),
MxArray.ValuesOneofCase.StringValues => array.StringValues.Values.ToArray(),
MxArray.ValuesOneofCase.TimestampValues =>
array.TimestampValues.Values.Select(timestamp => timestamp.ToDateTime()).ToArray(),
MxArray.ValuesOneofCase.RawValues => throw new ArgumentException(
"MxArray raw payloads cannot be written to MXAccess.", nameof(array)),
_ => throw new ArgumentException(
"MxArray has no element values set; nothing to write.", nameof(array)),
};
}
private static MxValue ConvertScalar(
object value,
MxDataType expectedDataType)
{
System.Type valueType = value.GetType();
string variantType = GetVariantTypeName(valueType);
switch (System.Type.GetTypeCode(valueType))
{
case TypeCode.Boolean:
return new MxValue
{
DataType = MxDataType.Boolean,
VariantType = variantType,
BoolValue = (bool)value,
};
case TypeCode.Byte:
case TypeCode.SByte:
case TypeCode.Int16:
case TypeCode.UInt16:
case TypeCode.Int32:
return new MxValue
{
DataType = MxDataType.Integer,
VariantType = variantType,
Int32Value = System.Convert.ToInt32(value, CultureInfo.InvariantCulture),
};
case TypeCode.UInt32:
case TypeCode.Int64:
return ConvertInt64Scalar(value, variantType, expectedDataType);
case TypeCode.UInt64:
return ConvertUInt64Scalar((ulong)value, variantType, expectedDataType);
case TypeCode.Single:
return new MxValue
{
DataType = MxDataType.Float,
VariantType = variantType,
FloatValue = (float)value,
};
case TypeCode.Double:
return new MxValue
{
DataType = MxDataType.Double,
VariantType = variantType,
DoubleValue = (double)value,
};
case TypeCode.Decimal:
return new MxValue
{
DataType = MxDataType.Double,
VariantType = variantType,
DoubleValue = System.Convert.ToDouble(value, CultureInfo.InvariantCulture),
RawDiagnostic = "Decimal value projected to double.",
};
case TypeCode.String:
case TypeCode.Char:
return new MxValue
{
DataType = MxDataType.String,
VariantType = variantType,
StringValue = System.Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty,
};
case TypeCode.DateTime:
return new MxValue
{
DataType = MxDataType.Time,
VariantType = variantType,
TimestampValue = ToTimestamp((DateTime)value),
};
default:
return CreateRawValue(value, expectedDataType);
}
}
private static MxValue ConvertInt64Scalar(
object value,
string variantType,
MxDataType expectedDataType)
{
long longValue = System.Convert.ToInt64(value, CultureInfo.InvariantCulture);
// The MxDataType.Time projection treats the source as a Windows FILETIME
// (a 64-bit 100-ns tick count since 1601). Only a genuine 64-bit source
// (long) can carry a valid full FILETIME; a uint can only hold the low
// 32 bits, which DateTime.FromFileTimeUtc would silently render as a
// near-1601 timestamp. For uint sources fall through to the integer
// projection rather than producing a bogus timestamp.
if (expectedDataType == MxDataType.Time && value is long)
{
return new MxValue
{
DataType = MxDataType.Time,
VariantType = variantType,
TimestampValue = Timestamp.FromDateTime(DateTime.FromFileTimeUtc(longValue)),
};
}
return new MxValue
{
DataType = MxDataType.Integer,
VariantType = variantType,
Int64Value = longValue,
};
}
private static MxValue ConvertUInt64Scalar(
ulong value,
string variantType,
MxDataType expectedDataType)
{
if (expectedDataType == MxDataType.Time && value <= long.MaxValue)
{
return new MxValue
{
DataType = MxDataType.Time,
VariantType = variantType,
TimestampValue = Timestamp.FromDateTime(DateTime.FromFileTimeUtc((long)value)),
};
}
if (value <= long.MaxValue)
{
return new MxValue
{
DataType = MxDataType.Integer,
VariantType = variantType,
Int64Value = (long)value,
};
}
return CreateRawValue(value, expectedDataType, "UInt64 value exceeds Int64 range.");
}
private static MxValue CreateNullValue(
object? value,
MxDataType expectedDataType)
{
return new MxValue
{
DataType = expectedDataType == MxDataType.Unspecified ? MxDataType.NoData : expectedDataType,
VariantType = value is DBNull ? "VT_NULL" : "VT_EMPTY",
IsNull = true,
};
}
private static MxValue CreateRawValue(
object value,
MxDataType expectedDataType,
string? diagnosticPrefix = null)
{
string diagnostic = CreateRawDiagnostic(value);
if (!string.IsNullOrWhiteSpace(diagnosticPrefix))
{
diagnostic = $"{diagnosticPrefix} {diagnostic}";
}
return new MxValue
{
DataType = MxDataType.Unknown,
VariantType = GetVariantTypeName(value.GetType()),
RawDataType = (int)expectedDataType,
RawDiagnostic = diagnostic,
RawValue = ByteString.CopyFromUtf8(System.Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty),
};
}
private static BoolArray ConvertBoolArray(Array array)
{
BoolArray values = new();
foreach (object? item in array)
{
values.Values.Add(item is not null && System.Convert.ToBoolean(item, CultureInfo.InvariantCulture));
}
return values;
}
private static Int32Array ConvertInt32Array(Array array)
{
Int32Array values = new();
foreach (object? item in array)
{
values.Values.Add(item is null ? 0 : System.Convert.ToInt32(item, CultureInfo.InvariantCulture));
}
return values;
}
private static Int64Array ConvertInt64Array(Array array)
{
Int64Array values = new();
foreach (object? item in array)
{
values.Values.Add(item is null ? 0 : System.Convert.ToInt64(item, CultureInfo.InvariantCulture));
}
return values;
}
private static FloatArray ConvertFloatArray(Array array)
{
FloatArray values = new();
foreach (object? item in array)
{
values.Values.Add(item is null ? 0 : System.Convert.ToSingle(item, CultureInfo.InvariantCulture));
}
return values;
}
private static DoubleArray ConvertDoubleArray(Array array)
{
DoubleArray values = new();
foreach (object? item in array)
{
values.Values.Add(item is null ? 0 : System.Convert.ToDouble(item, CultureInfo.InvariantCulture));
}
return values;
}
private static StringArray ConvertStringArray(Array array)
{
StringArray values = new();
foreach (object? item in array)
{
values.Values.Add(item is null ? string.Empty : System.Convert.ToString(item, CultureInfo.InvariantCulture) ?? string.Empty);
}
return values;
}
private static TimestampArray ConvertTimestampArray(Array array)
{
TimestampArray values = new();
foreach (object? item in array)
{
if (item is null)
{
values.Values.Add(Timestamp.FromDateTime(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)));
}
else if (item is DateTime dateTime)
{
values.Values.Add(ToTimestamp(dateTime));
}
else
{
long fileTime = System.Convert.ToInt64(item, CultureInfo.InvariantCulture);
values.Values.Add(Timestamp.FromDateTime(DateTime.FromFileTimeUtc(fileTime)));
}
}
return values;
}
private static RawArray ConvertRawArray(Array array)
{
RawArray values = new();
foreach (object? item in array)
{
string rawValue = item is null
? string.Empty
: System.Convert.ToString(item, CultureInfo.InvariantCulture) ?? string.Empty;
values.Values.Add(ByteString.CopyFromUtf8(rawValue));
}
return values;
}
private static MxDataType ResolveArrayElementDataType(
System.Type? elementType,
MxDataType expectedElementDataType)
{
if (expectedElementDataType != MxDataType.Unspecified)
{
return expectedElementDataType;
}
if (elementType == typeof(bool))
{
return MxDataType.Boolean;
}
if (elementType == typeof(byte)
|| elementType == typeof(sbyte)
|| elementType == typeof(short)
|| elementType == typeof(ushort)
|| elementType == typeof(int)
|| elementType == typeof(uint)
|| elementType == typeof(long)
|| elementType == typeof(ulong))
{
return MxDataType.Integer;
}
if (elementType == typeof(float))
{
return MxDataType.Float;
}
if (elementType == typeof(double) || elementType == typeof(decimal))
{
return MxDataType.Double;
}
if (elementType == typeof(string) || elementType == typeof(char))
{
return MxDataType.String;
}
if (elementType == typeof(DateTime))
{
return MxDataType.Time;
}
return MxDataType.Unknown;
}
private static Timestamp ToTimestamp(DateTime dateTime)
{
DateTime utcDateTime = dateTime.Kind switch
{
DateTimeKind.Utc => dateTime,
DateTimeKind.Local => dateTime.ToUniversalTime(),
_ => DateTime.SpecifyKind(dateTime, DateTimeKind.Utc),
};
return Timestamp.FromDateTime(utcDateTime);
}
private static string CreateArrayVariantType(Array array)
{
System.Type? elementType = array.GetType().GetElementType();
return $"SAFEARRAY({GetVariantTypeName(elementType)})";
}
private static string GetVariantTypeName(System.Type? type)
{
if (type is null)
{
return "VT_EMPTY";
}
System.Type nonNullableType = Nullable.GetUnderlyingType(type) ?? type;
if (nonNullableType == typeof(bool))
{
return "VT_BOOL";
}
if (nonNullableType == typeof(byte))
{
return "VT_UI1";
}
if (nonNullableType == typeof(sbyte))
{
return "VT_I1";
}
if (nonNullableType == typeof(short))
{
return "VT_I2";
}
if (nonNullableType == typeof(ushort))
{
return "VT_UI2";
}
if (nonNullableType == typeof(int))
{
return "VT_I4";
}
if (nonNullableType == typeof(uint))
{
return "VT_UI4";
}
if (nonNullableType == typeof(long))
{
return "VT_I8";
}
if (nonNullableType == typeof(ulong))
{
return "VT_UI8";
}
if (nonNullableType == typeof(float))
{
return "VT_R4";
}
if (nonNullableType == typeof(double) || nonNullableType == typeof(decimal))
{
return "VT_R8";
}
if (nonNullableType == typeof(string) || nonNullableType == typeof(char))
{
return "VT_BSTR";
}
if (nonNullableType == typeof(DateTime))
{
return "VT_DATE";
}
return $"CLR:{nonNullableType.FullName}";
}
private static string CreateRawDiagnostic(object value)
{
return $"Unsupported variant projection for CLR type '{value.GetType().FullName}'.";
}
}