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

namespace DD.Persistence.Repository.Extensions;

public static class EFExtensionsSortBy
{
    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 ConcurrentDictionary<Type, Dictionary<string, TypeAccessor>> TypePropSelectors { get; set; } =
        new();

    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 MethodInfo GetExtOrderMethod(string methodName)
        => typeof(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,
        IEnumerable<string> propertySorts)
    {
        if (propertySorts?.Any() != true)
            return (IOrderedQueryable<TSource>)query;

        var sortEnum = propertySorts.GetEnumerator();
        sortEnum.MoveNext();
        var orderedQuery = query.SortBy(sortEnum.Current);

        while (sortEnum.MoveNext())
            orderedQuery = orderedQuery.ThenSortBy(sortEnum.Current);

        return orderedQuery;
    }

    /// <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="propertySort">
    /// Свойство сортировки.
    /// Состоит из названия свойства (в любом регистре) 
    /// и опционально указания направления сортировки "asc" или "desc"
    /// </param>
    /// <example>
    /// var query = query("Date desc");
    /// </example>
    /// <returns>Запрос с примененной сортировкой</returns>
    public static IOrderedQueryable<TSource> ThenSortBy<TSource>(
        this IOrderedQueryable<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.ThenSortBy(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)
    {
        Type rootType = typeof(TSource);
        var typePropSelector = TypePropSelectors.GetOrAdd(rootType, MakeTypeAccessors);
        var propertyNameLower = propertyName.ToLower();

        MethodInfo orderByDescending;
        MethodInfo orderByAscending;

        LambdaExpression? lambdaExpression = null;

        if (propertyName.Contains('.'))
        {
            Type type = rootType;
            ParameterExpression rootExpression = Expression.Parameter(rootType, "x");
            Expression expr = rootExpression;

            var propertyPath = propertyName.Split(".", StringSplitOptions.RemoveEmptyEntries);

            for (int i = 0; i < propertyPath.Length; i++)
            {
                PropertyInfo pi = type.GetProperty(propertyPath[i])!;
                expr = Expression.Property(expr, pi);
                type = pi.PropertyType;
            }

            Type delegateType = typeof(Func<,>).MakeGenericType(rootType, type);
            lambdaExpression = Expression.Lambda(delegateType, expr, rootExpression);

            orderByAscending = methodOrderBy.MakeGenericMethod(rootType, type);
            orderByDescending = methodOrderByDescending.MakeGenericMethod(rootType, type);
        }
        else
        {
            var rootTypeAccessor = typePropSelector[propertyNameLower];
            orderByAscending = rootTypeAccessor.OrderBy;
            orderByDescending = rootTypeAccessor.OrderByDescending;
            lambdaExpression = rootTypeAccessor.KeySelector;
        }

        var genericMethod = isDesc
            ? orderByDescending
            : orderByAscending;

        var newQuery = (IOrderedQueryable<TSource>)genericMethod
            .Invoke(genericMethod, [query, lambdaExpression])!;
        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> ThenSortBy<TSource>(
        this IOrderedQueryable<TSource> query,
        string propertyName,
        bool isDesc)
    {
        Type rootType = typeof(TSource);
        var typePropSelector = TypePropSelectors.GetOrAdd(rootType, MakeTypeAccessors);
        var propertyNameLower = propertyName.ToLower();

        MethodInfo orderByDescending;
        MethodInfo orderByAscending;

        LambdaExpression? lambdaExpression = null;

        if (propertyName.Contains('.'))
        {
            Type type = rootType;
            ParameterExpression rootExpression = Expression.Parameter(rootType, "x");
            Expression expr = rootExpression;

            var propertyPath = propertyName.Split(".", StringSplitOptions.RemoveEmptyEntries);

            for (int i = 0; i < propertyPath.Length; i++)
            {
                PropertyInfo pi = type.GetProperty(propertyPath[i])!;
                expr = Expression.Property(expr, pi);
                type = pi.PropertyType;
            }

            Type delegateType = typeof(Func<,>).MakeGenericType(rootType, type);
            lambdaExpression = Expression.Lambda(delegateType, expr, rootExpression);

            orderByAscending = methodThenBy.MakeGenericMethod(rootType, type);
            orderByDescending = methodThenByDescending.MakeGenericMethod(rootType, type);
        }
        else
        {
            var rootTypeAccessor = typePropSelector[propertyNameLower];
            orderByAscending = rootTypeAccessor.ThenBy;
            orderByDescending = rootTypeAccessor.ThenByDescending;
            lambdaExpression = rootTypeAccessor.KeySelector;
        }

        var genericMethod = isDesc
            ? orderByDescending
            : orderByAscending;

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