using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEditor.Experimental.GraphView;
using UnityEditor.ShaderGraph.Drawing.Views;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor.ShaderGraph;

namespace UnityEditor.ShaderGraph.Drawing
{
    class BlackboardGroupInfo
    {
        [SerializeField]
        SerializableGuid m_Guid = new SerializableGuid();

        internal Guid guid => m_Guid.guid;

        [SerializeField]
        string m_GroupName;

        internal string GroupName
        {
            get => m_GroupName;
            set => m_GroupName = value;
        }

        BlackboardGroupInfo()
        {
        }
    }

    class SGBlackboard : GraphSubWindow, ISGControlledElement<BlackboardController>
    {
        VisualElement m_ScrollBoundaryTop;
        VisualElement m_ScrollBoundaryBottom;
        VisualElement m_BottomResizer;
        TextField m_PathLabelTextField;
        public VisualElement m_VariantExceededHelpBox;

        // --- Begin ISGControlledElement implementation
        public void OnControllerChanged(ref SGControllerChangedEvent e)
        {
        }

        public void OnControllerEvent(SGControllerEvent e)
        {
        }

        public void SetCurrentVariantUsage(int currentVariantCount, int maxVariantCount)
        {
            if (currentVariantCount < maxVariantCount && m_VariantExceededHelpBox != null)
            {
                RemoveAt(0);
                m_VariantExceededHelpBox = null;
            }
            else if (maxVariantCount <= currentVariantCount && m_VariantExceededHelpBox == null)
            {
                var helpBox = HelpBoxRow.CreateVariantLimitHelpBox(currentVariantCount, maxVariantCount);
                m_VariantExceededHelpBox = helpBox;
                Insert(0, helpBox);
            }
        }

        public BlackboardController controller
        {
            get => m_Controller;
            set
            {
                if (m_Controller != value)
                {
                    if (m_Controller != null)
                    {
                        m_Controller.UnregisterHandler(this);
                    }
                    m_Controller = value;

                    if (m_Controller != null)
                    {
                        m_Controller.RegisterHandler(this);
                    }
                }
            }
        }

        SGController ISGControlledElement.controller => m_Controller;

        // --- ISGControlledElement implementation

        BlackboardController m_Controller;

        BlackboardViewModel m_ViewModel;

        BlackboardViewModel ViewModel
        {
            get => m_ViewModel;
            set => m_ViewModel = value;
        }

        // List of user-made blackboard category views
        IList<SGBlackboardCategory> m_BlackboardCategories = new List<SGBlackboardCategory>();

        bool m_ScrollToTop = false;
        bool m_ScrollToBottom = false;
        bool m_EditPathCancelled = false;
        bool m_IsUserDraggingItems = false;
        int m_InsertIndex = -1;

        const int k_DraggedPropertyScrollSpeed = 6;

        public override string windowTitle => "Blackboard";
        public override string elementName => "SGBlackboard";
        public override string styleName => "SGBlackboard";
        public override string UxmlName => "Blackboard/SGBlackboard";
        public override string layoutKey => "UnityEditor.ShaderGraph.Blackboard";

        Action addItemRequested { get; set; }

        internal Action hideDragIndicatorAction { get; set; }

        GenericMenu m_AddBlackboardItemMenu;
        internal GenericMenu addBlackboardItemMenu => m_AddBlackboardItemMenu;

        VisualElement m_DragIndicator;

        public SGBlackboard(BlackboardViewModel viewModel, BlackboardController controller) : base(viewModel)
        {
            ViewModel = viewModel;
            this.controller = controller;

            InitializeAddBlackboardItemMenu();

            // By default dock blackboard to left of graph window
            windowDockingLayout.dockingLeft = true;

            if (m_MainContainer.Q(name: "addButton") is Button addButton)
                addButton.clickable.clicked += () =>
                {
                    InitializeAddBlackboardItemMenu();
                    addItemRequested?.Invoke();
                    ShowAddPropertyMenu();
                };

            ParentView.RegisterCallback<FocusOutEvent>(evt => OnDragExitedEvent(new DragExitedEvent()));

            m_TitleLabel.text = ViewModel.title;

            m_SubTitleLabel.RegisterCallback<MouseDownEvent>(OnMouseDownEvent);
            m_SubTitleLabel.text = ViewModel.subtitle;

            m_PathLabelTextField = this.Q<TextField>("subTitleTextField");
            m_PathLabelTextField.value = ViewModel.subtitle;
            m_PathLabelTextField.visible = false;
            m_PathLabelTextField.Q("unity-text-input").RegisterCallback<FocusOutEvent>(e => { OnEditPathTextFinished(); }, TrickleDown.TrickleDown);
            m_PathLabelTextField.Q("unity-text-input").RegisterCallback<KeyDownEvent>(OnPathTextFieldKeyPressed, TrickleDown.TrickleDown);

            // These callbacks make sure the scroll boundary regions and drag indicator don't show up user is not dragging/dropping properties/categories
            RegisterCallback<MouseUpEvent>(OnMouseUpEvent);
            RegisterCallback<DragExitedEvent>(OnDragExitedEvent);

            // Register drag callbacks
            RegisterCallback<DragUpdatedEvent>(OnDragUpdatedEvent);
            RegisterCallback<DragPerformEvent>(OnDragPerformEvent);
            RegisterCallback<DragLeaveEvent>(OnDragLeaveEvent);
            RegisterCallback<DragExitedEvent>(OnDragExitedEvent);

            // This callback makes sure the drag indicator is shown again if user exits and then re-enters blackboard while dragging
            RegisterCallback<MouseEnterEvent>(OnMouseEnterEvent);

            m_ScrollBoundaryTop = m_MainContainer.Q(name: "scrollBoundaryTop");
            m_ScrollBoundaryTop.RegisterCallback<MouseEnterEvent>(ScrollRegionTopEnter);
            m_ScrollBoundaryTop.RegisterCallback<DragUpdatedEvent>(OnFieldDragUpdate);
            m_ScrollBoundaryTop.RegisterCallback<MouseLeaveEvent>(ScrollRegionTopLeave);

            m_ScrollBoundaryBottom = m_MainContainer.Q(name: "scrollBoundaryBottom");
            m_ScrollBoundaryBottom.RegisterCallback<MouseEnterEvent>(ScrollRegionBottomEnter);
            m_ScrollBoundaryBottom.RegisterCallback<DragUpdatedEvent>(OnFieldDragUpdate);
            m_ScrollBoundaryBottom.RegisterCallback<MouseLeaveEvent>(ScrollRegionBottomLeave);

            m_BottomResizer = m_MainContainer.Q("bottom-resize");

            HideScrollBoundaryRegions();

            // Sets delegate association so scroll boundary regions are hidden when a blackboard property is dropped into graph
            if (ParentView is MaterialGraphView materialGraphView)
                materialGraphView.blackboardFieldDropDelegate = HideScrollBoundaryRegions;

            isWindowScrollable = true;
            isWindowResizable = true;
            focusable = true;

            m_DragIndicator = new VisualElement();
            m_DragIndicator.name = "categoryDragIndicator";
            m_DragIndicator.style.position = Position.Absolute;
            hierarchy.Add(m_DragIndicator);
            SetCategoryDragIndicatorVisible(false);
        }

        void SetCategoryDragIndicatorVisible(bool visible)
        {
            if (visible && (m_DragIndicator.parent == null))
            {
                hierarchy.Add(m_DragIndicator);
                m_DragIndicator.visible = true;
            }
            else if ((visible == false) && (m_DragIndicator.parent != null))
            {
                hierarchy.Remove(m_DragIndicator);
            }
        }

        public void OnDragEnterEvent(DragEnterEvent evt)
        {
            if (!m_IsUserDraggingItems)
            {
                m_IsUserDraggingItems = true;

                if (scrollableHeight > 0)
                {
                    // Interferes with scrolling functionality of properties with the bottom scroll boundary
                    m_BottomResizer.style.visibility = Visibility.Hidden;

                    var contentElement = m_MainContainer.Q(name: "content");
                    scrollViewIndex = contentElement.IndexOf(m_ScrollView);
                    contentElement.Insert(scrollViewIndex, m_ScrollBoundaryTop);
                    scrollViewIndex = contentElement.IndexOf(m_ScrollView);
                    contentElement.Insert(scrollViewIndex + 1, m_ScrollBoundaryBottom);
                }

                // If there are any categories in the selection, show drag indicator, otherwise hide
                SetCategoryDragIndicatorVisible(selection.OfType<SGBlackboardCategory>().Any());
            }
        }

        public void OnDragExitedEvent(DragExitedEvent evt)
        {
            SetCategoryDragIndicatorVisible(false);
            HideScrollBoundaryRegions();
        }

        void OnMouseEnterEvent(MouseEnterEvent evt)
        {
            if (m_IsUserDraggingItems && selection.OfType<SGBlackboardCategory>().Any())
                SetCategoryDragIndicatorVisible(true);
        }

        void HideScrollBoundaryRegions()
        {
            m_BottomResizer.style.visibility = Visibility.Visible;
            m_IsUserDraggingItems = false;
            m_ScrollBoundaryTop.RemoveFromHierarchy();
            m_ScrollBoundaryBottom.RemoveFromHierarchy();
        }

        int InsertionIndex(Vector2 pos)
        {
            VisualElement owner = contentContainer != null ? contentContainer : this;
            Vector2 localPos = this.ChangeCoordinatesTo(owner, pos);

            int index = BlackboardUtils.GetInsertionIndex(owner, localPos, Children());

            // Clamps the index between the min and max of the child indices based on the mouse position relative to the categories on the y-axis (up/down)
            // Checking for at least 2 children to make sure Children.First() and Children.Last() don't throw an exception
            if (index == -1 && childCount >= 2)
            {
                index = localPos.y < Children().First().layout.yMin ? 0 :
                                   localPos.y > Children().Last().layout.yMax ? childCount : -1;
            }

            // Don't allow the default category to be displaced
            return Mathf.Clamp(index, 1, index);
        }

        void OnDragUpdatedEvent(DragUpdatedEvent evt)
        {
            var selection = DragAndDrop.GetGenericData("DragSelection") as List<ISelectable>;
            if (selection == null)
            {
                SetCategoryDragIndicatorVisible(false);
                return;
            }

            foreach (ISelectable selectedElement in selection)
            {
                var sourceItem = selectedElement as VisualElement;
                // Don't allow user to move the default category
                if (sourceItem is SGBlackboardCategory blackboardCategory && blackboardCategory.controller.Model.IsNamedCategory() == false)
                {
                    DragAndDrop.visualMode = DragAndDropVisualMode.Rejected;
                    return;
                }
            }

            Vector2 localPosition = evt.localMousePosition;
            m_InsertIndex = InsertionIndex(localPosition);

            if (m_InsertIndex != -1)
            {
                float indicatorY = 0;
                if (m_InsertIndex == childCount)
                {
                    if (childCount > 0)
                    {
                        VisualElement lastChild = this[childCount - 1];
                        indicatorY = lastChild.ChangeCoordinatesTo(this, new Vector2(0, lastChild.layout.height + lastChild.resolvedStyle.marginBottom)).y;
                    }
                    else
                    {
                        indicatorY = this.contentRect.height;
                    }
                }
                else
                {
                    VisualElement childAtInsertIndex = this[m_InsertIndex];
                    indicatorY = childAtInsertIndex.ChangeCoordinatesTo(this, new Vector2(0, -childAtInsertIndex.resolvedStyle.marginTop)).y;
                }

                m_DragIndicator.style.top = indicatorY - m_DragIndicator.resolvedStyle.height * 0.5f;
                DragAndDrop.visualMode = DragAndDropVisualMode.Move;
            }
            else
            {
                SetCategoryDragIndicatorVisible(false);
            }

            evt.StopPropagation();
        }

        void OnDragPerformEvent(DragPerformEvent evt)
        {
            // Don't bubble up drop operations onto blackboard upto the graph view, as it leads to nodes being created without users knowledge behind the blackboard
            evt.StopPropagation();

            var selection = DragAndDrop.GetGenericData("DragSelection") as List<ISelectable>;
            if (selection == null || selection.Count == 0)
            {
                SetCategoryDragIndicatorVisible(false);
                return;
            }

            // Hide the category drag indicator if no categories in selection
            if (!selection.OfType<SGBlackboardCategory>().Any())
            {
                SetCategoryDragIndicatorVisible(false);
            }

            Vector2 localPosition = evt.localMousePosition;
            m_InsertIndex = InsertionIndex(localPosition);

            // Any categories in the selection that are from other graphs, would have to be copied as opposed to moving the categories within the same graph
            foreach (var item in selection.ToList())
            {
                if (item is SGBlackboardCategory category)
                {
                    var selectedCategoryData = category.controller.Model;
                    bool doesCategoryExistInGraph = controller.Model.ContainsCategory(selectedCategoryData);
                    if (doesCategoryExistInGraph == false)
                    {
                        var copyCategoryAction = new CopyCategoryAction();
                        copyCategoryAction.categoryToCopyReference = selectedCategoryData;
                        ViewModel.requestModelChangeAction(copyCategoryAction);
                        selection.Remove(item);

                        // Remove any child inputs that belong to this category from the selection, to prevent duplicates from being copied onto the graph
                        foreach (var otherItem in selection.ToList())
                        {
                            if (otherItem is SGBlackboardField blackboardField && category.Contains(blackboardField))
                                selection.Remove(otherItem);
                        }
                    }
                }
            }

            // Same as above, but for blackboard items (properties, keywords, dropdowns)
            foreach (var item in selection.ToList())
            {
                if (item is SGBlackboardField blackboardField)
                {
                    var selectedBlackboardItem = blackboardField.controller.Model;
                    bool doesInputExistInGraph = controller.Model.ContainsInput(selectedBlackboardItem);
                    if (doesInputExistInGraph == false)
                    {
                        var copyShaderInputAction = new CopyShaderInputAction();
                        copyShaderInputAction.shaderInputToCopy = selectedBlackboardItem;
                        ViewModel.requestModelChangeAction(copyShaderInputAction);
                        selection.Remove(item);
                    }
                }
            }

            var moveCategoryAction = new MoveCategoryAction();
            moveCategoryAction.newIndexValue = m_InsertIndex;
            moveCategoryAction.categoryGuids = selection.OfType<SGBlackboardCategory>().OrderBy(sgcat => sgcat.GetPosition().y).Select(cat => cat.viewModel.associatedCategoryGuid).ToList();
            ViewModel.requestModelChangeAction(moveCategoryAction);

            SetCategoryDragIndicatorVisible(false);
        }

        void OnDragLeaveEvent(DragLeaveEvent evt)
        {
            DragAndDrop.visualMode = DragAndDropVisualMode.Rejected;
            SetCategoryDragIndicatorVisible(false);
            m_InsertIndex = -1;
        }

        int scrollViewIndex { get; set; }

        void ScrollRegionTopEnter(MouseEnterEvent mouseEnterEvent)
        {
            if (m_IsUserDraggingItems)
            {
                SetCategoryDragIndicatorVisible(false);
                m_ScrollToTop = true;
                m_ScrollToBottom = false;
            }
        }

        void ScrollRegionTopLeave(MouseLeaveEvent mouseLeaveEvent)
        {
            if (m_IsUserDraggingItems)
            {
                m_ScrollToTop = false;
                // If there are any categories in the selection, show drag indicator, otherwise hide
                SetCategoryDragIndicatorVisible(selection.OfType<SGBlackboardCategory>().Any());
            }
        }

        void ScrollRegionBottomEnter(MouseEnterEvent mouseEnterEvent)
        {
            if (m_IsUserDraggingItems)
            {
                SetCategoryDragIndicatorVisible(false);
                m_ScrollToBottom = true;
                m_ScrollToTop = false;
            }
        }

        void ScrollRegionBottomLeave(MouseLeaveEvent mouseLeaveEvent)
        {
            if (m_IsUserDraggingItems)
            {
                m_ScrollToBottom = false;
                // If there are any categories in the selection, show drag indicator, otherwise hide
                SetCategoryDragIndicatorVisible(selection.OfType<SGBlackboardCategory>().Any());
            }
        }

        void OnFieldDragUpdate(DragUpdatedEvent dragUpdatedEvent)
        {
            // how far is the mouse into the drag boundary.
            float dragCoeff
                = m_ScrollToTop ? 1 - dragUpdatedEvent.localMousePosition.y / m_ScrollBoundaryBottom.contentRect.height
                : m_ScrollToBottom ? dragUpdatedEvent.localMousePosition.y / m_ScrollBoundaryBottom.contentRect.height
                : 0;

            dragCoeff = Mathf.Clamp(dragCoeff, .15f, .85f);

            // factor in fixed base speed and relative to % of total scrollable height.
            float dragSpeed = dragCoeff * k_DraggedPropertyScrollSpeed * (scrollableHeight / 100f);

            // Lastly, make sure the drag speed can't ever get too slow.
            dragSpeed = Mathf.Max(dragSpeed, k_DraggedPropertyScrollSpeed);

            if (m_ScrollToTop)
                m_ScrollView.scrollOffset = new Vector2(m_ScrollView.scrollOffset.x, Mathf.Clamp(m_ScrollView.scrollOffset.y - dragSpeed, 0, scrollableHeight));
            else if (m_ScrollToBottom)
                m_ScrollView.scrollOffset = new Vector2(m_ScrollView.scrollOffset.x, Mathf.Clamp(m_ScrollView.scrollOffset.y + dragSpeed, 0, scrollableHeight));
        }

        void InitializeAddBlackboardItemMenu()
        {
            m_AddBlackboardItemMenu = new GenericMenu();

            if (ViewModel == null)
            {
                AssertHelpers.Fail("SGBlackboard: View Model is null.");
                return;
            }

            // Add category at top, followed by separator
            m_AddBlackboardItemMenu.AddItem(new GUIContent("Category"), false, () => ViewModel.requestModelChangeAction(ViewModel.addCategoryAction));
            m_AddBlackboardItemMenu.AddSeparator($"/");

            var selectedCategoryGuid = controller.GetFirstSelectedCategoryGuid();
            foreach (var nameToAddActionTuple in ViewModel.propertyNameToAddActionMap)
            {
                string propertyName = nameToAddActionTuple.Key;
                AddShaderInputAction addAction = nameToAddActionTuple.Value as AddShaderInputAction;
                addAction.categoryToAddItemToGuid = selectedCategoryGuid;
                m_AddBlackboardItemMenu.AddItem(new GUIContent(propertyName), false, () => ViewModel.requestModelChangeAction(addAction));
            }
            m_AddBlackboardItemMenu.AddSeparator($"/");

            foreach (var nameToAddActionTuple in ViewModel.defaultKeywordNameToAddActionMap)
            {
                string defaultKeywordName = nameToAddActionTuple.Key;
                AddShaderInputAction addAction = nameToAddActionTuple.Value as AddShaderInputAction;
                addAction.categoryToAddItemToGuid = selectedCategoryGuid;
                m_AddBlackboardItemMenu.AddItem(new GUIContent($"Keyword/{defaultKeywordName}"), false, () => ViewModel.requestModelChangeAction(addAction));
            }
            m_AddBlackboardItemMenu.AddSeparator($"Keyword/");

            foreach (var nameToAddActionTuple in ViewModel.builtInKeywordNameToAddActionMap)
            {
                string builtInKeywordName = nameToAddActionTuple.Key;
                AddShaderInputAction addAction = nameToAddActionTuple.Value as AddShaderInputAction;
                addAction.categoryToAddItemToGuid = selectedCategoryGuid;
                m_AddBlackboardItemMenu.AddItem(new GUIContent($"Keyword/{builtInKeywordName}"), false, () => ViewModel.requestModelChangeAction(addAction));
            }

            foreach (string disabledKeywordName in ViewModel.disabledKeywordNameList)
            {
                m_AddBlackboardItemMenu.AddDisabledItem(new GUIContent($"Keyword/{disabledKeywordName}"));
            }

            if (ViewModel.defaultDropdownNameToAdd != null)
            {
                string defaultDropdownName = ViewModel.defaultDropdownNameToAdd.Item1;
                AddShaderInputAction addAction = ViewModel.defaultDropdownNameToAdd.Item2 as AddShaderInputAction;
                addAction.categoryToAddItemToGuid = selectedCategoryGuid;
                m_AddBlackboardItemMenu.AddItem(new GUIContent($"{defaultDropdownName}"), false, () => ViewModel.requestModelChangeAction(addAction));
            }

            foreach (string disabledDropdownName in ViewModel.disabledDropdownNameList)
            {
                m_AddBlackboardItemMenu.AddDisabledItem(new GUIContent(disabledDropdownName));
            }
        }

        void ShowAddPropertyMenu()
        {
            m_AddBlackboardItemMenu.ShowAsContext();
        }

        void OnMouseUpEvent(MouseUpEvent evt)
        {
            this.HideScrollBoundaryRegions();
        }

        void OnMouseDownEvent(MouseDownEvent evt)
        {
            if (evt.clickCount == 2 && evt.button == (int)MouseButton.LeftMouse)
            {
                StartEditingPath();
                evt.PreventDefault();
            }
        }

        void StartEditingPath()
        {
            m_SubTitleLabel.visible = false;
            m_PathLabelTextField.visible = true;
            m_PathLabelTextField.value = m_SubTitleLabel.text;
            m_PathLabelTextField.Q("unity-text-input").Focus();
            m_PathLabelTextField.SelectAll();
        }

        void OnPathTextFieldKeyPressed(KeyDownEvent evt)
        {
            switch (evt.keyCode)
            {
                case KeyCode.Escape:
                    m_EditPathCancelled = true;
                    m_PathLabelTextField.Q("unity-text-input").Blur();
                    break;
                case KeyCode.Return:
                case KeyCode.KeypadEnter:
                    m_PathLabelTextField.Q("unity-text-input").Blur();
                    break;
                default:
                    break;
            }
        }

        void OnEditPathTextFinished()
        {
            m_SubTitleLabel.visible = true;
            m_PathLabelTextField.visible = false;

            var newPath = m_PathLabelTextField.text;
            if (!m_EditPathCancelled && (newPath != m_SubTitleLabel.text))
            {
                newPath = BlackboardUtils.SanitizePath(newPath);
            }

            // Request graph path change action
            var pathChangeAction = new ChangeGraphPathAction();
            pathChangeAction.NewGraphPath = newPath;
            ViewModel.requestModelChangeAction(pathChangeAction);

            m_SubTitleLabel.text = BlackboardUtils.FormatPath(newPath);
            m_EditPathCancelled = false;
        }

        public override void Dispose()
        {
            m_PathLabelTextField.Q("unity-text-input").UnregisterCallback<FocusOutEvent>(e => { OnEditPathTextFinished(); }, TrickleDown.TrickleDown);
            m_PathLabelTextField.Q("unity-text-input").UnregisterCallback<KeyDownEvent>(OnPathTextFieldKeyPressed, TrickleDown.TrickleDown);
            UnregisterCallback<MouseUpEvent>(OnMouseUpEvent);
            UnregisterCallback<DragExitedEvent>(OnDragExitedEvent);
            UnregisterCallback<DragUpdatedEvent>(OnDragUpdatedEvent);
            UnregisterCallback<DragPerformEvent>(OnDragPerformEvent);
            UnregisterCallback<DragLeaveEvent>(OnDragLeaveEvent);
            UnregisterCallback<DragExitedEvent>(OnDragExitedEvent);
            UnregisterCallback<MouseEnterEvent>(OnMouseEnterEvent);
            m_ScrollBoundaryTop.UnregisterCallback<MouseEnterEvent>(ScrollRegionTopEnter);
            m_ScrollBoundaryTop.UnregisterCallback<DragUpdatedEvent>(OnFieldDragUpdate);
            m_ScrollBoundaryTop.UnregisterCallback<MouseLeaveEvent>(ScrollRegionTopLeave);
            m_ScrollBoundaryBottom.UnregisterCallback<MouseEnterEvent>(ScrollRegionBottomEnter);
            m_ScrollBoundaryBottom.UnregisterCallback<DragUpdatedEvent>(OnFieldDragUpdate);
            m_ScrollBoundaryBottom.UnregisterCallback<MouseLeaveEvent>(ScrollRegionBottomLeave);

            m_BlackboardCategories.Clear();
            m_ViewModel = null;
            m_DragIndicator = null;
            m_Controller = null;
            m_AddBlackboardItemMenu = null;
            addItemRequested = null;
            m_BottomResizer = null;
            m_ScrollBoundaryBottom = null;
            m_ScrollBoundaryTop = null;
            m_PathLabelTextField = null;
        }
    }
}