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

namespace ZB.MOM.WW.MxGateway.Worker.Conversion;

public sealed class VariantConverter
{
    /// <summary>
    /// Converts an object value to an MxValue without a specified data type.
    /// </summary>
    /// <param name="value">Value to convert.</param>
    /// <returns>Converted MxValue.</returns>
    public MxValue Convert(object? value)
    {
        return Convert(value, MxDataType.Unspecified);
    }

    /// <summary>
    /// Converts an object value to an MxValue with an expected data type.
    /// </summary>
    /// <param name="value">Value to convert.</param>
    /// <param name="expectedDataType">Expected MXAccess data type.</param>
    /// <returns>Converted MxValue.</returns>
    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);
    }

    /// <summary>
    /// Converts a .NET array to an MxArray.
    /// </summary>
    /// <param name="array">Array to convert.</param>
    /// <param name="expectedElementDataType">Expected data type for array elements.</param>
    /// <returns>Converted MxArray.</returns>
    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;
        }
    }

    /// <summary>
    /// Converts an <see cref="MxValue"/> 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 <see cref="Convert(object?)"/>.
    /// </summary>
    /// <param name="value">Protobuf value to convert.</param>
    /// <returns>A COM-marshalable value, or <see langword="null"/> for an MXAccess null.</returns>
    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
        // timestamp near the 1601 epoch. 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}'.";
    }
}
