using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using L10n = AvatarRefinery.Editor.Core.L10n;

namespace AvatarRefinery.Editor.Core.Rules
{
    /// <summary>マテリアルのシェーダー欠落(ピンク)と空スロット</summary>
    public sealed class MaterialRule : IRule
    {
        public string Id => "material.missing_shader";
        public bool CanRun(RuleContext ctx) => true;
        public IEnumerable<Issue> Evaluate(RuleContext ctx)
        {
            var missingShader = new List<(Renderer r, Material m)>();
            var emptySlots = new List<Renderer>();
            foreach (var r in ctx.GetAll<Renderer>())
            {
                var mats = r.sharedMaterials;
                for (int i = 0; i < mats.Length; i++)
                {
                    var m = mats[i];
                    if (m == null) { if (!emptySlots.Contains(r)) emptySlots.Add(r); continue; }
                    var sh = m.shader;
                    if (sh == null || sh.name == "Hidden/InternalErrorShader" || !sh.isSupported)
                        missingShader.Add((r, m));
                }
            }
            if (missingShader.Count > 0)
            {
                var names = missingShader.Select(x => x.m.name).Distinct().ToList();
                var issue = new Issue("material.missing_shader", Severity.Broken)
                {
                    Detail = (L10n.IsEnglish ? $"Materials with missing or unsupported shaders: {names.Count}: " : $"シェーダーが見つからないマテリアル {names.Count} 個: ") + string.Join(", ", names.Take(6)) + (names.Count > 6 ? " …" : ""),
                }.WithMetric("count", names.Count);
                foreach (var x in missingShader.Select(x => x.r).Distinct()) issue.WithTarget(x, ctx.Root);
                yield return issue;
            }
            if (emptySlots.Count > 0)
            {
                var issue = new Issue("material.slot_empty", Severity.Broken)
                {
                    Detail = (L10n.IsEnglish ? $"Renderers with empty material slots: {emptySlots.Count}: " : $"マテリアル未設定(None)のスロットがある Renderer {emptySlots.Count} 個: ") + string.Join(", ", emptySlots.Take(6).Select(r => r.name)),
                }.WithMetric("count", emptySlots.Count);
                foreach (var r in emptySlots) issue.WithTarget(r, ctx.Root);
                yield return issue;
            }
        }
    }

    /// <summary>
    /// テクスチャのインポート設定の読み取りヘルパ(診断ルールと製品版の修正で共有)。
    /// 「PC で非圧縮か」は、既定の Compression と Standalone のプラットフォーム上書き(明示フォーマット含む)から判定する。
    /// </summary>
    public static class TextureImportInfo
    {
        public const string PcPlatform = "Standalone";
        public const string AndroidPlatform = "Android";

        public static readonly HashSet<TextureFormat> CompressedFormats = new HashSet<TextureFormat>
        {
            TextureFormat.DXT1, TextureFormat.DXT5, TextureFormat.DXT1Crunched, TextureFormat.DXT5Crunched,
            TextureFormat.BC4, TextureFormat.BC5, TextureFormat.BC6H, TextureFormat.BC7,
            TextureFormat.ETC_RGB4, TextureFormat.ETC2_RGB, TextureFormat.ETC2_RGBA1, TextureFormat.ETC2_RGBA8,
            TextureFormat.ETC_RGB4Crunched, TextureFormat.ETC2_RGBA8Crunched, TextureFormat.EAC_R, TextureFormat.EAC_R_SIGNED, TextureFormat.EAC_RG, TextureFormat.EAC_RG_SIGNED,
            TextureFormat.ASTC_4x4, TextureFormat.ASTC_5x5, TextureFormat.ASTC_6x6, TextureFormat.ASTC_8x8, TextureFormat.ASTC_10x10, TextureFormat.ASTC_12x12,
            TextureFormat.ASTC_HDR_4x4, TextureFormat.ASTC_HDR_5x5, TextureFormat.ASTC_HDR_6x6, TextureFormat.ASTC_HDR_8x8, TextureFormat.ASTC_HDR_10x10, TextureFormat.ASTC_HDR_12x12,
            TextureFormat.PVRTC_RGB2, TextureFormat.PVRTC_RGB4, TextureFormat.PVRTC_RGBA2, TextureFormat.PVRTC_RGBA4,
        };

        /// <summary>プラットフォーム上書きで明示指定される非圧縮フォーマット</summary>
        public static bool IsExplicitUncompressedFormat(TextureImporterFormat f)
        {
            switch (f)
            {
                case TextureImporterFormat.Alpha8:
                case TextureImporterFormat.ARGB16:
                case TextureImporterFormat.RGB24:
                case TextureImporterFormat.RGBA32:
                case TextureImporterFormat.ARGB32:
                case TextureImporterFormat.RGB16:
                case TextureImporterFormat.R16:
                case TextureImporterFormat.R8:
                case TextureImporterFormat.RG16:
                case TextureImporterFormat.RGBA16:
                case TextureImporterFormat.RGBAHalf:
                case TextureImporterFormat.RGBAFloat:
                case TextureImporterFormat.RGB9E5:
                case TextureImporterFormat.RG32:
                case TextureImporterFormat.RGB48:
                case TextureImporterFormat.RGBA64:
                    return true;
                default:
                    return false;
            }
        }

        public static TextureImporter ImporterOf(Texture t)
        {
            var path = t != null ? AssetDatabase.GetAssetPath(t) : null;
            return string.IsNullOrEmpty(path) ? null : AssetImporter.GetAtPath(path) as TextureImporter;
        }

        /// <summary>指定プラットフォームで非圧縮になるインポート設定か(上書きが無ければ既定の Compression)</summary>
        public static bool IsUncompressedFor(TextureImporter imp, string platform)
        {
            var st = imp.GetPlatformTextureSettings(platform);
            if (st != null && st.overridden)
            {
                if (st.format != TextureImporterFormat.Automatic) return IsExplicitUncompressedFormat(st.format);
                return st.textureCompression == TextureImporterCompression.Uncompressed;
            }
            return imp.textureCompression == TextureImporterCompression.Uncompressed;
        }

        public static bool IsPcUncompressed(TextureImporter imp) => IsUncompressedFor(imp, PcPlatform);

        /// <summary>おおよその VRAM 使用量(byte)。圧縮形式ならおよそ 0.5〜1 byte/px、非圧縮は 4 byte/px。ミップ分 ×1.33。目安であり SDK の値が正。</summary>
        public static long EstimateBytes(Texture2D t)
        {
            double bpp = CompressedFormats.Contains(t.format) ? (t.format == TextureFormat.DXT1 || t.format == TextureFormat.DXT1Crunched || t.format == TextureFormat.BC4 || t.format == TextureFormat.ETC_RGB4 || t.format == TextureFormat.ETC2_RGB ? 0.5 : 1.0) : 4.0;
            double bytes = t.width * (double)t.height * bpp;
            if (t.mipmapCount > 1) bytes *= 1.3333;
            return (long)bytes;
        }

        /// <summary>圧縮(Normal Quality)にした場合の推定 VRAM(byte)。アルファ有り=DXT5(1 byte/px)、無し=DXT1(0.5 byte/px)。</summary>
        public static long EstimateCompressedBytes(int width, int height, bool hasAlpha, bool mipmaps)
        {
            double bytes = width * (double)height * (hasAlpha ? 1.0 : 0.5);
            if (mipmaps) bytes *= 1.3333;
            return (long)bytes;
        }
    }

    /// <summary>テクスチャのインポート状態: Streaming Mipmaps / 非圧縮 / 過大解像度</summary>
    public sealed class TextureRule : IRule
    {
        public string Id => "texture.uncompressed";
        public bool CanRun(RuleContext ctx) => true;

        private static HashSet<TextureFormat> Compressed => TextureImportInfo.CompressedFormats;

        public static IEnumerable<Texture> CollectTextures(RuleContext ctx)
        {
            var seen = new HashSet<Texture>();
            foreach (var r in ctx.GetAll<Renderer>())
            {
                foreach (var m in r.sharedMaterials)
                {
                    if (m == null || m.shader == null) continue;
                    foreach (var prop in m.GetTexturePropertyNames())
                    {
                        Texture t = null;
                        try { t = m.GetTexture(prop); } catch { }
                        if (t != null && seen.Add(t)) yield return t;
                    }
                }
            }
        }

        public static long EstimateBytes(Texture2D t) => TextureImportInfo.EstimateBytes(t);

        public IEnumerable<Issue> Evaluate(RuleContext ctx)
        {
            var uncompressed = new List<Texture2D>();
            var noStreaming = new List<Texture2D>();
            var oversized = new List<Texture2D>();
            int total = 0;
            foreach (var tex in CollectTextures(ctx))
            {
                var t = tex as Texture2D;
                if (t == null) continue;
                total++;
                var importer = TextureImportInfo.ImporterOf(t);

                bool isUncompressed = !Compressed.Contains(t.format);
                bool streaming = t.streamingMipmaps;
                if (importer != null)
                {
                    // TextureImporter がある場合はインポート設定を優先(プラットフォーム上書きは Standalone を見る。明示フォーマットも考慮)
                    isUncompressed = TextureImportInfo.IsPcUncompressed(importer);
                    streaming = importer.streamingMipmaps;
                }
                if (isUncompressed) uncompressed.Add(t);
                if (!streaming && t.mipmapCount > 1) noStreaming.Add(t);
                if (t.width >= 4096 || t.height >= 4096) oversized.Add(t);
            }

            if (noStreaming.Count > 0)
            {
                var issue = new Issue("texture.streaming_mipmaps_off", ctx.HasVrcSdk ? Severity.Blocker : Severity.Info)
                {
                    Detail = (L10n.IsEnglish ? $"Textures with Streaming Mip Maps disabled: {noStreaming.Count} / {total}: " : $"Streaming Mip Maps が無効なテクスチャ {noStreaming.Count} / {total} 枚: ") + string.Join(", ", noStreaming.Take(6).Select(t => t.name)) + (noStreaming.Count > 6 ? " …" : ""),
                    AutoFixable = true,
                }.WithMetric("count", noStreaming.Count).WithMetric("total", total);
                foreach (var t in noStreaming.Take(50)) issue.WithTarget(t);
                yield return issue;
            }
            if (uncompressed.Count > 0)
            {
                var ordered = uncompressed.OrderByDescending(EstimateBytes).ToList();
                double mb = ordered.Sum(EstimateBytes) / 1024.0 / 1024.0;
                var issue = new Issue("texture.uncompressed", Severity.Performance)
                {
                    Detail = (L10n.IsEnglish
                                ? $"Uncompressed textures: {uncompressed.Count} / {total} (estimated {mb:0} MB; compression typically uses about 1/4–1/8 as much memory)\n"
                                : $"非圧縮テクスチャ {uncompressed.Count} / {total} 枚(推定 {mb:0} MB。圧縮すると約 1/4〜1/8 に)\n") +
                             string.Join("\n", ordered.Take(8).Select(t => $"  - {t.name}  {t.width}x{t.height}  {t.format}  ≈{EstimateBytes(t) / 1024.0 / 1024.0:0.0} MB")) + (ordered.Count > 8 ? "\n  - …" : ""),
                    AutoFixable = true,
                }.WithMetric("count", uncompressed.Count).WithMetric("estimatedMB", mb.ToString("0.0"));
                foreach (var t in ordered.Take(50)) issue.WithTarget(t);
                yield return issue;
            }
            if (oversized.Count > 0)
            {
                var issue = new Issue("texture.oversized", Severity.Info)
                {
                    Detail = (L10n.IsEnglish ? $"Textures at least 4096 pixels: {oversized.Count}: " : $"4096 以上のテクスチャ {oversized.Count} 枚: ") + string.Join(", ", oversized.Take(6).Select(t => $"{t.name}({t.width}x{t.height})")),
                    AutoFixable = true,
                }.WithMetric("count", oversized.Count);
                foreach (var t in oversized.Take(50)) issue.WithTarget(t);
                yield return issue;
            }
        }
    }
}
