#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;

    [CustomEditor(typeof(EzMirror))]
    [CanEditMultipleObjects]
    public partial class EzMirrorEditor : UnityEditor.Editor
    {
        private const string LqBackgroundObjectName = "LQBackgroundClearSphere";
        private const int OffProfileIndex = 0;
        private const int LqProfileIndex = 3;
        private const string DefaultLayerName = "Default";
        private const string PlayerLayerName = "Player";
        private const string EnvironmentLayerName = "Environment";
        private const string PickupLayerName = "Pickup";
        private const string PickupNoEnvironmentLayerName = "PickupNoEnvironment";
        private const string MirrorReflectionLayerName = "MirrorReflection";
        private const string HardwareObjectsLayerName = "HardwareObjects";
        private const string InteractiveLayerName = "Interactive";
        private const string WalkthroughLayerName = "Walkthrough";
        private const string WaterLayerName = "Water";
        private const string EditorPreviewMaterialName = "EzMirror_EditorPreview";
        internal const string GeneratedMaterialPrefix = "EzMirror_";
        internal const string GeneratedMaterialLabel = "EzMirrorGenerated";
        internal const string GeneratedMaterialOwnerLabelPrefix = "EzMirrorOwner_";
        internal const string GeneratedMaterialProfileLabelPrefix = "EzMirrorProfile_";
        private const string SwitchUiObjectName = "SwitchUI";
        private const string SwitchModelObjectName = "Model_Switch";
        private const string SwitchBaseObjectName = "SwitchBase";
        private const string SwitchButtonObjectName = "Button";
        private const string LegacySwitch3DObjectName = "Switch3D";
        private const string LegacySwitchModelObjectName = "Hari_Switch";
        private const string LegacySwitchBaseObjectName = "台座";
        private const string LegacySwitchButtonObjectName = "スイッチ";
        private const string LegacyInputProxyObjectName = "SwitchInput";
        private const string InteractInputObjectName = "InteractInput";
        private const string ContactInputObjectName = "ContactInput";
        private static readonly Vector3 DefaultInputProxyLocalPosition =
            new Vector3(1.5947f, 0.11500001f, -0.0019f);
        private static readonly Vector3 DefaultInputProxyLocalScale =
            new Vector3(0.15f, 0.15f, 0.01f);
        private static readonly string[] HandContactCollisionTags =
        {
            EzMirrorContactTags.Hand,
            EzMirrorContactTags.HandL,
            EzMirrorContactTags.HandR,
            EzMirrorContactTags.Finger,
            EzMirrorContactTags.FingerL,
            EzMirrorContactTags.FingerR,
            EzMirrorContactTags.FingerIndex,
            EzMirrorContactTags.FingerIndexL,
            EzMirrorContactTags.FingerIndexR,
            EzMirrorContactTags.FingerMiddle,
            EzMirrorContactTags.FingerMiddleL,
            EzMirrorContactTags.FingerMiddleR,
            EzMirrorContactTags.FingerRingL,
            EzMirrorContactTags.FingerRingR,
            EzMirrorContactTags.FingerLittleL,
            EzMirrorContactTags.FingerLittleR,
        };
        private static readonly string[] BeginnerTemplatePaths =
        {
            "EzMirror_SFX",
            "Trigger",
            SwitchUiObjectName,
            SwitchUiObjectName + "/UI_Canvas",
            SwitchUiObjectName + "/UI_Canvas/ToggleUI",
            "UI_Canvas",
            "UI_Canvas/ToggleUI",
            "VRCMirror",
            LqBackgroundObjectName,
            LegacyInputProxyObjectName,
        };
        private static readonly string[] LegacyProfileIconNames =
        {
            "Icon_OFF",
            "Icon_MirrorHQ",
            "Icon_MirrorTransparent",
            "Icon_MirrorLQ",
        };
        private const string ToggleEventName = "SelectNextFromUgui";
        private const string DistanceFadeStartProperty = "_DistanceFadeStart";
        private const string DistanceFadeRangeProperty = "_DistanceFadeRange";
        private const string HorizontalFadeStartProperty = "_HorizontalFadeStart";
        private const string HorizontalFadeRangeProperty = "_HorizontalFadeRange";
        private const string HorizontalFadeCenterOffsetProperty = "_HorizontalFadeCenterOffset";
        private const float CopyButtonWidth = 68f;
        private const float CopyableCodeHeight = 72f;
        private const float ProfileHeaderHorizontalPadding = 4f;
        private const float ProfileHeaderGap = 6f;
        private const float ProfileHeaderLockedLabelWidth = 32f;
        private const float ProfileHeaderButtonWidth = 64f;
        private const float SectionOuterTopSpacing = 14f;
        private const float SectionHeaderBottomSpacing = 7f;
        private const float StepHeaderTopSpacing = 12f;
        private const float StepBodyTopPadding = 9f;
        private const float StepBodyBottomPadding = 10f;
        private const float StepSectionBottomSpacing = 10f;
        private const float ProfileCardTopSpacing = 10f;
        private const float ProfileCardBottomSpacing = 12f;
        private const float ProfileGroupHeaderTopSpacing = 9f;
        private const float ProfileGroupHeaderBottomSpacing = 4f;
        private const float IconPreviewSize = 60f;
        private const float IconPreviewPadding = 6f;
        private const float IconPreviewTextGap = 10f;
        private const float IconPreviewButtonWidth = 92f;
        private const string StepPrefsPrefix = "NiAkaDev.EzMirror.Step.";
        private const string DeveloperOptionsPrefsKey =
            "NiAkaDev.EzMirror.DeveloperOptions";
        private const string AdvancedPrefsPrefix = "NiAkaDev.EzMirror.Advanced.";
        private const string InteractAdvancedPrefsKey = AdvancedPrefsPrefix + "Interact";
        private const string ContactAdvancedPrefsKey = AdvancedPrefsPrefix + "Contact";
        private const string PersistAdvancedPrefsKey = AdvancedPrefsPrefix + "Persist";
        private const string SfxAdvancedPrefsKey = AdvancedPrefsPrefix + "Sfx";
        private const string HapticsAdvancedPrefsKey = AdvancedPrefsPrefix + "Haptics";
        private const string ProfileAdvancedPrefsPrefix = AdvancedPrefsPrefix + "Profile.";
        private const float TransparentProfileTransparency = 0.5f;
        private const string EditorPreviewMaterialLabel = "EzMirrorEditorPreview";

        internal static readonly Color NiAkaPink = new Color(1f, 0.41f, 0.71f);
        private static readonly Color NiAkaPinkLight = new Color(1f, 0.71f, 0.85f);
        private static readonly Color NiAkaBlackSoft = new Color(0.15f, 0.15f, 0.18f);
        private static readonly Color HeroBannerColor = new Color(0.86f, 0.22f, 0.52f);
        private static readonly Color HeroBannerBorderColor = new Color(0.56f, 0.12f, 0.32f, 0.45f);
        private static readonly Color HeroDescriptionBgColor = new Color(1f, 0.93f, 0.97f);
        private static readonly Color HeroDescriptionAccentColor = new Color(1f, 0.41f, 0.71f);
        internal static readonly Color StatusOkColor = new Color(0.18f, 0.68f, 0.32f);
        internal static readonly Color StatusErrorColor = new Color(0.9f, 0.22f, 0.18f);
        private static readonly Color IconPreviewBorderColor = new Color(0f, 0f, 0f, 0.22f);

        private SerializedProperty usePersist;
        private SerializedProperty persistKey;
        private SerializedProperty profileMirror;
        private SerializedProperty profileRenderer;
        private SerializedProperty triggerCollider;
        private SerializedProperty triggerRefreshInterval;
        private SerializedProperty profileMaterials;
        private SerializedProperty profileBaseMaterials;
        private SerializedProperty profileNames;
        private SerializedProperty profileIsDefault;
        private SerializedProperty profileMirrorEnabled;
        private SerializedProperty profileDisablePixelLights;
        private SerializedProperty profileTurnOffMirrorOcclusion;
        private SerializedProperty profileReflectLayers;
        private SerializedProperty profileCameraClearFlags;
        private SerializedProperty profileCustomSkyboxes;
        private SerializedProperty profileCustomClearColors;
        private SerializedProperty profileHideBackground;
        private SerializedProperty profileTransparency;
        private SerializedProperty profilePcOnly;
        private SerializedProperty profileActiveObjects;
        private SerializedProperty profileActiveObjectModes;
        private SerializedProperty profileInactiveObjects;
        private SerializedProperty profileInactiveObjectModes;
        private SerializedProperty iconImage;
        private SerializedProperty profileIconSprites;
        private SerializedProperty profileIconColors;
        private SerializedProperty profileIconObjects;
        private SerializedProperty enableUguiInput;
        private SerializedProperty enableInteractInput;
        private SerializedProperty enableContactInput;
        private SerializedProperty inputProxy;
        private SerializedProperty interactInputProxy;
        private SerializedProperty inputHitboxCollider;
        private SerializedProperty contactReceiver;
        private SerializedProperty contactCooldownSeconds;
        private SerializedProperty uiToggle;
        private SerializedProperty enableClickSound;
        private SerializedProperty clickClip;
        private SerializedProperty sfxAudioSource;
        private SerializedProperty enableHapticFeedback;
        private SerializedProperty hapticDuration;
        private SerializedProperty hapticAmplitude;
        private SerializedProperty hapticFrequency;
        private SerializedProperty useSwitchAnimator;
        private SerializedProperty switchAnimator;
        private SerializedProperty switchAnimatorProfileIndexParameter;
        private SerializedProperty switchAnimatorStateParameter;
        private SerializedProperty switchAnimatorPressParameter;
        private SerializedProperty switchAnimatorCanSetProfileIndex;
        private SerializedProperty switchAnimatorCanSetState;
        private SerializedProperty switchAnimatorCanSetPress;
        private string[] generatedMaterialCleanupPreviewPaths = new string[0];

        private bool[] profileFoldouts = Array.Empty<bool>();
        private GUIStyle profileCardStyle;
        private static bool editorPreviewRepairQueued;
        private static bool legacyProfileIconFoldout;
        private static GUIStyle sectionHeaderStyle;
        private static GUIStyle stepBodyStyle;
        private static GUIStyle statusBadgeStyle;
        private static GUIStyle statusTitleStyle;
        private static GUIStyle heroBannerTitleStyle;
        private static GUIStyle heroDescriptionStyle;
        private static GUIStyle iconPreviewPlaceholderStyle;
        private static GUIStyle iconPreviewBadgeStyle;

        private GUIStyle ProfileCardStyle
        {
            get
            {
                profileCardStyle ??= new GUIStyle(EditorStyles.helpBox)
                {
                    margin = new RectOffset(0, 0, (int)ProfileCardTopSpacing, (int)ProfileCardBottomSpacing),
                    padding = new RectOffset(12, 12, 10, 12)
                };
                return profileCardStyle;
            }
        }

        private static GUIStyle StepBodyStyle
        {
            get
            {
                stepBodyStyle ??= new GUIStyle(EditorStyles.helpBox)
                {
                    margin = new RectOffset(0, 0, 0, 0),
                    padding = new RectOffset(12, 12, (int)StepBodyTopPadding, (int)StepBodyBottomPadding)
                };
                return stepBodyStyle;
            }
        }

        private static string L(string key, string fallback)
        {
            return EzMirrorLocalization.Get(key, fallback);
        }

        private static string LF(string key, string fallback, params object[] args)
        {
            return EzMirrorLocalization.Format(key, fallback, args);
        }

        private static GUIContent LC(string key, string fallback)
        {
            return new GUIContent(L(key, fallback));
        }

        private static GUIContent LC(
            string key,
            string fallback,
            string tooltipKey,
            string tooltipFallback
        )
        {
            return new GUIContent(L(key, fallback), L(tooltipKey, tooltipFallback));
        }

        private static GUIStyle SectionHeaderStyle
        {
            get
            {
                if (sectionHeaderStyle == null)
                {
                    sectionHeaderStyle = new GUIStyle(EditorStyles.boldLabel)
                    {
                        fontSize = 12,
                        wordWrap = true
                    };
                    sectionHeaderStyle.normal.textColor = EditorGUIUtility.isProSkin
                        ? NiAkaPinkLight
                        : NiAkaBlackSoft;
                }
                return sectionHeaderStyle;
            }
        }

        private static GUIStyle StatusBadgeStyle
        {
            get
            {
                if (statusBadgeStyle == null)
                {
                    statusBadgeStyle = new GUIStyle(EditorStyles.boldLabel)
                    {
                        alignment = TextAnchor.MiddleLeft
                    };
                }
                return statusBadgeStyle;
            }
        }

        private static GUIStyle StatusTitleStyle
        {
            get
            {
                statusTitleStyle ??= new GUIStyle(EditorStyles.boldLabel);
                return statusTitleStyle;
            }
        }

        private static GUIStyle HeroBannerTitleStyle
        {
            get
            {
                if (heroBannerTitleStyle == null)
                {
                    heroBannerTitleStyle = new GUIStyle(EditorStyles.boldLabel)
                    {
                        fontSize = 15,
                        alignment = TextAnchor.MiddleLeft,
                        padding = new RectOffset(12, 12, 0, 0)
                    };
                    heroBannerTitleStyle.normal.textColor = new Color(1f, 0.98f, 0.99f);
                }
                return heroBannerTitleStyle;
            }
        }

        private static GUIStyle HeroDescriptionStyle
        {
            get
            {
                if (heroDescriptionStyle == null)
                {
                    heroDescriptionStyle = new GUIStyle(EditorStyles.label)
                    {
                        fontSize = 12,
                        wordWrap = true,
                        richText = true,
                        padding = new RectOffset(12, 10, 9, 9)
                    };
                }
                return heroDescriptionStyle;
            }
        }

        private static GUIStyle IconPreviewPlaceholderStyle
        {
            get
            {
                if (iconPreviewPlaceholderStyle == null)
                {
                    iconPreviewPlaceholderStyle = new GUIStyle(EditorStyles.centeredGreyMiniLabel)
                    {
                        alignment = TextAnchor.MiddleCenter,
                        fontStyle = FontStyle.Italic,
                        wordWrap = true
                    };
                }
                return iconPreviewPlaceholderStyle;
            }
        }

        private static GUIStyle IconPreviewBadgeStyle
        {
            get
            {
                if (iconPreviewBadgeStyle == null)
                {
                    iconPreviewBadgeStyle = new GUIStyle(EditorStyles.miniLabel)
                    {
                        alignment = TextAnchor.MiddleCenter,
                        padding = new RectOffset(4, 4, 1, 1)
                    };
                    iconPreviewBadgeStyle.normal.textColor = EditorGUIUtility.isProSkin
                        ? NiAkaPinkLight
                        : HeroBannerColor;
                }
                return iconPreviewBadgeStyle;
            }
        }

        [InitializeOnLoadMethod]
        private static void InitializeEditorPreviewMaterialRepair()
        {
            QueueEditorPreviewMaterialRepair();
            EditorApplication.projectChanged -= QueueEditorPreviewMaterialRepair;
            EditorApplication.projectChanged += QueueEditorPreviewMaterialRepair;
            EditorApplication.hierarchyChanged -= QueueEditorPreviewMaterialRepair;
            EditorApplication.hierarchyChanged += QueueEditorPreviewMaterialRepair;
            UnityEditor.SceneManagement.EditorSceneManager.sceneOpened -= OnEditorSceneOpened;
            UnityEditor.SceneManagement.EditorSceneManager.sceneOpened += OnEditorSceneOpened;
        }

        private static void OnEditorSceneOpened(
            UnityEngine.SceneManagement.Scene scene,
            UnityEditor.SceneManagement.OpenSceneMode mode
        )
        {
            QueueEditorPreviewMaterialRepair();
        }

        private static void QueueEditorPreviewMaterialRepair()
        {
            if (editorPreviewRepairQueued)
                return;

            editorPreviewRepairQueued = true;
            EditorApplication.delayCall += RunQueuedEditorPreviewMaterialRepair;
        }

        private static void RunQueuedEditorPreviewMaterialRepair()
        {
            editorPreviewRepairQueued = false;
            RepairPersistentEditorPreviewMaterial();
        }

        internal static void RepairPersistentEditorPreviewMaterial()
        {
            Material source = EzMirrorAssetResolver.LoadDummyBaseMaterial();
            Material previewMaterial = AssetDatabase.LoadAssetAtPath<Material>(
                EzMirrorAssetResolver.GetEditorPreviewMaterialPath()
            );
            if (source == null || previewMaterial == null)
                return;

            RepairEditorPreviewMaterial(previewMaterial, source);
            EnsureEditorPreviewMaterialLabel(previewMaterial);
        }

        private void OnEnable()
        {
            usePersist = serializedObject.FindProperty(nameof(EzMirror.usePersist));
            persistKey = serializedObject.FindProperty(nameof(EzMirror.persistKey));
            profileMirror = serializedObject.FindProperty(nameof(EzMirror.profileMirror));
            profileRenderer = serializedObject.FindProperty(nameof(EzMirror.profileRenderer));
            triggerCollider = serializedObject.FindProperty(nameof(EzMirror.triggerCollider));
            triggerRefreshInterval = serializedObject.FindProperty(nameof(EzMirror.triggerRefreshInterval));
            profileMaterials = serializedObject.FindProperty(nameof(EzMirror.profileMaterials));
            profileBaseMaterials = serializedObject.FindProperty(nameof(EzMirror.profileBaseMaterials));
            profileNames = serializedObject.FindProperty(nameof(EzMirror.profileNames));
            profileIsDefault = serializedObject.FindProperty(nameof(EzMirror.profileIsDefault));
            profileMirrorEnabled = serializedObject.FindProperty(nameof(EzMirror.profileMirrorEnabled));
            profileDisablePixelLights = serializedObject.FindProperty(nameof(EzMirror.profileDisablePixelLights));
            profileTurnOffMirrorOcclusion = serializedObject.FindProperty(nameof(EzMirror.profileTurnOffMirrorOcclusion));
            profileReflectLayers = serializedObject.FindProperty(nameof(EzMirror.profileReflectLayers));
            profileCameraClearFlags = serializedObject.FindProperty(nameof(EzMirror.profileCameraClearFlags));
            profileCustomSkyboxes = serializedObject.FindProperty(nameof(EzMirror.profileCustomSkyboxes));
            profileCustomClearColors = serializedObject.FindProperty(nameof(EzMirror.profileCustomClearColors));
            profileHideBackground = serializedObject.FindProperty(nameof(EzMirror.profileHideBackground));
            profileTransparency = serializedObject.FindProperty(nameof(EzMirror.profileTransparency));
            profilePcOnly = serializedObject.FindProperty(nameof(EzMirror.profilePcOnly));
            profileActiveObjects = serializedObject.FindProperty(nameof(EzMirror.profileActiveObjects));
            profileActiveObjectModes = serializedObject.FindProperty(nameof(EzMirror.profileActiveObjectModes));
            profileInactiveObjects = serializedObject.FindProperty(nameof(EzMirror.profileInactiveObjects));
            profileInactiveObjectModes = serializedObject.FindProperty(nameof(EzMirror.profileInactiveObjectModes));
            iconImage = serializedObject.FindProperty(nameof(EzMirror.iconImage));
            profileIconSprites = serializedObject.FindProperty(nameof(EzMirror.profileIconSprites));
            profileIconColors = serializedObject.FindProperty(nameof(EzMirror.profileIconColors));
            profileIconObjects = serializedObject.FindProperty(nameof(EzMirror.profileIconObjects));
            enableUguiInput = serializedObject.FindProperty(nameof(EzMirror.enableUguiInput));
            enableInteractInput = serializedObject.FindProperty(nameof(EzMirror.enableInteractInput));
            enableContactInput = serializedObject.FindProperty(nameof(EzMirror.enableContactInput));
            inputProxy = serializedObject.FindProperty(nameof(EzMirror.inputProxy));
            interactInputProxy = serializedObject.FindProperty(nameof(EzMirror.interactInputProxy));
            inputHitboxCollider = serializedObject.FindProperty(nameof(EzMirror.inputHitboxCollider));
            contactReceiver = serializedObject.FindProperty(nameof(EzMirror.contactReceiver));
            contactCooldownSeconds = serializedObject.FindProperty(nameof(EzMirror.contactCooldownSeconds));
            uiToggle = serializedObject.FindProperty(nameof(EzMirror.uiToggle));
            enableClickSound = serializedObject.FindProperty(nameof(EzMirror.enableClickSound));
            clickClip = serializedObject.FindProperty(nameof(EzMirror.clickClip));
            sfxAudioSource = serializedObject.FindProperty(nameof(EzMirror.sfxAudioSource));
            enableHapticFeedback = serializedObject.FindProperty(nameof(EzMirror.enableHapticFeedback));
            hapticDuration = serializedObject.FindProperty(nameof(EzMirror.hapticDuration));
            hapticAmplitude = serializedObject.FindProperty(nameof(EzMirror.hapticAmplitude));
            hapticFrequency = serializedObject.FindProperty(nameof(EzMirror.hapticFrequency));
            useSwitchAnimator = serializedObject.FindProperty(nameof(EzMirror.useSwitchAnimator));
            switchAnimator = serializedObject.FindProperty(nameof(EzMirror.switchAnimator));
            switchAnimatorProfileIndexParameter = serializedObject.FindProperty(nameof(EzMirror.switchAnimatorProfileIndexParameter));
            switchAnimatorStateParameter = serializedObject.FindProperty(nameof(EzMirror.switchAnimatorStateParameter));
            switchAnimatorPressParameter = serializedObject.FindProperty(nameof(EzMirror.switchAnimatorPressParameter));
            switchAnimatorCanSetProfileIndex = serializedObject.FindProperty(nameof(EzMirror.switchAnimatorCanSetProfileIndex));
            switchAnimatorCanSetState = serializedObject.FindProperty(nameof(EzMirror.switchAnimatorCanSetState));
            switchAnimatorCanSetPress = serializedObject.FindProperty(nameof(EzMirror.switchAnimatorCanSetPress));
        }

        public override void OnInspectorGUI()
        {
            if (UdonSharpGUI.DrawDefaultUdonSharpBehaviourHeader(target))
                return;

            serializedObject.Update();

            EzMirror mirror = (EzMirror)target;
            EnsureEditorPreviewMaterialsForTargets();
            DrawLanguageToolbar();
            DrawHero(mirror);
            DrawOneClickSetup();
            DrawSetupChecklist(mirror);
            DrawHealthCheck(mirror);
            DrawStepControls();
            DrawMirrorTransformStep();
            DrawSwitchTransformStep();
            DrawDisplayRangeStep();
            DrawFadeDistanceStep();
            DrawProfileSettingsStep();
            DrawSyncStep();
            DrawSfxStep();
            DrawDeveloperOptions();

            if (serializedObject.ApplyModifiedProperties())
            {
                CopyAllProxyValuesToUdon();
                ApplyProfileMaterialParametersToTargets();
                ApplyInputStateToTargets();
            }
        }

        private static void DrawLanguageToolbar()
        {
            using (new EditorGUILayout.HorizontalScope())
            {
                EditorGUILayout.LabelField(
                    EzMirrorLocalization.Get("Language", "言語"),
                    GUILayout.Width(48f)
                );
                int selected = GUILayout.Toolbar(
                    EzMirrorLocalization.CurrentLanguageIndex,
                    EzMirrorLocalization.LanguageNames
                );
                if (selected != EzMirrorLocalization.CurrentLanguageIndex)
                    EzMirrorLocalization.CurrentLanguageIndex = selected;
            }
        }

        private void DrawHero(EzMirror mirror)
        {
            EditorGUILayout.Space();
            DrawHeroHeader(
                L(
                    "HeroTitle",
                    "EzMirror かんたん設定"
                ),
                L(
                    "HeroDescription",
                    "1つの Mirror に、Off / HQ / 透明 / LQ などのプロファイルを切り替えて適用します。通常は「まず押す」だけで基本設定が整います。"
                )
            );
            EditorGUILayout.Space(4f);

            MessageType messageType = IsReadyForBasicUse(mirror)
                ? MessageType.Info
                : MessageType.Warning;
            EditorGUILayout.HelpBox(GetSetupMessage(mirror), messageType);
        }

        private static void DrawHeroHeader(string title, string description)
        {
            Rect bannerRect = GUILayoutUtility.GetRect(
                GUIContent.none,
                GUIStyle.none,
                GUILayout.Height(31f)
            );
            EditorGUI.DrawRect(bannerRect, HeroBannerColor);
            EditorGUI.DrawRect(
                new Rect(bannerRect.x, bannerRect.yMax - 1f, bannerRect.width, 1f),
                HeroBannerBorderColor
            );
            GUI.Label(bannerRect, title, HeroBannerTitleStyle);

            Color previousContentColor = GUI.contentColor;
            GUI.contentColor = Color.clear;
            GUILayout.Label(description, HeroDescriptionStyle);
            GUI.contentColor = previousContentColor;

            Rect descriptionRect = GUILayoutUtility.GetLastRect();
            EditorGUI.DrawRect(descriptionRect, ResolveHeroDescriptionBgColor());
            EditorGUI.DrawRect(
                new Rect(descriptionRect.x, descriptionRect.y, 3f, descriptionRect.height),
                HeroDescriptionAccentColor
            );
            EditorGUI.DrawRect(
                new Rect(descriptionRect.x, descriptionRect.yMax - 1f, descriptionRect.width, 1f),
                HeroBannerBorderColor
            );
            GUI.Label(descriptionRect, description, HeroDescriptionStyle);
        }

        private static Color ResolveHeroDescriptionBgColor()
        {
            if (!EditorGUIUtility.isProSkin)
                return HeroDescriptionBgColor;
            return new Color(0.22f, 0.15f, 0.19f);
        }


        private static void DrawStepControls()
        {
            using (new EditorGUILayout.HorizontalScope())
            {
                GUILayout.FlexibleSpace();
                if (GUILayout.Button(L("ExpandAllSteps", "全STEPを展開"), EditorStyles.miniButtonLeft, GUILayout.Width(96f)))
                    SetAllStepSectionsExpanded(true);
                if (GUILayout.Button(L("CollapseAllSteps", "全STEPを閉じる"), EditorStyles.miniButtonRight, GUILayout.Width(96f)))
                    SetAllStepSectionsExpanded(false);
            }
        }

        private static void SetAllStepSectionsExpanded(bool expanded)
        {
            for (int i = 1; i <= 7; i++)
                EditorPrefs.SetBool(StepPrefsPrefix + "Step " + i, expanded);
        }

        private void DrawMirrorTransformStep()
        {
            DrawStepSection("Step 1", "ミラーの大きさと位置を調整", () =>
            {
                EditorGUILayout.HelpBox(
                    L("Step1Help", "Sceneビューでミラー面を選択して、位置・回転・大きさを調整します。"),
                    MessageType.Info
                );
                SelectObjectReference(
                    profileRenderer,
                    L("SelectRendererButton", "ミラー面を選択"),
                    L("MissingRenderer", "ミラー面が未設定です。先に「基本セットアップを自動実行」するか、開発者オプションで設定してください。")
                );
            });
        }


        private void DrawDisplayRangeStep()
        {
            DrawStepSection("Step 3", "表示させつづける範囲を設定", () =>
            {
                EditorGUILayout.HelpBox(
                    L("Step3RendererHint", "この箱の中に入るとミラーが表示されます。Sceneビューで箱の大きさを調整してください。"),
                    MessageType.Info
                );
                EditorGUILayout.HelpBox(
                    L("Step3GizmoHelp", "Sceneビューで本体または子オブジェクトを選択すると、表示範囲の枠がGizmoで表示されます。"),
                    MessageType.None
                );
                SelectObjectReference(
                    triggerCollider,
                    L("SelectTriggerButton", "表示範囲の箱を選択"),
                    L("MissingTrigger", "表示範囲の箱が未設定です。先に「基本セットアップを自動実行」するか、開発者オプションで設定してください。")
                );
            });
        }

        private void DrawFadeDistanceStep()
        {
            DrawStepSection("Step 4", "フェード距離設定", DrawCommonFadeDistanceSettings);
        }

        private void DrawProfileSettingsStep()
        {
            DrawStepSection("Step 5", "プロファイル確認・設定（任意）", () =>
            {
                EditorGUILayout.HelpBox(
                    L("Step5Help", "デフォルトだとHQ⇒半透明⇒LQミラーとなります。PC以外はLQのみ"),
                    MessageType.Info
                );
                DrawProfileCards();
            });
        }

        private void DrawSyncStep()
        {
            DrawStepSection("Step 6", "保存設定（任意）", DrawLocalSaveSettings);
        }

        private void DrawSfxStep()
        {
            DrawStepSection("Step 7", "効果音・振動設定（任意）", DrawSfx);
        }

        private static void DrawReferenceField(SerializedProperty property, string label, string hint)
        {
            using (new EditorGUILayout.HorizontalScope())
            {
                EditorGUILayout.PropertyField(property, new GUIContent(label));
                GUILayout.Label(
                    property.objectReferenceValue == null ? L("NotSet", "未設定") : "OK",
                    GUILayout.Width(52)
                );
            }

            if (property.objectReferenceValue == null)
                EditorGUILayout.HelpBox(hint, MessageType.Warning);
        }

        private static void SelectObjectReference(
            SerializedProperty property,
            string buttonLabel,
            string missingMessage
        )
        {
            UnityEngine.Object targetObject = property.objectReferenceValue;
            using (new EditorGUI.DisabledScope(targetObject == null))
            {
                if (GUILayout.Button(buttonLabel))
                    SelectAndPing(targetObject);
            }

            if (targetObject == null)
                EditorGUILayout.HelpBox(missingMessage, MessageType.Warning);
        }


        private static void SelectAndPing(UnityEngine.Object targetObject)
        {
            UnityEngine.Object selection = targetObject;
            if (targetObject is Component component)
                selection = component.gameObject;

            Selection.activeObject = selection;
            EditorGUIUtility.PingObject(selection);
        }


        private bool IsDefaultProfileIndex(int index)
        {
            return index >= 0
                && index < profileIsDefault.arraySize
                && profileIsDefault.GetArrayElementAtIndex(index).boolValue;
        }

        private string GetProfileTitle(int index, Material material)
        {
            if (index == 0)
                return LF("OffProfileTitle", "プロファイル 0（Off固定） - {0}", GetProfileDisplayName(index));
            return LF("ProfileTitlePrefix", "プロファイル {0}", index)
                + " - " + GetProfileDisplayName(index)
                + (material == null ? L("MaterialMissingSuffix", " - Material未生成") : L("MaterialGeneratedSuffix", " - Material自動生成済み"));
        }

        private string GetProfileDisplayName(int index)
        {
            if (profileNames != null && index >= 0 && index < profileNames.arraySize)
            {
                string savedName = profileNames.GetArrayElementAtIndex(index).stringValue;
                if (!string.IsNullOrEmpty(savedName))
                    return savedName;
            }

            return GetFallbackProfileName(index);
        }


        private Material[] GetUniqueProfileFadeMaterials()
        {
            List<Material> materials = new List<Material>();
            for (int i = 0; i < targets.Length; i++)
            {
                EzMirror mirror = targets[i] as EzMirror;
                if (mirror == null)
                    continue;

                AddUniqueFadeMaterials(mirror.profileMaterials, materials);
            }

            AddUniqueFadeMaterials(profileMaterials, materials);
            return materials.ToArray();
        }


        private string GetIconCardTitle(int index)
        {
            return index == OffProfileIndex
                ? L("IconCardOffTitle", "OFF時のUIアイコン")
                : L("IconCardProfileTitle", "切替UIに出すアイコン");
        }

        private string GetIconCardDescription(
            int index,
            EzMirrorProfileAsset profileAsset,
            bool isMixed,
            bool isUsingDefault,
            bool hasLocalOverride,
            bool isEditingDisabled,
            Sprite previewSprite
        )
        {
            if (isEditingDisabled)
            {
                return L(
                    "IconMultiSelectDisabled",
                    "通常プロファイルのアイコンはプロファイルアセットに保存します。1つのEzMirrorだけを選択して編集してください。"
                );
            }

            if (isMixed)
                return L("IconPreviewMixed", "複数選択中はプレビューを省略します。");

            string source;
            if (hasLocalOverride)
            {
                source = L(
                    "IconSourceLegacyLocal",
                    "旧設定の個別アイコンを表示中です。変更するとプロファイルアセットへ保存されます。"
                );
            }
            else if (profileAsset != null)
            {
                source = L("IconSourceProfileAsset", "プロファイルアセットに保存されます。");
            }
            else if (index == OffProfileIndex)
            {
                source = L("IconSourceOff", "OFFアイコンはこのEzMirrorだけに保存されます。");
            }
            else
            {
                source = L("IconSourceLocal", "このEzMirrorだけのアイコンとして保存されます。");
            }

            string state;
            if (isUsingDefault)
                state = L("IconUsingDefault", "標準アイコンを使用中");
            else if (previewSprite != null)
                state = previewSprite.name;
            else
                state = L("IconPreviewMissing", "アイコン未設定です。標準アイコンも見つかりません。");

            return source + " " + state;
        }


        private void DrawLocalSaveSettings()
        {
            EditorGUI.indentLevel++;
            EditorGUILayout.HelpBox(
                L("LocalOnlyMirrorHelp", "ミラーの切り替え状態は各ユーザーごとに独立します。必要な場合だけ、同じユーザーが再入室した時の状態復元を有効にしてください。"),
                MessageType.Info
            );
            EditorGUILayout.PropertyField(usePersist, LC("LocalSaveLabel", "ローカル保存する"));
            if (usePersist.hasMultipleDifferentValues)
            {
                EditorGUILayout.HelpBox(
                    L("PersistMixedHelp", "複数選択中は、ローカル保存のON/OFFだけまとめて変更できます。保存キーの詳細は1つずつ確認してください。"),
                    MessageType.None
                );
                EditorGUI.indentLevel--;
                return;
            }

            if (!usePersist.boolValue)
            {
                EditorGUILayout.HelpBox(
                    L("PersistDisabledHelp", "OFFのままなら保存キーは不要です。"),
                    MessageType.None
                );
                EditorGUI.indentLevel--;
                return;
            }

            bool missingPersistKey = string.IsNullOrEmpty(persistKey.stringValue);
            EditorGUILayout.LabelField(
                L("PersistKeyStatusLabel", "保存キー"),
                missingPersistKey
                    ? L("PersistKeyMissing", "未作成")
                    : L("PersistKeyConfigured", "自動設定済み")
            );
            if (missingPersistKey)
            {
                EditorGUILayout.HelpBox(
                    L("PersistKeyMissingHelp", "ローカル保存を使うには保存キーが必要です。通常は自動作成で問題ありません。"),
                    MessageType.Warning
                );
                if (GUILayout.Button(L("CreatePersistKeyButton", "保存キーを自動作成")))
                {
                    ApplyToTargets(
                        mirror => EzMirrorRepairUtility.GeneratePersistKeyForMirror(mirror, targets.Length == 1),
                        "Generate EzMirror Persist Key"
                    );
                    GUIUtility.ExitGUI();
                }
            }

            if (DrawAdvancedFoldout(PersistAdvancedPrefsKey, L("PersistAdvancedFoldout", "保存キーの詳細設定")))
            {
                EditorGUILayout.PropertyField(persistKey, LC("PersistKeyLabel", "保存キー"));
                EditorGUILayout.HelpBox(
                    L("DefaultProfileHelp", "初期状態 / Reset 先はプロファイルカードの「このプロファイルを初期状態にする」で指定します。複数指定時は先頭、未指定時は Off になります。"),
                    MessageType.None
                );
            }
            EditorGUI.indentLevel--;
        }

        private void DrawSfx()
        {
            EditorGUI.indentLevel++;
            EditorGUILayout.PropertyField(enableClickSound, LC("EnableClickSoundLabel", "効果音を有効にする"));
            if (ShouldShowInputDetails(enableClickSound))
            {
                EditorGUILayout.PropertyField(clickClip, LC("ClickClipLabel", "クリック音"));
                if (DrawAdvancedFoldout(SfxAdvancedPrefsKey, L("SfxAdvancedFoldout", "効果音の詳細設定")))
                    EditorGUILayout.PropertyField(sfxAudioSource, LC("SfxAudioSourceLabel", "音を鳴らすAudio Source"));
            }
            EditorGUILayout.Space(4f);
            EditorGUILayout.PropertyField(enableHapticFeedback, LC("EnableHapticsLabel", "コントローラー振動を有効にする"));
            if (ShouldShowInputDetails(enableHapticFeedback)
                && DrawAdvancedFoldout(HapticsAdvancedPrefsKey, L("HapticsAdvancedFoldout", "振動の詳細設定")))
            {
                EditorGUILayout.PropertyField(hapticDuration, LC("HapticDurationLabel", "振動時間"));
                EditorGUILayout.PropertyField(hapticAmplitude, LC("HapticAmplitudeLabel", "振動の強さ"));
                EditorGUILayout.PropertyField(hapticFrequency, LC("HapticFrequencyLabel", "振動の周波数"));
            }
            EditorGUI.indentLevel--;
        }

        private static bool DrawAdvancedFoldout(string prefsKey, string label)
        {
            bool expanded = EditorPrefs.GetBool(prefsKey, false);
            bool newExpanded = EditorGUILayout.Foldout(expanded, label, true);
            if (newExpanded != expanded)
                EditorPrefs.SetBool(prefsKey, newExpanded);
            return newExpanded;
        }

        private static void DrawStepSection(string step, string title, Action drawContent)
        {
            string prefsKey = StepPrefsPrefix + step;
            bool expanded = EditorPrefs.GetBool(prefsKey, true);
            string localizedTitle = EzMirrorLocalization.Get(GetStepTitleKey(step), title);
            DrawInspectorSectionGap(StepHeaderTopSpacing);
            bool newExpanded = EditorGUILayout.Foldout(
                expanded,
                step + " - " + localizedTitle,
                true,
                EditorStyles.foldoutHeader
            );
            if (newExpanded != expanded)
                EditorPrefs.SetBool(prefsKey, newExpanded);
            if (!newExpanded)
                return;

            DrawInspectorSectionGap(SectionHeaderBottomSpacing);
            using (new EditorGUILayout.VerticalScope(StepBodyStyle))
            {
                drawContent();
            }
            DrawInspectorSectionGap(StepSectionBottomSpacing);
        }

        private static void DrawSectionHeader(string label)
        {
            DrawInspectorSectionGap(SectionOuterTopSpacing);
            string localizedLabel = label == "現在の状態"
                ? EzMirrorLocalization.Get("StatusHeader", label)
                : label;
            Rect rect = EditorGUILayout.GetControlRect(false, EditorGUIUtility.singleLineHeight + 3f);
            EditorGUI.DrawRect(
                new Rect(rect.x, rect.y + rect.height - 2f, rect.width, 2f),
                new Color(NiAkaPink.r, NiAkaPink.g, NiAkaPink.b, 0.45f)
            );
            EditorGUI.LabelField(rect, localizedLabel, SectionHeaderStyle);
            DrawInspectorSectionGap(SectionHeaderBottomSpacing);
        }

        private static void DrawInspectorSectionGap(float height)
        {
            if (height > 0f)
                EditorGUILayout.Space(height);
        }

        private static string GetStepTitleKey(string step)
        {
            switch (step)
            {
                case "Step 1":
                    return "Step1Title";
                case "Step 2":
                    return "Step2Title";
                case "Step 3":
                    return "Step3Title";
                case "Step 4":
                    return "Step4Title";
                case "Step 5":
                    return "Step5Title";
                case "Step 6":
                    return "Step6Title";
                case "Step 7":
                    return "Step7Title";
                default:
                    return step;
            }
        }


        private void ApplyToTargets(System.Action<EzMirror> action, string undoName)
        {
            for (int i = 0; i < targets.Length; i++)
            {
                EzMirror mirror = (EzMirror)targets[i];
                Undo.RecordObject(mirror, undoName);
                action(mirror);
                UdonSharpEditorUtility.CopyProxyToUdon(mirror);
                EditorUtility.SetDirty(mirror);
                PrefabUtility.RecordPrefabInstancePropertyModifications(mirror);
            }

            serializedObject.Update();
        }

        private void CopyAllProxyValuesToUdon()
        {
            for (int i = 0; i < targets.Length; i++)
            {
                UdonSharpEditorUtility.CopyProxyToUdon((EzMirror)targets[i]);
                EditorUtility.SetDirty(targets[i]);
                PrefabUtility.RecordPrefabInstancePropertyModifications(targets[i]);
            }
        }


        private void ApplyProfileMaterialParametersToTargets()
        {
            for (int i = 0; i < targets.Length; i++)
            {
                EzMirror mirror = targets[i] as EzMirror;
                if (mirror == null || mirror.profileMaterials == null)
                    continue;

                bool changed = false;
                for (int profileIndex = 1; profileIndex < mirror.profileMaterials.Length; profileIndex++)
                {
                    changed |= ApplyDefaultProfileMaterialParameters(
                        mirror.profileMaterials[profileIndex],
                        mirror,
                        profileIndex,
                        true,
                        false
                    );
                }

                if (!changed)
                    continue;
                EditorUtility.SetDirty(mirror);
                PrefabUtility.RecordPrefabInstancePropertyModifications(mirror);
            }
        }


        public static bool TryGetFadeDistances(
            Material material,
            out float fadeStart,
            out float fadeComplete
        )
        {
            fadeStart = 0f;
            fadeComplete = 0f;
            if (material == null)
                return false;
            if (!HasDistanceFadeProperties(material))
                return false;

            fadeStart = Mathf.Max(0f, material.GetFloat(DistanceFadeStartProperty));
            fadeComplete = fadeStart + Mathf.Max(0.01f, material.GetFloat(DistanceFadeRangeProperty));
            return true;
        }

        public static bool TryGetHorizontalFadeDistances(
            Material material,
            out float fadeStart,
            out float fadeComplete
        )
        {
            fadeStart = 0f;
            fadeComplete = 0f;
            if (!HasHorizontalFadeProperties(material))
                return false;

            fadeStart = Mathf.Max(0f, material.GetFloat(HorizontalFadeStartProperty));
            fadeComplete = fadeStart + Mathf.Max(0.01f, material.GetFloat(HorizontalFadeRangeProperty));
            return true;
        }

        public static bool TryGetCommonFadeDistances(
            Material[] materials,
            out bool hasDistanceFade,
            out float distanceStart,
            out float distanceComplete,
            out bool hasHorizontalFade,
            out float horizontalStart,
            out float horizontalComplete
        )
        {
            hasDistanceFade = false;
            distanceStart = 0f;
            distanceComplete = 0f;
            hasHorizontalFade = false;
            horizontalStart = 0f;
            horizontalComplete = 0f;
            if (materials == null)
                return false;

            for (int i = 0; i < materials.Length; i++)
            {
                if (
                    !hasDistanceFade
                    && TryGetFadeDistances(materials[i], out distanceStart, out distanceComplete)
                )
                    hasDistanceFade = true;

                if (
                    !hasHorizontalFade
                    && TryGetHorizontalFadeDistances(materials[i], out horizontalStart, out horizontalComplete)
                )
                    hasHorizontalFade = true;

                if (hasDistanceFade && hasHorizontalFade)
                    break;
            }

            return hasDistanceFade || hasHorizontalFade;
        }

        public static bool TryCalculateTriggerAlignedFadeDistances(
            BoxCollider trigger,
            Transform fadeReference,
            float distanceRange,
            float horizontalRange,
            out float distanceStart,
            out float distanceComplete,
            out float horizontalStart,
            out float horizontalComplete
        )
        {
            distanceStart = 0f;
            distanceComplete = 0f;
            horizontalStart = 0f;
            horizontalComplete = 0f;
            if (trigger == null || fadeReference == null)
                return false;

            GetTriggerBoundsInReferenceSpace(
                trigger,
                fadeReference,
                out float minX,
                out float maxX,
                out _,
                out _,
                out float minZ,
                out float maxZ
            );

            distanceComplete = minZ < 0f
                ? -minZ
                : Mathf.Max(Mathf.Abs(minZ), Mathf.Abs(maxZ));
            horizontalComplete = Mathf.Max(0f, (maxX - minX) * 0.5f);
            if (distanceComplete <= 0.001f && horizontalComplete <= 0.001f)
                return false;

            float resolvedDistanceRange = Mathf.Min(Mathf.Max(0.01f, distanceRange), Mathf.Max(0.01f, distanceComplete));
            float resolvedHorizontalRange = Mathf.Min(Mathf.Max(0.01f, horizontalRange), Mathf.Max(0.01f, horizontalComplete));
            distanceStart = Mathf.Max(0f, distanceComplete - resolvedDistanceRange);
            horizontalStart = Mathf.Max(0f, horizontalComplete - resolvedHorizontalRange);
            return true;
        }

        public static bool TryCalculateTriggerAlignedHorizontalFadeCenter(
            BoxCollider trigger,
            Transform fadeReference,
            out float horizontalCenter
        )
        {
            horizontalCenter = 0f;
            if (trigger == null || fadeReference == null)
                return false;

            GetTriggerBoundsInReferenceSpace(
                trigger,
                fadeReference,
                out float minX,
                out float maxX,
                out _,
                out _,
                out _,
                out _
            );

            if (maxX - minX <= 0.001f)
                return false;

            horizontalCenter = (minX + maxX) * 0.5f;
            return true;
        }

        public static bool TryCalculateTriggerFadeGizmoBounds(
            BoxCollider trigger,
            Transform fadeReference,
            out Vector3 center,
            out Vector3 size
        )
        {
            center = Vector3.zero;
            size = Vector3.zero;
            if (trigger == null || fadeReference == null)
                return false;

            GetTriggerBoundsInReferenceSpace(
                trigger,
                fadeReference,
                out float minX,
                out float maxX,
                out float minY,
                out float maxY,
                out float minZ,
                out float maxZ
            );

            size = new Vector3(
                Mathf.Max(0.01f, maxX - minX),
                Mathf.Max(0.01f, maxY - minY),
                Mathf.Max(0.01f, maxZ - minZ)
            );
            center = new Vector3(
                (minX + maxX) * 0.5f,
                (minY + maxY) * 0.5f,
                (minZ + maxZ) * 0.5f
            );
            return true;
        }


    }
}
#endif
