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

namespace VRC.SDK3.ClientSim
{
    /// <summary>
    /// This system is responsible for handling finding objects that can be interacted with or picked up.
    /// </summary>
    /// <remarks>
    /// Sends Events:
    /// - ClientSimRaycastHitResultsEvent
    /// - ClientSimInteractEvent
    /// Listens to Events:
    /// - ClientSimOnPlayerMovedEvent 
    /// - ClientSimPlayerDeathStatusChangedEvent
    /// Listens to Input Events:
    /// - Use
    /// </remarks>
    [AddComponentMenu("")]
    // Unity Event System Updates at -1000. Send raycast events before then to ensure UI interactions happen same frame.
    [DefaultExecutionOrder(-2000)] 
    public class ClientSimPlayerRaycaster : ClientSimBehaviour, IDisposable
    {
        [SerializeField]
        private ClientSimPlayerHand leftHand;
        [SerializeField]
        private ClientSimPlayerHand rightHand;

        private IClientSimEventDispatcher _eventDispatcher;
        private IClientSimInput _input;
        private IClientSimPlayerApiProvider _playerApiProvider;
        private IClientSimHighlightManager _highlightManager;
        private IClientSimTooltipManager _tooltipManager;
        private IClientSimInteractiveLayerProvider _interactiveLayerProvider;
        private IClientSimInteractManager _interactManager;
        private IClientSimTrackingProvider _trackingProvider;
        private IClientSimPlayerStationManager _stationManager;

        private ClientSimRaycaster _leftHandRaycaster;
        private ClientSimRaycaster _rightHandRaycaster;

        private ClientSimRaycastResults _hoverLeft;
        private ClientSimRaycastResults _hoverRight;
        
        public void Initialize(
            IClientSimEventDispatcher eventDispatcher,
            IClientSimInput input,
            IClientSimPlayerApiProvider playerApiProvider,
            IClientSimPlayerPickupData pickupData,
            IClientSimHighlightManager highlightManager,
            IClientSimTooltipManager tooltipManager,
            IClientSimInteractiveLayerProvider interactiveLayerProvider,
            IClientSimPlayerCameraProvider cameraProvider,
            IClientSimMousePositionProvider mousePositionProvider,
            IClientSimInteractManager interactManager,
            IClientSimTrackingProvider trackingProvider,
            IClientSimPlayerStationManager stationManager)
        {
            _eventDispatcher = eventDispatcher;
            _input = input;
            _playerApiProvider = playerApiProvider;
            _highlightManager = highlightManager;
            _tooltipManager = tooltipManager;
            _interactiveLayerProvider = interactiveLayerProvider;
            _interactManager = interactManager;
            _trackingProvider = trackingProvider;
            _stationManager = stationManager;
            
            leftHand.Initialize(_eventDispatcher, _input, trackingProvider, _playerApiProvider, pickupData);
            rightHand.Initialize(_eventDispatcher, _input, trackingProvider, _playerApiProvider, pickupData);
            
            // Input will be null with incorrect Unity input project settings.
            _input?.SubscribeUse(UseInput);
            _eventDispatcher.Subscribe<ClientSimOnPlayerMovedEvent>(OnPlayerMoved);
            _eventDispatcher.Subscribe<ClientSimPlayerDeathStatusChangedEvent>(CombatStatusEvent);
            
            // Create raycasters
            if (_trackingProvider.IsVR())
            {
                _leftHandRaycaster = new ClientSimRaycaster(
                    new ClientSimTransformRayProvider(_trackingProvider.GetHandRaycastTransform(HandType.LEFT)),
                    _interactiveLayerProvider, 
                    _interactManager);
            
                _rightHandRaycaster = new ClientSimRaycaster(
                    new ClientSimTransformRayProvider(_trackingProvider.GetHandRaycastTransform(HandType.RIGHT)),
                    _interactiveLayerProvider, 
                    _interactManager);
            }
            else
            {
                // Left hand is always null for desktop users.
                _leftHandRaycaster = null;
                
                // Right hand is from the player's camera.
                _rightHandRaycaster = new ClientSimRaycaster(
                    new ClientSimCameraRayProvider(cameraProvider, mousePositionProvider), 
                    _interactiveLayerProvider, 
                    _interactManager);
            }
        }

        private void OnDestroy()
        {
            Dispose();
        }

        public void Dispose()
        {
            _input?.UnsubscribeUse(UseInput);
            _eventDispatcher?.Unsubscribe<ClientSimOnPlayerMovedEvent>(OnPlayerMoved);
            _eventDispatcher?.Unsubscribe<ClientSimPlayerDeathStatusChangedEvent>(CombatStatusEvent);
        }

        private void Update()
        {
            UpdateHandPositions();

            SetHoverLeft(_leftHandRaycaster?.CheckForInteracts());
            SetHoverRight(_rightHandRaycaster.CheckForInteracts());
        }
        
        private void SetHoverLeft(ClientSimRaycastResults raycastResults)
        {
            SetHover(ref _hoverLeft, HandType.LEFT, leftHand, raycastResults);
        }
        
        private void SetHoverRight(ClientSimRaycastResults raycastResults)
        {
            SetHover(ref _hoverRight, HandType.RIGHT, rightHand, raycastResults);
        }
        
        private void SetHover(
            ref ClientSimRaycastResults handHover, 
            HandType handType,
            ClientSimPlayerHand playerHand, 
            ClientSimRaycastResults raycastResults)
        {
            // TODO optimize this to check if previous was the same as new and not disable/re-enable.
            if (handHover != null && handHover.interactable != null)
            {
                _highlightManager.DisableObjectHighlight(handHover.hitObject);
                _tooltipManager.DisableTooltip(handHover.interactable);
            }

            raycastResults = FilterRaycastResults(raycastResults);
            
            // If the player is not holding something, or the hit object is a UIShape, set the hover.
            // This allows players to interact with UI while still holding objects,
            // but holding objects will block pickups and interacts.
            if (!playerHand.IsHolding() || (raycastResults != null && raycastResults.uiShape != null))
            {
                handHover = raycastResults;
            }
            else
            {
                handHover = null;
            }

            // Highlight the object if it has an interactable
            if (handHover != null && handHover.interactable != null)
            {
                _highlightManager.EnableObjectHighlight(handHover.hitObject);
                _tooltipManager.DisplayTooltip(handHover.interactable);
            }

            // If the hovered object has a pickupable that can be interacted with, set that as this hand's hovered pickup.
            IClientSimPickupable pickupable = handHover?.GetPickupable();
            if (pickupable != null && !_interactManager.CanInteract(pickupable, handHover.distance))
            {
                pickupable = null;
            }
            playerHand.SetHoverPickupable(pickupable);
            
            _eventDispatcher.SendEvent(new ClientSimRaycastHitResultsEvent
            {
                handType = handType,
                raycastResults = handHover
            });
        }

        private ClientSimRaycastResults FilterRaycastResults(ClientSimRaycastResults results)
        {
            if (results == null || results.hitObject == null)
            {
                return results;
            }
            
            // If the user is in a station with "CanUseStationFromStation" set to false, disable interacts
            // for any object that contains a station script.
            if (_stationManager.InStation() 
                && !_stationManager.GetCurrentStation().CanUseStationFromStation()
                && results.hitObject.GetComponent<IClientSimStation>() != null)
            {
                return null;
            }

            return results;
        }

        private void TryInteract(HandType handType, ClientSimRaycastResults hover)
        {
            // Nothing to interact with.
            if (hover == null || hover.interactable == null)
            {
                return;
            }
            
            // Interact with the object and get the components that were interacted with.
            var interacts = _interactManager.Interact(hover.hitObject, hover.distance);
                
            // Notify ClientSim of interacted objects.
            _eventDispatcher.SendEvent(new ClientSimInteractEvent
            {
                handType = handType,
                interactObject = hover.hitObject,
                interactDistance = hover.distance,
                interacts = interacts,
            });
        }

        private void UpdateHandPositions()
        {
            // TODO do not update hands if player controller is stuck in a collider.
            // Note that these hands are only for pickups and that raycast logic uses the tracking provider hands. 
            
            var leftHandData = _trackingProvider.GetTrackingData(VRCPlayerApi.TrackingDataType.LeftHand);
            leftHand.transform.SetPositionAndRotation(leftHandData.position, leftHandData.rotation);
            leftHand.UpdatePosition();
            
            var rightHandData = _trackingProvider.GetTrackingData(VRCPlayerApi.TrackingDataType.RightHand);
            rightHand.transform.SetPositionAndRotation(rightHandData.position, rightHandData.rotation);
            rightHand.UpdatePosition();
        }

        #region ClientSim Input

        private void UseInput(bool value, HandType hand)
        {
            if (!value)
            {
                return;
            }

            if (hand == HandType.LEFT)
            {
                TryInteract(HandType.LEFT, _hoverLeft);
            }
            if (hand == HandType.RIGHT)
            {
                TryInteract(HandType.RIGHT, _hoverRight);
            }
        }
        
        #endregion

        #region ClientSim Events

        private void OnPlayerMoved(ClientSimOnPlayerMovedEvent moveEvent)
        {
            UpdateHandPositions();
        }

        private void CombatStatusEvent(ClientSimPlayerDeathStatusChangedEvent combatStatusEvent)
        {
            leftHand.ForceDrop();
            rightHand.ForceDrop();
        }

        #endregion
    
    }
}