using System.Collections;
using System.Collections.Generic;
using System;
using System.Reflection;
using System.Text.RegularExpressions;
using Godot;

namespace Rokojori
{
  public static class ReflectionHelper
  {
    public static List<Tuple<string,string>> GetExportedMembersAsJSON( object obj )
    {
      var type = obj.GetType();
      var results = new List<Tuple<string,string>>();

      foreach ( var field in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) )
      {
        if ( Attribute.IsDefined( field, typeof(ExportAttribute) ) )
        {
          var value = field.GetValue( obj );
          var json = JSON.StringifyObject( value );
          var tuple = new Tuple<string,string>( field.Name, json );
          results.Add( tuple );
        }
      }

      foreach ( var prop in type.GetProperties( BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance ) )
      {
        if ( Attribute.IsDefined( prop, typeof(ExportAttribute) ) && prop.CanRead )
        {
          var value = prop.GetValue( obj );
          var json = JSON.StringifyObject( value );
          var tuple = new Tuple<string,string>( prop.Name, json );
          results.Add( tuple );
        }
      }

      return results;
    }

    public static T Create<T>( Type type, params object[] args )
    {
      Type[] argTypes = Array.ConvertAll( args, arg => arg.GetType() );

      var constructorMethod = type.GetConstructor(
          BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
          null, argTypes, null
      );

      if ( constructorMethod == null )
      {
        return default(T);
      }
      
      return (T)constructorMethod.Invoke( args );

    }

    public static TEnum StringToEnum<TEnum>( string value ) where TEnum : struct, Enum
    {
      return (TEnum) (object) ( Enum.TryParse<TEnum>(value, false, out var result) ? result : (TEnum?) null );
    }

    public static TEnum StringToEnum<TEnum>( string value, TEnum fallback ) where TEnum : struct, Enum
    {
      return (TEnum) (object) ( Enum.TryParse<TEnum>(value, false, out var result) ? result : fallback );
    }

    public static int GetEnumSizeInBits( Type enumType )
    {
        if ( ! enumType.IsEnum )
        { 
          return -1;
        } 

        var underlyingType = Enum.GetUnderlyingType( enumType );
        return System.Runtime.InteropServices.Marshal.SizeOf( underlyingType ) * 8;
    }

    public static Type GetTypeByNameFromAssembly( string name, Assembly assembly )
    { 
      var type = assembly.GetType( name );

      if ( type != null )
      {
        return type;
      }

      var types = assembly.GetTypes();

      var genericEndingRegex = @"`\d+$";

      for ( int i = 0; i < types.Length; i++ )
      {
        var typeName = types[ i ].Name;
        var isGeneric = Regex.IsMatch( typeName, genericEndingRegex );

        if ( ! isGeneric )
        {
          continue;
        }

        var typeNameWithoutGeneric = Regex.Replace( typeName, genericEndingRegex, "" );
        
        if ( typeNameWithoutGeneric == name )
        {
          return types[ i ];
        }
      } 

      return null;

    }

    public static Type GetTypeByName( string name )
    { 
      var assemblies = new List<Assembly>();
      assemblies.AddRange( AppDomain.CurrentDomain.GetAssemblies() );
      assemblies.Reverse();

      var assembly = assemblies.Find( a =>  a.GetType( name ) != null );

      return assembly == null ? null : assembly.GetType( name );
    }

    public static bool IsList( Type type )
    {
      if ( ! ( type.IsGenericType ) )
      {
        return false;
      }

      return type.GetGenericTypeDefinition().IsAssignableFrom( typeof( List<> ) );
    }

    public static bool IsList( object objectValue )
    {
      if ( objectValue == null ) 
      { 
        return false; 
      }

      if ( ! ( objectValue is IList ) )
      {
        return false;
      }

      return IsList( objectValue.GetType() );
    }

    public static bool IsDictionary( Type type )
    {
      if ( ! ( type.IsGenericType ) )
      {
        return false;
      }

      return type.GetGenericTypeDefinition().IsAssignableFrom( typeof( Dictionary<,> ) );
    }

    public static bool IsDictionary( object objectValue )
    {
      if ( objectValue == null ) 
      { 
        return false; 
      }

      if ( ! ( objectValue is IDictionary ) )
      {
        return false;
      }

      return IsDictionary( objectValue.GetType() );
    }

    public static string GetMemberName( object obj, object member )
    {
      if ( obj == null || member == null )
      {
        return null;
      }

      var type = obj.GetType();
      var fields = type.GetFields();

      for ( int i = 0; i < fields.Length; i++ )
      {
        var value = fields[ i ].GetValue( obj );       

        if ( value == member )
        {
          return fields[ i ].Name;
        } 
      }

      return null;

    }

    public static List<FieldInfo> GetFieldInfosOfType<T>( object instance ) where T:class 
    {
      var type = instance.GetType();
      var fields = type.GetFields();
      var list = new List<FieldInfo>();

      for ( int i = 0; i < fields.Length; i++ )
      {
        var value = fields[ i ].GetValue( instance );
        var tValue = value as T;

        if ( tValue != null )
        {
          list.Add( fields[ i ] );
        } 
      }

      return list;
    }


    public static List<T> GetFieldValuesOfType<T>( object instance ) where T:class 
    {
      var type = instance.GetType();
      var fields = type.GetFields();
      var list = new List<T>();

      for ( int i = 0; i < fields.Length; i++ )
      {
        var value = fields[ i ].GetValue( instance );
        var tValue = value as T;

        if ( tValue != null )
        {
          list.Add( tValue );
        } 
      }

      return list;
    }

    public static void GetFields<C,T>( System.Action<System.Reflection.FieldInfo> action )
    {
      GetFields<T>( typeof( C ), action );
    }

    public static void GetFields<T>( System.Type c, System.Action<System.Reflection.FieldInfo> action )
    {
      var fields = c.GetFields();

      foreach( var f in fields )
      {
        if ( f.FieldType == typeof( T ) )
        {
          action( f );
        }  
      }
    }

    
    public static FieldInfo GetFieldInfo( object instance, string memberName )
    {
      var type = instance.GetType();
      var fields = type.GetFields();
      var index = -1;
      
      for ( int i = 0; i < fields.Length; i++ )
      {
        if ( fields[ i ].Name == memberName )
        {
          index = i;
        }
        
      }
      
      return index == -1 ? null : fields[ index ];
    } 

    public static List<MemberInfo> GetDataMemberInfos<T>( object instance )
    {
      var list = GetDataMemberInfos( instance, BindingFlags.Instance | BindingFlags.Public );
      list = Lists.Filter( list, m => IsType<T>( m ) );
      return list;
    }

    public static bool IsTypeOneOf<T>( T instance, params Type[] types )
    {
      if ( instance == null )
      {
        return false;
      } 

      var type = instance.GetType();

      for ( int i = 0; i < types.Length; i++ )
      {
        if ( type.IsAssignableFrom( types[ i ] ) )
        {
          return true;
        }
      }

      return false;
    }

    public static bool IsType<T>( MemberInfo mi )
    {
      Type type = null;

      if ( mi is FieldInfo fi )
      {
        type = fi.FieldType;
      }

      if ( mi is PropertyInfo pi )
      {
        type = pi.PropertyType;
      }

      return typeof( T ).IsAssignableFrom( type );
    }

    public static List<MemberInfo> GetDataMemberInfos( object instance, BindingFlags flags )
    {
      var list = new List<MemberInfo>();
      var type = instance.GetType();

      var fields = type.GetFields( flags );      

      for ( int i = 0; i < fields.Length; i++ )
      {
        list.Add( fields[ i ] );        
      }

      var properties = type.GetProperties( flags );

      for ( int i = 0; i < properties.Length; i++ )
      {
        list.Add( properties[ i ] );        
      }

      return list;
    }

    public static List<MemberInfo> GetDataMemberInfos<T>( BindingFlags flags = defaultBindings )
    {
      var list = new List<MemberInfo>();
      var type = typeof( T );

      var fields = type.GetFields( flags );      

      for ( int i = 0; i < fields.Length; i++ )
      {
        list.Add( fields[ i ] );        
      }

      var properties = type.GetProperties( flags | BindingFlags.GetProperty | BindingFlags.SetProperty );

      for ( int i = 0; i < properties.Length; i++ )
      {
        list.Add( properties[ i ] );        

        RJLog.Log( properties[ i ].Name );
      }

      return list;
    }

    public static T GetValue<T>( object instance, string name )
    {
      var mi = GetDataMemberInfo( instance, name );

      var value = GetDataMemberValue<T>( instance, mi );

      return value;
    }

    public static bool IsGenericType( object instace, Type type )
    {
      return instace != null && instace.GetType().IsGenericType && instace.GetType().GetGenericTypeDefinition() == type;
    }

    public static T GetMemberByPath<T>( this object root, string[] path, bool resolveToLast = true )
    {
      object it = root;
      var end = resolveToLast ? path.Length : ( path.Length - 1 );

      for ( int i = 0; i < end; i++ )
      {
        if ( it == null )
        {
          return default( T );
        }

        var before = it;
        var member = path[ i ];
        
        if ( it is Godot.Collections.Array array )
        {
          // RJLog.Log( "As array", before.GetType().Name, it );
          it = array[ member.ToInt() ];
        } 
        else if ( IsGenericType( it, typeof(Godot.Collections.Array<>) ) )
        {
          var indexer = it.GetType().GetProperty( "Item" );
          it = indexer.GetValue( it, new object[] { member.ToInt() });
        } 
        else if ( it is System.Collections.IList list )
        {
          // RJLog.Log( "As list", before.GetType().Name, it );
          it = list[ member.ToInt() ];
        } 
        else 
        {
          // RJLog.Log( "As object", before.GetType().Name, it );
          it = ReflectionHelper.GetValue<object>( it, member );
        }

        // RJLog.Log( before.GetType().Name, member, it, "    ", before );
      }

      return (T) it;
    }

    public static void SetValue<T>( object instance, string name, T value )
    {
      var mi = GetDataMemberInfo( instance, name );

      SetDataMemberValue<T>( instance, mi, value );
    }

    public static List<T> GetDataMemberValues<T>( object instance )
    {
      var infos = GetDataMemberInfos<T>( instance );

      return Lists.Map( infos,  i => GetDataMemberValue<T>( instance, i ) );
    }

    public static MemberInfo GetDataMemberInfo( object instance, string memberName, BindingFlags flags = BindingFlags.Public | BindingFlags.Instance )
    {
      var type = instance.GetType();
      var fieldInfo = type.GetField( memberName, flags );
      
      if ( fieldInfo != null )
      {
        return fieldInfo;
      }
      
      return type.GetProperty( memberName, flags );
    }

    public const BindingFlags defaultBindings =  BindingFlags.Public | 
                                                 BindingFlags.NonPublic | 
                                                 BindingFlags.Instance | 
                                                 BindingFlags.Static;

    public static bool HasDataMember( object instance, string memberName, BindingFlags flags = ReflectionHelper.defaultBindings )
    {
      return GetDataMemberInfo( instance, memberName, flags ) != null;
    }   

    public static Type GetMemberType( this MemberInfo memberInfo )
    {
      if ( memberInfo is FieldInfo fi )
      {
        return fi.FieldType;
      }

      if ( memberInfo is PropertyInfo pi )
      {
        return pi.PropertyType;
      }

      return null;
    }    

    public static bool IsMemberType<T>( this MemberInfo memberInfo )
    {
      return GetMemberType( memberInfo ) == typeof( T );
    }
 


    public static T GetDataMemberValue<T>( object instance, MemberInfo info ) 
    {
      if ( info == null )
      {
        return default(T);
      }

      if ( info is FieldInfo fieldInfo )
      {
        return (T) fieldInfo.GetValue( instance );
      }

      if ( info is PropertyInfo propertyInfo )
      {
        return (T) propertyInfo.GetValue( instance );
      }

      return default(T);
    }

    public static void SetDataMemberValue<T>( object instance, MemberInfo info, T value ) 
    {

      if ( info is FieldInfo fieldInfo )
      {
        fieldInfo.SetValue( instance, value );
      }

      if ( info is PropertyInfo propertyInfo )
      {
        propertyInfo.SetValue( instance, value );
      }

    }

    public static T GetDataMemberValue<T>( object instance, string memberName, BindingFlags flags = ReflectionHelper.defaultBindings ) 
    {
      return GetDataMemberValue<T>( instance, GetDataMemberInfo( instance, memberName, flags ) );      
    }

    public static void SetDataMemberValue( object instance, string memberName, object value, BindingFlags flags = ReflectionHelper.defaultBindings )
    {
      var info = GetDataMemberInfo( instance, memberName, flags );

      if ( info == null )
      {
        return;
      }

      if ( info is FieldInfo fieldInfo )
      {
        fieldInfo.SetValue( instance, value );
      }

      if ( info is PropertyInfo propertyInfo )
      {
        propertyInfo.SetValue( instance, value );
      }
    }

    public static void CopyDataMembersFromTo( object source, object target, List<string> dataMembers )
    {
      dataMembers.ForEach(
        dm =>
        {
          try
          {
            var value = GetDataMemberValue<object>( source, dm );
            SetDataMemberValue( target, dm, value );
          }
          catch( System.Exception e )
          {
            if ( source is Node n && target is Node n2 )
            {
              n.LogInfo( "Could not copy to ", dm, HierarchyName.Of( n2 ) );
            }
            else if ( source is Resource r && target is Resource r2 )
            {
              r.LogInfo( "Could not copy to ", dm, HierarchyName.Of( r2 ) );
            }
            else
            {
              RJLog.Log( "Could not copy from ", source, "to ", dm, ">>", target );
            }
            
          }

        }
      );
    }

  }
}