#nullable enable
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using Swan.Configuration;
using Swan.Reflection;
namespace Swan {
  /// 
  /// Provides various extension methods for Reflection and Types.
  ///  
  public static class ReflectionExtensions {
    private static readonly Lazy, Func>> CacheGetMethods = new Lazy, Func>>(() => new ConcurrentDictionary, Func>(), true);
    private static readonly Lazy, Action>> CacheSetMethods = new Lazy, Action>>(() => new ConcurrentDictionary, Action>(), true);
    #region Assembly Extensions
    /// 
    /// Gets all types within an assembly in a safe manner.
    ///  
    ///  The assembly.
    /// 
    /// Array of Type objects representing the types specified by an assembly.
    ///  
    /// assembly. 
    public static IEnumerable GetAllTypes(this Assembly assembly) {
      if(assembly == null) {
        throw new ArgumentNullException(nameof(assembly));
      }
      try {
        return assembly.GetTypes();
      } catch(ReflectionTypeLoadException e) {
        return e.Types.Where(t => t != null);
      }
    }
    #endregion
    #region Type Extensions
    /// 
    /// The closest programmatic equivalent of default(T).
    ///  
    ///  The type.
    /// 
    /// Default value of this type.
    ///  
    /// type. 
    public static Object? GetDefault(this Type type) {
      if(type == null) {
        throw new ArgumentNullException(nameof(type));
      }
      return type.IsValueType ? Activator.CreateInstance(type) : default;
    }
    /// 
    /// Determines whether this type is compatible with ICollection.
    ///  
    ///  The type.
    /// 
    ///   true  if the specified source type is collection; otherwise, false .
    ///  
    /// sourceType. 
    public static Boolean IsCollection(this Type sourceType) {
      if(sourceType == null) {
        throw new ArgumentNullException(nameof(sourceType));
      }
      return sourceType != typeof(String) && typeof(IEnumerable).IsAssignableFrom(sourceType);
    }
    /// 
    /// Gets a method from a type given the method name, binding flags, generic types and parameter types.
    ///  
    ///  Type of the source.
    ///  The binding flags.
    ///  Name of the method.
    ///  The generic types.
    ///  The parameter types.
    /// 
    /// An object that represents the method with the specified name.
    ///  
    /// 
    /// The exception that is thrown when binding to a member results in more than one member matching the 
    /// binding criteria. This class cannot be inherited.
    ///  
    public static MethodInfo GetMethod(this Type type, BindingFlags bindingFlags, String methodName, Type[] genericTypes, Type[] parameterTypes) {
      if(type == null) {
        throw new ArgumentNullException(nameof(type));
      }
      if(methodName == null) {
        throw new ArgumentNullException(nameof(methodName));
      }
      if(genericTypes == null) {
        throw new ArgumentNullException(nameof(genericTypes));
      }
      if(parameterTypes == null) {
        throw new ArgumentNullException(nameof(parameterTypes));
      }
      List methods = type.GetMethods(bindingFlags)
        .Where(mi => String.Equals(methodName, mi.Name, StringComparison.Ordinal))
        .Where(mi => mi.ContainsGenericParameters)
        .Where(mi => mi.GetGenericArguments().Length == genericTypes.Length)
        .Where(mi => mi.GetParameters().Length == parameterTypes.Length).Select(mi => mi.MakeGenericMethod(genericTypes))
        .Where(mi => mi.GetParameters().Select(pi => pi.ParameterType).SequenceEqual(parameterTypes)).ToList();
      return methods.Count > 1 ? throw new AmbiguousMatchException() : methods.FirstOrDefault();
    }
    /// 
    /// Determines whether [is i enumerable request].
    ///  
    ///  The type.
    /// 
    ///   true  if [is i enumerable request] [the specified type]; otherwise, false .
    ///  
    /// type. 
    public static Boolean IsIEnumerable(this Type type) => type == null ? throw new ArgumentNullException(nameof(type)) : type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>);
    #endregion
    /// 
    /// Tries to parse using the basic types.
    ///  
    ///  The type.
    ///  The value.
    ///  The result.
    /// 
    ///   true  if parsing was successful; otherwise, false .
    ///  
    /// type 
    public static Boolean TryParseBasicType(this Type type, Object value, out Object? result) {
      if(type == null) {
        throw new ArgumentNullException(nameof(type));
      }
      if(type != typeof(Boolean)) {
        return TryParseBasicType(type, value.ToStringInvariant(), out result);
      }
      result = value.ToBoolean();
      return true;
    }
    /// 
    /// Tries to parse using the basic types.
    ///  
    ///  The type.
    ///  The value.
    ///  The result.
    /// 
    ///   true  if parsing was successful; otherwise, false .
    ///  
    /// type 
    public static Boolean TryParseBasicType(this Type type, String value, out Object? result) {
      if(type == null) {
        throw new ArgumentNullException(nameof(type));
      }
      result = null;
      return Definitions.BasicTypesInfo.Value.ContainsKey(type) && Definitions.BasicTypesInfo.Value[type].TryParse(value, out result);
    }
    /// 
    /// Tries the type of the set basic value to a property.
    ///  
    ///  The property information.
    ///  The value.
    ///  The object.
    /// 
    ///   true  if parsing was successful; otherwise, false .
    ///  
    /// propertyInfo. 
    public static Boolean TrySetBasicType(this PropertyInfo propertyInfo, Object value, Object target) {
      if(propertyInfo == null) {
        throw new ArgumentNullException(nameof(propertyInfo));
      }
      try {
        if(propertyInfo.PropertyType.TryParseBasicType(value, out Object? propertyValue)) {
          propertyInfo.SetValue(target, propertyValue);
          return true;
        }
      } catch {
        // swallow
      }
      return false;
    }
    /// 
    /// Tries the type of the set to an array a basic type.
    ///  
    ///  The type.
    ///  The value.
    ///  The array.
    ///  The index.
    /// 
    ///   true  if parsing was successful; otherwise, false .
    ///  
    /// type 
    public static Boolean TrySetArrayBasicType(this Type type, Object value, Array target, Int32 index) {
      if(type == null) {
        throw new ArgumentNullException(nameof(type));
      }
      if(target == null) {
        return false;
      }
      try {
        if(value == null) {
          target.SetValue(null, index);
          return true;
        }
        if(type.TryParseBasicType(value, out Object? propertyValue)) {
          target.SetValue(propertyValue, index);
          return true;
        }
        if(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) {
          target.SetValue(null, index);
          return true;
        }
      } catch {
        // swallow
      }
      return false;
    }
    /// 
    /// Tries to set a property array with another array.
    ///  
    ///  The property.
    ///  The value.
    ///  The object.
    /// 
    ///   true  if parsing was successful; otherwise, false .
    ///  
    /// propertyInfo. 
    public static Boolean TrySetArray(this PropertyInfo propertyInfo, IEnumerable? value, Object obj) {
      if(propertyInfo == null) {
        throw new ArgumentNullException(nameof(propertyInfo));
      }
      Type? elementType = propertyInfo.PropertyType.GetElementType();
      if(elementType == null || value == null) {
        return false;
      }
      Array targetArray = Array.CreateInstance(elementType, value.Count());
      Int32 i = 0;
      foreach(Object sourceElement in value) {
        Boolean result = elementType.TrySetArrayBasicType(sourceElement, targetArray, i++);
        if(!result) {
          return false;
        }
      }
      propertyInfo.SetValue(obj, targetArray);
      return true;
    }
    /// 
    /// Gets property actual value or PropertyDisplayAttribute.DefaultValue  if presented.
    ///
    /// If the PropertyDisplayAttribute.Format  value is presented, the property value
    /// will be formatted accordingly.
    ///
    /// If the object contains a null value, a empty string will be returned.
    ///  
    ///  The property information.
    ///  The object.
    /// The property value or null. 
    /// propertyInfo. 
    public static String? ToFormattedString(this PropertyInfo propertyInfo, Object target) {
      if(propertyInfo == null) {
        throw new ArgumentNullException(nameof(propertyInfo));
      }
      try {
        Object? value = propertyInfo.GetValue(target);
        PropertyDisplayAttribute attr = AttributeCache.DefaultCache.Value.RetrieveOne(propertyInfo);
        if(attr == null) {
          return value?.ToString() ?? String.Empty;
        }
        Object valueToFormat = value ?? attr.DefaultValue;
        return String.IsNullOrEmpty(attr.Format) ? (valueToFormat?.ToString() ?? String.Empty) : ConvertObjectAndFormat(propertyInfo.PropertyType, valueToFormat, attr.Format);
      } catch {
        return null;
      }
    }
    /// 
    /// Gets a MethodInfo from a Property Get method.
    ///  
    ///  The property information.
    ///  if set to true  [non public].
    /// 
    /// The cached MethodInfo.
    ///  
    public static Func? GetCacheGetMethod(this PropertyInfo propertyInfo, Boolean nonPublic = false) {
      Tuple key = Tuple.Create(!nonPublic, propertyInfo);
      // TODO: Fix public logic
      return !nonPublic && !CacheGetMethods.Value.ContainsKey(key) && !propertyInfo.GetGetMethod(true)!.IsPublic ? null : CacheGetMethods.Value.GetOrAdd(key, x => y => x.Item2.GetGetMethod(nonPublic)?.Invoke(y, null)!);
      //y => x => y.Item2.CreatePropertyProxy().GetValue(x));
    }
    /// 
    /// Gets a MethodInfo from a Property Set method.
    ///  
    ///  The property information.
    ///  if set to true  [non public].
    /// 
    /// The cached MethodInfo.
    ///  
    public static Action? GetCacheSetMethod(this PropertyInfo propertyInfo, Boolean nonPublic = false) {
      Tuple key = Tuple.Create(!nonPublic, propertyInfo);
      return !nonPublic && !CacheSetMethods.Value.ContainsKey(key) && !propertyInfo.GetSetMethod(true)!.IsPublic ? null : CacheSetMethods.Value.GetOrAdd(key, x => (obj, args) => x.Item2.GetSetMethod(nonPublic)!.Invoke(obj, args));
      //y => (obj, args) => y.Item2.CreatePropertyProxy().SetValue(obj, args));
    }
    /// 
    /// Convert a string to a boolean.
    ///  
    ///  The string.
    /// 
    ///   true  if the string represents a valid truly value, otherwise false .
    ///  
    public static Boolean ToBoolean(this String str) {
      try {
        return Convert.ToBoolean(str);
      } catch(FormatException) {
        // ignored
      }
      try {
        return Convert.ToBoolean(Convert.ToInt32(str));
      } catch {
        // ignored
      }
      return false;
    }
    /// 
    /// Creates a property proxy that stores getter and setter delegates.
    ///  
    ///  The property information.
    /// 
    /// The property proxy.
    ///  
    /// this. 
    public static IPropertyProxy? CreatePropertyProxy(this PropertyInfo @this) {
      if(@this == null) {
        throw new ArgumentNullException(nameof(@this));
      }
      Type genericType = typeof(PropertyProxy<,>).MakeGenericType(@this.DeclaringType!, @this.PropertyType);
      return Activator.CreateInstance(genericType, @this) as IPropertyProxy;
    }
    /// 
    /// Convert a object to a boolean.
    ///  
    ///  The value.
    /// 
    ///   true  if the string represents a valid truly value, otherwise false .
    ///  
    public static Boolean ToBoolean(this Object value) => value.ToStringInvariant().ToBoolean();
    private static String ConvertObjectAndFormat(Type propertyType, Object value, String format) =>
      propertyType == typeof(DateTime) || propertyType == typeof(DateTime?)
      ? Convert.ToDateTime(value, CultureInfo.InvariantCulture).ToString(format)
      : propertyType == typeof(Int32) || propertyType == typeof(Int32?)
        ? Convert.ToInt32(value, CultureInfo.InvariantCulture).ToString(format)
      : propertyType == typeof(Decimal) || propertyType == typeof(Decimal?)
      ? Convert.ToDecimal(value, CultureInfo.InvariantCulture).ToString(format)
      : propertyType == typeof(Double) || propertyType == typeof(Double?)
      ? Convert.ToDouble(value, CultureInfo.InvariantCulture).ToString(format)
      : propertyType == typeof(Byte) || propertyType == typeof(Byte?)
      ? Convert.ToByte(value, CultureInfo.InvariantCulture).ToString(format)
      : value?.ToString() ?? String.Empty;
  }
}