using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Text;
using NitroxClient.Debuggers.Drawer;
using NitroxClient.MonoBehaviours;
using NitroxClient.Unity.Helper;
using UnityEngine;
using UnityEngine.SceneManagement;

namespace NitroxClient.Debuggers;

[ExcludeFromCodeCoverage]
public class SceneDebugger : BaseDebugger
{
    private readonly DrawerManager drawerManager;
    public GameObject SelectedObject { get; private set; }
    private int selectedComponentID;
    private Scene selectedScene;

    private bool showUnityMethods;
    private bool showSystemMethods;

    private Vector2 gameObjectScrollPos;
    private Vector2 hierarchyScrollPos;

    private readonly Dictionary<int, bool> componentsVisibilityByID = new();
    private readonly Dictionary<int, FieldInfo[]> cachedFieldsByComponentID = new();
    private readonly Dictionary<int, MethodInfo[]> cachedMethodsByComponentID = new();
    private readonly Dictionary<int, IDictionary<Type, bool>> enumVisibilityByComponentIDAndEnumType = new();

    public SceneDebugger() : base(650, null, KeyCode.S, true, false, false, GUISkinCreationOptions.DERIVEDCOPY)
    {
        drawerManager = new DrawerManager(this);
        ActiveTab = AddTab("Scenes", RenderTabScenes);
        AddTab("Hierarchy", RenderTabHierarchy);
        AddTab("GameObject", RenderTabGameObject);
    }

    protected override void OnSetSkin(GUISkin skin)
    {
        base.OnSetSkin(skin);

        skin.SetCustomStyle("sceneLoaded", skin.label, s =>
        {
            s.normal = new GUIStyleState { textColor = Color.green };
            s.fontStyle = FontStyle.Bold;
        });

        skin.SetCustomStyle("loadScene", skin.button, s => { s.fixedWidth = 60; });

        skin.SetCustomStyle("fillMessage", skin.label, s =>
        {
            s.stretchWidth = true;
            s.stretchHeight = true;
            s.fontSize = 24;
            s.alignment = TextAnchor.MiddleCenter;
            s.fontStyle = FontStyle.Italic;
        });

        skin.SetCustomStyle("breadcrumb", skin.label, s =>
        {
            s.fontSize = 20;
            s.fontStyle = FontStyle.Bold;
        });

        skin.SetCustomStyle("breadcrumbNav", skin.box, s =>
        {
            s.stretchWidth = false;
            s.fixedWidth = 100;
        });

        skin.SetCustomStyle("options", skin.textField, s =>
        {
            s.fixedWidth = 200;
            s.margin = new RectOffset(8, 8, 4, 4);
        });

        skin.SetCustomStyle("options_label", skin.label, s => { s.alignment = TextAnchor.MiddleLeft; });

        skin.SetCustomStyle("bold", skin.label, s =>
        {
            s.alignment = TextAnchor.LowerLeft;
            s.fontStyle = FontStyle.Bold;
        });

        skin.SetCustomStyle("boxHighlighted", skin.box, s =>
        {
            Texture2D result = new(1, 1);
            result.SetPixels(new[] { new Color(1f, 0.9f, 0f, 0.25f) });
            result.Apply();
            s.normal.background = result;
        });
    }

    private void RenderTabScenes()
    {
        using (new GUILayout.VerticalScope("box"))
        {
            GUILayout.Label("All scenes", "header");
            int maxSceneCount = Math.Max(SceneManager.sceneCount, SceneManager.sceneCountInBuildSettings);
            for (int i = 0; i < maxSceneCount + 1; i++)
            {
                // Getting the DontDestroyOnLoad though the NitroxBootstrapper instance
                Scene currentScene = i == maxSceneCount ? NitroxBootstrapper.Instance.gameObject.scene : SceneManager.GetSceneAt(i);

                bool isSelected = selectedScene.IsValid() && currentScene == selectedScene;
                bool isLoaded = currentScene.isLoaded;
                bool isDDOLScene = currentScene.name == "DontDestroyOnLoad";

                using (new GUILayout.HorizontalScope("box"))
                {
                    if (GUILayout.Button($"{(isSelected ? ">> " : "")}{i}: {(isDDOLScene ? currentScene.name : currentScene.path.TruncateLeft(35))}", isLoaded ? "sceneLoaded" : "label"))
                    {
                        selectedScene = currentScene;
                        ActiveTab = GetTab("Hierarchy").Value;
                    }

                    if (isLoaded)
                    {
                        if (!isDDOLScene && GUILayout.Button("Unload", "loadScene"))
                        {
                            SceneManager.UnloadSceneAsync(i);
                        }
                    }
                    else
                    {
                        if (!isDDOLScene && GUILayout.Button("Load", "loadScene"))
                        {
                            SceneManager.LoadSceneAsync(i);
                        }
                    }
                }
            }
        }
    }

    private void RenderTabHierarchy()
    {
        using (new GUILayout.HorizontalScope("box"))
        {
            StringBuilder breadcrumbBuilder = new();
            if (SelectedObject)
            {
                Transform parent = SelectedObject.transform;
                while (parent)
                {
                    breadcrumbBuilder.Insert(0, '/');
                    breadcrumbBuilder.Insert(0, string.IsNullOrEmpty(parent.name) ? "<no-name>" : parent.name);
                    parent = parent.parent;
                }
            }

            breadcrumbBuilder.Insert(0, "//");
            GUILayout.Label(breadcrumbBuilder.ToString(), "breadcrumb");

            using (new GUILayout.HorizontalScope("breadcrumbNav"))
            {
                if (GUILayout.Button("<<"))
                {
                    UpdateSelectedObject(null);
                }

                if (GUILayout.Button("<") && SelectedObject && SelectedObject.transform.parent)
                {
                    UpdateSelectedObject(SelectedObject.transform.parent.gameObject);
                }
            }
        }

        using (new GUILayout.VerticalScope("box"))
        {
            if (selectedScene.IsValid())
            {
                using GUILayout.ScrollViewScope scroll = new(hierarchyScrollPos);
                hierarchyScrollPos = scroll.scrollPosition;
                List<GameObject> showObjects = new();
                if (!SelectedObject)
                {
                    showObjects = selectedScene.GetRootGameObjects().ToList();
                }
                else
                {
                    foreach (Transform t in SelectedObject.transform)
                    {
                        showObjects.Add(t.gameObject);
                    }
                }

                foreach (GameObject child in showObjects)
                {
                    if (GUILayout.Button(child.name, child.transform.childCount > 0 ? "bold" : "label"))
                    {
                        UpdateSelectedObject(child);
                    }
                }
            }
            else
            {
                GUILayout.Label($"No selected scene\nClick on a Scene in '{GetTab("Hierarchy").Value.Name}'", "fillMessage");
            }
        }
    }

    private void RenderTabGameObject()
    {
        using (new GUILayout.VerticalScope("box"))
        {
            if (!SelectedObject)
            {
                GUILayout.Label($"No selected GameObject\nClick on an object in '{GetTab("Hierarchy").Value.Name}'", "fillMessage");
                return;
            }

            using GUILayout.ScrollViewScope scroll = new(gameObjectScrollPos);
            gameObjectScrollPos = scroll.scrollPosition;

            using (new GUILayout.HorizontalScope())
            {
                GUILayout.Label($"GameObject: {SelectedObject.name}", "bold", GUILayout.Height(25));
                GUILayout.Space(5);
            }

            foreach (Component component in SelectedObject.GetComponents<Component>())
            {
                int componentId = component.GetInstanceID();

                using (new GUILayout.VerticalScope(selectedComponentID == componentId ? "boxHighlighted" : "box"))
                {
                    if (!componentsVisibilityByID.TryGetValue(componentId, out bool visible))
                    {
                        visible = componentsVisibilityByID[componentId] = component is Transform or RectTransform; // Transform should be visible by default
                    }

                    Type componentType = component.GetType();
                    MonoBehaviour monoBehaviour = component as MonoBehaviour;

                    using (new GUILayout.HorizontalScope(GUILayout.Height(12f)))
                    {
                        if (monoBehaviour)
                        {
                            monoBehaviour.enabled = GUILayout.Toggle(monoBehaviour.enabled, GUIContent.none, GUILayout.Width(15));
                        }

                        GUILayout.Label(componentType.Name, "bold", GUILayout.Height(20));
                        NitroxGUILayout.Separator();

                        if (GUILayout.Button("Show / Hide", GUILayout.Width(100)))
                        {
                            componentsVisibilityByID[component.GetInstanceID()] = visible = !visible;
                        }
                    }

                    GUILayout.Space(5);

                    if (visible)
                    {
                        GUILayout.Space(10);
                        if (!drawerManager.TryDraw(component))
                        {
                            if (monoBehaviour)
                            {
                                DrawFields(monoBehaviour);
                                GUILayout.Space(20);
                                DrawMonoBehaviourMethods(monoBehaviour);
                            }
                            else
                            {
                                NitroxGUILayout.Separator();
                                GUILayout.Label("This component is not yet supported");
                                GUILayout.Space(10);
                            }
                        }
                    }

                    GUILayout.Space(3);
                }
            }
        }
    }

    private void DrawFields(UnityEngine.Object target)
    {
        if (!cachedFieldsByComponentID.TryGetValue(target.GetInstanceID(), out FieldInfo[] fields))
        {
            fields = cachedFieldsByComponentID[target.GetInstanceID()] = target.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic);
        }

        foreach (FieldInfo field in fields)
        {
            object fieldValue = field.GetValue(target);

            using (new GUILayout.HorizontalScope("box", GUILayout.MinHeight(35)))
            {
                GUILayout.Label($"[{field.FieldType.ToString().Split('.').Last()}]: {field.Name}", "options_label");
                NitroxGUILayout.Separator();

                if (fieldValue is Component component)
                {
                    if (GUILayout.Button("Goto"))
                    {
                        JumpToComponent(component);
                    }
                }
                else if (fieldValue != null && (field.FieldType.IsArray || typeof(IList).IsAssignableFrom(field.FieldType)))
                {
                    IList list = (IList)field.GetValue(target);
                    GUILayout.Box($"Length: {list.Count}", GUILayout.Width(NitroxGUILayout.VALUE_WIDTH));
                }
                else if (fieldValue != null && (typeof(IDictionary).IsAssignableFrom(field.FieldType)))
                {
                    IDictionary dict = (IDictionary)field.GetValue(target);
                    GUILayout.Box($"Length: {dict.Count}", GUILayout.Width(NitroxGUILayout.VALUE_WIDTH));
                }
                else if (drawerManager.TryDrawEditor(fieldValue, out object editedValue))
                {
                    field.SetValue(target, editedValue);
                }
                else if (!drawerManager.TryDraw(fieldValue))
                {
                    GUILayout.FlexibleSpace();

                    switch (fieldValue)
                    {
                        case null:
                            GUILayout.Box("Field is null", GUILayout.Width(NitroxGUILayout.VALUE_WIDTH));
                            break;
                        case ScriptableObject scriptableObject:
                            if (GUILayout.Button(field.Name, GUILayout.Width(NitroxGUILayout.VALUE_WIDTH)))
                            {
                                DrawFields(scriptableObject);
                            }

                            break;
                        case GameObject gameObject:
                            if (GUILayout.Button(field.Name, GUILayout.Width(NitroxGUILayout.VALUE_WIDTH)))
                            {
                                UpdateSelectedObject(gameObject);
                            }

                            break;
                        case bool boolValue:
                            if (GUILayout.Button(boolValue.ToString(), GUILayout.Width(NitroxGUILayout.VALUE_WIDTH)))
                            {
                                field.SetValue(target, !boolValue);
                            }

                            break;
                        case short:
                        case ushort:
                        case int:
                        case uint:
                        case long:
                        case ulong:
                        case float:
                        case double:
                            field.SetValue(target, NitroxGUILayout.ConvertibleField((IConvertible)fieldValue));
                            break;
                        case Enum enumValue:
                            DrawEnum(target, field, enumValue);
                            break;
                        default:
                            GUILayout.TextArea(fieldValue.ToString(), "options", GUILayout.Width(NitroxGUILayout.VALUE_WIDTH));
                            break;
                    }
                }
            }
        }
    }

    /// <summary>
    /// Draws an enum field on a component.
    /// </summary>
    /// <param name="target">The target containing the field.</param>
    /// <param name="field">The enum field</param>
    /// <param name="enumValue">The selected enum value.</param>
    private void DrawEnum(UnityEngine.Object target, FieldInfo field, Enum enumValue)
    {
        // This is the first time enountering this target type
        if (!enumVisibilityByComponentIDAndEnumType.TryGetValue(target.GetInstanceID(), out IDictionary<Type, bool> enumVisibilityByType))
        {
            // Add an empty subdictionary, it will be handled by the statement below.
            enumVisibilityByType = new Dictionary<Type, bool>();
            enumVisibilityByComponentIDAndEnumType.Add(target.GetInstanceID(), enumVisibilityByType);
        }

        // This is the first time we are encountering this enum type on this component
        if (!enumVisibilityByType.TryGetValue(field.FieldType, out _))
        {
            enumVisibilityByType.Add(field.FieldType, false);
        }

        using (new GUILayout.VerticalScope())
        {
            using (new GUILayout.HorizontalScope())
            {
                GUILayout.FlexibleSpace();

                // Toggle the visibility and save it in the subdictionary
                if (GUILayout.Button("Show / Hide", GUILayout.Width(NitroxGUILayout.VALUE_WIDTH)))
                {
                    enumVisibilityByType[field.FieldType] = !enumVisibilityByType[field.FieldType];
                }
            }

            // Show the enum list.
            if (enumVisibilityByType[field.FieldType])
            {
                GUILayout.Space(5);
                using (new GUILayout.HorizontalScope())
                {
                    GUILayout.FlexibleSpace();
                    field.SetValue(target, NitroxGUILayout.EnumPopup(enumValue, 250));
                }
            }
        }
    }

    private void DrawMonoBehaviourMethods(MonoBehaviour monoBehaviour)
    {
        using (new GUILayout.HorizontalScope("box"))
        {
            showSystemMethods = GUILayout.Toggle(showSystemMethods, "Show System inherit methods", GUILayout.Height(25));
            showUnityMethods = GUILayout.Toggle(showUnityMethods, "Show Unity inherit methods", GUILayout.Height(25));
        }

        if (!cachedMethodsByComponentID.TryGetValue(monoBehaviour.GetInstanceID(), out MethodInfo[] methods))
        {
            methods = cachedMethodsByComponentID[monoBehaviour.GetInstanceID()] = monoBehaviour.GetType().GetMethods(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy)
                                                                                               .OrderBy(m => m.Name).ToArray();
        }

        foreach (MethodInfo method in methods)
        {
            string methodAssemblyName = method.DeclaringType != null ? method.DeclaringType.Assembly.GetName().Name : string.Empty;
            if (!(!showSystemMethods && (methodAssemblyName.Contains("System") || methodAssemblyName.Contains("mscorlib"))) &&
                !(!showUnityMethods && methodAssemblyName.Contains("UnityEngine")))
            {
                using (new GUILayout.VerticalScope("box"))
                {
                    GUILayout.Label(method.ToString());

                    if (method.GetParameters().Any()) // TODO: Allow methods with parameters to be called.
                    {
                        continue;
                    }

                    if (GUILayout.Button("Invoke", GUILayout.MaxWidth(150)))
                    {
                        object result = method.Invoke(method.IsStatic ? null : monoBehaviour, Array.Empty<object>());
                        Log.InGame($"Invoked method {method.Name}");

                        if (method.ReturnType != typeof(void))
                        {
                            Log.InGame(result != null ? $"Returned: '{result}'" : "Return value was NULL.");
                        }
                    }
                }
            }
        }
    }

    public void UpdateSelectedObject(GameObject item)
    {
        if (SelectedObject == item)
        {
            return;
        }

        SelectedObject = item;
        selectedComponentID = default;
    }

    public void JumpToComponent(Component item)
    {
        UpdateSelectedObject(item.gameObject);
        RenderTabGameObject();
        selectedComponentID = item.GetInstanceID();
    }
}
