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

namespace ZB.MOM.WW.MxGateway.Server.Sessions;

/// <summary>
/// Expands a client-supplied sparse array write (<see cref="MxSparseArray"/>) into a
/// full, default-filled <see cref="MxArray"/> in place.
/// </summary>
/// <remarks>
/// MXAccess has no partial-array write primitive: a write replaces the whole array.
/// Clients that only care about a few indices send an <see cref="MxSparseArray"/> with
/// the total length plus the indices they want to set; this expander materializes the
/// full array so the worker can do an ordinary whole-array COM write. Indices the client
/// did not mention are reset to the element type's default (they are NOT preserved from
/// the live value); this is intentional, because the gateway cannot read-modify-write
/// without racing the provider.
///
/// For the MXAccess <c>Integer</c> element type the worker's COM-array converter (see
/// <c>VariantConverter.ConvertArray</c>) chooses between a 32-bit and 64-bit sub-array
/// based on the CLR element type. The sparse value carries no CLR array, so this expander
/// mirrors that choice by inspecting the supplied element value kinds: if any element is an
/// <see cref="MxValue.KindOneofCase.Int64Value"/> the whole array is emitted as
/// <see cref="MxArray.Int64Values"/>; otherwise it is emitted as
/// <see cref="MxArray.Int32Values"/> (matching a default Integer array).
/// </remarks>
internal static class SparseArrayExpander
{
    /// <summary>
    /// Replaces <paramref name="value"/>'s <see cref="MxValue.SparseArrayValue"/> with an
    /// equivalent full <see cref="MxValue.ArrayValue"/>. If <paramref name="value"/> is not
    /// a sparse array this is a no-op, so callers may invoke it unconditionally.
    /// </summary>
    /// <param name="value">The value to expand in place.</param>
    /// <param name="maxSparseArrayLength">
    /// The maximum <c>total_length</c> the sparse array may declare before the write is
    /// rejected, enforced before the full array is allocated (see
    /// <c>MxGateway:Events:MaxSparseArrayLength</c>). Defaults to <see cref="int.MaxValue"/>
    /// so the <see cref="Array.MaxLength"/> backstop is the only bound in test/unit-construction
    /// paths that do not thread the configured cap.
    /// </param>
    /// <exception cref="RpcException">
    /// <see cref="StatusCode.InvalidArgument"/> when the sparse payload is invalid: zero
    /// total length, a total length exceeding <paramref name="maxSparseArrayLength"/>, an index
    /// at or beyond the total length, a duplicate index, an unsupported element type, or an
    /// element value whose kind does not match the declared element type.
    /// </exception>
    public static void Expand(MxValue value, int maxSparseArrayLength = int.MaxValue)
    {
        ArgumentNullException.ThrowIfNull(value);

        if (value.KindCase != MxValue.KindOneofCase.SparseArrayValue)
        {
            return;
        }

        MxSparseArray sparse = value.SparseArrayValue;
        MxDataType elementType = sparse.ElementDataType;
        uint totalLength = sparse.TotalLength;

        if (totalLength == 0)
        {
            throw Invalid("Sparse array total_length must be greater than zero.");
        }

        if (!IsSupportedElementType(elementType))
        {
            throw Invalid($"Sparse array element_data_type '{elementType}' is not a supported scalar element type.");
        }

        if (totalLength > (uint)maxSparseArrayLength)
        {
            throw Invalid(
                $"Sparse array total_length {totalLength} exceeds the configured maximum {maxSparseArrayLength} (MxGateway:Events:MaxSparseArrayLength).");
        }

        if (totalLength > (uint)Array.MaxLength)
        {
            throw Invalid(
                $"Sparse array total_length {totalLength} exceeds the maximum supported array length {Array.MaxLength}.");
        }

        int length = (int)totalLength;
        HashSet<uint> seenIndices = new();

        foreach (MxSparseElement element in sparse.Elements)
        {
            if (element.Index >= totalLength)
            {
                throw Invalid(
                    $"Sparse array index {element.Index} is out of range for total_length {totalLength}.");
            }

            if (!seenIndices.Add(element.Index))
            {
                throw Invalid($"Sparse array has a duplicate index {element.Index}.");
            }

            ValidateElementKind(elementType, element);
        }

        MxArray array = BuildArray(elementType, length, sparse.Elements);
        array.ElementDataType = elementType;
        array.Dimensions.Add(totalLength);

        // Assigning ArrayValue switches the oneof and clears SparseArrayValue.
        value.ArrayValue = array;
    }

    private static MxArray BuildArray(
        MxDataType elementType,
        int length,
        IReadOnlyList<MxSparseElement> elements)
    {
        MxArray array = new();

        switch (elementType)
        {
            case MxDataType.Boolean:
            {
                BoolArray values = new();

                // Size the backing store once: the fill below adds exactly `length` elements,
                // so without this the RepeatedField doubles its array log2(length) times.
                values.Values.Capacity = length;
                for (int i = 0; i < length; i++)
                {
                    values.Values.Add(false);
                }

                foreach (MxSparseElement element in elements)
                {
                    values.Values[(int)element.Index] = element.Value.BoolValue;
                }

                array.BoolValues = values;
                break;
            }

            case MxDataType.Integer when UsesInt64(elements):
            {
                Int64Array values = new();
                values.Values.Capacity = length;
                for (int i = 0; i < length; i++)
                {
                    values.Values.Add(0L);
                }

                foreach (MxSparseElement element in elements)
                {
                    values.Values[(int)element.Index] = ReadInt64(element.Value);
                }

                array.Int64Values = values;
                break;
            }

            case MxDataType.Integer:
            {
                Int32Array values = new();
                values.Values.Capacity = length;
                for (int i = 0; i < length; i++)
                {
                    values.Values.Add(0);
                }

                foreach (MxSparseElement element in elements)
                {
                    values.Values[(int)element.Index] = element.Value.Int32Value;
                }

                array.Int32Values = values;
                break;
            }

            case MxDataType.Float:
            {
                FloatArray values = new();
                values.Values.Capacity = length;
                for (int i = 0; i < length; i++)
                {
                    values.Values.Add(0f);
                }

                foreach (MxSparseElement element in elements)
                {
                    values.Values[(int)element.Index] = element.Value.FloatValue;
                }

                array.FloatValues = values;
                break;
            }

            case MxDataType.Double:
            {
                DoubleArray values = new();
                values.Values.Capacity = length;
                for (int i = 0; i < length; i++)
                {
                    values.Values.Add(0d);
                }

                foreach (MxSparseElement element in elements)
                {
                    values.Values[(int)element.Index] = element.Value.DoubleValue;
                }

                array.DoubleValues = values;
                break;
            }

            case MxDataType.String:
            {
                StringArray values = new();
                values.Values.Capacity = length;
                for (int i = 0; i < length; i++)
                {
                    values.Values.Add(string.Empty);
                }

                foreach (MxSparseElement element in elements)
                {
                    values.Values[(int)element.Index] = element.Value.StringValue;
                }

                array.StringValues = values;
                break;
            }

            case MxDataType.Time:
            {
                TimestampArray values = new();
                values.Values.Capacity = length;
                for (int i = 0; i < length; i++)
                {
                    values.Values.Add(new Timestamp { Seconds = 0, Nanos = 0 });
                }

                foreach (MxSparseElement element in elements)
                {
                    values.Values[(int)element.Index] = element.Value.TimestampValue;
                }

                array.TimestampValues = values;
                break;
            }

            default:
                // Unreachable: IsSupportedElementType gates the element type before BuildArray.
                throw Invalid($"Sparse array element_data_type '{elementType}' is not supported.");
        }

        return array;
    }

    private static bool IsSupportedElementType(MxDataType elementType) => elementType switch
    {
        MxDataType.Boolean => true,
        MxDataType.Integer => true,
        MxDataType.Float => true,
        MxDataType.Double => true,
        MxDataType.String => true,
        MxDataType.Time => true,
        _ => false,
    };

    private static void ValidateElementKind(MxDataType elementType, MxSparseElement element)
    {
        MxValue.KindOneofCase kind = element.Value?.KindCase ?? MxValue.KindOneofCase.None;

        bool matches = elementType switch
        {
            MxDataType.Boolean => kind == MxValue.KindOneofCase.BoolValue,
            MxDataType.Integer => kind is MxValue.KindOneofCase.Int32Value or MxValue.KindOneofCase.Int64Value,
            MxDataType.Float => kind == MxValue.KindOneofCase.FloatValue,
            MxDataType.Double => kind == MxValue.KindOneofCase.DoubleValue,
            MxDataType.String => kind == MxValue.KindOneofCase.StringValue,
            MxDataType.Time => kind == MxValue.KindOneofCase.TimestampValue,
            _ => false,
        };

        if (!matches)
        {
            throw Invalid(
                $"Sparse array element at index {element.Index} has value kind '{kind}' which does not match element_data_type '{elementType}'.");
        }
    }

    private static bool UsesInt64(IReadOnlyList<MxSparseElement> elements)
    {
        foreach (MxSparseElement element in elements)
        {
            if (element.Value.KindCase == MxValue.KindOneofCase.Int64Value)
            {
                return true;
            }
        }

        return false;
    }

    private static long ReadInt64(MxValue value) =>
        value.KindCase == MxValue.KindOneofCase.Int64Value ? value.Int64Value : value.Int32Value;

    private static RpcException Invalid(string message) =>
        new(new Status(StatusCode.InvalidArgument, message));
}
