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

namespace EasyDevCore.Common.Caches
{
    /// <summary>
    /// 
    /// </summary>
    public static class ExpressionCache
    {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="dictionary">The dictionary.</param>
        /// <param name="o">The o.</param>
        public delegate void AppendToDictionary(IDictionary<string, object> dictionary, object o);

        /// <summary>
        /// The s type cache
        /// </summary>
        static readonly ConcurrentDictionary<Type, AppendToDictionary> s_typeCache = new ConcurrentDictionary<Type, AppendToDictionary>();
        /// <summary>
        /// The dictionary indexer property
        /// </summary>
        private static readonly PropertyInfo _dictionaryIndexerProperty = GetDictionaryIndexer();

        /// <summary>
        /// Gets the or create append to dictionary method.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <returns></returns>
        public static AppendToDictionary GetOrCreateAppendToDictionaryMethod(Type type) => s_typeCache.GetOrAdd(type, t => CreateAppendToDictionaryMethod(t));

        /// <summary>
        /// Creates the append to dictionary method.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <returns></returns>
        private static AppendToDictionary CreateAppendToDictionaryMethod(Type type)
        {
            var dictionaryParameter = Expression.Parameter(typeof(IDictionary<string, object>), "dictionary");
            var objectParameter = Expression.Parameter(typeof(object), "o");

            var castedParameter = Expression.Convert(objectParameter, type); // cast o to the actual type

            // Create setter for each properties
            // dictionary["PropertyName"] = o.PropertyName;
            var properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy);
            var setters =
                from prop in properties
                where prop.CanRead
                let indexerExpression = Expression.Property(dictionaryParameter, _dictionaryIndexerProperty, Expression.Constant(prop.Name))
                let getExpression = Expression.Property(castedParameter, prop.GetMethod)
                select Expression.Assign(indexerExpression, getExpression);

            var body = new List<Expression>(properties.Length + 1);
            body.Add(castedParameter);
            body.AddRange(setters);

            var lambdaExpression = Expression.Lambda<AppendToDictionary>(Expression.Block(body), dictionaryParameter, objectParameter);
            return lambdaExpression.Compile();
        }

        // Get the PropertyInfo for IDictionary<string, object>.this[string key]
        /// <summary>
        /// Gets the dictionary indexer.
        /// </summary>
        /// <returns></returns>
        private static PropertyInfo GetDictionaryIndexer()
        {
            var indexers = from prop in typeof(IDictionary<string, object>).GetProperties(BindingFlags.Instance | BindingFlags.Public)
                           let indexParameters = prop.GetIndexParameters()
                           where indexParameters.Length == 1 && typeof(string).IsAssignableFrom(indexParameters[0].ParameterType)
                           select prop;

            return indexers.Single();
        }
    }
}
