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

namespace Rokojori.Tools;

public static class ObjectGraphInspector
{
  public class Stats
  {
    public int totalObjects;
    public int maxDepth;
    public Dictionary<Type, int> typeCounts = new();
    public Dictionary<string, int> memberInfos = new();

    public override string ToString()
    {
      var info =  
        "Total Objects:" + totalObjects + " MaxDepth: " + maxDepth;  

      foreach ( var t in typeCounts )
      {
        info += "\n";
        info += t.Key + ": " + t.Value;
      }

      foreach ( var t in memberInfos )
      {
        info += "\n";
        info += t.Key + ": " + t.Value;
      }

      return info;
    }
  }

  public static Stats Inspect( object root, int maxDepth = 64 )
  {
    var visited = new HashSet<object>(ReferenceEqualityComparer.Instance);
    var stats = new Stats();

    Walk( root, 0, maxDepth, visited, stats, "<root>" );
    stats.totalObjects = visited.Count;

    return stats;
  }

  static void Walk( object obj, int depth, int maxDepth,  HashSet<object> visited, Stats stats, string memberInfo  )
  {
    if ( obj == null )
    {
      return;
    }

    var type = obj.GetType();

    if ( type.IsValueType || obj is string )
    {
      return;
    }

    if ( ! visited.Add(obj) )
    {
      return; 
    }

    stats.maxDepth = Math.Max( stats.maxDepth, depth );

    stats.typeCounts.TryGetValue( type, out int count );
    stats.typeCounts[ type ] = count + 1;

    var combinedInfo = memberInfo + "(" + type.Name + ")";

    stats.memberInfos.TryGetValue( combinedInfo, out int memberCount );
    stats.memberInfos[ combinedInfo ] = memberCount + 1;

    if ( depth >= maxDepth )
    {
      return;
    }

    if ( obj is IEnumerable enumerable )
    {
      foreach ( var item in enumerable )
      {
        Walk( item, depth + 1, maxDepth, visited, stats, type.Name + "." + item );
      }
    }

    var flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;

    foreach ( var field in type.GetFields( flags ) )
    {
      if ( field.FieldType.IsValueType || field.FieldType == typeof(string))
      {
        continue;
      }

      try
      {
        var value = field.GetValue( obj );
        Walk( value, depth + 1, maxDepth, visited, stats, type.Name + "." + field.Name );
      }
      catch
      {
          // Ignore reflection failures
      }
    }
  }

  class ReferenceEqualityComparer : IEqualityComparer<object>
  {
    public static readonly ReferenceEqualityComparer Instance = new();

    public new bool Equals(object x, object y) => ReferenceEquals(x, y);

    public int GetHashCode(object obj) =>
        System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj);
  }
}