using System.Dynamic;
using System.Reflection;

namespace adas_core.Domain.Utils;

/// <summary>
/// Provides a static mapping utility for instances of the reference type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The reference type that this mapper operates on, constrained to reference types via the <c>class</c> constraint.</typeparam>
/// <remarks>
/// As indicated by the <c>where T : class</c> constraint, only reference types can be supplied as the generic argument for <typeparamref name="T"/>.
/// </remarks>
/// <!-- aidoc:v1 sig=4a87291 -->
public static class Mapper<T>
    // We can only use reference types
    where T : class
{
    private static readonly Dictionary<string, PropertyInfo> PropertyMap;

    /// <summary>
    /// Initializes the static <see cref="Mapper{T}.PropertyMap"/> cache used by the mapper to look up <see cref="System.Reflection.PropertyInfo"/> entries for the type parameter T by their lowercased property name.
    /// </summary>
    /// <!-- aidoc:v1 sig=ed9693b body=3dc0e55 -->
    static Mapper()
    {
        // At this point we can convert each
        // property name to lower case so we avoid 
        // creating a new string more than once.
        PropertyMap =
            typeof(T)
                .GetProperties()
                .ToDictionary(
                    p => p.Name.ToLower(),
                    p => p
                );
    }

    /// <summary>
    /// Maps properties from an <see cref="ExpandoObject"/> source to a strongly-typed destination object using a pre-defined property map.
    /// Property lookups are case-insensitive. Null values are only assigned to reference types or <see cref="Nullable{T}"/> types; assigning null to a non-nullable value type throws. When a source value's type does not match the destination property's type, the value is converted using <see cref="Convert.ToDouble(object)"/>.
    /// </summary>
    /// <param name="source">The source <see cref="ExpandoObject"/> whose key-value pairs will be mapped onto the destination.</param>
    /// <param name="destination">The target object that will receive the mapped property values.</param>
    /// <exception cref="ArgumentNullException">Thrown when <paramref name="source"/> or <paramref name="destination"/> is null.</exception>
    /// <exception cref="ArgumentException">Thrown when a source value is null but the corresponding destination property is a value type that is not <see cref="Nullable{T}"/>.</exception>
    /// <!-- aidoc-review:v1 severity=high kind=wrong_summary
    ///      "The summary states null values are only assigned to reference types or Nullable<T> types, but the condition `propType is { IsByRef: false } && propType.Name != \"Nullable\\`1\"` is true for reference types as well, so the code throws ArgumentException for reference types too." -->
    /// <!-- aidoc-review:v1 severity=high kind=wrong_exception
    ///      "The exception is documented as thrown only when the destination property is a non-nullable value type, but the condition matches any non-Nullable property type (including reference types), so the exception is also thrown for reference-type properties." -->
    /// <!-- aidoc-review:v1 severity=high kind=wrong_summary
    ///      "The summary states that when source value type does not match the destination property type, the value is converted using Convert.ToDouble(object), but the unconditional `p.SetValue(destination, kv.Value, null)` after the else-if block overwrites the conversion, so the original (unconverted) value is always assigned." -->
    public static void Map(ExpandoObject source, T destination)
        {
            // Might as well take care of null references early.
            if (source == null) throw new ArgumentNullException(nameof(source));
    
            if (destination == null) throw new ArgumentNullException(nameof(destination));
    
            // By iterating the KeyValuePair<string, object> of
            // source we can avoid manually searching the keys of
            // source as we see in your original code.
            foreach (var kv in source)
                if (PropertyMap.TryGetValue(kv.Key.ToLower(), out var p))
                {
                    var propType = p.PropertyType;
                    if (kv.Value == null)
                    {
                        if (propType is { IsByRef: false } && propType.Name != "Nullable`1")
                            // Throw if type is a value type 
                            // but not Nullable<>
                            throw new ArgumentException("not nullable");
                    }
                    else if (kv.Value.GetType() != propType)
                    {
                        // You could make this a bit less strict 
                        // but I don't recommend it.
                        p.SetValue(destination, Convert.ToDouble(kv.Value), null);
                    }
    
                    p.SetValue(destination, kv.Value, null);
                }
        }
}