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

namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;

/// <summary>
///     Translates boxed CLR values from <c>WriteRequest.Value</c> into gateway-side
///     <see cref="MxValue"/> instances. Inverse of <see cref="MxValueDecoder"/>.
///     Handles the seven Galaxy data types — Boolean, Int32, Int64, Float32, Float64,
///     String, DateTime — and their array variants. Null + unsupported types throw
///     <see cref="ArgumentException"/> so the IWritable caller can fail the write with a
///     clear status code rather than silently mis-typing the wire payload.
/// </summary>
internal static class MxValueEncoder
{
    /// <summary>Encodes a CLR value as an MxValue for transmission to the gateway.</summary>
    /// <param name="value">The value to encode, or null for a null MxValue.</param>
    /// <returns>An MxValue instance representing the encoded value.</returns>
    /// <exception cref="ArgumentException">Thrown if the value type is not supported.</exception>
    public static MxValue Encode(object? value)
    {
        if (value is null) return new MxValue { IsNull = true };

        switch (value)
        {
            case bool b: return new MxValue { BoolValue = b };
            case sbyte i8: return new MxValue { Int32Value = i8 };
            case short i16: return new MxValue { Int32Value = i16 };
            case int i32: return new MxValue { Int32Value = i32 };
            case byte u8: return new MxValue { Int32Value = u8 };
            case ushort u16: return new MxValue { Int32Value = u16 };
            case uint u32 when u32 <= int.MaxValue: return new MxValue { Int32Value = (int)u32 };
            case long i64: return new MxValue { Int64Value = i64 };
            case ulong u64 when u64 <= long.MaxValue: return new MxValue { Int64Value = (long)u64 };
            case float f32: return new MxValue { FloatValue = f32 };
            case double f64: return new MxValue { DoubleValue = f64 };
            case string s: return new MxValue { StringValue = s };
            case DateTime dt: return new MxValue { TimestampValue = Timestamp.FromDateTime(EnsureUtc(dt)) };
            case DateTimeOffset dto: return new MxValue { TimestampValue = Timestamp.FromDateTimeOffset(dto) };

            case bool[] arr: return EncodeArray(arr, (mx, vs) => mx.BoolValues = ToBoolArray(vs));
            case int[] arr: return EncodeArray(arr, (mx, vs) => mx.Int32Values = ToInt32Array(vs));
            case long[] arr: return EncodeArray(arr, (mx, vs) => mx.Int64Values = ToInt64Array(vs));
            case float[] arr: return EncodeArray(arr, (mx, vs) => mx.FloatValues = ToFloatArray(vs));
            case double[] arr: return EncodeArray(arr, (mx, vs) => mx.DoubleValues = ToDoubleArray(vs));
            case string[] arr: return EncodeArray(arr, (mx, vs) => mx.StringValues = ToStringArray(vs));
            case DateTime[] arr: return EncodeArray(arr, (mx, vs) => mx.TimestampValues = ToTimestampArray(vs));

            default:
                throw new ArgumentException(
                    $"Cannot encode value of type {value.GetType()} as MxValue. Supported: " +
                    "bool, int / long (and their unsigned variants), float, double, string, DateTime, " +
                    "and their 1-D array variants.",
                    nameof(value));
        }
    }

    private static MxValue EncodeArray<T>(T[] values, Action<MxArray, T[]> populate)
    {
        var array = new MxArray();
        populate(array, values);
        array.Dimensions.Add((uint)values.Length);
        return new MxValue { ArrayValue = array };
    }

    private static BoolArray ToBoolArray(bool[] vs) { var a = new BoolArray(); a.Values.AddRange(vs); return a; }
    private static Int32Array ToInt32Array(int[] vs) { var a = new Int32Array(); a.Values.AddRange(vs); return a; }
    private static Int64Array ToInt64Array(long[] vs) { var a = new Int64Array(); a.Values.AddRange(vs); return a; }
    private static FloatArray ToFloatArray(float[] vs) { var a = new FloatArray(); a.Values.AddRange(vs); return a; }
    private static DoubleArray ToDoubleArray(double[] vs) { var a = new DoubleArray(); a.Values.AddRange(vs); return a; }
    private static StringArray ToStringArray(string[] vs) { var a = new StringArray(); a.Values.AddRange(vs); return a; }
    private static TimestampArray ToTimestampArray(DateTime[] vs)
    {
        var a = new TimestampArray();
        foreach (var dt in vs) a.Values.Add(Timestamp.FromDateTime(EnsureUtc(dt)));
        return a;
    }

    /// <summary>
    ///     <see cref="Timestamp.FromDateTime"/> requires UTC. Convert non-UTC inputs
    ///     explicitly so a caller passing local time gets predictable wire bytes.
    /// </summary>
    private static DateTime EnsureUtc(DateTime dt) => dt.Kind switch
    {
        DateTimeKind.Utc => dt,
        DateTimeKind.Local => dt.ToUniversalTime(),
        _ => DateTime.SpecifyKind(dt, DateTimeKind.Utc),
    };
}
