using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEditor.Graphing;
using UnityEditor.Graphing.Util;
using UnityEngine.Assertions;
using UnityEngine.Rendering;
using UnityEditor.ShaderGraph.Internal;
using Debug = UnityEngine.Debug;
using Object = UnityEngine.Object;
using Unity.Profiling;


namespace UnityEditor.ShaderGraph.Drawing
{
    delegate void OnPrimaryMasterChanged();

    class PreviewManager : IDisposable
    {
        GraphData m_Graph;
        MessageManager m_Messenger;

        MaterialPropertyBlock m_SharedPreviewPropertyBlock;         // stores preview properties (shared among ALL preview nodes)

        Dictionary<string, PreviewRenderData> m_RenderDatas = new Dictionary<string, PreviewRenderData>();  // stores all of the PreviewRendererData, mapped by node object ID
        PreviewRenderData m_MasterRenderData;                                                               // cache ref to preview renderer data for the master node

        int m_MaxPreviewsCompiling = 2;                                                                     // max preview shaders we want to async compile at once

        // state trackers
        HashSet<AbstractMaterialNode> m_NodesShaderChanged = new HashSet<AbstractMaterialNode>();           // nodes whose shader code has changed, this node and nodes that read from it are put into NeedRecompile
        HashSet<AbstractMaterialNode> m_NodesPropertyChanged = new HashSet<AbstractMaterialNode>();         // nodes whose property values have changed, the properties will need to be updated and all nodes that use that property re-rendered

        HashSet<PreviewRenderData> m_PreviewsNeedsRecompile = new HashSet<PreviewRenderData>();             // previews we need to recompile the preview shader
        HashSet<PreviewRenderData> m_PreviewsCompiling = new HashSet<PreviewRenderData>();                  // previews currently being compiled
        HashSet<PreviewRenderData> m_PreviewsToDraw = new HashSet<PreviewRenderData>();                     // previews to re-render the texture (either because shader compile changed or property changed)
        HashSet<PreviewRenderData> m_TimedPreviews = new HashSet<PreviewRenderData>();                      // previews that are dependent on a time node -- i.e. animated / need to redraw every frame

        bool m_TopologyDirty;                                                                               // indicates topology changed, used to rebuild timed node list and preview type (2D/3D) inheritance.

        HashSet<BlockNode> m_MasterNodePreviewBlocks = new HashSet<BlockNode>();                            // all blocks used for the most recent master node preview generation. this includes temporary blocks.

        PreviewSceneResources m_SceneResources;
        Texture2D m_ErrorTexture;
        Vector2? m_NewMasterPreviewSize;

        const AbstractMaterialNode kMasterProxyNode = null;

        public PreviewRenderData masterRenderData
        {
            get { return m_MasterRenderData; }
        }

        public PreviewManager(GraphData graph, MessageManager messenger)
        {
            m_SharedPreviewPropertyBlock = new MaterialPropertyBlock();
            m_Graph = graph;
            m_Messenger = messenger;
            m_ErrorTexture = GenerateFourSquare(Color.magenta, Color.black);
            m_SceneResources = new PreviewSceneResources();

            foreach (var node in m_Graph.GetNodes<AbstractMaterialNode>())
                AddPreview(node);

            AddMasterPreview();
        }

        static Texture2D GenerateFourSquare(Color c1, Color c2)
        {
            var tex = new Texture2D(2, 2);
            tex.SetPixel(0, 0, c1);
            tex.SetPixel(0, 1, c2);
            tex.SetPixel(1, 0, c2);
            tex.SetPixel(1, 1, c1);
            tex.filterMode = FilterMode.Point;
            tex.Apply();
            return tex;
        }

        public void ResizeMasterPreview(Vector2 newSize)
        {
            m_NewMasterPreviewSize = newSize;
        }

        public PreviewRenderData GetPreviewRenderData(AbstractMaterialNode node)
        {
            PreviewRenderData result = null;
            if (node == kMasterProxyNode ||
                node is BlockNode ||
                node == m_Graph.outputNode) // the outputNode, if it exists, is mapped to master
            {
                result = m_MasterRenderData;
            }
            else
            {
                m_RenderDatas.TryGetValue(node.objectId, out result);
            }
            
            return result;
        }

        void AddMasterPreview()
        {
            m_MasterRenderData = new PreviewRenderData
            {
                previewName = "Master Preview",
                renderTexture =
                    new RenderTexture(400, 400, 16, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Default)
                    {
                        hideFlags = HideFlags.HideAndDontSave
                    },
                previewMode = PreviewMode.Preview3D,
            };

            m_MasterRenderData.renderTexture.Create();

            var shaderData = new PreviewShaderData
            {
                // even though a SubGraphOutputNode can be directly mapped to master (via m_Graph.outputNode)
                // we always keep master node associated with kMasterProxyNode instead
                // just easier if the association is always dynamic
                node = kMasterProxyNode,
                passesCompiling = 0,
                isOutOfDate = true,
                hasError = false,
            };
            m_MasterRenderData.shaderData = shaderData;

            m_PreviewsNeedsRecompile.Add(m_MasterRenderData);
            m_PreviewsToDraw.Add(m_MasterRenderData);
            m_TopologyDirty = true;
        }

        public void UpdateMasterPreview(ModificationScope scope)
        {
            if (scope == ModificationScope.Topological ||
                scope == ModificationScope.Graph)
            {
                // mark the master preview for recompile if it exists
                // if not, no need to do it here, because it is always marked for recompile on creation
                if (m_MasterRenderData != null)
                    m_PreviewsNeedsRecompile.Add(m_MasterRenderData);
                m_TopologyDirty = true;
            }
            else if (scope == ModificationScope.Node)
            {
                if (m_MasterRenderData != null)
                    m_PreviewsToDraw.Add(m_MasterRenderData);
            }
        }

        void AddPreview(AbstractMaterialNode node)
        {
            Assert.IsNotNull(node);

            // BlockNodes have no preview for themselves, but are mapped to the "Master" preview
            // SubGraphOutput nodes have their own previews, but will use the "Master" preview if they are the m_Graph.outputNode
            if (node is BlockNode)
            {
                node.RegisterCallback(OnNodeModified);
                UpdateMasterPreview(ModificationScope.Topological);
                m_NodesPropertyChanged.Add(node);
                return;
            }

            var renderData = new PreviewRenderData
            {
                previewName = node.name ?? "UNNAMED NODE",
                renderTexture =
                    new RenderTexture(200, 200, 16, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Default)
                    {
                        hideFlags = HideFlags.HideAndDontSave
                    }
            };

            renderData.renderTexture.Create();

            var shaderData = new PreviewShaderData
            {
                node = node,
                passesCompiling = 0,
                isOutOfDate = true,
                hasError = false,
            };
            renderData.shaderData = shaderData;

            m_RenderDatas.Add(node.objectId, renderData);
            node.RegisterCallback(OnNodeModified);

            m_PreviewsNeedsRecompile.Add(renderData);
            m_NodesPropertyChanged.Add(node);
            m_TopologyDirty = true;
        }

        void OnNodeModified(AbstractMaterialNode node, ModificationScope scope)
        {
            Assert.IsNotNull(node);

            if (scope == ModificationScope.Topological ||
                scope == ModificationScope.Graph)
            {
                m_NodesShaderChanged.Add(node);     // shader code for this node changed, this will trigger m_PreviewsShaderChanged for all nodes downstream
                m_NodesPropertyChanged.Add(node);   // properties could also have changed at the same time and need to be re-collected
                m_TopologyDirty = true;
            }
            else if (scope == ModificationScope.Node)
            {
                // if we only changed a constant on the node, we don't have to recompile the shader for it, just re-render it with the updated constant
                // should instead flag m_NodesConstantChanged
                m_NodesPropertyChanged.Add(node);
            }
        }

        // temp structures that are kept around statically to avoid GC churn (not thread safe)
        static Stack<AbstractMaterialNode> m_TempNodeWave = new Stack<AbstractMaterialNode>();
        static HashSet<AbstractMaterialNode> m_TempAddedToNodeWave = new HashSet<AbstractMaterialNode>();

        // cache the Action to avoid GC
        Action<AbstractMaterialNode> AddNextLevelNodesToWave =
            nextLevelNode =>
            {
                if (!m_TempAddedToNodeWave.Contains(nextLevelNode))
                {
                    m_TempNodeWave.Push(nextLevelNode);
                    m_TempAddedToNodeWave.Add(nextLevelNode);
                }
            };

        enum PropagationDirection
        {
            Upstream,
            Downstream
        }

        // ADDs all nodes in sources, and all nodes in the given direction relative to them, into result
        // sources and result can be the same HashSet
        private static readonly ProfilerMarker PropagateNodesMarker = new ProfilerMarker("PropagateNodes");
        void PropagateNodes(HashSet<AbstractMaterialNode> sources, PropagationDirection dir, HashSet<AbstractMaterialNode> result)
        {
            using (PropagateNodesMarker.Auto())
            if (sources.Count > 0)
            {
                // NodeWave represents the list of nodes we still have to process and add to result
                m_TempNodeWave.Clear();
                m_TempAddedToNodeWave.Clear();
                foreach (var node in sources)
                {
                    m_TempNodeWave.Push(node);
                    m_TempAddedToNodeWave.Add(node);
                }

                while (m_TempNodeWave.Count > 0)
                {
                    var node = m_TempNodeWave.Pop();
                    if (node == null)
                        continue;

                    result.Add(node);

                    // grab connected nodes in propagation direction, add them to the node wave
                    ForeachConnectedNode(node, dir, AddNextLevelNodesToWave);
                }

                // clean up any temp data
                m_TempNodeWave.Clear();
                m_TempAddedToNodeWave.Clear();
            }
        }

        static void ForeachConnectedNode(AbstractMaterialNode node, PropagationDirection dir, Action<AbstractMaterialNode> action)
        {
            using (var tempEdges = PooledList<IEdge>.Get())
            using (var tempSlots = PooledList<MaterialSlot>.Get())
            {
                // Loop through all nodes that the node feeds into.
                if (dir == PropagationDirection.Downstream)
                    node.GetOutputSlots(tempSlots);
                else
                    node.GetInputSlots(tempSlots);

                foreach (var slot in tempSlots)
                {
                    // get the edges out of each slot
                    tempEdges.Clear();                            // and here we serialize another list, ouch!
                    node.owner.GetEdges(slot.slotReference, tempEdges);
                    foreach (var edge in tempEdges)
                    {
                        // We look at each node we feed into.
                        var connectedSlot = (dir == PropagationDirection.Downstream) ? edge.inputSlot : edge.outputSlot;
                        var connectedNode = connectedSlot.node;

                        action(connectedNode);
                    }
                }
            }
        }

        public void HandleGraphChanges()
        {
            foreach (var node in m_Graph.removedNodes)
            {
                DestroyPreview(node);
                m_TopologyDirty = true;
            }

            // remove the nodes from the state trackers
            m_NodesShaderChanged.ExceptWith(m_Graph.removedNodes);
            m_NodesPropertyChanged.ExceptWith(m_Graph.removedNodes);

            m_Messenger.ClearNodesFromProvider(this, m_Graph.removedNodes);

            foreach (var node in m_Graph.addedNodes)
            {
                AddPreview(node);
                m_TopologyDirty = true;
            }

            foreach (var edge in m_Graph.removedEdges)
            {
                var node = edge.inputSlot.node;
                if ((node is BlockNode) || (node is SubGraphOutputNode))
                    UpdateMasterPreview(ModificationScope.Topological);
                else
                {
                    m_NodesShaderChanged.Add(node);
                    //When an edge gets deleted, if the node had the edge on creation, the properties would get out of sync and no value would get set.
                    //Fix for https://fogbugz.unity3d.com/f/cases/1284033/
                    m_NodesPropertyChanged.Add(node);
                }
                m_TopologyDirty = true;
            }
            foreach (var edge in m_Graph.addedEdges)
            {
                var node = edge.inputSlot.node;
                if (node != null)
                {
                    if ((node is BlockNode) || (node is SubGraphOutputNode))
                        UpdateMasterPreview(ModificationScope.Topological);
                    else
                        m_NodesShaderChanged.Add(node);
                    m_TopologyDirty = true;
                }
            }
        }

        private static readonly ProfilerMarker CollectPreviewPropertiesMarker = new ProfilerMarker("CollectPreviewProperties");
        void CollectPreviewProperties(IEnumerable<AbstractMaterialNode> nodesToCollect, PooledList<PreviewProperty> perMaterialPreviewProperties)
        {
            using (CollectPreviewPropertiesMarker.Auto())
            using (var tempPreviewProps = PooledList<PreviewProperty>.Get())
            {
                // collect from all of the changed nodes
                foreach (var propNode in nodesToCollect)
                    propNode.CollectPreviewMaterialProperties(tempPreviewProps);

                // also grab all graph properties (they are updated every frame)
                foreach (var prop in m_Graph.properties)
                    tempPreviewProps.Add(prop.GetPreviewMaterialProperty());

                foreach (var previewProperty in tempPreviewProps)
                {
                    previewProperty.SetValueOnMaterialPropertyBlock(m_SharedPreviewPropertyBlock);

                    // virtual texture assignments must be pushed to the materials themselves (MaterialPropertyBlocks not supported)
                    if ((previewProperty.propType == PropertyType.VirtualTexture) &&
                        (previewProperty.vtProperty?.value?.layers != null))
                    {
                        perMaterialPreviewProperties.Add(previewProperty);
                    }
                }
            }
        }

        void AssignPerMaterialPreviewProperties(Material mat, List<PreviewProperty> perMaterialPreviewProperties)
        {
            foreach (var prop in perMaterialPreviewProperties)
            {
                switch (prop.propType)
                {
                    case PropertyType.VirtualTexture:

                        // setup the VT textures on the material
                        bool setAnyTextures = false;
                        var vt = prop.vtProperty.value;
                        for (int layer = 0; layer < vt.layers.Count; layer++)
                        {
                            var texture = vt.layers[layer].layerTexture?.texture;
                            int propIndex = mat.shader.FindPropertyIndex(vt.layers[layer].layerRefName);
                            if (propIndex != -1)
                            {
                                mat.SetTexture(vt.layers[layer].layerRefName, texture);
                                setAnyTextures = true;
                            }
                        }
                        // also put in a request for the VT tiles, since preview rendering does not have feedback enabled
                        if (setAnyTextures)
                        {
#if ENABLE_VIRTUALTEXTURES
                            int stackPropertyId = Shader.PropertyToID(prop.vtProperty.referenceName);
                            try
                            {
                                // Ensure we always request the mip sized 256x256
                                int width, height;
                                UnityEngine.Rendering.VirtualTexturing.Streaming.GetTextureStackSize(mat, stackPropertyId, out width, out height);
                                int textureMip = (int)Math.Max(Mathf.Log(width, 2f), Mathf.Log(height, 2f));
                                const int baseMip = 8;
                                int mip = Math.Max(textureMip - baseMip, 0);
                                UnityEngine.Rendering.VirtualTexturing.Streaming.RequestRegion(mat, stackPropertyId, new Rect(0.0f, 0.0f, 1.0f, 1.0f), mip, UnityEngine.Rendering.VirtualTexturing.System.AllMips);
                            }
                            catch (InvalidOperationException)
                            {
                                // This gets thrown when the system is in an indeterminate state (like a material with no textures assigned which can obviously never have a texture stack streamed).
                                // This is valid in this case as we're still authoring the material.
                            }
#endif // ENABLE_VIRTUALTEXTURES
                        }
                        break;
                }
            }   
        }

        private static readonly ProfilerMarker RenderPreviewsMarker = new ProfilerMarker("RenderPreviews");
        public void RenderPreviews(bool requestShaders = true)
        {
            using (RenderPreviewsMarker.Auto())
            using (var renderList2D = PooledList<PreviewRenderData>.Get())
            using (var renderList3D = PooledList<PreviewRenderData>.Get())
            using (var nodesToDraw = PooledHashSet<AbstractMaterialNode>.Get())
            using (var perMaterialPreviewProperties = PooledList<PreviewProperty>.Get())
            {
                // update topology cached data
                // including list of time-dependent previews, and the preview mode (2d/3d)
                UpdateTopology();

                if (requestShaders)
                    UpdateShaders();

                // all nodes downstream of a changed property must be redrawn (to display the updated the property value)
                PropagateNodes(m_NodesPropertyChanged, PropagationDirection.Downstream, nodesToDraw);

                CollectPreviewProperties(m_NodesPropertyChanged, perMaterialPreviewProperties);
                m_NodesPropertyChanged.Clear();

                // timed nodes change every frame, so must be drawn
                // (m_TimedPreviews has been pre-propagated downstream)
                // HOWEVER they do not need to collect properties. (the only property changing is time..)
                m_PreviewsToDraw.UnionWith(m_TimedPreviews);

                ForEachNodesPreview(nodesToDraw, p => m_PreviewsToDraw.Add(p));

                // redraw master when it is resized
                if (m_NewMasterPreviewSize.HasValue)
                    m_PreviewsToDraw.Add(m_MasterRenderData);

                // apply filtering to determine what nodes really get drawn
                bool renderMasterPreview = false;
                int drawPreviewCount = 0;
                foreach (var preview in m_PreviewsToDraw)
                {
                    Assert.IsNotNull(preview);

                    { // skip if the node doesn't have a preview expanded (unless it's master)
                        var node = preview.shaderData.node;
                        if ((node != kMasterProxyNode) && (!node.hasPreview || !node.previewExpanded))
                            continue;
                    }

                    // check that we've got shaders and materials generated
                    // if not ,replace the rendered texture with null
                    if ((preview.shaderData.shader == null) ||
                        (preview.shaderData.mat == null))
                    {
                        // avoid calling NotifyPreviewChanged repeatedly
                        if (preview.texture != null)
                        {
                            preview.texture = null;
                            preview.NotifyPreviewChanged();
                        }
                        continue;
                    }


                    if (preview.shaderData.hasError)
                    {
                        preview.texture = m_ErrorTexture;
                        preview.NotifyPreviewChanged();
                        continue;
                    }

                    // we want to render this thing, now categorize what kind of render it is
                    if (preview == m_MasterRenderData)
                        renderMasterPreview = true;
                    else if (preview.previewMode == PreviewMode.Preview2D)
                        renderList2D.Add(preview);
                    else
                        renderList3D.Add(preview);
                    drawPreviewCount++;
                }

                // if we actually don't want to render anything at all, early out here
                if (drawPreviewCount <= 0)
                    return;

                var time = Time.realtimeSinceStartup;
                var timeParameters = new Vector4(time, Mathf.Sin(time), Mathf.Cos(time), 0.0f);
                m_SharedPreviewPropertyBlock.SetVector("_TimeParameters", timeParameters);

                EditorUtility.SetCameraAnimateMaterialsTime(m_SceneResources.camera, time);

                m_SceneResources.light0.enabled = true;
                m_SceneResources.light0.intensity = 1.0f;
                m_SceneResources.light0.transform.rotation = Quaternion.Euler(50f, 50f, 0);
                m_SceneResources.light1.enabled = true;
                m_SceneResources.light1.intensity = 1.0f;
                m_SceneResources.camera.clearFlags = CameraClearFlags.Color;

                // Render 2D previews
                m_SceneResources.camera.transform.position = -Vector3.forward * 2;
                m_SceneResources.camera.transform.rotation = Quaternion.identity;
                m_SceneResources.camera.orthographicSize = 0.5f;
                m_SceneResources.camera.orthographic = true;

                foreach (var renderData in renderList2D)
                    RenderPreview(renderData, m_SceneResources.quad, Matrix4x4.identity, perMaterialPreviewProperties);

                // Render 3D previews
                m_SceneResources.camera.transform.position = -Vector3.forward * 5;
                m_SceneResources.camera.transform.rotation = Quaternion.identity;
                m_SceneResources.camera.orthographic = false;

                foreach (var renderData in renderList3D)
                    RenderPreview(renderData, m_SceneResources.sphere, Matrix4x4.identity, perMaterialPreviewProperties);

                if (renderMasterPreview)
                {
                    if (m_NewMasterPreviewSize.HasValue)
                    {
                        if (masterRenderData.renderTexture != null)
                            Object.DestroyImmediate(masterRenderData.renderTexture, true);
                        masterRenderData.renderTexture = new RenderTexture((int)m_NewMasterPreviewSize.Value.x, (int)m_NewMasterPreviewSize.Value.y, 16, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Default) { hideFlags = HideFlags.HideAndDontSave };
                        masterRenderData.renderTexture.Create();
                        masterRenderData.texture = masterRenderData.renderTexture;
                        m_NewMasterPreviewSize = null;
                    }
                    var mesh = m_Graph.previewData.serializedMesh.mesh ? m_Graph.previewData.serializedMesh.mesh : m_SceneResources.sphere;
                    var previewTransform = Matrix4x4.Rotate(m_Graph.previewData.rotation);
                    var scale = m_Graph.previewData.scale;
                    previewTransform *= Matrix4x4.Scale(scale * Vector3.one * (Vector3.one).magnitude / mesh.bounds.size.magnitude);
                    previewTransform *= Matrix4x4.Translate(-mesh.bounds.center);

                    RenderPreview(masterRenderData, mesh, previewTransform, perMaterialPreviewProperties);
                }

                m_SceneResources.light0.enabled = false;
                m_SceneResources.light1.enabled = false;

                foreach (var renderData in renderList2D)
                    renderData.NotifyPreviewChanged();
                foreach (var renderData in renderList3D)
                    renderData.NotifyPreviewChanged();
                if (renderMasterPreview)
                    masterRenderData.NotifyPreviewChanged();
            }
        }

        private static readonly ProfilerMarker ProcessCompletedShaderCompilationsMarker = new ProfilerMarker("ProcessCompletedShaderCompilations");
        private int compileFailRekicks = 0;
        void ProcessCompletedShaderCompilations()
        {
            // Check for shaders that finished compiling and set them to redraw
            using (ProcessCompletedShaderCompilationsMarker.Auto())
            using (var previewsCompiled = PooledHashSet<PreviewRenderData>.Get())
            {
                foreach (var preview in m_PreviewsCompiling)
                {
                    {
                        var node = preview.shaderData.node;
                        Assert.IsFalse(node is BlockNode);
                    }

                    PreviewRenderData renderData = preview;
                    PreviewShaderData shaderData = renderData.shaderData;

                    // Assert.IsTrue(shaderData.passesCompiling > 0);
                    if (shaderData.passesCompiling <= 0)
                    {
                        Debug.Log("Zero Passes: " + preview.previewName + " (" + shaderData.passesCompiling + " passes, " + renderData.shaderData.mat.passCount + " mat passes)");
                    }

                    if (shaderData.passesCompiling != renderData.shaderData.mat.passCount)
                    {
                        // attempt to re-kick the compilation a few times
                        Debug.Log("Rekicking Compiling: " + preview.previewName + " (" + shaderData.passesCompiling + " passes, " + renderData.shaderData.mat.passCount + " mat passes)");
                        compileFailRekicks++;
                        if (compileFailRekicks <= 3)
                        {
                            shaderData.passesCompiling = 0;
                            previewsCompiled.Add(renderData);
                            m_PreviewsNeedsRecompile.Add(renderData);
                            continue;
                        }
                        else if (compileFailRekicks == 4)
                        {
                            Debug.LogWarning("Unexpected error in compiling preview shaders: some previews might not update. You can try to re-open the Shader Graph window, or select <b>Help > Report a Bug</b> in the menu and report this bug.");
                        }
                    }

                    // check that all passes have compiled
                    var allPassesCompiled = true;
                    for (var i = 0; i < renderData.shaderData.mat.passCount; i++)
                    {
                        if (!ShaderUtil.IsPassCompiled(renderData.shaderData.mat, i))
                        {
                            allPassesCompiled = false;
                            break;
                        }
                    }

                    if (!allPassesCompiled)
                    {
                        // keep waiting
                        continue;
                    }

                    // Force the material to re-generate all it's shader properties, by reassigning the shader
                    renderData.shaderData.mat.shader = renderData.shaderData.shader;
                    renderData.shaderData.passesCompiling = 0;
                    renderData.shaderData.isOutOfDate = false;
                    CheckForErrors(renderData.shaderData);

                    previewsCompiled.Add(renderData);

                    if (renderData == m_MasterRenderData)
                    {
                        // TODO: this may be a good thing to do BEFORE requesting shader compilation
                        // Process preview materials
                        foreach (var target in m_Graph.activeTargets)
                        {
                            if (target.IsActive())
                            {
                                target.ProcessPreviewMaterial(renderData.shaderData.mat);
                            }
                        }
                    }
                }

                // removed compiled nodes from compiling list
                m_PreviewsCompiling.ExceptWith(previewsCompiled);

                // and add them to the draw list to display updated shader (note this will only redraw specifically this node, not any others)
                m_PreviewsToDraw.UnionWith(previewsCompiled);
            }
        }

        private static readonly ProfilerMarker KickOffShaderCompilationsMarker = new ProfilerMarker("KickOffShaderCompilations");
        void KickOffShaderCompilations()
        {
            // Start compilation for nodes that need to recompile
            using (KickOffShaderCompilationsMarker.Auto())
            using (var previewsToCompile = PooledHashSet<PreviewRenderData>.Get())
            {
                // master node compile is first in the priority list, as it takes longer than the other previews
                if (m_PreviewsCompiling.Count + previewsToCompile.Count < m_MaxPreviewsCompiling)
                {
                    if (m_PreviewsNeedsRecompile.Contains(m_MasterRenderData) &&
                        !m_PreviewsCompiling.Contains(m_MasterRenderData))
                    {
                        previewsToCompile.Add(m_MasterRenderData);
                        m_PreviewsNeedsRecompile.Remove(m_MasterRenderData);
                    }
                }

                // add each node to compile list if it needs a preview, is not already compiling, and we have room
                // (we don't want to double kick compiles, so wait for the first one to get back before kicking another)
                for (int i = 0; i < m_PreviewsNeedsRecompile.Count(); i++)
                {
                    if (m_PreviewsCompiling.Count + previewsToCompile.Count >= m_MaxPreviewsCompiling)
                        break;

                    var preview = m_PreviewsNeedsRecompile.ElementAt(i);
                    if (preview == m_MasterRenderData) // master preview is handled specially above
                        continue;

                    var node = preview.shaderData.node;
                    Assert.IsNotNull(node);
                    Assert.IsFalse(node is BlockNode);

                    if (node.hasPreview && node.previewExpanded && !m_PreviewsCompiling.Contains(preview))
                    {
                        previewsToCompile.Add(preview);
                    }
                }

                if (previewsToCompile.Count >= 0)
                using (var nodesToCompile = PooledHashSet<AbstractMaterialNode>.Get())
                {
                    // remove the selected nodes from the recompile list
                    m_PreviewsNeedsRecompile.ExceptWith(previewsToCompile);

                    // Reset error states for the UI, the shader, and all render data for nodes we're recompiling
                    nodesToCompile.UnionWith(previewsToCompile.Select(x => x.shaderData.node));
                    nodesToCompile.Remove(null);

                    // TODO: not sure if we need to clear BlockNodes when master gets rebuilt?
                    m_Messenger.ClearNodesFromProvider(this, nodesToCompile);

                    // Force async compile on
                    var wasAsyncAllowed = ShaderUtil.allowAsyncCompilation;
                    ShaderUtil.allowAsyncCompilation = true;

                    // kick async compiles for all nodes in m_NodeToCompile
                    foreach (var preview in previewsToCompile)
                    {
                        if (preview == m_MasterRenderData)
                        {
                            CompileMasterNodeShader();
                            continue;
                        }

                        var node = preview.shaderData.node;
                        Assert.IsNotNull(node); // master preview is handled above

                        // Get shader code and compile
                        var generator = new Generator(node.owner, node, GenerationMode.Preview, $"hidden/preview/{node.GetVariableNameForNode()}", null);
                        BeginCompile(preview, generator.generatedShader);
                    }

                    ShaderUtil.allowAsyncCompilation = wasAsyncAllowed;
                }
            }
        }

        private static readonly ProfilerMarker UpdateShadersMarker = new ProfilerMarker("UpdateShaders");
        void UpdateShaders()
        {
            using (UpdateShadersMarker.Auto())
            {
                ProcessCompletedShaderCompilations();

                if (m_NodesShaderChanged.Count > 0)
                {
                    // nodes with shader changes cause all downstream nodes to need recompilation
                    // (since they presumably include the code for these nodes)
                    using (var nodesToRecompile = PooledHashSet<AbstractMaterialNode>.Get())
                    {
                        PropagateNodes(m_NodesShaderChanged, PropagationDirection.Downstream, nodesToRecompile);
                        ForEachNodesPreview(nodesToRecompile, p => m_PreviewsNeedsRecompile.Add(p));

                        m_NodesShaderChanged.Clear();
                    }
                }

                // if there's nothing to update, or if too many nodes are still compiling, then just return
                if ((m_PreviewsNeedsRecompile.Count == 0) || (m_PreviewsCompiling.Count >= m_MaxPreviewsCompiling))
                    return;

                // flag all nodes in m_PreviewsNeedsRecompile as having out of date textures, and redraw them
                foreach (var preview in m_PreviewsNeedsRecompile)
                {
                    Assert.IsNotNull(preview);
                    if (!preview.shaderData.isOutOfDate)
                    {
                        preview.shaderData.isOutOfDate = true;
                        preview.NotifyPreviewChanged();
                    }
                }

                InitializeSRPIfNeeded();    // SRP must be initialized to compile master node previews

                KickOffShaderCompilations();
            }
        }

        private static readonly ProfilerMarker BeginCompileMarker = new ProfilerMarker("BeginCompile");
        void BeginCompile(PreviewRenderData renderData, string shaderStr)
        {
            using (BeginCompileMarker.Auto())
            {
                var shaderData = renderData.shaderData;

                // want to ensure this so we don't get confused with multiple compile versions in flight
                Assert.IsTrue(shaderData.passesCompiling == 0);

                if (shaderData.shader == null)
                {
                    shaderData.shader = ShaderUtil.CreateShaderAsset(shaderStr, false);
                    shaderData.shader.hideFlags = HideFlags.HideAndDontSave;
                }
                else
                {
                    ShaderUtil.ClearCachedData(shaderData.shader);
                    ShaderUtil.UpdateShaderAsset(shaderData.shader, shaderStr, false);
                }

                // Due to case 1259744, we have to re-create the material to update the preview material keywords
                Object.DestroyImmediate(shaderData.mat);

                if (shaderData.mat == null)
                {
                    shaderData.mat = new Material(shaderData.shader) { hideFlags = HideFlags.HideAndDontSave };
                }

                int materialPassCount = shaderData.mat.passCount;
                if (materialPassCount <= 0)
                    Debug.Log("Zero Passes ON COMPILE: " + shaderData.node.name + " (" + shaderData.passesCompiling + " passes, " + renderData.shaderData.mat.passCount + " mat passes)");
                else
                {
                    shaderData.passesCompiling = materialPassCount;
                    for (var i = 0; i < materialPassCount; i++)
                    {
                        ShaderUtil.CompilePass(shaderData.mat, i);
                    }
                    m_PreviewsCompiling.Add(renderData);
                }
            }
        }

        private void ForEachNodesPreview(
            IEnumerable<AbstractMaterialNode> nodes,
            Action<PreviewRenderData> action)
        {
            foreach (var node in nodes)
            {
                var preview = GetPreviewRenderData(node);
                if (preview != null)    // some output nodes may have no preview
                    action(preview);
            }
        }

        class NodeProcessor
        {
            // parameters
            GraphData graphData;
            Action<AbstractMaterialNode, IEnumerable<AbstractMaterialNode>> process;

            // node tracking state
            HashSet<AbstractMaterialNode> processing = new HashSet<AbstractMaterialNode>();
            HashSet<AbstractMaterialNode> processed = new HashSet<AbstractMaterialNode>();

            // iteration state stack
            Stack<AbstractMaterialNode> nodeStack = new Stack<AbstractMaterialNode>();
            Stack<int> childStartStack = new Stack<int>();
            Stack<int> curChildStack = new Stack<int>();
            Stack<int> stateStack = new Stack<int>();

            List<AbstractMaterialNode> allChildren = new List<AbstractMaterialNode>();

            public NodeProcessor(GraphData graphData, Action<AbstractMaterialNode, IEnumerable<AbstractMaterialNode>> process)
            {
                this.graphData = graphData;
                this.process = process;
            }

            public void ProcessInDependencyOrder(AbstractMaterialNode root)
            {
                // early out to skip a bit of work
                if (processed.Contains(root))
                    return;

                // push root node in the initial state
                stateStack.Push(0);
                nodeStack.Push(root);

                while (nodeStack.Count > 0)
                {
                    // check the state of the top of the stack
                    switch (stateStack.Pop())
                    {
                        case 0: // node initial state   (valid stacks:  nodeStack)
                            {
                                var node = nodeStack.Peek();
                                if (processed.Contains(node))
                                {
                                    // finished with this node, pop it off the stack
                                    nodeStack.Pop();
                                    continue;
                                }

                                if (processing.Contains(node))
                                {
                                    // not processed, but still processing.. means there was a circular dependency here
                                    throw new ArgumentException("ERROR: graph contains circular wire connections");
                                }

                                processing.Add(node);

                                int childStart = allChildren.Count;
                                childStartStack.Push(childStart);

                                // add immediate children
                                ForeachConnectedNode(node, PropagationDirection.Upstream, n => allChildren.Add(n));

                                if (allChildren.Count == childStart)
                                {
                                    // no children.. transition to state 2 (all children processed)
                                    stateStack.Push(2);
                                }
                                else
                                {
                                    // transition to state 1 (processing children)
                                    stateStack.Push(1);
                                    curChildStack.Push(childStart);
                                }
                            }
                            break;
                        case 1: // processing children (valid stacks:  nodeStack, childStartStack, curChildStack)
                            {
                                int curChild = curChildStack.Pop();

                                // first update our state for when we return from the cur child
                                int nextChild = curChild + 1;
                                if (nextChild < allChildren.Count)
                                {
                                    // we will process the next child
                                    stateStack.Push(1);
                                    curChildStack.Push(nextChild);
                                }
                                else
                                {
                                    // we will be done iterating children, move to state 2
                                    stateStack.Push(2);
                                }

                                // then push the current child in state 0 to process it
                                stateStack.Push(0);
                                nodeStack.Push(allChildren[curChild]);
                            }
                            break;
                        case 2: // all children processed (valid stacks: nodeStack, childStartStack)
                            {
                                // read state, popping all
                                var node = nodeStack.Pop();
                                int childStart = childStartStack.Pop();

                                // process node
                                process(node, allChildren.Slice(childStart, allChildren.Count));
                                processed.Add(node);

                                // remove the children that were added in state 0
                                allChildren.RemoveRange(childStart, allChildren.Count - childStart);

                                // terminate node, stacks are popped to state of parent node
                            }
                            break;
                    }
                }
            }

            public void ProcessInDependencyOrderRecursive(AbstractMaterialNode node)
            {
                if (processed.Contains(node))
                    return; // already processed

                if (processing.Contains(node))
                    throw new ArgumentException("ERROR: graph contains circular wire connections");

                processing.Add(node);

                int childStart = allChildren.Count;

                // add immediate children
                ForeachConnectedNode(node, PropagationDirection.Upstream, n => allChildren.Add(n));

                // process children
                var children = allChildren.Slice(childStart, allChildren.Count);
                foreach (var child in children)
                    ProcessInDependencyOrderRecursive(child);

                // process self
                process(node, children);
                processed.Add(node);

                // remove the children
                allChildren.RemoveRange(childStart, allChildren.Count - childStart);
            }
        }

        // Processes all the nodes in the upstream trees of rootNodes
        // Will only process each node once, even if the trees overlap
        // Processes a node ONLY after processing all of the nodes in its upstream subtree
        void ProcessUpstreamNodesInDependencyOrder(
            IEnumerable<AbstractMaterialNode> rootNodes,                                    // root nodes can share subtrees, but cannot themselves exist in any others subtree
            Action<AbstractMaterialNode, IEnumerable<AbstractMaterialNode>> process)       // process takes the node and it's list of immediate upstream children as parameters
        {
            if (rootNodes.Any())
            {
                NodeProcessor processor = new NodeProcessor(rootNodes.First().owner, process);
                foreach (var node in rootNodes)
                    processor.ProcessInDependencyOrderRecursive(node);
            }
        }

        private static readonly ProfilerMarker UpdateTopologyMarker = new ProfilerMarker("UpdateTopology");
        void UpdateTopology()
        {
            if (!m_TopologyDirty)
                return;

            using (UpdateTopologyMarker.Auto())
            using (var timedNodes = PooledHashSet<AbstractMaterialNode>.Get())
            {
                timedNodes.UnionWith(m_Graph.GetNodes<AbstractMaterialNode>().Where(n => n.RequiresTime()));

                // we pre-propagate timed nodes downstream, to reduce amount of propagation we have to do per frame
                PropagateNodes(timedNodes, PropagationDirection.Downstream, timedNodes);

                m_TimedPreviews.Clear();
                ForEachNodesPreview(timedNodes, p => m_TimedPreviews.Add(p));
            }

            // Calculate the PreviewMode from upstream nodes
            ProcessUpstreamNodesInDependencyOrder(
                // we just pass all the nodes we care about as the roots
                m_RenderDatas.Values.Select(p => p.shaderData.node).Where(n => n != null),
                (node, children) =>
                {
                    var preview = GetPreviewRenderData(node);

                    // set preview mode based on node
                    preview.previewMode = node.previewMode;

                    // then 2d upgrades to 3d if any child is 3d
                    if (preview.previewMode == PreviewMode.Preview2D)
                    {
                        foreach (var child in children)
                        {
                            if (GetPreviewRenderData(child).previewMode == PreviewMode.Preview3D)
                            {
                                preview.previewMode = PreviewMode.Preview3D;
                                break;
                            }
                        }
                    }
                });

            m_TopologyDirty = false;
        }

       private static readonly ProfilerMarker RenderPreviewMarker = new ProfilerMarker("RenderPreview");
        void RenderPreview(PreviewRenderData renderData, Mesh mesh, Matrix4x4 transform, PooledList<PreviewProperty> perMaterialPreviewProperties)
        {
            using (RenderPreviewMarker.Auto())
            {
                AssignPerMaterialPreviewProperties(renderData.shaderData.mat, perMaterialPreviewProperties);

                var previousRenderTexture = RenderTexture.active;

                //Temp workaround for alpha previews...
                var temp = RenderTexture.GetTemporary(renderData.renderTexture.descriptor);
                RenderTexture.active = temp;
                Graphics.Blit(Texture2D.whiteTexture, temp, m_SceneResources.checkerboardMaterial);

                // Mesh is invalid for VFXTarget
                // We should handle this more gracefully
                if (renderData != m_MasterRenderData || !m_Graph.isOnlyVFXTarget)
                {
                    m_SceneResources.camera.targetTexture = temp;
                    Graphics.DrawMesh(mesh, transform, renderData.shaderData.mat, 1, m_SceneResources.camera, 0, m_SharedPreviewPropertyBlock, ShadowCastingMode.Off, false, null, false);
                }

                var previousUseSRP = Unsupported.useScriptableRenderPipeline;
                Unsupported.useScriptableRenderPipeline = (renderData == m_MasterRenderData);
                m_SceneResources.camera.Render();
                Unsupported.useScriptableRenderPipeline = previousUseSRP;

                Graphics.Blit(temp, renderData.renderTexture, m_SceneResources.blitNoAlphaMaterial);
                RenderTexture.ReleaseTemporary(temp);

                RenderTexture.active = previousRenderTexture;
                renderData.texture = renderData.renderTexture;

                m_PreviewsToDraw.Remove(renderData);
            }
        }

        void InitializeSRPIfNeeded()
        {
            if ((Shader.globalRenderPipeline != null) && (Shader.globalRenderPipeline.Length > 0))
            {
                return;
            }

            // issue a dummy SRP render to force SRP initialization, use the master node texture
            PreviewRenderData renderData = m_MasterRenderData;
            var previousRenderTexture = RenderTexture.active;

            //Temp workaround for alpha previews...
            var temp = RenderTexture.GetTemporary(renderData.renderTexture.descriptor);
            RenderTexture.active = temp;
            Graphics.Blit(Texture2D.whiteTexture, temp, m_SceneResources.checkerboardMaterial);

            m_SceneResources.camera.targetTexture = temp;

            var previousUseSRP = Unsupported.useScriptableRenderPipeline;
            Unsupported.useScriptableRenderPipeline = true;
            m_SceneResources.camera.Render();
            Unsupported.useScriptableRenderPipeline = previousUseSRP;

            RenderTexture.ReleaseTemporary(temp);

            RenderTexture.active = previousRenderTexture;
        }

        void CheckForErrors(PreviewShaderData shaderData)
        {
            shaderData.hasError = ShaderUtil.ShaderHasError(shaderData.shader);
            if (shaderData.hasError)
            {
                var messages = ShaderUtil.GetShaderMessages(shaderData.shader);
                if (messages.Length > 0)
                {
                    // TODO: Where to add errors to the stack??
                    if(shaderData.node == null)
                        return;
                    
                    m_Messenger.AddOrAppendError(this, shaderData.node.objectId, messages[0]);
                }
            }
        }

        void CompileMasterNodeShader()
        {
            var shaderData = masterRenderData?.shaderData;
            
            // Skip generation for VFXTarget
            if(!m_Graph.isOnlyVFXTarget)
            {
                var generator = new Generator(m_Graph, m_Graph.outputNode, GenerationMode.Preview, "Master", null);
                shaderData.shaderString = generator.generatedShader;

                // Blocks from the generation include those temporarily created for missing stack blocks
                // We need to hold on to these to set preview property values during CollectShaderProperties
                m_MasterNodePreviewBlocks.Clear();
                foreach(var block in generator.blocks)
                {
                    m_MasterNodePreviewBlocks.Add(block);
                }
            }

            if (string.IsNullOrEmpty(shaderData.shaderString))
            {
                if (shaderData.shader != null)
                {
                    ShaderUtil.ClearShaderMessages(shaderData.shader);
                    Object.DestroyImmediate(shaderData.shader, true);
                    shaderData.shader = null;
                }
                return;
            }

            BeginCompile(masterRenderData, shaderData.shaderString);
        }

        void DestroyRenderData(PreviewRenderData renderData)
        {
            if (renderData.shaderData != null)
            {
                if (renderData.shaderData.mat != null)
                {
                    Object.DestroyImmediate(renderData.shaderData.mat, true);
                }
                if (renderData.shaderData.shader != null)
                {
                    Object.DestroyImmediate(renderData.shaderData.shader, true);
                }
            }

            if (renderData.renderTexture != null)
                Object.DestroyImmediate(renderData.renderTexture, true);

            if (renderData.shaderData != null && renderData.shaderData.node != null)
                renderData.shaderData.node.UnregisterCallback(OnNodeModified);
        }

        void DestroyPreview(AbstractMaterialNode node)
        {
            string nodeId = node.objectId;

            if (node is BlockNode)
            {
                // block nodes don't have preview render data
                Assert.IsFalse(m_RenderDatas.ContainsKey(node.objectId));
                node.UnregisterCallback(OnNodeModified);
                UpdateMasterPreview(ModificationScope.Topological);
                return;
            }

            if (!m_RenderDatas.TryGetValue(nodeId, out var renderData))
            {
                return;
            }

            m_PreviewsNeedsRecompile.Remove(renderData);
            m_PreviewsCompiling.Remove(renderData);
            m_PreviewsToDraw.Remove(renderData);
            m_TimedPreviews.Remove(renderData);

            DestroyRenderData(renderData);
            m_RenderDatas.Remove(nodeId);
        }

        void ReleaseUnmanagedResources()
        {
            if (m_ErrorTexture != null)
            {
                Object.DestroyImmediate(m_ErrorTexture);
                m_ErrorTexture = null;
            }
            if (m_SceneResources != null)
            {
                m_SceneResources.Dispose();
                m_SceneResources = null;
            }
            foreach (var renderData in m_RenderDatas.Values)
                DestroyRenderData(renderData);
            m_RenderDatas.Clear();
            m_SharedPreviewPropertyBlock.Clear();
        }

        public void Dispose()
        {
            ReleaseUnmanagedResources();
            GC.SuppressFinalize(this);
        }

        ~PreviewManager()
        {
            throw new Exception("PreviewManager was not disposed of properly.");
        }
    }

    delegate void OnPreviewChanged();

    class PreviewShaderData
    {
        public AbstractMaterialNode node;
        public Shader shader;
        public Material mat;
        public string shaderString;
        public int passesCompiling;
        public bool isOutOfDate;
        public bool hasError;
    }

    class PreviewRenderData
    {
        public string previewName;
        public PreviewShaderData shaderData;
        public RenderTexture renderTexture;
        public Texture texture;
        public PreviewMode previewMode;
        public OnPreviewChanged onPreviewChanged;

        public void NotifyPreviewChanged()
        {
            if (onPreviewChanged != null)
                onPreviewChanged();
        }
    }
}
