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

using System;
using System.Collections.Generic;
using System.Xml;
using FlaxEditor.Content;
using FlaxEditor.CustomEditors;
using FlaxEditor.CustomEditors.GUI;
using FlaxEditor.Gizmo;
using FlaxEditor.GUI;
using FlaxEditor.GUI.Input;
using FlaxEditor.SceneGraph;
using FlaxEditor.Viewport;
using FlaxEngine;
using FlaxEngine.GUI;

namespace FlaxEditor.Windows.Assets
{
    /// <summary>
    /// Prefab window allows to view and edit <see cref="Prefab"/> asset.
    /// </summary>
    /// <seealso cref="Prefab" />
    /// <seealso cref="FlaxEditor.Windows.Assets.AssetEditorWindow" />
    public sealed partial class PrefabWindow : AssetEditorWindowBase<Prefab>, IPresenterOwner, ISceneEditingContext
    {
        private readonly SplitPanel _split1;
        private readonly SplitPanel _split2;
        private readonly TextBox _searchBox;
        private readonly Panel _treePanel;
        private readonly PrefabTree _tree;
        private readonly PrefabWindowViewport _viewport;
        private readonly CustomEditorPresenter _propertiesEditor;
        private SearchBox _propertiesSearchBox;
        private Panel _propertiesScrollingPanel;

        private readonly ToolStripButton _saveButton;
        private readonly ToolStripButton _toolStripUndo;
        private readonly ToolStripButton _toolStripRedo;
        private readonly ToolStripButton _toolStripTranslate;
        private readonly ToolStripButton _toolStripRotate;
        private readonly ToolStripButton _toolStripScale;
        private readonly ToolStripButton _toolStripLiveReload;

        private Undo _undo;
        private bool _focusCamera;
        private bool _liveReload = false;
        private bool _isUpdatingSelection, _isScriptsReloading;
        private DateTime _modifiedTime = DateTime.MinValue;
        private bool _isDropping = false;

        private bool _lockSelection = false;

        /// <summary>
        /// Gets the prefab hierarchy tree control.
        /// </summary>
        public PrefabTree Tree => _tree;

        /// <summary>
        /// Gets the viewport.
        /// </summary>
        public PrefabWindowViewport Viewport => _viewport;

        /// <inheritdoc />
        public List<SceneGraphNode> Selection => _selection;

        /// <summary>
        /// Gets the prefab objects properties editor.
        /// </summary>
        public CustomEditorPresenter Presenter => _propertiesEditor;

        /// <summary>
        /// Gets the undo system used by this window for changes tracking.
        /// </summary>
        public Undo Undo => _undo;

        /// <summary>
        /// The local scene nodes graph used by the prefab editor.
        /// </summary>
        public readonly LocalSceneGraph Graph;

        /// <summary>
        /// Indication of if the prefab window selection is locked on specific objects.
        /// </summary>
        public bool LockSelection
        {
            get => _lockSelection;
            set
            {
                if (_lockSelection == value)
                    return;
                _lockSelection = value;
                if (!value)
                    OnSelectionChanged(Graph.Root.SceneContext.Selection.ToArray());
            }
        }

        /// <inheritdoc />
        public ISceneEditingContext SceneContext => this;

        /// <summary>
        /// Gets or sets a value indicating whether use live reloading for the prefab changes (applies prefab changes on modification by auto).
        /// </summary>
        public bool LiveReload
        {
            get => _liveReload;
            set
            {
                if (_liveReload != value)
                {
                    _liveReload = value;

                    UpdateToolstrip();
                }
            }
        }

        /// <summary>
        /// Gets or sets the live reload timeout. It defines the time to apply prefab changes after modification.
        /// </summary>
        public TimeSpan LiveReloadTimeout { get; set; } = TimeSpan.FromMilliseconds(100);

        /// <inheritdoc />
        public PrefabWindow(Editor editor, AssetItem item)
        : base(editor, item)
        {
            var inputOptions = Editor.Options.Options.Input;

            // Undo
            _undo = new Undo();
            _undo.UndoDone += OnUndoEvent;
            _undo.RedoDone += OnUndoEvent;
            _undo.ActionDone += OnUndoEvent;

            // Split Panel 1
            _split1 = new SplitPanel(Orientation.Horizontal, ScrollBars.None, ScrollBars.None)
            {
                AnchorPreset = AnchorPresets.StretchAll,
                Offsets = new Margin(0, 0, _toolstrip.Bottom, 0),
                SplitterValue = 0.2f,
                Parent = this
            };
            var sceneTreePanel = new SceneTreePanel(this)
            {
                Parent = _split1.Panel1,
            };

            // Split Panel 2
            _split2 = new SplitPanel(Orientation.Horizontal, ScrollBars.None, ScrollBars.None)
            {
                AnchorPreset = AnchorPresets.StretchAll,
                Offsets = Margin.Zero,
                SplitterValue = 0.6f,
                Parent = _split1.Panel2
            };

            // Prefab structure tree searching query input box
            var headerPanel = new ContainerControl
            {
                AnchorPreset = AnchorPresets.HorizontalStretchTop,
                BackgroundColor = Style.Current.Background,
                IsScrollable = false,
                Offsets = new Margin(0, 0, 0, 18 + 6),
            };
            _searchBox = new SearchBox
            {
                AnchorPreset = AnchorPresets.HorizontalStretchMiddle,
                Parent = headerPanel,
                Bounds = new Rectangle(4, 4, headerPanel.Width - 8, 18),
                TooltipText = "Search the prefab.\n\nYou can prefix your search with different search operators:\ns: -> Actor with script of type\na: -> Actor type\nc: -> Control type",
            };
            _searchBox.TextChanged += OnSearchBoxTextChanged;

            // Prefab structure tree
            _treePanel = new Panel
            {
                AnchorPreset = AnchorPresets.StretchAll,
                Offsets = new Margin(0.0f, 0.0f, headerPanel.Bottom, 0.0f),
                ScrollBars = ScrollBars.Both,
                IsScrollable = true,
                Parent = sceneTreePanel,
            };
            Graph = new LocalSceneGraph(new CustomRootNode(this));
            Graph.Root.TreeNode.Expand(true);
            _tree = new PrefabTree
            {
                Margin = new Margin(0.0f, 0.0f, -16.0f, _treePanel.ScrollBarsSize), // Hide root node
                IsScrollable = true,
            };
            _tree.AddChild(Graph.Root.TreeNode);
            _tree.SelectedChanged += OnTreeSelectedChanged;
            _tree.RightClick += OnTreeRightClick;
            _tree.Parent = _treePanel;
            headerPanel.Parent = sceneTreePanel;

            // Prefab viewport
            _viewport = new PrefabWindowViewport(this)
            {
                Parent = _split2.Panel1
            };
            _viewport.TransformGizmo.ModeChanged += UpdateToolstrip;

            // Prefab properties editor
            var propHeaderPanel = new ContainerControl
            {
                AnchorPreset = AnchorPresets.HorizontalStretchTop,
                BackgroundColor = Style.Current.Background,
                IsScrollable = false,
                Offsets = new Margin(0, 0, 0, 18 + 6),
                Parent = _split2.Panel2,
            };
            _propertiesSearchBox = new SearchBox
            {
                AnchorPreset = AnchorPresets.HorizontalStretchMiddle,
                Parent = propHeaderPanel,
                Bounds = new Rectangle(4, 4, propHeaderPanel.Width - 8, 18),
            };
            _propertiesSearchBox.TextChanged += ApplyPropertiesSearchFilter;

            _propertiesScrollingPanel = new Panel(ScrollBars.Vertical)
            {
                AnchorPreset = AnchorPresets.StretchAll,
                Offsets = new Margin(0, 0, propHeaderPanel.Bottom, 0),
                Parent = _split2.Panel2,
            };

            _propertiesEditor = new CustomEditorPresenter(_undo, null, this);
            _propertiesEditor.Panel.Parent = _propertiesScrollingPanel;
            _propertiesEditor.Modified += MarkAsEdited;
            _propertiesEditor.AfterLayout += OnPresenterAfterLayout;

            // Toolstrip
            _saveButton = _toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save", ref inputOptions.Save);
            _toolstrip.AddSeparator();
            _toolStripUndo = _toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo", ref inputOptions.Undo);
            _toolStripRedo = _toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo", ref inputOptions.Redo);
            _toolstrip.AddSeparator();
            _toolStripTranslate = _toolstrip.AddButton(Editor.Icons.Translate32, () => _viewport.TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Translate).LinkTooltip("Change Gizmo tool mode to Translate", ref inputOptions.TranslateMode);
            _toolStripRotate = _toolstrip.AddButton(Editor.Icons.Rotate32, () => _viewport.TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Rotate).LinkTooltip("Change Gizmo tool mode to Rotate", ref inputOptions.RotateMode);
            _toolStripScale = _toolstrip.AddButton(Editor.Icons.Scale32, () => _viewport.TransformGizmo.ActiveMode = TransformGizmoBase.Mode.Scale).LinkTooltip("Change Gizmo tool mode to Scale", ref inputOptions.ScaleMode);
            _toolstrip.AddSeparator();
            _toolStripLiveReload = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Refresh64, () => LiveReload = !LiveReload).SetChecked(true).SetAutoCheck(true).LinkTooltip("Live changes preview (applies prefab changes on modification by auto)");

            Editor.Prefabs.PrefabApplied += OnPrefabApplied;
            ScriptsBuilder.ScriptsReloadBegin += OnScriptsReloadBegin;

            // Setup input actions
            InputActions.Add(options => options.Undo, () =>
            {
                _undo.PerformUndo();
                Focus();
            });
            InputActions.Add(options => options.Redo, () =>
            {
                _undo.PerformRedo();
                Focus();
            });
            InputActions.Add(options => options.Cut, Cut);
            InputActions.Add(options => options.Copy, Copy);
            InputActions.Add(options => options.Paste, Paste);
            InputActions.Add(options => options.Duplicate, Duplicate);
            InputActions.Add(options => options.Delete, Delete);
            InputActions.Add(options => options.Rename, RenameSelection);
            InputActions.Add(options => options.FocusSelection, FocusSelection);
        }

        /// <summary>
        /// Enables or disables vertical and horizontal scrolling on the tree panel.
        /// </summary>
        /// <param name="enabled">The state to set scrolling to</param>
        public void ScrollingOnTreeView(bool enabled)
        {
            if (_treePanel.VScrollBar != null)
                _treePanel.VScrollBar.ThumbEnabled = enabled;
            if (_treePanel.HScrollBar != null)
                _treePanel.HScrollBar.ThumbEnabled = enabled;
        }

        /// <summary>
        /// Scrolls to the selected node in the tree.
        /// </summary>
        public void ScrollToSelectedNode()
        {
            // Scroll to node
            var nodeSelection = _tree.Selection;
            if (nodeSelection.Count != 0)
            {
                var scrollControl = nodeSelection[nodeSelection.Count - 1];
                _treePanel.ScrollViewTo(scrollControl);
            }
        }

        private void OnSearchBoxTextChanged()
        {
            // Skip events during setup or init stuff
            if (IsLayoutLocked)
                return;

            _tree.LockChildrenRecursive();

            // Update tree
            var query = _searchBox.Text;
            var root = Graph.Root;
            root.TreeNode.UpdateFilter(query);

            _tree.UnlockChildrenRecursive();
            PerformLayout();
            PerformLayout();
        }

        /// <inheritdoc />
        public override bool OnMouseUp(Float2 location, MouseButton button)
        {
            if (base.OnMouseUp(location, button))
                return true;

            if (button == MouseButton.Right && _treePanel.ContainsPoint(ref location))
            {
                _tree.Deselect();
                var locationCM = location + _searchBox.BottomLeft;
                ShowContextMenu(Parent, ref locationCM);
                return true;
            }

            if (button == MouseButton.Left && _treePanel.ContainsPoint(ref location) && !_isDropping)
            {
                _tree.Deselect();
                return true;
            }
            if (_isDropping)
            {
                _isDropping = false;
                return true;
            }
            return false;
        }

        /// <inheritdoc />
        protected override void OnScriptsReloadBegin()
        {
            base.OnScriptsReloadBegin();
            _isScriptsReloading = true;

            if (_asset == null || !_asset.IsLoaded)
                return;

            Editor.Log("Reloading prefab editor data on scripts reload. Prefab: " + _asset.Path);

            // Check if asset has been edited and not saved (and still has linked item)
            if (IsEdited)
            {
                // Ask user for further action
                var result = MessageBox.Show(
                                             string.Format("Asset \'{0}\' has been edited. Save before scripts reload?", _item.Path),
                                             "Save before reloading?",
                                             MessageBoxButtons.YesNo
                                            );
                if (result == DialogResult.OK || result == DialogResult.Yes)
                {
                    Save();
                }
            }

            // Cleanup
            Deselect();
            Graph.MainActor = null;
            _viewport.Prefab = null;
            _undo?.Clear(); // TODO: maybe don't clear undo?

            Close();
        }

        private void OnUndoEvent(IUndoAction action)
        {
            // All undo actions modify the asset except selection change
            if (!(action is SelectionChangeAction))
            {
                MarkAsEdited();
            }

            UpdateToolstrip();
        }

        private void OnPrefabApplied(Prefab prefab, Actor instance)
        {
            if (prefab == Asset)
            {
                ClearEditedFlag();

                _item.RefreshThumbnail();
            }
        }

        private void OnPrefabOpened()
        {
            _viewport.Prefab = _asset;
            if (Editor.ProjectCache.TryGetCustomData($"UIMode:{_asset.ID}", out bool value))
                _viewport.SetInitialUIMode(value);
            else
                _viewport.SetInitialUIMode(_viewport._hasUILinked);
            _viewport.UIModeToggled += OnUIModeToggled;
            _viewport.CreateViewScalingOptions();
            Graph.MainActor = _viewport.Instance;
            Selection.Clear();
            Select(Graph.Main);
            Graph.Root.TreeNode.Expand(true);
        }

        private void OnUIModeToggled(bool value)
        {
            Editor.ProjectCache.SetCustomData($"UIMode:{_asset.ID}", value);
        }

        /// <inheritdoc />
        public override void Save()
        {
            // Check if don't need to push any new changes to the original asset
            if (!IsEdited)
                return;

            try
            {
                Editor.Scene.OnSaveStart(_viewport._uiParentLink);

                // Simply update changes
                Editor.Prefabs.ApplyAll(_viewport.Instance);

                // Refresh properties panel to sync new prefab default values
                _propertiesEditor.BuildLayout();
            }
            catch (Exception)
            {
                // Disable live reload on error
                if (LiveReload)
                {
                    LiveReload = false;
                }

                throw;
            }
            finally
            {
                Editor.Scene.OnSaveEnd(_viewport._uiParentLink);
            }
        }

        /// <inheritdoc />
        protected override void UpdateToolstrip()
        {
            var undoRedo = _undo;
            var gizmo = _viewport.TransformGizmo;

            _saveButton.Enabled = IsEdited;
            _toolStripUndo.Enabled = undoRedo.CanUndo;
            _toolStripRedo.Enabled = undoRedo.CanRedo;
            //
            var gizmoMode = gizmo.ActiveMode;
            _toolStripTranslate.Checked = gizmoMode == TransformGizmoBase.Mode.Translate;
            _toolStripRotate.Checked = gizmoMode == TransformGizmoBase.Mode.Rotate;
            _toolStripScale.Checked = gizmoMode == TransformGizmoBase.Mode.Scale;
            //
            _toolStripLiveReload.Checked = _liveReload;

            base.UpdateToolstrip();
        }

        /// <inheritdoc />
        protected override void OnEditedState()
        {
            base.OnEditedState();

            _modifiedTime = DateTime.Now;
        }

        /// <inheritdoc />
        protected override void OnAssetLoaded()
        {
            // Skip during scripts reload to prevent issues
            if (_isScriptsReloading)
            {
                base.OnAssetLoaded();
                return;
            }

            OnPrefabOpened();
            _focusCamera = true;
            _undo.Clear();
            ClearEditedFlag();

            base.OnAssetLoaded();
        }

        /// <inheritdoc />
        protected override void UnlinkItem()
        {
            Deselect();
            Graph.MainActor = null;
            _viewport.Prefab = null;
            _undo?.Clear();

            base.UnlinkItem();
        }

        /// <inheritdoc />
        public override void Update(float deltaTime)
        {
            try
            {
                FlaxEngine.Profiler.BeginEvent("PrefabWindow.Update");
                if (Graph.Main != null)
                {
                    // Due to fact that actors in prefab editor are only created but not added to gameplay 
                    // we have to manually update some data (Level events work only for actors in a gameplay)
                    Update(Graph.Main);
                }

                base.Update(deltaTime);
            }
            catch (Exception ex)
            {
                // Info
                Editor.LogWarning("Error when updating prefab window for " + _asset);
                Editor.LogWarning(ex);

                // Refresh
                Deselect();
                Graph.MainActor = null;
                _viewport.Prefab = null;
                if (_asset.IsLoaded)
                {
                    OnPrefabOpened();
                }
            }
            finally
            {
                FlaxEngine.Profiler.EndEvent();
            }

            // Auto fit
            if (_focusCamera && _viewport.Task.FrameCount > 1)
            {
                _focusCamera = false;
                Editor.GetActorEditorSphere(_viewport.Instance, out BoundingSphere bounds);
                _viewport.ViewPosition = bounds.Center - _viewport.ViewDirection * (bounds.Radius * 1.2f);
            }

            // Auto save
            if (IsEdited && LiveReload && DateTime.Now - _modifiedTime > LiveReloadTimeout)
            {
                Save();
            }
        }

        /// <inheritdoc />
        public override bool UseLayoutData => true;

        /// <inheritdoc />
        public override void OnLayoutSerialize(XmlWriter writer)
        {
            LayoutSerializeSplitter(writer, "Split1", _split1);
            LayoutSerializeSplitter(writer, "Split2", _split2);
            writer.WriteAttributeString("LiveReload", LiveReload.ToString());
            writer.WriteAttributeString("GizmoMode", Viewport.TransformGizmo.ActiveMode.ToString());
        }

        /// <inheritdoc />
        public override void OnLayoutDeserialize(XmlElement node)
        {
            LayoutDeserializeSplitter(node, "Split1", _split1);
            LayoutDeserializeSplitter(node, "Split2", _split2);
            if (bool.TryParse(node.GetAttribute("LiveReload"), out bool value2))
                LiveReload = value2;
            if (Enum.TryParse(node.GetAttribute("GizmoMode"), out TransformGizmoBase.Mode value3))
                Viewport.TransformGizmo.ActiveMode = value3;
        }

        /// <inheritdoc />
        public override void OnLayoutDeserialize()
        {
            _split1.SplitterValue = 0.2f;
            _split2.SplitterValue = 0.6f;
            LiveReload = false;
        }

        /// <inheritdoc />
        public override void OnDestroy()
        {
            if (IsDisposing)
                return;
            base.OnDestroy();

            Editor.Prefabs.PrefabApplied -= OnPrefabApplied;
            ScriptsBuilder.ScriptsReloadBegin -= OnScriptsReloadBegin;

            _undo.Dispose();
            Graph.Dispose();
        }

        /// <inheritdoc />
        protected override void OnClose()
        {
            // Save current UI view size state.
            _viewport.SaveActiveUIScalingOption();
   
            base.OnClose();
        }

        /// <inheritdoc />
        public EditorViewport PresenterViewport => _viewport;

        private void OnPresenterAfterLayout(LayoutElementsContainer layout)
        {
            ApplyPropertiesSearchFilter();
        }

        private void ApplyPropertiesSearchFilter()
        {
            _propertiesEditor.ApplySearchFilter(_propertiesSearchBox.Text);
        }

        /// <inheritdoc />
        EditorViewport ISceneEditingContext.Viewport => Viewport;
    }
}
