﻿using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using UnityEngine;
using UnityEngine.UI;
using VRC.SDKBase;
using VRC.Udon.Common;

namespace VRC.SDK3.ClientSim
{
    /// <summary>
    /// System responsible for displaying the menu to the players and updating settings.
    /// </summary>
    /// <remarks>
    /// Sends Events:
    /// - ClientSimMenuStateChangedEvent
    /// - ClientSimMenuRespawnClickedEvent
    /// - ClientSimOnPlayerHeightUpdateEvent
    /// Listens to Events:
    /// - ClientSimReadyEvent
    /// - ClientSimOnPlayerMovedEvent
    /// - ClientSimOnNewMasterEvent
    /// - ClientSimOnPlayerJoinedEvent
    /// Listens to Input Events:
    /// - ToggleMenu
    /// </remarks>
    [AddComponentMenu("")]
    public class ClientSimMenu : ClientSimBehaviour, IDisposable
    {
        // Property name on UI shaders to set the ZTest mode. Used to make the menu appear on top of everything.
        private const string GUI_ZTEST_MODE_PROPERTY_NAME = "unity_GUIZTestMode";

        private const string HAS_USER_ACCEPTED_WARNING = "accepted_warning";
        
        public enum ClientSimDisplayedPage
        { 
            PAUSE_MENU,
            WARNING_PAGE,
            INVALID_SETTINGS_PAGE,
            DELAYED_START_PAGE,
        }
        
        // The method to open the settings window is set from Editor context.
        // This hook is set on playmode start in ClientSimEditorRuntimeLinker.cs
        internal static Action openSettingsHook;
        // This method allows the menu to check the editor only method if ClientSim has all settings properly set
        internal static Func<bool> checkValidSettingsHook;
        
        [SerializeField]
        private GameObject menu;

        public float menuScaleFactor = 0.0035f;
        
        [SerializeField]
        private GameObject pauseMenu;
        [SerializeField]
        private GameObject warningsPage;
        [SerializeField]
        private GameObject invalidSettingsPage;
        [SerializeField]
        private GameObject delayStartPage;


        [SerializeField]
        private Toggle tooltipsToggle;
        [SerializeField]
        private Toggle reticleToggle;
        [SerializeField]
        private Toggle invertMouseToggle;
        [SerializeField]
        private Toggle consoleLoggingToggle;
        [SerializeField]
        private Slider playerHeightSlider;
        [SerializeField]
        private Text playerHeightText;

        [SerializeField]
        private Text playerNameText;
        [SerializeField]
        private Text playerIdText;
        [SerializeField]
        private Toggle isMasterToggle;
        [SerializeField]
        private Toggle isInstanceOwnerToggle;
        [SerializeField]
        private Toggle isVRCPlusToggle;


        private IClientSimEventDispatcher _eventDispatcher;
        private IClientSimInput _input;
        private ClientSimSettings _settings;
        private IClientSimSessionState _sessionState;
        private IClientSimPlayerHeightManager _heightManager;

        private ClientSimDisplayedPage _displayedPage = ClientSimDisplayedPage.WARNING_PAGE;
        private bool _menuIsActive;
        private bool _stackedCameraReady = false;

        private float _playerHeightOriginalMaxvalue;

        private Canvas _menuCanvas;

        public void SetCanvasCamera(Camera cam)
        {
            _menuCanvas.worldCamera = cam;
        }
        
        protected override void Awake()
        {
            base.Awake();

            _menuCanvas = menu.GetComponent<Canvas>();
            _playerHeightOriginalMaxvalue = playerHeightSlider.maxValue;

        }

        public void Initialize(
            IClientSimEventDispatcher eventDispatcher, 
            IClientSimInput input, 
            ClientSimSettings settings,
            IClientSimSessionState sessionState,
            IClientSimPlayerHeightManager heightManager)
        {
            _eventDispatcher = eventDispatcher;
            _input = input;
            _settings = settings;
            _sessionState = sessionState;
            _heightManager = heightManager;

            // Input will be null with incorrect Unity input project settings.
            _input?.SubscribeToggleMenu(HandleInputMenuToggle);
            _eventDispatcher.Subscribe<ClientSimOnPlayerJoinedEvent>(OnPlayerJoined);
            _eventDispatcher.Subscribe<ClientSimOnNewMasterEvent>(OnMasterChange);
            _eventDispatcher.Subscribe<ClientSimReadyEvent>(OnReady);
            _eventDispatcher.Subscribe<ClientSimOnPlayerHeightUpdateEvent>(OnPlayerHeightUpdate);
            _eventDispatcher.Subscribe<ClientSimOnToggleManualScalingEvent>(OnManualScalingToggled);
            _eventDispatcher.Subscribe<ClientSimStackedCameraReadyEvent>(OnStackedCameraReady);

            playerNameText.text = "";
            playerIdText.text = "";
            isMasterToggle.isOn = false;
            isInstanceOwnerToggle.isOn = settings.isInstanceOwner;
            isVRCPlusToggle.isOn = settings.isVRCPlus;
            isVRCPlusToggle.onValueChanged.AddListener(OnVRCPlusToggleChanged);

            UpdateValuesFromSettings();
#if UNITY_EDITOR
            UnityEditor.SceneVisibilityManager.instance.Hide(gameObject, true);
#endif
        }

        public ClientSimDisplayedPage GetDisplayedPage()
        {
            return _displayedPage;
        }
        
        private void Start()
        {
            SetUIOverlayMaterial();
            
            bool shouldShowMenu = true;
            if (checkValidSettingsHook?.Invoke() == false)
            {
                // Force open the ClientSim settings window.
                // When first importing ClientSim and disabling legacy input, all UI menus fail to react to the mouse,
                // preventing users from clicking the settings window button.
                OpenSettings();
                SetDisplayedPage(ClientSimDisplayedPage.INVALID_SETTINGS_PAGE);
            }
            else if (_settings.initializationDelay > 0)
            {
                SetDisplayedPage(ClientSimDisplayedPage.DELAYED_START_PAGE);
            }
            else if (_settings.spawnPlayer)
            {
                DisplayInitialPageForPlayer();
            }
            else
            {
                shouldShowMenu = false;
            }
            
            ToggleMenu(shouldShowMenu);
        }

        private void OnDestroy()
        {
            Dispose();
        }

        public void Dispose()
        {
            _input?.UnsubscribeToggleMenu(HandleInputMenuToggle);
            
            _eventDispatcher?.Unsubscribe<ClientSimOnPlayerJoinedEvent>(OnPlayerJoined);
            _eventDispatcher?.Unsubscribe<ClientSimOnNewMasterEvent>(OnMasterChange);
            _eventDispatcher?.Unsubscribe<ClientSimReadyEvent>(OnReady);
            _eventDispatcher?.Unsubscribe<ClientSimOnPlayerMovedEvent>(OnPlayerMoved);
            _eventDispatcher?.Unsubscribe<ClientSimOnToggleManualScalingEvent>(OnManualScalingToggled);
        }

        private void Update()
        {
            // Only update the menu settings while the menu is displayed.
            if (_menuIsActive)
            {
                UpdateValuesFromSettings();
            }
        }

        private void SetUIOverlayMaterial()
        {
            int propertyId = Shader.PropertyToID(GUI_ZTEST_MODE_PROPERTY_NAME);
            
            // Solution provided from Unity forums:
            // https://answers.unity.com/questions/878667/world-space-canvas-on-top-of-everything.html
            Graphic[] graphics = GetComponentsInChildren<Graphic>(true);
            Dictionary<Material, Material> newMaterialMapping = new Dictionary<Material, Material>();
            foreach (var graphic in graphics)
            {
                Material mat = graphic.materialForRendering;
                if (mat == null)
                {
                    continue;
                }
                if (!newMaterialMapping.TryGetValue(mat, out Material updatedMaterial))
                {
                    updatedMaterial = new Material(mat);
                    newMaterialMapping.Add(mat, updatedMaterial);
                }
                updatedMaterial.SetInt(propertyId, (int)UnityEngine.Rendering.CompareFunction.Always);
                graphic.material = updatedMaterial;
            }
        }

        private void SetDisplayedPage(ClientSimDisplayedPage page)
        {
            _displayedPage = page;
            
            pauseMenu.SetActive(_displayedPage == ClientSimDisplayedPage.PAUSE_MENU);
            warningsPage.SetActive(_displayedPage == ClientSimDisplayedPage.WARNING_PAGE);
            delayStartPage.SetActive(_displayedPage == ClientSimDisplayedPage.DELAYED_START_PAGE);
            invalidSettingsPage.SetActive(_displayedPage == ClientSimDisplayedPage.INVALID_SETTINGS_PAGE);
        }

        private void DisplayInitialPageForPlayer()
        {
            SetDisplayedPage(_sessionState.GetBool(HAS_USER_ACCEPTED_WARNING)
                ? ClientSimDisplayedPage.PAUSE_MENU
                : ClientSimDisplayedPage.WARNING_PAGE);
        }
        
        private void UpdateValuesFromSettings()
        {
            tooltipsToggle.isOn = _settings.showTooltips;
            reticleToggle.isOn = _settings.showDesktopReticle;
            invertMouseToggle.isOn = _settings.invertMouseLook;
            consoleLoggingToggle.isOn = _settings.displayLogs;

            float playerHeight = _heightManager.GetAvatarEyeHeightAsMetersClamped();
            if (!Mathf.Approximately(playerHeight, playerHeightSlider.value))
            {
                // the player height slider obeys manual scale restrictions as if through the radial menu
                ClampPlayerHeightSliderBounds();
                
                playerHeightSlider.value = playerHeight;
                playerHeightText.text = playerHeight.ToString("F2");
                
                _eventDispatcher.SendEvent(new ClientSimOnPlayerHeightUpdateEvent { playerHeight = playerHeight });
            }
        }

        private void ClampPlayerHeightSliderBounds()
        {
            playerHeightSlider.minValue = _heightManager.GetAvatarEyeHeightMinimumAsMeters();
            playerHeightSlider.maxValue = _heightManager.GetAvatarEyeHeightMaximumAsMeters();
        }
        
        private void SaveSettings()
        {
#if UNITY_EDITOR
            ClientSimSettings.SaveSettings(_settings);
#endif
        }
        
        private void ToggleMenu(bool isActive)
        {
            // Don't allow the menu to be closed if the stacked camera system is not ready.
            if(!isActive && !_stackedCameraReady)
            {
                return;
            }
            
            _menuIsActive = isActive;
            menu.SetActive(isActive);

            // toggle internal UI camera stack to improve menu performance
            if (_menuCanvas.worldCamera != null)
                _menuCanvas.worldCamera.enabled = isActive;
            
            _eventDispatcher.SendEvent(new ClientSimMenuStateChangedEvent { isMenuOpen = _menuIsActive });

            if (_menuIsActive)
            {
                _eventDispatcher.Subscribe<ClientSimOnPlayerMovedEvent>(OnPlayerMoved);
                UpdateCanvasLocation();
                
                // If the user sets the player height value too large through the Settings window,
                // toggling the menu will clamp the max range to make it more usable again.
                ClampPlayerHeightSliderBounds();
            }
            else
            {
                _eventDispatcher.Unsubscribe<ClientSimOnPlayerMovedEvent>(OnPlayerMoved);
            }
        }

        // TODO update position based on tracking type. Desktop should always be in front of the camera and VR
        // should be stationary relative to the playspace position.
        private void UpdateCanvasLocation()
        {
            Camera cam = _menuCanvas.worldCamera;
            
            // Always use main camera to position the menu if the camera is missing or not enabled.
            if (cam == null || !cam.enabled || !cam.gameObject.activeInHierarchy)
            {
                cam = Camera.main;
            }

            // TODO handle missing camera better
            if (cam == null)
            {
                return;
            }
            
            Transform camTransform = cam.transform;
            menu.transform.localScale = camTransform.lossyScale * menuScaleFactor;
            
            Vector3 position = camTransform.TransformPoint(Vector3.forward * 2);
            menu.transform.SetPositionAndRotation(position, camTransform.rotation);
        }

#region ClientSim Input
        
        private void HandleInputMenuToggle(bool value, HandType hand)
        {
            // Only handle menu input down, and not on release.
            if (!value)
            {
                return;
            }
            
            // Users can only change the enabled state when the current page is the normal pause menu.
            if (_displayedPage != ClientSimDisplayedPage.PAUSE_MENU)
            {
                return;
            }
            
            // Player is not active, do not allow changing the state of the menu. 
            if (!_settings.spawnPlayer)
            {
                return;
            }
            
#if ENABLE_INPUT_SYSTEM
            // Ignore pressing escape to toggle the menu off. Due to Unity using the escape key as a special key to
            // remove focus from the game window, it is impossible to recapture it.
            if (_menuIsActive && (UnityEngine.InputSystem.Keyboard.current?.escapeKey.wasPressedThisFrame ?? false))
            {
                return;
            }
#endif
            ToggleMenu(!_menuIsActive);
        }

#endregion

#region ClientSim Events

        private void OnReady(ClientSimReadyEvent readyEvent)
        {
            // Disable the menu if the player is not spawned, and the displayed page isn't invalid.
            if (!_settings.spawnPlayer && _displayedPage != ClientSimDisplayedPage.INVALID_SETTINGS_PAGE)
            {
                ToggleMenu(false);
                return;
            }
            
            if (_displayedPage == ClientSimDisplayedPage.DELAYED_START_PAGE)
            {
                DisplayInitialPageForPlayer();
                UpdateCanvasLocation();
            }
        }

        private void OnStackedCameraReady(ClientSimStackedCameraReadyEvent readyEvent)
        {
            _stackedCameraReady = true;
            _eventDispatcher.Unsubscribe<ClientSimStackedCameraReadyEvent>(OnStackedCameraReady);
            if(_settings.hideMenuOnLaunch && _sessionState.GetBool(HAS_USER_ACCEPTED_WARNING))
                ToggleMenu(false);
            
        }
        
        private void OnPlayerJoined(ClientSimOnPlayerJoinedEvent joinEvent)
        {
            VRCPlayerApi player = joinEvent.player;
            if (!player.isLocal)
            {
                return;
            }

            isMasterToggle.isOn = player.isMaster;
            playerNameText.text = player.displayName;
            playerIdText.text = player.playerId.ToString();
        }

        private void OnMasterChange(ClientSimOnNewMasterEvent masterEvent)
        {
            isMasterToggle.isOn = Networking.IsMaster;
        }

        private void OnVRCPlusToggleChanged(bool value)
        {
            var localPlayer = Networking.LocalPlayer;
            if (localPlayer != null)
            {
                localPlayer.GetClientSimPlayer().isVRCPlus = value;
            }
        }

        private void OnPlayerMoved(ClientSimOnPlayerMovedEvent movedEvent)
        {
            UpdateCanvasLocation();
        }

        private void OnPlayerHeightUpdate(ClientSimOnPlayerHeightUpdateEvent heightEvent)
        {
            // if height was set programatically and exceeds a manual scaling limit, set slider to the exceeded limit 
            if (heightEvent.exceedsManualScalingMinimum)
                playerHeightSlider.SetValueWithoutNotify(_heightManager.GetAvatarEyeHeightMinimumAsMeters());
            
            else if (heightEvent.exceedsManualScalingMaximum)
                playerHeightSlider.SetValueWithoutNotify(_heightManager.GetAvatarEyeHeightMaximumAsMeters());
            
            else
                playerHeightSlider.SetValueWithoutNotify(heightEvent.playerHeight);
            
            playerHeightText.text = heightEvent.playerHeight.ToString("F2");
            ClientSimSettings.Instance.SetInitialPlayerHeight(heightEvent.playerHeight);
        }

        private void OnManualScalingToggled(ClientSimOnToggleManualScalingEvent toggleEvent)
        {
            playerHeightSlider.interactable = toggleEvent.manualScalingAllowed;
        }

#endregion

#region UI Hooks

        [PublicAPI]
        public void WarningAccepted()
        {
            CloseMenu();
            SetDisplayedPage(ClientSimDisplayedPage.PAUSE_MENU);
            _sessionState.SetBool(HAS_USER_ACCEPTED_WARNING, true);
        }

        [PublicAPI]
        public void OpenMenu()
        {
            ToggleMenu(true);
        }
        
        [PublicAPI]
        public void CloseMenu()
        {
            ToggleMenu(false);
        }

        [PublicAPI]
        public void Respawn()
        {
            CloseMenu();
            _eventDispatcher.SendEvent(new ClientSimMenuRespawnClickedEvent());
        }

        [PublicAPI]
        public void OpenSettings()
        {
            openSettingsHook?.Invoke();
        }

        [PublicAPI]
        public void ExitPlaymode()
        {
#if UNITY_EDITOR
            UnityEditor.EditorApplication.ExitPlaymode();
#endif
        }

        [PublicAPI]
        public void SpawnRemotePlayer()
        {
            ClientSimMain.SpawnRemotePlayer();
        }

        [PublicAPI]
        public void UpdatePlayerHeight(float playerHeight)
        {
            if (!_heightManager.GetManualAvatarScalingAllowed())
            {
                return;
            }
            
            if (Mathf.Approximately(_heightManager.GetAvatarEyeHeightAsMetersClamped(), playerHeight))
            {
                return;
            }

            _heightManager.SetAvatarEyeHeightByMeters(playerHeight, true);
            SaveSettings();
        }
        
        [PublicAPI]
        public void UpdateShowTooltips(bool showTooltips)
        {
            if (_settings.showTooltips == showTooltips)
            {
                return;
            }
            
            _settings.showTooltips = showTooltips;
            SaveSettings();
        }
        
        [PublicAPI]
        public void UpdateShowReticle(bool showReticle)
        {
            if (_settings.showDesktopReticle == showReticle)
            {
                return;
            }
            
            _settings.showDesktopReticle = showReticle;
            SaveSettings();
        }
        
        [PublicAPI]
        public void UpdateInvertMouseLook(bool invertMouseLook)
        {
            if (_settings.invertMouseLook == invertMouseLook)
            {
                return;
            }
            
            _settings.invertMouseLook = invertMouseLook;
            SaveSettings();
        }
        
        [PublicAPI]
        public void UpdateConsoleLogging(bool consoleLogging)
        {
            if (_settings.displayLogs == consoleLogging)
            {
                return;
            }
            
            _settings.displayLogs = consoleLogging;
            SaveSettings();
        }

#endregion
    }
}