using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text.RegularExpressions;
using UnityEngine;
using UnityEditor.Graphing;
using UnityEditor.Graphing.Util;
using UnityEditor.Rendering;
using UnityEditor.ShaderGraph.Internal;
using UnityEditor.ShaderGraph.Legacy;
using UnityEditor.ShaderGraph.Serialization;
using UnityEditor.ShaderGraph.Drawing;
using Edge = UnityEditor.Graphing.Edge;

using UnityEngine.UIElements;
using UnityEngine.Assertions;
using UnityEngine.Pool;
using UnityEngine.Serialization;

namespace UnityEditor.ShaderGraph
{
    [Serializable]
    [FormerName("UnityEditor.ShaderGraph.MaterialGraph")]
    [FormerName("UnityEditor.ShaderGraph.SubGraph")]
    [FormerName("UnityEditor.ShaderGraph.AbstractMaterialGraph")]
    sealed partial class GraphData : JsonObject
    {
        public override int latestVersion => 3;

        public GraphObject owner { get; set; }

        [NonSerialized]
        internal bool graphIsConcretizing = false;

        #region Input data

        [SerializeField]
        List<JsonData<AbstractShaderProperty>> m_Properties = new List<JsonData<AbstractShaderProperty>>();

        public DataValueEnumerable<AbstractShaderProperty> properties => m_Properties.SelectValue();

        [SerializeField]
        List<JsonData<ShaderKeyword>> m_Keywords = new List<JsonData<ShaderKeyword>>();

        public DataValueEnumerable<ShaderKeyword> keywords => m_Keywords.SelectValue();

        [SerializeField]
        List<JsonData<ShaderDropdown>> m_Dropdowns = new List<JsonData<ShaderDropdown>>();

        public DataValueEnumerable<ShaderDropdown> dropdowns => m_Dropdowns.SelectValue();

        [NonSerialized]
        List<ShaderInput> m_AddedInputs = new List<ShaderInput>();

        public IEnumerable<ShaderInput> addedInputs
        {
            get { return m_AddedInputs; }
        }

        [NonSerialized]
        List<ShaderInput> m_RemovedInputs = new List<ShaderInput>();

        public IEnumerable<ShaderInput> removedInputs
        {
            get { return m_RemovedInputs; }
        }

        [NonSerialized]
        List<ShaderInput> m_MovedInputs = new List<ShaderInput>();

        public IEnumerable<ShaderInput> movedInputs
        {
            get { return m_MovedInputs; }
        }

        [NonSerialized]
        List<CategoryData> m_AddedCategories = new List<CategoryData>();

        public IEnumerable<CategoryData> addedCategories
        {
            get { return m_AddedCategories; }
        }

        [NonSerialized]
        List<CategoryData> m_RemovedCategories = new List<CategoryData>();

        public IEnumerable<CategoryData> removedCategories
        {
            get { return m_RemovedCategories; }
        }

        [NonSerialized]
        List<CategoryData> m_MovedCategories = new List<CategoryData>();

        public IEnumerable<CategoryData> movedCategories
        {
            get { return m_MovedCategories; }
        }

        [NonSerialized]
        bool m_MovedContexts = false;
        public bool movedContexts => m_MovedContexts;

        public string assetGuid { get; set; }

        #endregion

        #region Category Data

        [SerializeField]
        List<JsonData<CategoryData>> m_CategoryData = new List<JsonData<CategoryData>>();

        public DataValueEnumerable<CategoryData> categories => m_CategoryData.SelectValue();

        #endregion

        #region Node data

        [SerializeField]
        List<JsonData<AbstractMaterialNode>> m_Nodes = new List<JsonData<AbstractMaterialNode>>();

        [NonSerialized]
        Dictionary<string, AbstractMaterialNode> m_NodeDictionary = new Dictionary<string, AbstractMaterialNode>();

        [NonSerialized]
        Dictionary<string, AbstractMaterialNode> m_LegacyUpdateDictionary = new Dictionary<string, AbstractMaterialNode>();

        public IEnumerable<T> GetNodes<T>()
        {
            return m_Nodes.SelectValue().OfType<T>();
        }

        [NonSerialized]
        List<AbstractMaterialNode> m_AddedNodes = new List<AbstractMaterialNode>();

        public IEnumerable<AbstractMaterialNode> addedNodes
        {
            get { return m_AddedNodes; }
        }

        [NonSerialized]
        List<AbstractMaterialNode> m_RemovedNodes = new List<AbstractMaterialNode>();

        public IEnumerable<AbstractMaterialNode> removedNodes
        {
            get { return m_RemovedNodes; }
        }

        [NonSerialized]
        List<AbstractMaterialNode> m_PastedNodes = new List<AbstractMaterialNode>();

        public IEnumerable<AbstractMaterialNode> pastedNodes
        {
            get { return m_PastedNodes; }
        }
        #endregion

        #region Group Data

        [SerializeField]
        List<JsonData<GroupData>> m_GroupDatas = new List<JsonData<GroupData>>();

        public DataValueEnumerable<GroupData> groups
        {
            get { return m_GroupDatas.SelectValue(); }
        }

        [NonSerialized]
        List<GroupData> m_AddedGroups = new List<GroupData>();

        public IEnumerable<GroupData> addedGroups
        {
            get { return m_AddedGroups; }
        }

        [NonSerialized]
        List<GroupData> m_RemovedGroups = new List<GroupData>();

        public IEnumerable<GroupData> removedGroups
        {
            get { return m_RemovedGroups; }
        }

        [NonSerialized]
        List<GroupData> m_PastedGroups = new List<GroupData>();

        public IEnumerable<GroupData> pastedGroups
        {
            get { return m_PastedGroups; }
        }

        [NonSerialized]
        List<ParentGroupChange> m_ParentGroupChanges = new List<ParentGroupChange>();

        public IEnumerable<ParentGroupChange> parentGroupChanges
        {
            get { return m_ParentGroupChanges; }
        }

        [NonSerialized]
        GroupData m_MostRecentlyCreatedGroup;

        public GroupData mostRecentlyCreatedGroup => m_MostRecentlyCreatedGroup;

        [NonSerialized]
        Dictionary<JsonRef<GroupData>, List<IGroupItem>> m_GroupItems = new Dictionary<JsonRef<GroupData>, List<IGroupItem>>();

        public IEnumerable<IGroupItem> GetItemsInGroup(GroupData groupData)
        {
            if (m_GroupItems.TryGetValue(groupData, out var nodes))
            {
                return nodes;
            }
            return Enumerable.Empty<IGroupItem>();
        }

        #endregion

        #region StickyNote Data
        [SerializeField]
        List<JsonData<StickyNoteData>> m_StickyNoteDatas = new List<JsonData<StickyNoteData>>();

        public DataValueEnumerable<StickyNoteData> stickyNotes => m_StickyNoteDatas.SelectValue();

        [NonSerialized]
        List<StickyNoteData> m_AddedStickyNotes = new List<StickyNoteData>();

        public List<StickyNoteData> addedStickyNotes => m_AddedStickyNotes;

        [NonSerialized]
        List<StickyNoteData> m_RemovedNotes = new List<StickyNoteData>();

        public IEnumerable<StickyNoteData> removedNotes => m_RemovedNotes;

        [NonSerialized]
        List<StickyNoteData> m_PastedStickyNotes = new List<StickyNoteData>();

        public IEnumerable<StickyNoteData> pastedStickyNotes => m_PastedStickyNotes;

        #endregion

        #region Edge data

        [SerializeField]
        List<Edge> m_Edges = new List<Edge>();

        public IEnumerable<Edge> edges => m_Edges;

        [NonSerialized]
        Dictionary<string, List<IEdge>> m_NodeEdges = new Dictionary<string, List<IEdge>>();

        [NonSerialized]
        List<IEdge> m_AddedEdges = new List<IEdge>();

        public IEnumerable<IEdge> addedEdges
        {
            get { return m_AddedEdges; }
        }

        [NonSerialized]
        List<IEdge> m_RemovedEdges = new List<IEdge>();

        public IEnumerable<IEdge> removedEdges
        {
            get { return m_RemovedEdges; }
        }

        #endregion

        #region Context Data

        [SerializeField]
        ContextData m_VertexContext;

        [SerializeField]
        ContextData m_FragmentContext;

        // We build this once and cache it as it uses reflection
        // This list is used to build the Create Node menu entries for Blocks
        // as well as when deserializing descriptor fields on serialized Blocks
        [NonSerialized]
        List<BlockFieldDescriptor> m_BlockFieldDescriptors;

        public ContextData vertexContext => m_VertexContext;
        public ContextData fragmentContext => m_FragmentContext;
        public List<BlockFieldDescriptor> blockFieldDescriptors => m_BlockFieldDescriptors;

        #endregion

        [SerializeField]
        InspectorPreviewData m_PreviewData = new InspectorPreviewData();

        public InspectorPreviewData previewData
        {
            get { return m_PreviewData; }
            set { m_PreviewData = value; }
        }

        [SerializeField]
        string m_Path;

        public string path
        {
            get { return m_Path; }
            set
            {
                if (m_Path == value)
                    return;
                m_Path = value;
                if (owner != null)
                    owner.RegisterCompleteObjectUndo("Change Path");
            }
        }

        public MessageManager messageManager { get; set; }
        public bool isSubGraph { get; set; }

        // we default this to Graph for subgraphs
        // but for shadergraphs, this will get replaced with Single
        [SerializeField]
        private GraphPrecision m_GraphPrecision = GraphPrecision.Graph;

        public GraphPrecision graphDefaultPrecision
        {
            get
            {
                // shader graphs are not allowed to have graph precision
                // we force them to Single if they somehow get set to graph
                if ((!isSubGraph) && (m_GraphPrecision == GraphPrecision.Graph))
                    return GraphPrecision.Single;

                return m_GraphPrecision;
            }
        }

        public ConcretePrecision graphDefaultConcretePrecision
        {
            get
            {
                // when in "Graph switchable" mode, we choose Half as the default concrete precision
                // so you can visualize the worst-case
                return graphDefaultPrecision.ToConcrete(ConcretePrecision.Half);
            }
        }

        // Some state has been changed that requires checking for the auto add/removal of blocks.
        // This needs to be checked at a later point in time so actions like replace (remove + add) don't remove blocks.
        internal bool checkAutoAddRemoveBlocks { get; set; }

        public void SetGraphDefaultPrecision(GraphPrecision newGraphDefaultPrecision)
        {
            if ((!isSubGraph) && (newGraphDefaultPrecision == GraphPrecision.Graph))
            {
                // shader graphs can't be set to "Graph", only subgraphs can
                Debug.LogError("Cannot set ShaderGraph to a default precision of Graph");
            }
            else
            {
                m_GraphPrecision = newGraphDefaultPrecision;
            }
        }

        // NOTE: having preview mode default to 3D preserves the old behavior of pre-existing subgraphs
        // if we change this, we would have to introduce a versioning step if we want to maintain the old behavior
        [SerializeField]
        private PreviewMode m_PreviewMode = PreviewMode.Preview3D;

        public PreviewMode previewMode
        {
            get => m_PreviewMode;
            set => m_PreviewMode = value;
        }

        [SerializeField]
        JsonRef<AbstractMaterialNode> m_OutputNode;

        public AbstractMaterialNode outputNode
        {
            get => m_OutputNode;
            set => m_OutputNode = value;
        }

        internal delegate void SaveGraphDelegate(Shader shader, object context);
        internal static SaveGraphDelegate onSaveGraph;

        #region Targets

        // Serialized list of user-selected active targets, sorted in displayName order (to maintain deterministic serialization order)
        // some of these may be MultiJsonInternal.UnknownTargetType if we can't recognize the type of the target
        [SerializeField]
        internal List<JsonData<Target>> m_ActiveTargets = new List<JsonData<Target>>();      // After adding to this list, you MUST call SortActiveTargets()
        public DataValueEnumerable<Target> activeTargets => m_ActiveTargets.SelectValue();

        // this stores all of the current possible Target types (including any unknown target types we serialized in)
        class PotentialTarget
        {
            // the potential Target
            Target m_Target;

            // a Target is either known (we know the Type) or unknown (can't find a matching definition of the Type)
            // Targets of unknown type are stored in an UnknownTargetType
            private Type m_KnownType;
            private MultiJsonInternal.UnknownTargetType m_UnknownTarget;

            public PotentialTarget(Target target)
            {
                m_Target = target;

                if (target is MultiJsonInternal.UnknownTargetType)
                {
                    m_UnknownTarget = (MultiJsonInternal.UnknownTargetType)target;
                    m_KnownType = null;
                }
                else
                {
                    m_UnknownTarget = null;
                    m_KnownType = target.GetType();
                }
            }

            public bool IsUnknown()
            {
                return m_UnknownTarget != null;
            }

            public MultiJsonInternal.UnknownTargetType GetUnknown()
            {
                return m_UnknownTarget;
            }

            public Type knownType { get { return m_KnownType; } }

            public bool Is(Target t)
            {
                return t == m_Target;
            }

            public string GetDisplayName()
            {
                return m_Target.displayName;
            }

            public void ReplaceStoredTarget(Target t)
            {
                if (m_KnownType != null)
                    Assert.IsTrue(t.GetType() == m_KnownType);
                m_Target = t;
            }

            public Target GetTarget()
            {
                return m_Target;
            }
        }
        [NonSerialized]
        List<PotentialTarget> m_AllPotentialTargets = new List<PotentialTarget>();
        public IEnumerable<Target> allPotentialTargets => m_AllPotentialTargets.Select(x => x.GetTarget());

        public int GetTargetIndexByKnownType(Type targetType)
        {
            return m_AllPotentialTargets.FindIndex(pt => pt.knownType == targetType);
        }

        public int GetTargetIndex(Target t)
        {
            int result = m_AllPotentialTargets.FindIndex(pt => pt.Is(t));
            return result;
        }

        public List<string> GetPotentialTargetDisplayNames()
        {
            List<string> displayNames = new List<string>(m_AllPotentialTargets.Count);
            for (int validIndex = 0; validIndex < m_AllPotentialTargets.Count; validIndex++)
            {
                displayNames.Add(m_AllPotentialTargets[validIndex].GetDisplayName());
            }
            return displayNames;
        }

        public void SetTargetActive(Target target, bool skipSortAndUpdate = false)
        {
            int activeIndex = m_ActiveTargets.IndexOf(target);
            if (activeIndex < 0)
            {
                activeIndex = m_ActiveTargets.Count;
                m_ActiveTargets.Add(target);
            }

            // active known targets should replace the stored Target in AllPotentialTargets
            if (target is MultiJsonInternal.UnknownTargetType unknownTarget)
            {
                // find any existing potential target with the same unknown jsonData
                int targetIndex = m_AllPotentialTargets.FindIndex(
                    pt => pt.IsUnknown() && (pt.GetUnknown().jsonData == unknownTarget.jsonData));

                // replace existing target, or add it if there is none
                if (targetIndex >= 0)
                    m_AllPotentialTargets[targetIndex] = new PotentialTarget(target);
                else
                    m_AllPotentialTargets.Add(new PotentialTarget(target));
            }
            else
            {
                // known types should already have been registered
                Type targetType = target.GetType();
                int targetIndex = GetTargetIndexByKnownType(targetType);
                Assert.IsTrue(targetIndex >= 0);
                m_AllPotentialTargets[targetIndex].ReplaceStoredTarget(target);
            }

            if (!skipSortAndUpdate)
                SortAndUpdateActiveTargets();
        }

        public void SetTargetActive(int targetIndex, bool skipSortAndUpdate = false)
        {
            Target target = m_AllPotentialTargets[targetIndex].GetTarget();
            SetTargetActive(target, skipSortAndUpdate);
        }

        public void SetTargetInactive(Target target, bool skipSortAndUpdate = false)
        {
            int activeIndex = m_ActiveTargets.IndexOf(target);
            if (activeIndex < 0)
                return;

            int targetIndex = GetTargetIndex(target);

            // if a target was in the active targets, it should also have been in the potential targets list
            Assert.IsTrue(targetIndex >= 0);

            m_ActiveTargets.RemoveAt(activeIndex);

            if (!skipSortAndUpdate)
                SortAndUpdateActiveTargets();
        }

        // this list is populated by graph validation, and lists all of the targets that nodes did not like
        [NonSerialized]
        List<Target> m_UnsupportedTargets = new List<Target>();
        public List<Target> unsupportedTargets { get => m_UnsupportedTargets; }

        private Comparison<Target> targetComparison = new Comparison<Target>((a, b) => string.Compare(a.displayName, b.displayName));
        public void SortActiveTargets()
        {
            activeTargets.Sort(targetComparison);
        }

        // TODO: Need a better way to handle this
#if VFX_GRAPH_10_0_0_OR_NEWER
        public bool hasVFXCompatibleTarget => activeTargets.Any(o => o.SupportsVFX());
        public bool hasVFXTarget
        {
            get
            {
                bool supports = true;
                supports &= !isSubGraph;
                supports &= activeTargets.Any();
                // Maintain support for VFXTarget and VFX compatible targets.
                supports &= activeTargets.OfType<VFXTarget>().Any() || hasVFXCompatibleTarget;
                return supports;
            }
        }

        public bool isOnlyVFXTarget => activeTargets.Count() == 1 &&
        activeTargets.Count(t => t is VFXTarget) == 1;
#else
        public bool isVFXTarget => false;
        public bool isOnlyVFXTarget => false;
#endif
        #endregion

        public GraphData()
        {
            m_GroupItems[null] = new List<IGroupItem>();
            GetBlockFieldDescriptors();
            AddKnownTargetsToPotentialTargets();
        }

        // used to initialize the graph with targets, i.e. when creating new graphs via the popup menu
        public void InitializeOutputs(Target[] targets, BlockFieldDescriptor[] blockDescriptors)
        {
            if (targets == null)
                return;

            foreach (var target in targets)
            {
                if (GetTargetIndexByKnownType(target.GetType()) >= 0)
                {
                    SetTargetActive(target, true);
                }
            }
            SortActiveTargets();

            if (blockDescriptors != null)
            {
                foreach (var descriptor in blockDescriptors)
                {
                    var contextData = descriptor.shaderStage == ShaderStage.Fragment ? m_FragmentContext : m_VertexContext;
                    var block = (BlockNode)Activator.CreateInstance(typeof(BlockNode));
                    block.Init(descriptor);
                    AddBlockNoValidate(block, contextData, contextData.blocks.Count);
                }
            }

            ValidateGraph();
            var activeBlocks = GetActiveBlocksForAllActiveTargets();
            UpdateActiveBlocks(activeBlocks);
        }

        void GetBlockFieldDescriptors()
        {
            m_BlockFieldDescriptors = new List<BlockFieldDescriptor>();

            var asmTypes = TypeCache.GetTypesWithAttribute<GenerateBlocksAttribute>();
            foreach (var type in asmTypes)
            {
                var attrs = type.GetCustomAttributes(typeof(GenerateBlocksAttribute), false);
                if (attrs == null || attrs.Length <= 0)
                    continue;

                var attribute = attrs[0] as GenerateBlocksAttribute;

                // Get all fields that are BlockFieldDescriptor
                // If field and context stages match add to list
                foreach (var fieldInfo in type.GetFields())
                {
                    if (fieldInfo.GetValue(type) is BlockFieldDescriptor blockFieldDescriptor)
                    {
                        blockFieldDescriptor.path = attribute.path;
                        m_BlockFieldDescriptors.Add(blockFieldDescriptor);
                    }
                }
            }
        }

        void AddKnownTargetsToPotentialTargets()
        {
            Assert.AreEqual(m_AllPotentialTargets.Count, 0);

            // Find all valid Targets by looking in the TypeCache
            var targetTypes = TypeCache.GetTypesDerivedFrom<Target>();
            foreach (var type in targetTypes)
            {
                if (type.IsAbstract || type.IsGenericType || !type.IsClass)
                    continue;

                // create a new instance of the Target, to represent the potential Target
                // NOTE: this instance may be replaced later if we serialize in an Active Target of that type
                var target = (Target)Activator.CreateInstance(type);
                if (!target.isHidden)
                {
                    m_AllPotentialTargets.Add(new PotentialTarget(target));
                }
            }
        }

        public void SortAndUpdateActiveTargets()
        {
            SortActiveTargets();
            ValidateGraph();
            NodeUtils.ReevaluateActivityOfNodeList(m_Nodes.SelectValue());
        }

        public void ClearChanges()
        {
            m_AddedNodes.Clear();
            m_RemovedNodes.Clear();
            m_PastedNodes.Clear();
            m_ParentGroupChanges.Clear();
            m_AddedGroups.Clear();
            m_RemovedGroups.Clear();
            m_PastedGroups.Clear();
            m_AddedEdges.Clear();
            m_RemovedEdges.Clear();
            m_AddedInputs.Clear();
            m_RemovedInputs.Clear();
            m_MovedInputs.Clear();
            m_AddedCategories.Clear();
            m_RemovedCategories.Clear();
            m_MovedCategories.Clear();
            m_AddedStickyNotes.Clear();
            m_RemovedNotes.Clear();
            m_PastedStickyNotes.Clear();
            m_MostRecentlyCreatedGroup = null;
            m_MovedContexts = false;
        }

        public void AddNode(AbstractMaterialNode node)
        {
            if (node is AbstractMaterialNode materialNode)
            {
                if (isSubGraph && !materialNode.allowedInSubGraph)
                {
                    Debug.LogWarningFormat("Attempting to add {0} to Sub Graph. This is not allowed.", materialNode.GetType());
                    return;
                }

                AddNodeNoValidate(materialNode);

                // If adding a Sub Graph node whose asset contains Keywords
                // Need to restest Keywords against the variant limit
                if (node is SubGraphNode subGraphNode &&
                    subGraphNode.asset != null &&
                    subGraphNode.asset.keywords.Any())
                {
                    OnKeywordChangedNoValidate();
                }

                ValidateGraph();
            }
            else
            {
                Debug.LogWarningFormat("Trying to add node {0} to Material graph, but it is not a {1}", node, typeof(AbstractMaterialNode));
            }
        }

        public void CreateGroup(GroupData groupData)
        {
            if (AddGroup(groupData))
            {
                m_MostRecentlyCreatedGroup = groupData;
            }
        }

        bool AddGroup(GroupData groupData)
        {
            if (m_GroupDatas.Contains(groupData))
                return false;

            m_GroupDatas.Add(groupData);
            m_AddedGroups.Add(groupData);
            m_GroupItems.Add(groupData, new List<IGroupItem>());

            return true;
        }

        public void RemoveGroup(GroupData groupData)
        {
            RemoveGroupNoValidate(groupData);
            ValidateGraph();
        }

        void RemoveGroupNoValidate(GroupData group)
        {
            if (!m_GroupDatas.Contains(group))
                throw new InvalidOperationException("Cannot remove a group that doesn't exist.");
            m_GroupDatas.Remove(group);
            m_RemovedGroups.Add(group);

            if (m_GroupItems.TryGetValue(group, out var items))
            {
                foreach (IGroupItem groupItem in items.ToList())
                {
                    SetGroup(groupItem, null);
                }

                m_GroupItems.Remove(group);
            }
        }

        public void AddStickyNote(StickyNoteData stickyNote)
        {
            if (m_StickyNoteDatas.Contains(stickyNote))
            {
                throw new InvalidOperationException("Sticky note has already been added to the graph.");
            }

            if (!m_GroupItems.ContainsKey(stickyNote.group))
            {
                throw new InvalidOperationException("Trying to add sticky note with group that doesn't exist.");
            }

            m_StickyNoteDatas.Add(stickyNote);
            m_AddedStickyNotes.Add(stickyNote);
            m_GroupItems[stickyNote.group].Add(stickyNote);
        }

        void RemoveNoteNoValidate(StickyNoteData stickyNote)
        {
            if (!m_StickyNoteDatas.Contains(stickyNote))
            {
                throw new InvalidOperationException("Cannot remove a note that doesn't exist.");
            }

            m_StickyNoteDatas.Remove(stickyNote);
            m_RemovedNotes.Add(stickyNote);

            if (m_GroupItems.TryGetValue(stickyNote.group, out var groupItems))
            {
                groupItems.Remove(stickyNote);
            }
        }

        public void RemoveStickyNote(StickyNoteData stickyNote)
        {
            RemoveNoteNoValidate(stickyNote);
            ValidateGraph();
        }

        public void SetGroup(IGroupItem node, GroupData group)
        {
            var groupChange = new ParentGroupChange()
            {
                groupItem = node,
                oldGroup = node.group,
                // Checking if the groupdata is null. If it is, then it means node has been removed out of a group.
                // If the group data is null, then maybe the old group id should be removed
                newGroup = group,
            };
            node.group = groupChange.newGroup;

            var oldGroupNodes = m_GroupItems[groupChange.oldGroup];
            oldGroupNodes.Remove(node);

            m_GroupItems[groupChange.newGroup].Add(node);
            m_ParentGroupChanges.Add(groupChange);
        }

        public void AddContexts()
        {
            m_VertexContext = new ContextData();
            m_VertexContext.shaderStage = ShaderStage.Vertex;
            m_VertexContext.position = new Vector2(0, 0);
            m_FragmentContext = new ContextData();
            m_FragmentContext.shaderStage = ShaderStage.Fragment;
            m_FragmentContext.position = new Vector2(0, 200);
        }

        public void AddBlock(BlockNode blockNode, ContextData contextData, int index)
        {
            AddBlockNoValidate(blockNode, contextData, index);
            ValidateGraph();

            var activeBlocks = GetActiveBlocksForAllActiveTargets();
            UpdateActiveBlocks(activeBlocks);
        }

        void AddBlockNoValidate(BlockNode blockNode, ContextData contextData, int index)
        {
            // Regular AddNode path
            AddNodeNoValidate(blockNode);

            // Set BlockNode properties
            blockNode.contextData = contextData;

            // Add to ContextData
            if (index == -1 || index >= contextData.blocks.Count())
            {
                contextData.blocks.Add(blockNode);
            }
            else
            {
                contextData.blocks.Insert(index, blockNode);
            }
        }

        public List<BlockFieldDescriptor> GetActiveBlocksForAllActiveTargets()
        {
            // Get list of active Block types
            var currentBlocks = GetNodes<BlockNode>();
            var context = new TargetActiveBlockContext(currentBlocks.Select(x => x.descriptor).ToList(), null);
            foreach (var target in activeTargets)
            {
                target.GetActiveBlocks(ref context);
            }

            // custom blocks aren't going to exist in GetActiveBlocks, we need to ensure we grab those too.
            foreach (var cibnode in currentBlocks.Where(bn => bn.isCustomBlock))
            {
                context.AddBlock(cibnode.descriptor);
            }
            return context.activeBlocks;
        }

        public void UpdateActiveBlocks(List<BlockFieldDescriptor> activeBlockDescriptors)
        {
            // Set Blocks as active based on supported Block list
            //Note: we never want unknown blocks to be active, so explicitly set them to inactive always
            bool disableCI = activeTargets.All(at => at.ignoreCustomInterpolators);
            foreach (var vertexBlock in vertexContext.blocks)
            {
                if (vertexBlock.value?.isCustomBlock == true)
                {
                    vertexBlock.value.SetOverrideActiveState(disableCI ? AbstractMaterialNode.ActiveState.ExplicitInactive : AbstractMaterialNode.ActiveState.ExplicitActive);
                }
                else if (vertexBlock.value?.descriptor?.isUnknown == true)
                {
                    vertexBlock.value.SetOverrideActiveState(AbstractMaterialNode.ActiveState.ExplicitInactive);
                }
                else
                {
                    vertexBlock.value.SetOverrideActiveState(activeBlockDescriptors.Contains(vertexBlock.value.descriptor) ? AbstractMaterialNode.ActiveState.ExplicitActive
                        : AbstractMaterialNode.ActiveState.ExplicitInactive);
                }
            }
            foreach (var fragmentBlock in fragmentContext.blocks)
            {
                if (fragmentBlock.value?.descriptor?.isUnknown == true)
                {
                    fragmentBlock.value.SetOverrideActiveState(AbstractMaterialNode.ActiveState.ExplicitInactive);
                }
                else
                {
                    fragmentBlock.value.SetOverrideActiveState(activeBlockDescriptors.Contains(fragmentBlock.value.descriptor) ? AbstractMaterialNode.ActiveState.ExplicitActive
                        : AbstractMaterialNode.ActiveState.ExplicitInactive);
                }
            }
        }

        public void AddRemoveBlocksFromActiveList(List<BlockFieldDescriptor> activeBlockDescriptors)
        {
            var blocksToRemove = ListPool<BlockNode>.Get();

            void GetBlocksToRemoveForContext(ContextData contextData)
            {
                for (int i = 0; i < contextData.blocks.Count; i++)
                {
                    if (contextData.blocks[i].value?.isCustomBlock == true) // custom interpolators are fine.
                        continue;

                    var block = contextData.blocks[i];
                    if (!activeBlockDescriptors.Contains(block.value.descriptor))
                    {
                        var slot = block.value.FindSlot<MaterialSlot>(0);
                        //Need to check if a slot is not default value OR is an untracked unknown block type
                        if (slot.IsUsingDefaultValue() || block.value.descriptor.isUnknown) // TODO: How to check default value
                        {
                            blocksToRemove.Add(block);
                        }
                    }
                }
            }

            void TryAddBlockToContext(BlockFieldDescriptor descriptor, ContextData contextData)
            {
                if (descriptor.shaderStage != contextData.shaderStage)
                    return;

                if (contextData.blocks.Any(x => x.value.descriptor.Equals(descriptor)))
                    return;

                var node = (BlockNode)Activator.CreateInstance(typeof(BlockNode));
                node.Init(descriptor);
                AddBlockNoValidate(node, contextData, contextData.blocks.Count);
            }

            // Get inactive Blocks to remove
            GetBlocksToRemoveForContext(vertexContext);
            GetBlocksToRemoveForContext(fragmentContext);

            // Remove blocks
            foreach (var block in blocksToRemove)
            {
                RemoveNodeNoValidate(block);
            }

            // Add active Blocks not currently in Contexts
            foreach (var descriptor in activeBlockDescriptors)
            {
                TryAddBlockToContext(descriptor, vertexContext);
                TryAddBlockToContext(descriptor, fragmentContext);
            }
        }

        void AddNodeNoValidate(AbstractMaterialNode node)
        {
            if (node.group != null && !m_GroupItems.ContainsKey(node.group))
            {
                throw new InvalidOperationException("Cannot add a node whose group doesn't exist.");
            }
            node.owner = this;
            m_Nodes.Add(node);
            m_NodeDictionary.Add(node.objectId, node);
            m_AddedNodes.Add(node);
            m_GroupItems[node.group].Add(node);
        }

        public void RemoveNode(AbstractMaterialNode node)
        {
            if (!node.canDeleteNode)
            {
                throw new InvalidOperationException($"Node {node.name} ({node.objectId}) cannot be deleted.");
            }
            RemoveNodeNoValidate(node);
            ValidateGraph();

            if (node is BlockNode blockNode)
            {
                var activeBlocks = GetActiveBlocksForAllActiveTargets();
                UpdateActiveBlocks(activeBlocks);
                blockNode.Dirty(ModificationScope.Graph);
            }
        }

        void RemoveNodeNoValidate(AbstractMaterialNode node)
        {
            if (!m_NodeDictionary.ContainsKey(node.objectId) && node.isActive && !m_RemovedNodes.Contains(node))
            {
                throw new InvalidOperationException("Cannot remove a node that doesn't exist.");
            }

            m_Nodes.Remove(node);
            m_NodeDictionary.Remove(node.objectId);
            messageManager?.RemoveNode(node.objectId);
            m_RemovedNodes.Add(node);

            if (m_GroupItems.TryGetValue(node.group, out var groupItems))
            {
                groupItems.Remove(node);
            }

            if (node is BlockNode blockNode && blockNode.contextData != null)
            {
                // Remove from ContextData
                blockNode.contextData.blocks.Remove(blockNode);
            }
        }

        void AddEdgeToNodeEdges(IEdge edge)
        {
            List<IEdge> inputEdges;
            if (!m_NodeEdges.TryGetValue(edge.inputSlot.node.objectId, out inputEdges))
                m_NodeEdges[edge.inputSlot.node.objectId] = inputEdges = new List<IEdge>();
            inputEdges.Add(edge);

            List<IEdge> outputEdges;
            if (!m_NodeEdges.TryGetValue(edge.outputSlot.node.objectId, out outputEdges))
                m_NodeEdges[edge.outputSlot.node.objectId] = outputEdges = new List<IEdge>();
            outputEdges.Add(edge);
        }

        IEdge ConnectNoValidate(SlotReference fromSlotRef, SlotReference toSlotRef)
        {
            var fromNode = fromSlotRef.node;
            var toNode = toSlotRef.node;

            if (fromNode == null || toNode == null)
                return null;

            // both nodes must belong to this graph
            if ((fromNode.owner != this) || (toNode.owner != this))
                return null;

            // if fromNode is already connected to toNode
            // do now allow a connection as toNode will then
            // have an edge to fromNode creating a cycle.
            // if this is parsed it will lead to an infinite loop.
            var dependentNodes = new List<AbstractMaterialNode>();
            NodeUtils.CollectNodesNodeFeedsInto(dependentNodes, toNode);
            if (dependentNodes.Contains(fromNode))
                return null;

            var fromSlot = fromNode.FindSlot<MaterialSlot>(fromSlotRef.slotId);
            var toSlot = toNode.FindSlot<MaterialSlot>(toSlotRef.slotId);

            if (fromSlot == null || toSlot == null)
                return null;

            if (fromSlot.isOutputSlot == toSlot.isOutputSlot)
                return null;

            var outputSlot = fromSlot.isOutputSlot ? fromSlotRef : toSlotRef;
            var inputSlot = fromSlot.isInputSlot ? fromSlotRef : toSlotRef;

            s_TempEdges.Clear();
            GetEdges(inputSlot, s_TempEdges);

            // remove any inputs that exits before adding
            foreach (var edge in s_TempEdges)
            {
                RemoveEdgeNoValidate(edge);
            }

            var newEdge = new Edge(outputSlot, inputSlot);
            m_Edges.Add(newEdge);
            m_AddedEdges.Add(newEdge);
            AddEdgeToNodeEdges(newEdge);
            NodeUtils.ReevaluateActivityOfConnectedNodes(toNode);

            //Debug.LogFormat("Connected edge: {0} -> {1} ({2} -> {3})\n{4}", newEdge.outputSlot.nodeGuid, newEdge.inputSlot.nodeGuid, fromNode.name, toNode.name, Environment.StackTrace);
            return newEdge;
        }

        public IEdge Connect(SlotReference fromSlotRef, SlotReference toSlotRef)
        {
            var newEdge = ConnectNoValidate(fromSlotRef, toSlotRef);
            ValidateGraph();
            return newEdge;
        }

        internal void UnnotifyAddedEdge(IEdge edge)
        {
            m_AddedEdges.Remove(edge);
        }

        public void RemoveEdge(IEdge e)
        {
            RemoveEdgeNoValidate(e);
            ValidateGraph();
        }

        public void RemoveElements(AbstractMaterialNode[] nodes, IEdge[] edges, GroupData[] groups, StickyNoteData[] notes)
        {
            foreach (var node in nodes)
            {
                if (!node.canDeleteNode)
                {
                    throw new InvalidOperationException($"Node {node.name} ({node.objectId}) cannot be deleted.");
                }
            }

            foreach (var edge in edges.ToArray())
            {
                RemoveEdgeNoValidate(edge);
            }

            foreach (var serializableNode in nodes)
            {
                // Check if it is a Redirect Node
                // Get the edges and then re-create all Edges
                // This only works if it has all the edges.
                // If one edge is already deleted then we can not re-create.
                if (serializableNode is RedirectNodeData redirectNode)
                {
                    redirectNode.GetOutputAndInputSlots(out SlotReference outputSlotRef, out var inputSlotRefs);

                    foreach (SlotReference slot in inputSlotRefs)
                    {
                        ConnectNoValidate(outputSlotRef, slot);
                    }
                }

                RemoveNodeNoValidate(serializableNode);
            }

            foreach (var noteData in notes)
            {
                RemoveNoteNoValidate(noteData);
            }

            foreach (var groupData in groups)
            {
                RemoveGroupNoValidate(groupData);
            }

            ValidateGraph();

            if (nodes.Any(x => x is BlockNode))
            {
                var activeBlocks = GetActiveBlocksForAllActiveTargets();
                UpdateActiveBlocks(activeBlocks);
            }
        }

        void RemoveEdgeNoValidate(IEdge e, bool reevaluateActivity = true)
        {
            e = m_Edges.FirstOrDefault(x => x.Equals(e));
            if (e == null)
                throw new ArgumentException("Trying to remove an edge that does not exist.", "e");
            m_Edges.Remove(e as Edge);

            AbstractMaterialNode input = e.inputSlot.node, output = e.outputSlot.node;
            if (input != null && ShaderGraphPreferences.autoAddRemoveBlocks)
            {
                checkAutoAddRemoveBlocks = true;
            }

            List<IEdge> inputNodeEdges;
            if (m_NodeEdges.TryGetValue(input.objectId, out inputNodeEdges))
                inputNodeEdges.Remove(e);

            List<IEdge> outputNodeEdges;
            if (m_NodeEdges.TryGetValue(output.objectId, out outputNodeEdges))
                outputNodeEdges.Remove(e);

            m_AddedEdges.Remove(e);
            m_RemovedEdges.Add(e);

            if (reevaluateActivity)
            {
                if (input != null)
                {
                    NodeUtils.ReevaluateActivityOfConnectedNodes(input);
                }

                if (output != null)
                {
                    NodeUtils.ReevaluateActivityOfConnectedNodes(output);
                }
            }
        }

        public AbstractMaterialNode GetNodeFromId(string nodeId)
        {
            m_NodeDictionary.TryGetValue(nodeId, out var node);
            return node;
        }

        public T GetNodeFromId<T>(string nodeId) where T : class
        {
            m_NodeDictionary.TryGetValue(nodeId, out var node);
            return node as T;
        }

        internal Texture2DShaderProperty GetMainTexture()
        {
            foreach (var prop in properties)
            {
                if (prop is Texture2DShaderProperty tex)
                {
                    if (tex.isMainTexture)
                    {
                        return tex;
                    }
                }
            }
            return null;
        }

        internal ColorShaderProperty GetMainColor()
        {
            foreach (var prop in properties)
            {
                if (prop is ColorShaderProperty col)
                {
                    if (col.isMainColor)
                    {
                        return col;
                    }
                }
            }
            return null;
        }

        public bool ContainsCategory(CategoryData categoryData)
        {
            return categories.Contains(categoryData);
        }

        public bool ContainsInput(ShaderInput shaderInput)
        {
            if (shaderInput == null)
                return false;

            return properties.Contains(shaderInput) || keywords.Contains(shaderInput) || dropdowns.Contains(shaderInput);
        }

        public bool ContainsNode(AbstractMaterialNode node)
        {
            if (node == null)
                return false;

            return m_NodeDictionary.TryGetValue(node.objectId, out var foundNode) && node == foundNode;
        }

        public void GetEdges(SlotReference s, List<IEdge> foundEdges)
        {
            MaterialSlot slot = s.slot;

            List<IEdge> candidateEdges;
            if (!m_NodeEdges.TryGetValue(s.node.objectId, out candidateEdges))
                return;

            foreach (var edge in candidateEdges)
            {
                var cs = slot.isInputSlot ? edge.inputSlot : edge.outputSlot;
                if (cs.node == s.node && cs.slotId == s.slotId)
                    foundEdges.Add(edge);
            }
        }

        public IEnumerable<IEdge> GetEdges(SlotReference s)
        {
            var edges = new List<IEdge>();
            GetEdges(s, edges);
            return edges;
        }

        public void GetEdges(AbstractMaterialNode node, List<IEdge> foundEdges)
        {
            if (m_NodeEdges.TryGetValue(node.objectId, out var edges))
            {
                foundEdges.AddRange(edges);
            }
        }

        public IEnumerable<IEdge> GetEdges(AbstractMaterialNode node)
        {
            List<IEdge> edges = new List<IEdge>();
            GetEdges(node, edges);
            return edges;
        }

        public void ForeachHLSLProperty(Action<HLSLProperty> action)
        {
            foreach (var prop in properties)
                prop.ForeachHLSLProperty(action);
        }

        public void CollectShaderProperties(PropertyCollector collector, GenerationMode generationMode)
        {
            foreach (var prop in properties)
            {
                // For VFX Shader generation, we must omit exposed properties from the Material CBuffer.
                // This is because VFX computes properties on the fly in the vertex stage, and packed into interpolator.
                if (generationMode == GenerationMode.VFX && prop.isExposed)
                {
                    prop.overrideHLSLDeclaration = true;
                    prop.hlslDeclarationOverride = HLSLDeclaration.DoNotDeclare;
                }

                // ugh, this needs to be moved to the gradient property implementation
                if (prop is GradientShaderProperty gradientProp && generationMode == GenerationMode.Preview)
                {
                    GradientUtil.GetGradientPropertiesForPreview(collector, gradientProp.referenceName, gradientProp.value);
                    continue;
                }

                collector.AddShaderProperty(prop);
            }
        }

        public void CollectShaderKeywords(KeywordCollector collector, GenerationMode generationMode)
        {
            foreach (var keyword in keywords)
            {
                collector.AddShaderKeyword(keyword);
            }

            // Alwways calculate permutations when collecting
            collector.CalculateKeywordPermutations();
        }

        public bool IsInputAllowedInGraph(ShaderInput input)
        {
            return (isSubGraph && input.allowedInSubGraph) || (!isSubGraph && input.allowedInMainGraph);
        }

        public bool IsInputAllowedInGraph(AbstractMaterialNode node)
        {
            return (isSubGraph && node.allowedInSubGraph) || (!isSubGraph && node.allowedInMainGraph);
        }

        // adds the input to the graph, and sanitizes the names appropriately
        public void AddGraphInput(ShaderInput input, int index = -1)
        {
            if (input == null)
                return;

            // sanitize the display name
            input.SetDisplayNameAndSanitizeForGraph(this);

            // sanitize the reference name
            input.SetReferenceNameAndSanitizeForGraph(this);

            AddGraphInputNoSanitization(input, index);
        }

        // just adds the input to the graph, does not fix colliding or illegal names
        internal void AddGraphInputNoSanitization(ShaderInput input, int index = -1)
        {
            if (input == null)
                return;

            switch (input)
            {
                case AbstractShaderProperty property:
                    if (m_Properties.Contains(property))
                        return;

                    if (index < 0)
                        m_Properties.Add(property);
                    else
                        m_Properties.Insert(index, property);

                    break;
                case ShaderKeyword keyword:
                    if (m_Keywords.Contains(keyword))
                        return;

                    if (index < 0)
                        m_Keywords.Add(keyword);
                    else
                        m_Keywords.Insert(index, keyword);

                    OnKeywordChangedNoValidate();

                    break;
                case ShaderDropdown dropdown:
                    if (m_Dropdowns.Contains(dropdown))
                        return;

                    if (index < 0)
                        m_Dropdowns.Add(dropdown);
                    else
                        m_Dropdowns.Insert(index, dropdown);

                    OnDropdownChangedNoValidate();

                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }

            m_AddedInputs.Add(input);
        }

        // only ignores names matching ignoreName on properties matching ignoreGuid
        public List<string> BuildPropertyDisplayNameList(AbstractShaderProperty ignoreProperty, string ignoreName)
        {
            List<String> result = new List<String>();
            foreach (var p in properties)
            {
                int before = result.Count;
                p.GetPropertyDisplayNames(result);

                if ((p == ignoreProperty) && (ignoreName != null))
                {
                    // remove ignoreName, if it was just added
                    for (int i = before; i < result.Count; i++)
                    {
                        if (result[i] == ignoreName)
                        {
                            result.RemoveAt(i);
                            break;
                        }
                    }
                }
            }

            return result;
        }

        // only ignores names matching ignoreName on properties matching ignoreGuid
        public List<string> BuildPropertyReferenceNameList(AbstractShaderProperty ignoreProperty, string ignoreName)
        {
            List<String> result = new List<String>();
            foreach (var p in properties)
            {
                int before = result.Count;
                p.GetPropertyReferenceNames(result);

                if ((p == ignoreProperty) && (ignoreName != null))
                {
                    // remove ignoreName, if it was just added
                    for (int i = before; i < result.Count; i++)
                    {
                        if (result[i] == ignoreName)
                        {
                            result.RemoveAt(i);
                            break;
                        }
                    }
                }
            }
            return result;
        }

        public string SanitizeGraphInputName(ShaderInput input, string desiredName)
        {
            string currentName = input.displayName;
            string sanitizedName = desiredName.Trim();
            switch (input)
            {
                case AbstractShaderProperty property:
                    sanitizedName = GraphUtil.SanitizeName(BuildPropertyDisplayNameList(property, currentName), "{0} ({1})", sanitizedName);
                    break;
                case ShaderKeyword keyword:
                    sanitizedName = GraphUtil.SanitizeName(keywords.Where(p => p != input).Select(p => p.displayName), "{0} ({1})", sanitizedName);
                    break;
                case ShaderDropdown dropdown:
                    sanitizedName = GraphUtil.SanitizeName(dropdowns.Where(p => p != input).Select(p => p.displayName), "{0} ({1})", sanitizedName);
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
            return sanitizedName;
        }

        public string SanitizeGraphInputReferenceName(ShaderInput input, string desiredName)
        {
            var sanitizedName = NodeUtils.ConvertToValidHLSLIdentifier(desiredName, (desiredName) => (NodeUtils.IsShaderLabKeyWord(desiredName) || NodeUtils.IsShaderGraphKeyWord(desiredName)));

            switch (input)
            {
                case AbstractShaderProperty property:
                {
                    // must deduplicate ref names against keywords, dropdowns, and properties, as they occupy the same name space
                    var existingNames = properties.Where(p => p != property).Select(p => p.referenceName).Union(keywords.Select(p => p.referenceName)).Union(dropdowns.Select(p => p.referenceName));
                    sanitizedName = GraphUtil.DeduplicateName(existingNames, "{0}_{1}", sanitizedName);
                }
                break;
                case ShaderKeyword keyword:
                {
                    // must deduplicate ref names against keywords, dropdowns, and properties, as they occupy the same name space
                    sanitizedName = sanitizedName.ToUpper();
                    var existingNames = properties.Select(p => p.referenceName).Union(keywords.Where(p => p != input).Select(p => p.referenceName)).Union(dropdowns.Select(p => p.referenceName));
                    sanitizedName = GraphUtil.DeduplicateName(existingNames, "{0}_{1}", sanitizedName);
                }
                break;
                case ShaderDropdown dropdown:
                {
                    // must deduplicate ref names against keywords, dropdowns, and properties, as they occupy the same name space
                    var existingNames = properties.Select(p => p.referenceName).Union(keywords.Select(p => p.referenceName)).Union(dropdowns.Where(p => p != input).Select(p => p.referenceName));
                    sanitizedName = GraphUtil.DeduplicateName(existingNames, "{0}_{1}", sanitizedName);
                }
                break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
            return sanitizedName;
        }

        // copies the ShaderInput, and adds it to the graph with proper name sanitization, returning the copy
        public ShaderInput AddCopyOfShaderInput(ShaderInput source, int insertIndex = -1)
        {
            ShaderInput copy = source.Copy();

            // some ShaderInputs cannot be copied (unknown types)
            if (copy == null)
                return null;

            // copy common properties that should always be copied over
            copy.generatePropertyBlock = source.generatePropertyBlock;      // the exposed toggle

            if ((source is AbstractShaderProperty sourceProp) && (copy is AbstractShaderProperty copyProp))
            {
                copyProp.hidden = sourceProp.hidden;
                copyProp.precision = sourceProp.precision;
                copyProp.overrideHLSLDeclaration = sourceProp.overrideHLSLDeclaration;
                copyProp.hlslDeclarationOverride = sourceProp.hlslDeclarationOverride;
                copyProp.useCustomSlotLabel = sourceProp.useCustomSlotLabel;
            }

            // sanitize the display name (we let the .Copy() function actually copy the display name over)
            copy.SetDisplayNameAndSanitizeForGraph(this);

            // copy and sanitize the reference name (must do this after the display name, so the default is correct)
            if (source.IsUsingNewDefaultRefName())
            {
                // if source was using new default, we can just rely on the default for the copy we made.
                // the code above has already handled collisions properly for the default,
                // and it will assign the same name as the source if there are no collisions.
                // Also it will result better names chosen when there are collisions.
            }
            else
            {
                // when the source is using an old default, we set it as an override
                copy.SetReferenceNameAndSanitizeForGraph(this, source.referenceName);
            }

            copy.OnBeforePasteIntoGraph(this);
            AddGraphInputNoSanitization(copy, insertIndex);

            return copy;
        }

        public void RemoveGraphInput(ShaderInput input)
        {
            switch (input)
            {
                case AbstractShaderProperty property:
                    var propertyNodes = GetNodes<PropertyNode>().Where(x => x.property == input).ToList();
                    foreach (var propertyNode in propertyNodes)
                        ReplacePropertyNodeWithConcreteNodeNoValidate(propertyNode);
                    break;
            }

            // Also remove this input from any category it existed in
            foreach (var categoryData in categories)
            {
                if (categoryData.IsItemInCategory(input))
                {
                    categoryData.RemoveItemFromCategory(input);
                    break;
                }
            }

            RemoveGraphInputNoValidate(input);
            ValidateGraph();
        }

        public void MoveCategory(CategoryData category, int newIndex)
        {
            if (newIndex > m_CategoryData.Count || newIndex < 0)
            {
                AssertHelpers.Fail("New index is not within categories list.");
                return;
            }
            var currentIndex = m_CategoryData.IndexOf(category);
            if (currentIndex == -1)
            {
                AssertHelpers.Fail("Category is not in graph.");
                return;
            }
            if (newIndex == currentIndex)
                return;
            m_CategoryData.RemoveAt(currentIndex);
            if (newIndex > currentIndex)
                newIndex--;
            var isLast = newIndex == m_CategoryData.Count;
            if (isLast)
                m_CategoryData.Add(category);
            else
                m_CategoryData.Insert(newIndex, category);
            if (!m_MovedCategories.Contains(category))
                m_MovedCategories.Add(category);
        }

        public void MoveItemInCategory(ShaderInput itemToMove, int newIndex, string associatedCategoryGuid)
        {
            foreach (var categoryData in categories)
            {
                if (categoryData.categoryGuid == associatedCategoryGuid && categoryData.IsItemInCategory(itemToMove))
                {
                    // Validate new index to move the item to
                    if (newIndex < -1 || newIndex >= categoryData.childCount)
                    {
                        AssertHelpers.Fail("Provided invalid index input to MoveItemInCategory.");
                        return;
                    }

                    categoryData.MoveItemInCategory(itemToMove, newIndex);
                    break;
                }
            }
        }

        public int GetGraphInputIndex(ShaderInput input)
        {
            switch (input)
            {
                case AbstractShaderProperty property:
                    return m_Properties.IndexOf(property);
                case ShaderKeyword keyword:
                    return m_Keywords.IndexOf(keyword);
                case ShaderDropdown dropdown:
                    return m_Dropdowns.IndexOf(dropdown);
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        void RemoveGraphInputNoValidate(ShaderInput shaderInput)
        {
            if (shaderInput is AbstractShaderProperty property && m_Properties.Remove(property) ||
                shaderInput is ShaderKeyword keyword && m_Keywords.Remove(keyword) ||
                shaderInput is ShaderDropdown dropdown && m_Dropdowns.Remove(dropdown))
            {
                m_RemovedInputs.Add(shaderInput);
                m_AddedInputs.Remove(shaderInput);
                m_MovedInputs.Remove(shaderInput);
            }
        }

        static List<IEdge> s_TempEdges = new List<IEdge>();

        public void ReplacePropertyNodeWithConcreteNode(PropertyNode propertyNode)
        {
            ReplacePropertyNodeWithConcreteNodeNoValidate(propertyNode);
            ValidateGraph();
        }

        void ReplacePropertyNodeWithConcreteNodeNoValidate(PropertyNode propertyNode, bool deleteNodeIfNoConcreteFormExists = true)
        {
            var property = properties.FirstOrDefault(x => x == propertyNode.property);
            if (property == null)
                return;

            var node = property.ToConcreteNode() as AbstractMaterialNode;
            if (node == null)   // Some nodes have no concrete form
            {
                if (deleteNodeIfNoConcreteFormExists)
                    RemoveNodeNoValidate(propertyNode);
                return;
            }

            var slot = propertyNode.FindOutputSlot<MaterialSlot>(PropertyNode.OutputSlotId);
            var newSlot = node.GetOutputSlots<MaterialSlot>().FirstOrDefault(s => s.valueType == slot.valueType);
            if (newSlot == null)
                return;

            node.drawState = propertyNode.drawState;
            node.group = propertyNode.group;
            AddNodeNoValidate(node);

            foreach (var edge in this.GetEdges(slot.slotReference))
                ConnectNoValidate(newSlot.slotReference, edge.inputSlot);

            RemoveNodeNoValidate(propertyNode);
        }

        public void AddCategory(CategoryData categoryDataReference)
        {
            m_CategoryData.Add(categoryDataReference);
            m_AddedCategories.Add(categoryDataReference);
        }

        public string FindCategoryForInput(ShaderInput input)
        {
            foreach (var categoryData in categories)
            {
                if (categoryData.IsItemInCategory(input))
                {
                    return categoryData.categoryGuid;
                }
            }

            AssertHelpers.Fail("Attempted to find category for an input that doesn't exist in the graph.");
            return String.Empty;
        }

        public void ChangeCategoryName(string categoryGUID, string newName)
        {
            foreach (var categoryData in categories)
            {
                if (categoryData.categoryGuid == categoryGUID)
                {
                    var sanitizedCategoryName = GraphUtil.SanitizeCategoryName(newName);
                    categoryData.name = sanitizedCategoryName;
                    return;
                }
            }

            AssertHelpers.Fail("Attempted to change name of a category that does not exist in the graph.");
        }

        public void InsertItemIntoCategory(string categoryGUID, ShaderInput itemToAdd, int insertionIndex = -1)
        {
            foreach (var categoryData in categories)
            {
                if (categoryData.categoryGuid == categoryGUID)
                {
                    categoryData.InsertItemIntoCategory(itemToAdd, insertionIndex);
                }
                // Also make sure to remove this items guid from an existing category if it exists within one
                else if (categoryData.IsItemInCategory(itemToAdd))
                {
                    categoryData.RemoveItemFromCategory(itemToAdd);
                }
            }
        }

        public void RemoveItemFromCategory(string categoryGUID, ShaderInput itemToRemove)
        {
            foreach (var categoryData in categories)
            {
                if (categoryData.categoryGuid == categoryGUID)
                {
                    categoryData.RemoveItemFromCategory(itemToRemove);
                    return;
                }
            }

            AssertHelpers.Fail("Attempted to remove item from a category that does not exist in the graph.");
        }

        public void RemoveCategory(string categoryGUID)
        {
            var existingCategory = categories.FirstOrDefault(category => category.categoryGuid == categoryGUID);
            if (existingCategory != null)
            {
                m_CategoryData.Remove(existingCategory);
                m_RemovedCategories.Add(existingCategory);

                // Whenever a category is removed, also remove any inputs within that category
                foreach (var shaderInput in existingCategory.Children)
                    RemoveGraphInput(shaderInput);
            }
            else
                AssertHelpers.Fail("Attempted to remove a category that does not exist in the graph.");
        }

        // This differs from the rest of the category handling functions due to how categories can be copied between graphs
        // Since we have no guarantee of us owning the categories, we need a direct reference to the category to copy
        public CategoryData CopyCategory(CategoryData categoryToCopy)
        {
            var copiedCategory = new CategoryData(categoryToCopy);
            AddCategory(copiedCategory);
            // Whenever a category is copied, also copy over all the inputs within that category
            foreach (var childInputToCopy in categoryToCopy.Children)
            {
                var newShaderInput = AddCopyOfShaderInput(childInputToCopy);
                copiedCategory.InsertItemIntoCategory(newShaderInput);
            }

            return copiedCategory;
        }

        public void OnKeywordChanged()
        {
            OnKeywordChangedNoValidate();
            ValidateGraph();
        }

        public void OnKeywordChangedNoValidate()
        {
            var allNodes = GetNodes<AbstractMaterialNode>();
            foreach (AbstractMaterialNode node in allNodes)
            {
                node.Dirty(ModificationScope.Topological);
                node.ValidateNode();
            }
        }

        public void OnDropdownChanged()
        {
            OnDropdownChangedNoValidate();
            ValidateGraph();
        }

        public void OnDropdownChangedNoValidate()
        {
            var allNodes = GetNodes<AbstractMaterialNode>();
            foreach (AbstractMaterialNode node in allNodes)
            {
                node.Dirty(ModificationScope.Topological);
                node.ValidateNode();
            }
        }

        public void CleanupGraph()
        {
            //First validate edges, remove any
            //orphans. This can happen if a user
            //manually modifies serialized data
            //of if they delete a node in the inspector
            //debug view.
            foreach (var edge in edges.ToArray())
            {
                var outputNode = edge.outputSlot.node;
                var inputNode = edge.inputSlot.node;

                MaterialSlot outputSlot = null;
                MaterialSlot inputSlot = null;
                if (ContainsNode(outputNode) && ContainsNode(inputNode))
                {
                    outputSlot = outputNode.FindOutputSlot<MaterialSlot>(edge.outputSlot.slotId);
                    inputSlot = inputNode.FindInputSlot<MaterialSlot>(edge.inputSlot.slotId);
                }

                if (outputNode == null
                    || inputNode == null
                    || outputSlot == null
                    || inputSlot == null)
                {
                    //orphaned edge
                    RemoveEdgeNoValidate(edge, false);
                }
            }
        }

        public void ValidateGraph()
        {
            messageManager?.ClearAllFromProvider(this);
            CleanupGraph();
            GraphSetup.SetupGraph(this);
            GraphConcretization.ConcretizeGraph(this);
            GraphValidation.ValidateGraph(this);

            foreach (var edge in m_AddedEdges.ToList())
            {
                if (!ContainsNode(edge.outputSlot.node) || !ContainsNode(edge.inputSlot.node))
                {
                    Debug.LogWarningFormat("Added edge is invalid: {0} -> {1}\n{2}", edge.outputSlot.node.objectId, edge.inputSlot.node.objectId, Environment.StackTrace);
                    m_AddedEdges.Remove(edge);
                }
            }

            foreach (var groupChange in m_ParentGroupChanges.ToList())
            {
                if (groupChange.groupItem is AbstractMaterialNode node && !ContainsNode(node))
                {
                    m_ParentGroupChanges.Remove(groupChange);
                }

                if (groupChange.groupItem is StickyNoteData stickyNote && !m_StickyNoteDatas.Contains(stickyNote))
                {
                    m_ParentGroupChanges.Remove(groupChange);
                }
            }

            var existingDefaultCategory = categories.FirstOrDefault();
            if (existingDefaultCategory?.childCount == 0 && categories.Count() == 1 && (properties.Count() != 0 || keywords.Count() != 0 || dropdowns.Count() != 0))
            {
                // Have a graph with category data in invalid state
                // there is only one category, the default category, and all shader inputs should belong to it
                // Clear category data as it will get reconstructed in the BlackboardController constructor
                m_CategoryData.Clear();
            }


            ValidateCustomBlockLimit();
            ValidateContextBlocks();
        }

        public void AddValidationError(string id, string errorMessage,
            ShaderCompilerMessageSeverity severity = ShaderCompilerMessageSeverity.Error)
        {
            messageManager?.AddOrAppendError(this, id, new ShaderMessage("Validation: " + errorMessage, severity));
        }

        public void AddSetupError(string id, string errorMessage,
            ShaderCompilerMessageSeverity severity = ShaderCompilerMessageSeverity.Error)
        {
            messageManager?.AddOrAppendError(this, id, new ShaderMessage("Setup: " + errorMessage, severity));
        }

        public void AddConcretizationError(string id, string errorMessage,
            ShaderCompilerMessageSeverity severity = ShaderCompilerMessageSeverity.Error)
        {
            messageManager?.AddOrAppendError(this, id, new ShaderMessage("Concretization: " + errorMessage, severity));
        }

        public void ClearErrorsForNode(AbstractMaterialNode node)
        {
            messageManager?.ClearNodesFromProvider(this, node.ToEnumerable());
        }

        public void ReplaceWith(GraphData other)
        {
            if (other == null)
                throw new ArgumentException("Can only replace with another AbstractMaterialGraph", "other");

            m_GraphPrecision = other.m_GraphPrecision;
            m_PreviewMode = other.m_PreviewMode;
            m_OutputNode = other.m_OutputNode;

            if ((this.vertexContext.position != other.vertexContext.position) ||
                (this.fragmentContext.position != other.fragmentContext.position))
            {
                this.vertexContext.position = other.vertexContext.position;
                this.fragmentContext.position = other.fragmentContext.position;
                m_MovedContexts = true;
            }

            using (var inputsToRemove = PooledList<ShaderInput>.Get())
            {
                foreach (var property in m_Properties.SelectValue())
                    inputsToRemove.Add(property);
                foreach (var keyword in m_Keywords.SelectValue())
                    inputsToRemove.Add(keyword);
                foreach (var dropdown in m_Dropdowns.SelectValue())
                    inputsToRemove.Add(dropdown);
                foreach (var input in inputsToRemove)
                    RemoveGraphInputNoValidate(input);
            }
            foreach (var otherProperty in other.properties)
            {
                AddGraphInputNoSanitization(otherProperty);
            }
            foreach (var otherKeyword in other.keywords)
            {
                AddGraphInputNoSanitization(otherKeyword);
            }
            foreach (var otherDropdown in other.dropdowns)
            {
                AddGraphInputNoSanitization(otherDropdown);
            }

            other.ValidateGraph();
            ValidateGraph();

            // Current tactic is to remove all nodes and edges and then re-add them, such that depending systems
            // will re-initialize with new references.

            using (ListPool<GroupData>.Get(out var removedGroupDatas))
            {
                removedGroupDatas.AddRange(m_GroupDatas.SelectValue());
                foreach (var groupData in removedGroupDatas)
                {
                    RemoveGroupNoValidate(groupData);
                }
            }

            using (ListPool<StickyNoteData>.Get(out var removedNoteDatas))
            {
                removedNoteDatas.AddRange(m_StickyNoteDatas.SelectValue());
                foreach (var groupData in removedNoteDatas)
                {
                    RemoveNoteNoValidate(groupData);
                }
            }

            using (var pooledList = ListPool<IEdge>.Get(out var removedNodeEdges))
            {
                removedNodeEdges.AddRange(m_Edges);
                foreach (var edge in removedNodeEdges)
                    RemoveEdgeNoValidate(edge);
            }

            using (var nodesToRemove = PooledList<AbstractMaterialNode>.Get())
            {
                nodesToRemove.AddRange(m_Nodes.SelectValue());
                foreach (var node in nodesToRemove)
                    RemoveNodeNoValidate(node);
            }

            // Clear category data too before re-adding
            m_CategoryData.Clear();

            ValidateGraph();

            foreach (GroupData groupData in other.groups)
                AddGroup(groupData);

            // If categories are ever removed completely, make sure there is always one default category that exists
            if (!other.categories.Any())
            {
                AddCategory(CategoryData.DefaultCategory());
            }
            else
            {
                foreach (CategoryData categoryData in other.categories)
                {
                    AddCategory(categoryData);
                }
            }


            foreach (var stickyNote in other.stickyNotes)
            {
                AddStickyNote(stickyNote);
            }

            foreach (var node in other.GetNodes<AbstractMaterialNode>())
            {
                if (node is BlockNode blockNode)
                {
                    var contextData = blockNode.descriptor.shaderStage == ShaderStage.Vertex ? vertexContext : fragmentContext;
                    AddBlockNoValidate(blockNode, contextData, blockNode.index);
                }
                else
                {
                    AddNodeNoValidate(node);
                }
            }

            foreach (var edge in other.edges)
            {
                ConnectNoValidate(edge.outputSlot, edge.inputSlot);
            }

            outputNode = other.outputNode;

            // clear our local active targets and copy state from the other GraphData

            // NOTE:  we DO NOT clear or rebuild m_AllPotentialTargets, in order to
            // retain the data from any inactive targets.
            // this allows the user can add them back and keep the old settings

            m_ActiveTargets.Clear();
            foreach (var target in other.activeTargets)
            {
                // Ensure target inits correctly
                var context = new TargetSetupContext();
                target.Setup(ref context);
                SetTargetActive(target, true);
            }
            SortActiveTargets();

            // Active blocks
            var activeBlocks = GetActiveBlocksForAllActiveTargets();
            UpdateActiveBlocks(activeBlocks);
            ValidateGraph();
        }

        internal void PasteGraph(CopyPasteGraph graphToPaste, List<AbstractMaterialNode> remappedNodes,
            List<Edge> remappedEdges)
        {
            var groupMap = new Dictionary<GroupData, GroupData>();
            foreach (var group in graphToPaste.groups)
            {
                var position = group.position;
                position.x += 30;
                position.y += 30;

                GroupData newGroup = new GroupData(group.title, position);

                groupMap[group] = newGroup;

                AddGroup(newGroup);
                m_PastedGroups.Add(newGroup);
            }

            foreach (var stickyNote in graphToPaste.stickyNotes)
            {
                var position = stickyNote.position;
                position.x += 30;
                position.y += 30;

                StickyNoteData pastedStickyNote = new StickyNoteData(stickyNote.title, stickyNote.content, position);
                pastedStickyNote.textSize = stickyNote.textSize;
                pastedStickyNote.theme = stickyNote.theme;
                if (stickyNote.group != null && groupMap.ContainsKey(stickyNote.group))
                {
                    pastedStickyNote.group = groupMap[stickyNote.group];
                }

                AddStickyNote(pastedStickyNote);
                m_PastedStickyNotes.Add(pastedStickyNote);
            }

            var edges = graphToPaste.edges.ToList();
            var nodeList = graphToPaste.GetNodes<AbstractMaterialNode>();
            foreach (var node in nodeList)
            {
                // cannot paste block nodes, or unknown node types
                if ((node is BlockNode) || (node is MultiJsonInternal.UnknownNodeType))
                    continue;

                if (!IsInputAllowedInGraph(node))
                    continue;

                AbstractMaterialNode pastedNode = node;

                // Check if the property nodes need to be made into a concrete node.
                if (node is PropertyNode propertyNode)
                {
                    // If the property is not in the current graph, do check if the
                    // property can be made into a concrete node.
                    var property = m_Properties.SelectValue().FirstOrDefault(x => x.objectId == propertyNode.property.objectId
                        || (x.propertyType == propertyNode.property.propertyType && x.referenceName == propertyNode.property.referenceName));
                    if (property != null)
                    {
                        propertyNode.property = property;
                    }
                    else
                    {
                        pastedNode = propertyNode.property.ToConcreteNode();
                        // some property nodes cannot be concretized..  fail to paste them
                        if (pastedNode == null)
                            continue;
                        pastedNode.drawState = node.drawState;
                        for (var i = 0; i < edges.Count; i++)
                        {
                            var edge = edges[i];
                            if (edge.outputSlot.node == node)
                            {
                                edges[i] = new Edge(new SlotReference(pastedNode, edge.outputSlot.slotId), edge.inputSlot);
                            }
                            else if (edge.inputSlot.node == node)
                            {
                                edges[i] = new Edge(edge.outputSlot, new SlotReference(pastedNode, edge.inputSlot.slotId));
                            }
                        }
                    }
                }

                // If the node has a group guid and no group has been copied, reset the group guid.
                // Check if the node is inside a group
                if (node.group != null)
                {
                    if (groupMap.ContainsKey(node.group))
                    {
                        var absNode = pastedNode;
                        absNode.group = groupMap[node.group];
                        pastedNode = absNode;
                    }
                    else
                    {
                        pastedNode.group = null;
                    }
                }

                remappedNodes.Add(pastedNode);
                AddNode(pastedNode);

                // add the node to the pasted node list
                m_PastedNodes.Add(pastedNode);

                // Check if the keyword nodes need to have their keywords copied.
                if (node is KeywordNode keywordNode)
                {
                    var keyword = m_Keywords.SelectValue().FirstOrDefault(x => x.objectId == keywordNode.keyword.objectId
                        || (x.keywordType == keywordNode.keyword.keywordType && x.referenceName == keywordNode.keyword.referenceName));
                    if (keyword != null)
                    {
                        keywordNode.keyword = keyword;
                    }
                    else
                    {
                        owner.graphDataStore.Dispatch(new AddShaderInputAction() { shaderInputReference = keywordNode.keyword });
                    }

                    // Always update Keyword nodes to handle any collisions resolved on the Keyword
                    keywordNode.UpdateNode();
                }

                // Check if the dropdown nodes need to have their dropdowns copied.
                if (node is DropdownNode dropdownNode)
                {
                    var dropdown = m_Dropdowns.SelectValue().FirstOrDefault(x => x.objectId == dropdownNode.dropdown.objectId
                        || x.referenceName == dropdownNode.dropdown.referenceName);
                    if (dropdown != null)
                    {
                        dropdownNode.dropdown = dropdown;
                    }
                    else
                    {
                        owner.graphDataStore.Dispatch(new AddShaderInputAction() { shaderInputReference = dropdownNode.dropdown });
                    }

                    // Always update Dropdown nodes to handle any collisions resolved on the Keyword
                    dropdownNode.UpdateNode();
                }
            }

            foreach (var edge in edges)
            {
                var newEdge = (Edge)Connect(edge.outputSlot, edge.inputSlot);
                if (newEdge != null)
                {
                    remappedEdges.Add(newEdge);
                }
            }

            ValidateGraph();
        }

        public override void OnBeforeSerialize()
        {
            m_Edges.Sort();
            ChangeVersion(latestVersion);
        }

        static T DeserializeLegacy<T>(string typeString, string json, Guid? overrideObjectId = null) where T : JsonObject
        {
            var jsonObj = MultiJsonInternal.CreateInstanceForDeserialization(typeString);
            var value = jsonObj as T;
            if (value == null)
            {
                Debug.Log($"Cannot create instance for {typeString}");
                return null;
            }

            // by default, MultiJsonInternal.CreateInstance will create a new objectID randomly..
            // we need some created objects to have deterministic objectIDs, because they affect the generated shader.
            // if the generated shader is not deterministic, it can create ripple effects (i.e. causing Materials to be modified randomly as properties are renamed)
            // so we provide this path to allow the calling code to override the objectID with something deterministic
            if (overrideObjectId.HasValue)
                value.OverrideObjectId(overrideObjectId.Value.ToString("N"));
            MultiJsonInternal.Enqueue(value, json);
            return value as T;
        }

        static AbstractMaterialNode DeserializeLegacyNode(string typeString, string json, Guid? overrideObjectId = null)
        {
            var jsonObj = MultiJsonInternal.CreateInstanceForDeserialization(typeString);
            var value = jsonObj as AbstractMaterialNode;
            if (value == null)
            {
                //Special case - want to support nodes of unknwon type for cross pipeline compatability
                value = new LegacyUnknownTypeNode(typeString, json);
                if (overrideObjectId.HasValue)
                    value.OverrideObjectId(overrideObjectId.Value.ToString("N"));
                MultiJsonInternal.Enqueue(value, json);
                return value as AbstractMaterialNode;
            }
            else
            {
                if (overrideObjectId.HasValue)
                    value.OverrideObjectId(overrideObjectId.Value.ToString("N"));
                MultiJsonInternal.Enqueue(value, json);
                return value as AbstractMaterialNode;
            }
        }

        public override void OnAfterDeserialize(string json)
        {
            if (sgVersion == 0)
            {
                var graphData0 = JsonUtility.FromJson<GraphData0>(json);
                //If a graph was previously updated to V2, since we had to rename m_Version to m_SGVersion to avoid collision with an upgrade system from
                //HDRP, we have to handle the case that our version might not be correct -
                if (graphData0.m_Version > 0)
                {
                    sgVersion = graphData0.m_Version;
                }
                else
                {
                    // graphData.m_Version == 0  (matches current sgVersion)
                    Guid assetGuid;
                    if (!Guid.TryParse(this.assetGuid, out assetGuid))
                        assetGuid = JsonObject.GenerateNamespaceUUID(Guid.Empty, json);

                    var nodeGuidMap = new Dictionary<string, AbstractMaterialNode>();
                    var propertyGuidMap = new Dictionary<string, AbstractShaderProperty>();
                    var keywordGuidMap = new Dictionary<string, ShaderKeyword>();
                    var groupGuidMap = new Dictionary<string, GroupData>();
                    var slotsField = typeof(AbstractMaterialNode).GetField("m_Slots", BindingFlags.Instance | BindingFlags.NonPublic);
                    var propertyField = typeof(PropertyNode).GetField("m_Property", BindingFlags.Instance | BindingFlags.NonPublic);
                    var keywordField = typeof(KeywordNode).GetField("m_Keyword", BindingFlags.Instance | BindingFlags.NonPublic);
                    var dropdownField = typeof(DropdownNode).GetField("m_Dropdown", BindingFlags.Instance | BindingFlags.NonPublic);
                    var defaultReferenceNameField = typeof(ShaderInput).GetField("m_DefaultReferenceName", BindingFlags.Instance | BindingFlags.NonPublic);

                    m_GroupDatas.Clear();
                    m_StickyNoteDatas.Clear();

                    foreach (var group0 in graphData0.m_Groups)
                    {
                        var group = new GroupData(group0.m_Title, group0.m_Position);
                        m_GroupDatas.Add(group);
                        if (!groupGuidMap.ContainsKey(group0.m_GuidSerialized))
                        {
                            groupGuidMap.Add(group0.m_GuidSerialized, group);
                        }
                        else if (!groupGuidMap[group0.m_GuidSerialized].Equals(group.objectId))
                        {
                            Debug.LogError("Group id mismatch");
                        }
                    }

                    foreach (var serializedProperty in graphData0.m_SerializedProperties)
                    {
                        var propObjectId = JsonObject.GenerateNamespaceUUID(assetGuid, serializedProperty.JSONnodeData);
                        var property = DeserializeLegacy<AbstractShaderProperty>(serializedProperty.typeInfo.fullName, serializedProperty.JSONnodeData, propObjectId);
                        if (property == null)
                            continue;

                        m_Properties.Add(property);

                        var input0 = JsonUtility.FromJson<ShaderInput0>(serializedProperty.JSONnodeData);
                        propertyGuidMap[input0.m_Guid.m_GuidSerialized] = property;

                        // Fix up missing reference names
                        // Properties on Sub Graphs in V0 never have reference names serialized
                        // To maintain Sub Graph node property mapping we force guid based reference names on upgrade
                        if (string.IsNullOrEmpty((string)defaultReferenceNameField.GetValue(property)))
                        {
                            // ColorShaderProperty is the only Property case where `GetDefaultReferenceName` was overriden
                            if (MultiJson.ParseType(serializedProperty.typeInfo.fullName) == typeof(ColorShaderProperty))
                            {
                                defaultReferenceNameField.SetValue(property, $"Color_{GuidEncoder.Encode(Guid.Parse(input0.m_Guid.m_GuidSerialized))}");
                            }
                            else
                            {
                                defaultReferenceNameField.SetValue(property, $"{property.concreteShaderValueType}_{GuidEncoder.Encode(Guid.Parse(input0.m_Guid.m_GuidSerialized))}");
                            }
                        }
                    }

                    foreach (var serializedKeyword in graphData0.m_SerializedKeywords)
                    {
                        var keyword = DeserializeLegacy<ShaderKeyword>(serializedKeyword.typeInfo.fullName, serializedKeyword.JSONnodeData);
                        if (keyword == null)
                        {
                            continue;
                        }

                        m_Keywords.Add(keyword);

                        var input0 = JsonUtility.FromJson<ShaderInput0>(serializedKeyword.JSONnodeData);
                        keywordGuidMap[input0.m_Guid.m_GuidSerialized] = keyword;
                    }

                    foreach (var serializedNode in graphData0.m_SerializableNodes)
                    {
                        var node0 = JsonUtility.FromJson<AbstractMaterialNode0>(serializedNode.JSONnodeData);

                        var nodeObjectId = JsonObject.GenerateNamespaceUUID(node0.m_GuidSerialized, "node");
                        var node = DeserializeLegacyNode(serializedNode.typeInfo.fullName, serializedNode.JSONnodeData, nodeObjectId);
                        if (node == null)
                        {
                            continue;
                        }

                        nodeGuidMap.Add(node0.m_GuidSerialized, node);
                        m_Nodes.Add(node);

                        if (!string.IsNullOrEmpty(node0.m_PropertyGuidSerialized) && propertyGuidMap.TryGetValue(node0.m_PropertyGuidSerialized, out var property))
                        {
                            propertyField.SetValue(node, (JsonRef<AbstractShaderProperty>)property);
                        }

                        if (!string.IsNullOrEmpty(node0.m_KeywordGuidSerialized) && keywordGuidMap.TryGetValue(node0.m_KeywordGuidSerialized, out var keyword))
                        {
                            keywordField.SetValue(node, (JsonRef<ShaderKeyword>)keyword);
                        }

                        var slots = (List<JsonData<MaterialSlot>>)slotsField.GetValue(node);
                        slots.Clear();

                        foreach (var serializedSlot in node0.m_SerializableSlots)
                        {
                            var slotObjectId = JsonObject.GenerateNamespaceUUID(node0.m_GuidSerialized, serializedSlot.JSONnodeData);
                            var slot = DeserializeLegacy<MaterialSlot>(serializedSlot.typeInfo.fullName, serializedSlot.JSONnodeData, slotObjectId);
                            if (slot == null)
                            {
                                continue;
                            }

                            slots.Add(slot);
                        }

                        if (!String.IsNullOrEmpty(node0.m_GroupGuidSerialized))
                        {
                            if (groupGuidMap.TryGetValue(node0.m_GroupGuidSerialized, out GroupData foundGroup))
                            {
                                node.group = foundGroup;
                            }
                        }
                    }

                    foreach (var stickyNote0 in graphData0.m_StickyNotes)
                    {
                        var stickyNote = new StickyNoteData(stickyNote0.m_Title, stickyNote0.m_Content, stickyNote0.m_Position);
                        if (!String.IsNullOrEmpty(stickyNote0.m_GroupGuidSerialized))
                        {
                            if (groupGuidMap.TryGetValue(stickyNote0.m_GroupGuidSerialized, out GroupData foundGroup))
                            {
                                stickyNote.group = foundGroup;
                            }
                        }
                        stickyNote.theme = stickyNote0.m_Theme;
                        stickyNote.textSize = stickyNote0.m_TextSize;
                        m_StickyNoteDatas.Add(stickyNote);
                    }

                    var subgraphOuput = GetNodes<SubGraphOutputNode>();
                    isSubGraph = subgraphOuput.Any();

                    if (isSubGraph)
                    {
                        m_OutputNode = subgraphOuput.FirstOrDefault();
                    }
                    else if (!string.IsNullOrEmpty(graphData0.m_ActiveOutputNodeGuidSerialized))
                    {
                        m_OutputNode = nodeGuidMap[graphData0.m_ActiveOutputNodeGuidSerialized];
                    }
                    else
                    {
                        m_OutputNode = (AbstractMaterialNode)GetNodes<IMasterNode1>().FirstOrDefault();
                    }

                    foreach (var serializedElement in graphData0.m_SerializableEdges)
                    {
                        var edge0 = JsonUtility.FromJson<Edge0>(serializedElement.JSONnodeData);
                        m_Edges.Add(new Edge(
                            new SlotReference(
                                nodeGuidMap[edge0.m_OutputSlot.m_NodeGUIDSerialized],
                                edge0.m_OutputSlot.m_SlotId),
                            new SlotReference(
                                nodeGuidMap[edge0.m_InputSlot.m_NodeGUIDSerialized],
                                edge0.m_InputSlot.m_SlotId)));
                    }
                }
            }
        }

        [Serializable]
        class OldGraphDataReadConcretePrecision
        {
            // old value just for upgrade
            [SerializeField]
            public ConcretePrecision m_ConcretePrecision = ConcretePrecision.Single;
        };

        public override void OnAfterMultiDeserialize(string json)
        {
            // Deferred upgrades
            if (sgVersion != latestVersion)
            {
                if (sgVersion < 2)
                {
                    var addedBlocks = ListPool<BlockFieldDescriptor>.Get();

                    void UpgradeFromBlockMap(Dictionary<BlockFieldDescriptor, int> blockMap)
                    {
                        // Map master node ports to blocks
                        if (blockMap != null)
                        {
                            foreach (var blockMapping in blockMap)
                            {
                                // Create a new BlockNode for each unique map entry
                                var descriptor = blockMapping.Key;
                                if (addedBlocks.Contains(descriptor))
                                    continue;

                                addedBlocks.Add(descriptor);

                                var contextData = descriptor.shaderStage == ShaderStage.Fragment ? m_FragmentContext : m_VertexContext;
                                var block = (BlockNode)Activator.CreateInstance(typeof(BlockNode));
                                block.Init(descriptor);
                                AddBlockNoValidate(block, contextData, contextData.blocks.Count);

                                // To avoid having to go around the following deserialization code
                                // We simply run OnBeforeSerialization here to ensure m_SerializedDescriptor is set
                                block.OnBeforeSerialize();

                                // Now remap the incoming edges to blocks
                                var slotId = blockMapping.Value;
                                var oldSlot = m_OutputNode.value.FindSlot<MaterialSlot>(slotId);
                                var newSlot = block.FindSlot<MaterialSlot>(0);
                                if (oldSlot == null)
                                    continue;

                                var oldInputSlotRef = m_OutputNode.value.GetSlotReference(slotId);
                                var newInputSlotRef = block.GetSlotReference(0);

                                // Always copy the value over for convenience
                                newSlot.CopyValuesFrom(oldSlot);

                                for (int i = 0; i < m_Edges.Count; i++)
                                {
                                    // Find all edges connected to the master node using slot ID from the block map
                                    // Remove them and replace them with new edges connected to the block nodes
                                    var edge = m_Edges[i];
                                    if (edge.inputSlot.Equals(oldInputSlotRef))
                                    {
                                        var outputSlot = edge.outputSlot;
                                        m_Edges.Remove(edge);
                                        m_Edges.Add(new Edge(outputSlot, newInputSlotRef));
                                    }
                                }

                                // manually handle a bug where fragment normal slots could get out of sync of the master node's set fragment normal space
                                if (descriptor == BlockFields.SurfaceDescription.NormalOS)
                                {
                                    NormalMaterialSlot norm = newSlot as NormalMaterialSlot;
                                    if (norm.space != CoordinateSpace.Object)
                                    {
                                        norm.space = CoordinateSpace.Object;
                                    }
                                }
                                else if (descriptor == BlockFields.SurfaceDescription.NormalTS)
                                {
                                    NormalMaterialSlot norm = newSlot as NormalMaterialSlot;
                                    if (norm.space != CoordinateSpace.Tangent)
                                    {
                                        norm.space = CoordinateSpace.Tangent;
                                    }
                                }
                                else if (descriptor == BlockFields.SurfaceDescription.NormalWS)
                                {
                                    NormalMaterialSlot norm = newSlot as NormalMaterialSlot;
                                    if (norm.space != CoordinateSpace.World)
                                    {
                                        norm.space = CoordinateSpace.World;
                                    }
                                }
                            }

                            // We need to call AddBlockNoValidate but this adds to m_AddedNodes resulting in duplicates
                            // Therefore we need to clear this list before the view is created
                            m_AddedNodes.Clear();
                        }
                    }

                    var masterNode = m_OutputNode.value as IMasterNode1;

                    // This is required for edge lookup during Target upgrade
                    if (m_OutputNode.value != null)
                    {
                        m_OutputNode.value.owner = this;
                    }
                    foreach (var edge in m_Edges)
                    {
                        AddEdgeToNodeEdges(edge);
                    }

                    // Ensure correct initialization of Contexts
                    AddContexts();

                    // Position Contexts to the match master node
                    var oldPosition = Vector2.zero;
                    if (m_OutputNode.value != null)
                    {
                        oldPosition = m_OutputNode.value.drawState.position.position;
                    }
                    m_VertexContext.position = oldPosition;
                    m_FragmentContext.position = new Vector2(oldPosition.x, oldPosition.y + 200);

                    // Try to upgrade all potential targets from master node
                    if (masterNode != null)
                    {
                        foreach (var potentialTarget in m_AllPotentialTargets)
                        {
                            if (potentialTarget.IsUnknown())
                                continue;

                            var target = potentialTarget.GetTarget();
                            if (!(target is ILegacyTarget legacyTarget))
                                continue;

                            if (!legacyTarget.TryUpgradeFromMasterNode(masterNode, out var newBlockMap))
                                continue;

                            // upgrade succeeded!  Activate it
                            SetTargetActive(target, true);
                            UpgradeFromBlockMap(newBlockMap);
                        }
                        SortActiveTargets();
                    }

                    // Clean up after upgrade
                    if (!isSubGraph)
                    {
                        m_OutputNode = null;
                    }

                    var masterNodes = GetNodes<IMasterNode1>().ToArray();
                    for (int i = 0; i < masterNodes.Length; i++)
                    {
                        var node = masterNodes.ElementAt(i) as AbstractMaterialNode;
                        m_Nodes.Remove(node);
                    }

                    m_NodeEdges.Clear();
                }

                if (sgVersion < 3)
                {
                    var oldGraph = JsonUtility.FromJson<OldGraphDataReadConcretePrecision>(json);

                    // upgrade concrete precision to the new graph precision
                    switch (oldGraph.m_ConcretePrecision)
                    {
                        case ConcretePrecision.Half:
                            m_GraphPrecision = GraphPrecision.Half;
                            break;
                        case ConcretePrecision.Single:
                            m_GraphPrecision = GraphPrecision.Single;
                            break;
                    }
                }

                ChangeVersion(latestVersion);
            }

            PooledList<(LegacyUnknownTypeNode, AbstractMaterialNode)> updatedNodes = PooledList<(LegacyUnknownTypeNode, AbstractMaterialNode)>.Get();
            foreach (var node in m_Nodes.SelectValue())
            {
                if (node is LegacyUnknownTypeNode lNode && lNode.foundType != null)
                {
                    AbstractMaterialNode legacyNode = (AbstractMaterialNode)Activator.CreateInstance(lNode.foundType);
                    JsonUtility.FromJsonOverwrite(lNode.serializedData, legacyNode);
                    legacyNode.group = lNode.group;
                    updatedNodes.Add((lNode, legacyNode));
                }
            }
            foreach (var nodePair in updatedNodes)
            {
                m_Nodes.Add(nodePair.Item2);
                ReplaceNodeWithNode(nodePair.Item1, nodePair.Item2);
            }
            updatedNodes.Dispose();

            m_NodeDictionary = new Dictionary<string, AbstractMaterialNode>(m_Nodes.Count);

            foreach (var group in m_GroupDatas.SelectValue())
            {
                m_GroupItems.Add(group, new List<IGroupItem>());
            }

            foreach (var node in m_Nodes.SelectValue())
            {
                node.owner = this;
                node.UpdateNodeAfterDeserialization();
                node.SetupSlots();
                m_NodeDictionary.Add(node.objectId, node);
                if (m_GroupItems.TryGetValue(node.group, out var groupItems))
                {
                    groupItems.Add(node);
                }
                else
                {
                    node.group = null;
                }
            }

            foreach (var stickyNote in m_StickyNoteDatas.SelectValue())
            {
                if (m_GroupItems.TryGetValue(stickyNote.group, out var groupItems))
                {
                    groupItems.Add(stickyNote);
                }
                else
                {
                    stickyNote.group = null;
                }
            }

            foreach (var edge in m_Edges)
                AddEdgeToNodeEdges(edge);

            // --------------------------------------------------
            // Deserialize Contexts & Blocks

            void DeserializeContextData(ContextData contextData, ShaderStage stage)
            {
                // Because Vertex/Fragment Contexts are serialized explicitly
                // we do not need to serialize the Stage value on the ContextData
                contextData.shaderStage = stage;

                var blocks = contextData.blocks.SelectValue().ToList();
                var blockCount = blocks.Count;
                for (int i = 0; i < blockCount; i++)
                {
                    // Update NonSerialized data on the BlockNode
                    var block = blocks[i];
                    // custom interpolators fully regenerate their own descriptor on deserialization
                    if (!block.isCustomBlock)
                    {
                        block.descriptor = m_BlockFieldDescriptors.FirstOrDefault(x => $"{x.tag}.{x.name}" == block.serializedDescriptor);
                    }
                    if (block.descriptor == null)
                    {
                        //Hit a descriptor that was not recognized from the assembly (likely from a different SRP)
                        //create a new entry for it and continue on
                        if (string.IsNullOrEmpty(block.serializedDescriptor))
                        {
                            throw new Exception($"Block {block} had no serialized descriptor");
                        }

                        var tmp = block.serializedDescriptor.Split('.');
                        if (tmp.Length != 2)
                        {
                            throw new Exception($"Block {block}'s serialized descriptor {block.serializedDescriptor} did not match expected format {{x.tag}}.{{x.name}}");
                        }
                        //right thing to do?
                        block.descriptor = new BlockFieldDescriptor(tmp[0], tmp[1], null, null, stage, true, true);
                        m_BlockFieldDescriptors.Add(block.descriptor);
                    }
                    block.contextData = contextData;
                }
            }

            // First deserialize the ContextDatas
            DeserializeContextData(m_VertexContext, ShaderStage.Vertex);
            DeserializeContextData(m_FragmentContext, ShaderStage.Fragment);

            // there should be no unknown potential targets at this point
            Assert.IsFalse(m_AllPotentialTargets.Any(pt => pt.IsUnknown()));

            foreach (var target in m_ActiveTargets.SelectValue())
            {
                var targetType = target.GetType();
                if (targetType == typeof(MultiJsonInternal.UnknownTargetType))
                {
                    // register any active UnknownTargetType as a potential target
                    m_AllPotentialTargets.Add(new PotentialTarget(target));
                }
                else
                {
                    // active known targets should replace the stored Target in AllPotentialTargets
                    int targetIndex = m_AllPotentialTargets.FindIndex(pt => pt.knownType == targetType);
                    m_AllPotentialTargets[targetIndex].ReplaceStoredTarget(target);
                }
            }

            SortActiveTargets();
        }

        private void ReplaceNodeWithNode(LegacyUnknownTypeNode nodeToReplace, AbstractMaterialNode nodeReplacement)
        {
            var oldSlots = new List<MaterialSlot>();
            nodeToReplace.GetSlots(oldSlots);
            var newSlots = new List<MaterialSlot>();
            nodeReplacement.GetSlots(newSlots);

            for (int i = 0; i < oldSlots.Count; i++)
            {
                newSlots[i].CopyValuesFrom(oldSlots[i]);
                var oldSlotRef = nodeToReplace.GetSlotReference(oldSlots[i].id);
                var newSlotRef = nodeReplacement.GetSlotReference(newSlots[i].id);

                for (int x = 0; x < m_Edges.Count; x++)
                {
                    var edge = m_Edges[x];
                    if (edge.inputSlot.Equals(oldSlotRef))
                    {
                        var outputSlot = edge.outputSlot;
                        m_Edges.Remove(edge);
                        m_Edges.Add(new Edge(outputSlot, newSlotRef));
                    }
                    else if (edge.outputSlot.Equals(oldSlotRef))
                    {
                        var inputSlot = edge.inputSlot;
                        m_Edges.Remove(edge);
                        m_Edges.Add(new Edge(newSlotRef, inputSlot));
                    }
                }
            }
        }

        public void OnEnable()
        {
            foreach (var node in GetNodes<AbstractMaterialNode>().OfType<IOnAssetEnabled>())
            {
                node.OnEnable();
            }

            ShaderGraphPreferences.onVariantLimitChanged += OnKeywordChanged;
        }

        public void OnDisable()
        {
            ShaderGraphPreferences.onVariantLimitChanged -= OnKeywordChanged;
        }

        internal void ValidateCustomBlockLimit()
        {
            if (m_ActiveTargets.Count() == 0)
                return;

            int nonCustomUsage = 0;
            foreach (var bnode in vertexContext.blocks.Where(jb => !jb.value.isCustomBlock).Select(b => b.value))
            {
                if (bnode == null || bnode.descriptor == null)
                    continue;

                if (bnode.descriptor.HasPreprocessor() || bnode.descriptor.HasSemantic() || bnode.descriptor.vectorCount == 0) // not packable.
                    nonCustomUsage += 4;
                else nonCustomUsage += bnode.descriptor.vectorCount;
            }
            int maxTargetUsage = m_ActiveTargets.Select(jt => jt.value.padCustomInterpolatorLimit).Max() * 4;

            int padding = nonCustomUsage + maxTargetUsage;

            int errRange = ShaderGraphProjectSettings.instance.customInterpolatorErrorThreshold;
            int warnRange = ShaderGraphProjectSettings.instance.customInterpolatorWarningThreshold;

            int errorLevel = errRange * 4 - padding;
            int warnLevel = warnRange * 4 - padding;

            int total = 0;

            // warn based on the interpolator's location in the block list.
            foreach (var cib in vertexContext.blocks.Where(jb => jb.value.isCustomBlock).Select(b => b.value))
            {
                ClearErrorsForNode(cib);
                total += (int)cib.customWidth;
                if (total > errorLevel)
                {
                    AddValidationError(cib.objectId, $"{cib.customName} exceeds the interpolation channel error threshold: {errRange}. See ShaderGraph project settings.");
                }
                else if (total > warnLevel)
                {
                    AddValidationError(cib.objectId, $"{cib.customName} exceeds the interpolation channel warning threshold: {warnRange}. See ShaderGraph project settings.", ShaderCompilerMessageSeverity.Warning);
                }
            }
        }

        void ValidateContextBlocks()
        {
            void ValidateContext(ContextData contextData, ShaderStage expectedShaderStage)
            {
                if (contextData == null)
                    return;

                foreach (var block in contextData.blocks)
                {
                    var slots = block.value.GetInputSlots<MaterialSlot>();
                    foreach (var slot in slots)
                        FindAndReportSlotErrors(slot, expectedShaderStage);
                }
            };

            ValidateContext(vertexContext, ShaderStage.Vertex);
            ValidateContext(fragmentContext, ShaderStage.Fragment);
        }

        void FindAndReportSlotErrors(MaterialSlot initialSlot, ShaderStage expectedShaderStage)
        {
            var expectedCapability = expectedShaderStage.GetShaderStageCapability();
            var errorSourceSlots = new HashSet<MaterialSlot>();
            var visitedNodes = new HashSet<AbstractMaterialNode>();

            var graph = initialSlot.owner.owner;
            var slotStack = new Stack<MaterialSlot>();
            slotStack.Clear();
            slotStack.Push(initialSlot);

            // Trace back and find any edges that introduce an error
            while (slotStack.Any())
            {
                var slot = slotStack.Pop();

                // If the slot is an input, jump across the connected edge to the output it's connected to
                if (slot.isInputSlot)
                {
                    foreach (var edge in graph.GetEdges(slot.slotReference))
                    {
                        var node = edge.outputSlot.node;

                        var outputSlot = node.FindOutputSlot<MaterialSlot>(edge.outputSlot.slotId);
                        // If the output slot this is connected to is invalid then this is a source of an error.
                        // Mark the slot and stop iterating, otherwise continue the recursion
                        if (!outputSlot.stageCapability.HasFlag(expectedCapability))
                            errorSourceSlots.Add(outputSlot);
                        else
                            slotStack.Push(outputSlot);
                    }
                }
                else
                {
                    // No need to double visit nodes
                    if (visitedNodes.Contains(slot.owner))
                        continue;
                    visitedNodes.Add(slot.owner);

                    var ownerSlots = slot.owner.GetInputSlots<MaterialSlot>(slot);
                    foreach (var ownerSlot in ownerSlots)
                        slotStack.Push(ownerSlot);
                }
            }

            bool IsEntireNodeStageLocked(AbstractMaterialNode node, ShaderStageCapability expectedNodeCapability)
            {
                var slots = node.GetOutputSlots<MaterialSlot>();
                foreach (var slot in slots)
                {
                    if (expectedNodeCapability != slot.stageCapability)
                        return false;
                }
                return true;
            };

            foreach (var errorSourceSlot in errorSourceSlots)
            {
                var errorNode = errorSourceSlot.owner;

                // Determine if only one slot or the entire node is at fault. Currently only slots are
                // denoted with stage capabilities so deduce this by checking all outputs
                string errorSource;
                if (IsEntireNodeStageLocked(errorNode, errorSourceSlot.stageCapability))
                    errorSource = $"Node {errorNode.name}";
                else
                    errorSource = $"Slot {errorSourceSlot.RawDisplayName()}";

                // Determine what action they can take. If the stage capability is None then this can't be connected to anything.
                string actionToTake;
                if (errorSourceSlot.stageCapability != ShaderStageCapability.None)
                {
                    var validStageName = errorSourceSlot.stageCapability.ToString().ToLower();
                    actionToTake = $"reconnect to a {validStageName} block or delete invalid connection";
                }
                else
                    actionToTake = "delete invalid connection";

                var invalidStageName = expectedShaderStage.ToString().ToLower();
                string message = $"{errorSource} is not compatible with {invalidStageName} block {initialSlot.RawDisplayName()}, {actionToTake}.";
                AddValidationError(errorNode.objectId, message, ShaderCompilerMessageSeverity.Error);
            }
        }
    }

    [Serializable]
    class InspectorPreviewData
    {
        public SerializableMesh serializedMesh = new SerializableMesh();
        public bool preventRotation;

        [NonSerialized]
        public Quaternion rotation = Quaternion.identity;

        [NonSerialized]
        public float scale = 1f;
    }
}