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

// ReSharper disable StaticMemberInGenericType

namespace VCommon.VAutoMapper.Internals
{
    internal static class MiddleMapper<TDto>
    {
        private static readonly Dictionary<Type, Type> MiddleClasses = new Dictionary<Type, Type>();
        private static readonly Dictionary<Type, Func<object, object>> MiddleProjection = new Dictionary<Type, Func<object, object>>();

        private static readonly Type DtoType = typeof(TDto);

        public static TDto[] ProjectionToArray<TEntity>(IQueryable<TEntity> queryable)
        {
            Func<object, object> mp;

            lock (MiddleClasses)
            {
                lock (MiddleProjection)
                {
                    var entityType = typeof(TEntity);
                    if (false == MiddleClasses.TryGetValue(entityType, out var mt))
                        mt = MiddleClasses[entityType] = MiddleClass.Create(entityType, DtoType);

                    if (false == MiddleProjection.TryGetValue(entityType, out mp))
                        mp = MiddleProjection[entityType] = CreateMiddleProjection(entityType, mt);
                }
            }

            var arr = mp(queryable);
            var result = arr.MapTo<TDto[]>();
            return result;
        }

        private static Func<object, object> CreateMiddleProjection(Type entityType, Type mt)
        {
            //  (object q) => ( (Queryable<[EntityType]>)q ).Projection<[mt]>().ToArray();

            var projectionArray = (typeof(VAutoMapper).GetMethod(nameof(VAutoMapper.ProjectionToArray), BindingFlags.Static | BindingFlags.Public) ?? throw new VAutoMapperException($"Missing method [{typeof(VAutoMapper).FullName}.{nameof(VAutoMapper.ProjectionToArray)}<>]?"))
                .MakeGenericMethod(entityType, mt);

            var p = Expression.Parameter(typeof(object));
            var unbox = Expression.Convert(p, typeof(IQueryable<>).MakeGenericType(entityType));
            var invoke = Expression.Call(null, projectionArray, unbox);
            var exp = Expression.Lambda<Func<object, object>>(invoke, p);

            return exp.Compile();
        }
    }
}