using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor.Graphing;
using UnityEditor.Experimental.GraphView;
using UnityEditor.ShaderGraph.Drawing.Controls;
using UnityEditor.ShaderGraph.Drawing.Inspector.PropertyDrawers;
using UnityEditor.ShaderGraph.Internal;
using UnityEngine.Assertions;

using ContextualMenuManipulator = UnityEngine.UIElements.ContextualMenuManipulator;
using GraphDataStore = UnityEditor.ShaderGraph.DataStore<UnityEditor.ShaderGraph.GraphData>;

namespace UnityEditor.ShaderGraph.Drawing
{
    class SGBlackboardField : GraphElement, IInspectable, ISGControlledElement<ShaderInputViewController>
    {
        static readonly Texture2D k_ExposedIcon = Resources.Load<Texture2D>("GraphView/Nodes/BlackboardFieldExposed");
        static readonly string k_UxmlTemplatePath = "UXML/Blackboard/SGBlackboardField";
        static readonly string k_StyleSheetPath = "Styles/SGBlackboard";

        ShaderInputViewModel m_ViewModel;

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

        VisualElement m_ContentItem;
        Pill m_Pill;
        Label m_TypeLabel;
        TextField m_TextField;
        internal TextField textField => m_TextField;

        Action m_ResetReferenceNameTrigger;
        List<Node> m_SelectedNodes = new List<Node>();

        public string text
        {
            get { return m_Pill.text; }
            set { m_Pill.text = value; }
        }

        public string typeText
        {
            get { return m_TypeLabel.text; }
            set { m_TypeLabel.text = value; }
        }

        public Texture icon
        {
            get { return m_Pill.icon; }
            set { m_Pill.icon = value; }
        }

        public bool highlighted
        {
            get { return m_Pill.highlighted; }
            set { m_Pill.highlighted = value; }
        }

        internal SGBlackboardField(ShaderInputViewModel viewModel)
        {
            ViewModel = viewModel;
            // Store ShaderInput in userData object
            userData = ViewModel.model;
            if (userData == null)
            {
                AssertHelpers.Fail("Could not initialize blackboard field as shader input was null.");
                return;
            }
            // Store the Model guid as viewDataKey as that is persistent
            viewDataKey = ViewModel.model.guid.ToString();

            var visualTreeAsset = Resources.Load<VisualTreeAsset>(k_UxmlTemplatePath);
            Assert.IsNotNull(visualTreeAsset);

            VisualElement mainContainer = visualTreeAsset.Instantiate();
            var styleSheet = Resources.Load<StyleSheet>(k_StyleSheetPath);
            Assert.IsNotNull(styleSheet);
            styleSheets.Add(styleSheet);

            mainContainer.AddToClassList("mainContainer");
            mainContainer.pickingMode = PickingMode.Ignore;

            m_ContentItem = mainContainer.Q("contentItem");
            m_Pill = mainContainer.Q<Pill>("pill");
            m_TypeLabel = mainContainer.Q<Label>("typeLabel");
            m_TextField = mainContainer.Q<TextField>("textField");
            m_TextField.style.display = DisplayStyle.None;

            // Update the Pill text if shader input name is changed
            // we handle this in controller if we change it through SGBlackboardField, but its possible to change through PropertyNodeView as well
            shaderInput.displayNameUpdateTrigger += newDisplayName => text = newDisplayName;

            // Handles the upgrade fix for the old color property deprecation
            if (shaderInput is AbstractShaderProperty property)
            {
                property.onAfterVersionChange += () =>
                {
                    this.typeText = property.GetPropertyTypeString();
                    this.m_InspectorUpdateDelegate();
                };
            }

            Add(mainContainer);

            RegisterCallback<MouseDownEvent>(OnMouseDownEvent);

            capabilities |= Capabilities.Selectable | Capabilities.Droppable | Capabilities.Deletable | Capabilities.Renamable;

            ClearClassList();
            AddToClassList("blackboardField");

            this.name = "SGBlackboardField";
            UpdateFromViewModel();

            // add the right click context menu
            IManipulator contextMenuManipulator = new ContextualMenuManipulator(AddContextMenuOptions);
            this.AddManipulator(contextMenuManipulator);
            this.AddManipulator(new SelectionDropper());
            this.AddManipulator(new ContextualMenuManipulator(BuildFieldContextualMenu));

            // When a display name is changed through the BlackboardPill, bind this callback to handle it with appropriate change action
            var textInputElement = m_TextField.Q(TextField.textInputUssName);
            textInputElement.RegisterCallback<FocusOutEvent>(e => { OnEditTextFinished(); });

            ShaderGraphPreferences.onAllowDeprecatedChanged += UpdateTypeText;

            RegisterCallback<MouseEnterEvent>(evt => OnMouseHover(evt, ViewModel.model));
            RegisterCallback<MouseLeaveEvent>(evt => OnMouseHover(evt, ViewModel.model));
            RegisterCallback<DragUpdatedEvent>(OnDragUpdatedEvent);

            var blackboard = ViewModel.parentView.GetFirstAncestorOfType<SGBlackboard>();
            if (blackboard != null)
            {
                // These callbacks are used for the property dragging scroll behavior
                RegisterCallback<DragEnterEvent>(blackboard.OnDragEnterEvent);
                RegisterCallback<DragExitedEvent>(blackboard.OnDragExitedEvent);

                // These callbacks are used for the property dragging scroll behavior
                RegisterCallback<DragEnterEvent>(blackboard.OnDragEnterEvent);
                RegisterCallback<DragExitedEvent>(blackboard.OnDragExitedEvent);
            }
        }

        ~SGBlackboardField()
        {
            ShaderGraphPreferences.onAllowDeprecatedChanged -= UpdateTypeText;
        }

        void AddContextMenuOptions(ContextualMenuPopulateEvent evt)
        {
            // Checks if the reference name has been overridden and appends menu action to reset it, if so
            if (shaderInput.isRenamable &&
                !string.IsNullOrEmpty(shaderInput.overrideReferenceName))
            {
                evt.menu.AppendAction(
                    "Reset Reference",
                    e =>
                    {
                        var resetReferenceNameAction = new ResetReferenceNameAction();
                        resetReferenceNameAction.shaderInputReference = shaderInput;
                        ViewModel.requestModelChangeAction(resetReferenceNameAction);
                        m_ResetReferenceNameTrigger();
                    },
                    DropdownMenuAction.AlwaysEnabled);
            }

            if (shaderInput is ColorShaderProperty colorProp)
            {
                PropertyNodeView.AddMainColorMenuOptions(evt, colorProp, controller.graphData, m_InspectorUpdateDelegate);
            }


            if (shaderInput is Texture2DShaderProperty texProp)
            {
                PropertyNodeView.AddMainTextureMenuOptions(evt, texProp, controller.graphData, m_InspectorUpdateDelegate);
            }
        }

        internal void UpdateFromViewModel()
        {
            this.text = ViewModel.inputName;
            this.icon = ViewModel.isInputExposed ? k_ExposedIcon : null;
            this.typeText = ViewModel.inputTypeName;
        }

        ShaderInputViewController m_Controller;

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

        public void OnControllerEvent(SGControllerEvent e)
        {
        }

        public ShaderInputViewController 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

        [Inspectable("Shader Input", null)]
        public ShaderInput shaderInput => ViewModel.model;

        public string inspectorTitle => ViewModel.inputName + " " + ViewModel.inputTypeName;

        public object GetObjectToInspect()
        {
            return shaderInput;
        }

        Action m_InspectorUpdateDelegate;

        public void SupplyDataToPropertyDrawer(IPropertyDrawer propertyDrawer, Action inspectorUpdateDelegate)
        {
            if (propertyDrawer is ShaderInputPropertyDrawer shaderInputPropertyDrawer)
            {
                // We currently need to do a halfway measure between the old way of handling stuff for property drawers (how FieldView and NodeView handle it)
                // and how we want to handle it with the new style of controllers and views. Ideally we'd just hand the property drawer a view model and thats it.
                // We've maintained all the old callbacks as they are in the PropertyDrawer to reduce possible halo changes and support PropertyNodeView functionality
                // Instead we supply different underlying methods for the callbacks in the new SGBlackboardField,
                // that way both code paths should work until we can refactor PropertyNodeView
                shaderInputPropertyDrawer.GetViewModel(
                    ViewModel,
                    controller.graphData,
                    ((triggerInspectorUpdate, modificationScope) =>
                    {
                        controller.DirtyNodes(modificationScope);
                        if (triggerInspectorUpdate)
                            inspectorUpdateDelegate();
                    }));

                m_ResetReferenceNameTrigger = shaderInputPropertyDrawer.ResetReferenceName;
                m_InspectorUpdateDelegate = inspectorUpdateDelegate;
            }
        }

        void OnMouseDownEvent(MouseDownEvent e)
        {
            if ((e.clickCount == 2) && e.button == (int)MouseButton.LeftMouse && IsRenamable())
            {
                OpenTextEditor();
                e.PreventDefault();
            }
            else
            {
                e.StopPropagation();
            }
        }

        void OnDragUpdatedEvent(DragUpdatedEvent evt)
        {
            if (m_SelectedNodes.Any())
            {
                foreach (var node in m_SelectedNodes)
                {
                    node.RemoveFromClassList("hovered");
                }
                m_SelectedNodes.Clear();
            }
        }

        // TODO: Move to controller? Feels weird for this to be directly communicating with PropertyNodes etc.
        // Better way would be to send event to controller that notified of hover enter/exit and have other controllers be sent those events in turn
        void OnMouseHover(EventBase evt, ShaderInput input)
        {
            var graphView = ViewModel.parentView.GetFirstAncestorOfType<MaterialGraphView>();
            if (evt.eventTypeId == MouseEnterEvent.TypeId())
            {
                foreach (var node in graphView.nodes.ToList())
                {
                    if (input is AbstractShaderProperty property)
                    {
                        if (node.userData is PropertyNode propertyNode)
                        {
                            if (propertyNode.property == input)
                            {
                                m_SelectedNodes.Add(node);
                                node.AddToClassList("hovered");
                            }
                        }
                    }
                    else if (input is ShaderKeyword keyword)
                    {
                        if (node.userData is KeywordNode keywordNode)
                        {
                            if (keywordNode.keyword == input)
                            {
                                m_SelectedNodes.Add(node);
                                node.AddToClassList("hovered");
                            }
                        }
                    }
                    else if (input is ShaderDropdown dropdown)
                    {
                        if (node.userData is DropdownNode dropdownNode)
                        {
                            if (dropdownNode.dropdown == input)
                            {
                                m_SelectedNodes.Add(node);
                                node.AddToClassList("hovered");
                            }
                        }
                    }
                }
            }
            else if (evt.eventTypeId == MouseLeaveEvent.TypeId() && m_SelectedNodes.Any())
            {
                foreach (var node in m_SelectedNodes)
                {
                    node.RemoveFromClassList("hovered");
                }
                m_SelectedNodes.Clear();
            }
        }

        void UpdateTypeText()
        {
            if (shaderInput is AbstractShaderProperty asp)
            {
                typeText = asp.GetPropertyTypeString();
            }
        }

        internal void OpenTextEditor()
        {
            m_TextField.SetValueWithoutNotify(text);
            m_TextField.style.display = DisplayStyle.Flex;
            m_ContentItem.visible = false;
            m_TextField.Q(TextField.textInputUssName).Focus();
            m_TextField.SelectAll();
        }

        void OnEditTextFinished()
        {
            m_ContentItem.visible = true;
            m_TextField.style.display = DisplayStyle.None;

            if (text != m_TextField.text && String.IsNullOrWhiteSpace(m_TextField.text) == false && String.IsNullOrEmpty(m_TextField.text) == false)
            {
                var changeDisplayNameAction = new ChangeDisplayNameAction();
                changeDisplayNameAction.shaderInputReference = shaderInput;
                changeDisplayNameAction.newDisplayNameValue = m_TextField.text;
                ViewModel.requestModelChangeAction(changeDisplayNameAction);
                m_InspectorUpdateDelegate?.Invoke();
            }
            else
            {
                // Reset text field to original name
                m_TextField.value = text;
            }
        }

        protected virtual void BuildFieldContextualMenu(ContextualMenuPopulateEvent evt)
        {
            evt.menu.AppendAction("Rename", (a) => OpenTextEditor(), DropdownMenuAction.AlwaysEnabled);
        }
    }
}