using System.Collections.Concurrent;
using System.Linq.Expressions;
using System.Reflection;

namespace DD.Persistence;
public static class EFExtensions
{
    struct TypeAccessor
    {
        public LambdaExpression KeySelector { get; set; }
        public MethodInfo OrderBy { get; set; }
        public MethodInfo OrderByDescending { get; set; }
        public MethodInfo ThenBy { get; set; }
        public MethodInfo ThenByDescending { get; set; }
    }
    private static readonly MethodInfo methodOrderBy = GetExtOrderMethod("OrderBy");

    private static readonly MethodInfo methodOrderByDescending = GetExtOrderMethod("OrderByDescending");

    private static readonly MethodInfo methodThenBy = GetExtOrderMethod("ThenBy");

    private static readonly MethodInfo methodThenByDescending = GetExtOrderMethod("ThenByDescending");
    private static ConcurrentDictionary<Type, Dictionary<string, TypeAccessor>> TypePropSelectors { get; set; } = new();

    private static MethodInfo GetExtOrderMethod(string methodName)
    => typeof(System.Linq.Queryable)
    .GetMethods()
    .Where(m => m.Name == methodName &&
        m.IsGenericMethodDefinition &&
        m.GetParameters().Length == 2 &&
        m.GetParameters()[1].ParameterType.IsAssignableTo(typeof(LambdaExpression)))
    .Single();
    private static Dictionary<string, TypeAccessor> MakeTypeAccessors(Type type)
    {
        var propContainer = new Dictionary<string, TypeAccessor>();
        var properties = type.GetProperties();
        foreach (var propertyInfo in properties)
        {
            var name = propertyInfo.Name.ToLower();
            ParameterExpression arg = Expression.Parameter(type, "x");
            MemberExpression property = Expression.Property(arg, propertyInfo.Name);
            var selector = Expression.Lambda(property, [arg]);
            var typeAccessor = new TypeAccessor
            {
                KeySelector = selector,
                OrderBy = methodOrderBy.MakeGenericMethod(type, propertyInfo.PropertyType),
                OrderByDescending = methodOrderByDescending.MakeGenericMethod(type, propertyInfo.PropertyType),
                ThenBy = methodThenBy.MakeGenericMethod(type, propertyInfo.PropertyType),
                ThenByDescending = methodThenByDescending.MakeGenericMethod(type, propertyInfo.PropertyType),
            };

            propContainer.Add(name, typeAccessor);
        }
        return propContainer;
    }

    /// <summary>
    /// Добавить в запрос сортировку по возрастанию или убыванию.
    /// Этот метод сбросит ранее наложенные сортировки.
    /// </summary>
    /// <typeparam name="TSource"></typeparam>
    /// <param name="query"></param>
    /// <param name="propertySort">
    /// Свойство сортировки.
    /// Состоит из названия свойства (в любом регистре) 
    /// и опционально указания направления сортировки "asc" или "desc"
    /// </param>
    /// <example>
    /// var query = query("Date desc");
    /// </example>
    /// <returns>Запрос с примененной сортировкой</returns>
    public static IOrderedQueryable<TSource> SortBy<TSource>(
        this IQueryable<TSource> query,
        string propertySort)
    {
        var parts = propertySort.Split(" ", 2, StringSplitOptions.RemoveEmptyEntries);
        var isDesc = parts.Length >= 2 && parts[1].ToLower().Trim() == "desc";
        var propertyName = parts[0];

        var newQuery = query.SortBy(propertyName, isDesc);
        return newQuery;
    }

    /// <summary>
    /// Добавить в запрос сортировку по возрастанию или убыванию
    /// </summary>
    /// <typeparam name="TSource"></typeparam>
    /// <param name="query"></param>
    /// <param name="propertyName">Название свойства (в любом регистре)</param>
    /// <param name="isDesc">Сортировать по убыванию</param>
    /// <returns>Запрос с примененной сортировкой</returns>
    public static IOrderedQueryable<TSource> SortBy<TSource>(
    this IQueryable<TSource> query,
    string propertyName,
    bool isDesc)
    {
        var typePropSelector = TypePropSelectors.GetOrAdd(typeof(TSource), MakeTypeAccessors);
        var propertyNameLower = propertyName.ToLower();
        var typeAccessor = typePropSelector[propertyNameLower];

        var genericMethod = isDesc
            ? typeAccessor.OrderByDescending
            : typeAccessor.OrderBy;

        var newQuery = (IOrderedQueryable<TSource>)genericMethod
             .Invoke(genericMethod, [query, typeAccessor.KeySelector])!;
        return newQuery;
    }
}