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

namespace AsbCloudDb
{
    public static class EFExtensionsSortBy
    {
        struct TypeAcessor
        {
            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, TypeAcessor>> 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(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, TypeAcessor> MakeTypeAcessors(Type type)
        {
            var propContainer = new Dictionary<string, TypeAcessor>();
            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, new ParameterExpression[] { arg });
                var typeAccessor = new TypeAcessor
                {
                    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)
        {
            var typePropSelector = TypePropSelectors.GetOrAdd(typeof(TSource), MakeTypeAcessors);
            var propertyNamelower = propertyName.ToLower();
            var typeAccessor = typePropSelector[propertyNamelower];

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

            var newQuery = (IOrderedQueryable<TSource>)genericMethod
                 .Invoke(genericMethod, new object[] { query, typeAccessor.KeySelector })!;
            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)
        {
            var typePropSelector = TypePropSelectors.GetOrAdd(typeof(TSource), MakeTypeAcessors);
            var propertyNamelower = propertyName.ToLower();
            var typeAccessor = typePropSelector[propertyNamelower];
            var genericMethod = isDesc
                ? typeAccessor.ThenByDescending
                : typeAccessor.ThenBy;

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