// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
using System.Text.Json;

namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure;

internal sealed class DefaultTempDataSerializer : TempDataSerializer
{
    public override IDictionary<string, object> Deserialize(byte[] value)
    {
        ArgumentNullException.ThrowIfNull(value);

        if (value.Length == 0)
        {
            return new Dictionary<string, object>();
        }

        using var jsonDocument = JsonDocument.Parse(value);
        var rootElement = jsonDocument.RootElement;
        return DeserializeDictionary(rootElement);
    }

    private static IDictionary<string, object> DeserializeDictionary(JsonElement rootElement)
    {
        var deserialized = new Dictionary<string, object>(StringComparer.Ordinal);

        foreach (var item in rootElement.EnumerateObject())
        {
            object deserializedValue;
            switch (item.Value.ValueKind)
            {
                case JsonValueKind.False:
                case JsonValueKind.True:
                    deserializedValue = item.Value.GetBoolean();
                    break;

                case JsonValueKind.Number:
                    deserializedValue = item.Value.GetInt32();
                    break;

                case JsonValueKind.String:
                    if (item.Value.TryGetGuid(out var guid))
                    {
                        deserializedValue = guid;
                    }
                    else if (item.Value.TryGetDateTime(out var dateTime))
                    {
                        deserializedValue = dateTime;
                    }
                    else
                    {
                        deserializedValue = item.Value.GetString();
                    }
                    break;

                case JsonValueKind.Null:
                    deserializedValue = null;
                    break;

                case JsonValueKind.Array:
                    deserializedValue = DeserializeArray(item.Value);
                    break;

                case JsonValueKind.Object:
                    deserializedValue = DeserializeDictionaryEntry(item.Value);
                    break;

                default:
                    throw new InvalidOperationException(Resources.FormatTempData_CannotDeserializeType(item.Value.ValueKind));
            }

            deserialized[item.Name] = deserializedValue;
        }

        return deserialized;
    }

    private static object DeserializeArray(in JsonElement arrayElement)
    {
        int arrayLength = arrayElement.GetArrayLength();
        if (arrayLength == 0)
        {
            // We have to infer the type of the array by inspecting it's elements.
            // If there's nothing to inspect, return a null value since we do not know
            // what type the user code is expecting.
            return null;
        }

        if (arrayElement[0].ValueKind == JsonValueKind.String)
        {
            var array = new List<string>(arrayLength);

            foreach (var item in arrayElement.EnumerateArray())
            {
                array.Add(item.GetString());
            }

            return array.ToArray();
        }
        else if (arrayElement[0].ValueKind == JsonValueKind.Number)
        {
            var array = new List<int>(arrayLength);

            foreach (var item in arrayElement.EnumerateArray())
            {
                array.Add(item.GetInt32());
            }

            return array.ToArray();
        }

        throw new InvalidOperationException(Resources.FormatTempData_CannotDeserializeType(arrayElement.ValueKind));
    }

    private static object DeserializeDictionaryEntry(in JsonElement objectElement)
    {
        var dictionary = new Dictionary<string, string>(StringComparer.Ordinal);
        foreach (var item in objectElement.EnumerateObject())
        {
            dictionary[item.Name] = item.Value.GetString();
        }

        return dictionary;
    }

    public override byte[] Serialize(IDictionary<string, object> values)
    {
        if (values == null || values.Count == 0)
        {
            return Array.Empty<byte>();
        }

        using (var bufferWriter = new PooledArrayBufferWriter<byte>())
        {
            using var writer = new Utf8JsonWriter(bufferWriter);
            writer.WriteStartObject();
            foreach (var (key, value) in values)
            {
                if (value == null)
                {
                    writer.WriteNull(key);
                    continue;
                }

                // We want to allow only simple types to be serialized.
                if (!CanSerializeType(value.GetType()))
                {
                    throw new InvalidOperationException(
                        Resources.FormatTempData_CannotSerializeType(
                            typeof(DefaultTempDataSerializer).FullName,
                            value.GetType()));
                }

                switch (value)
                {
                    case Enum _:
                        writer.WriteNumber(key, (int)value);
                        break;

                    case string stringValue:
                        writer.WriteString(key, stringValue);
                        break;

                    case int intValue:
                        writer.WriteNumber(key, intValue);
                        break;

                    case bool boolValue:
                        writer.WriteBoolean(key, boolValue);
                        break;

                    case DateTime dateTime:
                        writer.WriteString(key, dateTime);
                        break;

                    case Guid guid:
                        writer.WriteString(key, guid);
                        break;

                    case ICollection<int> intCollection:
                        writer.WriteStartArray(key);
                        foreach (var element in intCollection)
                        {
                            writer.WriteNumberValue(element);
                        }
                        writer.WriteEndArray();
                        break;

                    case ICollection<string> stringCollection:
                        writer.WriteStartArray(key);
                        foreach (var element in stringCollection)
                        {
                            writer.WriteStringValue(element);
                        }
                        writer.WriteEndArray();
                        break;

                    case IDictionary<string, string> dictionary:
                        writer.WriteStartObject(key);
                        foreach (var element in dictionary)
                        {
                            writer.WriteString(element.Key, element.Value);
                        }
                        writer.WriteEndObject();
                        break;
                }
            }
            writer.WriteEndObject();
            writer.Flush();

            return bufferWriter.WrittenMemory.ToArray();
        }
    }

    public override bool CanSerializeType(Type type)
    {
        ArgumentNullException.ThrowIfNull(type);

        type = Nullable.GetUnderlyingType(type) ?? type;

        return
            type.IsEnum ||
            type == typeof(int) ||
            type == typeof(string) ||
            type == typeof(bool) ||
            type == typeof(DateTime) ||
            type == typeof(Guid) ||
            typeof(ICollection<int>).IsAssignableFrom(type) ||
            typeof(ICollection<string>).IsAssignableFrom(type) ||
            typeof(IDictionary<string, string>).IsAssignableFrom(type);
    }
}
