#if ENABLE_INPUT_SYSTEM && ENABLE_INPUT_SYSTEM_PACKAGE
#define USE_INPUT_SYSTEM
#endif

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor.Callbacks;
using UnityEditor.Rendering.Analytics;
using UnityEditorInternal;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.Rendering;

namespace UnityEditor.Rendering
{
#pragma warning disable 414

    [Serializable]
    sealed class WidgetStateDictionary : SerializedDictionary<string, DebugState> { }

    sealed class DebugWindowSettings : ScriptableObject
    {
        // Keep these settings in a separate scriptable object so we can handle undo/redo on them
        // without the rest of the debug window interfering
        public int currentStateHash;

        public int selectedPanel
        {
            get => Mathf.Max(0, DebugManager.instance.PanelIndex(selectedPanelDisplayName));
            set
            {
                var displayName = DebugManager.instance.PanelDisplayName(value);
                if (!string.IsNullOrEmpty(displayName))
                    selectedPanelDisplayName = displayName;
            }
        }

        public string selectedPanelDisplayName;

        void OnEnable()
        {
            hideFlags = HideFlags.HideAndDontSave;
        }
    }

    [CoreRPHelpURL("Rendering-Debugger")]
    sealed class DebugWindow : EditorWindowWithHelpButton, IHasCustomMenu
    {
        static Styles s_Styles;
        static GUIStyle s_SplitterLeft;

        static float splitterPos = 150f;
        const float minSideBarWidth = 100;
        const float minContentWidth = 100;
        bool dragging = false;

        [SerializeField]
        WidgetStateDictionary m_WidgetStates;

        [SerializeField]
        DebugWindowSettings m_Settings;

        bool m_IsDirty;

        Vector2 m_PanelScroll;
        Vector2 m_ContentScroll;

        static bool s_TypeMapDirty;
        static Dictionary<Type, Type> s_WidgetStateMap; // DebugUI.Widget type -> DebugState type
        static Dictionary<Type, DebugUIDrawer> s_WidgetDrawerMap; // DebugUI.Widget type -> DebugUIDrawer

        public static bool open
        {
            get => DebugManager.instance.displayEditorUI;
            private set => DebugManager.instance.displayEditorUI = value;
        }

        [DidReloadScripts]
        static void OnEditorReload()
        {
            s_TypeMapDirty = true;

            //find if it where open, relink static event end propagate the info
            open = (Resources.FindObjectsOfTypeAll<DebugWindow>()?.Length ?? 0) > 0;
        }

        static void RebuildTypeMaps()
        {
            // Map states to widget (a single state can map to several widget types if the value to
            // serialize is the same)
            var attrType = typeof(DebugStateAttribute);
            var stateTypes = CoreUtils.GetAllTypesDerivedFrom<DebugState>()
                .Where(
                    t => t.IsDefined(attrType, false)
                    && !t.IsAbstract
                );

            s_WidgetStateMap = new Dictionary<Type, Type>();

            foreach (var stateType in stateTypes)
            {
                var attr = (DebugStateAttribute)stateType.GetCustomAttributes(attrType, false)[0];

                foreach (var t in attr.types)
                    s_WidgetStateMap.Add(t, stateType);
            }

            // Drawers
            attrType = typeof(DebugUIDrawerAttribute);
            var types = CoreUtils.GetAllTypesDerivedFrom<DebugUIDrawer>()
                .Where(
                    t => t.IsDefined(attrType, false)
                    && !t.IsAbstract
                );

            s_WidgetDrawerMap = new Dictionary<Type, DebugUIDrawer>();

            foreach (var t in types)
            {
                var attr = (DebugUIDrawerAttribute)t.GetCustomAttributes(attrType, false)[0];
                var inst = (DebugUIDrawer)Activator.CreateInstance(t);
                s_WidgetDrawerMap.Add(attr.type, inst);
            }

            // Done
            s_TypeMapDirty = false;
        }

        [MenuItem("Window/Analysis/Rendering Debugger", priority = 10005)]
        static void Init()
        {
            var window = GetWindow<DebugWindow>();
            window.titleContent = Styles.windowTitle;
            window.minSize = new Vector2(800f, 300f);
        }

        [MenuItem("Window/Analysis/Rendering Debugger", validate = true)]
        static bool ValidateMenuItem()
        {
            return RenderPipelineManager.currentPipeline != null;
        }

        void OnEnable()
        {
            open = true;

            DebugManager.instance.refreshEditorRequested = false;

            hideFlags = HideFlags.HideAndDontSave;
            autoRepaintOnSceneChange = true;

            if (m_Settings == null)
                m_Settings = CreateInstance<DebugWindowSettings>();

            // States are ScriptableObjects (necessary for Undo/Redo) but are not saved on disk so when the editor is closed then reopened, any existing debug window will have its states set to null
            // Since we don't care about persistence in this case, we just re-init everything.
            if (m_WidgetStates == null || !AreWidgetStatesValid())
                m_WidgetStates = new WidgetStateDictionary();

            if (s_WidgetStateMap == null || s_WidgetDrawerMap == null || s_TypeMapDirty)
                RebuildTypeMaps();

            Undo.undoRedoPerformed += OnUndoRedoPerformed;
            DebugManager.instance.onSetDirty += MarkDirty;

            // First init
            UpdateWidgetStates();

            EditorApplication.update -= Repaint;
            var panels = DebugManager.instance.panels;
            var selectedPanelIndex = m_Settings.selectedPanel;
            if (selectedPanelIndex >= 0
                && selectedPanelIndex < panels.Count
                && panels[selectedPanelIndex].editorForceUpdate)
                EditorApplication.update += Repaint;

            GraphicsToolLifetimeAnalytic.WindowOpened<DebugWindow>();
        }

        // Note: this won't get called if the window is opened when the editor itself is closed
        void OnDestroy()
        {
            open = false;
            DebugManager.instance.onSetDirty -= MarkDirty;
            Undo.ClearUndo(m_Settings);

            DestroyWidgetStates();
        }

        private void OnDisable()
        {
            GraphicsToolLifetimeAnalytic.WindowClosed<DebugWindow>();
        }

        public void DestroyWidgetStates()
        {
            if (m_WidgetStates == null)
                return;

            // Clear all the states from memory
            foreach (var state in m_WidgetStates)
            {
                var s = state.Value;
                Undo.ClearUndo(s); // Don't leave dangling states in the global undo/redo stack
                DestroyImmediate(s);
            }

            m_WidgetStates.Clear();
        }

        public void ReloadWidgetStates()
        {
            if (m_WidgetStates == null)
                return;

            // Clear states from memory that don't have a corresponding widget
            List<string> keysToRemove = new ();
            foreach (var state in m_WidgetStates)
            {
                var widget = DebugManager.instance.GetItem(state.Key);
                if (widget == null)
                {
                    var s = state.Value;
                    Undo.ClearUndo(s); // Don't leave dangling states in the global undo/redo stack
                    DestroyImmediate(s);
                    keysToRemove.Add(state.Key);
                }
            }

            // Cleanup null entries because they can break the dictionary serialization
            foreach (var key in keysToRemove)
            {
                m_WidgetStates.Remove(key);
            }

            UpdateWidgetStates();
        }

        bool AreWidgetStatesValid()
        {
            foreach (var state in m_WidgetStates)
            {
                if (state.Value == null)
                {
                    return false;
                }
            }
            return true;
        }

        void MarkDirty()
        {
            m_IsDirty = true;
        }

        // We use item states to keep a cached value of each serializable debug items in order to
        // handle domain reloads, play mode entering/exiting and undo/redo
        // Note: no removal of orphan states
        void UpdateWidgetStates()
        {
            foreach (var panel in DebugManager.instance.panels)
                UpdateWidgetStates(panel);
        }

        DebugState GetOrCreateDebugStateForValueField(DebugUI.Widget widget)
        {
            // Skip runtime & readonly only items
            if (widget.isInactiveInEditor)
                return null;

            if (widget is not DebugUI.IValueField valueField)
                return null;

            string queryPath = widget.queryPath;
            if (!m_WidgetStates.TryGetValue(queryPath, out var state) || state == null)
            {
                var widgetType = widget.GetType();
                if (s_WidgetStateMap.TryGetValue(widgetType, out Type stateType))
                {
                    Assert.IsNotNull(stateType);
                    state = (DebugState)CreateInstance(stateType);
                    state.queryPath = queryPath;
                    state.SetValue(valueField.GetValue(), valueField);
                    m_WidgetStates[queryPath] = state;
                }
            }

            return state;
        }

        void UpdateWidgetStates(DebugUI.IContainer container)
        {
            // Skip runtime only containers, we won't draw them so no need to serialize them either
            if (container is DebugUI.Widget actualWidget && actualWidget.isInactiveInEditor)
                return;

            // Recursively update widget states
            foreach (var widget in container.children)
            {
                // Skip non-serializable widgets but still traverse them in case one of their
                // children needs serialization support
                var state = GetOrCreateDebugStateForValueField(widget);

                if (state != null)
                    continue;

                // Recurse if the widget is a container
                if (widget is DebugUI.IContainer containerField)
                    UpdateWidgetStates(containerField);
            }
        }

        public void ApplyStates(bool forceApplyAll = false)
        {
            // If we are in playmode, and the runtime UI is shown, avoid that the editor UI
            // applies the data of the internal debug states, as they are not kept in sync
            if (Application.isPlaying && DebugManager.instance.displayRuntimeUI)
                return;

            if (!forceApplyAll && DebugState.m_CurrentDirtyState != null)
            {
                ApplyState(DebugState.m_CurrentDirtyState.queryPath, DebugState.m_CurrentDirtyState);
            }
            else
            {
                foreach (var state in m_WidgetStates)
                    ApplyState(state.Key, state.Value);
            }

            DebugState.m_CurrentDirtyState = null;
        }

        void ApplyState(string queryPath, DebugState state)
        {
            if (state == null || !(DebugManager.instance.GetItem(queryPath) is DebugUI.IValueField widget))
                return;

            widget.SetValue(state.GetValue());
        }

        void OnUndoRedoPerformed()
        {
            int stateHash = ComputeStateHash();

            // Something has been undone / redone, re-apply states to the debug tree
            if (stateHash != m_Settings.currentStateHash)
            {
                ApplyStates(true);
                m_Settings.currentStateHash = stateHash;
            }

            Repaint();
        }

        int ComputeStateHash()
        {
            unchecked
            {
                int hash = 13;

                foreach (var state in m_WidgetStates)
                    hash = hash * 23 + state.Value.GetHashCode();

                return hash;
            }
        }

        void Update()
        {
            // If the render pipeline asset has been reloaded we force-refresh widget states in case
            // some debug values need to be refresh/recreated as well (e.g. frame settings on HD)
            if (DebugManager.instance.refreshEditorRequested)
            {
                ReloadWidgetStates();
                m_IsDirty = true;
                DebugManager.instance.refreshEditorRequested = false;
            }

            int? requestedPanelIndex = DebugManager.instance.GetRequestedEditorWindowPanelIndex();
            if (requestedPanelIndex != null)
            {
                m_Settings.selectedPanel = requestedPanelIndex.Value;
            }

            if (m_IsDirty)
            {
                UpdateWidgetStates();
                ApplyStates();
                m_IsDirty = false;

                Repaint();
            }
        }

        void OnGUI()
        {
            if (s_Styles == null)
            {
                s_Styles = new Styles();
                s_SplitterLeft = new GUIStyle();
            }

            var panels = DebugManager.instance.panels;
            int itemCount = panels.Count(x => !x.isInactiveInEditor && x.children.Count(w => !w.isInactiveInEditor) > 0);

            if (itemCount == 0)
            {
                EditorGUILayout.HelpBox("No debug item found.", MessageType.Info);
                return;
            }

            GUILayout.BeginHorizontal(EditorStyles.toolbar);
            GUILayout.FlexibleSpace();
            if (GUILayout.Button(Styles.resetButtonContent, EditorStyles.toolbarButton))
            {
                DebugManager.instance.Reset();
                DestroyWidgetStates();
                UpdateWidgetStates();
                InternalEditorUtility.RepaintAllViews();
            }

            GUILayout.EndHorizontal();

            using (new EditorGUILayout.HorizontalScope())
            {
                // Side bar
                using (var scrollScope = new EditorGUILayout.ScrollViewScope(m_PanelScroll, s_Styles.sectionScrollView, GUILayout.Width(splitterPos)))
                {
                    if (m_Settings.selectedPanel >= panels.Count)
                        m_Settings.selectedPanel = 0;

                    // Validate container id
                    while (panels[m_Settings.selectedPanel].isInactiveInEditor || panels[m_Settings.selectedPanel].children.Count(x => !x.isInactiveInEditor) == 0)
                    {
                        m_Settings.selectedPanel++;

                        if (m_Settings.selectedPanel >= panels.Count)
                            m_Settings.selectedPanel = 0;
                    }

                    // Root children are containers
                    for (int i = 0; i < panels.Count; i++)
                    {
                        var panel = panels[i];

                        if (panel.isInactiveInEditor)
                            continue;

                        if (panel.children.Count(x => !x.isInactiveInEditor) == 0)
                            continue;

                        var elementRect = GUILayoutUtility.GetRect(EditorGUIUtility.TrTextContent(panel.displayName), s_Styles.sectionElement, GUILayout.ExpandWidth(true));

                        if (m_Settings.selectedPanel == i && Event.current.type == EventType.Repaint)
                            s_Styles.selected.Draw(elementRect, false, false, false, false);

                        EditorGUI.BeginChangeCheck();
                        GUI.Toggle(elementRect, m_Settings.selectedPanel == i, panel.displayName, s_Styles.sectionElement);
                        if (EditorGUI.EndChangeCheck())
                        {
                            Undo.RegisterCompleteObjectUndo(m_Settings, $"Debug Panel '{panel.displayName}' Selection");
                            var previousPanel = m_Settings.selectedPanel >= 0 && m_Settings.selectedPanel < panels.Count
                                ? panels[m_Settings.selectedPanel]
                                : null;
                            if (previousPanel != null && previousPanel.editorForceUpdate && !panel.editorForceUpdate)
                                EditorApplication.update -= Repaint;
                            else if ((previousPanel == null || !previousPanel.editorForceUpdate) && panel.editorForceUpdate)
                                EditorApplication.update += Repaint;
                            m_Settings.selectedPanel = i;
                        }
                    }

                    m_PanelScroll = scrollScope.scrollPosition;
                }

                Rect splitterRect = new Rect(splitterPos - 3, 0, 6, Screen.height);
                GUI.Box(splitterRect, "", s_SplitterLeft);

                const float topMargin = 2f;
                GUILayout.Space(topMargin);

                // Main section - traverse current container
                using (var changedScope = new EditorGUI.ChangeCheckScope())
                {
                    using (new EditorGUILayout.VerticalScope())
                    {
                        var selectedPanel = panels[m_Settings.selectedPanel];

                        using (new EditorGUILayout.HorizontalScope())
                        {
                            var style = new GUIStyle(CoreEditorStyles.sectionHeaderStyle) { fontStyle = FontStyle.Bold };
                            EditorGUILayout.LabelField(new GUIContent(selectedPanel.displayName), style);

                            // Context menu
                            var rect = GUILayoutUtility.GetLastRect();
                            var contextMenuRect = new Rect(rect.xMax, rect.y + 4f, 16f, 16f);

                            CoreEditorUtils.ShowHelpButton(contextMenuRect, selectedPanel.documentationUrl, new GUIContent($"{selectedPanel.displayName} panel."));
                        }

                        const float leftMargin = 4f;
                        GUILayout.Space(leftMargin);

                        using (var scrollScope = new EditorGUILayout.ScrollViewScope(m_ContentScroll))
                        {
                            const float scrollViewTopMargin = 4f;
                            GUILayout.Space(scrollViewTopMargin);
                            
                            TraverseContainerGUI(selectedPanel);
                            m_ContentScroll = scrollScope.scrollPosition;
                            
                            const float scrollViewBottomMargin = 10f;
                            GUILayout.Space(scrollViewBottomMargin);
                        }
                    }

                    if (changedScope.changed)
                    {
                        m_Settings.currentStateHash = ComputeStateHash();
                        DebugManager.instance.ReDrawOnScreenDebug();
                    }
                }

                // Splitter events
                if (Event.current != null)
                {
                    switch (Event.current.rawType)
                    {
                        case EventType.MouseDown:
                            if (splitterRect.Contains(Event.current.mousePosition))
                            {
                                dragging = true;
                            }
                            break;
                        case EventType.MouseDrag:
                            if (dragging)
                            {
                                splitterPos += Event.current.delta.x;
                                splitterPos = Mathf.Clamp(splitterPos, minSideBarWidth, position.width - minContentWidth);
                                Repaint();
                            }
                            break;
                        case EventType.MouseUp:
                            if (dragging)
                            {
                                dragging = false;
                            }
                            break;
                    }
                }
                EditorGUIUtility.AddCursorRect(splitterRect, MouseCursor.ResizeHorizontal);
            }
        }

        void OnWidgetGUI(DebugUI.Widget widget)
        {
            if (widget.isInactiveInEditor || widget.isHidden)
                return;

            if (widget.queryPath == null)
            {
                Debug.LogError($"Widget {widget.GetType()} query path is null");
                return;
            }

            if (!s_WidgetDrawerMap.TryGetValue(widget.GetType(), out DebugUIDrawer drawer))
            {
                foreach (var pair in s_WidgetDrawerMap)
                {
                    if (pair.Key.IsAssignableFrom(widget.GetType()))
                    {
                        drawer = pair.Value;
                        break;
                    }
                }
            }

            if (drawer == null)
                EditorGUILayout.LabelField("Drawer not found (" + widget.GetType() + ").");
            else
            {
                var state = GetOrCreateDebugStateForValueField(widget);

                drawer.Begin(widget, state);

                if (drawer.OnGUI(widget, state))
                {
                    if (widget is DebugUI.IContainer container)
                        TraverseContainerGUI(container);
                }

                drawer.End(widget, state);
            }
        }

        void TraverseContainerGUI(DebugUI.IContainer container)
        {
            // /!\ SHAAAAAAAME ALERT /!\
            // A container can change at runtime because of the way IMGUI works and how we handle
            // onValueChanged on widget so we have to take this into account while iterating
            try
            {
                foreach (var widget in container.children)
                    OnWidgetGUI(widget);
            }
            catch (InvalidOperationException)
            {
                Repaint();
            }
        }

        public class Styles
        {
            public static float s_DefaultLabelWidth = 0.5f;

            public static GUIContent windowTitle { get; } = EditorGUIUtility.TrTextContent("Rendering Debugger");

            public static GUIContent resetButtonContent { get; } = EditorGUIUtility.TrTextContent("Reset");

            public static GUIStyle foldoutHeaderStyle { get; } = new GUIStyle(EditorStyles.foldoutHeader)
            {
                fixedHeight = 20,
                fontStyle = FontStyle.Bold,
                margin = new RectOffset(0, 0, 0, 0)
            };

            public static GUIStyle labelWithZeroValueStyle { get; } = new GUIStyle(EditorStyles.label);

            public readonly GUIStyle sectionScrollView = "PreferencesSectionBox";
            public readonly GUIStyle sectionElement = new GUIStyle("PreferencesSection");
            public readonly GUIStyle selected = "OL SelectedRow";

            public static GUIStyle centeredLeft = new GUIStyle(EditorStyles.label) { alignment = TextAnchor.MiddleLeft };
            public static GUIStyle centeredLeftAlternate = new GUIStyle(EditorStyles.label) { alignment = TextAnchor.MiddleLeft };
            public static float singleRowHeight = EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;

            public static int foldoutColumnWidth = 70;

            public Styles()
            {
                centeredLeftAlternate.normal.background = CoreEditorUtils.CreateColoredTexture2D(
                    EditorGUIUtility.isProSkin
                        ? new Color(63 / 255.0f, 63 / 255.0f, 63 / 255.0f, 255 / 255.0f)
                        : new Color(211 / 255.0f, 211 / 255.0f, 211 / 255.0f, 211 / 255.0f),
                    "centeredLeftAlternate Background");

                sectionScrollView = new GUIStyle(sectionScrollView);
                sectionScrollView.overflow.bottom += 1;

                sectionElement.alignment = TextAnchor.MiddleLeft;

                labelWithZeroValueStyle.normal.textColor = Color.gray;

                // Make sure that textures are unloaded on domain reloads.
                void OnBeforeAssemblyReload()
                {
                    DestroyImmediate(centeredLeftAlternate.normal.background);
                    AssemblyReloadEvents.beforeAssemblyReload -= OnBeforeAssemblyReload;
                }

                AssemblyReloadEvents.beforeAssemblyReload += OnBeforeAssemblyReload;
            }
        }

        public void AddItemsToMenu(GenericMenu menu)
        {
            menu.AddItem(EditorGUIUtility.TrTextContent("Expand All"), false, () => SetExpanded(true));
            menu.AddItem(EditorGUIUtility.TrTextContent("Collapse All"), false, () => SetExpanded(false));
        }

        void SetExpanded(bool value)
        {
            var panels = DebugManager.instance.panels;
            foreach (var p in panels)
            {
                foreach (var w in p.children)
                {
                    if (w.GetType() == typeof(DebugUI.Foldout))
                    {
                        if (m_WidgetStates.TryGetValue(w.queryPath, out DebugState state))
                        {
                            var foldout = (DebugUI.Foldout)w;
                            state.SetValue(value, foldout);
                            foldout.SetValue(value);
                        }
                    }
                }
            }
        }
    }

#pragma warning restore 414
}