using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
using ZB.MOM.WW.MxGateway.Contracts.Proto;

namespace ZB.MOM.WW.MxGateway.Client;

/// <summary>
/// Creates and projects gateway MXAccess values without hiding the raw
/// protobuf value carried by command replies and events.
/// </summary>
public static class MxValueExtensions
{
    /// <summary>
    /// Converts a boolean value to an MxValue with MxDataType.Boolean.
    /// </summary>
    /// <param name="value">Scalar boolean value to wrap.</param>
    /// <returns>An <see cref="MxValue"/> carrying the wrapped boolean.</returns>
    public static MxValue ToMxValue(this bool value)
    {
        return new MxValue
        {
            DataType = MxDataType.Boolean,
            VariantType = "VT_BOOL",
            BoolValue = value,
        };
    }

    /// <summary>
    /// Converts a 32-bit integer value to an MxValue with MxDataType.Integer.
    /// </summary>
    /// <param name="value">32-bit integer value to wrap.</param>
    /// <returns>An <see cref="MxValue"/> carrying the wrapped integer.</returns>
    public static MxValue ToMxValue(this int value)
    {
        return new MxValue
        {
            DataType = MxDataType.Integer,
            VariantType = "VT_I4",
            Int32Value = value,
        };
    }

    /// <summary>
    /// Converts a 64-bit integer value to an MxValue with MxDataType.Integer.
    /// </summary>
    /// <param name="value">64-bit integer value to wrap.</param>
    /// <returns>An <see cref="MxValue"/> carrying the wrapped integer.</returns>
    public static MxValue ToMxValue(this long value)
    {
        return new MxValue
        {
            DataType = MxDataType.Integer,
            VariantType = "VT_I8",
            Int64Value = value,
        };
    }

    /// <summary>
    /// Converts a single-precision floating-point value to an MxValue with MxDataType.Float.
    /// </summary>
    /// <param name="value">Single-precision floating-point value to wrap.</param>
    /// <returns>An <see cref="MxValue"/> carrying the wrapped value.</returns>
    public static MxValue ToMxValue(this float value)
    {
        return new MxValue
        {
            DataType = MxDataType.Float,
            VariantType = "VT_R4",
            FloatValue = value,
        };
    }

    /// <summary>
    /// Converts a double-precision floating-point value to an MxValue with MxDataType.Double.
    /// </summary>
    /// <param name="value">Double-precision floating-point value to wrap.</param>
    /// <returns>An <see cref="MxValue"/> carrying the wrapped value.</returns>
    public static MxValue ToMxValue(this double value)
    {
        return new MxValue
        {
            DataType = MxDataType.Double,
            VariantType = "VT_R8",
            DoubleValue = value,
        };
    }

    /// <summary>
    /// Converts a string value to an MxValue with MxDataType.String.
    /// </summary>
    /// <param name="value">String value to wrap.</param>
    /// <returns>An <see cref="MxValue"/> carrying the wrapped string.</returns>
    public static MxValue ToMxValue(this string value)
    {
        ArgumentNullException.ThrowIfNull(value);

        return new MxValue
        {
            DataType = MxDataType.String,
            VariantType = "VT_BSTR",
            StringValue = value,
        };
    }

    /// <summary>
    /// Converts a DateTimeOffset value to an MxValue with MxDataType.Time.
    /// </summary>
    /// <param name="value">DateTimeOffset value to wrap.</param>
    /// <returns>An <see cref="MxValue"/> carrying the wrapped timestamp.</returns>
    public static MxValue ToMxValue(this DateTimeOffset value)
    {
        return new MxValue
        {
            DataType = MxDataType.Time,
            VariantType = "VT_DATE",
            TimestampValue = Timestamp.FromDateTimeOffset(value),
        };
    }

    /// <summary>
    /// Converts a DateTime value to an MxValue with MxDataType.Time.
    /// </summary>
    /// <param name="value">DateTime value to wrap.</param>
    /// <returns>An <see cref="MxValue"/> carrying the wrapped timestamp.</returns>
    public static MxValue ToMxValue(this DateTime value)
    {
        return new DateTimeOffset(
                value.Kind == DateTimeKind.Unspecified
                    ? DateTime.SpecifyKind(value, DateTimeKind.Utc)
                    : value.ToUniversalTime())
            .ToMxValue();
    }

    /// <summary>
    /// Converts a boolean array to an MxValue with MxDataType.Boolean.
    /// </summary>
    /// <param name="values">Array of boolean values to wrap.</param>
    /// <returns>An <see cref="MxValue"/> carrying the wrapped array.</returns>
    public static MxValue ToMxValue(this IReadOnlyList<bool> values)
    {
        ArgumentNullException.ThrowIfNull(values);

        var array = new BoolArray();
        array.Values.Add(values);
        return CreateArrayValue(MxDataType.Boolean, "VT_ARRAY|VT_BOOL", values.Count, new MxArray
        {
            ElementDataType = MxDataType.Boolean,
            VariantType = "VT_ARRAY|VT_BOOL",
            BoolValues = array,
        });
    }

    /// <summary>
    /// Converts a 32-bit integer array to an MxValue with MxDataType.Integer.
    /// </summary>
    /// <param name="values">Array of 32-bit integer values to wrap.</param>
    /// <returns>An <see cref="MxValue"/> carrying the wrapped array.</returns>
    public static MxValue ToMxValue(this IReadOnlyList<int> values)
    {
        ArgumentNullException.ThrowIfNull(values);

        var array = new Int32Array();
        array.Values.Add(values);
        return CreateArrayValue(MxDataType.Integer, "VT_ARRAY|VT_I4", values.Count, new MxArray
        {
            ElementDataType = MxDataType.Integer,
            VariantType = "VT_ARRAY|VT_I4",
            Int32Values = array,
        });
    }

    /// <summary>
    /// Converts a 64-bit integer array to an MxValue with MxDataType.Integer.
    /// </summary>
    /// <param name="values">Array of 64-bit integer values to wrap.</param>
    /// <returns>An <see cref="MxValue"/> carrying the wrapped array.</returns>
    public static MxValue ToMxValue(this IReadOnlyList<long> values)
    {
        ArgumentNullException.ThrowIfNull(values);

        var array = new Int64Array();
        array.Values.Add(values);
        return CreateArrayValue(MxDataType.Integer, "VT_ARRAY|VT_I8", values.Count, new MxArray
        {
            ElementDataType = MxDataType.Integer,
            VariantType = "VT_ARRAY|VT_I8",
            Int64Values = array,
        });
    }

    /// <summary>
    /// Converts a single-precision floating-point array to an MxValue with MxDataType.Float.
    /// </summary>
    /// <param name="values">Array of single-precision floating-point values to wrap.</param>
    /// <returns>An <see cref="MxValue"/> carrying the wrapped array.</returns>
    public static MxValue ToMxValue(this IReadOnlyList<float> values)
    {
        ArgumentNullException.ThrowIfNull(values);

        var array = new FloatArray();
        array.Values.Add(values);
        return CreateArrayValue(MxDataType.Float, "VT_ARRAY|VT_R4", values.Count, new MxArray
        {
            ElementDataType = MxDataType.Float,
            VariantType = "VT_ARRAY|VT_R4",
            FloatValues = array,
        });
    }

    /// <summary>
    /// Converts a double-precision floating-point array to an MxValue with MxDataType.Double.
    /// </summary>
    /// <param name="values">Array of double-precision floating-point values to wrap.</param>
    /// <returns>An <see cref="MxValue"/> carrying the wrapped array.</returns>
    public static MxValue ToMxValue(this IReadOnlyList<double> values)
    {
        ArgumentNullException.ThrowIfNull(values);

        var array = new DoubleArray();
        array.Values.Add(values);
        return CreateArrayValue(MxDataType.Double, "VT_ARRAY|VT_R8", values.Count, new MxArray
        {
            ElementDataType = MxDataType.Double,
            VariantType = "VT_ARRAY|VT_R8",
            DoubleValues = array,
        });
    }

    /// <summary>
    /// Converts a string array to an MxValue with MxDataType.String.
    /// </summary>
    /// <param name="values">Array of string values to wrap.</param>
    /// <returns>An <see cref="MxValue"/> carrying the wrapped array.</returns>
    public static MxValue ToMxValue(this IReadOnlyList<string> values)
    {
        ArgumentNullException.ThrowIfNull(values);

        var array = new StringArray();
        array.Values.Add(values);
        return CreateArrayValue(MxDataType.String, "VT_ARRAY|VT_BSTR", values.Count, new MxArray
        {
            ElementDataType = MxDataType.String,
            VariantType = "VT_ARRAY|VT_BSTR",
            StringValues = array,
        });
    }

    /// <summary>
    /// Converts a DateTimeOffset array to an MxValue with MxDataType.Time.
    /// </summary>
    /// <param name="values">Array of DateTimeOffset values to wrap.</param>
    /// <returns>An <see cref="MxValue"/> carrying the wrapped array.</returns>
    public static MxValue ToMxValue(this IReadOnlyList<DateTimeOffset> values)
    {
        ArgumentNullException.ThrowIfNull(values);

        var array = new TimestampArray();
        array.Values.Add(values.Select(Timestamp.FromDateTimeOffset));
        return CreateArrayValue(MxDataType.Time, "VT_ARRAY|VT_DATE", values.Count, new MxArray
        {
            ElementDataType = MxDataType.Time,
            VariantType = "VT_ARRAY|VT_DATE",
            TimestampValues = array,
        });
    }

    /// <summary>
    /// Gets the projection kind (field name) of the given MxValue's current oneof value.
    /// </summary>
    /// <param name="value">The MxValue whose oneof projection kind is returned.</param>
    /// <returns>The field name of the value's current oneof projection (e.g. "boolValue").</returns>
    public static string GetProjectionKind(this MxValue value)
    {
        ArgumentNullException.ThrowIfNull(value);

        return value.KindCase switch
        {
            MxValue.KindOneofCase.BoolValue => "boolValue",
            MxValue.KindOneofCase.Int32Value => "int32Value",
            MxValue.KindOneofCase.Int64Value => "int64Value",
            MxValue.KindOneofCase.FloatValue => "floatValue",
            MxValue.KindOneofCase.DoubleValue => "doubleValue",
            MxValue.KindOneofCase.StringValue => "stringValue",
            MxValue.KindOneofCase.TimestampValue => "timestampValue",
            MxValue.KindOneofCase.ArrayValue => "arrayValue",
            MxValue.KindOneofCase.RawValue => "rawValue",
            _ => value.IsNull ? "nullValue" : "unspecified",
        };
    }

    /// <summary>
    /// Converts an MxValue to a CLR object; returns the boxed value or null for null MxValues.
    /// </summary>
    /// <param name="value">The MxValue to convert.</param>
    /// <returns>The boxed CLR value, or <see langword="null"/> if the MxValue is null.</returns>
    public static object? ToClrValue(this MxValue value)
    {
        ArgumentNullException.ThrowIfNull(value);

        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,
            MxValue.KindOneofCase.TimestampValue => value.TimestampValue.ToDateTimeOffset(),
            MxValue.KindOneofCase.ArrayValue => value.ArrayValue.ToClrArrayValue(),
            MxValue.KindOneofCase.RawValue => value.RawValue.ToByteArray(),
            _ => value.IsNull ? null : value,
        };
    }

    /// <summary>
    /// Converts an MxArray to a CLR array; returns null if the array does not have a known element type.
    /// </summary>
    /// <param name="array">The MxArray to convert.</param>
    /// <returns>The CLR array, or <see langword="null"/> if the element type is not known.</returns>
    public static object? ToClrArrayValue(this MxArray array)
    {
        ArgumentNullException.ThrowIfNull(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.ToDateTimeOffset())
                .ToArray(),
            MxArray.ValuesOneofCase.RawValues => array.RawValues.Values
                .Select(value => value.ToByteArray())
                .ToArray(),
            _ => null,
        };
    }

    /// <summary>
    /// Creates an MxValue with MxDataType.Unknown from raw byte data, variant type, and diagnostic info.
    /// </summary>
    /// <param name="value">Raw byte data representing the value.</param>
    /// <param name="variantType">Variant type string (e.g., "VT_BSTR").</param>
    /// <param name="rawDiagnostic">Diagnostic string describing the raw value.</param>
    /// <param name="rawDataType">Optional MXAccess data type override.</param>
    /// <returns>An <see cref="MxValue"/> with MxDataType.Unknown carrying the raw payload.</returns>
    public static MxValue ToRawMxValue(
        byte[] value,
        string variantType,
        string rawDiagnostic,
        int rawDataType = 0)
    {
        ArgumentNullException.ThrowIfNull(value);

        return new MxValue
        {
            DataType = MxDataType.Unknown,
            VariantType = variantType,
            RawDiagnostic = rawDiagnostic,
            RawDataType = rawDataType,
            RawValue = ByteString.CopyFrom(value),
        };
    }

    private static MxValue CreateArrayValue(
        MxDataType dataType,
        string variantType,
        int length,
        MxArray array)
    {
        array.Dimensions.Add((uint)length);
        return new MxValue
        {
            DataType = dataType,
            VariantType = variantType,
            ArrayValue = array,
        };
    }
}
