
using System;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon.Common;

namespace VRC.SDK3.ClientSim
{
    // Sends Events:
    // - ClientSimOnPlayerTeleportedEvent
    // - ClientSimOnPlayerRespawnEvent  
    // - ClientSimOnPlayerMovedEvent
    // Listens to Events:
    // - ClientSimMenuStateChangedEvent 
    // - ClientSimMenuRespawnClickedEvent
    // - ClientSimMouseReleasedEvent
    // - ClientSimPlayerDeathStatusChangedEvent
    // - ClientSimOnTrackingScaleUpdateEvent
    // Listens to Input Events:
    // - Jump
    // - Run
    [AddComponentMenu("")]
    [DefaultExecutionOrder(-3000)] // Update before player raycasting
    public class ClientSimPlayerController : ClientSimBehaviour, IDisposable
    {
        private const float CROUCH_SPEED_MULTIPLIER = 0.35f;
        private const float PRONE_SPEED_MULTIPLIER = 0.15f;

        private const float STICK_TO_GROUND_FORCE = 2f;
        private const float RATE_OF_AIR_ACCELERATION = 5f;

        private IClientSimPlayerStationManager _stationManager;
        private IClientSimPlayerLocomotionData _playerLocomotionData;
        private IClientSimPlayerApiProvider _playerApi;
        private IClientSimSceneManager _sceneManager;
        private IClientSimEventDispatcher _eventDispatcher;
        private IClientSimInput _input;
        private IClientSimTrackingProvider _trackingProvider;
        
        private CharacterController _characterController;
        private Transform _cameraProxyObject;

        private bool _isDead;
        private bool _isWalking = true; // Player defaults to walking
        private bool _jump;
        
        private Vector2 _prevInput;
        // Check if the directionality changed to apply "stutter stepping" for legacy locomotion.
        private bool _directionChanged;

        private bool _velSet;
        // TODO fix handling of SetVelocity as retaining the velocity causes strange bugs due to ignoring collisions
        // that normally would stop the player.
        private Vector3 _playerRetainedVelocity;

        private bool _menuIsOpen;
        private bool _mouseReleased;

        protected override void Awake()
        {
            base.Awake();

            _characterController = GetComponent<CharacterController>();
        }

        public void Initialize(
            IClientSimEventDispatcher eventDispatcher,
            IClientSimInput input,
            IClientSimPlayerApiProvider playerApiProvider,
            IClientSimPlayerLocomotionData locomotionData,
            IClientSimSceneManager sceneManager,
            IClientSimProxyObjectProvider proxyProvider,
            IClientSimTrackingProvider trackingProvider,
            IClientSimPlayerStationManager stationManager)
        {
            _eventDispatcher = eventDispatcher;
            _input = input;
            _playerApi = playerApiProvider;
            _playerLocomotionData = locomotionData;
            _sceneManager = sceneManager;
            _trackingProvider = trackingProvider;
            _stationManager = stationManager;

            _cameraProxyObject = proxyProvider.CameraProxy().transform;
            
            Subscribe();
        }
        
        private void Start()
        {
            NotifyPlayerMoved();
        }

        private void OnDestroy()
        {
            Dispose();
        }
        
        private void Subscribe()
        {
            // Input will be null with incorrect Unity input project settings.
            _input?.SubscribeJump(JumpInput);
            _input?.SubscribeRun(RunInput);

            _eventDispatcher.Subscribe<ClientSimMenuStateChangedEvent>(SetMenuOpen);
            _eventDispatcher.Subscribe<ClientSimMenuRespawnClickedEvent>(MenuRespawnEvent);
            _eventDispatcher.Subscribe<ClientSimMouseReleasedEvent>(MouseReleasedEvent);
            _eventDispatcher.Subscribe<ClientSimPlayerDeathStatusChangedEvent>(CombatStatusEvent);
            _eventDispatcher.Subscribe<ClientSimOnTrackingScaleUpdateEvent>(OnTrackingScaleUpdated);
        }
        
        public void Dispose()
        {
            _input?.UnsubscribeJump(JumpInput);
            _input?.UnsubscribeRun(RunInput);

            _eventDispatcher.Unsubscribe<ClientSimMenuStateChangedEvent>(SetMenuOpen);
            _eventDispatcher.Unsubscribe<ClientSimMenuRespawnClickedEvent>(MenuRespawnEvent);
            _eventDispatcher.Unsubscribe<ClientSimMouseReleasedEvent>(MouseReleasedEvent);
            _eventDispatcher.Unsubscribe<ClientSimPlayerDeathStatusChangedEvent>(CombatStatusEvent);
            _eventDispatcher.Unsubscribe<ClientSimOnTrackingScaleUpdateEvent>(OnTrackingScaleUpdated);
        }

        #region Input Events

        private void JumpInput(bool value, HandType hand)
        {
            // Only handle on down, and not on release.
            if (!value)
            {
                return;
            }

            if (!_jump && _characterController.isGrounded && _playerLocomotionData.GetJump() > 0)
            {
                _jump = true;
            }
        }

        private void RunInput(bool value)
        {
            _isWalking = !value;
        }

        #endregion

        #region ClientSim Events

        private void SetMenuOpen(ClientSimMenuStateChangedEvent stateChangedEvent)
        {
            _menuIsOpen = stateChangedEvent.isMenuOpen;
        }
        
        private void MenuRespawnEvent(ClientSimMenuRespawnClickedEvent stateChangedEvent)
        {
            Respawn();
        }
        
        private void MouseReleasedEvent(ClientSimMouseReleasedEvent mouseReleasedEvent)
        {
            _mouseReleased = mouseReleasedEvent.isReleased;
        }
        
        private void CombatStatusEvent(ClientSimPlayerDeathStatusChangedEvent combatStatusEvent)
        {
            _isDead = combatStatusEvent.isDead;
        }

        private void OnTrackingScaleUpdated(ClientSimOnTrackingScaleUpdateEvent scaleUpdatedEvent)
        {
            NotifyPlayerMoved();
        }

        #endregion
        
        public Vector3 GetPosition()
        {
            return transform.position;
        }

        public Quaternion GetRotation()
        {
            return transform.rotation;
        }

        public Vector3 GetVelocity()
        {
            return _characterController.velocity;
        }
        
        public void SetVelocity(Vector3 velocity)
        {
            _playerRetainedVelocity = velocity;
            _velSet = true;
            _jump = false;
        }

        public bool IsGrounded()
        {
            return _characterController.isGrounded;
        }

        public void Respawn()
        {
            Transform spawnPoint = _sceneManager.GetSpawnPoint(false);
            Vector3 position = ClientSimPlayerSpawner.GetRandomPositionAroundSpawn(spawnPoint.position, _sceneManager.GetSpawnRadius());
            Teleport(position, spawnPoint.rotation, false);
            _eventDispatcher.SendEvent(new ClientSimOnPlayerRespawnEvent { player = _playerApi.Player });
        }

        public void Respawn(int index)
        {
            
            Transform spawnPoint = _sceneManager.GetSpawnPoint(index);
            if (spawnPoint == null)
            {
                this.LogError($"Spawn {index} not found. Spawning at spawn 0");
                spawnPoint = _sceneManager.GetSpawnPoint(0);
            }
            Vector3 position = ClientSimPlayerSpawner.GetRandomPositionAroundSpawn(spawnPoint.position, _sceneManager.GetSpawnRadius());
            Teleport(position, spawnPoint.rotation, false);
            _eventDispatcher.SendEvent(new ClientSimOnPlayerRespawnEvent { player =  _playerApi.Player });
        }

        public void Teleport(Transform point, bool fromPlaySpace)
        {
            Teleport(point.position, Quaternion.Euler(0, point.rotation.eulerAngles.y, 0), fromPlaySpace);
        }

        public void Teleport(Vector3 position, Quaternion floorRotation, bool fromPlaySpace)
        {
            floorRotation = Quaternion.Euler(0, floorRotation.eulerAngles.y, 0);
            if (fromPlaySpace)
            {
                VRCPlayerApi.TrackingData playspaceData = _trackingProvider.GetTrackingData(VRCPlayerApi.TrackingDataType.Origin);
                Vector3 playspacePos = playspaceData.position - transform.position;
                Quaternion playspaceRot = playspaceData.rotation * Quaternion.Inverse(transform.rotation);
                floorRotation = floorRotation * Quaternion.Inverse(playspaceRot);
                position += floorRotation * -playspacePos;
            }

            this.Log($"Moving player to {position.ToString("F3")} " +
                     $"and rotation {floorRotation.eulerAngles.ToString("F3")} " +
                     $"(fromPlaySpace={fromPlaySpace})");

            transform.rotation = floorRotation;
            transform.position = position;

            NotifyPlayerMoved();
            _eventDispatcher.SendEvent(new ClientSimOnPlayerTeleportedEvent { player = _playerApi.Player });
            
            Physics.SyncTransforms();
        }

        #region Stations

        public void EnterStation(IClientSimStation station)
        {
            if (!station.IsMobile())
            {
                _characterController.enabled = false;
                Transform spawn = station.EnterLocation();
                Teleport(spawn, false);
            }
            // VRChatBug: Note that in the else case, the player is teleported to a location that is twice the distance
            // to the station, but since this appears to be a bug, it will not be implemented.
        }

        public void ExitStation(IClientSimStation station, bool skipTeleport)
        {
            _characterController.enabled = true;

            if (!skipTeleport)
            {
                Transform spawn = station.ExitLocation();
                Teleport(spawn, false);
            }
            
            _jump = false;
        }
        
        public void SitPosition(Transform seat)
        {
            transform.SetPositionAndRotation(seat.position, seat.rotation);
            NotifyPlayerMoved();
        }

        #endregion

        private void Update()
        {
            // Handle below respawn height.
            if (transform.position.y < _sceneManager.GetRespawnHeight())
            {
                Respawn();
            }
            
            GetInput();

            NotifyPlayerMoved();
        }
   
        private void FixedUpdate()
        {
            Physics.SyncTransforms();
            Vector2 speed = GetSpeed();
            Vector2 input = _prevInput;

            if (!_stationManager.CanPlayerMove(input.magnitude))
            {
                return;
            }

            if (_menuIsOpen || _isDead)
            {
                input = Vector2.zero;
                _jump = false;
            }

            // Immobile does not affect Jump
            if (_playerLocomotionData.GetImmobilized())
            {
                input = Vector2.zero;
            }

            // Always move along the camera forward as it is the direction that it being aimed at
            Vector3 desiredMove = input.y * speed.x * transform.forward + input.x * speed.y * transform.right;
            desiredMove.y = 0;

            float gravityContribution = _playerLocomotionData.GetGravityStrength() * Time.fixedDeltaTime * Physics.gravity.y;

            if (!_velSet)
            {
                if (_characterController.isGrounded)
                {
                    _playerRetainedVelocity = Vector3.zero;
                    _playerRetainedVelocity.y = -STICK_TO_GROUND_FORCE;
                    if (_jump)
                    {
                        if (!_playerLocomotionData.GetUseLegacyLocomotion())
                        {
                            _playerRetainedVelocity = desiredMove;
                        }
                        _playerRetainedVelocity.y = _playerLocomotionData.GetJump();
                        desiredMove = Vector3.zero;
                        _jump = false;
                    }
                }
                else
                {
                    // Slowly add velocity from movement inputs
                    if (!_playerLocomotionData.GetUseLegacyLocomotion())
                    {
                        Vector3 localVelocity = transform.InverseTransformVector(_characterController.velocity);
                        localVelocity.x = Mathf.Clamp(localVelocity.x, -speed.y, speed.y);
                        localVelocity.z = Mathf.Clamp(localVelocity.z, -speed.x, speed.x);

                        Vector3 maxAc = new Vector3(speed.y - localVelocity.x, 0, speed.x - localVelocity.z);
                        Vector3 minAc = new Vector3(-speed.y - localVelocity.x, 0, -speed.x - localVelocity.z);

                        Vector3 inputAcceleration = Time.fixedDeltaTime * RATE_OF_AIR_ACCELERATION * new Vector3(input.x * speed.y, 0, input.y * speed.x);
                        inputAcceleration.x = Mathf.Clamp(inputAcceleration.x, minAc.x, maxAc.x);
                        inputAcceleration.z = Mathf.Clamp(inputAcceleration.z, minAc.z, maxAc.z);

                        inputAcceleration = transform.TransformVector(inputAcceleration);
                        _playerRetainedVelocity += inputAcceleration;
                        desiredMove = Vector3.zero;
                    }
                    // Legacy stutter stepping
                    else if (_directionChanged)
                    {
                        _playerRetainedVelocity = Vector3.zero;
                    }
                    _playerRetainedVelocity.y += gravityContribution;
                }
            }
            else // Dumb behavior that hopefully needs to be removed
            {
                _characterController.Move(new Vector3(desiredMove.x * 0.05f, desiredMove.y * 0.05f + gravityContribution, desiredMove.z * 0.05f) * Time.fixedDeltaTime);
                desiredMove = Vector3.zero;
            }

            desiredMove += _playerRetainedVelocity;

            _characterController.Move(desiredMove * Time.fixedDeltaTime);

            _velSet = false;

            NotifyPlayerMoved();
        }

        #region Input

        private void GetInput()
        {
            GetMovementInput();
            if (_menuIsOpen && Mathf.Max(_prevInput.x, _prevInput.y) > 0)
            {
                _input.SendToggleMenuEvent(true, HandType.RIGHT);
            }
            // Only allow these input actions while the menu is closed
            if (!_menuIsOpen)
            {
                RotateView();
            }
        }
        
        private void GetMovementInput()
        {
            Vector2 input = _input.GetMovementAxes();
            if (input.sqrMagnitude > 1)
            {
                input.Normalize();
            }
            
            _directionChanged = (input.sqrMagnitude < 1e-3 ^ _prevInput.sqrMagnitude < 1e-3);
            _prevInput = input;
        }
        
        // TODO Move rotation of the player controller to be done in the tracking provider and have the player controller
        // copy the head rotation. This would allow more generic handling of mouse released, VR snap turning, and locked in station.
        private void RotateView()
        {
            // Allow player controller to look left and right when not in a locked station and for desktop users
            // when the mouse is not released..
            if (!_mouseReleased && !_stationManager.IsLockedInStation())
            {
                float yRot = _input.GetLookHorizontal();
                transform.rotation *= Quaternion.Euler(0f, yRot, 0f);
            }
        }
        
        // Used in tests to help look a specific direction
        public void LookTowardsPoint(Vector3 point)
        {
            if (_stationManager.IsLockedInStation())
            {
                return;
            }

            point.y = transform.position.y;
            transform.LookAt(point);
        }

        #endregion

        // Notify all systems that are dependent on the player's position that the player has moved to a new location.
        private void NotifyPlayerMoved()
        {
            _eventDispatcher.SendEvent(new ClientSimOnPlayerMovedEvent { player = _playerApi.Player });

            if (_cameraProxyObject == null)
            {
                return;
            }
            var cameraTrackingData = _trackingProvider.GetTrackingData(VRCPlayerApi.TrackingDataType.Head);
            _cameraProxyObject.SetPositionAndRotation(cameraTrackingData.position, cameraTrackingData.rotation);
            _cameraProxyObject.localScale = _trackingProvider.GetTrackingScale() * Vector3.one; 
        }

        private Vector2 GetSpeed()
        {
            // TODO check current bindings to see if non keyboard and only use runspeed.
            Vector2 speed = new Vector2(
                _isWalking? _playerLocomotionData.GetWalkSpeed() : _playerLocomotionData.GetRunSpeed(),
                _playerLocomotionData.GetStrafeSpeed());

            switch (_trackingProvider.GetPlayerStance())
            {
                case ClientSimPlayerStanceEnum.CROUCHING:
                    speed *= CROUCH_SPEED_MULTIPLIER;
                    break;
                case ClientSimPlayerStanceEnum.PRONE:
                    speed *= PRONE_SPEED_MULTIPLIER;
                    break;
            }

            return speed;
        }
    }
}
