using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace IRaCIS.Core.Infra.EFCore
{

    public static class DynamicRelationalExtensions
    {
        static MethodInfo UpdateMethodInfo =
            typeof(RelationalQueryableExtensions).GetMethod(nameof(RelationalQueryableExtensions.ExecuteUpdate));

        static MethodInfo UpdateAsyncMethodInfo =
            typeof(RelationalQueryableExtensions).GetMethod(nameof(RelationalQueryableExtensions.ExecuteUpdateAsync));

        #region 避免使用

        public static int ExecuteUpdate(this IQueryable query, string fieldName, object? fieldValue)
        {
            var updateBody = BuildUpdateBody(query.ElementType,
                new Dictionary<string, object?> { { fieldName, fieldValue } });

            return (int)UpdateMethodInfo.MakeGenericMethod(query.ElementType).Invoke(null, new object?[] { query, updateBody });
        }

        public static int ExecuteUpdate(this IQueryable query, IReadOnlyDictionary<string, object?> fieldValues)
        {
            var updateBody = BuildUpdateBody(query.ElementType, fieldValues);

            return (int)UpdateMethodInfo.MakeGenericMethod(query.ElementType).Invoke(null, new object?[] { query, updateBody });
        }
        public static Task<int> ExecuteUpdateAsync(this IQueryable query, string fieldName, object? fieldValue, CancellationToken cancellationToken = default)
        {
            var updateBody = BuildUpdateBody(query.ElementType,
                new Dictionary<string, object?> { { fieldName, fieldValue } });

            return (Task<int>)UpdateAsyncMethodInfo.MakeGenericMethod(query.ElementType).Invoke(null, new object?[] { query, updateBody, cancellationToken })!;
        }
        #endregion

        public static Dictionary<string, object?> ExtractFieldValues<TSource>(this Expression<Func<TSource, TSource>> updateFactory)
        {
            var dic = new Dictionary<string, object?>();
            var obj = (TSource)Activator.CreateInstance(typeof(TSource));

            Func<TSource, TSource> func = updateFactory.Compile();

            TSource applyObj = func(obj);

            var propList = ((MemberInitExpression)updateFactory.Body).Bindings.Select(mb => mb.Member.Name)
                            .Select(propName => typeof(TSource).GetProperty(propName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)).ToList();


            foreach (PropertyInfo prop in propList)
            {
                object value = prop.GetValue(applyObj);
                dic.Add(prop.Name, value);
            }

            return dic;
        }



        public static Task<int> ExecuteUpdateAsync(this IQueryable query, IReadOnlyDictionary<string, object?> fieldValues, CancellationToken cancellationToken = default)
        {
            var updateBody = BuildUpdateBody(query.ElementType, fieldValues);

            return (Task<int>)UpdateAsyncMethodInfo.MakeGenericMethod(query.ElementType).Invoke(null, new object?[] { query, updateBody, cancellationToken })!;
        }


        static LambdaExpression BuildUpdateBody(Type entityType, IReadOnlyDictionary<string, object?> fieldValues)
        {
            var setParam = Expression.Parameter(typeof(SetPropertyCalls<>).MakeGenericType(entityType), "s");
            var objParam = Expression.Parameter(entityType, "e");

            Expression setBody = setParam;

            foreach (var pair in fieldValues)
            {
                var propExpression = Expression.PropertyOrField(objParam, pair.Key);
                var valueExpression = ValueForType(propExpression.Type, pair.Value);

                // s.SetProperty(e => e.SomeField, value)
                setBody = Expression.Call(setBody, nameof(SetPropertyCalls<object>.SetProperty),
                    new[] { propExpression.Type }, Expression.Lambda(propExpression, objParam), valueExpression);

            }

            // s => s.SetProperty(e => e.SomeField, value)
            var updateBody = Expression.Lambda(setBody, setParam);

            return updateBody;
        }

        static Expression ValueForType(Type desiredType, object? value)
        {
            if (value == null)
            {
                return Expression.Default(desiredType);
            }

            if (value.GetType() != desiredType)
            {
                return Expression.Convert(Expression.Constant(value), desiredType);
            }

            return Expression.Constant(value);
        }
    }

}
