using AutoMapper.QueryableExtensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using VCommon.Reflection;
using VCommon.VAutoMapper.Internals;

namespace VCommon.VAutoMapper
{
    public static class VAutoMapper
    {
        private static VAutoMapperConfiguration Config { get; } = new VAutoMapperConfiguration();
        private static readonly HashSet<Type> PolyfillTypes = new HashSet<Type>();

        static VAutoMapper()
        {
            foreach (var type in TypeFinder.AllTypes) ConfigMapping(type);
            FlushMapper();
        }

        internal static void FlushMapper() => Config.FlushMapper();

        internal static void ConfigMapping(Type type)
        {
            foreach (var autoMapFromAttribute in type.GetCustomAttributes<AutoMapFromAttribute>())
            {
                foreach (var f in autoMapFromAttribute.FromTypes) ConfigMapping(f, type);
            }

            foreach (var autoMapToAttribute in type.GetCustomAttributes<AutoMapToAttribute>())
            {
                foreach (var t in autoMapToAttribute.ToTypes) ConfigMapping(type, t);
            }
        }

        internal static void ConfigMapping(Type from, Type to)
        {
            var cfg = Config.CreateMap(from, to);

            var toProps = to.GetPublicInstanceProperties();
            var fromProps = from.GetPublicInstanceProperties();

            foreach (var toProp in toProps.Where(p => p.IsDefined(typeof(AutoMapperIgnoreAttribute))))
            {
                var toAttr = toProp.GetCustomAttribute<AutoMapperIgnoreAttribute>();
                if (toAttr.Direction == AutoMapperIgnoreDirection.Both || toAttr.Direction == AutoMapperIgnoreDirection.From) cfg.ForMember(toProp.Name, p => p.Ignore());
            }

            foreach (var fromProp in fromProps.Where(p => p.IsDefined(typeof(AutoMapperIgnoreAttribute))))
            {
                var fromAttr = fromProp.GetCustomAttribute<AutoMapperIgnoreAttribute>();
                if (fromAttr.Direction == AutoMapperIgnoreDirection.Both || fromAttr.Direction == AutoMapperIgnoreDirection.To) cfg.ForMember(fromProp.Name, p => p.Ignore());
            }

            foreach (var toProp in toProps.Where(p => p.IsDefined(typeof(AutoMapJsonConvertAttribute))))
            {
                PolyfillTypes.Add(to);

                //from.fromProp:string -deson-> to.toProp:obj

                var fromProp = from.GetProperty(toProp.Name);
                if (null == fromProp) continue;

                if (typeof(string) != fromProp.PropertyType) throw new VAutoMapperException($"Invalid json mapping attribute for [{from.FullName}.{fromProp.Name}] it must be string");

                //  (object p) =>  JsonSerializer.Deserialize(   (([type])p).[fromProp]  ), [toProp].PropertyType )

                var dejson = JsonConvertHolder.GetDeserializationExpression(fromProp, toProp);

                var paramFrom = Expression.Parameter(typeof(object));
                var paramFromUnboxing = Expression.Convert(paramFrom, from);
                var callDeJson = Expression.Invoke(dejson, Expression.Property(paramFromUnboxing, fromProp.Name), Expression.Constant(toProp.PropertyType));
                var expression = Expression.Lambda<Func<object, object>>(callDeJson, paramFrom);
                var func = expression.Compile();

                cfg.ForMember(fromProp.Name, x => x.MapFrom(obj => func(obj)));
            }

            foreach (var fromProp in fromProps.Where(p => p.IsDefined(typeof(AutoMapJsonConvertAttribute))))
            {
                PolyfillTypes.Add(from);

                //from.fromProp:obj -json-> to.toProp:string

                var toProp = to.GetProperty(fromProp.Name);
                if (null == toProp) continue;

                if (typeof(string) != toProp.PropertyType) throw new VAutoMapperException($"Invalid mapping attribute on {to.FullName}.{toProp.Name} it must be string");

                //  (object p) =>   JsonSerializer.Serialize(   (([type])p).[fromProp]  ))

                var json = JsonConvertHolder.GetSerializationExpression(fromProp, toProp);

                var paramFrom = Expression.Parameter(typeof(object));
                var paramFromUnboxing = Expression.Convert(paramFrom, from);
                var callJson = Expression.Invoke(json, Expression.Property(paramFromUnboxing, fromProp.Name));
                var expression = Expression.Lambda<Func<object, object>>(callJson, paramFrom);
                var func = expression.Compile();

                cfg.ForMember(fromProp.Name, x => x.MapFrom(obj => func(obj)));
            }
        }

        //---------------------------------------

        public static TD MapTo<TD>(this object from) => Config.Mapper.Map<TD>(from);

        public static TD MapTo<TF, TD>(this TF from, TD to) => Config.Mapper.Map(from, to);

        public static TD[] ProjectionToArray<TF, TD>(this IQueryable<TF> queryable)
        {
            return PolyfillTypes.Contains(typeof(TD))
                ? MiddleMapper<TD>.ProjectionToArray(queryable)
                : queryable.ProjectTo<TD>(Config.Config).ToArray();
        }

        public static TD ProjectionFirstOrDefault<TF, TD>(this IQueryable<TF> queryable)
        {
            return PolyfillTypes.Contains(typeof(TD))
                ? MiddleMapper<TD>.ProjectionToArray(queryable).FirstOrDefault()
                : queryable.ProjectTo<TD>(Config.Config).FirstOrDefault();
        }
    }
}