using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace DataAccessLayer.Filtering
{
    public static class FilterExtensions
    {

        public static IQueryable<T> ApplyFilterAll<T>(this IQueryable<T> q, Filter filter)
        {
            q = q.ApplyFilter(filter);

            q = q.ApplySorting(filter);

            q = q.ApplyPaging(filter);

            return q;
        }

        public static IQueryable<T> ApplyFilter<T>(this IQueryable<T> q, Filter filter)
        {
            if (filter == null || filter.Conditions == null || filter.Conditions.Count() == 0)
                return q;

            var tp = typeof(T);
            var pp = Expression.Parameter(tp, "x");

            foreach (var condition in filter.Conditions)
            {
                var lambda = Expression.Lambda<Func<T, bool>>(condition.CreateExpression<T>(pp), pp);
                q = q.Where(lambda);
            }

            return q;
        }

        public static IQueryable<T> ApplySorting<T>(this IQueryable<T> q, Filter filter)
        {
            if (filter == null || filter.Sorting == null || filter.Sorting.Count() == 0)
                return q;

            bool first = true;

            var tp = typeof(T);
            var pp = Expression.Parameter(tp);

            IOrderedQueryable<T> result = null;

            foreach (var item in filter.Sorting)
            {
                if (first)
                {
                    if (item.Ascending)
                        result = OrderByMemberUsing(q, pp, item.Column, "OrderBy");
                    else
                        result = OrderByMemberUsing(q, pp, item.Column, "OrderByDescending");
                }
                else
                {
                    if (item.Ascending)
                        result = OrderByMemberUsing(result, pp, item.Column, "ThenBy");
                    else
                        result = OrderByMemberUsing(result, pp, item.Column, "ThenByDescending");
                }

                first = false;
            }

            return result;
        }

        public static IQueryable<T> ApplyPaging<T>(this IQueryable<T> q, Filter filter)
        {
            if (filter == null || (filter.PageSize ?? 0) <= 0 || (filter.Page ?? -1) < 0)
                return q;

            if (filter.Page.Value > 0)
                q = q.Skip(filter.PageSize.Value * filter.Page.Value);

            q = q.Take(filter.PageSize.Value);

            return q;
        }

        private static Expression CreateExpression<T>(this ConditionItem condition, ParameterExpression parameter)
        {
            if (condition == null)
                throw new ArgumentNullException("condition");

            switch (condition.Type)
            {
                default:
                case ConditionTypeEnum.Unknown:
                    break;

                case ConditionTypeEnum.Not:
                    return CreateExpressionUnary<T>(condition, parameter);

                case ConditionTypeEnum.And:
                case ConditionTypeEnum.Or:
                    return CreateExpressionLogicalOperator<T>(condition, parameter);

                case ConditionTypeEnum.Equal:
                case ConditionTypeEnum.NotEqual:
                case ConditionTypeEnum.Less:
                case ConditionTypeEnum.LessOrEqual:
                case ConditionTypeEnum.Greater:
                case ConditionTypeEnum.GreaterOrEqual:
                    return CreateExpressionOperator<T>(condition, parameter);

                case ConditionTypeEnum.Contains:
                case ConditionTypeEnum.NotContains:
                case ConditionTypeEnum.StartsWith:
                case ConditionTypeEnum.NotStartsWith:
                case ConditionTypeEnum.EndsWith:
                case ConditionTypeEnum.NotEndsWith:
                    return CreateExpressionCallFunction<T>(condition, parameter);
            }

            throw new ArgumentOutOfRangeException($"condition.Type: {condition.Type}");
        }

        private static Expression CreateExpressionLogicalOperator<T>(ConditionItem condition, ParameterExpression parameter)
        {
            var el = CreateExpression<T>(condition.Left, parameter);
            var er = CreateExpression<T>(condition.Right, parameter);

            BinaryExpression be;

            switch (condition.Type)
            {
                case ConditionTypeEnum.And:
                    be = Expression.AndAlso(el, er);
                    break;

                case ConditionTypeEnum.Or:
                    be = Expression.OrElse(el, er);
                    break;

                default:
                    throw new ArgumentException($"Invalid Type {condition.Type} for logical binary expression tree");
            }

            return be;
        }

        private static Expression CreateExpressionCallFunction<T>(ConditionItem condition, ParameterExpression parameter)
        {
            var prop = (typeof(T)).GetProperty(condition.Column);
            var column = Expression.Property(parameter, prop);

            var fp = CultureInfo.InvariantCulture;
            object o = Convert.ChangeType(condition.Value, prop.PropertyType, fp);

            var par = Expression.Constant(o, prop.PropertyType);

            string methodName;
            bool isNot = false;

            switch (condition.Type)
            {
                case ConditionTypeEnum.Contains:
                    methodName = "Contains";
                    break;

                case ConditionTypeEnum.NotContains:
                    methodName = "Contains";
                    isNot = true;
                    break;

                case ConditionTypeEnum.StartsWith:
                    methodName = "StartsWith";
                    break;

                case ConditionTypeEnum.NotStartsWith:
                    methodName = "StartsWith";
                    isNot = true;
                    break;

                case ConditionTypeEnum.EndsWith:
                    methodName = "EndsWith";
                    break;

                case ConditionTypeEnum.NotEndsWith:
                    methodName = "EndsWith";
                    isNot = true;
                    break;

                default:
                    throw new ArgumentException($"Invalid Type {condition.Type} for function call expression tree");
            }

            var method = typeof(string).GetMethod(methodName, new[] { typeof(string) });

            var me = Expression.Call(
                column,
                method,
                par);

            return isNot ? Expression.Not(me) : (Expression)me;
        }

        private static Expression CreateExpressionUnary<T>(ConditionItem condition, ParameterExpression parameter)
        {
            var cnd = CreateExpression<T>(condition.Condition, parameter);

            return Expression.Not(cnd);
        }

        private static Expression CreateExpressionOperator<T>(ConditionItem condition, ParameterExpression parameter)
        {
            var prop = (typeof(T)).GetProperty(condition.Column);
            var el = Expression.Property(parameter, prop);

            var fp = CultureInfo.InvariantCulture;

            object o = new object();
            if (prop.PropertyType == typeof(Guid) || prop.PropertyType == typeof(Guid?))
            {
                if (Guid.TryParse(condition.Value, out Guid guid))
                {
                    o = guid;
                }
                else
                {
                    o = null;
                }

            }
            else if (prop.PropertyType.GetTypeInfo().IsEnum)
            {
                o = Enum.Parse(prop.PropertyType, condition.Value);
            }
            else if (Nullable.GetUnderlyingType(prop.PropertyType) != null)
            {
                o = Convert.ChangeType(condition.Value, Nullable.GetUnderlyingType(prop.PropertyType), fp);
            }
            else
            {
                o = Convert.ChangeType(condition.Value, prop.PropertyType, fp);
            }

            var er = Expression.Constant(o, prop.PropertyType);

            BinaryExpression be;

            switch (condition.Type)
            {
                case ConditionTypeEnum.Equal:
                    be = Expression.Equal(el, er);
                    break;

                case ConditionTypeEnum.NotEqual:
                    be = Expression.NotEqual(el, er);
                    break;

                case ConditionTypeEnum.Less:
                    be = Expression.LessThan(el, er);
                    break;

                case ConditionTypeEnum.LessOrEqual:
                    be = Expression.LessThanOrEqual(el, er);
                    break;

                case ConditionTypeEnum.Greater:
                    be = Expression.GreaterThan(el, er);
                    break;

                case ConditionTypeEnum.GreaterOrEqual:
                    be = Expression.GreaterThanOrEqual(el, er);
                    break;

                default:
                    throw new ArgumentException($"Invalid Type {condition.Type} for binary expression tree");
            }

            return be;
        }

        private static IOrderedQueryable<T> OrderByMemberUsing<T>(this IQueryable<T> source, ParameterExpression parameter, string memberPath, string method)
        {
            var member = memberPath.Split('.').Aggregate((Expression)parameter, Expression.PropertyOrField);

            var keySelector = Expression.Lambda(member, parameter);

            var methodCall = Expression.Call(
                typeof(Queryable), method, new[] { parameter.Type, member.Type },
                source.Expression, Expression.Quote(keySelector));

            return (IOrderedQueryable<T>)source.Provider.CreateQuery(methodCall);
        }
    }
}

