using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

namespace eagle {
    internal static class BeastMaterialCompatibility {
        private const string UniversalLitShaderName = "Universal Render Pipeline/Lit";
        private const string UniversalParticleUnlitShaderName =
                "Universal Render Pipeline/Particles/Unlit";
        private const string BuiltInParticleStandardUnlitShaderName = "Particles/Standard Unlit";
        private static readonly string[] BaseTexturePropertyNames =
                { "_MainTex", "_BaseMap", "_Albedo", "_TextureSample0" };
        private static readonly Dictionary<Material, Texture2D> BakedPaletteTextures = new();
        private static readonly PaletteEntry[] PolytopePaletteEntries = {
            new(new Color(0.4980392f, 1f, 1f), "_FEATHERS2COLOR", "_FEATHERS2COLOR1"),
            new(new Color(0.4980392f, 1f, 0.4980392f), "_FEATHERS1COLOR", "_FEATHERS1COLOR1"),
            new(new Color(0f, 0f, 1f), "_CLOTH4COLOR", "_CLOTH4COLOR1"),
            new(new Color(0f, 1f, 1f), "_CLOTH3COLOR", "_CLOTH3COLOR1"),
            new(new Color(0f, 1f, 0f), "_CLOTH2COLOR", "_CLOTH2COLOR1"),
            new(new Color(0f, 0.4980392f, 0f), "_CLOTH1COLOR", "_CLOTH1COLOR1"),
            new(new Color(1f, 0.4980392f, 0.4980392f), "_LEATHER4COLOR", "_LEATHER4COLOR1"),
            new(new Color(1f, 1f, 0.4980392f), "_LEATHER3COLOR", "_LEATHER3COLOR1"),
            new(new Color(1f, 0f, 1f), "_LEATHER2COLOR", "_LEATHER2COLOR1"),
            new(new Color(1f, 0.4980392f, 1f), "_LEATHER1COLOR", "_LEATHER1COLOR1"),
            new(new Color(0.4980392f, 0.4980392f, 1f), "_METAL4COLOR", "_METAL4COLOR1"),
            new(new Color(0f, 0.4980392f, 0.4980392f), "_METAL3COLOR", "_METAL3COLOR1"),
            new(new Color(0f, 0f, 0.4980392f), "_METAL2COLOR", "_METAL2COLOR1"),
            new(new Color(0.4980392f, 0f, 0.4980392f), "_METAL1COLOR", "_METAL1COLOR1"),
            new(new Color(1f, 1f, 0f), "_OTHERCOLOR", "_OTHERCOLOR1"),
            new(new Color(0.4980392f, 0.4980392f, 0f), "_LIPSCOLOR", "_LIPSCOLOR1"),
            new(new Color(0.4980392f, 0.4980392f, 0.4980392f), "_SCLERACOLOR", "_SCLERACOLOR1"),
            new(new Color(1f, 0f, 0f), "_EYESCOLOR", "_EYESCOLOR1"),
            new(new Color(1f, 0.4980392f, 0f), "_HAIRCOLOR", "_HAIRCOLOR1"),
            new(new Color(0.4980392f, 0f, 0f), "_SKINCOLOR", "_SKINCOLOR1")
        };

        public static void ConfigureRenderer(Renderer renderer) {
            if (!IsUniversalRenderPipelineActive()) { return; }

            var sourceMaterials = renderer.sharedMaterials;
            var materials = renderer.materials;
            var changed = false;
            for (var i = 0; i < materials.Length; i++) {
                if (!ShouldReplaceMaterial(materials[i])) { continue; }

                var sourceMaterial = i < sourceMaterials.Length ? sourceMaterials[i] : materials[i];
                materials[i] = CreateUniversalLitMaterial(sourceMaterial);
                changed = true;
            }

            if (changed) { renderer.materials = materials; }
        }

        public static Material CreateParticleMaterial(Material source, Texture texture) {
            if (!IsUniversalRenderPipelineActive()) {
                var builtInMaterial =
                        source != null ? new Material(source)
                                       : CreateMaterial(BuiltInParticleStandardUnlitShaderName);
                if (builtInMaterial != null && texture != null) {
                    builtInMaterial.mainTexture = texture;
                }
                return builtInMaterial;
            }

            var material = CreateMaterial(UniversalParticleUnlitShaderName) ??
                           CreateMaterial("Universal Render Pipeline/Unlit");
            if (material == null) { return source != null ? new Material(source) : null; }

            CopyBaseTextureAndColor(source, material, texture);
            material.SetFloat("_Surface", 1f);
            material.SetFloat("_Blend", 0f);
            material.SetInt("_SrcBlend", (int)BlendMode.SrcAlpha);
            material.SetInt("_DstBlend", (int)BlendMode.OneMinusSrcAlpha);
            material.SetInt("_ZWrite", 0);
            material.EnableKeyword("_SURFACE_TYPE_TRANSPARENT");
            material.renderQueue = (int)RenderQueue.Transparent;
            return material;
        }

        private static bool ShouldReplaceMaterial(Material material) {
            if (material == null || material.shader == null) { return false; }

            var shaderName = material.shader.name;
            if (shaderName.StartsWith("Universal Render Pipeline/") ||
                shaderName.StartsWith("Sprites/")) {
                return false;
            }

            return true;
        }

        private static Material CreateUniversalLitMaterial(Material source) {
            var shader = Shader.Find(UniversalLitShaderName);
            if (shader == null) { return new Material(source); }

            var material = new Material(shader);
            if (TryCreatePolytopePaletteTexture(source, out var paletteTexture)) {
                CopyBaseTextureAndColor(source, material, paletteTexture);
            } else {
                CopyBaseTextureAndColor(source, material, null);
            }
            return material;
        }

        private static Material CreateMaterial(string shaderName) {
            var shader = Shader.Find(shaderName);
            return shader != null ? new Material(shader) : null;
        }

        private static void
        CopyBaseTextureAndColor(Material source, Material target, Texture overrideTexture) {
            var texture = overrideTexture;
            if (texture == null && source != null) {
                texture = FindFirstTexture(source, BaseTexturePropertyNames);
            }

            if (texture != null) {
                if (target.HasProperty("_BaseMap")) { target.SetTexture("_BaseMap", texture); }
                if (target.HasProperty("_MainTex")) { target.SetTexture("_MainTex", texture); }
            }

            var color = Color.white;
            if (source != null) {
                if (source.HasProperty("_Color")) {
                    color = source.GetColor("_Color");
                } else if (source.HasProperty("_BaseColor")) {
                    color = source.GetColor("_BaseColor");
                }
            }

            if (target.HasProperty("_BaseColor")) { target.SetColor("_BaseColor", color); }
            if (target.HasProperty("_Color")) { target.SetColor("_Color", color); }
        }

        private static Texture FindFirstTexture(Material source, string[] propertyNames) {
            foreach (var propertyName in propertyNames) {
                if (!source.HasProperty(propertyName)) { continue; }

                var texture = source.GetTexture(propertyName);
                if (texture != null) { return texture; }
            }

            return null;
        }

        private static bool TryCreatePolytopePaletteTexture(
                Material source,
                out Texture2D texture) {
            texture = null;
            if (source == null || !HasAnyProperty(source, "_SKINCOLOR", "_SKINCOLOR1")) {
                return false;
            }

            var maskTexture =
                    GetTexture(source, "_TextureSample0") ?? GetTexture(source, "_TextureSample1");
            var shadeTexture =
                    GetTexture(source, "_TextureSample2") ?? GetTexture(source, "_TextureSample3");
            if (maskTexture == null || shadeTexture == null) { return false; }

            if (BakedPaletteTextures.TryGetValue(source, out texture)) { return true; }

            var maskPixels = ReadTexturePixels(maskTexture);
            var shadePixels = ReadTexturePixels(shadeTexture);
            var width = Mathf.Min(maskPixels.width, shadePixels.width);
            var height = Mathf.Min(maskPixels.height, shadePixels.height);
            texture = new Texture2D(width, height, TextureFormat.RGBA32, true);
            texture.name = $"{source.name}_URP_BakedPalette";
            texture.wrapMode = maskTexture.wrapMode;
            texture.filterMode = maskTexture.filterMode;

            for (var y = 0; y < height; y++) {
                for (var x = 0; x < width; x++) {
                    var maskColor = maskPixels.texture.GetPixel(x, y);
                    var shadeColor = shadePixels.texture.GetPixel(x, y);
                    var paletteColor = ResolvePolytopePaletteColor(source, maskColor);
                    texture.SetPixel(x, y, Multiply(shadeColor, paletteColor));
                }
            }

            texture.Apply(updateMipmaps: true, makeNoLongerReadable: false);
            BakedPaletteTextures[source] = texture;
            DestroyIfTemporary(maskPixels.texture, maskTexture);
            DestroyIfTemporary(shadePixels.texture, shadeTexture);
            return true;
        }

        private static Color ResolvePolytopePaletteColor(Material source, Color maskColor) {
            var bestDistance = float.MaxValue;
            var bestColor = Color.white;
            foreach (var entry in PolytopePaletteEntries) {
                var distance = ColorDistance(maskColor, entry.MaskColor);
                if (distance >= bestDistance) { continue; }

                bestDistance = distance;
                bestColor = GetColor(source, entry.PropertyName, entry.AlternatePropertyName);
            }

            return bestDistance <= 0.12f ? bestColor : Color.white;
        }

        private static Color
        GetColor(Material source, string propertyName, string alternatePropertyName) {
            if (source.HasProperty(propertyName)) { return source.GetColor(propertyName); }
            if (source.HasProperty(alternatePropertyName)) {
                return source.GetColor(alternatePropertyName);
            }

            return Color.white;
        }

        private static Texture GetTexture(Material source, string propertyName) {
            return source.HasProperty(propertyName) ? source.GetTexture(propertyName) : null;
        }

        private static bool HasAnyProperty(Material source, params string[] propertyNames) {
            foreach (var propertyName in propertyNames) {
                if (source.HasProperty(propertyName)) { return true; }
            }

            return false;
        }

        private static TexturePixels ReadTexturePixels(Texture source) {
            var renderTexture = RenderTexture.GetTemporary(
                    source.width,
                    source.height,
                    0,
                    RenderTextureFormat.ARGB32,
                    RenderTextureReadWrite.Default);
            var previous = RenderTexture.active;
            Graphics.Blit(source, renderTexture);
            RenderTexture.active = renderTexture;

            var texture = new Texture2D(source.width, source.height, TextureFormat.RGBA32, false);
            texture.ReadPixels(new Rect(0, 0, source.width, source.height), 0, 0);
            texture.Apply();

            RenderTexture.active = previous;
            RenderTexture.ReleaseTemporary(renderTexture);
            return new TexturePixels(texture);
        }

        private static Color Multiply(Color a, Color b) {
            return new Color(a.r * b.r, a.g * b.g, a.b * b.b, a.a);
        }

        private static float ColorDistance(Color a, Color b) {
            var r = a.r - b.r;
            var g = a.g - b.g;
            var blue = a.b - b.b;
            return Mathf.Sqrt(r * r + g * g + blue * blue);
        }

        private static void DestroyIfTemporary(Texture2D texture, Texture original) {
            if (texture != original) { Object.Destroy(texture); }
        }

        private static bool IsUniversalRenderPipelineActive() {
            var pipeline = GraphicsSettings.currentRenderPipeline;
            return pipeline != null && pipeline.GetType().Name.Contains("UniversalRenderPipeline");
        }

        private readonly struct PaletteEntry {
            public readonly Color MaskColor;
            public readonly string PropertyName;
            public readonly string AlternatePropertyName;

            public PaletteEntry(
                    Color maskColor,
                    string propertyName,
                    string alternatePropertyName) {
                MaskColor = maskColor;
                PropertyName = propertyName;
                AlternatePropertyName = alternatePropertyName;
            }
        }

        private readonly struct TexturePixels {
            public readonly Texture2D texture;
            public readonly int width;
            public readonly int height;

            public TexturePixels(Texture2D texture) {
                this.texture = texture;
                width = texture.width;
                height = texture.height;
            }
        }
    }
}
