using UnityEngine;
using UnityEditor;

namespace TMPro.EditorUtilities
{
    /// <summary>Base class for TextMesh Pro shader GUIs.</summary>
    public abstract class TMP_BaseShaderGUI : ShaderGUI
    {
        /// <summary>Representation of a #pragma shader_feature.</summary>
        /// <description>It is assumed that the first feature option is for no keyword (underscores).</description>
        protected class ShaderFeature
        {
            public string undoLabel;

            public GUIContent label;

            /// <summary>The keyword labels, for display. Include the no-keyword as the first option.</summary>
            public GUIContent[] keywordLabels;

            /// <summary>The shader keywords. Exclude the no-keyword option.</summary>
            public string[] keywords;

            int m_State;

            public bool Active
            {
                get { return m_State >= 0; }
            }

            public int State
            {
                get { return m_State; }
            }

            public void ReadState(Material material)
            {
                for (int i = 0; i < keywords.Length; i++)
                {
                    if (material.IsKeywordEnabled(keywords[i]))
                    {
                        m_State = i;
                        return;
                    }
                }

                m_State = -1;
            }

            public void SetActive(bool active, Material material)
            {
                m_State = active ? 0 : -1;
                SetStateKeywords(material);
            }

            public void DoPopup(MaterialEditor editor, Material material)
            {
                EditorGUI.BeginChangeCheck();
                int selection = EditorGUILayout.Popup(label, m_State + 1, keywordLabels);
                if (EditorGUI.EndChangeCheck())
                {
                    m_State = selection - 1;
                    editor.RegisterPropertyChangeUndo(undoLabel);
                    SetStateKeywords(material);
                }
            }

            void SetStateKeywords(Material material)
            {
                for (int i = 0; i < keywords.Length; i++)
                {
                    if (i == m_State)
                    {
                        material.EnableKeyword(keywords[i]);
                    }
                    else
                    {
                        material.DisableKeyword(keywords[i]);
                    }
                }
            }
        }

        static GUIContent s_TempLabel = new GUIContent();

        protected static bool s_DebugExtended;

        static int s_UndoRedoCount, s_LastSeenUndoRedoCount;

        static float[][] s_TempFloats =
        {
            null, new float[1], new float[2], new float[3], new float[4]
        };

        protected static GUIContent[] s_XywhVectorLabels =
        {
            new GUIContent("X"),
            new GUIContent("Y"),
            new GUIContent("W", "Width"),
            new GUIContent("H", "Height")
        };

        protected static GUIContent[] s_LbrtVectorLabels =
        {
            new GUIContent("L", "Left"),
            new GUIContent("B", "Bottom"),
            new GUIContent("R", "Right"),
            new GUIContent("T", "Top")
        };

        protected static GUIContent[] s_CullingTypeLabels =
        {
            new GUIContent("Off"),
            new GUIContent("Front"),
            new GUIContent("Back")
        };

        static TMP_BaseShaderGUI()
        {
            // Keep track of how many undo/redo events happened.
            Undo.undoRedoPerformed += () => s_UndoRedoCount += 1;
        }

        bool m_IsNewGUI = true;

        float m_DragAndDropMinY;

        protected MaterialEditor m_Editor;

        protected Material m_Material;
        private int m_ShaderID;

        protected MaterialProperty[] m_Properties;

        void PrepareGUI()
        {
            m_IsNewGUI = false;
            ShaderUtilities.GetShaderPropertyIDs();

            // New GUI just got constructed. This happens in response to a selection,
            // but also after undo/redo events.
            if (s_LastSeenUndoRedoCount != s_UndoRedoCount)
            {
                // There's been at least one undo/redo since the last time this GUI got constructed.
                // Maybe the undo/redo was for this material? Assume that is was.
                TMPro_EventManager.ON_MATERIAL_PROPERTY_CHANGED(true, m_Material);
            }

            s_LastSeenUndoRedoCount = s_UndoRedoCount;
        }

        public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties)
        {
            m_Editor = materialEditor;
            m_Material = materialEditor.target as Material;
            this.m_Properties = properties;

            if (m_IsNewGUI)
                PrepareGUI();

            DoDragAndDropBegin();
            EditorGUI.BeginChangeCheck();
            DoGUI();
            if (EditorGUI.EndChangeCheck())
            {
                TMPro_EventManager.ON_MATERIAL_PROPERTY_CHANGED(true, m_Material);
            }

            DoDragAndDropEnd();
        }

        public override void AssignNewShaderToMaterial(Material material, Shader oldShader, Shader newShader)
        {
            base.AssignNewShaderToMaterial(material, oldShader, newShader);

            TMPro_EventManager.ON_MATERIAL_PROPERTY_CHANGED(true, material);
        }

        /// <summary>Override this method to create the specific shader GUI.</summary>
        protected abstract void DoGUI();

        static string[] s_PanelStateLabel = new string[] { "\t- <i>Click to collapse</i> -", "\t- <i>Click to expand</i>  -" };

        protected bool BeginPanel(string panel, bool expanded)
        {
            EditorGUI.indentLevel = 0;

            EditorGUILayout.BeginVertical(EditorStyles.helpBox);
            Rect r = EditorGUI.IndentedRect(GUILayoutUtility.GetRect(20, 18));
            r.x += 20;
            r.width += 6;

            bool enabled = GUI.enabled;
            GUI.enabled = true;
            expanded = TMP_EditorUtility.EditorToggle(r, expanded, new GUIContent(panel), TMP_UIStyleManager.panelTitle);
            r.width -= 30;
            EditorGUI.LabelField(r, new GUIContent(expanded ? s_PanelStateLabel[0] : s_PanelStateLabel[1]), TMP_UIStyleManager.rightLabel);
            GUI.enabled = enabled;

            EditorGUI.indentLevel += 1;
            EditorGUI.BeginDisabledGroup(false);

            return expanded;
        }

        protected bool BeginPanel(string panel, ShaderFeature feature, bool expanded, bool readState = true)
        {
            EditorGUI.indentLevel = 0;

            if (readState)
            {
                feature.ReadState(m_Material);
            }

            EditorGUI.BeginChangeCheck();

            EditorGUILayout.BeginVertical(EditorStyles.helpBox);
            GUILayout.BeginHorizontal();

            Rect r = EditorGUI.IndentedRect(GUILayoutUtility.GetRect(20, 20, GUILayout.Width(20f)));
            bool active = EditorGUI.Toggle(r, feature.Active);

            if (EditorGUI.EndChangeCheck())
            {
                m_Editor.RegisterPropertyChangeUndo(feature.undoLabel);
                feature.SetActive(active, m_Material);
            }

            r = EditorGUI.IndentedRect(GUILayoutUtility.GetRect(20, 18));
            r.width += 6;

            bool enabled = GUI.enabled;
            GUI.enabled = true;
            expanded = TMP_EditorUtility.EditorToggle(r, expanded, new GUIContent(panel), TMP_UIStyleManager.panelTitle);
            r.width -= 10;
            EditorGUI.LabelField(r, new GUIContent(expanded ? s_PanelStateLabel[0] : s_PanelStateLabel[1]), TMP_UIStyleManager.rightLabel);
            GUI.enabled = enabled;

            GUILayout.EndHorizontal();

            EditorGUI.indentLevel += 1;
            EditorGUI.BeginDisabledGroup(!active);

            return expanded;
        }

        public void EndPanel()
        {
            EditorGUI.EndDisabledGroup();
            EditorGUI.indentLevel -= 1;
            EditorGUILayout.EndVertical();
        }

        MaterialProperty BeginProperty(string name)
        {
            MaterialProperty property = FindProperty(name, m_Properties);
            EditorGUI.BeginChangeCheck();
            EditorGUI.showMixedValue = property.hasMixedValue;
            m_Editor.BeginAnimatedCheck(Rect.zero, property);

            return property;
        }

        bool EndProperty()
        {
            m_Editor.EndAnimatedCheck();
            EditorGUI.showMixedValue = false;
            return EditorGUI.EndChangeCheck();
        }

        protected void DoPopup(string name, string label, GUIContent[] options)
        {
            MaterialProperty property = BeginProperty(name);
            s_TempLabel.text = label;
            int index = EditorGUILayout.Popup(s_TempLabel, (int)property.floatValue, options);
            if (EndProperty())
            {
                property.floatValue = index;
            }
        }

        protected void DoCubeMap(string name, string label)
        {
            DoTexture(name, label, typeof(Cubemap));
        }

        protected void DoTexture2D(string name, string label, bool withTilingOffset = false, string[] speedNames = null)
        {
            DoTexture(name, label, typeof(Texture2D), withTilingOffset, speedNames);
        }

        void DoTexture(string name, string label, System.Type type, bool withTilingOffset = false, string[] speedNames = null)
        {
            float objFieldSize = 60f;
            bool smallLayout = EditorGUIUtility.currentViewWidth <= 330f && (withTilingOffset || speedNames != null);
            float controlHeight = smallLayout ? objFieldSize * 2 : objFieldSize;

            MaterialProperty property = FindProperty(name, m_Properties);
            m_Editor.BeginAnimatedCheck(Rect.zero, property);

            Rect rect = EditorGUILayout.GetControlRect(true, controlHeight);
            float totalWidth = rect.width;
            rect.width = EditorGUIUtility.labelWidth + objFieldSize;
            rect.height = objFieldSize;
            s_TempLabel.text = label;

            EditorGUI.BeginChangeCheck();
            Object tex = EditorGUI.ObjectField(rect, s_TempLabel, property.textureValue, type, false);
            if (EditorGUI.EndChangeCheck())
            {
                property.textureValue = tex as Texture;
            }

            float additionalHeight = controlHeight - objFieldSize;
            float xOffset = smallLayout ? rect.width - objFieldSize : rect.width;

            rect.y += additionalHeight;
            rect.x += xOffset;
            rect.width = totalWidth - xOffset;
            rect.height = EditorGUIUtility.singleLineHeight;

            if (withTilingOffset)
            {
                DoTilingOffset(rect, property);
                rect.y += (rect.height + 2f) * 2f;
            }

            m_Editor.EndAnimatedCheck();

            if (speedNames != null)
            {
                DoUVSpeed(rect, speedNames);
            }
        }

        void DoTilingOffset(Rect rect, MaterialProperty property)
        {
            float labelWidth = EditorGUIUtility.labelWidth;
            int indentLevel = EditorGUI.indentLevel;
            EditorGUI.indentLevel = 0;
            EditorGUIUtility.labelWidth = Mathf.Min(40f, rect.width * 0.40f);

            Vector4 vector = property.textureScaleAndOffset;

            bool changed = false;
            float[] values = s_TempFloats[2];

            s_TempLabel.text = "Tiling";
            Rect vectorRect = EditorGUI.PrefixLabel(rect, s_TempLabel);
            values[0] = vector.x;
            values[1] = vector.y;

            EditorGUI.BeginChangeCheck();
            EditorGUI.MultiFloatField(vectorRect, s_XywhVectorLabels, values);
            if (EditorGUI.EndChangeCheck())
            {
                vector.x = values[0];
                vector.y = values[1];
                changed = true;
            }

            rect.y += rect.height + 2f;
            s_TempLabel.text = "Offset";
            vectorRect = EditorGUI.PrefixLabel(rect, s_TempLabel);
            values[0] = vector.z;
            values[1] = vector.w;

            EditorGUI.BeginChangeCheck();
            EditorGUI.MultiFloatField(vectorRect, s_XywhVectorLabels, values);
            if (EditorGUI.EndChangeCheck())
            {
                vector.z = values[0];
                vector.w = values[1];
                changed = true;
            }

            if (changed)
            {
                property.textureScaleAndOffset = vector;
            }

            EditorGUIUtility.labelWidth = labelWidth;
            EditorGUI.indentLevel = indentLevel;
        }

        protected void DoUVSpeed(Rect rect, string[] names)
        {
            float labelWidth = EditorGUIUtility.labelWidth;
            int indentLevel = EditorGUI.indentLevel;
            EditorGUI.indentLevel = 0;
            EditorGUIUtility.labelWidth = Mathf.Min(40f, rect.width * 0.40f);

            s_TempLabel.text = "Speed";
            rect = EditorGUI.PrefixLabel(rect, s_TempLabel);

            EditorGUIUtility.labelWidth = 10f;
            rect.width = rect.width * 0.5f - 2f;

            if (names.Length == 1)
            {
                DoFloat2(rect, names[0]);
            }
            else
            {
                DoFloat(rect, names[0], "X");
                rect.x += rect.width + 4f;
                DoFloat(rect, names[1], "Y");
            }

            EditorGUIUtility.labelWidth = labelWidth;
            EditorGUI.indentLevel = indentLevel;
        }

        protected void DoToggle(string name, string label)
        {
            MaterialProperty property = BeginProperty(name);
            s_TempLabel.text = label;
            bool value = EditorGUILayout.Toggle(s_TempLabel, property.floatValue == 1f);
            if (EndProperty())
            {
                property.floatValue = value ? 1f : 0f;
            }
        }

        protected void DoFloat(string name, string label)
        {
            MaterialProperty property = BeginProperty(name);
            Rect rect = EditorGUILayout.GetControlRect();
            rect.width = EditorGUIUtility.labelWidth + 55f;
            s_TempLabel.text = label;
            float value = EditorGUI.FloatField(rect, s_TempLabel, property.floatValue);
            if (EndProperty())
            {
                property.floatValue = value;
            }
        }

        protected void DoColor(string name, string label)
        {
            MaterialProperty property = BeginProperty(name);
            s_TempLabel.text = label;
            Color value = EditorGUI.ColorField(EditorGUILayout.GetControlRect(), s_TempLabel, property.colorValue, false, true, true);
            if (EndProperty())
            {
                property.colorValue = value;
            }
        }

        void DoFloat(Rect rect, string name, string label)
        {
            MaterialProperty property = BeginProperty(name);
            s_TempLabel.text = label;
            float value = EditorGUI.FloatField(rect, s_TempLabel, property.floatValue);
            if (EndProperty())
            {
                property.floatValue = value;
            }
        }

        void DoFloat2(Rect rect, string name)
        {
            MaterialProperty property = BeginProperty(name);

            float x = EditorGUI.FloatField(rect, "X", property.vectorValue.x);
            rect.x += rect.width + 4f;
            float y = EditorGUI.FloatField(rect, "Y", property.vectorValue.y);

            if (EndProperty())
            {
                property.vectorValue = new Vector2(x, y);
            }
        }

        protected void DoOffset(string name, string label)
        {
            MaterialProperty property = BeginProperty(name);
            s_TempLabel.text = label;
            Vector2 value = EditorGUI.Vector2Field(EditorGUILayout.GetControlRect(), s_TempLabel, property.vectorValue);
            if (EndProperty())
            {
                property.vectorValue = value;
            }
        }

        protected void DoSlider(string name, string label)
        {
            MaterialProperty property = BeginProperty(name);
            Vector2 range = property.rangeLimits;
            s_TempLabel.text = label;
            float value = EditorGUI.Slider(EditorGUILayout.GetControlRect(), s_TempLabel, property.floatValue, range.x, range.y);
            if (EndProperty())
            {
                property.floatValue = value;
            }
        }

        protected void DoSlider(string name, Vector2 range, string label)
        {
            MaterialProperty property = BeginProperty(name);
            s_TempLabel.text = label;
            float value = EditorGUI.Slider(EditorGUILayout.GetControlRect(), s_TempLabel, property.floatValue, range.x, range.y);
            if (EndProperty())
            {
                property.floatValue = value;
            }
        }

        protected void DoSlider(string propertyName, string propertyField, string label)
        {
            MaterialProperty property = BeginProperty(propertyName);
            Vector2 range = property.rangeLimits;
            s_TempLabel.text = label;

            Vector4 value = property.vectorValue;

            switch (propertyField)
            {
                case "X":
                    value.x = EditorGUI.Slider(EditorGUILayout.GetControlRect(), s_TempLabel, value.x, range.x, range.y);
                    break;
                case "Y":
                    value.y = EditorGUI.Slider(EditorGUILayout.GetControlRect(), s_TempLabel, value.y, range.x, range.y);
                    break;
                case "Z":
                    value.z = EditorGUI.Slider(EditorGUILayout.GetControlRect(), s_TempLabel, value.z, range.x, range.y);
                    break;
                case "W":
                    value.w = EditorGUI.Slider(EditorGUILayout.GetControlRect(), s_TempLabel, value.w, range.x, range.y);
                    break;
            }

            if (EndProperty())
            {
                property.vectorValue = value;
            }
        }

        protected void DoSlider(string propertyName, string propertyField, Vector2 range, string label)
        {
            MaterialProperty property = BeginProperty(propertyName);
            s_TempLabel.text = label;

            Vector4 value = property.vectorValue;

            switch (propertyField)
            {
                case "X":
                    value.x = EditorGUI.Slider(EditorGUILayout.GetControlRect(), s_TempLabel, value.x, range.x, range.y);
                    break;
                case "Y":
                    value.y = EditorGUI.Slider(EditorGUILayout.GetControlRect(), s_TempLabel, value.y, range.x, range.y);
                    break;
                case "Z":
                    value.z = EditorGUI.Slider(EditorGUILayout.GetControlRect(), s_TempLabel, value.z, range.x, range.y);
                    break;
                case "W":
                    value.w = EditorGUI.Slider(EditorGUILayout.GetControlRect(), s_TempLabel, value.w, range.x, range.y);
                    break;
            }

            if (EndProperty())
            {
                property.vectorValue = value;
            }
        }

        protected void DoVector2(string name, string label)
        {
            MaterialProperty property = BeginProperty(name);
            s_TempLabel.text = label;
            Vector4 value = EditorGUILayout.Vector3Field(s_TempLabel, property.vectorValue);
            if (EndProperty())
            {
                property.vectorValue = value;
            }
        }

        protected void DoVector3(string name, string label)
        {
            MaterialProperty property = BeginProperty(name);
            s_TempLabel.text = label;
            Vector4 value = EditorGUILayout.Vector3Field(s_TempLabel, property.vectorValue);
            if (EndProperty())
            {
                property.vectorValue = value;
            }
        }

        protected void DoVector(string name, string label, GUIContent[] subLabels)
        {
            MaterialProperty property = BeginProperty(name);
            Rect rect = EditorGUILayout.GetControlRect();
            s_TempLabel.text = label;
            rect = EditorGUI.PrefixLabel(rect, s_TempLabel);
            Vector4 vector = property.vectorValue;

            float[] values = s_TempFloats[subLabels.Length];
            for (int i = 0; i < subLabels.Length; i++)
            {
                values[i] = vector[i];
            }

            EditorGUI.MultiFloatField(rect, subLabels, values);
            if (EndProperty())
            {
                for (int i = 0; i < subLabels.Length; i++)
                {
                    vector[i] = values[i];
                }

                property.vectorValue = vector;
            }
        }

        bool IsNewShader()
        {
            if (m_Material == null)
                return false;

            int currentShaderID = m_Material.shader.GetInstanceID();

            if (m_ShaderID == currentShaderID)
                return false;

            m_ShaderID = currentShaderID;

            return true;
        }

        void DoDragAndDropBegin()
        {
            m_DragAndDropMinY = GUILayoutUtility.GetRect(0f, 0f, GUILayout.ExpandWidth(true)).y;
        }

        void DoDragAndDropEnd()
        {
            Rect rect = GUILayoutUtility.GetRect(0f, 0f, GUILayout.ExpandWidth(true));
            Event evt = Event.current;

            if (evt.type == EventType.DragUpdated)
            {
                DragAndDrop.visualMode = DragAndDropVisualMode.Generic;
                evt.Use();
            }
            else if (evt.type == EventType.DragPerform && Rect.MinMaxRect(rect.xMin, m_DragAndDropMinY, rect.xMax, rect.yMax).Contains(evt.mousePosition))
            {
                DragAndDrop.AcceptDrag();
                evt.Use();
                Material droppedMaterial = DragAndDrop.objectReferences[0] as Material;
                if (droppedMaterial && droppedMaterial != m_Material)
                {
                    PerformDrop(droppedMaterial);
                }
            }
            else if (evt.type == EventType.DragExited)
            {
                if (IsNewShader())
                    TMPro_EventManager.ON_MATERIAL_PROPERTY_CHANGED(true, m_Material);
            }
        }

        void PerformDrop(Material droppedMaterial)
        {
            Texture droppedTex = droppedMaterial.GetTexture(ShaderUtilities.ID_MainTex);
            if (!droppedTex)
            {
                return;
            }

            Texture currentTex = m_Material.GetTexture(ShaderUtilities.ID_MainTex);
            TMP_FontAsset requiredFontAsset = null;
            if (droppedTex != currentTex)
            {
                requiredFontAsset = TMP_EditorUtility.FindMatchingFontAsset(droppedMaterial);
                if (!requiredFontAsset)
                {
                    return;
                }
            }

            foreach (GameObject o in Selection.gameObjects)
            {
                if (requiredFontAsset)
                {
                    TMP_Text textComponent = o.GetComponent<TMP_Text>();
                    if (textComponent)
                    {
                        Undo.RecordObject(textComponent, "Font Asset Change");
                        textComponent.font = requiredFontAsset;
                    }
                }

                TMPro_EventManager.ON_DRAG_AND_DROP_MATERIAL_CHANGED(o, m_Material, droppedMaterial);
                EditorUtility.SetDirty(o);
            }
        }
    }
}
