namespace NiAkaDev.EzMirror
{
    using UdonSharp;
    using UnityEngine;
    using UnityEngine.UI;
    using VRC.SDK3.Components;
    using VRC.SDK3.Dynamics.Contact.Components;
    using VRC.SDK3.Persistence;
    using VRC.SDKBase;

    /// <summary>
    /// 単一 VRCMirrorReflection に複数 Profile を適用する EzMirror 本体。
    /// UI の OnValueChanged / SendCustomEvent から SelectNext / TurnOff / ResetToDefault を呼び出す。
    /// 他の UdonSharp から特定 Profile を選ぶ場合は SelectProfile(int profileIndex) を直接呼び出す。
    /// </summary>
    [UdonBehaviourSyncMode(BehaviourSyncMode.None)]
    public class EzMirror : UdonSharpBehaviour
    {
        private const string HideBackgroundPropertyName = "_HideBackground";
        private const string TransparencyPropertyName = "_Transparency";
        private const string SwitchAnimatorPath = "SwitchUI/Model_Switch";
        private const float TransparentProfileDefaultTransparency = 0.5f;
        private const float TriggerBoundsWorldEpsilon = 0.05f;
        private const float MinimumTriggerScale = 0.0001f;
        private const float InputToggleDebounceSeconds = 0.15f;
        public const string DefaultSwitchAnimatorProfileIndexParameter = "EzMirror_ProfileIndex";
        public const string DefaultSwitchAnimatorStateParameter = "EzMirror_State";
        public const string DefaultSwitchAnimatorPressParameter = "EzMirror_Press";
        public const int SwitchAnimatorStateOff = 0;
        public const int SwitchAnimatorStateReady = 1;
        public const int SwitchAnimatorStateVisible = 2;
        public const int HapticHandBoth = 0;
        public const int HapticHandLeft = 1;
        public const int HapticHandRight = 2;

        [Header("Local Save")]
        [Tooltip("true で同じユーザーが再入室した時に状態復元")]
        public bool usePersist;

        [Tooltip("PlayerData のキー名")]
        public string persistKey = "EZ_MIRROR_INDEX";

        [Header("Mirror")]
        [Tooltip("Profile を適用する VRCMirrorReflection")]
        public VRCMirrorReflection profileMirror;

        [Tooltip("Profile Material と表示制御を適用する Renderer")]
        public Renderer profileRenderer;

        [Tooltip("ローカルプレイヤーが入っている間だけ Mirror を表示する Trigger")]
        public BoxCollider triggerCollider;

        [Tooltip("Trigger Collider が別 GameObject にある構成でも表示を追従する再判定間隔")]
        public float triggerRefreshInterval = 0.25f;

        [Header("Profiles")]
        [Tooltip("Editor が自動生成した Profile ごとの Material。Profile 0 は Off 固定")]
        [HideInInspector]
        public Material[] profileMaterials;

        [Tooltip("Editor で自動生成する Profile Material の基盤。通常は変更不要")]
        [HideInInspector]
        public Material[] profileBaseMaterials;

        [Tooltip("プロファイルごとの表示名。空欄の場合は Editor 側で安全な表示名にフォールバック")]
        public string[] profileNames;

        [Tooltip("true の Profile を初期状態 / Reset 先として使う。複数 true なら先頭、未指定なら Off")]
        public bool[] profileIsDefault;

        [Tooltip("true の Profile は Trigger 内だけ Mirror を表示する。Profile 0 は常に false")]
        public bool[] profileMirrorEnabled;

        public bool[] profileDisablePixelLights;
        public bool[] profileTurnOffMirrorOcclusion;
        public LayerMask[] profileReflectLayers;
        public MirrorClearFlags[] profileCameraClearFlags;
        public Material[] profileCustomSkyboxes;
        public Color[] profileCustomClearColors;

        [Tooltip("true の Profile は Material の _HideBackground を 1 にする")]
        public bool[] profileHideBackground;

        [Tooltip("Profile ごとの Material _Transparency。0=不透明、1=完全透明")]
        public float[] profileTransparency;

        [Tooltip("true の Profile は PCVR/Desktop 以外では表示しない。Profile 0 は常に対応")]
        public bool[] profilePcOnly;

        [Header("Profile Objects")]
        [Tooltip("この Profile で ON にするオブジェクト")]
        public GameObject[] profileActiveObjects;

        [Tooltip("profileActiveObjects と同じ長さ。Profile index (0..N-1) を指定")]
        public int[] profileActiveObjectModes;

        [Tooltip("この Profile で OFF にするオブジェクト")]
        public GameObject[] profileInactiveObjects;

        [Tooltip("profileInactiveObjects と同じ長さ。Profile index (0..N-1) を指定")]
        public int[] profileInactiveObjectModes;

        [Header("Icons")]
        public Image iconImage;
        public Sprite[] profileIconSprites;
        public Color[] profileIconColors;

        [HideInInspector]
        public GameObject[] profileIconObjects;

        [Header("Input")]
        [Tooltip("uGUI の Toggle をレーザーで押して切り替える")]
        public bool enableUguiInput = true;

        [Tooltip("InteractInput を Use して切り替える")]
        public bool enableInteractInput;

        [Tooltip("ContactInput に手で触れた時に切り替える")]
        public bool enableContactInput;

        [Tooltip("Contact を本体へ中継する入力プロキシ")]
        public EzMirrorInputProxy inputProxy;

        [Tooltip("Interact を本体へ中継する入力プロキシ")]
        public EzMirrorInteractInputProxy interactInputProxy;

        [Tooltip("Interact 用の当たり判定")]
        public BoxCollider inputHitboxCollider;

        [Tooltip("手の接触で切り替える Contact Receiver")]
        public VRCContactReceiver contactReceiver;

        [Tooltip("Contact 入力の連続切替を防ぐ秒数")]
        public float contactCooldownSeconds = 0.5f;

        [Header("UI / SFX")]
        public Toggle uiToggle;
        public bool enableClickSound = true;
        public AudioClip clickClip;
        public AudioSource sfxAudioSource;

        [Header("Haptic")]
        [Tooltip("切り替えた本人のコントローラーを短く振動させる")]
        public bool enableHapticFeedback = true;

        [Tooltip("振動時間（秒）")]
        public float hapticDuration = 0.08f;

        [Tooltip("振動の強さ")]
        public float hapticAmplitude = 0.35f;

        [Tooltip("振動の周波数")]
        public float hapticFrequency = 0.5f;

        [Header("Switch Animator")]
        [Tooltip("3Dスイッチの見た目をAnimatorで同期する")]
        public bool useSwitchAnimator = true;

        [Tooltip("3Dスイッチを制御するAnimator")]
        public Animator switchAnimator;

        [Tooltip("現在のプロファイル番号を渡すIntパラメーター")]
        public string switchAnimatorProfileIndexParameter = DefaultSwitchAnimatorProfileIndexParameter;

        [Tooltip("Off / 待機 / 表示中を渡すIntパラメーター")]
        public string switchAnimatorStateParameter = DefaultSwitchAnimatorStateParameter;

        [Tooltip("入力を受け付けた時に押すTriggerパラメーター")]
        public string switchAnimatorPressParameter = DefaultSwitchAnimatorPressParameter;

        [HideInInspector]
        public bool switchAnimatorCanSetProfileIndex = true;

        [HideInInspector]
        public bool switchAnimatorCanSetState = true;

        [HideInInspector]
        public bool switchAnimatorCanSetPress = true;

        private int localIndex;
        private int _platform = -1;
        private bool _isLocalInsideTrigger = true;
        private bool _triggerRefreshLoopActive;
        private VRCTweenHandle _triggerRefreshTimer;
        private Material _defaultMaterial;
        private GameObject[] _profileInactiveInitialObjects;
        private bool[] _profileInactiveInitialActiveStates;
        private float _lastInputToggleTime = -1000f;

        public void Start()
        {
            ResolveReferences();
            DetectPlatform();
            CaptureProfileInactiveInitialStates();

            if (!HasProfileTarget())
            {
                Debug.LogWarning("[EzMirror] Mirror target is missing.");
                enabled = false;
                return;
            }

            RefreshTriggerStateFromLocalPlayer();
            StartTriggerRefreshLoopIfNeeded();

            RestorePersistDataAtStart();
        }

        public override void OnPlayerRestored(VRCPlayerApi player)
        {
            if (!IsLocalValidPlayer(player))
                return;
            RestorePersistDataAtStart();
        }

        public override void OnPlayerTriggerEnter(VRCPlayerApi player)
        {
            if (!IsLocalValidPlayer(player))
                return;
            RefreshLocalTriggerStateAndApply();
        }

        public override void OnPlayerTriggerExit(VRCPlayerApi player)
        {
            if (!IsLocalValidPlayer(player))
                return;
            RefreshLocalTriggerStateAndApply();
        }

        public void RefreshTriggerStateFromLocalPlayer()
        {
            RefreshLocalTriggerStateAndApply();
        }

        private void RefreshLocalTriggerStateAndApply()
        {
            bool previous = _isLocalInsideTrigger;
            UpdateLocalTriggerState();
            if (previous != _isLocalInsideTrigger)
                ApplyIndex(GetCurrentSourceIndex());
        }

        public void RefreshTriggerStateLoop()
        {
            _triggerRefreshTimer.Kill();
            if (!enabled || triggerCollider == null)
            {
                _triggerRefreshLoopActive = false;
                return;
            }

            RefreshTriggerStateFromLocalPlayer();
            ScheduleTriggerRefreshLoop();
        }

        public void OnDisable()
        {
            StopTriggerRefreshLoop();
        }

        public void OnDestroy()
        {
            StopTriggerRefreshLoop();
        }

        public void SelectNext()
        {
            SelectNextWithFeedback(HapticHandBoth);
        }

        public void SelectNextFromUgui()
        {
            if (!enableUguiInput)
            {
                ApplyToggleState(GetCurrentSourceIndex());
                return;
            }
            if (!TryReserveInputToggle())
            {
                ApplyToggleState(GetCurrentSourceIndex());
                return;
            }
            PulseSwitchAnimatorPress();
            SelectNextWithFeedback(HapticHandBoth);
        }

        public void SelectNextFromInteract()
        {
            if (!enableInteractInput)
                return;
            if (!TryReserveInputToggle())
                return;
            PulseSwitchAnimatorPress();
            SelectNextWithFeedback(HapticHandBoth);
        }

        public void SelectNextFromContact(int handHint)
        {
            if (!enableContactInput)
                return;
            if (!TryReserveInputToggle())
                return;
            PulseSwitchAnimatorPress();
            SelectNextWithFeedback(ClampHapticHandHint(handHint));
        }

        public void OnToggleAccessed()
        {
            SelectNextFromUgui();
        }

        public void TurnOff()
        {
            SelectIndex(0);
        }

        public void SelectProfile(int profileIndex)
        {
            SelectIndex(profileIndex);
        }

        // 互換維持用。新規の外部 UdonSharp 連携では SelectProfile(int profileIndex) を使う。
        public void SelectProfile1()
        {
            SelectProfile(1);
        }

        public void SelectProfile2()
        {
            SelectProfile(2);
        }

        public void SelectProfile3()
        {
            SelectProfile(3);
        }

        public void SelectProfile4()
        {
            SelectProfile(4);
        }

        public void SelectProfile5()
        {
            SelectProfile(5);
        }

        public void SelectProfile6()
        {
            SelectProfile(6);
        }

        public void SelectProfile7()
        {
            SelectProfile(7);
        }

        public void SelectProfile8()
        {
            SelectProfile(8);
        }

        public void ResetToDefault()
        {
            SelectIndex(GetDefaultProfileIndex(0));
        }

        private void SelectIndex(int requestedIndex)
        {
            SelectIndexWithFeedback(requestedIndex, HapticHandBoth);
        }

        private void SelectNextWithFeedback(int handHint)
        {
            SelectIndexWithFeedback(
                GetNextValidIndex(GetCurrentSourceIndex()),
                handHint
            );
        }

        private bool TryReserveInputToggle()
        {
            float now = Time.time;
            if (now - _lastInputToggleTime < InputToggleDebounceSeconds)
                return false;

            _lastInputToggleTime = now;
            return true;
        }

        private void SelectIndexWithFeedback(
            int requestedIndex,
            int handHint
        )
        {
            if (!enabled)
                return;

            ResolveReferences();
            CaptureProfileInactiveInitialStates();
            RefreshTriggerStateFromLocalPlayer();
            int clamped = ClampIndex(requestedIndex);
            if (!IsProfileIndexSupported(clamped))
                clamped = GetSupportedDefaultProfileIndex();

            localIndex = clamped;
            ApplyIndex(localIndex);
            SavePersistData();

            PlayClickSound();
            PlayHapticFeedback(handHint);
        }

        private void RestorePersistDataAtStart()
        {
            int initialIndex = GetDefaultProfileIndex(0);
            VRCPlayerApi localPlayer = Networking.LocalPlayer;
            if (
                usePersist
                && localPlayer != null
                && localPlayer.IsValid()
                && IsPersistKeyValid()
                && PlayerData.HasKey(localPlayer, persistKey)
            )
            {
                initialIndex = PlayerData.GetInt(localPlayer, persistKey);
            }

            localIndex = ClampIndex(initialIndex);
            if (!IsProfileIndexSupported(localIndex))
                localIndex = GetSupportedDefaultProfileIndex();
            ApplyIndex(localIndex);
        }

        private void ApplyIndex(int modeIndex)
        {
            int clamped = ClampIndex(modeIndex);
            int effectiveIndex = IsProfileIndexSupported(clamped)
                ? clamped
                : GetSupportedDefaultProfileIndex();
            bool isProfileDisplayActive = IsProfileDisplayActive(effectiveIndex);
            bool isProfileEnabled = IsProfileMirrorEnabled(effectiveIndex);
            bool isMirrorVisible = isProfileEnabled && _isLocalInsideTrigger;

            ApplyIconState(effectiveIndex);
            ApplyToggleState(effectiveIndex);
            ApplyProfileSettings(effectiveIndex);
            ApplyProfileObjects(effectiveIndex, isProfileDisplayActive);
            SetMirrorVisible(isMirrorVisible);
            ApplySwitchAnimatorState(effectiveIndex, isProfileEnabled, isMirrorVisible);
        }

        private void ApplySwitchAnimatorState(
            int profileIndex,
            bool isProfileEnabled,
            bool isMirrorVisible
        )
        {
            if (!CanUseSwitchAnimator())
                return;

            if (switchAnimatorCanSetProfileIndex && IsAnimatorParameterNameValid(switchAnimatorProfileIndexParameter))
                switchAnimator.SetInteger(switchAnimatorProfileIndexParameter, profileIndex);

            if (switchAnimatorCanSetState && IsAnimatorParameterNameValid(switchAnimatorStateParameter))
                switchAnimator.SetInteger(
                    switchAnimatorStateParameter,
                    GetSwitchAnimatorState(isProfileEnabled, isMirrorVisible)
                );
        }

        private int GetSwitchAnimatorState(bool isProfileEnabled, bool isMirrorVisible)
        {
            if (!isProfileEnabled)
                return SwitchAnimatorStateOff;
            return isMirrorVisible ? SwitchAnimatorStateVisible : SwitchAnimatorStateReady;
        }

        private void PulseSwitchAnimatorPress()
        {
            if (
                !CanUseSwitchAnimator()
                || !switchAnimatorCanSetPress
                || !IsAnimatorParameterNameValid(switchAnimatorPressParameter)
            )
            {
                return;
            }

            switchAnimator.ResetTrigger(switchAnimatorPressParameter);
            switchAnimator.SetTrigger(switchAnimatorPressParameter);
        }

        private bool CanUseSwitchAnimator()
        {
            return useSwitchAnimator && switchAnimator != null;
        }

        private bool IsAnimatorParameterNameValid(string parameterName)
        {
            return parameterName != null && parameterName.Length > 0;
        }

        private void ApplyProfileSettings(int profileIndex)
        {
            if (profileMirror != null)
            {
                if (profileDisablePixelLights != null && profileIndex < profileDisablePixelLights.Length)
                    profileMirror.m_DisablePixelLights = profileDisablePixelLights[profileIndex];
                if (
                    profileTurnOffMirrorOcclusion != null
                    && profileIndex < profileTurnOffMirrorOcclusion.Length
                )
                {
                    profileMirror.TurnOffMirrorOcclusion =
                        profileTurnOffMirrorOcclusion[profileIndex];
                }
                if (profileReflectLayers != null && profileIndex < profileReflectLayers.Length)
                    profileMirror.m_ReflectLayers = profileReflectLayers[profileIndex];
                if (profileCameraClearFlags != null && profileIndex < profileCameraClearFlags.Length)
                    profileMirror.cameraClearFlags = profileCameraClearFlags[profileIndex];
                if (profileCustomSkyboxes != null && profileIndex < profileCustomSkyboxes.Length)
                    profileMirror.customSkybox = profileCustomSkyboxes[profileIndex];
                if (
                    profileCustomClearColors != null
                    && profileIndex < profileCustomClearColors.Length
                )
                {
                    profileMirror.customClearColor = profileCustomClearColors[profileIndex];
                }
            }

            Material material = null;
            if (profileMaterials != null && profileIndex < profileMaterials.Length)
                material = profileMaterials[profileIndex];
            if (material == null && profileIndex > 0)
                material = _defaultMaterial;

            if (profileRenderer != null && material != null && profileRenderer.sharedMaterial != material)
                profileRenderer.sharedMaterial = material;

            ApplyMaterialProfileProperties(material, profileIndex);
        }

        private void ApplyMaterialProfileProperties(Material material, int profileIndex)
        {
            if (material == null)
                return;

            ApplyMaterialHideBackground(material, profileIndex);
            ApplyMaterialTransparency(material, profileIndex);
        }

        private void ApplyMaterialHideBackground(Material material, int profileIndex)
        {
            if (!material.HasProperty(HideBackgroundPropertyName))
                return;

            float value = IsProfileHideBackground(profileIndex) ? 1f : 0f;
            if (material.GetFloat(HideBackgroundPropertyName) != value)
                material.SetFloat(HideBackgroundPropertyName, value);
        }

        private void ApplyMaterialTransparency(Material material, int profileIndex)
        {
            if (!material.HasProperty(TransparencyPropertyName))
                return;

            float value = GetProfileTransparency(profileIndex);
            if (material.GetFloat(TransparencyPropertyName) != value)
                material.SetFloat(TransparencyPropertyName, value);
        }

        private void ApplyProfileObjects(int profileIndex, bool isProfileDisplayActive)
        {
            if (profileActiveObjects != null)
            {
                for (int i = 0; i < profileActiveObjects.Length; i++)
                {
                    GameObject target = profileActiveObjects[i];
                    if (target == null)
                        continue;

                    int targetMode = GetProfileActiveObjectMode(i);
                    bool active = targetMode == profileIndex && isProfileDisplayActive;
                    if (target.activeSelf != active)
                        target.SetActive(active);
                }
            }

            if (profileInactiveObjects == null)
                return;

            CaptureProfileInactiveInitialStates();
            for (int i = 0; i < profileInactiveObjects.Length; i++)
            {
                GameObject target = profileInactiveObjects[i];
                if (target == null)
                    continue;
                if (IsProfileActiveObjectForCurrentProfile(target, profileIndex, isProfileDisplayActive))
                    continue;

                int targetMode = GetProfileInactiveObjectMode(i);
                bool initialActive = GetProfileInactiveInitialActive(i, target);
                bool active = targetMode == profileIndex && isProfileDisplayActive
                    ? false
                    : initialActive;
                if (target.activeSelf != active)
                    target.SetActive(active);
            }
        }

        private bool IsProfileActiveObjectForCurrentProfile(
            GameObject target,
            int profileIndex,
            bool isProfileDisplayActive
        )
        {
            if (!isProfileDisplayActive || target == null || profileActiveObjects == null)
                return false;

            for (int i = 0; i < profileActiveObjects.Length; i++)
            {
                if (
                    profileActiveObjects[i] == target
                    && GetProfileActiveObjectMode(i) == profileIndex
                )
                {
                    return true;
                }
            }

            return false;
        }

        private int GetProfileActiveObjectMode(int index)
        {
            if (
                profileActiveObjectModes == null
                || index < 0
                || index >= profileActiveObjectModes.Length
            )
            {
                return 0;
            }

            return profileActiveObjectModes[index];
        }

        private int GetProfileInactiveObjectMode(int index)
        {
            if (
                profileInactiveObjectModes == null
                || index < 0
                || index >= profileInactiveObjectModes.Length
            )
            {
                return 0;
            }

            return profileInactiveObjectModes[index];
        }

        private void CaptureProfileInactiveInitialStates()
        {
            int count = profileInactiveObjects == null ? 0 : profileInactiveObjects.Length;
            if (
                _profileInactiveInitialActiveStates != null
                && _profileInactiveInitialObjects != null
                && _profileInactiveInitialActiveStates.Length == count
                && _profileInactiveInitialObjects.Length == count
            )
            {
                return;
            }

            _profileInactiveInitialObjects = new GameObject[count];
            _profileInactiveInitialActiveStates = new bool[count];
            for (int i = 0; i < count; i++)
            {
                GameObject target = profileInactiveObjects[i];
                _profileInactiveInitialObjects[i] = target;
                _profileInactiveInitialActiveStates[i] = target != null && target.activeSelf;
            }
        }

        private bool GetProfileInactiveInitialActive(int index, GameObject target)
        {
            if (
                _profileInactiveInitialActiveStates == null
                || _profileInactiveInitialObjects == null
                || index < 0
                || index >= _profileInactiveInitialActiveStates.Length
                || index >= _profileInactiveInitialObjects.Length
            )
            {
                return target != null && target.activeSelf;
            }

            if (_profileInactiveInitialObjects[index] != target)
            {
                _profileInactiveInitialObjects[index] = target;
                _profileInactiveInitialActiveStates[index] = target != null && target.activeSelf;
            }

            return _profileInactiveInitialActiveStates[index];
        }

        private int GetNextValidIndex(int current)
        {
            int profileCount = GetProfileCount();
            if (profileCount <= 0)
                return 0;

            int next = ClampIndex(current);
            for (int i = 0; i < profileCount; i++)
            {
                next = (next + 1) % profileCount;
                if (IsProfileIndexSupported(next))
                    return next;
            }

            return 0;
        }

        private int ClampIndex(int index)
        {
            int profileCount = GetProfileCount();
            if (profileCount <= 0 || index < 0)
                return 0;
            if (index >= profileCount)
                return profileCount - 1;
            return index;
        }

        private bool IsProfileIndexSupported(int modeIndex)
        {
            if (modeIndex == 0)
                return true;
            if (modeIndex < 0 || modeIndex >= GetProfileCount())
                return false;
            if (IsProfilePcOnly(modeIndex) && !IsPcPlatform())
                return false;
            return true;
        }

        private bool IsProfilePcOnly(int profileIndex)
        {
            if (profileIndex <= 0)
                return false;
            return profilePcOnly != null
                && profileIndex < profilePcOnly.Length
                && profilePcOnly[profileIndex];
        }

        private bool IsPcPlatform()
        {
            return _platform == 0 || _platform == 1;
        }

        private bool IsProfileMirrorEnabled(int profileIndex)
        {
            if (profileIndex <= 0)
                return false;
            if (profileMirrorEnabled == null || profileIndex >= profileMirrorEnabled.Length)
                return true;
            return profileMirrorEnabled[profileIndex];
        }

        private bool IsProfileDisplayActive(int profileIndex)
        {
            if (IsProfileMirrorEnabled(profileIndex))
                return _isLocalInsideTrigger;
            return true;
        }

        private bool IsProfileHideBackground(int profileIndex)
        {
            return profileHideBackground != null
                && profileIndex >= 0
                && profileIndex < profileHideBackground.Length
                && profileHideBackground[profileIndex];
        }

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

            return profileIndex == 2 ? TransparentProfileDefaultTransparency : 0f;
        }

        private int GetDefaultProfileIndex(int unused)
        {
            int profileCount = GetProfileCount();
            if (profileIsDefault != null)
            {
                for (int i = 0; i < profileIsDefault.Length && i < profileCount; i++)
                {
                    if (profileIsDefault[i])
                        return i;
                }
            }

            return 0;
        }

        private int GetSupportedDefaultProfileIndex()
        {
            int defaultIndex = ClampIndex(GetDefaultProfileIndex(0));
            if (IsProfileIndexSupported(defaultIndex))
                return defaultIndex;
            return 0;
        }

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

        private void ResolveReferences()
        {
            if (interactInputProxy == null)
                interactInputProxy = GetComponentInChildren<EzMirrorInteractInputProxy>(true);
            if (inputProxy == null)
                inputProxy = GetComponentInChildren<EzMirrorInputProxy>(true);
            if (inputHitboxCollider == null && interactInputProxy != null)
                inputHitboxCollider = (BoxCollider)interactInputProxy.GetComponent(typeof(BoxCollider));
            if (contactReceiver == null && inputProxy != null)
                contactReceiver = (VRCContactReceiver)inputProxy.GetComponent(typeof(VRCContactReceiver));
            if (contactReceiver == null)
                contactReceiver = (VRCContactReceiver)GetComponentInChildren(typeof(VRCContactReceiver), true);
            if (profileMirror == null)
                profileMirror = (VRCMirrorReflection)GetComponentInChildren(typeof(VRCMirrorReflection), true);
            if (profileRenderer == null && profileMirror != null)
                profileRenderer = (Renderer)profileMirror.GetComponent(typeof(Renderer));
            if (profileRenderer == null)
                profileRenderer = (Renderer)GetComponentInChildren(typeof(Renderer), true);
            if (triggerCollider == null || triggerCollider == inputHitboxCollider)
                triggerCollider = FindTriggerCollider();
            if (uiToggle == null)
                uiToggle = (Toggle)GetComponentInChildren(typeof(Toggle), true);
            if (switchAnimator == null && useSwitchAnimator)
                switchAnimator = FindSwitchAnimator();
            if (_defaultMaterial == null)
                _defaultMaterial = ResolveDefaultMaterial();
            ApplyInputComponentState();
        }

        private Animator FindSwitchAnimator()
        {
            Transform switchModel = transform.Find(SwitchAnimatorPath);
            if (switchModel == null)
                return null;

            return (Animator)switchModel.GetComponent(typeof(Animator));
        }

        private Material ResolveDefaultMaterial()
        {
            if (profileMaterials != null)
            {
                for (int i = 1; i < profileMaterials.Length; i++)
                {
                    if (profileMaterials[i] != null)
                        return profileMaterials[i];
                }
            }

            if (profileRenderer == null)
                return null;

            return profileRenderer.sharedMaterial;
        }

        private BoxCollider FindTriggerCollider()
        {
            BoxCollider[] colliders = GetComponentsInChildren<BoxCollider>(true);
            BoxCollider namedTrigger = FindNamedTriggerCollider(colliders);
            if (namedTrigger != null)
                return namedTrigger;

            for (int i = 0; i < colliders.Length; i++)
            {
                if (IsSelectableDisplayTriggerCollider(colliders[i], true))
                    return colliders[i];
            }

            for (int i = 0; i < colliders.Length; i++)
            {
                if (IsSelectableDisplayTriggerCollider(colliders[i], false))
                    return colliders[i];
            }

            return null;
        }

        private BoxCollider FindNamedTriggerCollider(BoxCollider[] colliders)
        {
            for (int i = 0; i < colliders.Length; i++)
            {
                BoxCollider collider = colliders[i];
                if (
                    IsSelectableDisplayTriggerCollider(collider, true)
                    && collider.gameObject.name == "Trigger"
                )
                {
                    return collider;
                }
            }

            return null;
        }

        private bool IsSelectableDisplayTriggerCollider(
            BoxCollider collider,
            bool requireTrigger
        )
        {
            if (collider == null)
                return false;
            if (collider == inputHitboxCollider)
                return false;
            if (interactInputProxy != null && collider.gameObject == interactInputProxy.gameObject)
                return false;
            if (inputProxy != null && collider.gameObject == inputProxy.gameObject)
                return false;
            if (contactReceiver != null && collider.gameObject == contactReceiver.gameObject)
                return false;
            if (requireTrigger && !collider.isTrigger)
                return false;
            return true;
        }

        private void ApplyInputComponentState()
        {
            if (inputHitboxCollider != null)
            {
                inputHitboxCollider.enabled = enableInteractInput;
                SetInputObjectActive(inputHitboxCollider.gameObject, enableInteractInput);
            }
            if (contactReceiver != null)
            {
                contactReceiver.enabled = enableContactInput;
                SetInputObjectActive(contactReceiver.gameObject, enableContactInput);
            }
            SetCanvasInputColliderState();
        }

        private void SetInputObjectActive(GameObject target, bool active)
        {
            if (target == null)
                return;
            if (target.activeSelf != active)
                target.SetActive(active);
        }

        private void SetCanvasInputColliderState()
        {
            if (uiToggle == null)
                return;

            Canvas canvas = (Canvas)uiToggle.GetComponentInParent(typeof(Canvas));
            if (canvas == null)
                return;

            BoxCollider canvasCollider = (BoxCollider)canvas.GetComponent(typeof(BoxCollider));
            if (canvasCollider == null)
                return;

            canvasCollider.enabled = enableUguiInput;
        }

        private void StartTriggerRefreshLoopIfNeeded()
        {
            if (_triggerRefreshLoopActive || triggerCollider == null)
                return;
            _triggerRefreshLoopActive = true;
            ScheduleTriggerRefreshLoop();
        }

        private void ScheduleTriggerRefreshLoop()
        {
            _triggerRefreshTimer.Kill();
            _triggerRefreshTimer = VRCTween.DelayedCall(
                this,
                nameof(RefreshTriggerStateLoop),
                Mathf.Max(0.05f, triggerRefreshInterval)
            );
        }

        private void StopTriggerRefreshLoop()
        {
            _triggerRefreshTimer.Kill();
            _triggerRefreshLoopActive = false;
        }

        private void UpdateLocalTriggerState()
        {
            ResolveReferences();
            if (triggerCollider == null || !triggerCollider.enabled)
            {
                _isLocalInsideTrigger = true;
                return;
            }

            VRCPlayerApi localPlayer = Networking.LocalPlayer;
            if (localPlayer == null || !localPlayer.IsValid())
            {
                _isLocalInsideTrigger = true;
                return;
            }

            _isLocalInsideTrigger = IsLocalPlayerInsideTrigger(localPlayer);
        }

        private bool IsLocalPlayerInsideTrigger(VRCPlayerApi localPlayer)
        {
            Vector3 feetPosition = localPlayer.GetPosition();
            if (IsWorldPositionInsideTrigger(feetPosition))
                return true;

            VRCPlayerApi.TrackingData head = localPlayer.GetTrackingData(
                VRCPlayerApi.TrackingDataType.Head
            );
            Vector3 headPosition = head.position;
            if (IsWorldPositionInsideTrigger(headPosition))
                return true;

            return IsWorldPositionInsideTrigger((feetPosition + headPosition) * 0.5f);
        }

        private bool IsWorldPositionInsideTrigger(Vector3 worldPosition)
        {
            if (triggerCollider == null || !triggerCollider.enabled)
                return false;

            Vector3 local = triggerCollider.transform.InverseTransformPoint(worldPosition);
            Vector3 delta = local - triggerCollider.center;
            Vector3 half = triggerCollider.size * 0.5f;
            Vector3 epsilon = GetLocalTriggerBoundsEpsilon();
            return Mathf.Abs(delta.x) <= half.x + epsilon.x
                && Mathf.Abs(delta.y) <= half.y + epsilon.y
                && Mathf.Abs(delta.z) <= half.z + epsilon.z;
        }

        private Vector3 GetLocalTriggerBoundsEpsilon()
        {
            Vector3 scale = triggerCollider.transform.lossyScale;
            return new Vector3(
                GetLocalTriggerBoundsEpsilonAxis(scale.x),
                GetLocalTriggerBoundsEpsilonAxis(scale.y),
                GetLocalTriggerBoundsEpsilonAxis(scale.z)
            );
        }

        private float GetLocalTriggerBoundsEpsilonAxis(float worldScale)
        {
            float scale = Mathf.Abs(worldScale);
            if (scale < MinimumTriggerScale)
                return TriggerBoundsWorldEpsilon;
            return TriggerBoundsWorldEpsilon / scale;
        }

        private void SetMirrorVisible(bool visible)
        {
            GameObject rendererObject = profileRenderer == null ? null : profileRenderer.gameObject;
            GameObject mirrorObject = profileMirror == null ? null : profileMirror.gameObject;

            if (visible)
            {
                SetMirrorTargetObjectActive(rendererObject, true);
                if (mirrorObject != rendererObject)
                    SetMirrorTargetObjectActive(mirrorObject, true);
            }

            if (profileRenderer != null && profileRenderer.enabled != visible)
                profileRenderer.enabled = visible;
            if (profileMirror != null && profileMirror.enabled != visible)
                profileMirror.enabled = visible;

            if (!visible)
            {
                if (mirrorObject != rendererObject)
                    SetMirrorTargetObjectActive(mirrorObject, false);
                SetMirrorTargetObjectActive(rendererObject, false);
            }
        }

        private void SetMirrorTargetObjectActive(GameObject target, bool active)
        {
            if (target == null || target == gameObject)
                return;
            if (target.activeSelf != active)
                target.SetActive(active);
        }

        private void ApplyToggleState(int modeIndex)
        {
            if (uiToggle != null)
            {
                uiToggle.interactable = enableUguiInput;
                SetCanvasInputColliderState();
                uiToggle.SetIsOnWithoutNotify(modeIndex > 0);
            }
        }

        private void ApplyIconState(int modeIndex)
        {
            if (TryApplyProfileIconSprite(modeIndex))
            {
                SetAllLegacyIconObjectsActive(false);
                return;
            }

            if (iconImage != null && iconImage.enabled)
                iconImage.enabled = false;

            ApplyLegacyIconObjectState(modeIndex);
        }

        private bool TryApplyProfileIconSprite(int modeIndex)
        {
            if (iconImage == null || profileIconSprites == null)
                return false;
            if (modeIndex < 0 || modeIndex >= profileIconSprites.Length)
                return false;

            Sprite sprite = profileIconSprites[modeIndex];
            if (sprite == null)
                return false;

            if (iconImage.sprite != sprite)
                iconImage.sprite = sprite;
            Color iconColor = GetProfileIconColor(modeIndex);
            if (iconImage.color != iconColor)
                iconImage.color = iconColor;
            if (!iconImage.enabled)
                iconImage.enabled = true;
            return true;
        }

        private Color GetProfileIconColor(int modeIndex)
        {
            if (profileIconColors == null || modeIndex < 0 || modeIndex >= profileIconColors.Length)
                return Color.white;

            return profileIconColors[modeIndex];
        }

        private void SetAllLegacyIconObjectsActive(bool active)
        {
            if (profileIconObjects == null)
                return;

            for (int i = 0; i < profileIconObjects.Length; i++)
                SetLegacyIconObjectActive(profileIconObjects[i], active);
        }

        private void ApplyLegacyIconObjectState(int modeIndex)
        {
            if (profileIconObjects == null)
                return;

            for (int i = 0; i < profileIconObjects.Length; i++)
            {
                GameObject iconObject = profileIconObjects[i];
                SetLegacyIconObjectActive(iconObject, i == modeIndex);
            }
        }

        private void SetLegacyIconObjectActive(GameObject iconObject, bool active)
        {
            if (iconObject == null || iconObject.activeSelf == active)
                return;

            iconObject.SetActive(active);
        }

        private bool HasProfileTarget()
        {
            ResolveReferences();
            return profileMirror != null || profileRenderer != null;
        }

        private int GetCurrentSourceIndex()
        {
            return localIndex;
        }

        private void SavePersistData()
        {
            if (!usePersist || !IsPersistKeyValid())
                return;
            VRCPlayerApi localPlayer = Networking.LocalPlayer;
            if (localPlayer == null || !localPlayer.IsValid())
                return;
            PlayerData.SetInt(persistKey, localIndex);
        }

        private bool IsLocalValidPlayer(VRCPlayerApi player)
        {
            return player != null && player.IsValid() && player.isLocal;
        }

        private bool IsPersistKeyValid()
        {
            return persistKey != null && persistKey.Length > 0;
        }

        private void DetectPlatform()
        {
#if UNITY_ANDROID
            _platform = 2;
#elif UNITY_IOS
            _platform = 4;
#else
            _platform = 1;
#endif
        }

        private void PlayClickSound()
        {
            if (!enableClickSound)
                return;
            if (clickClip != null && sfxAudioSource != null)
                sfxAudioSource.PlayOneShot(clickClip);
        }

        private void PlayHapticFeedback(int handHint)
        {
            if (!enableHapticFeedback)
                return;

            VRCPlayerApi localPlayer = Networking.LocalPlayer;
            if (localPlayer == null || !localPlayer.IsValid())
                return;

            float duration = Mathf.Max(0f, hapticDuration);
            float amplitude = Mathf.Clamp01(hapticAmplitude);
            float frequency = Mathf.Clamp01(hapticFrequency);
            if (duration <= 0f || amplitude <= 0f)
                return;

            if (handHint == HapticHandLeft)
            {
                localPlayer.PlayHapticEventInHand(
                    VRC_Pickup.PickupHand.Left,
                    duration,
                    amplitude,
                    frequency
                );
                return;
            }

            if (handHint == HapticHandRight)
            {
                localPlayer.PlayHapticEventInHand(
                    VRC_Pickup.PickupHand.Right,
                    duration,
                    amplitude,
                    frequency
                );
                return;
            }

            localPlayer.PlayHapticEventInHand(
                VRC_Pickup.PickupHand.Left,
                duration,
                amplitude,
                frequency
            );
            localPlayer.PlayHapticEventInHand(
                VRC_Pickup.PickupHand.Right,
                duration,
                amplitude,
                frequency
            );
        }

        private int ClampHapticHandHint(int handHint)
        {
            if (handHint == HapticHandLeft || handHint == HapticHandRight)
                return handHint;
            return HapticHandBoth;
        }
    }
}
