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

    using EPiServer.Find;
    using EPiServer.Find.Api.Facets;

    public static class FacetExtensions
    {
        /// <summary>
        /// Adds facets to a Find query base on an attribute.
        /// </summary>
        /// <typeparam name="T">The content type to add the facets for.</typeparam>
        /// <param name="query">The query.</param>
        /// <returns>The ITypeSearch{T} with facets.</returns>
        public static ITypeSearch<T> AddFacets<T>(this ITypeSearch<T> query)
            where T : IContent, new()
        {
            IEnumerable<PropertyInfo> propertyInfoList = GetFacetedPropertiesSortedByIndex<T>();

            foreach (PropertyInfo propertyInfo in propertyInfoList)
            {
                try
                {
                    FacetAttribute facetAttribute =
                        Attribute.GetCustomAttribute(element: propertyInfo, typeof(FacetAttribute)) as FacetAttribute;

                    if (facetAttribute == null)
                    {
                        continue;
                    }

                    ParameterExpression expParam = Expression.Parameter(typeof(T), "x");
                    MemberExpression expProp = Expression.Property(expression: expParam, property: propertyInfo);
                    Type delegateType = typeof(Func<,>).MakeGenericType(typeof(T), propertyInfo.PropertyType);
                    dynamic expression = Expression.Lambda(delegateType: delegateType, body: expProp, expParam);

                    switch (facetAttribute.FacetType)
                    {
                        case FacetType.TermsFacetFor:
                            query = TypeSearchExtensions.TermsFacetFor(
                                query,
                                expression,
                                FacetRequestActionForField(propertyInfo: propertyInfo, size: facetAttribute.Amount));
                            break;

                        case FacetType.TermsFacetForWordsIn:
                            query = TypeSearchExtensions.TermsFacetForWordsIn(query, expression, facetAttribute.Amount);
                            break;

                        default:
                            query = TypeSearchExtensions.TermsFacetFor(
                                query,
                                expression,
                                FacetRequestActionForField(propertyInfo: propertyInfo, size: facetAttribute.Amount));
                            break;
                    }
                }
                catch (Exception)
                {
                }
            }

            return query;
        }

        /// <summary>
        /// Gets the facet values.
        /// </summary>
        /// <typeparam name="T">The content type to retrieve the facet values for.</typeparam>
        /// <param name="contentResult">The content result.</param>
        /// <returns>A Dictionary{System.String, IEnumerable{TermCount}}.</returns>
        public static Dictionary<string, IEnumerable<TermCount>> GetFacetValues<T>(
            this IHasFacetResults<T> contentResult)
            where T : IContent, new()
        {
            ConcurrentDictionary<string, IEnumerable<TermCount>> facetResults = new();

            IEnumerable<PropertyInfo> propertyInfoList = GetFacetedPropertiesSortedByIndex<T>();

            foreach (PropertyInfo propertyInfo in propertyInfoList)
            {
                try
                {
                    FacetAttribute facetAttribute =
                        Attribute.GetCustomAttribute(element: propertyInfo, typeof(FacetAttribute)) as FacetAttribute;

                    if (facetAttribute == null)
                    {
                        continue;
                    }

                    ParameterExpression expParam = Expression.Parameter(typeof(T), "x");
                    MemberExpression expProp = Expression.Property(expression: expParam, property: propertyInfo);
                    Expression conversion = Expression.Convert(expression: expProp, typeof(object));
                    Expression<Func<T, object>> expression = Expression.Lambda<Func<T, object>>(body: conversion, expParam);

                    IEnumerable<TermCount> termCounts = contentResult.TermsFacetFor(fieldSelector: expression);

                    facetResults.AddOrUpdate(
                        key: propertyInfo.Name,
                        addValue: termCounts,
                        (s, oldValue) => oldValue.Concat(second: termCounts));
                }
                catch (Exception)
                {
                }
            }

            Dictionary<string, IEnumerable<TermCount>> facetResultsDictionary = facetResults.ToDictionary(
                kvp => kvp.Key,
                kvp => kvp.Value,
                comparer: facetResults.Comparer);

            return facetResultsDictionary;
        }

        /// <summary>
        /// Facets the request action for field.
        /// </summary>
        /// <param name="fieldName">Name of the field.</param>
        /// <param name="size">The size.</param>
        /// <returns>The Action{TermsFacetRequest{&gt;}.</returns>
        private static Action<TermsFacetRequest> FacetRequestActionForField(string fieldName, int size)
        {
            return x =>
                {
                    x.Field = fieldName;
                    x.Size = size;
                };
        }

        /// <summary>
        /// Facets the request action for field.
        /// </summary>
        /// <param name="propertyInfo">The property information.</param>
        /// <param name="size">The size.</param>
        /// <returns>The Action{TermsFacetRequest{&gt;}.</returns>
        private static Action<TermsFacetRequest> FacetRequestActionForField(PropertyInfo propertyInfo, int size)
        {
            return FacetRequestActionForField(fieldName: propertyInfo.Name, size: size);
        }

        /// <summary>
        /// Gets the properties that has the FacetAttribute sorted by the index.
        /// </summary>
        /// <typeparam name="T">The type to get the properties for.</typeparam>
        /// <returns>An IEnumerable{PropertyInfo}.</returns>
        private static IEnumerable<PropertyInfo> GetFacetedPropertiesSortedByIndex<T>()
            where T : IContent
        {
            return GetFacetedPropertiesSortedByIndex(typeof(T));
        }

        /// <summary>
        /// Gets the properties that has the FacetAttribute sorted by the index.
        /// </summary>
        /// <param name="type">The type to get the properties for.</param>
        /// <returns>An IEnumerable{PropertyInfo}.</returns>
        private static IEnumerable<PropertyInfo> GetFacetedPropertiesSortedByIndex(Type type)
        {
            PropertyInfo[] allProperties = type.GetProperties().Where(predicate: HasAttribute<FacetAttribute>)
                .Select(
                    x => new
                             {
                                 Property = x,
                                 Attribute = (FacetAttribute)Attribute.GetCustomAttribute(
                                     element: x,
                                     typeof(FacetAttribute),
                                     true)
                             }).OrderBy(x => x.Attribute?.Index ?? -1).Select(x => x.Property).ToArray();

            return allProperties;
        }

        /// <summary>
        ///     Determines whether the specified property has the specified attribute.
        /// </summary>
        /// <typeparam name="T">The attribute type.</typeparam>
        /// <param name="propertyInfo">The propertyInfo.</param>
        /// <returns><c>true</c> if the specified self has attribute; otherwise, <c>false</c>.</returns>
        private static bool HasAttribute<T>(PropertyInfo propertyInfo)
            where T : Attribute
        {
            T attr = default;

            try
            {
                attr = (T)Attribute.GetCustomAttribute(element: propertyInfo, typeof(T));
            }
            catch (Exception)
            {
            }

            return attr != null;
        }
    }