namespace TagFighter.UI.Editor
{
    using UnityEditor;
    using UnityEngine.UIElements;
    using UnityEditor.Experimental.GraphView;
    using System;
    using TagFighter.Effects;
    using TagFighter.Effects.Steps;
    using System.Collections.Generic;
    using System.Linq;
    using UnityEngine;
    using System.Reflection;

    [Serializable]
    public class EffectStepNodeGraph
    {
        public List<EffectStepNode> Nodes;
        public Vector2 Center;

        public EffectStepNodeGraph(EffectStepNodeGraphData nodesData) {
            Dictionary<string, EffectStepNode> guidToNode = new();
            Nodes = new();
            foreach (var nodeData in nodesData.Nodes) {
                var newNode = ScriptableObject.CreateInstance(nodeData.SerializedType.Type) as EffectStepNode;
                Nodes.Add(newNode);
                guidToNode.TryAdd(nodeData.Guid, newNode);
            }

            foreach (var (node, data) in Nodes.Zip(nodesData.Nodes, (node, data) => (node, data))) {
                node.FromData(data, guidToNode);
            }
            Center = nodesData.Center;

        }
    }

    [Serializable]
    public class EffectStepNodeGraphData
    {
        public List<EffectStepNodeData> Nodes;
        public Vector2 Center;

        public EffectStepNodeGraphData() {
            Nodes = new();
            Center = Vector2.zero;
        }

        public EffectStepNodeGraphData(IEnumerable<EffectStepNodeView> nodes) {
            Nodes = nodes.Select(nodeView => nodeView.Node.ToData()).ToList();
            Center = new Vector2(nodes.Select(node => node.layout.center.x).Average(), nodes.Select(node => node.layout.center.y).Average());
        }
    }

    public class RuneEffectView : GraphView
    {
        RuneEffect _runeEffect;
        public Action<EffectStepNodeView> OnNodeSelected;
        Vector2 _lastMousePosition = default;

        public RuneEffectView() {
            Insert(0, new GridBackground());

            this.AddManipulator(new ContentZoomer());
            this.AddManipulator(new ContentDragger());
            this.AddManipulator(new SelectionDragger());
            this.AddManipulator(new RectangleSelector());

            serializeGraphElements += CutCopyOperation;
            canPasteSerializedData += CanPasteOperation;
            unserializeAndPaste += PasteOperation;
            RegisterCallback<MouseMoveEvent>(OnMouseMove);

            var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Editor/Rune/RuneEditor.uss");
            styleSheets.Add(styleSheet);
        }

        void OnMouseMove(MouseMoveEvent evt) {
            _lastMousePosition = evt.localMousePosition;
        }

        bool CanPasteOperation(string data) {
            return data != null && _runeEffect != null;
        }

        void PasteOperation(string operationName, string data) {
            EffectStepNodeGraphData dataGraph = new();
            EditorJsonUtility.FromJsonOverwrite(data, dataGraph);

            Dictionary<string, string> originalToNewGuid = new();

            var mousePositionOnGraph = CalculatePositionOnGraph(_lastMousePosition);
            foreach (var node in dataGraph.Nodes) {
                var newGuid = Guid.NewGuid().ToString();
                originalToNewGuid.TryAdd(node.Guid, newGuid);

                node.Guid = newGuid;
                node.Position = node.Position - dataGraph.Center + mousePositionOnGraph;
            }

            foreach (var port in dataGraph.Nodes.SelectMany(node => node.PortsData)) {
                port.ConnectedNodesGuid = port.ConnectedNodesGuid.Select(origGuid => originalToNewGuid.GetValueOrDefault(origGuid)).Where(guid => guid != null).ToList();
            }

            EffectStepNodeGraph clonedGraph = new(dataGraph);

            RegisterNodesEnum(clonedGraph.Nodes);
            CreateNodeViews(clonedGraph.Nodes);
            CreateEdges(clonedGraph.Nodes);
            RefreshSelection(clonedGraph.Nodes);
        }

        string CutCopyOperation(IEnumerable<GraphElement> elements) {

            EffectStepNodeGraphData graph = new(elements.Select(e => e as EffectStepNodeView).Where(e => e != null));

            var data = EditorJsonUtility.ToJson(graph);

            return data;
        }

        internal void PopulateView(RuneEffect runeEffect) {
            graphViewChanged -= OnGraphViewChanged;
            DeleteElements(graphElements);

            _runeEffect = runeEffect;

            if (_runeEffect == null) {
                return;
            }

            graphViewChanged += OnGraphViewChanged;
            CreateNodeViews(_runeEffect.Nodes);
            CreateEdges(_runeEffect.Nodes);
        }

        public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter) {
            return ports.Where(
                endPort => endPort.direction != startPort.direction
                && endPort.node != startPort.node
                && startPort.portType == endPort.portType
                ).ToList();
        }

        void CreateNodeViews(IEnumerable<EffectStepNode> nodes) {
            foreach (var node in nodes) {
                CreateNodeView(node);
            }
        }

        void CreateEdges(IEnumerable<EffectStepNode> nodes) {
            foreach (var inputNode in nodes) {
                var inputView = GetNodeByGuid(inputNode.Guid) as EffectStepNodeView;

                foreach (var inputPort in inputNode.Inputs) {
                    foreach (var outputNode in inputPort.Connections()) {
                        var outputView = GetNodeByGuid(outputNode.Guid) as EffectStepNodeView;
                        var outputPortView = outputView.outputContainer.Children().FirstOrDefault(portView => portView is Port) as Port;
                        var inputPortView = inputView.inputContainer.Children().FirstOrDefault(portView => (((IPort port, EffectStepNode _))portView.userData).port == inputPort) as Port;
                        var edge = outputPortView.ConnectTo(inputPortView);
                        AddElement(edge);
                    }
                }
            }
        }

        void RefreshSelection(IEnumerable<EffectStepNode> nodes) {
            ClearSelection();
            foreach (var nodeView in nodes.Select(n => GetNodeByGuid(n.Guid) as EffectStepNodeView)) {
                AddToSelection(nodeView);
            }
        }

        GraphViewChange OnGraphViewChanged(GraphViewChange graphViewChange) {
            if (graphViewChange.elementsToRemove != null) {
                foreach (var elementToRemove in graphViewChange.elementsToRemove) {
                    switch (elementToRemove) {
                        case EffectStepNodeView effectStepNodeView:
                            Undo.RecordObject(_runeEffect, "Rune Effect (Delete Node)");
                            _runeEffect.DeleteNode(effectStepNodeView.Node);
                            Undo.DestroyObjectImmediate(effectStepNodeView.Node);
                            break;
                        case Edge edge:
                            var outputNodeView = edge.output.node as EffectStepNodeView;
                            (var inputPort, var node) = ((IPort, EffectStepNode))edge.input.userData;
                            Undo.RecordObject(node, "Rune Effect (Delete Edge)");
                            inputPort.TryDisconnect(outputNodeView.Node);
                            break;
                    }
                }
            }

            if (graphViewChange.edgesToCreate != null) {
                foreach (var edge in graphViewChange.edgesToCreate) {
                    var outputNodeView = edge.output.node as EffectStepNodeView;
                    (var inputPort, var node) = ((IPort, EffectStepNode))edge.input.userData;
                    Undo.RecordObject(node, "Rune Effect (Create Edge)");
                    inputPort.TryConnect(outputNodeView.Node);
                }
            }

            return graphViewChange;
        }
        public override void BuildContextualMenu(ContextualMenuPopulateEvent e) {
            // base.BuildContextualMenu(e);
            {
                var mousePositionOnGraph = CalculatePositionOnGraph(this.WorldToLocal(e.mousePosition));
                List<Type> additional = new();
                foreach (var (type, stepType) in TypeCache.GetTypesDerivedFrom(typeof(EffectStepNode))
                                              .Where(type => type.IsAbstract == false)
                                              .Select(type => (type, type.GetTypeInfo().GetCustomAttribute<StepTypeAttribute>()))) {
                    if (stepType != null) {
                        e.menu.AppendAction($"{stepType}/{type.Name}", action => AddNode(type, mousePositionOnGraph));
                    }
                    else {
                        additional.Add(type);
                    }
                }
                foreach (var type in additional) {
                    e.menu.AppendAction($"{type.Name}", action => AddNode(type, mousePositionOnGraph));
                }
            }
        }

        UnityEngine.Vector2 CalculatePositionOnGraph(UnityEngine.Vector2 localPosition) {
            return viewTransform.matrix.inverse.MultiplyPoint(localPosition);
        }

        void AddNode(System.Type type, UnityEngine.Vector2 position) {
            if (_runeEffect == null) {
                return;
            }
            var node = CreateNode(type, position);
            RegisterNodes(node);
            CreateNodeView(node);
        }

        void RegisterNodes(params EffectStepNode[] nodes) => RegisterNodesEnum(nodes);

        void RegisterNodesEnum(IEnumerable<EffectStepNode> nodes) {
            foreach (var node in nodes) {
                Undo.RecordObject(_runeEffect, "Rune Effect (Create Node)");
                _runeEffect.AddNode(node);

                AssetDatabase.AddObjectToAsset(node, _runeEffect);
                Undo.RegisterCreatedObjectUndo(node, "Rune Effect (Create Node)");
            }
        }
        EffectStepNode CreateNode(System.Type type, UnityEngine.Vector2 position) {
            if (_runeEffect == null) {
                return null;
            }

            var node = ScriptableObject.CreateInstance(type) as EffectStepNode;
            node.name = type.Name;
            node.Position = position;

            return node;
        }
        void CreateNodeView(EffectStepNode node) {
            EffectStepNodeView nodeView = new(node) {
                OnNodeSelected = OnNodeSelected,
            };
            AddElement(nodeView);
        }
    }
}