#if UNITY_EDITOR
namespace NiAkaDev.EzMirror.Editor
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using UdonSharpEditor;
    using UnityEditor;
    using UnityEditor.Animations;
    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 const float InputAdvancedSectionIndent = 14f;

        private void DrawSwitchTransformStep()
        {
            DrawStepSection("Step 2", "切替方法とスイッチ位置を調整", () =>
            {
                EditorGUILayout.HelpBox(
                    L("Step2Help", "切替方法は組み合わせて使えます。レーザーやクリックで押す、近づいてUseする、手で触る、の3通りを選べます。"),
                    MessageType.Info
                );
                EditorGUILayout.PropertyField(
                    enableUguiInput,
                    LC("InputUgui", "UIボタンで切り替える（レーザー/クリック）")
                );
                EditorGUILayout.PropertyField(
                    enableInteractInput,
                    LC("InputInteract", "Useボタンで切り替える（近づいて押す）")
                );
                EditorGUILayout.PropertyField(
                    enableContactInput,
                    LC("InputContact", "手で触れて切り替える")
                );

                EditorGUILayout.HelpBox(
                    L("Step2PositionHelp", "スイッチUIの位置を基準に、UIボタン・Useできる場所・触れる判定をまとめて調整します。"),
                    MessageType.None
                );
                if (ShouldShowInputDetails(enableUguiInput))
                    SelectToggleCanvas();
                if (ShouldShowInputDetails(enableInteractInput))
                    SelectInteractInputHitbox();
                if (ShouldShowInputDetails(enableContactInput))
                    SelectContactInputTarget();

                DrawInputAdvancedSection(
                    enableInteractInput,
                    InteractAdvancedPrefsKey,
                    L("InteractAdvancedFoldout", "Useの詳細設定"),
                    DrawInteractInputDetails
                );
                DrawInputAdvancedSection(
                    enableContactInput,
                    ContactAdvancedPrefsKey,
                    L("ContactAdvancedFoldout", "触れる判定の詳細設定"),
                    DrawContactInputDetails
                );
                DrawSwitchAnimatorBeginnerSection();
            });
        }

        private void DrawSwitchAnimatorBeginnerSection()
        {
            EditorGUILayout.Space(4f);
            using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
            {
                EditorGUILayout.LabelField(
                    L("SwitchAnimatorBeginnerTitle", "3Dスイッチモデル"),
                    SectionHeaderStyle
                );
                EditorGUILayout.HelpBox(
                    L(
                        "SwitchAnimatorBeginnerHelp",
                        "3Dスイッチの押し込み動作やランプ表示を、現在のプロファイルに合わせて自動で動かします。通常はONのままで問題ありません。"
                    ),
                    MessageType.Info
                );
                EditorGUILayout.PropertyField(
                    useSwitchAnimator,
                    LC("UseSwitchAnimatorLabel", "3Dスイッチをアニメーションさせる")
                );

                EzMirror mirror = target as EzMirror;
                DrawSwitchAnimatorStatus(mirror);

                using (new EditorGUILayout.HorizontalScope())
                {
                    if (GUILayout.Button(L("RepairSwitchAnimatorButton", "3Dスイッチを自動修復")))
                    {
                        ApplyToTargets(RepairSwitchAnimatorSetup, "Repair EzMirror 3D Switch");
                        GUIUtility.ExitGUI();
                    }
                    using (new EditorGUI.DisabledScope(mirror == null || mirror.switchAnimator == null))
                    {
                        if (GUILayout.Button(L("SelectSwitchAnimatorButton", "3Dスイッチを選択")))
                            Selection.activeObject = mirror.switchAnimator.gameObject;
                    }
                }
            }
        }

        private static void DrawSwitchAnimatorStatus(EzMirror mirror)
        {
            string message;
            MessageType messageType;
            if (mirror == null)
            {
                message = L("SwitchAnimatorStatusMultiEdit", "複数選択時は個別の3Dスイッチ状態を確認してください。");
                messageType = MessageType.None;
            }
            else if (!mirror.useSwitchAnimator)
            {
                message = L("SwitchAnimatorStatusDisabled", "3DスイッチアニメーションはOFFです。");
                messageType = MessageType.None;
            }
            else if (mirror.switchAnimator == null)
            {
                message = L("SwitchAnimatorStatusMissing", "3DスイッチのAnimatorが未設定です。自動修復できます。");
                messageType = MessageType.Warning;
            }
            else if (mirror.switchAnimator.runtimeAnimatorController == null)
            {
                message = L("SwitchAnimatorStatusNoController", "Animator Controller が未設定です。自動修復できます。");
                messageType = MessageType.Warning;
            }
            else if (
                !mirror.switchAnimatorCanSetProfileIndex
                || !mirror.switchAnimatorCanSetState
                || !mirror.switchAnimatorCanSetPress
            )
            {
                message = L("SwitchAnimatorStatusInvalidParameters", "Animatorパラメーターに不足があります。開発者オプションで確認できます。");
                messageType = MessageType.Warning;
            }
            else
            {
                message = L("SwitchAnimatorStatusReady", "3Dスイッチは現在のプロファイルと入力に追従できます。");
                messageType = MessageType.Info;
            }

            EditorGUILayout.HelpBox(message, messageType);
        }

        private static void RepairSwitchAnimatorSetup(EzMirror mirror)
        {
            if (mirror == null)
                return;
            if (!mirror.useSwitchAnimator)
                mirror.useSwitchAnimator = true;

            Transform switchUi = EnsureSwitchUiRoot(
                mirror,
                out _,
                out bool migratedLegacyInput
            );
            EnsureSwitchAnimatorControls(mirror, switchUi);
            if (migratedLegacyInput)
                AutoWireReferences(mirror);
        }

        private void DrawInputAdvancedSection(
            SerializedProperty enabledProperty,
            string prefsKey,
            string label,
            Action drawDetails
        )
        {
            if (!ShouldShowInputDetails(enabledProperty))
                return;

            using (new EditorGUILayout.HorizontalScope())
            {
                GUILayout.Space(InputAdvancedSectionIndent);
                using (new EditorGUILayout.VerticalScope())
                {
                    if (DrawAdvancedFoldout(prefsKey, label))
                        drawDetails();
                }
            }
        }

        private void DrawInteractInputDetails()
        {
            if (!ShouldShowInputDetails(enableInteractInput))
                return;

            using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
            {
                EditorGUILayout.LabelField(
                    L("InteractDetailsTitle", "Useボタン設定"),
                    EditorStyles.boldLabel
                );
                EditorGUILayout.HelpBox(
                    L("InteractDetailsHelp", "近づいて Use したときに表示される文字と、Useできる距離です。通常はそのままで問題ありません。"),
                    MessageType.None
                );
                if (DrawInputDetailsMultiEditHelp())
                    return;

                UdonBehaviour backing = GetSingleInteractBacking();
                if (backing == null)
                {
                    EditorGUILayout.HelpBox(
                        L("MissingInteractProxy", "InteractInput が未設定です。「切替入力を自動修復」を押してください。"),
                        MessageType.Warning
                    );
                    return;
                }

                DrawUdonInteractSettings(backing);
            }
        }

        private void DrawContactInputDetails()
        {
            if (!ShouldShowInputDetails(enableContactInput))
                return;

            using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
            {
                EditorGUILayout.LabelField(
                    L("ContactDetailsTitle", "手のContact設定"),
                    EditorStyles.boldLabel
                );
                EditorGUILayout.HelpBox(
                    L("ContactDetailsHelp", "どのContactタグに反応するかを設定します。通常は Avatar の手・指タグだけを受け取る初期設定のままで問題ありません。"),
                    MessageType.None
                );
                EditorGUILayout.PropertyField(
                    contactCooldownSeconds,
                    LC("ContactCooldownLabel", "Contact連続切替防止秒数")
                );
                if (DrawInputDetailsMultiEditHelp())
                    return;

                VRCContactReceiver receiver = GetSingleContactReceiver();
                if (receiver == null)
                {
                    EditorGUILayout.HelpBox(
                        L("MissingContactReceiver", "ContactInput が未設定です。「切替入力を自動修復」を押してください。"),
                        MessageType.Warning
                    );
                    return;
                }

                DrawContactReceiverSettings(receiver);
            }
        }

        private bool DrawInputDetailsMultiEditHelp()
        {
            if (!serializedObject.isEditingMultipleObjects)
                return false;

            EditorGUILayout.HelpBox(
                L("InputDetailsMultiEditHelp", "複数選択中は詳細設定を個別に編集してください。切替方法のON/OFFはこのまままとめて変更できます。"),
                MessageType.Info
            );
            return true;
        }

        private static bool ShouldShowInputDetails(SerializedProperty enabledProperty)
        {
            return enabledProperty != null
                && (enabledProperty.boolValue || enabledProperty.hasMultipleDifferentValues);
        }

        private UdonBehaviour GetSingleInteractBacking()
        {
            EzMirror mirror = GetSingleTargetMirror();
            if (mirror == null)
                return null;

            EzMirrorInteractInputProxy proxy = mirror.interactInputProxy;
            if (proxy == null)
                proxy = mirror.GetComponentInChildren<EzMirrorInteractInputProxy>(true);
            return proxy == null ? null : UdonSharpEditorUtility.GetBackingUdonBehaviour(proxy);
        }

        private VRCContactReceiver GetSingleContactReceiver()
        {
            EzMirror mirror = GetSingleTargetMirror();
            if (mirror == null)
                return null;

            if (mirror.contactReceiver != null)
                return mirror.contactReceiver;
            return mirror.GetComponentInChildren<VRCContactReceiver>(true);
        }

        private EzMirror GetSingleTargetMirror()
        {
            return targets.Length == 1 ? target as EzMirror : null;
        }

        private void DrawUdonInteractSettings(UdonBehaviour backing)
        {
            SerializedObject serializedBacking = new SerializedObject(backing);
            serializedBacking.Update();
            EditorGUI.BeginChangeCheck();
            DrawSerializedProperty(
                serializedBacking,
                "interactText",
                LC("InteractTextLabel", "Use時に表示する文字")
            );
            DrawSerializedSlider(
                serializedBacking,
                "proximity",
                LC("InteractProximityLabel", "Useできる距離"),
                0f,
                100f
            );
            ApplyNestedComponentChanges(serializedBacking, backing);
        }

        private void DrawContactReceiverSettings(VRCContactReceiver receiver)
        {
            EditorGUILayout.HelpBox(
                L("ContactAvatarOnlyHelp", "送信元は Avatar の Contact Sender だけを受け取る設定を推奨します。World のContactには通常対応しません。"),
                MessageType.Info
            );

            SerializedObject serializedReceiver = new SerializedObject(receiver);
            serializedReceiver.Update();
            EditorGUI.BeginChangeCheck();
            DrawSerializedProperty(
                serializedReceiver,
                "size",
                LC("ContactShapeSizeLabel", "判定サイズ")
            );
            DrawSerializedProperty(
                serializedReceiver,
                "contentTypes",
                LC("ContactContentTypesLabel", "反応する送信元")
            );
            DrawSerializedProperty(
                serializedReceiver,
                "collisionTags",
                LC("ContactTagsLabel", "反応するContactタグ"),
                true
            );
            DrawSerializedProperty(
                serializedReceiver,
                "allowSelf",
                LC("ContactAllowSelfLabel", "自分のContactに反応")
            );
            DrawSerializedProperty(
                serializedReceiver,
                "allowOthers",
                LC("ContactAllowOthersLabel", "他人のContactに反応")
            );
            DrawSerializedProperty(
                serializedReceiver,
                "localOnly",
                LC("ContactLocalOnlyLabel", "ローカルだけで判定")
            );
            ApplyNestedComponentChanges(serializedReceiver, receiver);
        }

        private static void DrawSerializedProperty(
            SerializedObject serializedObject,
            string propertyName,
            GUIContent label,
            bool includeChildren = false
        )
        {
            SerializedProperty property = serializedObject.FindProperty(propertyName);
            if (property != null)
                EditorGUILayout.PropertyField(property, label, includeChildren);
        }

        private static void DrawSerializedSlider(
            SerializedObject serializedObject,
            string propertyName,
            GUIContent label,
            float minValue,
            float maxValue
        )
        {
            SerializedProperty property = serializedObject.FindProperty(propertyName);
            if (property != null)
                EditorGUILayout.Slider(property, minValue, maxValue, label);
        }

        private static void ApplyNestedComponentChanges(
            SerializedObject serializedObject,
            UnityEngine.Object targetObject
        )
        {
            bool changed = EditorGUI.EndChangeCheck();
            serializedObject.ApplyModifiedProperties();
            if (!changed)
                return;

            EditorUtility.SetDirty(targetObject);
            PrefabUtility.RecordPrefabInstancePropertyModifications(targetObject);
        }

        private void SelectToggleCanvas()
        {
            Toggle toggle = uiToggle.objectReferenceValue as Toggle;
            Canvas canvas = toggle == null ? null : toggle.GetComponentInParent<Canvas>(true);
            using (new EditorGUI.DisabledScope(canvas == null))
            {
                if (GUILayout.Button(L("SelectToggleCanvasButton", "スイッチUIを選択")))
                    SelectAndPing(canvas);
            }

            if (toggle == null)
            {
                EditorGUILayout.HelpBox(
                    L("MissingToggle", "切替UI Toggleが未設定です。先に「基本セットアップを自動実行」するか、開発者オプションで設定してください。"),
                    MessageType.Warning
                );
                return;
            }

            if (canvas == null)
                EditorGUILayout.HelpBox(L("MissingToggleCanvas", "ToggleUI の親に Canvas が見つかりません。"), MessageType.Warning);
        }

        private void SelectInteractInputHitbox()
        {
            BoxCollider collider = inputHitboxCollider.objectReferenceValue as BoxCollider;
            UnityEngine.Object targetObject = collider == null ? null : collider.gameObject;
            using (new EditorGUI.DisabledScope(targetObject == null))
            {
                if (GUILayout.Button(L("SelectInteractInputButton", "Useできる場所を選択")))
                    SelectAndPing(targetObject);
            }

            if (collider == null)
            {
                EditorGUILayout.HelpBox(
                    L("MissingSwitchInput", "Useできる場所が未設定です。先に「基本セットアップを自動実行」するか、開発者オプションで修復してください。"),
                    MessageType.Warning
                );
            }
        }

        private void SelectContactInputTarget()
        {
            VRCContactReceiver receiver = contactReceiver.objectReferenceValue as VRCContactReceiver;
            UnityEngine.Object targetObject = receiver == null ? null : receiver.gameObject;
            using (new EditorGUI.DisabledScope(targetObject == null))
            {
                if (GUILayout.Button(L("SelectContactInputButton", "触れる判定を選択")))
                    SelectAndPing(targetObject);
            }

            if (receiver == null)
            {
                EditorGUILayout.HelpBox(
                    L("MissingContactReceiver", "触れる判定が未設定です。先に「基本セットアップを自動実行」するか、開発者オプションで修復してください。"),
                    MessageType.Warning
                );
            }
        }

        private void ApplyInputStateToTargets()
        {
            for (int i = 0; i < targets.Length; i++)
                ApplyInputState(targets[i] as EzMirror);
        }

        private static void ApplyInputState(EzMirror mirror)
        {
            if (mirror == null)
                return;

            if (mirror.inputHitboxCollider != null)
            {
                SetColliderEnabled(mirror.inputHitboxCollider, mirror.enableInteractInput);
                SetGameObjectActive(mirror.inputHitboxCollider.gameObject, mirror.enableInteractInput);
            }
            if (mirror.contactReceiver != null)
            {
                SetBehaviourEnabled(mirror.contactReceiver, mirror.enableContactInput);
                SetGameObjectActive(mirror.contactReceiver.gameObject, mirror.enableContactInput);
            }
            SetCanvasColliderState(mirror);
        }

        private static void SetColliderEnabled(Collider collider, bool enabled)
        {
            if (collider == null || collider.enabled == enabled)
                return;

            Undo.RecordObject(collider, "Apply EzMirror Input State");
            collider.enabled = enabled;
            EditorUtility.SetDirty(collider);
            PrefabUtility.RecordPrefabInstancePropertyModifications(collider);
        }

        private static void SetBehaviourEnabled(Behaviour behaviour, bool enabled)
        {
            if (behaviour == null || behaviour.enabled == enabled)
                return;

            Undo.RecordObject(behaviour, "Apply EzMirror Input State");
            behaviour.enabled = enabled;
            EditorUtility.SetDirty(behaviour);
            PrefabUtility.RecordPrefabInstancePropertyModifications(behaviour);
        }

        private static void SetGameObjectActive(GameObject target, bool active)
        {
            if (target == null || target.activeSelf == active)
                return;

            Undo.RecordObject(target, "Apply EzMirror Input State");
            target.SetActive(active);
            EditorUtility.SetDirty(target);
            PrefabUtility.RecordPrefabInstancePropertyModifications(target);
        }

        private static void SetCanvasColliderState(EzMirror mirror)
        {
            if (mirror == null || mirror.uiToggle == null)
                return;

            Canvas canvas = mirror.uiToggle.GetComponentInParent<Canvas>(true);
            BoxCollider collider = canvas == null ? null : canvas.GetComponent<BoxCollider>();
            if (collider == null || collider.enabled == mirror.enableUguiInput)
                return;

            Undo.RecordObject(collider, "Apply EzMirror Canvas Input State");
            collider.enabled = mirror.enableUguiInput;
            EditorUtility.SetDirty(collider);
            PrefabUtility.RecordPrefabInstancePropertyModifications(collider);
        }

        private static void EnsureInputControls(EzMirror mirror)
        {
            if (mirror == null)
                return;

            Transform switchUi = EnsureSwitchUiRoot(
                mirror,
                out Vector3 inputLocalScale,
                out bool migratedLegacyInput
            );
            Transform canvas = EnsureSwitchCanvas(mirror, switchUi);
            Transform interact = EnsureSwitchInputChild(
                switchUi,
                InteractInputObjectName,
                inputLocalScale,
                migratedLegacyInput,
                !migratedLegacyInput
            );
            Transform contact = EnsureSwitchInputChild(
                switchUi,
                ContactInputObjectName,
                inputLocalScale,
                migratedLegacyInput,
                !migratedLegacyInput
            );

            BoxCollider canvasCollider = canvas == null ? null : canvas.GetComponent<BoxCollider>();
            if (canvas != null && canvasCollider == null)
                canvasCollider = Undo.AddComponent<BoxCollider>(canvas.gameObject);
            if (canvasCollider != null)
                canvasCollider.enabled = mirror.enableUguiInput;

            BoxCollider hitbox = interact.GetComponent<BoxCollider>();
            if (hitbox == null)
                hitbox = Undo.AddComponent<BoxCollider>(interact.gameObject);
            hitbox.isTrigger = false;
            hitbox.size = Vector3.one;
            hitbox.enabled = mirror.enableInteractInput;
            mirror.inputHitboxCollider = hitbox;
            SetLayerIfExists(interact.gameObject, InteractiveLayerName);
            interact.gameObject.SetActive(mirror.enableInteractInput);

            EzMirrorInteractInputProxy interactProxy =
                interact.GetComponent<EzMirrorInteractInputProxy>();
            if (interactProxy == null)
                interactProxy = UdonSharpUndo.AddComponent<EzMirrorInteractInputProxy>(interact.gameObject);
            interactProxy.targetMirror = mirror;
            mirror.interactInputProxy = interactProxy;

            VRCContactReceiver receiver = contact.GetComponent<VRCContactReceiver>();
            bool createdReceiver = receiver == null;
            if (createdReceiver)
                receiver = Undo.AddComponent<VRCContactReceiver>(contact.gameObject);
            if (createdReceiver)
                ConfigureContactReceiver(receiver, Vector3.one);
            else if (!HasContactCollisionTags(receiver))
                EnsureDefaultContactCollisionTags(receiver);
            receiver.enabled = mirror.enableContactInput;
            mirror.contactReceiver = receiver;
            SetLayerIfExists(contact.gameObject, InteractiveLayerName);
            contact.gameObject.SetActive(mirror.enableContactInput);

            EzMirrorInputProxy proxy = contact.GetComponent<EzMirrorInputProxy>();
            if (proxy == null)
                proxy = UdonSharpUndo.AddComponent<EzMirrorInputProxy>(contact.gameObject);
            proxy.targetMirror = mirror;
            mirror.inputProxy = proxy;

            EnsureSwitchAnimatorControls(mirror, switchUi);

            if (canvas != null)
            {
                SetLayerRecursivelyIfExists(canvas.gameObject, WalkthroughLayerName);
                EditorUtility.SetDirty(canvas.gameObject);
                PrefabUtility.RecordPrefabInstancePropertyModifications(canvas.gameObject);
            }

            UdonSharpEditorUtility.CopyProxyToUdon(interactProxy);
            UdonSharpEditorUtility.CopyProxyToUdon(proxy);
            EditorUtility.SetDirty(switchUi.gameObject);
            EditorUtility.SetDirty(interact.gameObject);
            EditorUtility.SetDirty(contact.gameObject);
            if (canvasCollider != null)
                EditorUtility.SetDirty(canvasCollider);
            EditorUtility.SetDirty(hitbox);
            EditorUtility.SetDirty(receiver);
            EditorUtility.SetDirty(interactProxy);
            EditorUtility.SetDirty(proxy);
            PrefabUtility.RecordPrefabInstancePropertyModifications(switchUi.gameObject);
            PrefabUtility.RecordPrefabInstancePropertyModifications(interact.gameObject);
            PrefabUtility.RecordPrefabInstancePropertyModifications(contact.gameObject);
            if (canvasCollider != null)
                PrefabUtility.RecordPrefabInstancePropertyModifications(canvasCollider);
            PrefabUtility.RecordPrefabInstancePropertyModifications(hitbox);
            PrefabUtility.RecordPrefabInstancePropertyModifications(receiver);
            PrefabUtility.RecordPrefabInstancePropertyModifications(interactProxy);
            PrefabUtility.RecordPrefabInstancePropertyModifications(proxy);
        }

        private static void EnsureSwitchAnimatorControls(EzMirror mirror, Transform switchUi)
        {
            if (mirror == null || switchUi == null || !mirror.useSwitchAnimator)
                return;

            bool managesDefaultSwitchModel;
            Transform switchModel = EnsureSwitchModelRoot(
                switchUi,
                out managesDefaultSwitchModel
            );

            Animator animator = switchModel.GetComponent<Animator>();
            if (animator == null)
                animator = Undo.AddComponent<Animator>(switchModel.gameObject);
            if (animator.runtimeAnimatorController == null)
                animator.runtimeAnimatorController =
                    EzMirrorAssetResolver.LoadDefaultSwitchAnimatorController();

            mirror.switchAnimator = animator;
            ResetSwitchAnimatorParameterNamesIfEmpty(mirror);
            ValidateSwitchAnimatorParameters(mirror);
            if (managesDefaultSwitchModel)
                AssignSwitchModelMaterials(switchModel);

            EditorUtility.SetDirty(animator);
            EditorUtility.SetDirty(mirror);
            PrefabUtility.RecordPrefabInstancePropertyModifications(animator);
            PrefabUtility.RecordPrefabInstancePropertyModifications(mirror);
        }

        private static Transform EnsureSwitchModelRoot(
            Transform switchUi,
            out bool managesDefaultSwitchModel
        )
        {
            managesDefaultSwitchModel = false;
            Transform switchModel = switchUi.Find(SwitchModelObjectName);
            if (switchModel != null)
            {
                managesDefaultSwitchModel = ShouldManageExistingSwitchModel(switchModel);
                if (managesDefaultSwitchModel)
                    FinalizeExistingSwitchModelRoot(switchModel);
                return switchModel;
            }

            Transform legacyRoot = switchUi.Find(LegacySwitch3DObjectName);
            if (legacyRoot != null)
            {
                managesDefaultSwitchModel = true;
                return MigrateLegacySwitchModelRoot(legacyRoot);
            }

            managesDefaultSwitchModel = true;
            Transform modelTemplate = FindDefaultPrefabTemplate(
                SwitchUiObjectName + "/" + SwitchModelObjectName
            );
            GameObject modelObject =
                modelTemplate == null ? null : Instantiate(modelTemplate.gameObject);
            if (modelObject == null)
            {
                GameObject modelPrefab = EzMirrorAssetResolver.LoadDefaultSwitchModel();
                modelObject = modelPrefab == null
                    ? new GameObject(SwitchModelObjectName)
                    : (GameObject)PrefabUtility.InstantiatePrefab(modelPrefab);
            }
            Undo.RegisterCreatedObjectUndo(modelObject, "Create EzMirror Switch Model");
            modelObject.name = SwitchModelObjectName;
            modelObject.transform.SetParent(switchUi, false);
            if (modelTemplate == null)
                FinalizeDefaultSwitchModelRoot(modelObject.transform);
            else
                CopyLocalTransform(modelTemplate, modelObject.transform);
            return modelObject.transform;
        }

        private static void FinalizeDefaultSwitchModelRoot(Transform switchModel)
        {
            UnpackDefaultSwitchModelPrefabRootIfNeeded(switchModel);
            NormalizeSwitchModelTransform(switchModel);
            RenameSwitchModelChildren(switchModel);
            RestoreMissingSwitchModelChildren(switchModel);
            RenameSwitchModelChildren(switchModel);
        }

        private static void FinalizeExistingSwitchModelRoot(Transform switchModel)
        {
            bool isDefaultPrefabRoot = IsDefaultSwitchModelPrefabRoot(switchModel);

            if (isDefaultPrefabRoot)
            {
                UnpackDefaultSwitchModelPrefabRootIfNeeded(switchModel);
                NormalizeSwitchModelTransform(switchModel);
                RenameSwitchModelChildren(switchModel);
                RestoreMissingSwitchModelChildren(switchModel);
                RenameSwitchModelChildren(switchModel);
                return;
            }

            if (switchModel.childCount == 0)
                RestoreMissingSwitchModelChildren(switchModel);
        }

        private static void UnpackDefaultSwitchModelPrefabRootIfNeeded(Transform switchModel)
        {
            if (switchModel == null)
                return;

            GameObject nearestPrefabRoot =
                PrefabUtility.GetNearestPrefabInstanceRoot(switchModel.gameObject);
            if (nearestPrefabRoot != switchModel.gameObject)
                return;
            if (!IsDefaultSwitchModelPrefabRoot(switchModel))
                return;

            PrefabUtility.UnpackPrefabInstance(
                nearestPrefabRoot,
                PrefabUnpackMode.Completely,
                InteractionMode.AutomatedAction
            );
        }

        private static Transform MigrateLegacySwitchModelRoot(Transform legacyRoot)
        {
            Undo.RecordObject(legacyRoot.gameObject, "Migrate EzMirror 3D Switch Model");
            legacyRoot.gameObject.name = SwitchModelObjectName;
            UnpackDefaultSwitchModelPrefabRootIfNeeded(legacyRoot);

            Transform nestedLegacyModel = legacyRoot.Find(LegacySwitchModelObjectName);
            if (nestedLegacyModel != null)
            {
                while (nestedLegacyModel.childCount > 0)
                {
                    Transform child = nestedLegacyModel.GetChild(0);
                    Undo.SetTransformParent(child, legacyRoot, "Migrate EzMirror Switch Model Child");
                }
                Undo.DestroyObjectImmediate(nestedLegacyModel.gameObject);
            }

            NormalizeSwitchModelTransform(legacyRoot);
            RenameSwitchModelChildren(legacyRoot);
            RestoreMissingSwitchModelChildren(legacyRoot);
            RenameSwitchModelChildren(legacyRoot);
            return legacyRoot;
        }

        private static bool IsDefaultSwitchModelPrefabRoot(Transform switchModel)
        {
            if (switchModel == null)
                return false;

            GameObject source =
                PrefabUtility.GetCorrespondingObjectFromSource(switchModel.gameObject);
            return source != null && AssetDatabase.GetAssetPath(source) == EzMirrorPackageDefinition.SwitchModelPath;
        }

        private static void NormalizeSwitchModelTransform(Transform switchModel)
        {
            Undo.RecordObject(switchModel, "Normalize EzMirror Switch Model");
            switchModel.localPosition = Vector3.zero;
            switchModel.localRotation = Quaternion.Euler(90f, 180f, 0f);
            switchModel.localScale = Vector3.one;
        }

        private static void RenameSwitchModelChildren(Transform switchModel)
        {
            RenameChildIfExists(switchModel, LegacySwitchBaseObjectName, SwitchBaseObjectName);
            RenameChildIfExists(switchModel, LegacySwitchButtonObjectName, SwitchButtonObjectName);
            SetChildSiblingIndexIfExists(switchModel, SwitchBaseObjectName, 0);
            SetChildSiblingIndexIfExists(switchModel, SwitchButtonObjectName, 1);
        }

        private static bool ShouldManageExistingSwitchModel(Transform switchModel)
        {
            if (switchModel == null)
                return false;

            return IsDefaultSwitchModelPrefabRoot(switchModel) || switchModel.childCount == 0;
        }

        private static void RestoreMissingSwitchModelChildren(Transform switchModel)
        {
            if (switchModel == null || HasRequiredSwitchModelChildren(switchModel))
                return;

            Transform prefabModel = FindDefaultPrefabTemplate(
                SwitchUiObjectName + "/" + SwitchModelObjectName
            );
            if (prefabModel != null)
            {
                CopyMissingSwitchModelChild(
                    switchModel,
                    prefabModel,
                    SwitchBaseObjectName
                );
                CopyMissingSwitchModelChild(
                    switchModel,
                    prefabModel,
                    SwitchButtonObjectName
                );
                return;
            }

            GameObject modelPrefab = EzMirrorAssetResolver.LoadDefaultSwitchModel();
            if (modelPrefab == null)
                return;

            GameObject sourceRoot = (GameObject)PrefabUtility.InstantiatePrefab(modelPrefab);
            if (sourceRoot == null)
                return;

            try
            {
                sourceRoot.name = SwitchModelObjectName;
                UnpackDefaultSwitchModelPrefabRootIfNeeded(sourceRoot.transform);
                RenameSwitchModelChildren(sourceRoot.transform);
                CopyMissingSwitchModelChild(
                    switchModel,
                    sourceRoot.transform,
                    SwitchBaseObjectName
                );
                CopyMissingSwitchModelChild(
                    switchModel,
                    sourceRoot.transform,
                    SwitchButtonObjectName
                );
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(sourceRoot);
            }
        }

        private static bool HasRequiredSwitchModelChildren(Transform switchModel)
        {
            return
                switchModel.Find(SwitchBaseObjectName) != null
                && switchModel.Find(SwitchButtonObjectName) != null;
        }

        private static void CopyMissingSwitchModelChild(
            Transform switchModel,
            Transform sourceRoot,
            string childName
        )
        {
            if (switchModel.Find(childName) != null)
                return;

            Transform sourceChild = sourceRoot.Find(childName);
            if (sourceChild == null)
                return;

            GameObject restoredChild = UnityEngine.Object.Instantiate(
                sourceChild.gameObject,
                switchModel,
                false
            );
            restoredChild.name = childName;
            restoredChild.transform.localPosition = sourceChild.localPosition;
            restoredChild.transform.localRotation = sourceChild.localRotation;
            restoredChild.transform.localScale = sourceChild.localScale;
            Undo.RegisterCreatedObjectUndo(restoredChild, "Restore EzMirror Switch Model Child");
            EditorUtility.SetDirty(restoredChild);
        }

        private static void RenameChildIfExists(Transform parent, string fromName, string toName)
        {
            Transform child = parent.Find(fromName);
            if (child == null)
                return;

            Undo.RecordObject(child.gameObject, "Rename EzMirror Switch Model Child");
            child.gameObject.name = toName;
        }

        private static void SetChildSiblingIndexIfExists(
            Transform parent,
            string childName,
            int siblingIndex
        )
        {
            Transform child = parent.Find(childName);
            if (child == null)
                return;

            Undo.RecordObject(child, "Reorder EzMirror Switch Model Child");
            child.SetSiblingIndex(siblingIndex);
        }

        private static void ResetSwitchAnimatorParameterNamesIfEmpty(EzMirror mirror)
        {
            if (string.IsNullOrEmpty(mirror.switchAnimatorProfileIndexParameter))
            {
                mirror.switchAnimatorProfileIndexParameter =
                    EzMirror.DefaultSwitchAnimatorProfileIndexParameter;
            }
            if (string.IsNullOrEmpty(mirror.switchAnimatorStateParameter))
                mirror.switchAnimatorStateParameter = EzMirror.DefaultSwitchAnimatorStateParameter;
            if (string.IsNullOrEmpty(mirror.switchAnimatorPressParameter))
                mirror.switchAnimatorPressParameter = EzMirror.DefaultSwitchAnimatorPressParameter;
        }

        private static void ValidateSwitchAnimatorParameters(EzMirror mirror)
        {
            AnimatorController controller = GetAnimatorController(mirror.switchAnimator);
            mirror.switchAnimatorCanSetProfileIndex = HasAnimatorParameter(
                controller,
                mirror.switchAnimatorProfileIndexParameter,
                AnimatorControllerParameterType.Int
            );
            mirror.switchAnimatorCanSetState = HasAnimatorParameter(
                controller,
                mirror.switchAnimatorStateParameter,
                AnimatorControllerParameterType.Int
            );
            mirror.switchAnimatorCanSetPress = HasAnimatorParameter(
                controller,
                mirror.switchAnimatorPressParameter,
                AnimatorControllerParameterType.Trigger
            );
        }

        private static AnimatorController GetAnimatorController(Animator animator)
        {
            if (animator == null || animator.runtimeAnimatorController == null)
                return null;

            return animator.runtimeAnimatorController as AnimatorController;
        }

        private static bool HasAnimatorParameter(
            AnimatorController controller,
            string parameterName,
            AnimatorControllerParameterType parameterType
        )
        {
            if (controller == null || string.IsNullOrEmpty(parameterName))
                return false;

            AnimatorControllerParameter[] parameters = controller.parameters;
            for (int i = 0; i < parameters.Length; i++)
            {
                AnimatorControllerParameter parameter = parameters[i];
                if (parameter.name == parameterName && parameter.type == parameterType)
                    return true;
            }
            return false;
        }

        private static void AssignSwitchModelMaterials(Transform switchModel)
        {
            if (switchModel == null)
                return;

            Material baseMaterial = EzMirrorAssetResolver.LoadDefaultSwitchBaseMaterial();
            Material lampMaterial = EzMirrorAssetResolver.LoadDefaultSwitchLampMaterial();
            Material buttonMaterial = EzMirrorAssetResolver.LoadDefaultSwitchButtonMaterial();
            Renderer[] renderers = switchModel.GetComponentsInChildren<Renderer>(true);
            for (int i = 0; i < renderers.Length; i++)
            {
                Renderer renderer = renderers[i];
                if (renderer == null)
                    continue;

                Material[] materials = BuildSwitchModelMaterials(
                    renderer,
                    baseMaterial,
                    lampMaterial,
                    buttonMaterial
                );
                Material[] mergedMaterials = MergeMissingSwitchModelMaterials(
                    renderer.sharedMaterials,
                    materials
                );
                if (mergedMaterials == null || MaterialsEqual(renderer.sharedMaterials, mergedMaterials))
                    continue;

                Undo.RecordObject(renderer, "Assign EzMirror Switch Material");
                renderer.sharedMaterials = mergedMaterials;
                EditorUtility.SetDirty(renderer);
                PrefabUtility.RecordPrefabInstancePropertyModifications(renderer);
            }
        }

        private static Material[] BuildSwitchModelMaterials(
            Renderer renderer,
            Material baseMaterial,
            Material lampMaterial,
            Material buttonMaterial
        )
        {
            if (renderer == null)
                return null;
            if (renderer.gameObject.name == SwitchBaseObjectName)
                return baseMaterial == null ? null : new[] { baseMaterial };
            if (renderer.gameObject.name == SwitchButtonObjectName)
            {
                if (lampMaterial != null && buttonMaterial != null)
                    return new[] { lampMaterial, buttonMaterial };
                if (buttonMaterial != null)
                    return new[] { buttonMaterial };
                if (lampMaterial != null)
                    return new[] { lampMaterial };
            }
            return null;
        }

        private static Material[] MergeMissingSwitchModelMaterials(
            Material[] current,
            Material[] expected
        )
        {
            if (expected == null || expected.Length == 0)
                return null;

            if (current == null || current.Length == 0)
                return expected;

            Material[] merged = new Material[current.Length];
            bool changed = false;
            for (int i = 0; i < current.Length; i++)
            {
                Material currentMaterial = current[i];
                if (currentMaterial != null || i >= expected.Length)
                {
                    merged[i] = currentMaterial;
                    continue;
                }

                merged[i] = expected[i];
                changed = merged[i] != null;
            }

            return changed ? merged : null;
        }

        private static bool MaterialsEqual(Material[] current, Material[] expected)
        {
            if (current == null || expected == null || current.Length != expected.Length)
                return false;
            for (int i = 0; i < current.Length; i++)
            {
                if (current[i] != expected[i])
                    return false;
            }
            return true;
        }

        private static Transform EnsureSwitchUiRoot(
            EzMirror mirror,
            out Vector3 inputLocalScale,
            out bool migratedLegacyInput
        )
        {
            inputLocalScale = DefaultInputProxyLocalScale;
            migratedLegacyInput = false;
            bool removeLegacyComponentsFromSwitchUi = false;
            bool normalizeSwitchUiTransform = false;
            Transform directSwitchUi = mirror.transform.Find(SwitchUiObjectName);
            Transform directLegacyInput = mirror.transform.Find(LegacyInputProxyObjectName);
            GameObject switchUiObject = directSwitchUi == null ? null : directSwitchUi.gameObject;
            GameObject legacyInputObject = directLegacyInput == null ? null : directLegacyInput.gameObject;

            if (switchUiObject == null && legacyInputObject != null)
            {
                switchUiObject = legacyInputObject;
                inputLocalScale = legacyInputObject.transform.localScale;
                migratedLegacyInput = true;
                removeLegacyComponentsFromSwitchUi = true;
                normalizeSwitchUiTransform = true;
                Undo.RecordObject(switchUiObject, "Migrate EzMirror SwitchInput");
                switchUiObject.name = SwitchUiObjectName;
            }
            else if (switchUiObject != null && legacyInputObject != null)
            {
                inputLocalScale = legacyInputObject.transform.localScale;
                migratedLegacyInput = true;
                normalizeSwitchUiTransform = true;
                Undo.RecordObject(switchUiObject.transform, "Migrate EzMirror SwitchInput Transform");
                switchUiObject.transform.localPosition = legacyInputObject.transform.localPosition;
                switchUiObject.transform.localRotation = legacyInputObject.transform.localRotation;
                Undo.DestroyObjectImmediate(legacyInputObject);
            }

            if (switchUiObject == null)
            {
                switchUiObject = new GameObject(SwitchUiObjectName);
                Undo.RegisterCreatedObjectUndo(switchUiObject, "Create EzMirror SwitchUI");
                switchUiObject.transform.SetParent(mirror.transform, false);
                Transform template = FindDefaultPrefabTemplate(SwitchUiObjectName);
                if (template == null)
                {
                    switchUiObject.transform.localPosition = DefaultInputProxyLocalPosition;
                    switchUiObject.transform.localRotation = Quaternion.identity;
                    normalizeSwitchUiTransform = true;
                }
                else
                {
                    CopyLocalTransform(template, switchUiObject.transform);
                }
            }

            if (removeLegacyComponentsFromSwitchUi)
                RemoveComponentsExceptTransform(switchUiObject);
            if (normalizeSwitchUiTransform)
                switchUiObject.transform.localScale = Vector3.one;
            return switchUiObject.transform;
        }

        private static Transform EnsureSwitchCanvas(EzMirror mirror, Transform switchUi)
        {
            Transform canvas = switchUi.Find("UI_Canvas");
            Transform directRootCanvas = mirror.transform.Find("UI_Canvas");
            GameObject rootCanvas = directRootCanvas == null ? null : directRootCanvas.gameObject;
            if (canvas == null && rootCanvas != null)
            {
                Vector3 localScale = rootCanvas.transform.localScale;
                Undo.SetTransformParent(rootCanvas.transform, switchUi, "Move EzMirror Canvas");
                canvas = rootCanvas.transform;
                AlignMovedCanvasToSwitchUi(canvas, localScale);
            }
            else if (canvas != null && rootCanvas != null && rootCanvas.transform.parent == mirror.transform)
            {
                Vector3 localScale = rootCanvas.transform.localScale;
                Undo.DestroyObjectImmediate(canvas.gameObject);
                Undo.SetTransformParent(rootCanvas.transform, switchUi, "Move EzMirror Canvas");
                canvas = rootCanvas.transform;
                AlignMovedCanvasToSwitchUi(canvas, localScale);
            }

            if (canvas == null)
            {
                Transform template = FindDefaultPrefabTemplate(SwitchUiObjectName + "/UI_Canvas");
                if (template == null)
                    return null;

                GameObject canvasObject = Instantiate(template.gameObject);
                canvasObject.name = template.name;
                Undo.RegisterCreatedObjectUndo(canvasObject, "Create EzMirror Canvas");
                canvasObject.transform.SetParent(switchUi, false);
                CopyLocalTransform(template, canvasObject.transform);
                canvas = canvasObject.transform;
            }

            return canvas;
        }

        private static void AlignMovedCanvasToSwitchUi(Transform canvas, Vector3 localScale)
        {
            canvas.localPosition = Vector3.zero;
            canvas.localRotation = Quaternion.identity;
            canvas.localScale = localScale;
        }

        private static Transform EnsureSwitchInputChild(
            Transform switchUi,
            string childName,
            Vector3 inputLocalScale,
            bool resetExistingTransform,
            bool useTemplateTransform
        )
        {
            Transform child = switchUi.Find(childName);
            if (child == null)
            {
                GameObject childObject = new GameObject(childName);
                Undo.RegisterCreatedObjectUndo(childObject, "Create EzMirror Input");
                childObject.transform.SetParent(switchUi, false);
                child = childObject.transform;
                resetExistingTransform = true;
            }

            if (resetExistingTransform)
            {
                Transform template = useTemplateTransform
                    ? FindDefaultPrefabTemplate(SwitchUiObjectName + "/" + childName)
                    : null;
                if (template == null)
                {
                    child.localPosition = Vector3.zero;
                    child.localRotation = Quaternion.identity;
                    child.localScale = inputLocalScale;
                }
                else
                {
                    CopyLocalTransform(template, child);
                }
            }
            return child;
        }

        private static void RemoveComponentsExceptTransform(GameObject target)
        {
            Component[] components = target.GetComponents<Component>();
            for (int i = components.Length - 1; i >= 0; i--)
            {
                Component component = components[i];
                if (component == null || component is Transform)
                    continue;
                if (component is UdonSharp.UdonSharpBehaviour proxy)
                {
                    UdonSharpUndo.DestroyImmediate(proxy);
                    continue;
                }

                Undo.DestroyObjectImmediate(component);
            }
        }

        private static void ConfigureContactReceiver(VRCContactReceiver receiver, Vector3 boxSize)
        {
            if (receiver == null)
                return;

            SerializedObject serializedReceiver = new SerializedObject(receiver);
            SetSerializedEnumIndex(
                serializedReceiver,
                "shapeType",
                (int)ContactBase.ShapeType.Box
            );
            SetSerializedVector3(serializedReceiver, "size", boxSize);
            SetSerializedBool(serializedReceiver, "useFaceProximity", false);
            SetSerializedBool(serializedReceiver, "allowSelf", true);
            SetSerializedBool(serializedReceiver, "allowOthers", false);
            SetSerializedBool(serializedReceiver, "localOnly", true);
            SetSerializedFloat(serializedReceiver, "radius", 0.08f);
            SetSerializedFloat(serializedReceiver, "height", 0.2f);
            SetSerializedFloat(serializedReceiver, "minVelocity", 0f);
            SetSerializedEnum(
                serializedReceiver,
                "contentTypes",
                nameof(DynamicsUsageFlags.Avatar)
            );
            SetSerializedStringArray(
                serializedReceiver,
                "collisionTags",
                HandContactCollisionTags
            );
            SetSerializedEnum(serializedReceiver, "receiverType", "OnEnter");
            serializedReceiver.ApplyModifiedPropertiesWithoutUndo();
        }

        private static bool HasContactCollisionTags(VRCContactReceiver receiver)
        {
            if (receiver == null)
                return false;

            SerializedObject serializedReceiver = new SerializedObject(receiver);
            SerializedProperty collisionTags = serializedReceiver.FindProperty("collisionTags");
            return collisionTags != null && collisionTags.isArray && collisionTags.arraySize > 0;
        }

        private static void EnsureDefaultContactCollisionTags(VRCContactReceiver receiver)
        {
            if (receiver == null)
                return;

            SerializedObject serializedReceiver = new SerializedObject(receiver);
            SetSerializedStringArray(
                serializedReceiver,
                "collisionTags",
                HandContactCollisionTags
            );
            serializedReceiver.ApplyModifiedPropertiesWithoutUndo();
        }

        private static void SetSerializedBool(SerializedObject serializedObject, string propertyName, bool value)
        {
            SerializedProperty property = serializedObject.FindProperty(propertyName);
            if (property != null)
                property.boolValue = value;
        }

        private static void SetSerializedFloat(SerializedObject serializedObject, string propertyName, float value)
        {
            SerializedProperty property = serializedObject.FindProperty(propertyName);
            if (property != null)
                property.floatValue = value;
        }

        private static void SetSerializedVector3(
            SerializedObject serializedObject,
            string propertyName,
            Vector3 value
        )
        {
            SerializedProperty property = serializedObject.FindProperty(propertyName);
            if (property != null)
                property.vector3Value = value;
        }

        private static void SetSerializedEnumIndex(
            SerializedObject serializedObject,
            string propertyName,
            int enumValueIndex
        )
        {
            SerializedProperty property = serializedObject.FindProperty(propertyName);
            if (property != null && property.propertyType == SerializedPropertyType.Enum)
                property.enumValueIndex = enumValueIndex;
        }

        private static void SetSerializedStringArray(
            SerializedObject serializedObject,
            string propertyName,
            string[] values
        )
        {
            SerializedProperty property = serializedObject.FindProperty(propertyName);
            if (property == null || !property.isArray)
                return;

            property.arraySize = values.Length;
            for (int i = 0; i < values.Length; i++)
                property.GetArrayElementAtIndex(i).stringValue = values[i];
        }

        private static void SetSerializedEnum(
            SerializedObject serializedObject,
            string propertyName,
            string enumName
        )
        {
            SerializedProperty property = serializedObject.FindProperty(propertyName);
            if (property == null || property.propertyType != SerializedPropertyType.Enum)
                return;

            string[] enumNames = property.enumNames;
            for (int i = 0; i < enumNames.Length; i++)
            {
                if (enumNames[i] != enumName)
                    continue;
                property.enumValueIndex = i;
                return;
            }
        }
    }
}
#endif
