using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using FlatKit.Water;
using FlatKit.Editor;
using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;

public class FlatKitWaterEditor : ShaderGUI {
    private Gradient _gradient;

    private const string RenderingOptionsName = "Rendering Options";
    private GUIStyle _foldoutStyle;
    private static readonly Dictionary<string, bool> FoldoutStates =
        new Dictionary<string, bool> { { RenderingOptionsName, false } };

    public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties) {
        Material targetMaterial = materialEditor.target as Material;
        string[] keywords = targetMaterial.shaderKeywords;

        if (!targetMaterial.IsKeywordEnabled("_COLORMODE_LINEAR") &&
            !targetMaterial.IsKeywordEnabled("_COLORMODE_GRADIENT_TEXTURE")) {
            targetMaterial.EnableKeyword("_COLORMODE_LINEAR");
        }

        bool isColorModeGradient = targetMaterial.IsKeywordEnabled("_COLORMODE_GRADIENT_TEXTURE");

        int originalIntentLevel = EditorGUI.indentLevel;
        int foldoutRemainingItems = 0;
        bool latestFoldoutState = false;
        _foldoutStyle ??= new GUIStyle(EditorStyles.foldout) {
            fontStyle = FontStyle.Bold,
            margin = {
                left = -10
            },
            padding = {
                left = 20
            },
        };

        foreach (MaterialProperty property in properties) {
            bool skipProperty = false;

            if (isColorModeGradient) {
                skipProperty |= ShowColorGradientExportBox(materialEditor, property);
            }

            {
                var brackets = property.displayName.Split('[', ']');
                foreach (var bracket in brackets) {
                    if (bracket.Contains("FOLDOUT") || !property.displayName.Contains('[' + bracket + ']')) {
                        continue;
                    }

                    bool isNegative = bracket.StartsWith("!");
                    bool isPositive = !isNegative;
                    var param = bracket.TrimStart('!');
                    bool keywordOn = Array.IndexOf(keywords, param) != -1;

                    if (isPositive && !keywordOn) {
                        skipProperty = true;
                    }

                    if (isNegative && keywordOn) {
                        skipProperty = true;
                    }

                    if (skipProperty) {
                        break;
                    }
                }
            }

            // Foldouts.
            {
                string displayName = property.displayName;
                if (displayName.Contains("FOLDOUT")) {
                    string foldoutName = displayName.Split('(', ')')[1];
                    string foldoutItemCount = displayName.Split('{', '}')[1];
                    foldoutRemainingItems = Convert.ToInt32(foldoutItemCount);
                    FoldoutStates.TryAdd(property.name, false);
                    EditorGUILayout.Space();
                    FoldoutStates[property.name] =
                        EditorGUILayout.Foldout(FoldoutStates[property.name], foldoutName, true, _foldoutStyle);
                    latestFoldoutState = FoldoutStates[property.name];
                }

                if (foldoutRemainingItems > 0) {
                    skipProperty = skipProperty || !latestFoldoutState;
                    EditorGUI.indentLevel += 1;
                    --foldoutRemainingItems;
                }
            }

            bool hideInInspector = (property.GetShaderPropertyFlags() & ShaderPropertyFlags.HideInInspector) != 0;
            if (!skipProperty && !hideInInspector) {
                DrawStandard(materialEditor, property);
            }

            if (targetMaterial.IsKeywordEnabled("_COLORMODE_GRADIENT_TEXTURE") &&
                property.GetShaderPropertyType() == ShaderPropertyType.Texture &&
                property.displayName.StartsWith("[_COLORMODE_GRADIENT_TEXTURE]") &&
                property.textureValue != null) {
                GUILayout.Space(-50);
                using (new EditorGUILayout.HorizontalScope()) {
                    GUILayout.Space(15);
                    if (GUILayout.Button("Reset", EditorStyles.miniButtonLeft,
                            GUILayout.Width(60f), GUILayout.ExpandWidth(false))) {
                        property.textureValue = null;
                    }

                    GUILayout.FlexibleSpace();
                }

                EditorGUILayout.Space(60);
            }

            EditorGUI.indentLevel = originalIntentLevel;
        }

        EditorGUILayout.Space();
        FoldoutStates[RenderingOptionsName] =
            EditorGUILayout.Foldout(FoldoutStates[RenderingOptionsName], RenderingOptionsName, true, _foldoutStyle);
        if (FoldoutStates[RenderingOptionsName]) {
            EditorGUI.indentLevel += 1;
            DrawOpaqueField(targetMaterial);
            DrawQueueOffsetField(properties, targetMaterial);
        }
    }

    private void DrawOpaqueField(Material material) {
        var opaque = EditorGUILayout.Toggle("Opaque", material.GetTag("RenderType", false) == "Opaque");
        if (opaque) {
            material.SetOverrideTag("RenderType", "Opaque");
            material.SetInt("_ZWrite", 1);
            material.renderQueue = (int)RenderQueue.Geometry;
        } else {
            material.SetOverrideTag("RenderType", "Transparent");
            material.SetInt("_ZWrite", 0);
            material.renderQueue = (int)RenderQueue.Transparent;
        }
    }

    private void DrawQueueOffsetField(MaterialProperty[] properties, Material material) {
        GUIContent queueSlider = new GUIContent("Priority",
            "Determines the chronological rendering order for a Material. High values are rendered first.");
        const int queueOffsetRange = 50;
        MaterialProperty queueOffsetProp = FindProperty("_QueueOffset", properties, false);
        if (queueOffsetProp == null) return;
        EditorGUI.BeginChangeCheck();
        EditorGUI.showMixedValue = queueOffsetProp.hasMixedValue;
        var queue = EditorGUILayout.IntSlider(queueSlider, (int)queueOffsetProp.floatValue, -queueOffsetRange,
            queueOffsetRange);
        if (EditorGUI.EndChangeCheck())
            queueOffsetProp.floatValue = queue;
        EditorGUI.showMixedValue = false;
        material.renderQueue += queue;
    }

    private void DrawStandard(MaterialEditor materialEditor, MaterialProperty property) {
        string displayName = property.displayName;

        // Remove everything in brackets.
        displayName = Regex.Replace(displayName, @" ?\[.*?\]", string.Empty);

        Tooltips.map.TryGetValue(displayName.Trim(), out string tooltip);
        displayName = Regex.Replace(displayName, @" ?\{.*?\}", string.Empty);

        var guiContent = new GUIContent(displayName, tooltip);

        if (property.GetShaderPropertyType() == ShaderPropertyType.Texture) {
            materialEditor.TexturePropertySingleLine(guiContent, property);
        } else {
            materialEditor.ShaderProperty(property, guiContent);
        }
    }

    private bool ShowColorGradientExportBox(MaterialEditor materialEditor, MaterialProperty property) {
        bool isGradientTexture = property.GetShaderPropertyType() == ShaderPropertyType.Texture &&
                                 property.displayName.StartsWith("[_COLORMODE_GRADIENT_TEXTURE]");
        if (isGradientTexture) {
            if (property.textureValue != null) {
                return false;
            }
        } else {
            return false;
        }

        var messageContent =
            EditorGUIUtility.TrTextContent(
                "Before the gradient can be used it needs to be exported as a texture.");
        var buttonContent = EditorGUIUtility.TrTextContent("Export");
        bool buttonPressed = GradientBoxWithButton(messageContent, buttonContent);
        if (buttonPressed) {
            var texture = GradientToTexture(_gradient);
            PromptTextureSave(materialEditor, texture, property.name);
        }

        return true;
    }

    private bool GradientBoxWithButton(GUIContent messageContent, GUIContent buttonContent) {
        float boxHeight = 40f;
        EditorGUILayout.Space(5);
        Rect rect = GUILayoutUtility.GetRect(messageContent, EditorStyles.helpBox);
        GUILayoutUtility.GetRect(0f, boxHeight);
        rect.height += boxHeight;
        var style = new GUIStyle(EditorStyles.helpBox);
        style.fontSize += 2;
        GUI.Label(rect, messageContent, style);
        float secondLineHeight = 20f;
        float secondLineY = rect.yMax - secondLineHeight - 15f;
        var buttonPosition = new Rect(rect.xMax - 60f - 4f, secondLineY, 60f, secondLineHeight);
        bool result = GUI.Button(buttonPosition, buttonContent);

        var gradientPosition = new Rect(rect.xMin + 8f, secondLineY, rect.width - 60f - 18f, secondLineHeight);
        if (_gradient == null) {
            _gradient = new Gradient();
        }

        _gradient = EditorGUI.GradientField(gradientPosition, _gradient);
        EditorGUILayout.Space(10);
        return result;
    }

    private Texture2D GradientToTexture(Gradient g) {
        const int width = 256;

        Texture2D texture = new Texture2D(width, 1, TextureFormat.RGBA32, /*mipChain=*/false) {
            name = "Flat Kit Water Color Gradient",
            wrapMode = TextureWrapMode.Clamp,
            hideFlags = HideFlags.HideAndDontSave,
            filterMode = FilterMode.Point
        };
        for (float x = 0;
             x < width;
             x++) {
            Color32 color = g.Evaluate(x / (width - 1));
            texture.SetPixel(Mathf.CeilToInt(x), 0, color);
        }

        texture.Apply();
        return texture;
    }

    private void PromptTextureSave(MaterialEditor materialEditor, Texture2D texture, string propertyName) {
        Material material = materialEditor.target as Material;
        if (material == null) {
            return;
        }

        var pngNameWithExtension = $"{materialEditor.target.name}{propertyName}.png";

        var fullPath =
            EditorUtility.SaveFilePanel("Save Gradient Texture", "Assets", pngNameWithExtension, "png");
        if (fullPath.Length > 0) {
            SaveTextureAsPng(texture, fullPath, FilterMode.Point);
            var loadedTexture = LoadTexture(fullPath);
            if (loadedTexture != null) {
                material.SetTexture(propertyName, loadedTexture);
            } else {
                Debug.LogWarning($"Could not load the texture from {fullPath}");
            }
        }
    }

    private void SaveTextureAsPng(Texture2D texture, string fullPath, FilterMode filterMode) {
        byte[] bytes = texture.EncodeToPNG();
        if (bytes == null) {
            Debug.LogError("Could not encode texture as PNG.");
            return;
        }

        File.WriteAllBytes(fullPath, bytes);
        AssetDatabase.Refresh();
        Debug.Log($"Texture saved as: {fullPath}");

        string pathRelativeToAssets = ConvertFullPathToAssetPath(fullPath);
        if (pathRelativeToAssets.Length == 0) {
            Debug.LogWarning($"Could not save the texture to {fullPath}.");
        }

        TextureImporter importer = (TextureImporter)TextureImporter.GetAtPath(pathRelativeToAssets);
        Debug.Assert(importer != null,
            $"[FlatKit] Could not create importer at {pathRelativeToAssets}.");
        if (importer != null) {
            importer.filterMode = filterMode;
            importer.textureType = TextureImporterType.Default;
            importer.textureCompression = TextureImporterCompression.Uncompressed;
            importer.mipmapEnabled = false;
            var textureSettings = new TextureImporterPlatformSettings {
                format = TextureImporterFormat.RGBA32
            };
            importer.SetPlatformTextureSettings(textureSettings);
            EditorUtility.SetDirty(importer);
            importer.SaveAndReimport();
        }

        Debug.Assert(importer != null,
            $"[FlatKit] Could not change import settings of {fullPath} [{pathRelativeToAssets}]");
    }

    private static Texture2D LoadTexture(string fullPath) {
        string pathRelativeToAssets = ConvertFullPathToAssetPath(fullPath);
        if (pathRelativeToAssets.Length == 0) {
            return null;
        }

        var loadedTexture = AssetDatabase.LoadAssetAtPath(pathRelativeToAssets, typeof(Texture2D)) as Texture2D;
        if (loadedTexture == null) {
            Debug.LogError($"[FlatKit] Could not load texture from {fullPath} [{pathRelativeToAssets}].");
            return null;
        }

        loadedTexture.filterMode = FilterMode.Point;
        loadedTexture.wrapMode = TextureWrapMode.Clamp;
        return loadedTexture;
    }

    private static string ConvertFullPathToAssetPath(string fullPath) {
        int count = (Directory.GetCurrentDirectory() + System.IO.Path.DirectorySeparatorChar).Length;
        return fullPath.Remove(0, count);
    }
}