using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using UnityEditor.Graphing;
using UnityEditor.Graphing.Util;
using UnityEditor.ShaderGraph.Drawing.Controls;
using UnityEditor.ShaderGraph.Internal;
using UnityEditor.UIElements;
using UnityEditorInternal;
using UnityEngine;
using UnityEngine.UIElements;
using FloatField = UnityEditor.ShaderGraph.Drawing.FloatField;
using ContextualMenuManipulator = UnityEngine.UIElements.ContextualMenuManipulator;

using GraphDataStore = UnityEditor.ShaderGraph.DataStore<UnityEditor.ShaderGraph.GraphData>;
using UnityEditor.Rendering;

namespace UnityEditor.ShaderGraph.Drawing.Inspector.PropertyDrawers
{
    [SGPropertyDrawer(typeof(ShaderInput))]
    class ShaderInputPropertyDrawer : IPropertyDrawer
    {
        internal delegate void ChangeExposedFieldCallback(bool newValue);
        internal delegate void ChangeValueCallback(object newValue);
        internal delegate void PreChangeValueCallback(string actionName);
        internal delegate void PostChangeValueCallback(bool bTriggerPropertyUpdate = false, ModificationScope modificationScope = ModificationScope.Node);

        // Keyword
        ReorderableList m_KeywordReorderableList;
        int m_KeywordSelectedIndex;

        // Dropdown
        ReorderableList m_DropdownReorderableList;
        ShaderDropdown m_Dropdown;
        int m_DropdownId;
        int m_DropdownSelectedIndex;

        //Virtual Texture
        ReorderableList m_VTReorderableList;
        int m_VTSelectedIndex;
        private static GUIStyle greyLabel;
        TextField m_VTLayer_Name;
        IdentifierField m_VTLayer_RefName;
        ObjectField m_VTLayer_Texture;
        EnumField m_VTLayer_TextureType;

        // Display Name
        TextField m_DisplayNameField;

        TextField m_CustomSlotLabelField;

        // Reference Name
        TextPropertyDrawer m_ReferenceNameDrawer;
        TextField m_ReferenceNameField;

        ShaderInput shaderInput;

        Toggle exposedToggle;
        VisualElement keywordScopeField;
        // Should be provided by the Inspectable
        ShaderInputViewModel m_ViewModel;
        ShaderInputViewModel ViewModel => m_ViewModel;

        const string m_DisplayNameDisallowedPattern = "[^\\w_ ]";
        const string m_ReferenceNameDisallowedPattern = @"(?:[^A-Za-z_0-9_])";
        const string m_EnumRefDisallowedPattern = @"(?:[^A-Za-z_0-9_.])";
        const string m_AttributeValueDisallowedPattern = @"(?:[^A-Za-z_0-9._ ])";

        const string kHDRTextureToggleTooltip =
            "When enabled, any node which samples this texture will automatically decode the sampled value" +
            " if the texture was stored using an encoded HDR format such as dLDR.";

        public ShaderInputPropertyDrawer()
        {
            greyLabel = new GUIStyle(EditorStyles.label);
            greyLabel.normal = new GUIStyleState { textColor = Color.grey };
            greyLabel.focused = new GUIStyleState { textColor = Color.grey };
            greyLabel.hover = new GUIStyleState { textColor = Color.grey };
        }

        GraphData graphData;
        bool isSubGraph { get; set; }
        ChangeExposedFieldCallback _exposedFieldChangedCallback;
        Action _precisionChangedCallback;
        Action _keywordChangedCallback;
        Action _dropdownChangedCallback;
        Action<string> _displayNameChangedCallback;
        Action<string> _referenceNameChangedCallback;
        ChangeValueCallback _changeValueCallback;
        PreChangeValueCallback _preChangeValueCallback;
        PostChangeValueCallback _postChangeValueCallback;

        internal void GetViewModel(ShaderInputViewModel shaderInputViewModel, GraphData inGraphData, PostChangeValueCallback postChangeValueCallback)
        {
            m_ViewModel = shaderInputViewModel;
            this.isSubGraph = m_ViewModel.isSubGraph;
            this.graphData = inGraphData;
            this._keywordChangedCallback = () => graphData.OnKeywordChanged();
            this._dropdownChangedCallback = () => graphData.OnDropdownChanged();
            this._precisionChangedCallback = () => graphData.ValidateGraph();

            this._exposedFieldChangedCallback = newValue =>
            {
                var changeExposedFlagAction = new ChangeExposedFlagAction(shaderInput, newValue);
                ViewModel.requestModelChangeAction(changeExposedFlagAction);
            };

            this._displayNameChangedCallback = newValue =>
            {
                var changeDisplayNameAction = new ChangeDisplayNameAction();
                changeDisplayNameAction.shaderInputReference = shaderInput;
                changeDisplayNameAction.newDisplayNameValue = newValue;
                ViewModel.requestModelChangeAction(changeDisplayNameAction);
            };

            this._changeValueCallback = newValue =>
            {
                var changeDisplayNameAction = new ChangePropertyValueAction();
                changeDisplayNameAction.shaderInputReference = shaderInput;
                changeDisplayNameAction.newShaderInputValue = newValue;
                ViewModel.requestModelChangeAction(changeDisplayNameAction);
            };

            this._referenceNameChangedCallback = newValue =>
            {
                var changeReferenceNameAction = new ChangeReferenceNameAction();
                changeReferenceNameAction.shaderInputReference = shaderInput;
                changeReferenceNameAction.newReferenceNameValue = newValue;
                ViewModel.requestModelChangeAction(changeReferenceNameAction);
            };

            this._preChangeValueCallback = (actionName) => this.graphData.owner.RegisterCompleteObjectUndo(actionName);

            if (shaderInput is AbstractShaderProperty abstractShaderProperty)
            {
                var changePropertyValueAction = new ChangePropertyValueAction();
                changePropertyValueAction.shaderInputReference = abstractShaderProperty;
                this._changeValueCallback = newValue =>
                {
                    changePropertyValueAction.newShaderInputValue = newValue;
                    ViewModel.requestModelChangeAction(changePropertyValueAction);
                };
            }

            this._postChangeValueCallback = postChangeValueCallback;
        }

        public Action inspectorUpdateDelegate { get; set; }

        public VisualElement DrawProperty(
            PropertyInfo propertyInfo,
            object actualObject,
            InspectableAttribute attribute)
        {
            var propertySheet = new PropertySheet();
            shaderInput = actualObject as ShaderInput;
            BuildPropertyNameLabel(propertySheet);
            BuildDisplayNameField(propertySheet);
            BuildReferenceNameField(propertySheet);
            BuildPromoteField(propertySheet);
            BuildPropertyFields(propertySheet);            
            BuildKeywordFields(propertySheet, shaderInput);
            BuildDropdownFields(propertySheet, shaderInput);
            BuildAttributesFields(propertySheet);
            UpdateEnableState();
            return propertySheet;
        }

        void IPropertyDrawer.DisposePropertyDrawer() { }

        void BuildPropertyNameLabel(PropertySheet propertySheet)
        {
            string prefix;
            if (shaderInput is ShaderKeyword)
                prefix = "Keyword";
            else if (shaderInput is ShaderDropdown)
                prefix = "Dropdown";
            else
                prefix = "Property";

            propertySheet.headerContainer.Add(PropertyDrawerUtils.CreateLabel($"{prefix}: {shaderInput.displayName}", 0, FontStyle.Bold));
        }

        void BuildPerRendererDataField(PropertySheet propertySheet)
        {
            if ((!isSubGraph || shaderInput.promoteToFinalShader) && shaderInput is AbstractShaderProperty property && property.isExposed)
            {
                var toggleDataPropertyDrawer = new ToggleDataPropertyDrawer();
                propertySheet.Add(toggleDataPropertyDrawer.CreateGUI(
                    evt =>
                    {
                        this._preChangeValueCallback("Change PerRendererData Toggle");
                        property.PerRendererData = evt.isOn;
                        this._postChangeValueCallback(true, ModificationScope.Graph);
                    },
                    new ToggleData(property.PerRendererData),
                    "Read Only",
                    out var perRendererDataToggleVisualElement,
                    tooltip: "Mark this property with [PerRendererData] attribute.\nThis makes it Read-Only in the material inspector."));
                //perRendererDataToggleVisualElement.SetEnabled(property.isExposed);
            }
        }

        void BuildPromoteField(PropertySheet propertySheet)
        {
            if (!isSubGraph || !shaderInput.canPromoteToFinalShader)
                return;

            var toggleDataPropertyDrawer = new ToggleDataPropertyDrawer();
            propertySheet.Add(toggleDataPropertyDrawer.CreateGUI(
                evt =>
                {
                    this._preChangeValueCallback("Change Promote to Shader toggle");
                    shaderInput.promotedFromAssetID = evt.isOn ? graphData.assetGuid : null;
                    this._postChangeValueCallback(true, ModificationScope.Graph);
                },
                new ToggleData(shaderInput.promoteToFinalShader),
                "Promote to final Shader",
                out var promoteToggleVisualElement,
                tooltip: "Promote this as a material property to the final shader. It will not show up as an input port on the Subgraph Node."));
        }

        void BuildExposedField(PropertySheet propertySheet)
        {
            if (!isSubGraph || shaderInput.promoteToFinalShader)
            {
                var toggleDataPropertyDrawer = new ToggleDataPropertyDrawer();
                propertySheet.Add(toggleDataPropertyDrawer.CreateGUI(
                    evt =>
                    {
                        if (shaderInput is AbstractShaderProperty property &&
                            (!property.overrideHLSLDeclaration || property.hlslDeclarationOverride == HLSLDeclaration.DoNotDeclare))
                        {
                            property.hlslDeclarationOverride = property.GetDefaultHLSLDeclaration();
                            property.overrideHLSLDeclaration = true;
                        }
                        this._preChangeValueCallback("Change Exposed Toggle");
                        this._exposedFieldChangedCallback(evt.isOn);
                        this._postChangeValueCallback(true, ModificationScope.Graph);
                    },
                    new ToggleData(shaderInput.isExposed),
                    shaderInput is ShaderKeyword ? "Generate Material Property" : "Show In Inspector",
                    out var exposedToggleVisualElement,
                    tooltip: shaderInput is ShaderKeyword ? "Generate a material property declaration to show this field in the material inspector." : "Hide or Show this property in the material inspector."));
                exposedToggle = exposedToggleVisualElement as Toggle;
            }
        }

        void BuildCustomBindingField(PropertySheet propertySheet, ShaderInput property)
        {
            if (isSubGraph && property.isCustomSlotAllowed && !shaderInput.promoteToFinalShader)
            {
                var toggleDataPropertyDrawer = new ToggleDataPropertyDrawer();
                propertySheet.Add(toggleDataPropertyDrawer.CreateGUI(
                    newValue =>
                    {
                        if (property.useCustomSlotLabel == newValue.isOn)
                            return;
                        this._preChangeValueCallback("Change Custom Binding");
                        property.useCustomSlotLabel = newValue.isOn;
                        graphData.ValidateGraph();
                        this._postChangeValueCallback(true, ModificationScope.Topological);
                    },
                    new ToggleData(property.isConnectionTestable),
                    "Use Custom Binding",
                    out var exposedToggleVisualElement));
                exposedToggleVisualElement.SetEnabled(true);

                if (property.useCustomSlotLabel)
                {
                    var textPropertyDrawer = new TextPropertyDrawer();
                    var guiElement = textPropertyDrawer.CreateGUI(
                        null,
                        (string)shaderInput.customSlotLabel,
                        "Label",
                        1);

                    m_CustomSlotLabelField = textPropertyDrawer.textField;
                    m_CustomSlotLabelField.RegisterValueChangedCallback(
                        evt =>
                        {
                            if (evt.newValue != shaderInput.customSlotLabel)
                            {
                                this._preChangeValueCallback("Change Custom Binding Label");
                                shaderInput.customSlotLabel = evt.newValue;
                                m_CustomSlotLabelField.AddToClassList("modified");
                                this._postChangeValueCallback(true, ModificationScope.Topological);
                            }
                        });

                    if (!string.IsNullOrEmpty(shaderInput.customSlotLabel))
                        m_CustomSlotLabelField.AddToClassList("modified");
                    m_CustomSlotLabelField.styleSheets.Add(Resources.Load<StyleSheet>("Styles/CustomSlotLabelField"));

                    propertySheet.Add(guiElement);
                }
            }
        }

        void UpdateEnableState()
        {
            // some changes may change the exposed state
            exposedToggle?.SetValueWithoutNotify(shaderInput.isExposed);
            if (shaderInput is ShaderKeyword keyword)
            {
                keywordScopeField?.SetEnabled(!keyword.isBuiltIn && (keyword.keywordDefinition != KeywordDefinition.Predefined));
                exposedToggle?.SetEnabled((keyword.keywordDefinition != KeywordDefinition.Predefined));
                this._exposedFieldChangedCallback(keyword.generatePropertyBlock); // change exposed icon appropriately
            }
            else if (shaderInput is AbstractShaderProperty property)
            {
                // this is the best field to represent whether the toggle should be presented or not.
                exposedToggle?.SetEnabled(property.shouldForceExposed);
            }
            else
            {
                exposedToggle?.SetEnabled(shaderInput.isExposable && !shaderInput.isAlwaysExposed);
            }
        }

        void BuildDisplayNameField(PropertySheet propertySheet)
        {
            var textPropertyDrawer = new TextPropertyDrawer();
            propertySheet.Add(textPropertyDrawer.CreateGUI(
                null,
                (string)shaderInput.displayName,
                "Name",
                tooltip: "Display name used in the material inspector."));

            m_DisplayNameField = textPropertyDrawer.textField;
            m_DisplayNameField.RegisterValueChangedCallback(
                evt =>
                {
                    if (evt.newValue != shaderInput.displayName)
                    {
                        this._preChangeValueCallback("Change Display Name");
                        shaderInput.SetDisplayNameAndSanitizeForGraph(graphData, evt.newValue);
                        this._displayNameChangedCallback(evt.newValue);

                        if (string.IsNullOrEmpty(shaderInput.displayName))
                            m_DisplayNameField.RemoveFromClassList("modified");
                        else
                            m_DisplayNameField.AddToClassList("modified");

                        this._postChangeValueCallback(true, ModificationScope.Layout);
                    }
                });

            if (!string.IsNullOrEmpty(shaderInput.displayName))
                m_DisplayNameField.AddToClassList("modified");
            m_DisplayNameField.SetEnabled(shaderInput.isRenamable);
            m_DisplayNameField.styleSheets.Add(Resources.Load<StyleSheet>("Styles/PropertyNameReferenceField"));
        }

        void BuildReferenceNameField(PropertySheet propertySheet)
        {
            if ((!isSubGraph || shaderInput.promoteToFinalShader) || shaderInput is ShaderKeyword)
            {
                m_ReferenceNameDrawer = new TextPropertyDrawer();
                propertySheet.Add(m_ReferenceNameDrawer.CreateGUI(
                    null,
                    (string)shaderInput.referenceNameForEditing,
                    "Reference",
                    tooltip: "HLSL identifier used in the generated shader code. Use this with the material scripting API."));

                m_ReferenceNameField = m_ReferenceNameDrawer.textField;
                m_ReferenceNameField.RegisterValueChangedCallback(
                    evt =>
                    {
                        this._preChangeValueCallback("Change Reference Name");

                        if (evt.newValue != shaderInput.referenceName)
                        {
                            shaderInput.SetReferenceNameAndSanitizeForGraph(graphData, evt.newValue);
                            this._referenceNameChangedCallback(evt.newValue);
                        }

                        if (string.IsNullOrEmpty(shaderInput.overrideReferenceName))
                        {
                            m_ReferenceNameField.RemoveFromClassList("modified");
                            m_ReferenceNameDrawer.label.RemoveFromClassList("modified");
                        }
                        else
                        {
                            m_ReferenceNameField.AddToClassList("modified");
                            m_ReferenceNameDrawer.label.AddToClassList("modified");
                        }

                        this._postChangeValueCallback(true, ModificationScope.Graph);
                    });

                if (!string.IsNullOrEmpty(shaderInput.overrideReferenceName))
                {
                    m_ReferenceNameDrawer.textField.AddToClassList("modified");
                    m_ReferenceNameDrawer.label.AddToClassList("modified");
                }
                m_ReferenceNameDrawer.textField.SetEnabled(shaderInput.isReferenceRenamable);

                // add the right click context menu to the label
                IManipulator contextMenuManipulator = new ContextualMenuManipulator((evt) => AddShaderInputOptionsToContextMenu(shaderInput, evt));
                m_ReferenceNameDrawer.label.AddManipulator(contextMenuManipulator);
            }
        }

        void AddShaderInputOptionsToContextMenu(ShaderInput shaderInput, ContextualMenuPopulateEvent evt)
        {
            if (shaderInput.isRenamable && !string.IsNullOrEmpty(shaderInput.overrideReferenceName))
                evt.menu.AppendAction(
                    "Reset Reference",
                    e => { ResetReferenceName(); },
                    DropdownMenuAction.AlwaysEnabled);

            if (shaderInput.IsUsingOldDefaultRefName())
                evt.menu.AppendAction(
                    "Upgrade To New Reference Name",
                    e => { UpgradeDefaultReferenceName(); },
                    DropdownMenuAction.AlwaysEnabled);
        }

        public void ResetReferenceName()
        {
            this._preChangeValueCallback("Reset Reference Name");
            var refName = shaderInput.ResetReferenceName(graphData);
            m_ReferenceNameField.value = refName;
            this._referenceNameChangedCallback(refName);
            this._postChangeValueCallback(true, ModificationScope.Graph);
        }

        public void UpgradeDefaultReferenceName()
        {
            this._preChangeValueCallback("Upgrade Reference Name");
            var refName = shaderInput.UpgradeDefaultReferenceName(graphData);
            m_ReferenceNameField.value = refName;
            this._referenceNameChangedCallback(refName);
            this._postChangeValueCallback(true, ModificationScope.Graph);
        }

        bool isCurrentPropertyGlobal;
        void BuildPropertyFields(PropertySheet propertySheet)
        {
            if (shaderInput is AbstractShaderProperty property)
            {
                if (property.sgVersion < property.latestVersion)
                {
                    var typeString = property.propertyType.ToString();

                    Action dismissAction = null;
                    if (property.dismissedUpdateVersion < property.latestVersion)
                    {
                        dismissAction = () =>
                        {
                            _preChangeValueCallback("Dismiss Property Update");
                            property.dismissedUpdateVersion = property.latestVersion;
                            _postChangeValueCallback();
                            inspectorUpdateDelegate?.Invoke();
                        };
                    }

                    var help = HelpBoxRow.CreateUpgradePrompt($"{typeString} Property",
                        () => property.ChangeVersion(property.latestVersion),
                        dismissAction);
                    if (help != null)
                    {
                        propertySheet.Insert(0, help);
                    }
                }

                isCurrentPropertyGlobal = property.GetDefaultHLSLDeclaration() == HLSLDeclaration.Global;

                BuildPrecisionField(propertySheet, property);
                BuildHLSLDeclarationOverrideFields(propertySheet, property);
                BuildExposedField(propertySheet);
                BuildPerRendererDataField(propertySheet);

                switch (property)
                {
                    case IShaderPropertyDrawer propDrawer:
                        propDrawer.HandlePropertyField(propertySheet, _preChangeValueCallback, _postChangeValueCallback);
                        break;
                    case UnityEditor.ShaderGraph.Serialization.MultiJsonInternal.UnknownShaderPropertyType:
                        var helpBox = new HelpBoxRow("Cannot find the code for this Property, a package may be missing.", MessageType.Warning);
                        propertySheet.Add(helpBox);
                        break;
                    case Vector1ShaderProperty vector1Property:
                        HandleVector1ShaderProperty(propertySheet, vector1Property);
                        break;
                    case Vector2ShaderProperty vector2Property:
                        HandleVector2ShaderProperty(propertySheet, vector2Property);
                        break;
                    case Vector3ShaderProperty vector3Property:
                        HandleVector3ShaderProperty(propertySheet, vector3Property);
                        break;
                    case Vector4ShaderProperty vector4Property:
                        HandleVector4ShaderProperty(propertySheet, vector4Property);
                        break;
                    case ColorShaderProperty colorProperty:
                        HandleColorProperty(propertySheet, colorProperty);
                        break;
                    case Texture2DShaderProperty texture2DProperty:
                        HandleTexture2DProperty(propertySheet, texture2DProperty);
                        break;
                    case Texture2DArrayShaderProperty texture2DArrayProperty:
                        HandleTexture2DArrayProperty(propertySheet, texture2DArrayProperty);
                        break;
                    case VirtualTextureShaderProperty virtualTextureProperty:
                        HandleVirtualTextureProperty(propertySheet, virtualTextureProperty);
                        break;
                    case Texture3DShaderProperty texture3DProperty:
                        HandleTexture3DProperty(propertySheet, texture3DProperty);
                        break;
                    case CubemapShaderProperty cubemapProperty:
                        HandleCubemapProperty(propertySheet, cubemapProperty);
                        break;
                    case BooleanShaderProperty booleanProperty:
                        HandleBooleanProperty(propertySheet, booleanProperty);
                        break;
                    case Matrix2ShaderProperty matrix2Property:
                        HandleMatrix2PropertyField(propertySheet, matrix2Property);
                        break;
                    case Matrix3ShaderProperty matrix3Property:
                        HandleMatrix3PropertyField(propertySheet, matrix3Property);
                        break;
                    case Matrix4ShaderProperty matrix4Property:
                        HandleMatrix4PropertyField(propertySheet, matrix4Property);
                        break;
                    case SamplerStateShaderProperty samplerStateProperty:
                        HandleSamplerStatePropertyField(propertySheet, samplerStateProperty);
                        break;
                    case GradientShaderProperty gradientProperty:
                        HandleGradientPropertyField(propertySheet, gradientProperty);
                        break;
                }
            }

            BuildCustomBindingField(propertySheet, shaderInput);
        }

        static string[] allHLSLDeclarationStrings = new string[]
        {
            "Do Not Declare",       // HLSLDeclaration.DoNotDeclare
            "Global",               // HLSLDeclaration.Global
            "Per Material",         // HLSLDeclaration.UnityPerMaterial
            "Hybrid Per Instance",  // HLSLDeclaration.HybridPerInstance
        };

        void BuildHLSLDeclarationOverrideFields(PropertySheet propertySheet, AbstractShaderProperty property)
        {
            if (isSubGraph && !shaderInput.promoteToFinalShader)
                return;

            var hlslDecls = Enum.GetValues(typeof(HLSLDeclaration));
            var allowedDecls = new List<HLSLDeclaration>();

            bool anyAllowed = false;
            for (int i = 0; i < hlslDecls.Length; i++)
            {
                HLSLDeclaration decl = (HLSLDeclaration)hlslDecls.GetValue(i);
                var allowed = property.AllowHLSLDeclaration(decl);
                anyAllowed = anyAllowed || allowed;
                if (allowed)
                    allowedDecls.Add(decl);
            }

            const string tooltip = "Indicate where the property is expected to be changed.";
            if (anyAllowed)
            {
                var propRow = new PropertyRow(PropertyDrawerUtils.CreateLabel("Scope", 0));
                propRow.tooltip = tooltip;
                var popupField = new PopupField<HLSLDeclaration>(
                    allowedDecls,
                    property.GetDefaultHLSLDeclaration(),
                    (h => allHLSLDeclarationStrings[(int)h]),
                    (h => allHLSLDeclarationStrings[(int)h]));

                popupField.RegisterValueChangedCallback(
                    evt =>
                    {
                        this._preChangeValueCallback("Change Scope");
                        if (property.hlslDeclarationOverride == evt.newValue)
                            return;
                        property.hlslDeclarationOverride = evt.newValue;
                        property.overrideHLSLDeclaration = true;
                        UpdateEnableState();
                        if ((evt.newValue == HLSLDeclaration.Global) ^ (evt.previousValue == HLSLDeclaration.Global))
                        {
                            this._exposedFieldChangedCallback(evt.newValue != HLSLDeclaration.Global);
                            property.generatePropertyBlock = evt.newValue != HLSLDeclaration.Global;
                        }
                        this._postChangeValueCallback(true, ModificationScope.Graph);
                    });

                propRow.Add(popupField);
                propertySheet.Add(propRow);
            }
        }

        void BuildPrecisionField(PropertySheet propertySheet, AbstractShaderProperty property)
        {
            var enumPropertyDrawer = new EnumPropertyDrawer();
            propertySheet.Add(enumPropertyDrawer.CreateGUI(newValue =>
                {
                    this._preChangeValueCallback("Change Precision");
                    if (property.precision == (Precision)newValue)
                        return;
                    property.precision = (Precision)newValue;
                    this._precisionChangedCallback();
                    this._postChangeValueCallback();
                },
                (PropertyDrawerUtils.UIPrecisionForShaderGraphs)property.precision,
                "Precision",
                PropertyDrawerUtils.UIPrecisionForShaderGraphs.Inherit,
                out var precisionField,
                tooltip: "Request that the property uses Single 32-bit or Half 16-bit floating point precision."));
            if (property is Serialization.MultiJsonInternal.UnknownShaderPropertyType)
                precisionField.SetEnabled(false);
        }

        enum EnumTypeForUI { ExplicitValues = EnumType.Enum, TypeReference = EnumType.CSharpEnum }

        void HandleVector1ShaderProperty(PropertySheet propertySheet, Vector1ShaderProperty vector1ShaderProperty)
        {
            if (shaderInput.isExposed && (!isSubGraph || shaderInput.promoteToFinalShader) && !isCurrentPropertyGlobal)
            {
                var enumPropertyDrawer = new EnumPropertyDrawer();
                propertySheet.Add(enumPropertyDrawer.CreateGUI(
                    newValue =>
                    {
                        this._preChangeValueCallback("Change Vector1 Mode");
                        vector1ShaderProperty.floatType = (FloatType)newValue;
                        this._postChangeValueCallback(true);
                    },
                    vector1ShaderProperty.floatType,
                    "Mode",
                    FloatType.Default,
                    out var modePropertyEnumField,
                    tooltip: "Indicate how this float property should appear in the material inspector UI."));
            }

            var floatType = (!shaderInput.isExposed || isSubGraph || isCurrentPropertyGlobal) && !shaderInput.promoteToFinalShader ? FloatType.Default : vector1ShaderProperty.floatType;
            // Handle vector 1 mode parameters
            switch (floatType)
            {
                case FloatType.Slider:
                    var sliderTypePropertyDrawer = new EnumPropertyDrawer();
                    propertySheet.Add(sliderTypePropertyDrawer.CreateGUI(
                        newValue =>
                        {
                            this._preChangeValueCallback("Change Slider Type");
                            vector1ShaderProperty.sliderType = (SliderType)newValue;
                            this._postChangeValueCallback(true);
                        },
                        vector1ShaderProperty.sliderType,
                    "Slider Type",
                        SliderType.Default,
                        out var sliderTypePropertyEnumField,
                        tooltip: "Set the Slider type."));

                    var floatPropertyDrawer = new FloatPropertyDrawer();
                    // Default field
                    propertySheet.Add(floatPropertyDrawer.CreateGUI(
                        newValue =>
                        {
                            _preChangeValueCallback("Change Property Value");
                            _changeValueCallback(newValue);
                            _postChangeValueCallback();
                        },
                        vector1ShaderProperty.value,
                        isCurrentPropertyGlobal ? "Preview Value" : "Default Value",
                        out var propertyFloatField));

                    // Min field
                    propertySheet.Add(floatPropertyDrawer.CreateGUI(
                        newValue =>
                        {
                            if (newValue > vector1ShaderProperty.rangeValues.y)
                                propertySheet.warningContainer.Q<Label>().text = "Min cannot be greater than Max.";
                            _preChangeValueCallback("Change Range Property Minimum");
                            vector1ShaderProperty.rangeValues = new Vector2(newValue, vector1ShaderProperty.rangeValues.y);
                            _postChangeValueCallback();
                        },
                        vector1ShaderProperty.rangeValues.x,
                        "Min",
                        out var minFloatField));

                    // Max field
                    propertySheet.Add(floatPropertyDrawer.CreateGUI(
                        newValue =>
                        {
                            if (newValue < vector1ShaderProperty.rangeValues.x)
                                propertySheet.warningContainer.Q<Label>().text = "Max cannot be lesser than Min.";
                            this._preChangeValueCallback("Change Range Property Maximum");
                            vector1ShaderProperty.rangeValues = new Vector2(vector1ShaderProperty.rangeValues.x, newValue);
                            this._postChangeValueCallback();
                        },
                        vector1ShaderProperty.rangeValues.y,
                        "Max",
                        out var maxFloatField));

                    var defaultField = (FloatField)propertyFloatField;
                    var minField = (FloatField)minFloatField;
                    var maxField = (FloatField)maxFloatField;

                    defaultField.RegisterValueChangedCallback(v =>
                    {
                        if (vector1ShaderProperty.sliderType == SliderType.Integer)
                            defaultField.value = MathF.Round((float)v.newValue);
                    });

                    minField.RegisterValueChangedCallback(v =>
                    {
                        if (vector1ShaderProperty.sliderType == SliderType.Integer)
                            minField.value = MathF.Round((float)v.newValue);
                    });

                    maxField.RegisterValueChangedCallback(v =>
                    {
                        if (vector1ShaderProperty.sliderType == SliderType.Integer)
                            maxField.value = MathF.Round((float)v.newValue);
                    });

                    minField.Q("unity-text-input").RegisterCallback<FocusOutEvent>(evt =>
                    {
                        propertySheet.warningContainer.Q<Label>().text = "";
                        vector1ShaderProperty.value = Mathf.Max(Mathf.Min(vector1ShaderProperty.value, vector1ShaderProperty.rangeValues.y), vector1ShaderProperty.rangeValues.x);
                        defaultField.value = vector1ShaderProperty.value;
                        _postChangeValueCallback();
                    }, TrickleDown.TrickleDown);

                    maxField.Q("unity-text-input").RegisterCallback<FocusOutEvent>(evt =>
                    {
                        propertySheet.warningContainer.Q<Label>().text = "";
                        vector1ShaderProperty.value = Mathf.Max(Mathf.Min(vector1ShaderProperty.value, vector1ShaderProperty.rangeValues.y), vector1ShaderProperty.rangeValues.x);
                        defaultField.value = vector1ShaderProperty.value;
                        _postChangeValueCallback();
                    }, TrickleDown.TrickleDown);

                    if (vector1ShaderProperty.sliderType == SliderType.Power)
                    {
                        propertySheet.Add(floatPropertyDrawer.CreateGUI(
                        newValue =>
                        {
                            _preChangeValueCallback("Change Slider Power Value");
                            vector1ShaderProperty.sliderPower = newValue;
                            _postChangeValueCallback();
                        },
                        vector1ShaderProperty.sliderPower,
                        "Power",
                        out var propertySliderPowerField));
                        propertySliderPowerField.tooltip = "The Power factor applied to the Slider";
                    }

                    break;

                case FloatType.Integer:
                    var integerPropertyDrawer = new IntegerPropertyDrawer();
                    // Default field
                    propertySheet.Add(integerPropertyDrawer.CreateGUI(
                        newValue =>
                        {
                            this._preChangeValueCallback("Change property value");
                            this._changeValueCallback((float)newValue);
                            this._postChangeValueCallback();
                        },
                        (int)vector1ShaderProperty.value,
                        isCurrentPropertyGlobal ? "Preview Value" : "Default Value",
                        out var integerPropertyField));
                    break;

                case FloatType.Enum:
                    var enumValuePropertyDrawer = new IntegerPropertyDrawer();
                    // Default field
                    propertySheet.Add(enumValuePropertyDrawer.CreateGUI(
                        newValue =>
                        {
                            this._preChangeValueCallback("Change property value");
                            this._changeValueCallback((float)newValue);
                            this._postChangeValueCallback();
                        },
                        (int)vector1ShaderProperty.value,
                        isCurrentPropertyGlobal ? "Preview Value" : "Default Value",
                        out var enumValuePropertyField));

                    var enumPropertyDrawer = new EnumPropertyDrawer();
                    propertySheet.Add(enumPropertyDrawer.CreateGUI(
                        newValue =>
                        {
                            this._preChangeValueCallback("Change Enum Type");
                            vector1ShaderProperty.enumType = (EnumType)newValue;
                            this._postChangeValueCallback(true);
                        },
                        (EnumTypeForUI)vector1ShaderProperty.enumType,
                        "Enum Type",
                        EnumTypeForUI.ExplicitValues,
                        out var modePropertyEnumField,
                        tooltip: "Set the Enum value type."));

                    switch (vector1ShaderProperty.enumType)
                    {
                        case EnumType.Enum:
                            var container = new IMGUIContainer(() => OnEnumGUIHandler()) { name = "ListContainer" };
                            AddPropertyRowToSheet(propertySheet, container, "Entries");
                            break;
                        case EnumType.CSharpEnum:
                            var textPropertyDrawer = new TextPropertyDrawer();
                            propertySheet.Add(textPropertyDrawer.CreateGUI(
                                newValue =>
                                {
                                    this._preChangeValueCallback("Change C# Enum Type");
                                    newValue = GetSanitizedEnumRefName(newValue);
                                    vector1ShaderProperty.cSharpEnumString = newValue;
                                    this._postChangeValueCallback(true);
                                },
                                vector1ShaderProperty.cSharpEnumString,
                                "C# Enum Type",
                                tooltip: "Enter an Enum type."));
                            break;
                        case EnumType.KeywordEnum:
                        default:
                            break;
                    }
                    break;

                default:
                    var defaultFloatPropertyDrawer = new FloatPropertyDrawer();
                    // Default field
                    propertySheet.Add(defaultFloatPropertyDrawer.CreateGUI(
                        newValue =>
                        {
                            this._preChangeValueCallback("Change property value");
                            this._changeValueCallback(newValue);
                            this._postChangeValueCallback();
                        },
                        vector1ShaderProperty.value,
                        isCurrentPropertyGlobal ? "Preview Value" : "Default Value",
                        out var defaultFloatPropertyField));
                    if (isSubGraph)
                    {
                        var boolPropertyDrawer = new BoolPropertyDrawer();
                        propertySheet.Add(boolPropertyDrawer.CreateGUI(
                            newValue =>
                            {
                                _preChangeValueCallback("Change Const Int Mode");
                                vector1ShaderProperty.LiteralFloatMode = newValue;
                                // see the node in PropertyNode about a potentially better way to handle this
                                this._precisionChangedCallback();
                                _postChangeValueCallback();
                            },
                            vector1ShaderProperty.LiteralFloatMode,
                            "Requires Literal Input",
                            out _));
                    }
                    break;
            }
        }

        void HandleVector2ShaderProperty(PropertySheet propertySheet, Vector2ShaderProperty vector2ShaderProperty)
        {
            var vector2PropertyDrawer = new Vector2PropertyDrawer();
            vector2PropertyDrawer.preValueChangeCallback = () => this._preChangeValueCallback("Change property value");
            vector2PropertyDrawer.postValueChangeCallback = () => this._postChangeValueCallback();

            propertySheet.Add(vector2PropertyDrawer.CreateGUI(
                newValue => _changeValueCallback(newValue),
                vector2ShaderProperty.value,
                isCurrentPropertyGlobal ? "Preview Value" : "Default Value",
                out var propertyVec2Field));
        }

        void HandleVector3ShaderProperty(PropertySheet propertySheet, Vector3ShaderProperty vector3ShaderProperty)
        {
            var vector3PropertyDrawer = new Vector3PropertyDrawer();
            vector3PropertyDrawer.preValueChangeCallback = () => this._preChangeValueCallback("Change property value");
            vector3PropertyDrawer.postValueChangeCallback = () => this._postChangeValueCallback();

            propertySheet.Add(vector3PropertyDrawer.CreateGUI(
                newValue => _changeValueCallback(newValue),
                vector3ShaderProperty.value,
                isCurrentPropertyGlobal ? "Preview Value" : "Default Value",
                out var propertyVec3Field));
        }

        void HandleVector4ShaderProperty(PropertySheet propertySheet, Vector4ShaderProperty vector4Property)
        {
            var vector4PropertyDrawer = new Vector4PropertyDrawer();
            vector4PropertyDrawer.preValueChangeCallback = () => this._preChangeValueCallback("Change property value");
            vector4PropertyDrawer.postValueChangeCallback = () => this._postChangeValueCallback();

            propertySheet.Add(vector4PropertyDrawer.CreateGUI(
                newValue => _changeValueCallback(newValue),
                vector4Property.value,
                isCurrentPropertyGlobal ? "Preview Value" : "Default Value",
                out var propertyVec4Field));
        }

        void HandleColorProperty(PropertySheet propertySheet, ColorShaderProperty colorProperty)
        {
            var colorPropertyDrawer = new ColorPropertyDrawer();

            if (!isSubGraph || shaderInput.promoteToFinalShader)
            {
                if (colorProperty.isMainColor)
                {
                    var mainColorLabel = new IMGUIContainer(() =>
                    {
                        EditorGUI.indentLevel++;
                        EditorGUILayout.LabelField("Main Color", EditorStyles.largeLabel);
                        EditorGUILayout.Space();
                        EditorGUI.indentLevel--;
                    });
                    propertySheet.Insert(2, mainColorLabel);
                }
            }

            if (/*shaderInput.isExposed && */(!isSubGraph || shaderInput.promoteToFinalShader) && !isCurrentPropertyGlobal)
            {
                var enumPropertyDrawer = new EnumPropertyDrawer();

                propertySheet.Add(enumPropertyDrawer.CreateGUI(
                    newValue =>
                    {
                        this._preChangeValueCallback("Change Color Mode");
                        colorProperty.colorMode = (ColorMode)newValue;
                        this._postChangeValueCallback(true, ModificationScope.Graph);
                    },
                    colorProperty.colorMode,
                    "Mode",
                    ColorMode.Default,
                    out var colorModeField));
            }

            propertySheet.Add(colorPropertyDrawer.CreateGUI(
                newValue =>
                {
                    this._preChangeValueCallback("Change property value");
                    this._changeValueCallback(newValue);
                    this._postChangeValueCallback();
                },
                colorProperty.value,
                isCurrentPropertyGlobal ? "Preview Value" : "Default Value",
                out var propertyColorField));

            var colorField = (ColorField)propertyColorField;
            colorField.hdr = colorProperty.colorMode == ColorMode.HDR;
        }

        void HandleTexture2DProperty(PropertySheet propertySheet, Texture2DShaderProperty texture2DProperty)
        {
            var texture2DPropertyDrawer = new Texture2DPropertyDrawer();

            if (!isSubGraph || shaderInput.promoteToFinalShader)
            {
                if (texture2DProperty.isMainTexture)
                {
                    var mainTextureLabel = new IMGUIContainer(() =>
                    {
                        EditorGUI.indentLevel++;
                        EditorGUILayout.LabelField("Main Texture", EditorStyles.largeLabel);
                        EditorGUILayout.Space();
                        EditorGUI.indentLevel--;
                    });
                    propertySheet.Insert(2, mainTextureLabel);
                }
            }


            propertySheet.Add(texture2DPropertyDrawer.CreateGUI(
                newValue =>
                {
                    this._preChangeValueCallback("Change property value");
                    this._changeValueCallback(newValue);
                    this._postChangeValueCallback();
                },
                texture2DProperty.value.texture,
                isCurrentPropertyGlobal ? "Preview Value" : "Default Value",
                out var texture2DField
            ));

            if ((!isSubGraph || shaderInput.promoteToFinalShader) && !isCurrentPropertyGlobal)
            {
                var enumPropertyDrawer = new EnumPropertyDrawer();
                propertySheet.Add(enumPropertyDrawer.CreateGUI(
                    newValue =>
                    {
                        this._preChangeValueCallback("Change Texture mode");
                        if (texture2DProperty.defaultType == (Texture2DShaderProperty.DefaultType)newValue)
                            return;
                        texture2DProperty.defaultType = (Texture2DShaderProperty.DefaultType)newValue;
                        this._postChangeValueCallback(false, ModificationScope.Graph);
                    },
                    texture2DProperty.defaultType,
                    "Mode",
                    Texture2DShaderProperty.DefaultType.White,
                    out var textureModeField,
                    tooltip: "Fallback texture if none is provided."));

                textureModeField.SetEnabled(texture2DProperty.generatePropertyBlock);
            }
            if (!isSubGraph || shaderInput.promoteToFinalShader)
            {
                var togglePropertyDrawer = new ToggleDataPropertyDrawer();
                propertySheet.Add(togglePropertyDrawer.CreateGUI(
                    newValue =>
                    {
                        this._preChangeValueCallback("Change Use Tilling and Offset");
                        if (texture2DProperty.useTilingAndOffset == newValue.isOn)
                            return;
                        texture2DProperty.useTilingAndOffset = newValue.isOn;
                        this._postChangeValueCallback();
                    },
                    new ToggleData(texture2DProperty.useTilingAndOffset, true),
                    "Use Tiling and Offset",
                    out var tilingAndOffsetToggle));
                propertySheet.Add(togglePropertyDrawer.CreateGUI(
                    newValue =>
                    {
                        this._preChangeValueCallback("Change Use TexelSize");
                        if (texture2DProperty.useTexelSize == newValue.isOn)
                            return;
                        texture2DProperty.useTexelSize = newValue.isOn;
                        this._postChangeValueCallback();
                    },
                    new ToggleData(texture2DProperty.useTexelSize, true),
                    "Use TexelSize",
                    out var texelSizeToggle));
                    propertySheet.Add(togglePropertyDrawer.CreateGUI(
                    newValue =>
                    {
                        this._preChangeValueCallback("Change Is HDR");
                        if (texture2DProperty.isHDR == newValue.isOn)
                            return;
                        texture2DProperty.isHDR = newValue.isOn;
                        this._postChangeValueCallback(modificationScope: ModificationScope.Graph);
                    },
                    new ToggleData(texture2DProperty.isHDR, true),
                    "Is HDR",
                    out var isHDRToggle,
                    tooltip: kHDRTextureToggleTooltip));
            }
        }

        void HandleTexture2DArrayProperty(PropertySheet propertySheet, Texture2DArrayShaderProperty texture2DArrayProperty)
        {
            var texture2DArrayPropertyDrawer = new Texture2DArrayPropertyDrawer();
            propertySheet.Add(texture2DArrayPropertyDrawer.CreateGUI(
                newValue =>
                {
                    this._preChangeValueCallback("Change property value");
                    this._changeValueCallback(newValue);
                    this._postChangeValueCallback();
                },
                texture2DArrayProperty.value.textureArray,
                isCurrentPropertyGlobal ? "Preview Value" : "Default Value",
                out var texture2DArrayField
            ));

            if (!isSubGraph || shaderInput.promoteToFinalShader)
            {
                var togglePropertyDrawer = new ToggleDataPropertyDrawer();
                propertySheet.Add(togglePropertyDrawer.CreateGUI(
                newValue =>
                {
                    this._preChangeValueCallback("Change Is HDR");
                    if (texture2DArrayProperty.isHDR == newValue.isOn)
                        return;
                    texture2DArrayProperty.isHDR = newValue.isOn;
                    this._postChangeValueCallback(modificationScope: ModificationScope.Graph);
                },
                new ToggleData(texture2DArrayProperty.isHDR, true),
                "Is HDR",
                out var isHDRToggle,
                tooltip: kHDRTextureToggleTooltip));
            }
        }

        #region VT reorderable list handler
        void HandleVirtualTextureProperty(PropertySheet propertySheet, VirtualTextureShaderProperty virtualTextureProperty)
        {
            var container = new IMGUIContainer(() => OnVTGUIHandler(virtualTextureProperty)) { name = "ListContainer" };
            AddPropertyRowToSheet(propertySheet, container, "Layers");

            m_VTLayer_Name = new TextField();
            m_VTLayer_Name.isDelayed = true;
            m_VTLayer_Name.RegisterValueChangedCallback(
                evt =>
                {
                    int index = m_VTReorderableList.index;
                    if (index >= 0 && index < m_VTReorderableList.list.Count)
                    {
                        var svt = m_VTReorderableList.list[index] as SerializableVirtualTextureLayer;
                        var otherPropertyNames = graphData.BuildPropertyDisplayNameList(virtualTextureProperty, svt.layerName);
                        var newLayerName = GraphUtil.SanitizeName(otherPropertyNames, "{0} ({1})", evt.newValue);
                        if (newLayerName != svt.layerName)
                        {
                            this._preChangeValueCallback("Change Layer Name");
                            svt.layerName = newLayerName;
                            this._postChangeValueCallback(false, ModificationScope.Graph);
                            m_VTLayer_Name.SetValueWithoutNotify(newLayerName);
                        }
                    }
                });
            AddPropertyRowToSheet(propertySheet, m_VTLayer_Name, "  Layer Name");

            m_VTLayer_RefName = new IdentifierField();
            m_VTLayer_RefName.isDelayed = true;
            m_VTLayer_RefName.RegisterValueChangedCallback(
                evt =>
                {
                    int index = m_VTReorderableList.index;
                    if (index >= 0 && index < m_VTReorderableList.list.Count)
                    {
                        var svt = m_VTReorderableList.list[index] as SerializableVirtualTextureLayer;
                        var otherPropertyRefNames = graphData.BuildPropertyReferenceNameList(virtualTextureProperty, svt.layerRefName);
                        var newName = NodeUtils.ConvertToValidHLSLIdentifier(evt.newValue);
                        var newLayerRefName = GraphUtil.SanitizeName(otherPropertyRefNames, "{0}_{1}", newName);
                        if (newLayerRefName != svt.layerRefName)
                        {
                            this._preChangeValueCallback("Change Layer Ref Name");
                            svt.layerRefName = newLayerRefName;
                            this._postChangeValueCallback(false, ModificationScope.Graph);
                        }
                        // Always update the display name to the sanitized name. If an invalid name was entered that ended up being sanitized to the old value,
                        // the text box still needs to be updated to display the sanitized name.
                        m_VTLayer_RefName.SetValueWithoutNotify(newLayerRefName);
                    }
                });
            AddPropertyRowToSheet(propertySheet, m_VTLayer_RefName, "  Layer Reference");

            m_VTLayer_Texture = new ObjectField();
            m_VTLayer_Texture.objectType = typeof(Texture);
            m_VTLayer_Texture.allowSceneObjects = false;
            m_VTLayer_Texture.RegisterValueChangedCallback(
                evt =>
                {
                    this._preChangeValueCallback("Change Layer Texture");

                    int index = m_VTReorderableList.index;
                    if (index >= 0 && index < m_VTReorderableList.list.Count)
                        (m_VTReorderableList.list[index] as SerializableVirtualTextureLayer).layerTexture.texture = (evt.newValue as Texture);

                    this._postChangeValueCallback(false, ModificationScope.Graph);
                });
            AddPropertyRowToSheet(propertySheet, m_VTLayer_Texture, "  Layer Texture");


            m_VTLayer_TextureType = new EnumField();
            m_VTLayer_TextureType.Init(LayerTextureType.Default);
            m_VTLayer_TextureType.RegisterValueChangedCallback(
                evt =>
                {
                    this._preChangeValueCallback("Change Layer Texture Type");

                    int index = m_VTReorderableList.index;
                    if (index >= 0 && index < m_VTReorderableList.list.Count)
                        (m_VTReorderableList.list[index] as SerializableVirtualTextureLayer).layerTextureType = (LayerTextureType)evt.newValue;

                    this._postChangeValueCallback(false, ModificationScope.Graph);
                });
            AddPropertyRowToSheet(propertySheet, m_VTLayer_TextureType, "  Layer Texture Type");
        }

        private void OnVTGUIHandler(VirtualTextureShaderProperty property)
        {
            if (m_VTReorderableList == null)
            {
                VTRecreateList(property);
                VTAddCallbacks(property);

                // update selected entry to reflect default selection
                VTSelectEntry(m_VTReorderableList);
            }

            m_VTReorderableList.index = m_VTSelectedIndex;
            m_VTReorderableList.DoLayoutList();
        }

        internal void VTRecreateList(VirtualTextureShaderProperty property)
        {
            // Create reorderable list from entries
            m_VTReorderableList = new ReorderableList(property.value.layers, typeof(SerializableVirtualTextureLayer), true, true, true, true);
        }

        private void VTAddCallbacks(VirtualTextureShaderProperty property)
        {
            // Draw Header
            m_VTReorderableList.drawHeaderCallback = (Rect rect) =>
            {
                int indent = 14;
                var displayRect = new Rect(rect.x + indent, rect.y, rect.width, rect.height);
                EditorGUI.LabelField(displayRect, "Layer Name");
            };

            // Draw Element
            m_VTReorderableList.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) =>
            {
                SerializableVirtualTextureLayer entry = ((SerializableVirtualTextureLayer)m_VTReorderableList.list[index]);
                EditorGUI.BeginChangeCheck();

                EditorGUI.LabelField(rect, entry.layerName);
            };

            // Element height
            m_VTReorderableList.elementHeightCallback = (int indexer) =>
            {
                return m_VTReorderableList.elementHeight;
            };

            // Can add
            m_VTReorderableList.onCanAddCallback = (ReorderableList list) =>
            {
                return list.count < 4;
            };

            // Can remove
            m_VTReorderableList.onCanRemoveCallback = (ReorderableList list) =>
            {
                return list.count > 1;
            };

            void AddEntryLamda(ReorderableList list) => VTAddEntry(list, property);
            void RemoveEntryLamda(ReorderableList list) => VTRemoveEntry(list, property);
            // Add callback delegates
            m_VTReorderableList.onSelectCallback += VTSelectEntry;
            m_VTReorderableList.onAddCallback += AddEntryLamda;
            m_VTReorderableList.onRemoveCallback += RemoveEntryLamda;
            m_VTReorderableList.onReorderCallback += VTReorderEntries;
        }

        private void VTSelectEntry(ReorderableList list)
        {
            m_VTSelectedIndex = list.index;
            if (m_VTSelectedIndex >= 0 && m_VTSelectedIndex < list.count)
            {
                m_VTLayer_Name.SetEnabled(true);
                m_VTLayer_RefName.SetEnabled(true);
                m_VTLayer_Texture.SetEnabled(true);
                m_VTLayer_TextureType.SetEnabled(true);
                m_VTLayer_Name.SetValueWithoutNotify((list.list[m_VTSelectedIndex] as SerializableVirtualTextureLayer).layerName);
                m_VTLayer_RefName.SetValueWithoutNotify((list.list[m_VTSelectedIndex] as SerializableVirtualTextureLayer).layerRefName);
                m_VTLayer_Texture.SetValueWithoutNotify((list.list[m_VTSelectedIndex] as SerializableVirtualTextureLayer).layerTexture.texture);
                m_VTLayer_TextureType.SetValueWithoutNotify((list.list[m_VTSelectedIndex] as SerializableVirtualTextureLayer).layerTextureType);
            }
            else
            {
                m_VTLayer_Name.SetEnabled(false);
                m_VTLayer_RefName.SetEnabled(false);
                m_VTLayer_Texture.SetEnabled(false);
                m_VTLayer_TextureType.SetEnabled(false);
                m_VTLayer_Name.SetValueWithoutNotify("");
                m_VTLayer_RefName.SetValueWithoutNotify("");
                m_VTLayer_Texture.SetValueWithoutNotify(null);
                m_VTLayer_TextureType.SetValueWithoutNotify(LayerTextureType.Default);
            }
        }

        private void VTAddEntry(ReorderableList list, VirtualTextureShaderProperty property)
        {
            this._preChangeValueCallback("Add Virtual Texture Entry");

            int index = VTGetFirstUnusedID(property);
            if (index <= 0)
                return; // Error has already occured, don't attempt to add this entry.

            var layerName = "Layer" + index.ToString();
            // Add new entry
            property.value.layers.Add(new SerializableVirtualTextureLayer(layerName, new SerializableTexture()));

            // Update Blackboard & Nodes
            //DirtyNodes();
            this._postChangeValueCallback(true);
            m_VTSelectedIndex = list.list.Count - 1;
            //Hack to handle downstream SampleVirtualTextureNodes
            graphData.ValidateGraph();
        }

        // Allowed indicies are 1-MAX_ENUM_ENTRIES
        private int VTGetFirstUnusedID(VirtualTextureShaderProperty property)
        {
            List<int> ususedIDs = new List<int>();

            foreach (SerializableVirtualTextureLayer virtualTextureEntry in property.value.layers)
            {
                ususedIDs.Add(property.value.layers.IndexOf(virtualTextureEntry));
            }

            for (int x = 1; x <= 4; x++)
            {
                if (!ususedIDs.Contains(x))
                    return x;
            }

            Debug.LogError("GetFirstUnusedID: Attempting to get unused ID when all IDs are used.");
            return -1;
        }

        private void VTRemoveEntry(ReorderableList list, VirtualTextureShaderProperty property)
        {
            this._preChangeValueCallback("Remove Virtual Texture Entry");

            // Remove entry
            m_VTSelectedIndex = list.index;
            var selectedEntry = (SerializableVirtualTextureLayer)m_VTReorderableList.list[list.index];
            property.value.layers.Remove(selectedEntry);

            // Update Blackboard & Nodes
            //DirtyNodes();
            this._postChangeValueCallback(true);
            m_VTSelectedIndex = m_VTSelectedIndex >= list.list.Count - 1 ? list.list.Count - 1 : m_VTSelectedIndex;
            //Hack to handle downstream SampleVirtualTextureNodes
            graphData.ValidateGraph();
        }

        private void VTReorderEntries(ReorderableList list)
        {
            this._postChangeValueCallback(true);
        }

        #endregion
        void HandleTexture3DProperty(PropertySheet propertySheet, Texture3DShaderProperty texture3DShaderProperty)
        {
            var texture3DPropertyDrawer = new Texture3DPropertyDrawer();
            propertySheet.Add(texture3DPropertyDrawer.CreateGUI(
                newValue =>
                {
                    this._preChangeValueCallback("Change property value");
                    this._changeValueCallback(newValue);
                    this._postChangeValueCallback();
                },
                texture3DShaderProperty.value.texture,
                isCurrentPropertyGlobal ? "Preview Value" : "Default Value",
                out var texture3DField
            ));

            if (!isSubGraph || shaderInput.promoteToFinalShader)
            {
                ToggleDataPropertyDrawer togglePropertyDrawer = new ToggleDataPropertyDrawer();
                propertySheet.Add(togglePropertyDrawer.CreateGUI(
                    newValue =>
                    {
                        this._preChangeValueCallback("Change Is HDR");
                        if (texture3DShaderProperty.isHDR == newValue.isOn)
                            return;
                        texture3DShaderProperty.isHDR = newValue.isOn;
                        this._postChangeValueCallback(modificationScope: ModificationScope.Graph);
                    },
                    new ToggleData(texture3DShaderProperty.isHDR, true),
                    "Is HDR",
                    out var isHDRToggle,
                    tooltip: kHDRTextureToggleTooltip));
            }
        }

        void HandleCubemapProperty(PropertySheet propertySheet, CubemapShaderProperty cubemapProperty)
        {
            var cubemapPropertyDrawer = new CubemapPropertyDrawer();
            propertySheet.Add(cubemapPropertyDrawer.CreateGUI(
                newValue =>
                {
                    this._preChangeValueCallback("Change property value");
                    this._changeValueCallback(newValue);
                    this._postChangeValueCallback();
                },
                cubemapProperty.value.cubemap,
                isCurrentPropertyGlobal ? "Preview Value" : "Default Value",
                out var propertyCubemapField
            ));

            if (!isSubGraph || shaderInput.promoteToFinalShader)
            {
                ToggleDataPropertyDrawer togglePropertyDrawer = new ToggleDataPropertyDrawer();
                propertySheet.Add(togglePropertyDrawer.CreateGUI(
                    newValue =>
                    {
                        this._preChangeValueCallback("Change Is HDR");
                        if (cubemapProperty.isHDR == newValue.isOn)
                            return;
                        cubemapProperty.isHDR = newValue.isOn;
                        this._postChangeValueCallback(modificationScope: ModificationScope.Graph);
                    },
                    new ToggleData(cubemapProperty.isHDR, true),
                    "Is HDR",
                    out var isHDRToggle,
                    tooltip: kHDRTextureToggleTooltip));
            }
        }

        void HandleBooleanProperty(PropertySheet propertySheet, BooleanShaderProperty booleanProperty)
        {
            var toggleDataPropertyDrawer = new ToggleDataPropertyDrawer();
            propertySheet.Add(toggleDataPropertyDrawer.CreateGUI(
                newValue =>
                {
                    this._preChangeValueCallback("Change property value");
                    this._changeValueCallback(newValue);
                    this._postChangeValueCallback();
                },
                new ToggleData(booleanProperty.value),
                isCurrentPropertyGlobal ? "Preview Value" : "Default Value",
                out var propertyToggle));
        }

        void HandleMatrix2PropertyField(PropertySheet propertySheet, Matrix2ShaderProperty matrix2Property)
        {
            var matrixPropertyDrawer = new MatrixPropertyDrawer
            {
                dimension = MatrixPropertyDrawer.MatrixDimensions.Two,
                PreValueChangeCallback = () => this._preChangeValueCallback("Change property value"),
                PostValueChangeCallback = () => this._postChangeValueCallback(),
                MatrixRowFetchCallback = (rowNumber) => matrix2Property.value.GetRow(rowNumber)
            };

            propertySheet.Add(matrixPropertyDrawer.CreateGUI(
                newValue => { this._changeValueCallback(newValue); },
                matrix2Property.value,
                "Preview Value",
                out var propertyMatrixField));
        }

        void HandleMatrix3PropertyField(PropertySheet propertySheet, Matrix3ShaderProperty matrix3Property)
        {
            var matrixPropertyDrawer = new MatrixPropertyDrawer
            {
                dimension = MatrixPropertyDrawer.MatrixDimensions.Three,
                PreValueChangeCallback = () => this._preChangeValueCallback("Change property value"),
                PostValueChangeCallback = () => this._postChangeValueCallback(),
                MatrixRowFetchCallback = (rowNumber) => matrix3Property.value.GetRow(rowNumber)
            };

            propertySheet.Add(matrixPropertyDrawer.CreateGUI(
                newValue => { this._changeValueCallback(newValue); },
                matrix3Property.value,
                "Preview Value",
                out var propertyMatrixField));
        }

        void HandleMatrix4PropertyField(PropertySheet propertySheet, Matrix4ShaderProperty matrix4Property)
        {
            var matrixPropertyDrawer = new MatrixPropertyDrawer
            {
                dimension = MatrixPropertyDrawer.MatrixDimensions.Four,
                PreValueChangeCallback = () => this._preChangeValueCallback("Change property value"),
                PostValueChangeCallback = () => this._postChangeValueCallback(),
                MatrixRowFetchCallback = (rowNumber) => matrix4Property.value.GetRow(rowNumber)
            };

            propertySheet.Add(matrixPropertyDrawer.CreateGUI(
                newValue => { this._changeValueCallback(newValue); },
                matrix4Property.value,
                "Preview Value",
                out var propertyMatrixField));
        }

        void HandleSamplerStatePropertyField(PropertySheet propertySheet, SamplerStateShaderProperty samplerStateShaderProperty)
        {
            var enumPropertyDrawer = new EnumPropertyDrawer();

            propertySheet.Add(enumPropertyDrawer.CreateGUI(
                newValue =>
                {
                    this._preChangeValueCallback("Change property value");
                    TextureSamplerState state = samplerStateShaderProperty.value;
                    state.filter = (TextureSamplerState.FilterMode)newValue;
                    samplerStateShaderProperty.value = state;
                    this._postChangeValueCallback(false, ModificationScope.Graph);
                    this.inspectorUpdateDelegate();
                },
                samplerStateShaderProperty.value.filter,
                "Filter",
                TextureSamplerState.FilterMode.Linear,
                out var filterVisualElement));

            propertySheet.Add(enumPropertyDrawer.CreateGUI(
                newValue =>
                {
                    this._preChangeValueCallback("Change property value");
                    TextureSamplerState state = samplerStateShaderProperty.value;
                    state.wrap = (TextureSamplerState.WrapMode)newValue;
                    samplerStateShaderProperty.value = state;
                    this._postChangeValueCallback(false, ModificationScope.Graph);
                    this.inspectorUpdateDelegate();
                },
                samplerStateShaderProperty.value.wrap,
                "Wrap",
                TextureSamplerState.WrapMode.Repeat,
                out var wrapVisualElement));

            propertySheet.Add(enumPropertyDrawer.CreateGUI(
                newValue =>
                {
                    this._preChangeValueCallback("Change property value");
                    TextureSamplerState state = samplerStateShaderProperty.value;
                    state.anisotropic = (TextureSamplerState.Anisotropic)newValue;
                    samplerStateShaderProperty.value = state;
                    this._postChangeValueCallback(false, ModificationScope.Graph);
                    this.inspectorUpdateDelegate();
                },
                samplerStateShaderProperty.value.anisotropic,
                "Aniso",
                TextureSamplerState.Anisotropic.None,
                out var anisoVisualElement));
        }

        void HandleGradientPropertyField(PropertySheet propertySheet, GradientShaderProperty gradientShaderProperty)
        {
            var gradientPropertyDrawer = new GradientPropertyDrawer();
            propertySheet.Add(gradientPropertyDrawer.CreateGUI(
                newValue =>
                {
                    this._preChangeValueCallback("Change property value");
                    this._changeValueCallback(newValue);
                    this._postChangeValueCallback();
                },
                gradientShaderProperty.value,
                "Default Value",
                out var propertyGradientField));
        }

        void BuildAttributesFields(PropertySheet propertySheet)
        {
            if ((isSubGraph || isCurrentPropertyGlobal) && !shaderInput.promoteToFinalShader)
                return;

            if (shaderInput.isExposed && shaderInput is AbstractShaderProperty property)
            {
                var container = new IMGUIContainer(() => OnCustomAttributesGUIHandler()) { name = "ListContainer" };
                AddPropertyRowToSheet(propertySheet, container, "Custom Attributes", "Add Custom Attributes to be used with Custom Property Drawers.");
            }
        }

        ReorderableList m_CustomAttributesReorderableList;
        int m_CustomAttributesSelectedIndex;

        void OnCustomAttributesGUIHandler()
        {
            if (m_CustomAttributesReorderableList == null)
            {
                CustomAttributesRecreateList();
                CustomAttributesAddCallbacks();
            }

            m_CustomAttributesReorderableList.index = m_CustomAttributesSelectedIndex;
            m_CustomAttributesReorderableList.DoLayoutList();
        }

        internal void CustomAttributesRecreateList()
        {
            if (shaderInput is AbstractShaderProperty property)
            {
                // Create reorderable list from entries
                m_CustomAttributesReorderableList = new ReorderableList(property.customAttributes, typeof(Tuple<string, int>), true, true, true, true);
            }
        }

        void CustomAttributesAddCallbacks()
        {
            if (shaderInput is AbstractShaderProperty property)
            {
                // Draw Header
                m_CustomAttributesReorderableList.drawHeaderCallback = (Rect rect) =>
                {
                    int indent = 14;
                    var displayRect = new Rect(rect.x + indent, rect.y, (rect.width - indent) / 2, rect.height);
                    EditorGUI.LabelField(displayRect, "Name");
                    var referenceRect = new Rect((rect.x + indent) + (rect.width - indent) / 2, rect.y, (rect.width - indent) / 2, rect.height);
                    EditorGUI.LabelField(referenceRect, "Value");
                };

                // Draw Element
                m_CustomAttributesReorderableList.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) =>
                {
                    var entry = (AbstractShaderProperty.PropertyAttribute)m_CustomAttributesReorderableList.list[index];
                    float entryHeight = EditorGUIUtility.singleLineHeight;
                    float columnWidth = rect.width / 2;
                    float verticalPadding = (rect.height - entryHeight) / 2;
                    Rect nameDisplayRect = new Rect(rect.x, rect.y + verticalPadding, columnWidth, entryHeight);
                    Rect valueDisplayRect = new Rect(rect.x + columnWidth, rect.y + verticalPadding, columnWidth, entryHeight);

                    EditorGUI.BeginChangeCheck();
                    var name = EditorGUI.DelayedTextField(nameDisplayRect, entry.name, EditorStyles.label);
                    var value = EditorGUI.DelayedTextField(valueDisplayRect, entry.value);

                    if (EditorGUI.EndChangeCheck())
                    {
                        this._preChangeValueCallback("Edit Custom Attribute Entry");
                        name = GetSanitizedReferenceName(name);
                        if (string.IsNullOrWhiteSpace(name))
                            Debug.LogWarning("Invalid Attribute name. Attribute names cannot be empty or all whitespace.");
                        else if (int.TryParse(name, out int intVal) || float.TryParse(name, out float floatVal))
                            Debug.LogWarning("Invalid Attribute name. Attribute names cannot be valid integer or floating point numbers.");

                        value = GetSanitizedAttributeValue(value);
                        property.customAttributes[index].name = name;
                        property.customAttributes[index].value = value;

                        this._postChangeValueCallback(true);
                    }
                };

                // Add
                m_CustomAttributesReorderableList.onAddCallback = (ReorderableList list) =>
                {
                    this._preChangeValueCallback("Add Custom Attribute");

                    // Add new entry
                    property.customAttributes.Add(new("MyCustomAttribute", string.Empty));

                    // Update GUI
                    this._postChangeValueCallback(true);
                    m_CustomAttributesSelectedIndex = list.list.Count - 1;
                };

                // Remove
                m_CustomAttributesReorderableList.onRemoveCallback = (ReorderableList list) =>
                {
                    this._preChangeValueCallback("Remove Custom Attribute");

                    // Remove entry
                    property.customAttributes.RemoveAt(list.index);

                    // Rebuild();
                    this._postChangeValueCallback(true);
                    m_CustomAttributesSelectedIndex = m_CustomAttributesSelectedIndex >= list.list.Count - 1 ? list.list.Count - 1 : m_CustomAttributesSelectedIndex;
                };

                // Element height
                m_CustomAttributesReorderableList.elementHeightCallback = (int indexer) =>
                {
                    return m_CustomAttributesReorderableList.elementHeight;
                };

                // Can add
                m_CustomAttributesReorderableList.onCanAddCallback = (ReorderableList list) =>
                {
                    return list.count < 8;
                };

                // Can remove
                m_CustomAttributesReorderableList.onCanRemoveCallback = (ReorderableList list) => true;

                // Select
                m_CustomAttributesReorderableList.onSelectCallback = (ReorderableList list) =>
                {
                    m_CustomAttributesSelectedIndex = list.index;
                };

                // Reorder
                m_CustomAttributesReorderableList.onReorderCallbackWithDetails = (ReorderableList list, int oldIndex, int newIndex) =>
                {
                    this._preChangeValueCallback("Reordered Custom Attributes");
                    this._postChangeValueCallback(true);
                };
            }
        }

        enum KeywordShaderStageDropdownUI    // maps to KeywordShaderStage, this enum ONLY used for the UI dropdown menu
        {
            All = KeywordShaderStage.All,
            Vertex = KeywordShaderStage.Vertex,
            Fragment = KeywordShaderStage.Fragment,
        }

        void BuildKeywordFields(PropertySheet propertySheet, ShaderInput shaderInput)
        {
            var keyword = shaderInput as ShaderKeyword;
            if (keyword == null)
                return;

            var enumPropertyDrawer = new EnumPropertyDrawer();
            propertySheet.Add(enumPropertyDrawer.CreateGUI(
                newValue =>
                {
                    this._preChangeValueCallback("Change Keyword type");
                    if (keyword.keywordDefinition == (KeywordDefinition)newValue)
                        return;
                    keyword.keywordDefinition = (KeywordDefinition)newValue;
                    UpdateEnableState();
                    this._postChangeValueCallback(true);
                    this._keywordChangedCallback();
                },
                keyword.keywordDefinition,
                "Definition",
                KeywordDefinition.ShaderFeature,
                out var typeField,
                tooltip: "Indicate how the keyword is defined and under what circumstances its permutations will be compiled."));

            if (keyword.keywordDefinition == KeywordDefinition.ShaderFeature && isSubGraph && !keyword.promoteToFinalShader)
            {
                var help = new HelpBoxRow("Shader Feature Keywords in SubGraphs do not generate variant permutations.", MessageType.Info);
                propertySheet.Add(help);
            }

            var isShaderBuildSettingsCompatibleDrawer = new ToggleDataPropertyDrawer();
            propertySheet.Add(isShaderBuildSettingsCompatibleDrawer.CreateGUI(
                newValue =>
                {
                    this._preChangeValueCallback("Change Keyword Allow Definition Override");
                    keyword.IsShaderBuildSettingsCompatible = newValue.isOn;
                    this._postChangeValueCallback(modificationScope: ModificationScope.Graph);
                },
                new ToggleData(keyword.IsShaderBuildSettingsCompatible),
                "Allow Definition Override",
                out VisualElement isShaderBuildSettingsCompatibleToggle,
                tooltip: "Indicates whether this keyword's definition can be overridden by the project's shader build settings."));

            typeField.SetEnabled(!keyword.isBuiltIn);
            {
                var isOverridablePropertyDrawer = new ToggleDataPropertyDrawer();
                bool enabledState = keyword.keywordDefinition != KeywordDefinition.Predefined;
                bool toggleState = keyword.keywordScope == KeywordScope.Global || !enabledState;
                propertySheet.Add(isOverridablePropertyDrawer.CreateGUI(
                    newValue =>
                    {
                        this._preChangeValueCallback("Change Keyword Allow State Override");
                        keyword.keywordScope = newValue.isOn
                            ? KeywordScope.Global
                            : KeywordScope.Local;
                    },
                    new ToggleData(toggleState),
                    "Allow State Override",
                    out keywordScopeField,
                    tooltip: "Indicates whether this keyword's state can be overridden through the Shader.SetKeyword scripting interface."));
                keywordScopeField.SetEnabled(enabledState);
            }
            BuildExposedField(propertySheet);
            {
                propertySheet.Add(enumPropertyDrawer.CreateGUI(
                    newValue =>
                    {
                        this._preChangeValueCallback("Change Keyword stage");
                        if (keyword.keywordStages == (KeywordShaderStage)newValue)
                            return;
                        keyword.keywordStages = (KeywordShaderStage)newValue;
                    },
                    (KeywordShaderStageDropdownUI)keyword.keywordStages,
                    "Stages",
                    KeywordShaderStageDropdownUI.All,
                    out keywordScopeField,
                    tooltip: "Indicates which shader stages this keyword is relevant for."));
            }

            switch (keyword.keywordType)
            {
                case KeywordType.Boolean:
                    BuildBooleanKeywordField(propertySheet, keyword);
                    break;
                case KeywordType.Enum:
                    BuildEnumKeywordField(propertySheet, keyword);
                    break;
            }
        }

        void BuildBooleanKeywordField(PropertySheet propertySheet, ShaderKeyword keyword)
        {
            var toggleDataPropertyDrawer = new ToggleDataPropertyDrawer();
            propertySheet.Add(toggleDataPropertyDrawer.CreateGUI(
                newValue =>
                {
                    this._preChangeValueCallback("Change property value");
                    keyword.value = newValue.isOn ? 1 : 0;
                    if (graphData.owner.materialArtifact)
                    {
                        graphData.owner.materialArtifact.SetFloat(keyword.referenceName, keyword.value);
                        MaterialEditor.ApplyMaterialPropertyDrawers(graphData.owner.materialArtifact);
                    }
                    this._postChangeValueCallback(false, ModificationScope.Graph);
                },
                new ToggleData(keyword.value == 1),
                keyword.keywordDefinition == KeywordDefinition.Predefined ? "Preview Value" : "Default Value",
                out var boolKeywordField));
        }

        void BuildEnumKeywordField(PropertySheet propertySheet, ShaderKeyword keyword)
        {
            // Clamp value between entry list
            int value = Mathf.Clamp(keyword.value, 0, keyword.entries.Count - 1);

            // Include "none" entry
            var includeNoneDrawer = new ToggleDataPropertyDrawer();
            propertySheet.Add(includeNoneDrawer.CreateGUI(
                (newValue) =>
                {
                    this._preChangeValueCallback("Change Include None Value");
                    if (newValue.isOn)
                        keyword.entries.Insert(0, new KeywordEntry(GetFirstUnusedKeywordID(), "None", string.Empty));
                    else
                        keyword.entries.RemoveAll(entry => entry.IsNoneKeyword);
                    this._postChangeValueCallback();
                    this._keywordChangedCallback();
                },
                new ToggleData(keyword.HasNoneEntry),
                "Include \"none\" entry",
                out VisualElement includeNoneToggle,
                tooltip: "Indicates whether the enum can assume a \"none\" value as indicated by the \"_\" keyword."));

            // Default field
            var field = new PopupField<string>(keyword.entries.Select(x => x.displayName).ToList(), value);
            field.RegisterValueChangedCallback(evt =>
            {
                this._preChangeValueCallback("Change Keyword Value");
                keyword.value = field.index;
                if (graphData.owner.materialArtifact)
                {
                    graphData.owner.materialArtifact.SetFloat(keyword.referenceName, field.index);
                    MaterialEditor.ApplyMaterialPropertyDrawers(graphData.owner.materialArtifact);
                }
                this._postChangeValueCallback(false, ModificationScope.Graph);
            });

            AddPropertyRowToSheet(propertySheet, field, keyword.keywordDefinition == KeywordDefinition.Predefined ? "Preview Value" : "Default Value");

            var container = new IMGUIContainer(() => OnKeywordGUIHandler()) { name = "ListContainer" };
            AddPropertyRowToSheet(propertySheet, container, "Entries");
            container.SetEnabled(!keyword.isBuiltIn);
        }

        static void AddPropertyRowToSheet(PropertySheet propertySheet, VisualElement control, string labelName, string tooltip = default)
        {
            propertySheet.Add(new PropertyRow(new Label(labelName)), (row) =>
            {
                row.styleSheets.Add(Resources.Load<StyleSheet>("Styles/PropertyRow"));
                row.Add(control);
                row.tooltip = tooltip;
            });
        }

        ReorderableList m_EnumReorderableList;
        int m_EnumSelectedIndex;

        void OnEnumGUIHandler()
        {
            if (m_EnumReorderableList == null)
            {
                EnumRecreateList();
                EnumAddCallbacks();
            }

            m_EnumReorderableList.index = m_EnumSelectedIndex;
            m_EnumReorderableList.DoLayoutList();
        }

        void CheckEnumNamesAndValuesListLength(Vector1ShaderProperty vector1ShaderProperty)
        {
            var diff = vector1ShaderProperty.enumNames.Count - vector1ShaderProperty.enumValues.Count;
            if (diff == 0)
                return;
            if (diff > 0) // missing values
            {
                for (int i = 0; i < diff; i++)
                    vector1ShaderProperty.enumValues.Add(0);
            }
            else // missing names
            {
                for (int i = 0; i < -diff; i++)
                    vector1ShaderProperty.enumNames.Add("_");
            }
        }

        internal void EnumRecreateList()
        {
            if (shaderInput is Vector1ShaderProperty vector1Property && vector1Property.floatType == FloatType.Enum && vector1Property.enumType == EnumType.Enum)
            {
                // Create a virtual list of names and values
                List<(string name, int value)> list = new();
                CheckEnumNamesAndValuesListLength(vector1Property);
                for (int i = 0; i < vector1Property.enumNames.Count; i++)
                {
                    var name = vector1Property.enumNames[i];
                    var value = vector1Property.enumValues[i];
                    list.Add(new(name, value));
                }
                // Create reorderable list from entries
                m_EnumReorderableList = new ReorderableList(list, typeof(Tuple<string, int>), true, true, true, true);
            }
        }

        void EnumAddCallbacks()
        {
            if (shaderInput is Vector1ShaderProperty vector1Property && vector1Property.floatType == FloatType.Enum && vector1Property.enumType == EnumType.Enum)
            {
                // Draw Header
                m_EnumReorderableList.drawHeaderCallback = (Rect rect) =>
                {
                    int indent = 14;
                    var displayRect = new Rect(rect.x + indent, rect.y, (rect.width - indent) / 2, rect.height);
                    EditorGUI.LabelField(displayRect, "Name");
                    var referenceRect = new Rect((rect.x + indent) + (rect.width - indent) / 2, rect.y, (rect.width - indent) / 2, rect.height);
                    EditorGUI.LabelField(referenceRect, "Value");
                };

                // Draw Element
                m_EnumReorderableList.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) =>
                {
                    (string name, int value) entry = ((string name, int value))m_EnumReorderableList.list[index];
                    Rect displayRect = new Rect(rect.x, rect.y, rect.width / 2, EditorGUIUtility.singleLineHeight);

                    EditorGUI.BeginChangeCheck();
                    var name = EditorGUI.DelayedTextField(displayRect, entry.name, EditorStyles.label);
                    var value = EditorGUI.IntField(new Rect(rect.x + rect.width / 2, rect.y, rect.width / 2, EditorGUIUtility.singleLineHeight), entry.value);

                    if (EditorGUI.EndChangeCheck())
                    {
                        this._preChangeValueCallback("Edit Enum Entry");
                        name = GraphUtil.SanitizeName(vector1Property.enumNames, "{0} {1}", name, m_DisplayNameDisallowedPattern);
                        if (string.IsNullOrWhiteSpace(name))
                            Debug.LogWarning("Invalid display name. Display names cannot be empty or all whitespace.");
                        else if (int.TryParse(name, out int intVal) || float.TryParse(name, out float floatVal))
                            Debug.LogWarning("Invalid display name. Display names cannot be valid integer or floating point numbers.");

                        vector1Property.enumNames[index] = name;
                        vector1Property.enumValues[index] = value;

                        this._postChangeValueCallback(true);
                    }
                };

                // Add
                m_EnumReorderableList.onAddCallback = (ReorderableList list) =>
                {
                    this._preChangeValueCallback("Add Enum Entry");

                    var name = "New";
                    name = GraphUtil.SanitizeName(vector1Property.enumNames, "{0} {1}", name, m_DisplayNameDisallowedPattern);
                    var value = FindSmallestMissingPositive(vector1Property.enumValues);

                    // Add new entry
                    vector1Property.enumNames.Add(name);
                    vector1Property.enumValues.Add(value);

                    // Update GUI
                    this._postChangeValueCallback(true);
                    m_EnumSelectedIndex = list.count - 1;
                };

                // Remove
                m_EnumReorderableList.onRemoveCallback = (ReorderableList list) =>
                {
                    this._preChangeValueCallback("Remove Keyword Entry");

                    // Remove entry
                    vector1Property.enumNames.RemoveAt(list.index);
                    vector1Property.enumValues.RemoveAt(list.index);

                    // Rebuild();
                    this._postChangeValueCallback(true);
                    m_EnumSelectedIndex = m_EnumSelectedIndex >= list.list.Count - 1 ? list.list.Count - 1 : m_EnumSelectedIndex;
                };

                // Element height
                m_EnumReorderableList.elementHeightCallback = (int indexer) =>
                {
                    return m_EnumReorderableList.elementHeight;
                };

                // Can add
                m_EnumReorderableList.onCanAddCallback = (ReorderableList list) =>
                {
                    return list.count < 7;
                };

                // Can remove
                m_EnumReorderableList.onCanRemoveCallback = (ReorderableList list) =>
                {
                    return list.count > 1;
                };

                // Select
                m_EnumReorderableList.onSelectCallback = (ReorderableList list) =>
                {
                    m_EnumSelectedIndex = list.index;
                };

                // Reorder
                m_EnumReorderableList.onReorderCallbackWithDetails = (ReorderableList list, int oldIndex, int newIndex) =>
                {
                    this._preChangeValueCallback("Reordered Enum Entries");
                    var name = vector1Property.enumNames[oldIndex];
                    var value = vector1Property.enumValues[oldIndex];
                    vector1Property.enumNames.RemoveAt(oldIndex);
                    vector1Property.enumValues.RemoveAt(oldIndex);
                    vector1Property.enumNames.Insert(newIndex, name);
                    vector1Property.enumValues.Insert(newIndex, value);
                    this._postChangeValueCallback(true);
                };
            }
        }

        static int FindSmallestMissingPositive(List<int> nums)
        {
            HashSet<int> positiveNumbers = new HashSet<int>(nums.Where(x => x >= 0));

            int smallestMissing = 0;
            while (positiveNumbers.Contains(smallestMissing))
            {
                smallestMissing++;
            }

            return smallestMissing;
        }

        void OnKeywordGUIHandler()
        {
            if (m_KeywordReorderableList == null)
            {
                KeywordRecreateList();
                KeywordAddCallbacks();
            }

            m_KeywordReorderableList.index = m_KeywordSelectedIndex;
            m_KeywordReorderableList.DoLayoutList();
        }

        internal void KeywordRecreateList()
        {
            if (!(shaderInput is ShaderKeyword keyword))
                return;

            // Create reorderable list from entries
            m_KeywordReorderableList = new ReorderableList(keyword.entries, typeof(KeywordEntry), true, true, true, true);
        }

        void KeywordAddCallbacks()
        {
            if (!(shaderInput is ShaderKeyword keyword))
                return;

            // Draw Header
            m_KeywordReorderableList.drawHeaderCallback = (Rect rect) =>
            {
                int indent = 14;
                var displayRect = new Rect(rect.x + indent, rect.y, (rect.width - indent) / 2, rect.height);
                EditorGUI.LabelField(displayRect, "Entry Name");
                var referenceRect = new Rect((rect.x + indent) + (rect.width - indent) / 2, rect.y, (rect.width - indent) / 2, rect.height);
                EditorGUI.LabelField(referenceRect, "Reference Suffix", keyword.isBuiltIn ? EditorStyles.label : greyLabel);
            };

            // Draw Element
            m_KeywordReorderableList.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) =>
            {
                KeywordEntry entry = ((KeywordEntry)m_KeywordReorderableList.list[index]);
                EditorGUI.BeginChangeCheck();
                EditorGUI.BeginDisabled(entry.IsNoneKeyword);

                Rect displayRect = new Rect(rect.x, rect.y, rect.width / 2, EditorGUIUtility.singleLineHeight);
                var displayName = EditorGUI.DelayedTextField(displayRect, entry.displayName, EditorStyles.label);
                //This is gross but I cant find any other way to make a DelayedTextField have a tooltip (tried doing the empty label on the field itself and it didnt work either)
                EditorGUI.LabelField(displayRect, new GUIContent("", "Enum keyword display names can only use alphanumeric characters, whitespace and `_`"));
                var referenceName = EditorGUI.TextField(new Rect(rect.x + rect.width / 2, rect.y, rect.width / 2, EditorGUIUtility.singleLineHeight), entry.referenceName,
                    keyword.isBuiltIn ? EditorStyles.label : greyLabel);

                EditorGUI.EndDisabled();
                if (EditorGUI.EndChangeCheck())
                {
                    this._preChangeValueCallback("Edit Enum Keyword Entry");

                    displayName = GetSanitizedDisplayName(displayName);
                    referenceName = GetSanitizedReferenceName(displayName.ToUpper());
                    var duplicateIndex = FindDuplicateKeywordReferenceNameIndex(entry.id, referenceName);
                    if (duplicateIndex != -1)
                    {
                        var duplicateEntry = ((KeywordEntry)m_KeywordReorderableList.list[duplicateIndex]);
                        Debug.LogWarning($"Display name '{displayName}' will create the same reference name '{referenceName}' as entry {duplicateIndex + 1} with display name '{duplicateEntry.displayName}'.");
                    }
                    else if (string.IsNullOrWhiteSpace(displayName))
                        Debug.LogWarning("Invalid display name. Display names cannot be empty or all whitespace.");
                    else if (int.TryParse(displayName, out int intVal) || float.TryParse(displayName, out float floatVal))
                        Debug.LogWarning("Invalid display name. Display names cannot be valid integer or floating point numbers.");
                    else
                        keyword.entries[index] = new KeywordEntry(entry.id, displayName, referenceName);

                    this._postChangeValueCallback(true);
                    this._keywordChangedCallback();
                }
            };

            // Element height
            m_KeywordReorderableList.elementHeightCallback = (int indexer) =>
            {
                return m_KeywordReorderableList.elementHeight;
            };

            // Can add
            m_KeywordReorderableList.onCanAddCallback = (ReorderableList list) =>
            {
                return list.count < 8;
            };

            // Can remove
            m_KeywordReorderableList.onCanRemoveCallback = (ReorderableList list) =>
            {
                return list.count > 2;
            };

            // Add callback delegates
            m_KeywordReorderableList.onSelectCallback += KeywordSelectEntry;
            m_KeywordReorderableList.onAddCallback += KeywordAddEntry;
            m_KeywordReorderableList.onRemoveCallback += KeywordRemoveEntry;
            m_KeywordReorderableList.onReorderCallback += KeywordReorderEntries;
        }

        void KeywordSelectEntry(ReorderableList list)
        {
            m_KeywordSelectedIndex = list.index;
        }

        // Allowed indicies are 1-MAX_ENUM_ENTRIES
        int GetFirstUnusedKeywordID()
        {
            if (!(shaderInput is ShaderKeyword keyword))
                return 0;

            List<int> unusedIDs = new List<int>();

            foreach (KeywordEntry keywordEntry in keyword.entries)
            {
                unusedIDs.Add(keywordEntry.id);
            }

            for (int x = 1; x <= KeywordNode.k_MaxEnumEntries; x++)
            {
                if (!unusedIDs.Contains(x))
                    return x;
            }

            Debug.LogError("GetFirstUnusedID: Attempting to get unused ID when all IDs are used.");
            return -1;
        }

        void KeywordAddEntry(ReorderableList list)
        {
            if (!(shaderInput is ShaderKeyword keyword))
                return;

            this._preChangeValueCallback("Add Keyword Entry");

            int index = GetFirstUnusedKeywordID();
            if (index <= 0)
                return; // Error has already occured, don't attempt to add this entry.

            var displayName = "New";
            var referenceName = "NEW";
            GetDuplicateSafeEnumNames(index, "New", out displayName, out referenceName);

            // Add new entry
            keyword.entries.Add(new KeywordEntry(index, displayName, referenceName));

            // Update GUI
            this._postChangeValueCallback(true);
            this._keywordChangedCallback();
            m_KeywordSelectedIndex = list.list.Count - 1;
        }

        void KeywordRemoveEntry(ReorderableList list)
        {
            if (!(shaderInput is ShaderKeyword keyword))
                return;

            this._preChangeValueCallback("Remove Keyword Entry");

            // Remove entry
            m_KeywordSelectedIndex = list.index;
            var selectedEntry = (KeywordEntry)m_KeywordReorderableList.list[list.index];
            keyword.entries.Remove(selectedEntry);

            // Clamp value within new entry range
            int value = Mathf.Clamp(keyword.value, 0, keyword.entries.Count - 1);
            keyword.value = value;

            // Rebuild();
            this._postChangeValueCallback(true);
            this._keywordChangedCallback();
            m_KeywordSelectedIndex = m_KeywordSelectedIndex >= list.list.Count - 1 ? list.list.Count - 1 : m_KeywordSelectedIndex;
        }

        void KeywordReorderEntries(ReorderableList list)
        {
            if (shaderInput is not ShaderKeyword keyword)
                return;

            // Ensure that the "none" entry is always at position 0
            int noneEntryIndex = 0;
            for (; noneEntryIndex < list.list.Count; ++noneEntryIndex)
            {
                KeywordEntry entry = (KeywordEntry)list.list[noneEntryIndex];
                if (entry.IsNoneKeyword)
                    break;
            }
            if (0 < noneEntryIndex && noneEntryIndex < list.list.Count)
            {
                object noneEntry = list.list[noneEntryIndex];
                list.list.RemoveAt(noneEntryIndex);
                list.list.Insert(0, noneEntry);
            }

            this._preChangeValueCallback("Reorder Keyword Entry");
            this._postChangeValueCallback(true);
            this._keywordChangedCallback();

        }

        public string GetDuplicateSafeEnumDisplayName(int id, string name)
        {
            name = name.Trim();
            var entryList = m_KeywordReorderableList.list as List<KeywordEntry>;
            return GraphUtil.SanitizeName(entryList.Where(p => p.id != id).Select(p => p.displayName), "{0} {1}", name, m_DisplayNameDisallowedPattern);
        }

        void GetDuplicateSafeEnumNames(int id, string name, out string displayName, out string referenceName)
        {
            name = name.Trim();
            // Get de-duplicated display and reference names
            displayName = GetDuplicateSafeEnumDisplayName(id, name);
            referenceName = GetDuplicateSafeReferenceName(id, displayName.ToUpper());
            // Check when the simple reference name should be for the display name.
            // If these don't match then there will be a desync which causes the enum entry to not work.
            // An example where this happens is ["new 1", "NEW_1"] already exists.
            // The display name "New_1" is added.
            // This new display name doesn't exist, but it finds the reference name of "NEW_1" already exists so we get the pair ["New_1", "NEW_2"] which is invalid.
            // The easiest fix in this case is to just use the safe reference name as the new display name which is guaranteed to be unique.
            var simpleReferenceName = Regex.Replace(displayName.ToUpper(), m_ReferenceNameDisallowedPattern, "_");
            if (referenceName != simpleReferenceName)
                displayName = referenceName;
        }

        string GetSanitizedDisplayName(string name)
        {
            name = name.Trim();
            return Regex.Replace(name, m_DisplayNameDisallowedPattern, "_");
        }

        public string GetDuplicateSafeReferenceName(int id, string name)
        {
            name = name.Trim();
            var entryList = m_KeywordReorderableList.list as List<KeywordEntry>;
            return GraphUtil.SanitizeName(entryList.Where(p => p.id != id).Select(p => p.referenceName), "{0}_{1}", name, m_ReferenceNameDisallowedPattern);
        }

        string GetSanitizedReferenceName(string name)
        {
            name = name.Trim();
            return Regex.Replace(name, m_ReferenceNameDisallowedPattern, "_");
        }

        string GetSanitizedEnumRefName(string value)
        {
            value = value.Trim();
            return Regex.Replace(value, m_EnumRefDisallowedPattern, "_");
        }

        string GetSanitizedAttributeValue(string value)
        {
            var values = value.Split(',', StringSplitOptions.RemoveEmptyEntries);
            if (values.Length == 0)
            {
                return string.Empty;
            }
            else
            {
                for (int i = 0; i < values.Length; i++)
                {
                    values[i] = Regex.Replace(values[i], m_AttributeValueDisallowedPattern, "_").Trim();
                }
                return string.Join(", ", values);
            }
        }

        int FindDuplicateKeywordReferenceNameIndex(int id, string referenceName)
        {
            var entryList = m_KeywordReorderableList.list as List<KeywordEntry>;
            return entryList.FindIndex(entry => entry.id != id && entry.referenceName == referenceName);
        }

        void BuildDropdownFields(PropertySheet propertySheet, ShaderInput shaderInput)
        {
            var dropdown = shaderInput as ShaderDropdown;
            if (dropdown == null)
                return;

            BuildDropdownField(propertySheet, dropdown);
        }

        void BuildDropdownField(PropertySheet propertySheet, ShaderDropdown dropdown)
        {
            // Clamp value between entry list
            int value = Mathf.Clamp(dropdown.value, 0, dropdown.entries.Count - 1);

            // Default field
            var field = new PopupField<string>(dropdown.entries.Select(x => x.displayName).ToList(), value);
            field.RegisterValueChangedCallback(evt =>
            {
                this._preChangeValueCallback("Change Dropdown Value");
                dropdown.value = field.index;
                m_DropdownId = dropdown.entryId;
                this._postChangeValueCallback(false, ModificationScope.Graph);
            });

            AddPropertyRowToSheet(propertySheet, field, "Default");

            var container = new IMGUIContainer(() => OnDropdownGUIHandler()) { name = "ListContainer" };
            AddPropertyRowToSheet(propertySheet, container, "Entries");
            container.SetEnabled(true);
        }

        void OnDropdownGUIHandler()
        {
            if (m_DropdownReorderableList == null)
            {
                DropdownRecreateList();
                DropdownAddCallbacks();
            }

            m_DropdownReorderableList.index = m_DropdownSelectedIndex;
            m_DropdownReorderableList.DoLayoutList();
        }

        internal void DropdownRecreateList()
        {
            if (!(shaderInput is ShaderDropdown dropdown))
                return;

            // Create reorderable list from entries
            m_DropdownReorderableList = new ReorderableList(dropdown.entries, typeof(DropdownEntry), true, true, true, true);
            m_Dropdown = dropdown;
            m_DropdownId = dropdown.entryId;
        }

        void DropdownAddCallbacks()
        {
            if (!(shaderInput is ShaderDropdown dropdown))
                return;

            // Draw Header
            m_DropdownReorderableList.drawHeaderCallback = (Rect rect) =>
            {
                int indent = 14;
                var displayRect = new Rect(rect.x + indent, rect.y, (rect.width - indent) / 2, rect.height);
                EditorGUI.LabelField(displayRect, "Entry Name");
            };

            // Draw Element
            m_DropdownReorderableList.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) =>
            {
                DropdownEntry entry = ((DropdownEntry)m_DropdownReorderableList.list[index]);
                EditorGUI.BeginChangeCheck();

                Rect displayRect = new Rect(rect.x, rect.y, rect.width / 2, EditorGUIUtility.singleLineHeight);
                var displayName = EditorGUI.DelayedTextField(displayRect, entry.displayName, EditorStyles.label);

                if (EditorGUI.EndChangeCheck())
                {
                    displayName = GetSanitizedDisplayName(displayName);
                    var duplicateIndex = FindDuplicateDropdownDisplayNameIndex(entry.id, displayName);
                    if (duplicateIndex != -1)
                    {
                        var duplicateEntry = ((DropdownEntry)m_DropdownReorderableList.list[duplicateIndex]);
                        Debug.LogWarning($"Display name '{displayName}' will create the same display name as entry {duplicateIndex + 1}.");
                    }
                    else if (string.IsNullOrWhiteSpace(displayName))
                        Debug.LogWarning("Invalid display name. Display names cannot be empty or all whitespace.");
                    else if (int.TryParse(displayName, out int intVal) || float.TryParse(displayName, out float floatVal))
                        Debug.LogWarning("Invalid display name. Display names cannot be valid integer or floating point numbers.");
                    else
                        dropdown.entries[index] = new DropdownEntry(GetFirstUnusedDropdownID(), displayName);

                    this._postChangeValueCallback(true);
                }
            };

            // Element height
            m_DropdownReorderableList.elementHeightCallback = (int indexer) =>
            {
                return m_DropdownReorderableList.elementHeight;
            };

            // Can add
            m_DropdownReorderableList.onCanAddCallback = (ReorderableList list) =>
            {
                return true;
            };

            // Can remove
            m_DropdownReorderableList.onCanRemoveCallback = (ReorderableList list) =>
            {
                return list.count > DropdownNode.k_MinEnumEntries;
            };

            // Add callback delegates
            m_DropdownReorderableList.onSelectCallback += DropdownSelectEntry;
            m_DropdownReorderableList.onAddCallback += DropdownAddEntry;
            m_DropdownReorderableList.onRemoveCallback += DropdownRemoveEntry;
            m_DropdownReorderableList.onReorderCallback += DropdownReorderEntries;
        }

        void DropdownSelectEntry(ReorderableList list)
        {
            m_DropdownSelectedIndex = list.index;
        }

        int GetFirstUnusedDropdownID()
        {
            if (!(shaderInput is ShaderDropdown dropdown))
                return 0;

            List<int> ids = new List<int>();

            foreach (DropdownEntry dropdownEntry in dropdown.entries)
            {
                ids.Add(dropdownEntry.id);
            }

            for (int x = 1; ; x++)
            {
                if (!ids.Contains(x))
                    return x;
            }
        }

        void DropdownAddEntry(ReorderableList list)
        {
            if (!(shaderInput is ShaderDropdown dropdown))
                return;

            this._preChangeValueCallback("Add Dropdown Entry");

            int index = GetFirstUnusedDropdownID();

            var displayName = GetDuplicateSafeDropdownDisplayName(index, "New");

            // Add new entry
            dropdown.entries.Add(new DropdownEntry(index, displayName));

            // Update GUI
            this._postChangeValueCallback(true);
            this._dropdownChangedCallback();
            m_DropdownSelectedIndex = list.list.Count - 1;
        }

        void DropdownRemoveEntry(ReorderableList list)
        {
            if (!(shaderInput is ShaderDropdown dropdown))
                return;

            this._preChangeValueCallback("Remove Dropdown Entry");

            // Remove entry
            m_DropdownSelectedIndex = list.index;
            var selectedEntry = (DropdownEntry)m_DropdownReorderableList.list[list.index];
            dropdown.entries.Remove(selectedEntry);

            // Clamp value within new entry range
            int value = Mathf.Clamp(dropdown.value, 0, dropdown.entries.Count - 1);
            dropdown.value = value;

            this._postChangeValueCallback(true);
            this._dropdownChangedCallback();
            m_DropdownSelectedIndex = m_DropdownSelectedIndex >= list.list.Count - 1 ? list.list.Count - 1 : m_DropdownSelectedIndex;
        }

        void DropdownReorderEntries(ReorderableList list)
        {
            var index = m_Dropdown.IndexOfId(m_DropdownId);
            if (index != m_Dropdown.value)
                m_Dropdown.value = index;
            this._postChangeValueCallback(true);
        }

        public string GetDuplicateSafeDropdownDisplayName(int id, string name)
        {
            var entryList = m_DropdownReorderableList.list as List<DropdownEntry>;
            return GraphUtil.SanitizeName(entryList.Where(p => p.id != id).Select(p => p.displayName), "{0} {1}", name, m_DisplayNameDisallowedPattern);
        }

        int FindDuplicateDropdownDisplayNameIndex(int id, string displayName)
        {
            var entryList = m_DropdownReorderableList.list as List<DropdownEntry>;
            return entryList.FindIndex(entry => entry.id != id && entry.displayName == displayName);
        }
    }
}