#if UNITY_EDITOR
namespace NiAkaDev.EzMirror.Editor
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using UdonSharpEditor;
    using UnityEditor;
    using UnityEngine;
    using UnityEngine.UI;
    using VRC.Dynamics;
    using VRC.SDK3.Components;
    using VRC.SDK3.Dynamics.Contact.Components;
    using VRC.SDKBase;
    using VRC.Udon;

    public partial class EzMirrorEditor : UnityEditor.Editor
    {
        private void DrawProfileMaterialGenerationControls()
        {
            using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
            {
                EditorGUILayout.LabelField(
                    L("ProfileMaterialGenerationHeader", "生成Material"),
                    EditorStyles.boldLabel
                );
                EditorGUILayout.HelpBox(
                    L("ProfileMaterialGenerationHelp", "プロファイル名や画質設定を確認したら、ここでMaterialを自動生成/更新できます。空やMissingのMaterialも再生成します。"),
                    MessageType.Info
                );

                if (GUILayout.Button(L("GenerateMaterialButton", "Materialを自動生成/外周に合わせて更新")))
                {
                    Material[] materials = GetUniqueProfileFadeMaterials();
                    TryGetCommonFadeRanges(
                        materials,
                        out float distanceRange,
                        out float horizontalRange
                    );
                    ApplyTriggerAlignedFadeToTargets(distanceRange, horizontalRange, false);
                    GUIUtility.ExitGUI();
                }

                DrawGeneratedMaterialCleanupControls();
            }
        }

        private void DrawGeneratedMaterialCleanupControls()
        {
            EditorGUILayout.Space();
            EditorGUILayout.LabelField(L("DeveloperDangerZone", "危険操作"), EditorStyles.boldLabel);
            EditorGUILayout.HelpBox(
                L(
                    "DeveloperDangerZoneHelp",
                    "生成物の掃除や強制再生成など、通常は不要で影響範囲が大きい操作です。対象を確認してから実行してください。"
                ),
                MessageType.Warning
            );
            EditorGUILayout.HelpBox(
                L(
                    "GeneratedMaterialCleanupHelp",
                    "未参照の生成Materialだけを確認してから掃除できます。現在開いているScene / PrefabとProject内依存関係で参照中のMaterialは候補から除外します。"
                ),
                MessageType.None
            );

            if (GUILayout.Button(L("PreviewUnusedGeneratedMaterialsButton", "未参照生成Materialを確認")))
            {
                generatedMaterialCleanupPreviewPaths =
                    EzMirrorGeneratedMaterialCleanup.PreviewUnusedGeneratedMaterialPaths();
            }

            if (generatedMaterialCleanupPreviewPaths == null)
                return;

            EditorGUILayout.LabelField(
                L("UnusedGeneratedMaterialCountLabel", "掃除候補数"),
                generatedMaterialCleanupPreviewPaths.Length.ToString()
            );

            if (generatedMaterialCleanupPreviewPaths.Length == 0)
                return;

            EditorGUILayout.TextArea(
                string.Join("\n", generatedMaterialCleanupPreviewPaths),
                GUILayout.MinHeight(54f),
                GUILayout.MaxHeight(140f)
            );

            if (!GUILayout.Button(L("DeleteUnusedGeneratedMaterialsButton", "確認した候補をゴミ箱へ移動")))
                return;

            bool confirmed = EditorUtility.DisplayDialog(
                L("DeleteUnusedGeneratedMaterialsDialogTitle", "未参照生成Materialの掃除"),
                LF(
                    "DeleteUnusedGeneratedMaterialsDialogMessage",
                    "{0} 件の未参照生成Materialをゴミ箱へ移動します。参照状態を再確認してから実行します。",
                    generatedMaterialCleanupPreviewPaths.Length
                ),
                L("DeleteUnusedGeneratedMaterialsDialogOk", "ゴミ箱へ移動"),
                L("CancelButton", "キャンセル")
            );
            if (!confirmed)
                return;

            int deletedCount = EzMirrorGeneratedMaterialCleanup.DeleteUnusedGeneratedMaterials(
                generatedMaterialCleanupPreviewPaths
            );
            generatedMaterialCleanupPreviewPaths =
                EzMirrorGeneratedMaterialCleanup.PreviewUnusedGeneratedMaterialPaths();
            Debug.Log("[EzMirror] Deleted unused generated materials: " + deletedCount);
        }

        private void DrawCommonFadeDistanceSettings()
        {
            EditorGUILayout.HelpBox(
                L("FadeWidthHelp", "ユーザーが設定するのはフェード幅だけです。フェード終了位置は表示範囲の外周に自動で合わせます。"),
                MessageType.Info
            );

            Material[] materials = GetUniqueProfileFadeMaterials();
            TryGetCommonFadeRanges(materials, out float distanceRange, out float horizontalRange);

            EzMirror activeMirror = target as EzMirror;
            if (
                activeMirror == null
                || activeMirror.triggerCollider == null
                || GetFadeReferenceTransform(activeMirror, activeMirror.triggerCollider) == null
            )
            {
                EditorGUILayout.HelpBox(
                    L("MissingFadeReferences", "表示範囲または表示Rendererが未設定です。先に「基本セットアップを自動実行」を押してください。"),
                    MessageType.Warning
                );
            }
            else
            {
                DrawTriggerAlignedFadePreview(activeMirror, distanceRange, horizontalRange);
            }

            DrawFadeWidthFields(distanceRange, horizontalRange);
        }

        private void DrawFadeWidthFields(float distanceRange, float horizontalRange)
        {
            EditorGUI.BeginChangeCheck();

            float newDistanceRange = EditorGUILayout.FloatField(
                LC("DistanceFadeWidthLabel", "前後フェード幅", "DistanceFadeWidthTooltip", "表示範囲の前面外周へ向かって消える距離です。"),
                Mathf.Max(0.01f, distanceRange)
            );
            float newHorizontalRange = EditorGUILayout.FloatField(
                LC("HorizontalFadeWidthLabel", "左右フェード幅", "HorizontalFadeWidthTooltip", "表示範囲の左右外周へ向かって消える距離です。"),
                Mathf.Max(0.01f, horizontalRange)
            );

            if (EditorGUI.EndChangeCheck())
            {
                ApplyTriggerAlignedFadeToTargets(newDistanceRange, newHorizontalRange, false);
            }
        }

        private static void DrawTriggerAlignedFadePreview(
            EzMirror mirror,
            float distanceRange,
            float horizontalRange
        )
        {
            Transform fadeTransform = GetFadeReferenceTransform(mirror, mirror.triggerCollider);
            if (
                !TryCalculateTriggerAlignedFadeDistances(
                    mirror.triggerCollider,
                    fadeTransform,
                    distanceRange,
                    horizontalRange,
                    out float distanceStart,
                    out float distanceComplete,
                    out float horizontalStart,
                    out float horizontalComplete
                )
            )
                return;
            TryCalculateTriggerAlignedHorizontalFadeCenter(
                mirror.triggerCollider,
                fadeTransform,
                out float horizontalCenter
            );

            EditorGUILayout.HelpBox(
                LF(
                    "FadePreviewText",
                    "前後: {0:0.##}m からフェード開始 / {1:0.##}m でフェード終了（表示範囲外周）です。\n左右: 中心 {2:0.##}m / 幅 {3:0.##}m からフェード開始 / 幅 {4:0.##}m でフェード終了（表示範囲外周）です。",
                    distanceStart,
                    distanceComplete,
                    horizontalCenter,
                    horizontalStart,
                    horizontalComplete
                ),
                MessageType.None
            );
        }

        private void ApplyTriggerAlignedFadeToTargets(
            float distanceRange,
            float horizontalRange,
            bool forceRegenerateMaterials
        )
        {
            for (int i = 0; i < targets.Length; i++)
            {
                EzMirror mirror = targets[i] as EzMirror;
                if (mirror == null)
                    continue;

                Undo.RecordObject(mirror, "Update EzMirror Fade Materials");
                EnsureGeneratedProfileMaterials(mirror, forceRegenerateMaterials);
                ApplyTriggerAlignedFadeToMaterials(mirror, distanceRange, horizontalRange, true);
                EnsureEditorPreviewMaterial(mirror, true);
                UdonSharpEditorUtility.CopyProxyToUdon(mirror);
                EditorUtility.SetDirty(mirror);
                PrefabUtility.RecordPrefabInstancePropertyModifications(mirror);
            }

            serializedObject.Update();
        }

        internal static bool EnsureDedicatedGeneratedProfileMaterials(EzMirror mirror)
        {
            if (mirror == null)
                return false;

            bool hasFadeRanges = TryGetCommonFadeRanges(
                mirror.profileMaterials,
                out float distanceRange,
                out float horizontalRange
            );
            bool changed = EnsureGeneratedProfileMaterials(mirror, false, false);
            if (
                !hasFadeRanges
                && TryGetCommonFadeRanges(
                    mirror.profileMaterials,
                    out float generatedDistanceRange,
                    out float generatedHorizontalRange
                )
            )
            {
                distanceRange = generatedDistanceRange;
                horizontalRange = generatedHorizontalRange;
                hasFadeRanges = true;
            }

            if (hasFadeRanges)
                changed |= ApplyTriggerAlignedFadeToMaterials(mirror, distanceRange, horizontalRange, false);
            changed |= EnsurePersistentRuntimeRendererMaterial(mirror);

            if (!changed)
                return false;

            UdonSharpEditorUtility.CopyProxyToUdon(mirror);
            EditorUtility.SetDirty(mirror);
            return true;
        }

        private static bool ApplyTriggerAlignedFadeToMaterials(
            EzMirror mirror,
            float distanceRange,
            float horizontalRange,
            bool registerUndo
        )
        {
            if (mirror == null || mirror.triggerCollider == null || mirror.profileMaterials == null)
                return false;

            Transform fadeTransform = GetFadeReferenceTransform(mirror, mirror.triggerCollider);
            if (
                !TryCalculateTriggerAlignedFadeDistances(
                    mirror.triggerCollider,
                    fadeTransform,
                    distanceRange,
                    horizontalRange,
                    out float distanceStart,
                    out float distanceComplete,
                    out float horizontalStart,
                    out float horizontalComplete
                )
            )
                return false;
            TryCalculateTriggerAlignedHorizontalFadeCenter(
                mirror.triggerCollider,
                fadeTransform,
                out float horizontalCenter
            );

            float resolvedDistanceRange = Mathf.Max(0.01f, distanceComplete - distanceStart);
            float resolvedHorizontalRange = Mathf.Max(0.01f, horizontalComplete - horizontalStart);
            bool changed = false;
            for (int i = 1; i < mirror.profileMaterials.Length; i++)
            {
                Material material = mirror.profileMaterials[i];
                if (material == null)
                    continue;
                if (IsEditorPreviewBaseMaterial(material))
                    continue;

                bool needsUpdate = false;
                if (HasDistanceFadeProperties(material))
                {
                    needsUpdate |= NeedsMaterialFloatUpdate(material, DistanceFadeStartProperty, distanceStart);
                    needsUpdate |= NeedsMaterialFloatUpdate(
                        material,
                        DistanceFadeRangeProperty,
                        resolvedDistanceRange
                    );
                }
                if (HasHorizontalFadeProperties(material))
                {
                    if (material.HasProperty(HorizontalFadeCenterOffsetProperty))
                    {
                        needsUpdate |= NeedsMaterialFloatUpdate(
                            material,
                            HorizontalFadeCenterOffsetProperty,
                            horizontalCenter
                        );
                    }
                    needsUpdate |= NeedsMaterialFloatUpdate(
                        material,
                        HorizontalFadeStartProperty,
                        horizontalStart
                    );
                    needsUpdate |= NeedsMaterialFloatUpdate(
                        material,
                        HorizontalFadeRangeProperty,
                        resolvedHorizontalRange
                    );
                }
                if (material.HasProperty("_HideBackground"))
                {
                    bool hideBackground = mirror.profileHideBackground != null
                        && i < mirror.profileHideBackground.Length
                        && mirror.profileHideBackground[i];
                    needsUpdate |= NeedsMaterialFloatUpdate(
                        material,
                        "_HideBackground",
                        hideBackground ? 1f : 0f
                    );
                }
                if (!needsUpdate)
                    continue;

                if (registerUndo)
                    Undo.RecordObject(material, "Update EzMirror Fade Material");
                if (HasDistanceFadeProperties(material))
                {
                    material.SetFloat(DistanceFadeStartProperty, distanceStart);
                    material.SetFloat(DistanceFadeRangeProperty, resolvedDistanceRange);
                }
                if (HasHorizontalFadeProperties(material))
                {
                    if (material.HasProperty(HorizontalFadeCenterOffsetProperty))
                        material.SetFloat(HorizontalFadeCenterOffsetProperty, horizontalCenter);
                    material.SetFloat(HorizontalFadeStartProperty, horizontalStart);
                    material.SetFloat(HorizontalFadeRangeProperty, resolvedHorizontalRange);
                }
                if (material.HasProperty("_HideBackground"))
                {
                    bool hideBackground = mirror.profileHideBackground != null
                        && i < mirror.profileHideBackground.Length
                        && mirror.profileHideBackground[i];
                    material.SetFloat("_HideBackground", hideBackground ? 1f : 0f);
                }
                EditorUtility.SetDirty(material);
                changed = true;
            }
            return changed;
        }

        private static void EnsureGeneratedProfileMaterials(EzMirror mirror, bool forceRegenerate)
        {
            EnsureGeneratedProfileMaterials(mirror, forceRegenerate, true);
        }

        private static bool EnsureGeneratedProfileMaterials(
            EzMirror mirror,
            bool forceRegenerate,
            bool registerUndo
        )
        {
            if (mirror == null)
                return false;

            int profileCount = Mathf.Max(1, GetEditorProfileCount(mirror));
            string ownerKey = GetGeneratedMaterialOwnerKey(mirror);
            bool changed =
                mirror.profileMaterials == null
                || mirror.profileMaterials.Length != profileCount
                || mirror.profileBaseMaterials == null
                || mirror.profileBaseMaterials.Length != profileCount
                || (mirror.profileMaterials.Length > 0 && mirror.profileMaterials[0] != null)
                || (mirror.profileBaseMaterials.Length > 0 && mirror.profileBaseMaterials[0] != null);
            mirror.profileMaterials = ResizeCopy(mirror.profileMaterials, profileCount);
            mirror.profileBaseMaterials = ResizeCopy(mirror.profileBaseMaterials, profileCount);
            mirror.profileMaterials[0] = null;
            mirror.profileBaseMaterials[0] = null;

            for (int i = 1; i < profileCount; i++)
            {
                if (mirror.profileBaseMaterials[i] == null)
                {
                    mirror.profileBaseMaterials[i] = GetDefaultBaseMaterialForProfile(i);
                    changed = true;
                }

                Material generatedMaterial = EnsureGeneratedProfileMaterial(
                    mirror,
                    ownerKey,
                    i,
                    forceRegenerate,
                    registerUndo,
                    out bool materialChanged,
                    out bool preserveSourceHideBackground
                );
                if (generatedMaterial == null)
                    continue;

                if (mirror.profileMaterials[i] != generatedMaterial)
                {
                    mirror.profileMaterials[i] = generatedMaterial;
                    changed = true;
                }
                changed |= materialChanged;
                changed |= ApplyDefaultProfileMaterialParameters(
                    generatedMaterial,
                    mirror,
                    i,
                    registerUndo,
                    preserveSourceHideBackground
                );
            }

            return changed;
        }

        private static bool ApplyDefaultProfileMaterialParameters(
            Material material,
            EzMirror mirror,
            int profileIndex,
            bool registerUndo,
            bool preserveHideBackground
        )
        {
            if (material == null || IsEditorPreviewBaseMaterial(material))
                return false;

            float transparency = GetProfileTransparency(mirror, profileIndex);
            bool hideBackground =
                mirror != null
                && mirror.profileHideBackground != null
                && profileIndex < mirror.profileHideBackground.Length
                && mirror.profileHideBackground[profileIndex];

            bool needsUpdate = false;
            needsUpdate |= NeedsMaterialFloatUpdate(material, "_Transparency", transparency);
            if (!preserveHideBackground)
            {
                needsUpdate |= NeedsMaterialFloatUpdate(
                    material,
                    "_HideBackground",
                    hideBackground ? 1f : 0f
                );
            }

            if (!needsUpdate)
                return false;

            if (registerUndo)
                Undo.RecordObject(material, "EzMirror プロファイルマテリアルを更新");
            if (material.HasProperty("_Transparency"))
                material.SetFloat("_Transparency", transparency);
            if (!preserveHideBackground && material.HasProperty("_HideBackground"))
                material.SetFloat("_HideBackground", hideBackground ? 1f : 0f);
            EditorUtility.SetDirty(material);
            return true;
        }

        private static float GetProfileTransparency(EzMirror mirror, int profileIndex)
        {
            if (
                mirror != null
                && mirror.profileTransparency != null
                && profileIndex >= 0
                && profileIndex < mirror.profileTransparency.Length
            )
            {
                return Mathf.Clamp01(mirror.profileTransparency[profileIndex]);
            }

            return GetDefaultProfileTransparency(profileIndex);
        }

        private static float GetDefaultProfileTransparency(int profileIndex)
        {
            return profileIndex == 2 ? TransparentProfileTransparency : 0f;
        }

        private static bool NeedsMaterialFloatUpdate(Material material, string propertyName, float value)
        {
            return material.HasProperty(propertyName)
                && !Mathf.Approximately(material.GetFloat(propertyName), value);
        }

        private static bool IsValidGeneratedMaterial(
            Material material,
            string ownerKey,
            int profileIndex
        )
        {
            if (!HasGeneratedMaterialIdentity(material))
                return false;
            return IsCanonicalGeneratedMaterialPath(material, ownerKey, profileIndex)
                && HasMaterialLabel(material, GeneratedMaterialLabel)
                && HasMaterialLabel(material, GetGeneratedMaterialOwnerLabel(ownerKey))
                && HasMaterialLabel(material, GetGeneratedMaterialProfileLabel(profileIndex));
        }

        private static bool HasProfileMaterialsNeedingRegeneration(EzMirror mirror)
        {
            if (mirror == null || mirror.profileMaterials == null)
                return false;

            string ownerKey = GetGeneratedMaterialOwnerKey(mirror);
            for (int i = 1; i < mirror.profileMaterials.Length; i++)
            {
                Material material = mirror.profileMaterials[i];
                if (material != null && !IsValidGeneratedMaterial(material, ownerKey, i))
                    return true;
            }
            return false;
        }

        private static bool HasGeneratedMaterialIdentity(Material material)
        {
            if (material == null)
                return false;

            string path = AssetDatabase.GetAssetPath(material);
            string fileName = Path.GetFileNameWithoutExtension(path);
            return !string.IsNullOrEmpty(path)
                && EzMirrorAssetResolver.IsInGeneratedMaterialFolder(path)
                && (
                    material.name.StartsWith(GeneratedMaterialPrefix, StringComparison.Ordinal)
                    || fileName.StartsWith(GeneratedMaterialPrefix, StringComparison.Ordinal)
                );
        }

        private static Material EnsureGeneratedProfileMaterial(
            EzMirror mirror,
            string ownerKey,
            int profileIndex,
            bool forceRegenerate,
            bool registerUndo,
            out bool changed,
            out bool preserveSourceHideBackground
        )
        {
            changed = false;
            preserveSourceHideBackground = false;
            EnsureGeneratedMaterialFolder();
            string canonicalPath = EzMirrorAssetResolver.GetGeneratedMaterialPath(
                ownerKey,
                profileIndex
            );
            string canonicalName = EzMirrorAssetResolver.GetGeneratedMaterialAssetName(
                ownerKey,
                profileIndex
            );

            Material currentMaterial =
                mirror != null
                    && mirror.profileMaterials != null
                    && profileIndex < mirror.profileMaterials.Length
                    ? mirror.profileMaterials[profileIndex]
                    : null;
            Material canonicalMaterial = AssetDatabase.LoadAssetAtPath<Material>(canonicalPath);
            UnityEngine.Object canonicalAsset = AssetDatabase.LoadMainAssetAtPath(canonicalPath);

            if (canonicalAsset != null && canonicalMaterial == null)
            {
                WarnCanonicalGeneratedMaterialConflict(canonicalPath, mirror);
                return currentMaterial;
            }

            if (canonicalMaterial != null)
            {
                if (!CanUseCanonicalGeneratedMaterial(canonicalMaterial, ownerKey, profileIndex))
                {
                    WarnCanonicalGeneratedMaterialConflict(canonicalPath, mirror);
                    return currentMaterial;
                }

                changed |= EnsureCanonicalGeneratedMaterialMetadata(
                    canonicalMaterial,
                    ownerKey,
                    profileIndex,
                    canonicalName
                );
                if (
                    !forceRegenerate
                    && ShouldRefreshCanonicalFromShiftedCurrentMaterial(
                        currentMaterial,
                        canonicalMaterial,
                        ownerKey,
                        profileIndex
                    )
                )
                {
                    changed |= ResetGeneratedMaterialFromBase(
                        canonicalMaterial,
                        currentMaterial,
                        canonicalName,
                        registerUndo
                    );
                }
                else if (forceRegenerate)
                {
                    Material baseMaterial = GetProfileMaterialBase(mirror, profileIndex, false);
                    changed |= ResetGeneratedMaterialFromBase(
                        canonicalMaterial,
                        baseMaterial,
                        canonicalName,
                        registerUndo
                    );
                }
                return canonicalMaterial;
            }

            if (
                TryMoveConfirmedLegacyGeneratedMaterialToCanonical(
                    currentMaterial,
                    ownerKey,
                    profileIndex,
                    canonicalPath,
                    canonicalName,
                    out Material migratedMaterial
                )
            )
            {
                changed = true;
                if (forceRegenerate)
                {
                    Material baseMaterial = GetProfileMaterialBase(mirror, profileIndex, false);
                    changed |= ResetGeneratedMaterialFromBase(
                        migratedMaterial,
                        baseMaterial,
                        canonicalName,
                        registerUndo
                    );
                }
                return migratedMaterial;
            }

            Material sourceMaterial = GetProfileMaterialBase(mirror, profileIndex, !forceRegenerate);
            preserveSourceHideBackground =
                currentMaterial != null
                && sourceMaterial == currentMaterial
                && HasGeneratedMaterialIdentity(currentMaterial);
            Material generated = CreateGeneratedMaterialVariant(
                sourceMaterial,
                ownerKey,
                profileIndex,
                canonicalPath,
                canonicalName,
                registerUndo
            );
            if (generated == null)
                return null;

            changed = true;
            return generated;
        }

        private static Material CreateGeneratedMaterialVariant(
            Material baseMaterial,
            string ownerKey,
            int profileIndex,
            string path,
            string assetName,
            bool registerUndo
        )
        {
            Material generated = CreateMaterialFromBaseOrShader(baseMaterial);
            if (generated == null)
                return null;

            generated.name = assetName;
            generated.hideFlags = HideFlags.HideInInspector;
            AssetDatabase.CreateAsset(generated, path);
            AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceSynchronousImport);
            Material loaded = AssetDatabase.LoadAssetAtPath<Material>(path);
            if (loaded == null)
            {
                AssetDatabase.DeleteAsset(path);
                AssetDatabase.Refresh();
                return null;
            }

            if (registerUndo)
                Undo.RegisterCreatedObjectUndo(loaded, "Create EzMirror Generated Material");
            SetGeneratedMaterialLabels(loaded, ownerKey, profileIndex);
            EditorUtility.SetDirty(loaded);
            AssetDatabase.SaveAssets();
            return loaded;
        }

        private static bool TryMoveConfirmedLegacyGeneratedMaterialToCanonical(
            Material material,
            string ownerKey,
            int profileIndex,
            string canonicalPath,
            string canonicalName,
            out Material migratedMaterial
        )
        {
            migratedMaterial = null;
            if (!IsConfirmedLegacyGeneratedMaterial(material, ownerKey, profileIndex))
                return false;

            string currentPath = AssetDatabase.GetAssetPath(material);
            if (currentPath == canonicalPath)
            {
                migratedMaterial = material;
                return true;
            }

            string error = AssetDatabase.MoveAsset(currentPath, canonicalPath);
            if (!string.IsNullOrEmpty(error))
            {
                Debug.LogWarning(
                    "[EzMirror] Failed to move generated material to canonical path: "
                        + currentPath
                        + " -> "
                        + canonicalPath
                        + "\n"
                        + error
                );
                return false;
            }

            AssetDatabase.ImportAsset(canonicalPath, ImportAssetOptions.ForceSynchronousImport);
            migratedMaterial = AssetDatabase.LoadAssetAtPath<Material>(canonicalPath);
            if (migratedMaterial == null)
                migratedMaterial = material;
            EnsureCanonicalGeneratedMaterialMetadata(
                migratedMaterial,
                ownerKey,
                profileIndex,
                canonicalName
            );
            return true;
        }

        private static bool IsConfirmedLegacyGeneratedMaterial(
            Material material,
            string ownerKey,
            int profileIndex
        )
        {
            if (!HasGeneratedMaterialIdentity(material))
                return false;
            if (IsCanonicalGeneratedMaterialPath(material, ownerKey, profileIndex))
                return false;
            return HasMaterialLabel(material, GeneratedMaterialLabel)
                && HasMaterialLabel(material, GetGeneratedMaterialOwnerLabel(ownerKey))
                && HasMaterialLabel(material, GetGeneratedMaterialProfileLabel(profileIndex));
        }

        private static bool CanUseCanonicalGeneratedMaterial(
            Material material,
            string ownerKey,
            int profileIndex
        )
        {
            if (!HasGeneratedMaterialIdentity(material))
                return false;
            if (HasConflictingGeneratedMaterialLabel(material, ownerKey, profileIndex))
                return false;
            return true;
        }

        private static bool HasConflictingGeneratedMaterialLabel(
            Material material,
            string ownerKey,
            int profileIndex
        )
        {
            string ownerLabel = GetGeneratedMaterialOwnerLabel(ownerKey);
            string profileLabel = GetGeneratedMaterialProfileLabel(profileIndex);
            string[] labels = AssetDatabase.GetLabels(material);
            for (int i = 0; i < labels.Length; i++)
            {
                string label = labels[i];
                if (
                    label.StartsWith(GeneratedMaterialOwnerLabelPrefix, StringComparison.Ordinal)
                    && label != ownerLabel
                )
                    return true;
                if (
                    label.StartsWith(GeneratedMaterialProfileLabelPrefix, StringComparison.Ordinal)
                    && label != profileLabel
                )
                    return true;
            }
            return false;
        }

        private static bool IsCanonicalGeneratedMaterialPath(
            Material material,
            string ownerKey,
            int profileIndex
        )
        {
            string path = AssetDatabase.GetAssetPath(material);
            return path == EzMirrorAssetResolver.GetGeneratedMaterialPath(ownerKey, profileIndex);
        }

        private static bool EnsureCanonicalGeneratedMaterialMetadata(
            Material material,
            string ownerKey,
            int profileIndex,
            string canonicalName
        )
        {
            if (material == null)
                return false;

            bool changed = material.name != canonicalName
                || material.hideFlags != HideFlags.HideInInspector
                || !HasMaterialLabel(material, GeneratedMaterialLabel)
                || !HasMaterialLabel(material, GetGeneratedMaterialOwnerLabel(ownerKey))
                || !HasMaterialLabel(material, GetGeneratedMaterialProfileLabel(profileIndex));
            material.name = canonicalName;
            material.hideFlags = HideFlags.HideInInspector;
            SetGeneratedMaterialLabels(material, ownerKey, profileIndex);
            if (!changed)
                return false;

            EditorUtility.SetDirty(material);
            return true;
        }

        private static bool ResetGeneratedMaterialFromBase(
            Material target,
            Material baseMaterial,
            string canonicalName,
            bool registerUndo
        )
        {
            if (target == null)
                return false;

            Material source = CreateMaterialFromBaseOrShader(baseMaterial);
            if (source == null)
                return false;

            if (registerUndo)
                Undo.RecordObject(target, "Regenerate EzMirror Generated Material");

            target.shader = source.shader;
            target.CopyPropertiesFromMaterial(source);
            target.renderQueue = source.renderQueue;
            target.globalIlluminationFlags = source.globalIlluminationFlags;
            target.enableInstancing = source.enableInstancing;
            target.doubleSidedGI = source.doubleSidedGI;
            target.name = canonicalName;
            target.hideFlags = HideFlags.HideInInspector;

            if (source != baseMaterial)
                DestroyImmediate(source);

            EditorUtility.SetDirty(target);
            return true;
        }

        private static bool ShouldRefreshCanonicalFromShiftedCurrentMaterial(
            Material currentMaterial,
            Material canonicalMaterial,
            string ownerKey,
            int profileIndex
        )
        {
            if (currentMaterial == null || currentMaterial == canonicalMaterial)
                return false;
            if (!HasGeneratedMaterialIdentity(currentMaterial))
                return false;
            if (!HasMaterialLabel(currentMaterial, GeneratedMaterialLabel))
                return false;
            if (!HasMaterialLabel(currentMaterial, GetGeneratedMaterialOwnerLabel(ownerKey)))
                return false;
            if (HasMaterialLabel(currentMaterial, GetGeneratedMaterialProfileLabel(profileIndex)))
                return false;
            return HasGeneratedMaterialProfileLabel(currentMaterial);
        }

        private static bool HasGeneratedMaterialProfileLabel(Material material)
        {
            if (material == null)
                return false;

            string[] labels = AssetDatabase.GetLabels(material);
            for (int i = 0; i < labels.Length; i++)
            {
                if (labels[i].StartsWith(GeneratedMaterialProfileLabelPrefix, StringComparison.Ordinal))
                    return true;
            }
            return false;
        }

        private static void WarnCanonicalGeneratedMaterialConflict(string path, EzMirror mirror)
        {
            Debug.LogWarning(
                "[EzMirror] Canonical generated material path is occupied by an unrelated asset. "
                    + "Keeping the current reference: "
                    + path,
                mirror
            );
        }

        private static Material CreateMaterialFromBaseOrShader(Material baseMaterial)
        {
            if (baseMaterial != null)
                return new Material(baseMaterial);

            Shader shader = Shader.Find("NiAka/EzMirror");
            if (shader == null)
                return null;

            return new Material(shader)
            {
                hideFlags = HideFlags.HideInInspector
            };
        }

        private static Material GetProfileMaterialBase(
            EzMirror mirror,
            int profileIndex,
            bool allowGeneratedCurrentAsSource
        )
        {
            if (mirror == null)
                return null;

            Material current =
                mirror.profileMaterials != null && profileIndex < mirror.profileMaterials.Length
                    ? mirror.profileMaterials[profileIndex]
                    : null;
            if (
                current != null
                && allowGeneratedCurrentAsSource
                && HasGeneratedMaterialIdentity(current)
            )
                return current;

            Material configuredBase =
                mirror.profileBaseMaterials != null && profileIndex < mirror.profileBaseMaterials.Length
                    ? mirror.profileBaseMaterials[profileIndex]
                    : null;
            if (
                configuredBase != null
                && !HasGeneratedMaterialIdentity(configuredBase)
                && !IsEditorPreviewBaseMaterial(configuredBase)
            )
            {
                return configuredBase;
            }

            if (
                current != null
                && (allowGeneratedCurrentAsSource || !HasGeneratedMaterialIdentity(current))
                && !IsEditorPreviewBaseMaterial(current)
            )
            {
                return current;
            }

            return GetDefaultBaseMaterialForProfile(profileIndex);
        }

        private static string GetGeneratedMaterialOwnerKey(EzMirror mirror)
        {
            if (mirror == null)
                return StableHash16("EzMirror:null");

            string globalId = GlobalObjectId.GetGlobalObjectIdSlow(mirror).ToString();
            if (IsStableGlobalObjectId(globalId))
                return StableHash16(globalId);

            string prefabPath = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(
                mirror.gameObject
            );
            if (!string.IsNullOrEmpty(prefabPath))
            {
                return StableHash16(
                    "prefab:"
                        + AssetDatabase.AssetPathToGUID(prefabPath)
                        + ":"
                        + GetTransformPath(mirror.transform)
                );
            }

            string scenePath = mirror.gameObject.scene.path;
            if (!string.IsNullOrEmpty(scenePath))
            {
                return StableHash16(
                    "scene:"
                        + AssetDatabase.AssetPathToGUID(scenePath)
                        + ":"
                        + GetTransformPath(mirror.transform)
                );
            }

            return StableHash16("unsaved:" + mirror.GetInstanceID());
        }

        private static bool IsStableGlobalObjectId(string globalId)
        {
            return !string.IsNullOrEmpty(globalId)
                && !globalId.Contains("-00000000000000000000000000000000-")
                && !globalId.EndsWith("-0-0", StringComparison.Ordinal);
        }

        private static string StableHash16(string value)
        {
            const ulong offsetBasis = 14695981039346656037UL;
            const ulong prime = 1099511628211UL;
            unchecked
            {
                ulong hash = offsetBasis;
                string text = value ?? string.Empty;
                for (int i = 0; i < text.Length; i++)
                {
                    hash ^= text[i];
                    hash *= prime;
                }
                return hash.ToString("x16");
            }
        }

        private static string GetTransformPath(Transform transform)
        {
            if (transform == null)
                return string.Empty;

            string path = transform.GetSiblingIndex() + ":" + transform.name;
            Transform parent = transform.parent;
            while (parent != null)
            {
                path = parent.GetSiblingIndex() + ":" + parent.name + "/" + path;
                parent = parent.parent;
            }
            return path;
        }

        private static string GetGeneratedMaterialOwnerLabel(string ownerKey)
        {
            return GeneratedMaterialOwnerLabelPrefix + ownerKey;
        }

        private static string GetGeneratedMaterialProfileLabel(int profileIndex)
        {
            return GeneratedMaterialProfileLabelPrefix + Mathf.Max(0, profileIndex);
        }

        private static void SetGeneratedMaterialLabels(
            Material material,
            string ownerKey,
            int profileIndex
        )
        {
            if (material == null)
                return;

            string ownerLabel = GetGeneratedMaterialOwnerLabel(ownerKey);
            string profileLabel = GetGeneratedMaterialProfileLabel(profileIndex);
            string[] currentLabels = AssetDatabase.GetLabels(material);
            List<string> labels = new List<string>();
            for (int i = 0; i < currentLabels.Length; i++)
            {
                string label = currentLabels[i];
                if (
                    label == GeneratedMaterialLabel
                    || label.StartsWith(GeneratedMaterialOwnerLabelPrefix, StringComparison.Ordinal)
                    || label.StartsWith(GeneratedMaterialProfileLabelPrefix, StringComparison.Ordinal)
                )
                    continue;
                labels.Add(label);
            }

            labels.Add(GeneratedMaterialLabel);
            labels.Add(ownerLabel);
            labels.Add(profileLabel);
            AssetDatabase.SetLabels(material, labels.ToArray());
        }

        private static bool HasMaterialLabel(Material material, string label)
        {
            if (material == null || string.IsNullOrEmpty(label))
                return false;
            string[] labels = AssetDatabase.GetLabels(material);
            for (int i = 0; i < labels.Length; i++)
            {
                if (labels[i] == label)
                    return true;
            }
            return false;
        }

        private static bool IsEditorPreviewBaseMaterial(Material material)
        {
            return EzMirrorAssetResolver.IsDummyBaseMaterial(material);
        }

        private static void EnsureGeneratedMaterialFolder()
        {
            EzMirrorAssetResolver.GetGeneratedMaterialFolder();
        }

        private static Material GetDefaultBaseMaterialForProfile(int profileIndex)
        {
            return EzMirrorAssetResolver.LoadDefaultBaseMaterialForProfile(profileIndex);
        }

        private static Material LoadEditorPreviewMaterial()
        {
            Material previewBaseMaterial = EzMirrorAssetResolver.LoadDummyBaseMaterial();
            if (previewBaseMaterial == null)
                return null;

            EnsureGeneratedMaterialFolder();
            string previewPath = EzMirrorAssetResolver.GetEditorPreviewMaterialPath();
            Material previewMaterial = AssetDatabase.LoadAssetAtPath<Material>(previewPath);
            if (previewMaterial == null)
            {
                previewMaterial = CreateEditorPreviewMaterial(previewBaseMaterial);
                AssetDatabase.CreateAsset(previewMaterial, previewPath);
                AssetDatabase.ImportAsset(
                    previewPath,
                    ImportAssetOptions.ForceSynchronousImport
                );
                previewMaterial = AssetDatabase.LoadAssetAtPath<Material>(previewPath);
            }

            if (previewMaterial == null)
                return null;

            RepairEditorPreviewMaterial(previewMaterial, previewBaseMaterial);
            EnsureEditorPreviewMaterialLabel(previewMaterial);
            return previewMaterial;
        }

        private void EnsureEditorPreviewMaterialsForTargets()
        {
            if (EditorApplication.isPlayingOrWillChangePlaymode)
                return;

            for (int i = 0; i < targets.Length; i++)
                EnsureEditorPreviewMaterial(targets[i] as EzMirror, false);
        }

        internal static bool EnsureEditorPreviewMaterial(EzMirror mirror, bool registerUndo)
        {
            if (mirror == null || mirror.profileRenderer == null)
                return false;

            Material previewMaterial = LoadEditorPreviewMaterial();
            if (previewMaterial == null)
                return false;

            Material currentMaterial = mirror.profileRenderer.sharedMaterial;
            if (IsUsableEditorPreviewMaterial(currentMaterial, previewMaterial))
                return false;

            if (registerUndo)
                Undo.RecordObject(mirror.profileRenderer, "Assign EzMirror Editor Preview Material");
            mirror.profileRenderer.sharedMaterial = previewMaterial;
            if (IsTransientEditorPreviewMaterial(currentMaterial))
                UnityEngine.Object.DestroyImmediate(currentMaterial);
            EditorUtility.SetDirty(mirror.profileRenderer);
            PrefabUtility.RecordPrefabInstancePropertyModifications(mirror.profileRenderer);
            return true;
        }

        private static Material CreateEditorPreviewMaterial(Material source)
        {
            Material material = new Material(source)
            {
                name = EditorPreviewMaterialName,
                hideFlags = HideFlags.None
            };
            return material;
        }

        private static bool IsUsableEditorPreviewMaterial(Material material, Material previewMaterial)
        {
            return material != null
                && previewMaterial != null
                && material == previewMaterial
                && IsEditorPreviewMaterial(material);
        }

        private static bool IsEditorPreviewMaterial(Material material)
        {
            if (material == null)
                return false;

            string path = AssetDatabase.GetAssetPath(material);
            return path == EzMirrorAssetResolver.GetEditorPreviewMaterialPath()
                || HasMaterialLabel(material, EditorPreviewMaterialLabel)
                || IsTransientEditorPreviewMaterial(material);
        }

        private static bool IsTransientEditorPreviewMaterial(Material material)
        {
            return material != null
                && material.name == EditorPreviewMaterialName
                && !EditorUtility.IsPersistent(material);
        }

        private static void RepairEditorPreviewMaterial(Material previewMaterial, Material source)
        {
            if (previewMaterial == null || source == null)
                return;

            bool changed = previewMaterial.name != EditorPreviewMaterialName
                || previewMaterial.shader != source.shader
                || previewMaterial.renderQueue != source.renderQueue
                || previewMaterial.globalIlluminationFlags != source.globalIlluminationFlags
                || !ColorsApproximatelyEqual(GetMaterialColor(previewMaterial), GetMaterialColor(source))
                || previewMaterial.mainTexture != source.mainTexture
                || previewMaterial.hideFlags != HideFlags.None;

            previewMaterial.shader = source.shader;
            previewMaterial.CopyPropertiesFromMaterial(source);
            previewMaterial.name = EditorPreviewMaterialName;
            previewMaterial.hideFlags = HideFlags.None;

            if (!changed)
                return;

            EditorUtility.SetDirty(previewMaterial);
            AssetDatabase.SaveAssets();
        }

        private static void EnsureEditorPreviewMaterialLabel(Material previewMaterial)
        {
            if (previewMaterial == null || HasMaterialLabel(previewMaterial, EditorPreviewMaterialLabel))
                return;

            AssetDatabase.SetLabels(previewMaterial, new[] { EditorPreviewMaterialLabel });
            EditorUtility.SetDirty(previewMaterial);
            AssetDatabase.SaveAssets();
        }

        private static Color GetMaterialColor(Material material)
        {
            if (material != null && material.HasProperty("_Color"))
                return material.GetColor("_Color");
            return Color.clear;
        }

        private static bool ColorsApproximatelyEqual(Color a, Color b)
        {
            return Mathf.Approximately(a.r, b.r)
                && Mathf.Approximately(a.g, b.g)
                && Mathf.Approximately(a.b, b.b)
                && Mathf.Approximately(a.a, b.a);
        }

        internal static bool EnsurePersistentRuntimeRendererMaterial(EzMirror mirror)
        {
            if (mirror == null || mirror.profileRenderer == null)
                return false;

            Material runtimeMaterial = GetFirstRuntimeProfileMaterial(mirror);
            if (runtimeMaterial == null || mirror.profileRenderer.sharedMaterial == runtimeMaterial)
                return false;

            Material currentMaterial = mirror.profileRenderer.sharedMaterial;
            mirror.profileRenderer.sharedMaterial = runtimeMaterial;
            if (IsTransientEditorPreviewMaterial(currentMaterial))
                UnityEngine.Object.DestroyImmediate(currentMaterial);

            EditorUtility.SetDirty(mirror.profileRenderer);
            PrefabUtility.RecordPrefabInstancePropertyModifications(mirror.profileRenderer);
            return true;
        }

        private static Material GetFirstRuntimeProfileMaterial(EzMirror mirror)
        {
            if (mirror == null || mirror.profileMaterials == null)
                return null;

            for (int i = 1; i < mirror.profileMaterials.Length; i++)
            {
                Material material = mirror.profileMaterials[i];
                if (material != null && !IsEditorPreviewBaseMaterial(material))
                    return material;
            }

            return null;
        }

        private static int GetEditorProfileCount(EzMirror mirror)
        {
            if (mirror == null)
                return 1;

            int count = 1;
            if (mirror.profileMaterials != null && mirror.profileMaterials.Length > count)
                count = mirror.profileMaterials.Length;
            if (mirror.profileBaseMaterials != null && mirror.profileBaseMaterials.Length > count)
                count = mirror.profileBaseMaterials.Length;
            int profileAssetCount = GetProfileAssetReferencesCount(mirror);
            if (profileAssetCount > count)
                count = profileAssetCount;
            if (mirror.profileNames != null && mirror.profileNames.Length > count)
                count = mirror.profileNames.Length;
            if (mirror.profileIsDefault != null && mirror.profileIsDefault.Length > count)
                count = mirror.profileIsDefault.Length;
            if (mirror.profileMirrorEnabled != null && mirror.profileMirrorEnabled.Length > count)
                count = mirror.profileMirrorEnabled.Length;
            if (
                mirror.profileDisablePixelLights != null
                && mirror.profileDisablePixelLights.Length > count
            )
                count = mirror.profileDisablePixelLights.Length;
            if (
                mirror.profileTurnOffMirrorOcclusion != null
                && mirror.profileTurnOffMirrorOcclusion.Length > count
            )
                count = mirror.profileTurnOffMirrorOcclusion.Length;
            if (mirror.profileReflectLayers != null && mirror.profileReflectLayers.Length > count)
                count = mirror.profileReflectLayers.Length;
            if (
                mirror.profileCameraClearFlags != null
                && mirror.profileCameraClearFlags.Length > count
            )
                count = mirror.profileCameraClearFlags.Length;
            if (
                mirror.profileCustomSkyboxes != null
                && mirror.profileCustomSkyboxes.Length > count
            )
                count = mirror.profileCustomSkyboxes.Length;
            if (
                mirror.profileCustomClearColors != null
                && mirror.profileCustomClearColors.Length > count
            )
                count = mirror.profileCustomClearColors.Length;
            if (mirror.profileHideBackground != null && mirror.profileHideBackground.Length > count)
                count = mirror.profileHideBackground.Length;
            if (mirror.profileTransparency != null && mirror.profileTransparency.Length > count)
                count = mirror.profileTransparency.Length;
            if (mirror.profilePcOnly != null && mirror.profilePcOnly.Length > count)
                count = mirror.profilePcOnly.Length;
            if (mirror.profileIconSprites != null && mirror.profileIconSprites.Length > count)
                count = mirror.profileIconSprites.Length;
            if (mirror.profileIconColors != null && mirror.profileIconColors.Length > count)
                count = mirror.profileIconColors.Length;
            if (mirror.profileIconObjects != null && mirror.profileIconObjects.Length > count)
                count = mirror.profileIconObjects.Length;
            return count;
        }

        private static bool TryGetCommonFadeRanges(
            Material[] materials,
            out float distanceRange,
            out float horizontalRange
        )
        {
            distanceRange = 1f;
            horizontalRange = 1f;
            if (materials == null)
                return false;

            bool found = false;
            for (int i = 0; i < materials.Length; i++)
            {
                Material material = materials[i];
                if (material == null)
                    continue;

                if (HasDistanceFadeProperties(material))
                {
                    distanceRange = Mathf.Max(0.01f, material.GetFloat(DistanceFadeRangeProperty));
                    found = true;
                }
                if (HasHorizontalFadeProperties(material))
                {
                    horizontalRange = Mathf.Max(0.01f, material.GetFloat(HorizontalFadeRangeProperty));
                    found = true;
                }
                if (found)
                    return true;
            }

            return false;
        }

        private static void AddUniqueFadeMaterials(
            SerializedProperty serializedMaterials,
            List<Material> materials
        )
        {
            if (serializedMaterials == null)
                return;

            for (int i = 0; i < serializedMaterials.arraySize; i++)
            {
                Material material = serializedMaterials.GetArrayElementAtIndex(i).objectReferenceValue as Material;
                AddUniqueFadeMaterial(material, materials);
            }
        }

        private static void AddUniqueFadeMaterials(
            Material[] sourceMaterials,
            List<Material> materials
        )
        {
            if (sourceMaterials == null)
                return;

            for (int i = 0; i < sourceMaterials.Length; i++)
                AddUniqueFadeMaterial(sourceMaterials[i], materials);
        }

        private static void AddUniqueFadeMaterial(Material material, List<Material> materials)
        {
            if (material == null || !HasAnyFadeProperty(material))
                return;
            if (materials.Contains(material))
                return;

            materials.Add(material);
        }

        private static bool HasAnyFadeProperty(Material material)
        {
            return HasDistanceFadeProperties(material) || HasHorizontalFadeProperties(material);
        }

        private static bool HasDistanceFadeProperties(Material material)
        {
            return material != null
                && material.HasProperty(DistanceFadeStartProperty)
                && material.HasProperty(DistanceFadeRangeProperty);
        }

        private static bool HasHorizontalFadeProperties(Material material)
        {
            return material != null
                && material.HasProperty(HorizontalFadeStartProperty)
                && material.HasProperty(HorizontalFadeRangeProperty);
        }
    }
}
#endif
