//#define TMP_DEBUG_MODE

using UnityEngine;
using System.Collections.Generic;

using UnityEngine.UI;


namespace TMPro
{

    public static class TMP_MaterialManager
    {
        private static List<MaskingMaterial> m_materialList = new List<MaskingMaterial>();

        private static Dictionary<long, FallbackMaterial> m_fallbackMaterials = new Dictionary<long, FallbackMaterial>();
        private static Dictionary<int, long> m_fallbackMaterialLookup = new Dictionary<int, long>();
        private static List<FallbackMaterial> m_fallbackCleanupList = new List<FallbackMaterial>();

        private static bool isFallbackListDirty;

        static TMP_MaterialManager()
        {
            Canvas.willRenderCanvases += OnPreRender;
        }

        static void OnPreRender()
        {
            if (isFallbackListDirty)
            {
                //Debug.Log("2 - Cleaning up Fallback Materials.");
                CleanupFallbackMaterials();
                isFallbackListDirty = false;
            }
        }

        /// <summary>
        /// Create a Masking Material Instance for the given ID
        /// </summary>
        /// <param name="baseMaterial"></param>
        /// <param name="stencilID"></param>
        /// <returns></returns>
        public static Material GetStencilMaterial(Material baseMaterial, int stencilID)
        {
            // Check if Material supports masking
            if (!baseMaterial.HasProperty(ShaderUtilities.ID_StencilID))
            {
                Debug.LogWarning("Selected Shader does not support Stencil Masking. Please select the Distance Field or Mobile Distance Field Shader.");
                return baseMaterial;
            }

            int baseMaterialID = baseMaterial.GetInstanceID();

            // If baseMaterial already has a corresponding masking material, return it.
            for (int i = 0; i < m_materialList.Count; i++)
            {
                if (m_materialList[i].baseMaterial.GetInstanceID() == baseMaterialID && m_materialList[i].stencilID == stencilID)
                {
                    m_materialList[i].count += 1;

                    #if TMP_DEBUG_MODE
                    ListMaterials();
                    #endif

                    return m_materialList[i].stencilMaterial;
                }
            }

            // No matching masking material found. Create and return a new one.

            Material stencilMaterial;

            //Create new Masking Material Instance for this Base Material
            stencilMaterial = new Material(baseMaterial);
            stencilMaterial.hideFlags = HideFlags.HideAndDontSave;

            #if UNITY_EDITOR
                stencilMaterial.name += " Masking ID:" + stencilID;
            #endif

            stencilMaterial.shaderKeywords = baseMaterial.shaderKeywords;

            // Set Stencil Properties
            ShaderUtilities.GetShaderPropertyIDs();
            stencilMaterial.SetFloat(ShaderUtilities.ID_StencilID, stencilID);
            //stencilMaterial.SetFloat(ShaderUtilities.ID_StencilOp, 0);
            stencilMaterial.SetFloat(ShaderUtilities.ID_StencilComp, 4);
            //stencilMaterial.SetFloat(ShaderUtilities.ID_StencilReadMask, stencilID);
            //stencilMaterial.SetFloat(ShaderUtilities.ID_StencilWriteMask, 0);

            MaskingMaterial temp = new MaskingMaterial();
            temp.baseMaterial = baseMaterial;
            temp.stencilMaterial = stencilMaterial;
            temp.stencilID = stencilID;
            temp.count = 1;

            m_materialList.Add(temp);

            #if TMP_DEBUG_MODE
            ListMaterials();
            #endif

            return stencilMaterial;
        }


        /// <summary>
        /// Function to release the stencil material.
        /// </summary>
        /// <param name="stencilMaterial"></param>
        public static void ReleaseStencilMaterial(Material stencilMaterial)
        {
            int stencilMaterialID = stencilMaterial.GetInstanceID();

            for (int i = 0; i < m_materialList.Count; i++)
            {
                if (m_materialList[i].stencilMaterial.GetInstanceID() == stencilMaterialID)
                {
                    if (m_materialList[i].count > 1)
                        m_materialList[i].count -= 1;
                    else
                    {
                        Object.DestroyImmediate(m_materialList[i].stencilMaterial);
                        m_materialList.RemoveAt(i);
                        stencilMaterial = null;
                    }

                    break;
                }
            }


            #if TMP_DEBUG_MODE
            ListMaterials();
            #endif
        }


        // Function which returns the base material associated with a Masking Material
        public static Material GetBaseMaterial(Material stencilMaterial)
        {
            // Check if maskingMaterial already has a base material associated with it.
            int index = m_materialList.FindIndex(item => item.stencilMaterial == stencilMaterial);

            if (index == -1)
                return null;
            else
                return m_materialList[index].baseMaterial;

        }


        /// <summary>
        /// Function to set the Material Stencil ID
        /// </summary>
        /// <param name="material"></param>
        /// <param name="stencilID"></param>
        /// <returns></returns>
        public static Material SetStencil(Material material, int stencilID)
        {
            material.SetFloat(ShaderUtilities.ID_StencilID, stencilID);

            if (stencilID == 0)
                material.SetFloat(ShaderUtilities.ID_StencilComp, 8);
            else
                material.SetFloat(ShaderUtilities.ID_StencilComp, 4);

            return material;
        }


        public static void AddMaskingMaterial(Material baseMaterial, Material stencilMaterial, int stencilID)
        {
            // Check if maskingMaterial already has a base material associated with it.
            int index = m_materialList.FindIndex(item => item.stencilMaterial == stencilMaterial);

            if (index == -1)
            {
                MaskingMaterial temp = new MaskingMaterial();
                temp.baseMaterial = baseMaterial;
                temp.stencilMaterial = stencilMaterial;
                temp.stencilID = stencilID;
                temp.count = 1;

                m_materialList.Add(temp);
            }
            else
            {
                stencilMaterial = m_materialList[index].stencilMaterial;
                m_materialList[index].count += 1;
            }
        }



        public static void RemoveStencilMaterial(Material stencilMaterial)
        {
            // Check if maskingMaterial is already on the list.
            int index = m_materialList.FindIndex(item => item.stencilMaterial == stencilMaterial);

            if (index != -1)
            {
                m_materialList.RemoveAt(index);
            }

            #if TMP_DEBUG_MODE
            ListMaterials();
            #endif
        }



        public static void ReleaseBaseMaterial(Material baseMaterial)
        {
            // Check if baseMaterial already has a masking material associated with it.
            int index = m_materialList.FindIndex(item => item.baseMaterial == baseMaterial);

            if (index == -1)
            {
                Debug.Log("No Masking Material exists for " + baseMaterial.name);
            }
            else
            {
                if (m_materialList[index].count > 1)
                {
                    m_materialList[index].count -= 1;
                    Debug.Log("Removed (1) reference to " + m_materialList[index].stencilMaterial.name + ". There are " + m_materialList[index].count + " references left.");
                }
                else
                {
                    Debug.Log("Removed last reference to " + m_materialList[index].stencilMaterial.name + " with ID " + m_materialList[index].stencilMaterial.GetInstanceID());
                    Object.DestroyImmediate(m_materialList[index].stencilMaterial);
                    m_materialList.RemoveAt(index);
                }
            }

            #if TMP_DEBUG_MODE
            ListMaterials();
            #endif
        }


        public static void ClearMaterials()
        {
            if (m_materialList.Count == 0)
            {
                Debug.Log("Material List has already been cleared.");
                return;
            }

            for (int i = 0; i < m_materialList.Count; i++)
            {
                //Material baseMaterial = m_materialList[i].baseMaterial;
                Material stencilMaterial = m_materialList[i].stencilMaterial;

                Object.DestroyImmediate(stencilMaterial);
            }
            m_materialList.Clear();
        }


        /// <summary>
        /// Function to get the Stencil ID
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public static int GetStencilID(GameObject obj)
        {
            // Implementation is almost copied from Unity UI

            var count = 0;

            var transform = obj.transform;
            var stopAfter = FindRootSortOverrideCanvas(transform);
            if (transform == stopAfter)
                return count;

            var t = transform.parent;
            var components = TMP_ListPool<Mask>.Get();
            while (t != null)
            {
                t.GetComponents<Mask>(components);
                for (var i = 0; i < components.Count; ++i)
                {
                    var mask = components[i];
                    if (mask != null && mask.MaskEnabled() && mask.graphic.IsActive())
            {
                        ++count;
                        break;
                    }
            }

                if (t == stopAfter)
                    break;

                t = t.parent;
            }
            TMP_ListPool<Mask>.Release(components);

            return Mathf.Min((1 << count) - 1, 255);
        }


        public static Material GetMaterialForRendering(MaskableGraphic graphic, Material baseMaterial)
        {
            if (baseMaterial == null)
                return null;

            var modifiers = TMP_ListPool<IMaterialModifier>.Get();
            graphic.GetComponents(modifiers);

            var result = baseMaterial;
            for (int i = 0; i < modifiers.Count; i++)
                result = modifiers[i].GetModifiedMaterial(result);

            TMP_ListPool<IMaterialModifier>.Release(modifiers);

            return result;
        }

        private static Transform FindRootSortOverrideCanvas(Transform start)
        {
            // Implementation is copied from Unity UI

            var canvasList = TMP_ListPool<Canvas>.Get();
            start.GetComponentsInParent(false, canvasList);
            Canvas canvas = null;

            for (int i = 0; i < canvasList.Count; ++i)
            {
                canvas = canvasList[i];

                // We found the canvas we want to use break
                if (canvas.overrideSorting)
                    break;
            }
            TMP_ListPool<Canvas>.Release(canvasList);

            return canvas != null ? canvas.transform : null;
        }


        internal static Material GetFallbackMaterial(TMP_FontAsset fontAsset, Material sourceMaterial, int atlasIndex)
        {
            int sourceMaterialID = sourceMaterial.GetInstanceID();
            Texture tex = fontAsset.atlasTextures[atlasIndex];
            int texID = tex.GetInstanceID();
            long key = (long)sourceMaterialID << 32 | (long)(uint)texID;
            FallbackMaterial fallback;

            if (m_fallbackMaterials.TryGetValue(key, out fallback))
            {
                // Check if source material properties have changed.
                int sourceMaterialCRC = sourceMaterial.ComputeCRC();
                if (sourceMaterialCRC == fallback.sourceMaterialCRC)
                    return fallback.fallbackMaterial;

                CopyMaterialPresetProperties(sourceMaterial, fallback.fallbackMaterial);
                fallback.sourceMaterialCRC = sourceMaterialCRC;
                return fallback.fallbackMaterial;
            }

            // Create new material from the source material and assign relevant atlas texture
            Material fallbackMaterial = new Material(sourceMaterial);
            fallbackMaterial.SetTexture(ShaderUtilities.ID_MainTex, tex);

            fallbackMaterial.hideFlags = HideFlags.HideAndDontSave;

            #if UNITY_EDITOR
                fallbackMaterial.name += " + " + tex.name;
            #endif

            fallback = new FallbackMaterial();
            fallback.fallbackID = key;
            fallback.sourceMaterial = fontAsset.material;
            fallback.sourceMaterialCRC = sourceMaterial.ComputeCRC();
            fallback.fallbackMaterial = fallbackMaterial;
            fallback.count = 0;

            m_fallbackMaterials.Add(key, fallback);
            m_fallbackMaterialLookup.Add(fallbackMaterial.GetInstanceID(), key);

            #if TMP_DEBUG_MODE
            ListFallbackMaterials();
            #endif

            return fallbackMaterial;
        }


        /// <summary>
        /// This function returns a material instance using the material properties of a previous material but using the font atlas texture of the new font asset.
        /// </summary>
        /// <param name="sourceMaterial">The material containing the source material properties to be copied to the new material.</param>
        /// <param name="targetMaterial">The font atlas texture that should be assigned to the new material.</param>
        /// <returns></returns>
        public static Material GetFallbackMaterial (Material sourceMaterial, Material targetMaterial)
        {
            int sourceID = sourceMaterial.GetInstanceID();
            Texture tex = targetMaterial.GetTexture(ShaderUtilities.ID_MainTex);
            int texID = tex.GetInstanceID();
            long key = (long)sourceID << 32 | (long)(uint)texID;
            FallbackMaterial fallback;

            if (m_fallbackMaterials.TryGetValue(key, out fallback))
            {
                // Check if source material properties have changed.
                int sourceMaterialCRC = sourceMaterial.ComputeCRC();
                if (sourceMaterialCRC == fallback.sourceMaterialCRC)
                    return fallback.fallbackMaterial;

                CopyMaterialPresetProperties(sourceMaterial, fallback.fallbackMaterial);
                fallback.sourceMaterialCRC = sourceMaterialCRC;
                return fallback.fallbackMaterial;
            }

            // Create new material from the source material and copy properties if using distance field shaders.
            Material fallbackMaterial;
            if (sourceMaterial.HasProperty(ShaderUtilities.ID_GradientScale) && targetMaterial.HasProperty(ShaderUtilities.ID_GradientScale))
            {
                fallbackMaterial = new Material(sourceMaterial);
                fallbackMaterial.hideFlags = HideFlags.HideAndDontSave;

                #if UNITY_EDITOR
                fallbackMaterial.name += " + " + tex.name;
                //Debug.Log("Creating new fallback material for " + fallbackMaterial.name);
                #endif

                fallbackMaterial.SetTexture(ShaderUtilities.ID_MainTex, tex);
                // Retain material properties unique to target material.
                fallbackMaterial.SetFloat(ShaderUtilities.ID_GradientScale, targetMaterial.GetFloat(ShaderUtilities.ID_GradientScale));
                fallbackMaterial.SetFloat(ShaderUtilities.ID_TextureWidth, targetMaterial.GetFloat(ShaderUtilities.ID_TextureWidth));
                fallbackMaterial.SetFloat(ShaderUtilities.ID_TextureHeight, targetMaterial.GetFloat(ShaderUtilities.ID_TextureHeight));
                fallbackMaterial.SetFloat(ShaderUtilities.ID_WeightNormal, targetMaterial.GetFloat(ShaderUtilities.ID_WeightNormal));
                fallbackMaterial.SetFloat(ShaderUtilities.ID_WeightBold, targetMaterial.GetFloat(ShaderUtilities.ID_WeightBold));
            }
            else
            {
                fallbackMaterial = new Material(targetMaterial);
            }

            fallback = new FallbackMaterial();
            fallback.fallbackID = key;
            fallback.sourceMaterial = sourceMaterial;
            fallback.sourceMaterialCRC = sourceMaterial.ComputeCRC();
            fallback.fallbackMaterial = fallbackMaterial;
            fallback.count = 0;

            m_fallbackMaterials.Add(key, fallback);
            m_fallbackMaterialLookup.Add(fallbackMaterial.GetInstanceID(), key);

            #if TMP_DEBUG_MODE
            ListFallbackMaterials();
            #endif

            return fallbackMaterial;
        }


        /// <summary>
        ///
        /// </summary>
        /// <param name="targetMaterial"></param>
        public static void AddFallbackMaterialReference(Material targetMaterial)
        {
            if (targetMaterial == null) return;

            int sourceID = targetMaterial.GetInstanceID();
            long key;

            // Lookup key to retrieve
            if (m_fallbackMaterialLookup.TryGetValue(sourceID, out key))
            {
                FallbackMaterial fallback;
                if (m_fallbackMaterials.TryGetValue(key, out fallback))
                {
                    //Debug.Log("Adding Fallback material " + fallback.fallbackMaterial.name + " with reference count of " + (fallback.count + 1));
                    fallback.count += 1;
                }
            }
        }


        /// <summary>
        ///
        /// </summary>
        /// <param name="targetMaterial"></param>
        public static void RemoveFallbackMaterialReference(Material targetMaterial)
        {
            if (targetMaterial == null) return;

            int sourceID = targetMaterial.GetInstanceID();
            long key;

            // Lookup key to retrieve
            if (m_fallbackMaterialLookup.TryGetValue(sourceID, out key))
            {
                FallbackMaterial fallback;
                if (m_fallbackMaterials.TryGetValue(key, out fallback))
                {
                    fallback.count -= 1;

                    if (fallback.count < 1)
                        m_fallbackCleanupList.Add(fallback);
                }
            }
        }


        /// <summary>
        ///
        /// </summary>
        public static void CleanupFallbackMaterials()
        {
            // Return if the list is empty.
            if (m_fallbackCleanupList.Count == 0) return;

            for (int i = 0; i < m_fallbackCleanupList.Count; i++)
            {
                FallbackMaterial fallback = m_fallbackCleanupList[i];

                if (fallback.count < 1)
                {
                    //Debug.Log("Cleaning up " + fallback.fallbackMaterial.name);

                    Material mat = fallback.fallbackMaterial;
                    m_fallbackMaterials.Remove(fallback.fallbackID);
                    m_fallbackMaterialLookup.Remove(mat.GetInstanceID());
                    Object.DestroyImmediate(mat);
                    mat = null;
                }
            }

            m_fallbackCleanupList.Clear();
        }


        /// <summary>
        /// Function to release the fallback material.
        /// </summary>
        /// <param name="fallbackMaterial">Material to be released.</param>
        public static void ReleaseFallbackMaterial(Material fallbackMaterial)
        {
            if (fallbackMaterial == null) return;

            int materialID = fallbackMaterial.GetInstanceID();
            long key;

            if (m_fallbackMaterialLookup.TryGetValue(materialID, out key))
            {
                FallbackMaterial fallback;
                if (m_fallbackMaterials.TryGetValue(key, out fallback))
                {
                    //Debug.Log("Releasing Fallback material " + fallback.fallbackMaterial.name + " with remaining reference count of " + (fallback.count - 1));

                    fallback.count -= 1;

                    if (fallback.count < 1)
                        m_fallbackCleanupList.Add(fallback);
                }
            }

            isFallbackListDirty = true;

            #if TMP_DEBUG_MODE
            ListFallbackMaterials();
            #endif
        }


        private class FallbackMaterial
        {
            public long fallbackID;
            public Material sourceMaterial;
            internal int sourceMaterialCRC;
            public Material fallbackMaterial;
            public int count;
        }


        private class MaskingMaterial
        {
            public Material baseMaterial;
            public Material stencilMaterial;
            public int count;
            public int stencilID;
        }


        /// <summary>
        /// Function to copy the properties of a source material preset to another while preserving the unique font asset properties of the destination material.
        /// </summary>
        /// <param name="source"></param>
        /// <param name="destination"></param>
        public static void CopyMaterialPresetProperties(Material source, Material destination)
        {
            if (!source.HasProperty(ShaderUtilities.ID_GradientScale) || !destination.HasProperty(ShaderUtilities.ID_GradientScale))
                return;

            // Save unique material properties
            Texture dst_texture = destination.GetTexture(ShaderUtilities.ID_MainTex);
            float dst_gradientScale = destination.GetFloat(ShaderUtilities.ID_GradientScale);
            float dst_texWidth = destination.GetFloat(ShaderUtilities.ID_TextureWidth);
            float dst_texHeight = destination.GetFloat(ShaderUtilities.ID_TextureHeight);
            float dst_weightNormal = destination.GetFloat(ShaderUtilities.ID_WeightNormal);
            float dst_weightBold = destination.GetFloat(ShaderUtilities.ID_WeightBold);

            // Copy all material properties
            destination.CopyPropertiesFromMaterial(source);

            // Copy shader keywords
            destination.shaderKeywords = source.shaderKeywords;

            // Restore unique material properties
            destination.SetTexture(ShaderUtilities.ID_MainTex, dst_texture);
            destination.SetFloat(ShaderUtilities.ID_GradientScale, dst_gradientScale);
            destination.SetFloat(ShaderUtilities.ID_TextureWidth, dst_texWidth);
            destination.SetFloat(ShaderUtilities.ID_TextureHeight, dst_texHeight);
            destination.SetFloat(ShaderUtilities.ID_WeightNormal, dst_weightNormal);
            destination.SetFloat(ShaderUtilities.ID_WeightBold, dst_weightBold);
        }


        #if TMP_DEBUG_MODE
        /// <summary>
        ///
        /// </summary>
        public static void ListMaterials()
        {

            if (m_materialList.Count == 0)
            {
                Debug.Log("Material List is empty.");
                return;
            }

            //Debug.Log("List contains " + m_materialList.Count() + " items.");

            for (int i = 0; i < m_materialList.Count; i++)
            {
                Material baseMaterial = m_materialList[i].baseMaterial;
                Material stencilMaterial = m_materialList[i].stencilMaterial;

                Debug.Log("Item #" + (i + 1) + " - Base Material is [" + baseMaterial.name + "] with ID " + baseMaterial.GetInstanceID() + " is associated with [" + (stencilMaterial != null ? stencilMaterial.name : "Null") + "] Stencil ID " + m_materialList[i].stencilID + " with ID " + (stencilMaterial != null ? stencilMaterial.GetInstanceID() : 0) + " and is referenced " + m_materialList[i].count + " time(s).");
            }
        }


        /// <summary>
        ///
        /// </summary>
        public static void ListFallbackMaterials()
        {

            if (m_fallbackMaterials.Count == 0)
            {
                Debug.Log("Material List is empty.");
                return;
            }

            Debug.Log("List contains " + m_fallbackMaterials.Count + " items.");

            int count = 0;
            foreach (var fallback in m_fallbackMaterials)
            {
                Material baseMaterial = fallback.Value.baseMaterial;
                Material fallbackMaterial = fallback.Value.fallbackMaterial;

                string output = "Item #" + (count++);
                if (baseMaterial != null)
                    output += " - Base Material is [" + baseMaterial.name + "] with ID " + baseMaterial.GetInstanceID();
                if (fallbackMaterial != null)
                    output += " is associated with [" + fallbackMaterial.name + "] with ID " + fallbackMaterial.GetInstanceID() + " and is referenced " + fallback.Value.count + " time(s).";

                Debug.Log(output);
            }
        }
        #endif
    }

}

