using System;
using System.Reflection;
using System.ComponentModel;
using System.Linq;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;

namespace EasyDevCore.Common
{        
    /// <summary>
    /// Reflection Tools
    /// </summary>
    public static class ReflectionHelper
    {
        /// <summary>
        /// Creates the object.
        /// </summary>
        /// <param name="typeName">Name of the type.</param>
        /// <param name="args">The args.</param>
        /// <returns></returns>
        public static object CreateInstance(string typeName, params object[] args) => CreateInstance(Type.GetType(typeName, true, true), args);

        /// <summary>
        /// Creates the object.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="args">The arguments.</param>
        /// <returns></returns>
        public static T CreateInstance<T>(params object[] args) => (T)CreateInstance(typeof(T), args);

        /// <summary>
        /// Creates the object.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <param name="args">The arguments.</param>
        /// <returns>
        ///   <br />
        /// </returns>
        public static object CreateInstance(this Type type, params object[] args)
        {
            if (args.Length > 0)
            {

                return Activator.CreateInstance(type, args);
            }
            else
            {
                return Activator.CreateInstance(type);
            }
        }

        /// <summary>
        /// Finds the types.
        /// </summary>
        /// <param name="searchTypes">The search types.</param>
        /// <param name="isClass">if set to <c>true</c> [is class].</param>
        /// <param name="isAbstract">if set to <c>true</c> [is abstract].</param>
        /// <param name="isInterface">if set to <c>true</c> [is interface].</param>
        /// <param name="assemblyPredicate">The assembly predicate.</param>
        /// <param name="typePredicate">The type predicate.</param>
        /// <returns></returns>
        public static IList<Type> FindTypes(Type[] searchTypes, bool? isClass = null, bool? isAbstract = null, bool? isInterface = null, Func<Assembly, bool> assemblyPredicate = null, Func<Type, bool> typePredicate = null)
        {
            IEnumerable<Assembly> assemblies = AppDomain.CurrentDomain.GetAssemblies();
            if (assemblyPredicate != null) assemblies = assemblies.Where(assemblyPredicate);
            IEnumerable<Type> types = assemblies.SelectMany(a => a.GetTypes());
            bool multiType = searchTypes.Length > 1;
            types = types.Where(t =>
            {
                bool ok = t.IsPublic && ((!multiType && searchTypes[0].IsAssignableFrom(t)) || (multiType && searchTypes.Any(st => st.IsAssignableFrom(t))));
                if(isClass.HasValue && t.IsClass != isClass.Value) ok = false;
                if(isAbstract.HasValue && t.IsAbstract != isAbstract.Value) ok = false;
                if(isInterface.HasValue && t.IsInterface != isInterface.Value) ok = false;
                return ok;
            });
            if (typePredicate != null) types = types.Where(typePredicate);
            return types.ToList();
        }

        /// <summary>
        /// Finds the types.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="isClass">if set to <c>true</c> [is class].</param>
        /// <param name="isAbstract">if set to <c>true</c> [is abstract].</param>
        /// <param name="isInterface">if set to <c>true</c> [is interface].</param>
        /// <param name="assemblyPredicate">The assembly predicate.</param>
        /// <param name="typePredicate">The type predicate.</param>
        /// <returns></returns>
        public static IList<Type> FindTypes<T>(bool? isClass = null, bool? isAbstract = null, bool? isInterface = null, Func<Assembly, bool> assemblyPredicate = null, Func<Type, bool> typePredicate = null)
        {
            Type[] types = new Type[1];
            types[0] = typeof(T);
            return FindTypes(types, isClass, isAbstract, isInterface, assemblyPredicate, typePredicate);
        }

        /// <summary>
        /// Gets the expression custom attribute.
        /// </summary>
        /// <typeparam name="ModelType">The type of the odel type.</typeparam>
        /// <typeparam name="ValueType">The type of the alue type.</typeparam>
        /// <typeparam name="T"></typeparam>
        /// <param name="expression">The expression.</param>
        /// <returns></returns>
        /// <exception cref="System.ArgumentException">
        /// </exception>
        public static T GetExpressionCustomAttribute<ModelType, ValueType, T>(this Expression<Func<ModelType, ValueType>> expression)
        {
            MemberExpression member = expression.Body as MemberExpression;            

            if (member == null)
                throw new ArgumentException(string.Format("Expression '{0}' refers to a method, not a property.", expression.ToString()));

            PropertyInfo propInfo = member.Member as PropertyInfo;
           
            if (propInfo == null)
                throw new ArgumentException(string.Format("Expression '{0}' refers to a field, not a property.", expression.ToString()));

            var attributes = propInfo.GetCustomAttributes(typeof(T), true);
            if (attributes.Length > 0)
                return (T)attributes[0];
            return default(T);
        }

        /// <summary>
        /// Gets the expression value.
        /// </summary>
        /// <typeparam name="ModelType">The type of the model type.</typeparam>
        /// <typeparam name="ValueType">The type of the value type.</typeparam>
        /// <param name="expression">The expression.</param>
        /// <param name="source">The source.</param>
        /// <returns>ValueType.</returns>
        /// <exception cref="ArgumentException">
        /// </exception>
        public static ValueType GetExpressionValue<ModelType, ValueType>(this Expression<Func<ModelType, ValueType>> expression, ModelType source)
        {
            MemberExpression member = expression.Body as MemberExpression;
            if (member == null)
                throw new ArgumentException(string.Format("Expression '{0}' refers to a method, not a property.", expression.ToString()));

            PropertyInfo propInfo = member.Member as PropertyInfo;
            if (propInfo == null)
                throw new ArgumentException(string.Format("Expression '{0}' refers to a field, not a property.", expression.ToString()));

            if (source == null) throw new ArgumentNullException(nameof(source));

            return (ValueType)source.GetPropertyValue(propInfo.Name);
        }

        /// <summary>
        /// Gets the name of the member.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="memberExpression">The member expression.</param>
        /// <returns></returns>
        public static string GetMemberName<T>(Expression<Func<T>> memberExpression)
        {
            MemberExpression expressionBody = (MemberExpression)memberExpression.Body;
            return expressionBody.Member.Name;
        }

        /// <summary>
        /// Gets the PropertyInfo for the TargetProperty.
        /// </summary>
        /// <param name="target">The target object.</param>
        /// <param name="targetProperty">The target property.</param>
        /// <param name="returnType">Type of the return.</param>
        /// <param name="paramTypes">The param types (System.Type.EmptyTypes = no parameters).</param>
        /// <returns></returns>
        public static PropertyInfo GetPropertyInfo(this object target, string targetProperty, Type returnType = null, Type[] paramTypes = null)
        {
            if ((returnType != null) || (paramTypes != null))
            {
                return target.GetType().GetProperty(targetProperty, BindingFlags.Public | BindingFlags.Instance, null, returnType, paramTypes, null);
            }
            else
            {
                return target.GetType().GetProperty(targetProperty);
            }
        }

        /// <summary>
        /// Gets the property infos.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <returns></returns>
        public static PropertyInfo[] GetPropertyInfos(this Type type)
        {
            return type.GetProperties(BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public).ToArray<PropertyInfo>();
        }

        /// <summary>
        /// Gets multi PropertyInfo for the TargetProperty.
        /// </summary>
        /// <param name="target">The target.</param>
        /// <returns></returns>
        public static PropertyInfo[] GetPropertyInfos(this object target)
        {
            return GetPropertyInfos(target.GetType());
        }

        /// <summary>
        /// Gets the method info.
        /// </summary>
        /// <param name="target">The target.</param>
        /// <param name="targetMethod">The target method.</param>
        /// <param name="paramTypes">The param types (System.Type.EmptyTypes = no parameters).</param>
        /// <returns></returns>
        public static MethodInfo GetMethodInfo(this object target, string targetMethod, Type[] paramTypes = null)
        {
            if (paramTypes != null)
            {
                return target.GetType().GetMethod(targetMethod, BindingFlags.Public | BindingFlags.Instance, null, CallingConventions.Any, paramTypes, null);
            }
            else
            {
                return target.GetType().GetMethod(targetMethod);
            }
        }

        /// <summary>
        /// Gets the method info.
        /// </summary>
        /// <param name="target">The target.</param>
        /// <param name="targetMethod">The target method.</param>
        /// <returns></returns>
        public static MethodInfo[] GetMethodInfos(this object target, string targetMethod)
        {
            return target.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public)
                        .Where((mi) => mi.Name == targetMethod).ToArray<MethodInfo>();
        }

        /// <summary>
        /// Invokes the method.
        /// </summary>
        /// <param name="target">The target.</param>
        /// <param name="targetMethod">The target method.</param>
        /// <param name="args">The args.</param>
        /// <returns></returns>
        public static object InvokeMethod<T>(this T target, string targetMethod, params object[] args) where T: class
        {
            MethodInfo methodInfo = GetMethodInfo(target, targetMethod);
            if (methodInfo == null)
                return null;
            return methodInfo.Invoke(target, args);
        }

        /// <summary>
        /// Invokes the method asynchronous.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <typeparam name="R"></typeparam>
        /// <param name="target">The target.</param>
        /// <param name="targetMethod">The target method.</param>
        /// <param name="args">The arguments.</param>
        /// <returns></returns>
        public static async Task<R> InvokeMethodAsync<T, R>(this T target, string targetMethod, params object[] args) where T : class
        {
            dynamic awaitable = (R)target.InvokeMethod(targetMethod, args);
            await awaitable;
            return awaitable.GetAwaiter().GetResult();
        }

        /// <summary>
        /// Checks the attribute exists.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="target">The target.</param>
        /// <returns></returns>
        public static bool CheckAttributeExists<T>(this object target)
            where T : Attribute
        {
            return Attribute.IsDefined(target.GetType().Assembly, typeof(T));
        }

        /// <summary>
        /// Gets the attribute.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="target">The target.</param>
        /// <returns></returns>
        public static T GetAttribute<T>(this object target)
            where T : Attribute
        {
            return (T)Attribute.GetCustomAttribute(target.GetType().Assembly, typeof(T));
        }

        /// <summary>
        /// Gets the attributes.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="target">The target.</param>
        /// <returns></returns>
        public static T[] GetAttributes<T>(this object target) 
            where T : Attribute
        {
            return (T[])Attribute.GetCustomAttributes(target.GetType().Assembly, typeof(T));
        }

        /// <summary>
        /// Checks the property exists.
        /// </summary>
        /// <param name="target">The target.</param>
        /// <param name="targetProperty">The target property.</param>
        /// <returns></returns>
        public static bool CheckPropertyExist(this object target, string targetProperty)
        {
            if (target is System.Dynamic.ExpandoObject)
            {
                return ((IDictionary<String, object>)target).ContainsKey(targetProperty);
            }
            else
            {
                object obj = target;
                PropertyDescriptor prop;
                while (true)
                {
                    if (targetProperty.Contains("."))
                    {
                        string parentProperty = targetProperty.Substring(0, targetProperty.IndexOf("."));
                        prop = TypeDescriptor.GetProperties(obj)[parentProperty];
                        obj = prop.GetValue(obj);
                        targetProperty = targetProperty.Substring(parentProperty.Length + 1);
                    }
                    else
                    {
                        prop = TypeDescriptor.GetProperties(obj)[targetProperty];
                        break;
                    }
                }
                return (prop != null);
            }
        }

        /// <summary>
        /// Gets the property values.
        /// </summary>
        /// <param name="target">The target.</param>
        /// <returns></returns>
        public static IDictionary<string, object> GetPropertyValues(this object target)
        {
            Dictionary<string, object> valuePairs = new Dictionary<string, object>();
            foreach(PropertyDescriptor prop in TypeDescriptor.GetProperties(target))
            {
                valuePairs.Add(prop.Name, prop.GetValue(target));
            }
            return valuePairs;
        }

        /// <summary>
        /// Gets the property value for the TargetProperty.
        /// </summary>
        /// <param name="target">The target object.</param>
        /// <param name="targetProperty">The target property.</param>
        /// <returns>The property value.</returns>
        public static object GetPropertyValue(this object target, string targetProperty)
        {
            if (target is System.Dynamic.ExpandoObject)
            {
                if (CheckPropertyExist(target, targetProperty))
                {
                    return ((IDictionary<String, object>)target)[targetProperty];
                }
                return null;
            }
            else
            {
                object obj = target;
                PropertyDescriptor prop;
                while (true)
                {
                    if (targetProperty.Contains("."))
                    {
                        string parentProperty = targetProperty.Substring(0, targetProperty.IndexOf("."));
                        prop = TypeDescriptor.GetProperties(obj)[parentProperty];
                        obj = prop.GetValue(obj);
                        targetProperty = targetProperty.Substring(parentProperty.Length + 1);
                    }
                    else
                    {
                        prop = TypeDescriptor.GetProperties(obj)[targetProperty];
                        break;
                    }
                }

                return (prop == null) ? null : prop.GetValue(obj);
            }
        }

        /// <summary>
        /// Gets the indexer value.
        /// </summary>
        /// <param name="target">The target.</param>
        /// <param name="indexValue">The index value.</param>
        /// <param name="targetParentProperty">The target parent property.</param>
        /// <returns></returns>
        public static object GetIndexerValue(this object target, object indexValue, string targetParentProperty = "")
        {
            object obj = target;
            PropertyDescriptor prop;
            PropertyInfo pi;
            while (true)
            {
                if (targetParentProperty.Contains("."))
                {
                    string parentProperty = targetParentProperty.Substring(0, targetParentProperty.IndexOf("."));
                    prop = TypeDescriptor.GetProperties(obj)[parentProperty];
                    obj = prop.GetValue(obj);
                    targetParentProperty = targetParentProperty.Substring(parentProperty.Length + 1);
                }
                else
                {
                    pi = GetPropertyInfo(obj, "Item", null, new Type[] { indexValue.GetType() });                    
                    break;
                }
            }
            return (pi == null) ? null : pi.GetValue(obj, new object[] { indexValue });
        }

        /// <summary>
        /// Gets the indexer value.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="target">The target.</param>
        /// <param name="indexValue">The index value.</param>
        /// <param name="targetParentProperty">The target parent property.</param>
        /// <returns></returns>
        public static T GetIndexerValue<T>(this object target, object indexValue, string targetParentProperty = "")
        {
            return (T)GetIndexerValue(target, indexValue, targetParentProperty);
        }

        /// <summary>
        /// Sets the indexer value.
        /// </summary>
        /// <param name="target">The target.</param>
        /// <param name="indexValue">The index value.</param>
        /// <param name="value">The value.</param>
        /// <param name="targetParentProperty">The target parent property.</param>
        public static void SetIndexerValue<T>(this T target, object indexValue, object value, string targetParentProperty = "") where T: class
        {
            object obj = target;
            PropertyDescriptor prop;
            PropertyInfo pi;
            while (true)
            {
                if (targetParentProperty.Contains("."))
                {
                    string parentProperty = targetParentProperty.Substring(0, targetParentProperty.IndexOf("."));
                    prop = TypeDescriptor.GetProperties(obj)[parentProperty];
                    obj = prop.GetValue(obj);
                    targetParentProperty = targetParentProperty.Substring(parentProperty.Length + 1);
                }
                else
                {
                    pi = GetPropertyInfo(obj, "Item", null, new Type[] { indexValue.GetType() });
                    break;
                }
            }
            pi.SetValue(obj, value, new object[] { indexValue });
        }

        /// <summary>
        /// Gets the property value for the TargetProperty.
        /// </summary>
        /// <typeparam name="T">The type of the property.</typeparam>
        /// <param name="target">The target object.</param>
        /// <param name="targetProperty">The target property.</param>
        /// <returns>The property value.</returns>
        public static T GetPropertyValue<T>(this object target, string targetProperty)
        {
            return (T)(GetPropertyValue(target, targetProperty) ?? default(T));
        }

        /// <summary>
        /// Gets the property value for the TargetProperty and convert to T, if not success uses default value.
        /// </summary>
        /// <typeparam name="T">The type of the property.</typeparam>
        /// <param name="target">The target object.</param>
        /// <param name="targetProperty">The target property.</param>
        /// <param name="defaultValue">The default value.</param>
        /// <returns>
        /// The property value.
        /// </returns>
        public static T GetPropertyValue<T>(this object target, string targetProperty, T defaultValue)
        {
            if (CheckPropertyExist(target, targetProperty))
            {
                var value = GetPropertyValue(target, targetProperty);
                return value == null ? defaultValue : value.ConvertTo<T>(defaultValue);
            }
            else
            {
                return defaultValue;
            }
        }

        
        /// <summary>
        /// Sets the property value for the TargetProperty.
        /// </summary>
        /// <param name="target">The target object.</param>
        /// <param name="targetProperty">The target property.</param>
        /// <param name="value">The value to set.</param>
        public static void SetPropertyValue<T>(this T target, string targetProperty, object value)
        {
            if (target == null)
                throw new ArgumentNullException("target", "Target object can not be Null.");

            if (target is System.Dynamic.ExpandoObject)
            {
                ((IDictionary<String, object>)target)[targetProperty] = value;
            }
            else
            {
                object obj = target;
                PropertyDescriptor prop;
                while (true)
                {
                    if (targetProperty.Contains("."))
                    {
                        string parentProperty = targetProperty.Substring(0, targetProperty.IndexOf("."));
                        prop = TypeDescriptor.GetProperties(obj)[parentProperty];
                        obj = prop.GetValue(obj);
                        targetProperty = targetProperty.Substring(parentProperty.Length + 1);
                    }
                    else
                    {
                        prop = TypeDescriptor.GetProperties(obj)[targetProperty];
                        break;
                    }
                }

                if (value == null)
                    prop.SetValue(obj, null);
                else
                {
                    SetPropertyValue(target, prop, value);
                }
            }
        }

        /// <summary>
        /// Sets the property value.
        /// </summary>
        /// <typeparam name="TModel">The type of the model.</typeparam>
        /// <param name="model">The model.</param>
        /// <param name="prop">The property.</param>
        /// <param name="value">The value.</param>
        public static void SetPropertyValue<TModel>(TModel model, PropertyDescriptor prop, object value)
        {
            if (value == null)
            {
                prop.SetValue(model, null);
                return;
            }
            Type pType = prop.PropertyType.GetUnderlyingType();
            Type vType = value.GetType().GetUnderlyingType();

            if (pType != null && (pType.Equals(vType) || pType.FullName == "System.Object"))
            {
                // types match, just copy value
                prop.SetValue(model, value);
            }
            else
            {
                // types don't match, try to coerce
                if (pType.Equals(typeof(Guid)))
                {
                    prop.SetValue(model, new Guid(value.ToString()));
                }
                else if (pType.IsEnum && vType.Equals(typeof(string)))
                    prop.SetValue(model, Enum.Parse(pType, value.ToString()));
                else if (pType.IsEnum && vType.IsNumericType())
                    prop.SetValue(model, value.ConvertTo<int>());
                else
                    prop.SetValue(model, Convert.ChangeType(value, pType));
            }
        }

        /// <summary>
        /// Invokes the generic method.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <param name="interfaceType">Type of the interface.</param>
        /// <param name="methodName">Name of the method.</param>
        /// <param name="args">The arguments.</param>
        /// <returns></returns>
        public static object InvokeGenericStaticMethod(this Type type, Type interfaceType, string methodName, params object[] args)
        {
            MethodInfo mi = type.GetMethods(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).First(f => f.Name == methodName);
            MethodInfo miConstructed = mi.MakeGenericMethod(interfaceType);
            return miConstructed.Invoke(null, args != null && args.Length > 0 ? args : null);
        }

        /// <summary>
        /// Invokes the generic static method asynchronous.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <param name="interfaceType">Type of the interface.</param>
        /// <param name="methodName">Name of the method.</param>
        /// <param name="args">The arguments.</param>
        /// <returns></returns>
        public static async Task<T> InvokeGenericStaticMethodAsync<T>(this Type type, Type interfaceType, string methodName, params object[] args)
        {
            MethodInfo mi = type.GetMethods(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).First(f => f.Name == methodName);
            MethodInfo miConstructed = mi.MakeGenericMethod(interfaceType);
            Task<T> result = (Task<T>)miConstructed.Invoke(null, args != null && args.Length > 0 ? args : null);
            return await result;
        }

        /// <summary>
        /// Invokes the generic method.
        /// </summary>
        /// <param name="obj">The object.</param>
        /// <param name="interfaceType">Type of the interface.</param>
        /// <param name="methodName">Name of the method.</param>
        /// <param name="args">The arguments.</param>
        /// <returns></returns>
        public static object InvokeGenericMethod(this object obj, Type interfaceType, string methodName, params object[] args)
        {
            MethodInfo mi = obj.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).First(f => f.Name == methodName);
            MethodInfo miConstructed = mi.MakeGenericMethod(interfaceType);
            return miConstructed.Invoke(obj, args != null && args.Length > 0 ? args : null);
        }

        /// <summary>
        /// Invokes the generic method asynchronous.
        /// </summary>
        /// <param name="obj">The object.</param>
        /// <param name="interfaceType">Type of the interface.</param>
        /// <param name="methodName">Name of the method.</param>
        /// <param name="args">The arguments.</param>
        /// <returns></returns>
        public static async Task<object> InvokeGenericMethodAsync(this object obj, Type interfaceType, string methodName, params object[] args)
        {
            MethodInfo mi = obj.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).First(f => f.Name == methodName);
            MethodInfo miConstructed = mi.MakeGenericMethod(interfaceType);
            Task<object> result = (Task<object>)miConstructed.Invoke(obj, args != null && args.Length > 0 ? args : null);
            return await result;
        }
    }
}
