// Copyright (c) Wojciech Figat. All rights reserved.

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Xml;
using FlaxEditor.Content;
using FlaxEditor.GUI.Dialogs;
using FlaxEditor.Windows;
using FlaxEditor.Windows.Assets;
using FlaxEditor.Windows.Profiler;
using FlaxEngine;
using FlaxEngine.Assertions;
using FlaxEngine.GUI;
using DockPanel = FlaxEditor.GUI.Docking.DockPanel;
using DockState = FlaxEditor.GUI.Docking.DockState;
using FloatWindowDockPanel = FlaxEditor.GUI.Docking.FloatWindowDockPanel;
using Window = FlaxEngine.Window;

namespace FlaxEditor.Modules
{
    /// <summary>
    /// Manages Editor windows and popups.
    /// </summary>
    /// <seealso cref="FlaxEditor.Modules.EditorModule" />
    public sealed class WindowsModule : EditorModule
    {
        private DateTime _lastLayoutSaveTime;
        private float _projectIconScreenshotTimeout = -1;
        private string _windowsLayoutPath;

        private struct WindowRestoreData
        {
            public string AssemblyName;
            public string TypeName;

            public DockState DockState;
            public DockPanel DockedTo;
            public int DockedTabIndex;
            public float? SplitterValue = null;

            public bool SelectOnShow = false;

            public bool Maximize;
            public bool Minimize;
            public Float2 FloatSize;
            public Float2 FloatPosition;

            public Guid AssetItemID;

            // Constructor, to allow for default values
            public WindowRestoreData()
            {
            }
        }

        private readonly List<WindowRestoreData> _restoreWindows = new List<WindowRestoreData>();

        /// <summary>
        /// The main editor window.
        /// </summary>
        public Window MainWindow { get; private set; }

        /// <summary>
        /// Occurs when main editor window is being closed.
        /// </summary>
        public event Action MainWindowClosing;

        /// <summary>
        /// The content window.
        /// </summary>
        public ContentWindow ContentWin;

        /// <summary>
        /// The edit game window.
        /// </summary>
        public EditGameWindow EditWin;

        /// <summary>
        /// The game window.
        /// </summary>
        public GameWindow GameWin;

        /// <summary>
        /// The properties window.
        /// </summary>
        public PropertiesWindow PropertiesWin;

        /// <summary>
        /// The scene tree window.
        /// </summary>
        public SceneTreeWindow SceneWin;

        /// <summary>
        /// The debug log window.
        /// </summary>
        public DebugLogWindow DebugLogWin;

        /// <summary>
        /// The output log window.
        /// </summary>
        public OutputLogWindow OutputLogWin;

        /// <summary>
        /// The toolbox window.
        /// </summary>
        public ToolboxWindow ToolboxWin;

        /// <summary>
        /// The graphics quality window.
        /// </summary>
        public GraphicsQualityWindow GraphicsQualityWin;

        /// <summary>
        /// The game cooker window.
        /// </summary>
        public GameCookerWindow GameCookerWin;

        /// <summary>
        /// The profiler window.
        /// </summary>
        public ProfilerWindow ProfilerWin;

        /// <summary>
        /// The editor options window.
        /// </summary>
        public EditorOptionsWindow EditorOptionsWin;

        /// <summary>
        /// The plugins manager window.
        /// </summary>
        public PluginsWindow PluginsWin;

        /// <summary>
        /// The Visual Script debugger window.
        /// </summary>
        public VisualScriptDebuggerWindow VisualScriptDebuggerWin;

        /// <summary>
        /// List with all created editor windows.
        /// </summary>
        public readonly List<EditorWindow> Windows = new List<EditorWindow>(32);

        /// <summary>
        /// Occurs when new window gets opened and added to the editor windows list.
        /// </summary>
        public event Action<EditorWindow> WindowAdded;

        /// <summary>
        /// Occurs when new window gets closed and removed from the editor windows list.
        /// </summary>
        public event Action<EditorWindow> WindowRemoved;

        internal WindowsModule(Editor editor)
        : base(editor)
        {
            InitOrder = -75;
        }

        /// <summary>
        /// Takes the screenshot of the current viewport.
        /// </summary>
        public void TakeScreenshot()
        {
            // Select task
            SceneRenderTask target = null;
            if (Editor.Windows.EditWin.IsSelected)
            {
                // Use editor window
                target = EditWin.Viewport.Task;
            }
            else
            {
                // Use game window
                GameWin.FocusOrShow();
            }

            // Fire screenshot taking
            Screenshot.Capture(target);
        }

        /// <summary>
        /// Updates the main window title.
        /// </summary>
        public void UpdateWindowTitle()
        {
            var mainWindow = MainWindow;
            if (mainWindow)
            {
                var engineVersion = Editor.EngineProject.Version;
                var engineVersionText = engineVersion.Revision > 0 ? $"{engineVersion.Major}.{engineVersion.Minor}.{engineVersion.Revision}" : $"{engineVersion.Major}.{engineVersion.Minor}";
                var title = $"Flax Editor {engineVersionText} - \'{Editor.GameProject.Name}\'";
                mainWindow.Title = title;
            }
        }

        /// <summary>
        /// Flash main editor window to catch user attention
        /// </summary>
        public void FlashMainWindow()
        {
            MainWindow?.FlashWindow();
        }

        /// <summary>
        /// Finds the first window that is using given element to view/edit it.
        /// </summary>
        /// <param name="item">The item.</param>
        /// <returns>Editor window or null if cannot find any window.</returns>
        public EditorWindow FindEditor(ContentItem item)
        {
            if (item == null)
                return null;
            for (int i = 0; i < Windows.Count; i++)
            {
                var win = Windows[i];
                if (win.IsEditingItem(item))
                {
                    return win;
                }
            }
            return null;
        }

        /// <summary>
        /// Closes all windows that are using given element to view/edit it.
        /// </summary>
        /// <param name="item">The item.</param>
        public void CloseAllEditors(ContentItem item)
        {
            for (int i = 0; i < Windows.Count; i++)
            {
                var win = Windows[i];
                if (win.IsEditingItem(item))
                {
                    win.Close();
                    i--;
                }
            }
        }

        /// <summary>
        /// Saves the current workspace layout.
        /// </summary>
        public void SaveCurrentLayout()
        {
            _lastLayoutSaveTime = DateTime.UtcNow;
            SaveLayout(_windowsLayoutPath);
        }

        /// <summary>
        /// Loads the default workspace layout for the current editor version.
        /// </summary>
        public void LoadDefaultLayout()
        {
            var path = StringUtils.CombinePaths(Globals.EngineContentFolder, "Editor/LayoutDefault.xml");
            if (File.Exists(path))
            {
                LoadLayout(path);
            }
        }

        /// <summary>
        /// Loads the layout from the file.
        /// </summary>
        /// <param name="path">The layout file path.</param>
        /// <returns>True if layout has been loaded otherwise if failed (e.g. missing file).</returns>
        public bool LoadLayout(string path)
        {
            if (Editor.IsHeadlessMode)
                return false;

            Editor.Log(string.Format("Loading editor windows layout from \'{0}\'", path));

            if (!File.Exists(path))
            {
                Editor.LogWarning("Cannot load windows layout. File is missing.");
                return false;
            }

            XmlDocument doc = new XmlDocument();
            var masterPanel = Editor.UI.MasterPanel;

            try
            {
                doc.Load(path);
                var root = doc["DockPanelLayout"];
                if (root == null)
                {
                    Editor.LogWarning("Invalid windows layout file.");
                    return false;
                }

                // Reset existing layout
                masterPanel.ResetLayout();

                // Get metadata
                int version = int.Parse(root.Attributes["Version"].Value, CultureInfo.InvariantCulture);

                switch (version)
                {
                case 4:
                {
                    // Main window info
                    if (MainWindow)
                    {
                        var mainWindowNode = root["MainWindow"];
                        bool isMaximized = true, isMinimized = false;
                        Rectangle bounds = LoadBounds(mainWindowNode, ref isMaximized, ref isMinimized);
                        LoadWindow(MainWindow, ref bounds, isMaximized, false);
                    }

                    // Load master panel structure
                    var masterPanelNode = root["MasterPanel"];
                    if (masterPanelNode != null)
                    {
                        LoadPanel(masterPanelNode, masterPanel);
                    }

                    // Load all floating windows structure
                    var floating = root.SelectNodes("Float");
                    if (floating != null)
                    {
                        foreach (XmlElement child in floating)
                        {
                            if (child == null)
                                continue;

                            // Get window properties
                            bool isMaximized = false, isMinimized = false;
                            Rectangle bounds = LoadBounds(child, ref isMaximized, ref isMinimized);

                            // Create window and floating dock panel
                            var window = FloatWindowDockPanel.CreateFloatWindow(MainWindow.GUI, bounds.Location, bounds.Size, WindowStartPosition.Manual, string.Empty);
                            var panel = new FloatWindowDockPanel(masterPanel, window.GUI);
                            LoadWindow(panel.Window.Window, ref bounds, isMaximized, isMinimized);

                            // Load structure
                            LoadPanel(child, panel);

                            // Check if no child windows loaded (due to file errors or loading problems)
                            if (panel.TabsCount == 0 && panel.ChildPanelsCount == 0)
                            {
                                // Close empty window
                                Editor.LogWarning("Empty floating window inside layout.");
                                window.Close();
                            }
                            else
                            {
                                // Perform layout
                                var windowGUI = window.GUI;
                                windowGUI.IsLayoutLocked = false;
                                windowGUI.PerformLayout();

                                // Show
                                window.Show();
                                window.Focus();

                                // Perform layout again
                                windowGUI.PerformLayout();
                            }
                        }
                    }

                    break;
                }

                default:
                {
                    Editor.LogWarning("Unsupported windows layout version");
                    return false;
                }
                }
            }
            catch (Exception ex)
            {
                Editor.LogWarning("Failed to load windows layout.");
                Editor.LogWarning(ex);
                return false;
            }
            finally
            {
                masterPanel.PerformLayout();
            }

            return true;
        }

        private void SavePanel(XmlWriter writer, DockPanel panel)
        {
            writer.WriteAttributeString("SelectedTab", panel.SelectedTabIndex.ToString());

            for (int i = 0; i < panel.TabsCount; i++)
            {
                var win = panel.Tabs[i];
                writer.WriteStartElement("Window");

                writer.WriteAttributeString("Typename", win.SerializationTypename);

                if (win.UseLayoutData)
                {
                    writer.WriteStartElement("Data");
                    win.OnLayoutSerialize(writer);
                    writer.WriteEndElement();
                }

                writer.WriteEndElement();
            }

            for (int i = 0; i < panel.ChildPanelsCount; i++)
            {
                var p = panel.ChildPanels[i];

                // Skip empty panels
                if (p.TabsCount == 0)
                    continue;

                writer.WriteStartElement("Panel");

                DockState state = p.TryGetDockState(out float splitterValue);

                writer.WriteAttributeString("DockState", ((int)state).ToString());
                writer.WriteAttributeString("SplitterValue", splitterValue.ToString(CultureInfo.InvariantCulture));

                SavePanel(writer, p);

                writer.WriteEndElement();
            }
        }

        private void LoadPanel(XmlElement node, DockPanel panel)
        {
            int selectedTab = int.Parse(node.GetAttribute("SelectedTab"), CultureInfo.InvariantCulture);

            // Load docked windows
            var windows = node.SelectNodes("Window");
            if (windows != null)
            {
                foreach (XmlElement child in windows)
                {
                    if (child == null)
                        continue;

                    var typename = child.GetAttribute("Typename");
                    var window = GetWindow(typename);
                    if (window != null)
                    {
                        if (child.SelectSingleNode("Data") is XmlElement data)
                        {
                            window.OnLayoutDeserialize(data);
                        }
                        else
                        {
                            window.OnLayoutDeserialize();
                        }

                        window.Show(DockState.DockFill, panel);
                    }
                }
            }

            // Load child panels
            var panels = node.SelectNodes("Panel");
            if (panels != null)
            {
                foreach (XmlElement child in panels)
                {
                    if (child == null)
                        continue;

                    // Create child panel
                    DockState state = (DockState)int.Parse(child.GetAttribute("DockState"), CultureInfo.InvariantCulture);
                    float splitterValue = float.Parse(child.GetAttribute("SplitterValue"), CultureInfo.InvariantCulture);
                    var p = panel.CreateChildPanel(state, splitterValue);

                    LoadPanel(child, p);

                    // Check if panel has no docked window (due to loading problems or sth)
                    if (p.TabsCount == 0 && p.ChildPanelsCount == 0)
                    {
                        // Remove empty panel
                        Editor.LogWarning("Empty panel inside layout.");
                        p.RemoveIt();
                    }
                    else
                    {
                        p.CollapseEmptyTabsProxy();
                    }
                }
            }

            panel.SelectTab(selectedTab);
            panel.CollapseEmptyTabsProxy();
        }

        private static void SaveBounds(XmlWriter writer, Window win)
        {
            writer.WriteStartElement("Bounds");
            {
                var bounds = win.ClientBounds;
                writer.WriteAttributeString("X", bounds.X.ToString(CultureInfo.InvariantCulture));
                writer.WriteAttributeString("Y", bounds.Y.ToString(CultureInfo.InvariantCulture));
                writer.WriteAttributeString("Width", bounds.Width.ToString(CultureInfo.InvariantCulture));
                writer.WriteAttributeString("Height", bounds.Height.ToString(CultureInfo.InvariantCulture));
                writer.WriteAttributeString("IsMaximized", win.IsMaximized.ToString());
                writer.WriteAttributeString("IsMinimized", win.IsMinimized.ToString());
            }
            writer.WriteEndElement();
        }

        private static Rectangle LoadBounds(XmlElement node, ref bool isMaximized, ref bool isMinimized)
        {
            var bounds = node["Bounds"];
            var isMaximizedText = bounds.GetAttribute("IsMaximized");
            if (!string.IsNullOrEmpty(isMaximizedText) && bool.TryParse(isMaximizedText, out var tmpBool))
                isMaximized = tmpBool;
            var isMinimizedText = bounds.GetAttribute("IsMinimized");
            if (!string.IsNullOrEmpty(isMinimizedText) && bool.TryParse(isMinimizedText, out tmpBool))
                isMinimized = tmpBool;
            float x = float.Parse(bounds.GetAttribute("X"), CultureInfo.InvariantCulture);
            float y = float.Parse(bounds.GetAttribute("Y"), CultureInfo.InvariantCulture);
            float width = float.Parse(bounds.GetAttribute("Width"), CultureInfo.InvariantCulture);
            float height = float.Parse(bounds.GetAttribute("Height"), CultureInfo.InvariantCulture);
            return new Rectangle(x, y, width, height);
        }

        private static void LoadWindow(Window win, ref Rectangle bounds, bool isMaximized, bool isMinimized)
        {
            var virtualDesktopBounds = Platform.VirtualDesktopBounds;
            var virtualDesktopSafeLeftCorner = virtualDesktopBounds.Location;
            var virtualDesktopSafeRightCorner = virtualDesktopBounds.BottomRight;

            // Clamp position to match current desktop dimensions (if window was on desktop that is now inactive)
            if (bounds.X < virtualDesktopSafeLeftCorner.X || bounds.Y < virtualDesktopSafeLeftCorner.Y || bounds.X > virtualDesktopSafeRightCorner.X || bounds.Y > virtualDesktopSafeRightCorner.Y)
                bounds.Location = virtualDesktopSafeLeftCorner;

            if (isMaximized)
            {
                if (win.IsMaximized)
                    win.Restore();
                win.ClientPosition = bounds.Location;
                win.Maximize();
            }
            else
            {
                if (Mathf.Min(bounds.Size.X, bounds.Size.Y) >= 1)
                {
                    win.ClientBounds = bounds;
                }
                else
                {
                    win.ClientPosition = bounds.Location;
                }
                if (isMinimized)
                    win.Minimize();
            }
        }

        private class LayoutNameDialog : Dialog
        {
            private TextBox _textbox;

            public LayoutNameDialog()
            : base("Enter Layout Name")
            {
                var name = new TextBox(false, 8, 8, 200)
                {
                    WatermarkText = "Enter layout slot name",
                    Parent = this,
                };
                _textbox = name;

                var okButton = new Button(name.Right - 50, name.Bottom + 4, 50)
                {
                    Text = "OK",
                    Parent = this,
                };
                okButton.Clicked += OnSubmit;

                var cancelButton = new Button(okButton.Left - 54, okButton.Y, 50)
                {
                    Text = "Cancel",
                    Parent = this,
                };
                cancelButton.Clicked += OnCancel;

                _dialogSize = okButton.BottomRight + new Float2(8);
            }

            /// <inheritdoc />
            public override void OnSubmit()
            {
                var name = _textbox.Text;
                if (name.Length == 0)
                {
                    MessageBox.Show("Cannot use the empty name.");
                    return;
                }
                if (Utilities.Utils.HasInvalidPathChar(name))
                {
                    MessageBox.Show("Cannot use this name. It contains one or more invalid characters.");
                    return;
                }

                base.OnSubmit();

                var path = StringUtils.CombinePaths(Editor.LocalCachePath, "LayoutsCache", "Layout_" + name + ".xml");
                Editor.Instance.Windows.SaveLayout(path);
            }
        }

        /// <summary>
        /// Asks user for the layout name and saves the current windows layout in the current project cache folder.
        /// </summary>
        public void SaveLayout()
        {
            if (Editor.IsHeadlessMode)
                return;

            new LayoutNameDialog().Show();
        }

        /// <summary>
        /// Saves the layout to the file.
        /// </summary>
        /// <param name="path">The layout file path.</param>
        public void SaveLayout(string path)
        {
            if (Editor.IsHeadlessMode)
                return;

            //Editor.Log(string.Format("Saving editor windows layout to \'{0}\'", path));

            var settings = new XmlWriterSettings
            {
                Indent = true,
                IndentChars = "\t",
                Encoding = Encoding.UTF8,
                OmitXmlDeclaration = true,
            };

            var masterPanel = Editor.UI.MasterPanel;
            if (masterPanel == null)
                return;

            using (XmlWriter writer = XmlWriter.Create(path, settings))
            {
                writer.WriteStartDocument();
                writer.WriteStartElement("DockPanelLayout");

                // Metadata
                writer.WriteAttributeString("Version", "4");

                // Main window info
                if (MainWindow)
                {
                    writer.WriteStartElement("MainWindow");
                    SaveBounds(writer, MainWindow);
                    writer.WriteEndElement();
                }

                // Master panel structure
                writer.WriteStartElement("MasterPanel");
                SavePanel(writer, masterPanel);
                writer.WriteEndElement();

                // Save all floating windows structure
                for (int i = 0; i < masterPanel.FloatingPanels.Count; i++)
                {
                    var panel = masterPanel.FloatingPanels[i];
                    var window = panel.Window;
                    if (window == null)
                        continue;

                    writer.WriteStartElement("Float");
                    SavePanel(writer, panel);
                    SaveBounds(writer, window.Window);
                    writer.WriteEndElement();
                }

                writer.WriteEndElement();
                writer.WriteEndDocument();
            }
        }

        /// <summary>
        /// Opens the specified editor window (shows it with editor options handling for new windows).
        /// </summary>
        /// <param name="window">The window.</param>
        public void Open(EditorWindow window)
        {
            var newLocation = (DockState)Editor.Options.Options.Interface.NewWindowLocation;
            if (newLocation == DockState.Float)
            {
                // Check if there is a floating window that has the same size
                var dpi = (float)Platform.Dpi / 96.0f;
#if PLATFORM_MAC && !PLATFORM_SDL
                dpi = 1.0f; // TODO: refactor DPI support on macOS to skip such hacks
#endif
                var dpiScale = Platform.CustomDpiScale;
                var defaultSize = window.DefaultSize * dpi;
                for (var i = 0; i < Editor.UI.MasterPanel.FloatingPanels.Count; i++)
                {
                    var win = Editor.UI.MasterPanel.FloatingPanels[i];
                    if (Float2.Abs(win.Size - defaultSize).LengthSquared < 100)
                    {
                        window.Show(DockState.DockFill, win);
                        window.Focus();
                        return;
                    }
                }

                window.ShowFloating(defaultSize * dpiScale);
            }
            else
            {
                window.Show(newLocation);
            }
        }

        /// <summary>
        /// Gets <see cref="EditorWindow"/> that is represented by the given serialized typename. Used to restore workspace layout.
        /// </summary>
        /// <param name="typename">The typename.</param>
        /// <returns>The window or null if failed.</returns>
        private EditorWindow GetWindow(string typename)
        {
            // Try use already opened window
            for (int i = 0; i < Windows.Count; i++)
            {
                if (string.Equals(Windows[i].SerializationTypename, typename, StringComparison.OrdinalIgnoreCase))
                    return Windows[i];
            }

            // Check if it's an asset ID
            if (Guid.TryParse(typename, out Guid id))
            {
                var el = Editor.ContentDatabase.Find(id);
                if (el != null)
                {
                    // Open asset
                    return Editor.ContentEditing.Open(el, true);
                }
            }

            return null;
        }

        /// <inheritdoc />
        public override void OnInit()
        {
            Assert.IsNull(MainWindow);

            _windowsLayoutPath = StringUtils.CombinePaths(Globals.ProjectCacheFolder, "WindowsLayout.xml");

            if (!Editor.IsHeadlessMode)
            {
                // Create main window
                var settings = CreateWindowSettings.Default;
                settings.Title = "Flax Editor";
                settings.Size = Platform.DesktopSize * 0.75f;
                settings.MinimumSize = new Float2(200, 150);
                settings.StartPosition = WindowStartPosition.CenterScreen;
                settings.ShowAfterFirstPaint = true;

                if (Utilities.Utils.UseCustomWindowDecorations(isMainWindow: true))
                {
                    settings.HasBorder = false;
#if PLATFORM_WINDOWS && !PLATFORM_SDL
                    // Skip OS sizing frame and implement it using LeftButtonHit
                    settings.HasSizingFrame = false;
#endif
                }
#if PLATFORM_LINUX && !PLATFORM_SDL
                settings.HasBorder = false;
#endif
                MainWindow = Platform.CreateWindow(ref settings);
                if (MainWindow == null)
                {
                    Editor.LogError("Failed to create editor main window!");
                    return;
                }
                UpdateWindowTitle();

                // Link for main window events
                MainWindow.Closing += MainWindow_OnClosing;
                MainWindow.Closed += MainWindow_OnClosed;
            }

            // Create default editor windows
            ContentWin = new ContentWindow(Editor);
            EditWin = new EditGameWindow(Editor);
            GameWin = new GameWindow(Editor);
            PropertiesWin = new PropertiesWindow(Editor);
            SceneWin = new SceneTreeWindow(Editor);
            DebugLogWin = new DebugLogWindow(Editor);
            OutputLogWin = new OutputLogWindow(Editor);
            ToolboxWin = new ToolboxWindow(Editor);
            GraphicsQualityWin = new GraphicsQualityWindow(Editor);
            GameCookerWin = new GameCookerWindow(Editor);
            ProfilerWin = new ProfilerWindow(Editor);
            EditorOptionsWin = new EditorOptionsWindow(Editor);
            PluginsWin = new PluginsWindow(Editor);
            VisualScriptDebuggerWin = new VisualScriptDebuggerWindow(Editor);

            // Bind events
            Level.SceneSaveError += OnSceneSaveError;
            Level.SceneLoaded += OnSceneLoaded;
            Level.SceneLoadError += OnSceneLoadError;
            Level.SceneLoading += OnSceneLoading;
            Level.SceneSaved += OnSceneSaved;
            Level.SceneSaving += OnSceneSaving;
            Level.SceneUnloaded += OnSceneUnloaded;
            Level.SceneUnloading += OnSceneUnloading;
            Editor.ContentDatabase.WorkspaceRebuilt += OnWorkspaceRebuilt;
            Editor.StateMachine.StateChanged += OnEditorStateChanged;
        }

        internal void AddToRestore(AssetEditorWindow win)
        {
            AddToRestore(win, win.GetType(), new WindowRestoreData
            {
                AssetItemID = win.Item.ID,
            });
        }

        internal void AddToRestore(CustomEditorWindow win)
        {
            // Validate if can restore type
            var type = win.GetType();
            var constructor = type.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null);
            if (constructor == null || type.IsGenericType)
                return;

            AddToRestore(win.Window, type, new WindowRestoreData());
        }

        private void AddToRestore(EditorWindow win, Type type, WindowRestoreData winData)
        {
            // Ensure that this window is only selected following recompilation
            // if it was the active tab in its dock panel. Otherwise, there is a
            // risk of interrupting the user's workflow by potentially selecting
            // background tabs.
            var window = win.RootWindow?.Window;
            var panel = win.ParentDockPanel;
            winData.SelectOnShow = panel.SelectedTab == win;
            winData.DockedTabIndex = 0;
            if (panel is FloatWindowDockPanel && window != null && panel.TabsCount == 1)
            {
                winData.DockState = DockState.Float;
                winData.FloatPosition = window.Position;
                winData.FloatSize = window.ClientSize;
                winData.Maximize = window.IsMaximized;
                winData.Minimize = window.IsMinimized;
                winData.DockedTo = panel;
            }
            else
            {
                for (int i = 0; i < panel.Tabs.Count; i++)
                {
                    if (panel.Tabs[i] == win)
                    {
                        winData.DockedTabIndex = i;
                        break;
                    }
                }
                if (panel.TabsCount > 1)
                {
                    winData.DockState = DockState.DockFill;
                    winData.DockedTo = panel;
                }
                else
                {
                    winData.DockState = panel.TryGetDockState(out var splitterValue);
                    winData.DockedTo = panel.ParentDockPanel;
                    winData.SplitterValue = splitterValue;
                }
            }
            winData.AssemblyName = type.Assembly.GetName().Name;
            winData.TypeName = type.FullName;
            _restoreWindows.Add(winData);
        }

        private void OnWorkspaceRebuilt()
        {
            // Go in reverse order to create floating Prefab windows first before docked windows
            for (int i = _restoreWindows.Count - 1; i >= 0; i--)
            {
                var winData = _restoreWindows[i];

                try
                {
                    var assembly = Utils.GetAssemblyByName(winData.AssemblyName);
                    if (assembly == null)
                        continue;

                    var type = assembly.GetType(winData.TypeName);
                    if (type == null)
                        continue;

                    if (type.IsAssignableTo(typeof(AssetEditorWindow)))
                    {
                        var assetItem = Editor.ContentDatabase.FindAsset(winData.AssetItemID);
                        var assetType = assetItem.GetType();
                        var ctor = type.GetConstructor(new Type[] { typeof(Editor), assetType });
                        var win = (AssetEditorWindow)ctor.Invoke(new object[] { Editor.Instance, assetItem });

                        win.Show(winData.DockState, winData.DockState != DockState.Float ? winData.DockedTo : null, winData.SelectOnShow, winData.SplitterValue);
                        if (winData.DockState == DockState.Float)
                        {
                            var window = win.RootWindow.Window;
                            window.Position = winData.FloatPosition;
                            if (winData.Maximize)
                            {
                                window.Maximize();
                            }
                            else if (winData.Minimize)
                            {
                                window.Minimize();
                            }
                            else
                            {
                                window.ClientSize = winData.FloatSize;
                            }

                            // Update panel reference in other windows docked to this panel
                            foreach (ref var otherData in CollectionsMarshal.AsSpan(_restoreWindows))
                            {
                                if (otherData.DockedTo == winData.DockedTo)
                                    otherData.DockedTo = win.ParentDockPanel;
                            }
                        }
                        var panel = win.ParentDockPanel;
                        int currentTabIndex = 0;
                        for (int pi = 0; pi < panel.TabsCount; pi++)
                        {
                            if (panel.Tabs[pi] == win)
                            {
                                currentTabIndex = pi;
                                break;
                            }
                        }
                        while (currentTabIndex > winData.DockedTabIndex)
                        {
                            win.ParentDockPanel.MoveTabLeft(currentTabIndex);
                            currentTabIndex--;
                        }
                        while (currentTabIndex < winData.DockedTabIndex)
                        {
                            win.ParentDockPanel.MoveTabRight(currentTabIndex);
                            currentTabIndex++;
                        }
                        panel.PerformLayout(true);
                    }
                    else
                    {
                        var win = (CustomEditorWindow)Activator.CreateInstance(type);
                        win.Show(winData.DockState, winData.DockedTo, winData.SelectOnShow, winData.SplitterValue);
                        if (winData.DockState == DockState.Float)
                        {
                            var window = win.Window.RootWindow.Window;
                            window.Position = winData.FloatPosition;
                            if (winData.Maximize)
                            {
                                window.Maximize();
                            }
                            else if (winData.Minimize)
                            {
                                window.Minimize();
                            }
                            else
                            {
                                window.ClientSize = winData.FloatSize;
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    Editor.LogWarning(ex);
                    Editor.LogWarning(string.Format("Failed to restore window {0} (assembly: {1})", winData.TypeName, winData.AssemblyName));
                }
            }

            // Restored windows stole the focus from Editor
            if (_restoreWindows.Count > 0)
                Editor.Instance.Windows.MainWindow.Focus();

            _restoreWindows.Clear();
        }

        private void MainWindow_OnClosing(ClosingReason reason, ref bool cancel)
        {
            Editor.Log("Main window is closing, reason: " + reason);

            if (Editor.StateMachine.IsPlayMode)
            {
                // Cancel closing but leave the play mode
                cancel = true;
                Editor.Log("Skip closing editor and leave the play mode");
                Editor.Simulation.RequestStopPlay();
                return;
            }

            SaveCurrentLayout();

            // Block closing only on user events
            if (reason == ClosingReason.User)
            {
                // Check if cancel action or save scene before exit
                if (Editor.Scene.CheckSaveBeforeClose())
                {
                    // Cancel
                    cancel = true;
                    return;
                }

                // Close all asset editor windows
                for (int i = 0; i < Windows.Count; i++)
                {
                    if (Windows[i] is AssetEditorWindow assetEditorWindow)
                    {
                        if (assetEditorWindow.Close(ClosingReason.User))
                        {
                            // Cancel
                            cancel = true;
                            return;
                        }

                        // Remove it
                        OnWindowRemove(assetEditorWindow);
                        i--;
                    }
                }
            }

            MainWindowClosing?.Invoke();
        }

        private void MainWindow_OnClosed()
        {
            Editor.Log("Main window is closed");
            MainWindow = null;

            // Capture project icon screenshot (not in play mode and if editor was used for some time)
            if (!Editor.StateMachine.IsPlayMode &&
                Time.TimeSinceStartup >= 5.0f &&
                !Editor.IsHeadlessMode &&
                EditWin.Viewport.Task != null &&
                EditWin.Viewport.Task.LastUsedFrame > 100 &&
                GPUDevice.Instance?.RendererType != RendererType.Null)
            {
                Editor.Log("Capture project icon screenshot");
                _projectIconScreenshotTimeout = Time.TimeSinceStartup + 0.8f; // wait 800ms for a screenshot task
                EditWin.Viewport.SaveProjectIcon();
            }
            else
            {
                // Close editor
                Engine.RequestExit();
            }
        }

        internal void OnWindowAdd(EditorWindow window)
        {
            Windows.Add(window);
            WindowAdded?.Invoke(window);
        }

        internal void OnWindowRemove(EditorWindow window)
        {
            Windows.Remove(window);
            WindowRemoved?.Invoke(window);
        }

        /// <inheritdoc />
        public override void OnEndInit()
        {
            UpdateWindowTitle();

            // Initialize windows
            for (int i = 0; i < Windows.Count; i++)
            {
                try
                {
                    Windows[i].OnInit();
                }
                catch (Exception ex)
                {
                    Editor.LogWarning(ex);
                    Editor.LogError("Failed to init window " + Windows[i]);
                }
            }

            // Load current workspace layout
            if (!LoadLayout(_windowsLayoutPath))
                LoadDefaultLayout();

            // Clear timer flag
            _lastLayoutSaveTime = DateTime.UtcNow;
        }

        /// <inheritdoc />
        public override void OnUpdate()
        {
            Profiler.BeginEvent("WindowsModule.Update");

            // Auto save workspace layout every few seconds
            var now = DateTime.UtcNow;
            if (_lastLayoutSaveTime.Ticks > 10 && now - _lastLayoutSaveTime >= TimeSpan.FromSeconds(10))
            {
                Profiler.BeginEvent("Save Layout");
                SaveCurrentLayout();
                Profiler.EndEvent();
            }

            // Auto close on project icon saving end
            if (_projectIconScreenshotTimeout > 0 && Time.TimeSinceStartup > _projectIconScreenshotTimeout)
            {
                Editor.Log("Closing Editor after project icon screenshot");
                EditWin.Viewport.SaveProjectIconEnd();
                Engine.RequestExit();
            }

            // Update editor windows
            for (int i = 0; i < Windows.Count; i++)
            {
                Windows[i].OnUpdate();
            }

            Profiler.EndEvent();
        }

        /// <inheritdoc />
        public override void OnExit()
        {
            // Unbind events
            Level.SceneSaveError -= OnSceneSaveError;
            Level.SceneLoaded -= OnSceneLoaded;
            Level.SceneLoadError -= OnSceneLoadError;
            Level.SceneLoading -= OnSceneLoading;
            Level.SceneSaved -= OnSceneSaved;
            Level.SceneSaving -= OnSceneSaving;
            Level.SceneUnloaded -= OnSceneUnloaded;
            Level.SceneUnloading -= OnSceneUnloading;
            Editor.ContentDatabase.WorkspaceRebuilt -= OnWorkspaceRebuilt;
            Editor.StateMachine.StateChanged -= OnEditorStateChanged;

            // Close main window
            MainWindow?.Close(ClosingReason.EngineExit);
            MainWindow = null;

            // Close all windows
            var windows = Windows.ToArray();
            for (int i = 0; i < windows.Length; i++)
            {
                if (windows[i] != null)
                    windows[i].Close(ClosingReason.EngineExit);
            }
        }

        #region Window Events

        private void OnEditorStateChanged()
        {
            for (int i = 0; i < Windows.Count; i++)
                Windows[i].OnEditorStateChanged();
        }

        private void OnSceneSaveError(Scene scene, Guid sceneId)
        {
            for (int i = 0; i < Windows.Count; i++)
                Windows[i].OnSceneSaveError(scene, sceneId);
        }

        private void OnSceneLoaded(Scene scene, Guid sceneId)
        {
            for (int i = 0; i < Windows.Count; i++)
                Windows[i].OnSceneLoaded(scene, sceneId);
        }

        private void OnSceneLoadError(Scene scene, Guid sceneId)
        {
            for (int i = 0; i < Windows.Count; i++)
                Windows[i].OnSceneLoadError(scene, sceneId);
        }

        private void OnSceneLoading(Scene scene, Guid sceneId)
        {
            for (int i = 0; i < Windows.Count; i++)
                Windows[i].OnSceneLoading(scene, sceneId);
        }

        private void OnSceneSaved(Scene scene, Guid sceneId)
        {
            for (int i = 0; i < Windows.Count; i++)
                Windows[i].OnSceneSaved(scene, sceneId);
        }

        private void OnSceneSaving(Scene scene, Guid sceneId)
        {
            for (int i = 0; i < Windows.Count; i++)
                Windows[i].OnSceneSaving(scene, sceneId);
        }

        private void OnSceneUnloaded(Scene scene, Guid sceneId)
        {
            for (int i = 0; i < Windows.Count; i++)
                Windows[i].OnSceneUnloaded(scene, sceneId);
        }

        private void OnSceneUnloading(Scene scene, Guid sceneId)
        {
            for (int i = 0; i < Windows.Count; i++)
                Windows[i].OnSceneUnloading(scene, sceneId);
        }

        /// <inheritdoc />
        public override void OnPlayBeginning()
        {
            for (int i = 0; i < Windows.Count; i++)
                Windows[i].OnPlayBeginning();
        }

        /// <inheritdoc />
        public override void OnPlayBegin()
        {
            for (int i = 0; i < Windows.Count; i++)
                Windows[i].OnPlayBegin();
        }

        /// <inheritdoc />
        public override void OnPlayEnding()
        {
            for (int i = 0; i < Windows.Count; i++)
                Windows[i].OnPlayEnding();
        }

        /// <inheritdoc />
        public override void OnPlayEnd()
        {
            for (int i = 0; i < Windows.Count; i++)
                Windows[i].OnPlayEnd();
        }

        #endregion
    }
}
