/// <summary>
 /// gets an expression that specifies a property to be tested
 /// </summary>
 /// <param name="filter"></param>
 /// <param name="paramExp"></param>
 /// <param name="value"></param>
 /// <returns></returns>
 private static Expression GetFilteredProperty(ReadFilter filter, ParameterExpression paramExp)
 {
     return(WrapIntoDbFunction(
                filter,
                Expression.Property(paramExp, filter.Property)
                ));
 }
 /// <summary>
 /// Wraps an expression into a single param db fn
 /// </summary>
 /// <param name="filter"></param>
 /// <param name="e"></param>
 /// <returns></returns>
 private static Expression WrapIntoDbFunction(ReadFilter filter, Expression e)
 {
     if (!string.IsNullOrEmpty(filter.DbFn))
     {
         return(Expression.Call(typeof(DbFunctions), filter.DbFn, Type.EmptyTypes, e));
     }
     return(e);
 }
 /// <summary>
 /// Gets an expression that specifies the value to be tested against
 /// </summary>
 /// <param name="filter"></param>
 /// <param name="paramExp"></param>
 /// <param name="value"></param>
 /// <returns></returns>
 private static Expression GetFilteredValue(ReadFilter filter, ParameterExpression paramExp,
                                            object value = null)
 {
     return(WrapIntoDbFunction(
                filter,
                Expression.Convert(
                    Expression.Constant(value ?? filter.Value),
                    Expression.Property(paramExp, filter.Property).Type
                    )
                ));
 }
        /// <summary>
        /// Reads applications that are visible to an organisation and can be linked to it - returns apps that require auth and apps that are non-public (specific to that very org)
        /// </summary>
        /// <param name="dbCtx"></param>
        /// <param name="sort"></param>
        /// <param name="filter"></param>
        /// <param name="start"></param>
        /// <param name="limit"></param>
        /// <returns></returns>
        public async Task <Tuple <IEnumerable <Application>, int> > GetOrganisationLinkableApps(DbContext dbCtx, string sort = null, string filter = null,
                                                                                                int start = 0,
                                                                                                int limit = 25)
        {
            //first need to get objects for an organisation, and then add an extra filter with object guids
            var orgObjIds = await GetOrganisationObjectIdsAsync <Application>(dbCtx);

            //do make sure there is something to read!
            var filters = filter.ExtJsJsonFiltersToReadFilters();


            //return all the apps
            var preFilter = new ReadFilter
            {
                Property      = nameof(Application.RequiresAuth),
                Value         = true,
                Operator      = "==",
                ExactMatch    = true, //make the whole filter exact
                AndJoin       = true,
                NestedFilters = new List <ReadFilter>
                {
                    //need common apps
                    new ReadFilter
                    {
                        Property = nameof(Application.IsCommon),
                        Value    = true
                    },
                    //but not the dashboard. users always can access the dashboard!
                    new ReadFilter
                    {
                        Property = nameof(Application.IsDefault),
                        Value    = false
                    }
                }
            };

            //if there are org specific apps always return them in addition to the ones filtered by default
            if (orgObjIds.Any())
            {
                preFilter = new ReadFilter
                {
                    Property      = "Uuid",
                    Value         = orgObjIds.AsReadFilterList(),
                    Operator      = "in",
                    ExactMatch    = true,
                    AndJoin       = false,
                    NestedFilters = new List <ReadFilter>
                    {
                        preFilter
                    }
                };
            }

            filters.Add(preFilter);


            var app  = new Application();
            var apps = await app.ReadAsync(dbCtx, sort.ExtJsJsonSortersToReadSorters(), filters, start, limit);

            if (!apps.Any())
            {
                return(null);
            }

            var count = await apps.First().ReadCountAsync(dbCtx, filters);

            return(new Tuple <IEnumerable <Application>, int>(apps, count));
        }
        /// <summary>
        /// Gets a filter expression for a specified filter
        /// </summary>
        /// <param name="filter"></param>
        /// <param name="targetType"></param>
        /// <returns></returns>
        private static Expression GetFilterExpression(ReadFilter filter, Type targetType, ParameterExpression paramExp)
        {
            Expression filterExpression = null;


            //adjust "guid" filter types to just "==", so they're filtered the same way
            if (filter.Operator == "guid")
            {
                Guid guid;
                if (!Guid.TryParse(filter.Value, out guid))
                {
                    throw new ArgumentException($"Filter on {filter.Property} with a value of {filter.Value} is specified as a guid filter; the value is nto parsable to guid though.");
                }

                filter.Operator   = "==";
                filter.Value      = guid;
                filter.ExactMatch = true;
            }


            //if filter operator is not defined make it "==" for bools, "like" for strings and "eq" for numbers and dates
            //this is mainly because when filtering directly on store, operator is not sent and may be null
            if (string.IsNullOrEmpty(filter.Operator))
            {
                if (filter.Value is string)
                {
                    filter.Operator = "like";
                }

                else if (filter.Value is bool)
                {
                    filter.Operator = "==";
                }

                else if (filter.Value.IsNumeric() || filter.Value is DateTime)
                {
                    filter.Operator = "eq";
                }
            }

            //TODO - support some more filter operators such as =, <, <=, >, >=, notin, !=; this should be simply just a minor modification of the conditions below


            //Check if model property exists; if not this is a bad, bad request...
            var propertyToFilterBy =
                targetType.GetProperties()
                .FirstOrDefault(
                    p => string.Equals(p.Name, filter.Property, StringComparison.CurrentCultureIgnoreCase));

            if (propertyToFilterBy == null)
            {
                throw new BadRequestException(
                          $"The property {targetType}.{filter.Property} is not defined for the model!");
            }

            //work out what sort of filtering should be applied for a property...

            //string filter
            if (filter.Operator == "like" && filter.Value is string)
            {
                //ExtJs sends string filters ike this:
                //{"operator":"like","value":"some value","property":"name"}

                //Note:
                //In some cases complex types are used to simplify interaction with them in an object orineted way (so through properties) and at the same time
                //such types are stored as a single json string entry in the database
                //in such case a type shoudl implement IJsonSerialisableType. I so it should be possible to filter such type as it was a string
                //(which it indeed is on the db side)
                if (propertyToFilterBy.PropertyType.GetInterfaces().Contains(typeof(IJsonSerialisableType)))
                {
                    //This will call to lower on the property that the filter applies to;
                    //pretty much means call ToLower on the property - something like p => p.ToLower()
                    var toLower = Expression.Call(
                        Expression.Property( //this specify the property to filter by on the param object specified earlier - so p => p.Property
                            Expression.Property(paramExp, filter.Property),
                            "serialised"     //and we dig deeper here to reach p.Property.Serialised
                            ),
                        typeof(string).GetMethod("ToLower", Type.EmptyTypes)
                        //this is the method to be called on the property specified above
                        );

                    //finally need to assemble an equivaluent of p.Property.ToLower().Contains(filter.Value)
                    filterExpression = Expression.Call(toLower, "Contains", null,
                                                       Expression.Constant(((string)filter.Value.ToString()).ToLower(), typeof(string)));
                }
                else
                {
                    //this should be a string

                    //make sure the type of the property to filter by is ok
                    if (propertyToFilterBy.PropertyType != typeof(string))
                    {
                        throw new BadRequestException(
                                  $"The property {targetType}.{propertyToFilterBy.Name} is not of 'string' type.");
                    }


                    //This will call to lower on the property that the filter applies to;
                    //pretty much means call ToLower on the property - something like p => p.ToLower()
                    var toLower = Expression.Call(
                        Expression.Property(paramExp, filter.Property),
                        //this specify the property to filter by on the param object specified earlier - so p => p.Property
                        typeof(string).GetMethod("ToLower", Type.EmptyTypes)
                        //this is the method to be called on the property specified above
                        );

                    //finally need to assemble an equivaluent of p.Property.ToLower().Contains(filter.Value)
                    filterExpression = Expression.Call(toLower, "Contains", null,
                                                       Expression.Constant(((string)filter.Value.ToString()).ToLower(), typeof(string)));
                }
            }

            //range filter
            else if (filter.Operator == "in" && filter.Value is JArray)
            {
                //{"operator":"in","value":[11,18],"property":"name"}

                try
                {
                    var json = ((string)filter.Value.ToString()).Trim().TrimStart('{').TrimEnd('}');
                    var list = JsonConvert.DeserializeObject <List <object> >(json);

                    foreach (var item in list)
                    {
                        Expression inExpression;

                        if (Expression.Property(paramExp, filter.Property).Type == typeof(Guid))
                        {
                            //property to string
                            //var toStr = Expression.Call(
                            //    GetFilteredProperty(filter, paramExp),
                            //    typeof(Guid).GetMethod("ToString", Type.EmptyTypes)
                            //);
                            ////and then lowercase
                            //var toLower = Expression.Call(typeof(string).GetMethod("ToString", Type.EmptyTypes));

                            //inExpression = Expression.Call(toLower,
                            //    typeof(string).GetMethod("Equals", new[] {typeof(string)}),
                            //    Expression.Constant(item.ToString().ToLower())
                            //);

                            Guid guid;
                            if (!Guid.TryParse((string)item, out guid))
                            {
                                continue;
                            }

                            inExpression = Expression.Equal(
                                GetFilteredProperty(filter, paramExp),
                                GetFilteredValue(filter, paramExp, guid)
                                );
                        }
                        else
                        {
                            inExpression = Expression.Equal(
                                GetFilteredProperty(filter, paramExp),
                                GetFilteredValue(filter, paramExp)
                                );
                        }


                        //join the filters for each item in a list with OR
                        filterExpression = filterExpression == null
                            ? inExpression
                            : Expression.OrElse(filterExpression, inExpression);
                    }
                }
                catch (Exception ex)
                {
                    throw new BadRequestException("Property: " + propertyToFilterBy.Name + ", " + ex.Message,
                                                  ex.InnerException);
                }
            }

            //boolean filter
            //else if (filter.Operator == "==" && filter.Value is bool)
            else if (filter.Operator == "==")
            {
                //{"operator":"==","value":true,"property":"name"}

                //if (propertyToFilterBy.PropertyType != typeof(bool))
                //        throw new BadRequestException($"The property { targetType }.{ propertyToFilterBy.Name} is not of 'bool' type.");

                //filterExpression = Expression.Call(Expression.Property(expType, filter.Property), "Equals", null, Expression.Constant(filter.Value, typeof(bool)));

                filterExpression = Expression.Equal(
                    GetFilteredProperty(filter, paramExp),
                    GetFilteredValue(filter, paramExp)
                    );
            }

            //Lower than / greater than / equals; applies to numbers and dates
            else if ((filter.Operator == "lt" || filter.Operator == "gt" || filter.Operator == "eq") &&
                     (filter.Value.IsNumeric() || filter.Value is DateTime))
            {
                //{"operator":"lt","value":10,"property":"name"}
                //{"operator":"gt","value":10,"property":"name"}
                //{"operator":"eq","value":10,"property":"name"}
                //{"operator":"lt","value":"2016-05-08T22:00:00.000Z","property":"name"}
                //{"operator":"gt","value":"2016-05-08T22:00:00.000Z","property":"name"}
                //{"operator":"eq","value":"2016-05-08T22:00:00.000Z","property":"name"}

                try
                {
                    switch (filter.Operator)
                    {
                    case "eq":
                        filterExpression = Expression.Equal(
                            GetFilteredProperty(filter, paramExp),
                            GetFilteredValue(filter, paramExp)
                            );
                        break;

                    case "lt":
                        filterExpression = Expression.LessThan(
                            GetFilteredProperty(filter, paramExp),
                            GetFilteredValue(filter, paramExp)
                            );
                        break;

                    case "gt":
                        filterExpression = Expression.GreaterThan(
                            GetFilteredProperty(filter, paramExp),
                            GetFilteredValue(filter, paramExp)
                            );
                        break;
                    }
                }
                catch (Exception ex)
                {
                    throw new BadRequestException("Property: " + propertyToFilterBy.Name + ", " + ex.Message,
                                                  ex.InnerException);
                }
            }

            // If no expression generated then throw exception
            if (filterExpression == null)
            {
                throw new BadRequestException(
                          $"Filter operator: {filter.Operator} for type: {filter.Value.GetType()} is not implemented (property: {propertyToFilterBy.Name} should be {propertyToFilterBy.PropertyType})");
            }


            //check if there are nested filters and process them the same way as itself!
            if (filter.NestedFilters?.Count > 0)
            {
                foreach (var nestedFilter in filter.NestedFilters)
                {
                    var nested = GetFilterExpression(nestedFilter, targetType, paramExp);
                    filterExpression =
                        filter.AndJoin
                            ? Expression.AndAlso(filterExpression, nested)
                            : Expression.OrElse(filterExpression, nested);
                }
            }

            return(filterExpression);
        }