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

using System.Collections;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using Google.Protobuf;
using Google.Protobuf.Reflection;
using Grpc.Shared;
using Type = System.Type;

namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.Json;

internal sealed class MessageTypeInfoResolver : IJsonTypeInfoResolver
{
    private readonly JsonContext _context;

    public MessageTypeInfoResolver(JsonContext context)
    {
        _context = context;
    }

    public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options)
    {
        if (!IsStandardMessage(type, out var messageDescriptor))
        {
            return null;
        }

        var typeInfo = JsonTypeInfo.CreateJsonTypeInfo(type, options);
        typeInfo.CreateObject = () => Activator.CreateInstance(type)!;

        var fields = messageDescriptor.Fields.InFieldNumberOrder();

        // The field map can have multiple entries for a property:
        // 1. The JSON field name, e.g. firstName. This is used to serialize and deserialize JSON.
        // 2. The original field name, e.g. first_name. This might be different. It is only used to deserialize JSON.
        var mappings = CreateJsonFieldMap(fields);

        foreach (var field in fields)
        {
            var propertyInfo = CreatePropertyInfo(typeInfo, field.JsonName, field, isSerializable: true);
            typeInfo.Properties.Add(propertyInfo);

            // We have a property for reading and writing the JSON name so remove from mappings.
            mappings.Remove(field.JsonName);
        }

        // Fields have two mappings: the original field name and the camelcased JSON name.
        // The JSON name can also be customized in proto with json_name option.
        // Remaining mappings are for extra setter only properties.
        foreach (var mapping in mappings)
        {
            var propertyInfo = CreatePropertyInfo(typeInfo, mapping.Key, mapping.Value, isSerializable: false);
            typeInfo.Properties.Add(propertyInfo);
        }

        return typeInfo;
    }

    private bool IsStandardMessage(Type type, [NotNullWhen(true)] out MessageDescriptor? messageDescriptor)
    {
        if (!typeof(IMessage).IsAssignableFrom(type))
        {
            messageDescriptor = null;
            return false;
        }

        messageDescriptor = (MessageDescriptor?) _context.DescriptorRegistry.FindDescriptorByType(type);
        if (messageDescriptor == null)
        {
            throw new InvalidOperationException("Couldn't resolve descriptor for message type: " + type);
        }

        // Wrappers and well known types are handled by converters.
        if (ServiceDescriptorHelpers.IsWrapperType(messageDescriptor))
        {
            return false;
        }
        if (JsonConverterHelper.WellKnownTypeNames.ContainsKey(messageDescriptor.FullName))
        {
            return false;
        }

        return true;
    }

    private JsonPropertyInfo CreatePropertyInfo(JsonTypeInfo typeInfo, string name, FieldDescriptor field, bool isSerializable)
    {
        var propertyInfo = typeInfo.CreateJsonPropertyInfo(
            JsonConverterHelper.GetFieldType(field),
            name);

        propertyInfo.ShouldSerialize = (o, v) =>
        {
            // Properties that don't have this flag set are only used to deserialize incoming JSON.
            if (!isSerializable)
            {
                return false;
            }
            return JsonConverterHelper.ShouldFormatFieldValue((IMessage)o, field, v, !_context.Settings.IgnoreDefaultValues);
        };
        propertyInfo.Get = (o) =>
        {
            return field.Accessor.GetValue((IMessage)o);
        };

        if (field.IsMap || field.IsRepeated)
        {
            // Collection properties are read-only. Populate values into existing collection.
            propertyInfo.ObjectCreationHandling = JsonObjectCreationHandling.Populate;
        }
        else
        {
            propertyInfo.Set = GetSetMethod(field);
        }

        return propertyInfo;
    }

    private static Action<object, object?> GetSetMethod(FieldDescriptor field)
    {
        Debug.Assert(!field.IsRepeated && !field.IsMap, "Collections shouldn't have a setter.");

        if (field.RealContainingOneof != null)
        {
            return (o, v) =>
            {
                var caseField = field.RealContainingOneof.Accessor.GetCaseFieldDescriptor((IMessage)o);
                if (caseField != null)
                {
                    throw new InvalidOperationException($"Multiple values specified for oneof {field.RealContainingOneof.Name}.");
                }

                SetFieldValue(field, (IMessage)o, v);
            };
        }

        return (o, v) =>
        {
            SetFieldValue(field, (IMessage)o, v);
        };

        static void SetFieldValue(FieldDescriptor field, IMessage m, object? v)
        {
            if (v != null)
            {
                field.Accessor.SetValue(m, v);
            }
            else
            {
                field.Accessor.Clear(m);
            }
        }
    }

    private static Dictionary<string, FieldDescriptor> CreateJsonFieldMap(IList<FieldDescriptor> fields)
    {
        var map = new Dictionary<string, FieldDescriptor>();
        // The ordering is important here: JsonName takes priority over Name,
        // which means we need to put JsonName values in the map after *all*
        // Name keys have been added. See https://github.com/protocolbuffers/protobuf/issues/11987
        foreach (var field in fields)
        {
            map[field.Name] = field;
        }
        foreach (var field in fields)
        {
            map[field.JsonName] = field;
        }
        return map;
    }
}
