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

namespace EasyDevCore.Common
{
    /// <summary>
    /// https://www.devtrends.co.uk/blog/stop-using-automapper-in-your-data-access-code
    /// </summary>
    public static class QueryableExtensions
    {

        /// <summary>
        /// Projects the specified source.
        /// Students.Project().To&lt;StudentAddressDetails&gt;();
        /// </summary>
        /// <typeparam name="TSource">The type of the source.</typeparam>
        /// <param name="source">The source.</param>
        /// <returns></returns>
        public static ProjectionExpression<TSource> Project<TSource>(this IQueryable<TSource> source)
        {
            return new ProjectionExpression<TSource>(source);
        }
    }

    /// <summary>
    /// 
    /// </summary>
    /// <typeparam name="TSource">The type of the source.</typeparam>
    public class ProjectionExpression<TSource>
    {
        /// <summary>
        /// The expression cache
        /// </summary>
        private static readonly Dictionary<string, Expression> ExpressionCache = new Dictionary<string, Expression>();

        /// <summary>
        /// The source
        /// </summary>
        private readonly IQueryable<TSource> _source;

        /// <summary>
        /// Initializes a new instance of the <see cref="ProjectionExpression{TSource}"/> class.
        /// </summary>
        /// <param name="source">The source.</param>
        public ProjectionExpression(IQueryable<TSource> source)
        {
            _source = source;
        }

        /// <summary>
        /// To this instance.
        /// </summary>
        /// <typeparam name="TDest">The type of the dest.</typeparam>
        /// <returns></returns>
        public IQueryable<TDest> To<TDest>()
        {
            var queryExpression = GetCachedExpression<TDest>() ?? BuildExpression<TDest>();

            return _source.Select(queryExpression);
        }

        /// <summary>
        /// Gets the cached expression.
        /// </summary>
        /// <typeparam name="TDest">The type of the dest.</typeparam>
        /// <returns></returns>
        private static Expression<Func<TSource, TDest>> GetCachedExpression<TDest>()
        {
            var key = GetCacheKey<TDest>();

            return ExpressionCache.ContainsKey(key) ? ExpressionCache[key] as Expression<Func<TSource, TDest>> : null;
        }

        /// <summary>
        /// Builds the expression.
        /// </summary>
        /// <typeparam name="TDest">The type of the dest.</typeparam>
        /// <returns></returns>
        private static Expression<Func<TSource, TDest>> BuildExpression<TDest>()
        {
            var sourceProperties = typeof(TSource).GetProperties();
            var destinationProperties = typeof(TDest).GetProperties().Where(dest => dest.CanWrite);
            var parameterExpression = Expression.Parameter(typeof(TSource), "src");

            var bindings = destinationProperties
                                .Select(destinationProperty => BuildBinding(parameterExpression, destinationProperty, sourceProperties))
                                .Where(binding => binding != null);

            var expression = Expression.Lambda<Func<TSource, TDest>>(Expression.MemberInit(Expression.New(typeof(TDest)), bindings), parameterExpression);

            var key = GetCacheKey<TDest>();

            ExpressionCache.Add(key, expression);

            return expression;
        }

        /// <summary>
        /// Builds the binding.
        /// </summary>
        /// <param name="parameterExpression">The parameter expression.</param>
        /// <param name="destinationProperty">The destination property.</param>
        /// <param name="sourceProperties">The source properties.</param>
        /// <returns></returns>
        private static MemberAssignment BuildBinding(Expression parameterExpression, MemberInfo destinationProperty, IEnumerable<PropertyInfo> sourceProperties)
        {
            var sourceProperty = sourceProperties.FirstOrDefault(src => src.Name == destinationProperty.Name);

            if (sourceProperty != null) {
                return Expression.Bind(destinationProperty, Expression.Property(parameterExpression, sourceProperty));
            }

            var propertyNames = SplitCamelCase(destinationProperty.Name);

            if (propertyNames.Length == 2) {
                sourceProperty = sourceProperties.FirstOrDefault(src => src.Name == propertyNames[0]);

                if (sourceProperty != null) {
                    var sourceChildProperty = sourceProperty.PropertyType.GetProperties().FirstOrDefault(src => src.Name == propertyNames[1]);

                    if (sourceChildProperty != null) {
                        return Expression.Bind(destinationProperty, Expression.Property(Expression.Property(parameterExpression, sourceProperty), sourceChildProperty));
                    }
                }
            }

            return null;
        }

        /// <summary>
        /// Gets the cache key.
        /// </summary>
        /// <typeparam name="TDest">The type of the dest.</typeparam>
        /// <returns></returns>
        private static string GetCacheKey<TDest>()
        {
            return string.Concat(typeof(TSource).FullName, typeof(TDest).FullName);
        }

        /// <summary>
        /// Splits the camel case.
        /// </summary>
        /// <param name="input">The input.</param>
        /// <returns></returns>
        private static string[] SplitCamelCase(string input)
        {
            return Regex.Replace(input, "([A-Z])", " $1", RegexOptions.Compiled).Trim().Split(' ');
        }
    }

}
