// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

using UnityEngine;

#if UNITY_WSA
#if UNITY_2017_2_OR_NEWER
using UnityEngine.XR.WSA.Input;
#else
using UnityEngine.VR.WSA.Input;
#endif
#endif

namespace HoloToolkit.Unity.InputModule
{
    /// <summary>
    /// Show a hand guidance indicator when the user's hand is close to leaving the camera's view.
    /// </summary>
    public class HandGuidance : Singleton<HandGuidance>
    {
        [Tooltip("The Cursor object the HandGuidanceIndicator will be positioned around.")]
        public GameObject Cursor;

        [Tooltip("GameObject to display when your hand is about to lose tracking.")]
        public GameObject HandGuidanceIndicator;

        // Hand source loss risk to start showing a hand indicator.
        // As the source loss risk approaches 1, the hand is closer to being out of view.
        [Range(0.0f, 1.0f)]
        [Tooltip("When to start showing the Hand Guidance Indicator. 1 is out of view, 0 is centered in view.")]
        public float HandGuidanceThreshold = 0.5f;

#if UNITY_WSA
        private GameObject handGuidanceIndicatorGameObject;

        private Quaternion defaultHandGuidanceRotation;

        private uint? currentlyTrackedHand;

        protected override void Awake()
        {
            base.Awake();
            if (HandGuidanceIndicator == null)
            {
                Debug.LogError("Please include a GameObject for the Hand Guidance Indicator.");
            }

            if (Cursor == null)
            {
                Debug.LogError("Please include a GameObject for the Cursor to display the indicator around.");
            }

            if (HandGuidanceIndicator != null)
            {
                // Cache the initial rotation of the HandGuidanceIndicator so future rotations 
                // can be done with respect to this rotation.
                defaultHandGuidanceRotation = HandGuidanceIndicator.transform.rotation;
            }

            // Create an object in the scene for the guidance indicator and default it to not be visible.
            handGuidanceIndicatorGameObject = Instantiate(HandGuidanceIndicator);
            handGuidanceIndicatorGameObject.SetActive(false);

            // Register for hand and finger events to know where your hand
            // is being tracked and what state it is in.
#if UNITY_2017_2_OR_NEWER
            InteractionManager.InteractionSourceLost += InteractionManager_InteractionSourceLost;
            InteractionManager.InteractionSourceUpdated += InteractionManager_InteractionSourceUpdated;
            InteractionManager.InteractionSourceReleased += InteractionManager_InteractionSourceReleased;
#else
            InteractionManager.SourceLost += InteractionManager_SourceLost;
            InteractionManager.SourceUpdated += InteractionManager_SourceUpdated;
            InteractionManager.SourceReleased += InteractionManager_SourceReleased;
#endif
        }

        private void ShowHandGuidanceIndicator(InteractionSourceState hand)
        {
            if (!currentlyTrackedHand.HasValue)
            {
                return;
            }

            // Get the position and rotation of the hand guidance indicator and display the indicator object.
            if (handGuidanceIndicatorGameObject != null)
            {
                Vector3 position;
                Quaternion rotation;
                GetIndicatorPositionAndRotation(hand, out position, out rotation);

                handGuidanceIndicatorGameObject.transform.position = position;
                handGuidanceIndicatorGameObject.transform.rotation = rotation * defaultHandGuidanceRotation;
                handGuidanceIndicatorGameObject.SetActive(true);
            }
        }

        private void HideHandGuidanceIndicator(InteractionSourceState hand)
        {
            if (!currentlyTrackedHand.HasValue)
            {
                return;
            }

            if (handGuidanceIndicatorGameObject != null)
            {
                handGuidanceIndicatorGameObject.SetActive(false);
            }
        }

        private void GetIndicatorPositionAndRotation(InteractionSourceState hand, out Vector3 position, out Quaternion rotation)
        {
            // Update the distance from IndicatorParent based on the user's hand's distance from the center of the view.
            // Bound this distance by this maxDistanceFromCenter field, in meters.
            const float maxDistanceFromCenter = 0.3f;
            float distanceFromCenter = (float)(hand.properties.sourceLossRisk * maxDistanceFromCenter);

            // Subtract direction from origin so that the indicator is between the hand and the origin.
            position = Cursor.transform.position - hand.properties.sourceLossMitigationDirection * distanceFromCenter;
            rotation = Quaternion.LookRotation(CameraCache.Main.transform.forward, hand.properties.sourceLossMitigationDirection);
        }

#if UNITY_2017_2_OR_NEWER
        private void InteractionManager_InteractionSourceUpdated(InteractionSourceUpdatedEventArgs obj)
        {
            if (obj.state.source.kind == InteractionSourceKind.Hand)
            {
                InteractionSourceState hand = obj.state;

                // Only display hand indicators when we are in a holding state, since hands going out of view will affect any active gestures.
                if (!hand.anyPressed)
                {
                    return;
                }

                // Only track a new hand if are not currently tracking a hand.
                if (!currentlyTrackedHand.HasValue)
                {
                    currentlyTrackedHand = hand.source.id;
                }
                else if (currentlyTrackedHand.Value != hand.source.id)
                {
                    // This hand is not the currently tracked hand, do not drawn a guidance indicator for this hand.
                    return;
                }

                // Start showing an indicator to move your hand toward the center of the view.
                if (hand.properties.sourceLossRisk > HandGuidanceThreshold)
                {
                    ShowHandGuidanceIndicator(hand);
                }
                else
                {
                    HideHandGuidanceIndicator(hand);
                }
            }
        }

        private void InteractionManager_InteractionSourceReleased(InteractionSourceReleasedEventArgs obj)
        {
            if (obj.state.source.kind == InteractionSourceKind.Hand)
            {
                // Stop displaying the guidance indicator when the user releases their finger from the pressed state.
                RemoveTrackedHand(obj.state);
            }
        }

        private void InteractionManager_InteractionSourceLost(InteractionSourceLostEventArgs obj)
        {
            if (obj.state.source.kind == InteractionSourceKind.Hand)
            {
                // Stop displaying the guidance indicator when the user's hand leaves the view.
                RemoveTrackedHand(obj.state);
            }
        }

        private void RemoveTrackedHand(InteractionSourceState hand)
        {
            // Only remove a hand if we are currently tracking a hand, and the hand to remove matches this tracked hand.
            if (currentlyTrackedHand.HasValue && currentlyTrackedHand.Value == hand.source.id)
            {
                // Remove a hand by hiding the guidance indicator and nulling out the currentlyTrackedHand field.
                handGuidanceIndicatorGameObject.SetActive(false);
                currentlyTrackedHand = null;
            }
        }

        protected override void OnDestroy()
        {
            InteractionManager.InteractionSourceLost -= InteractionManager_InteractionSourceLost;
            InteractionManager.InteractionSourceUpdated -= InteractionManager_InteractionSourceUpdated;
            InteractionManager.InteractionSourceReleased -= InteractionManager_InteractionSourceReleased;

            base.OnDestroy();
        }
#else
        private void InteractionManager_SourceUpdated(InteractionSourceState hand)
        {
            // Only display hand indicators when we are in a holding state, since hands going out of view will affect any active gestures.
            if (!hand.pressed)
            {
                return;
            }

            // Only track a new hand if are not currently tracking a hand.
            if (!currentlyTrackedHand.HasValue)
            {
                currentlyTrackedHand = hand.source.id;
            }
            else if (currentlyTrackedHand.Value != hand.source.id)
            {
                // This hand is not the currently tracked hand, do not drawn a guidance indicator for this hand.
                return;
            }

            // Start showing an indicator to move your hand toward the center of the view.
            if (hand.properties.sourceLossRisk > HandGuidanceThreshold)
            {
                ShowHandGuidanceIndicator(hand);
            }
            else
            {
                HideHandGuidanceIndicator(hand);
            }
        }

        private void InteractionManager_SourceReleased(InteractionSourceState hand)
        {
            // Stop displaying the guidance indicator when the user releases their finger from the pressed state.
            RemoveTrackedHand(hand);
        }

        private void InteractionManager_SourceLost(InteractionSourceState hand)
        {
            // Stop displaying the guidance indicator when the user's hand leaves the view.
            RemoveTrackedHand(hand);
        }

        private void RemoveTrackedHand(InteractionSourceState hand)
        {
            // Only remove a hand if we are currently tracking a hand, and the hand to remove matches this tracked hand.
            if (currentlyTrackedHand.HasValue && currentlyTrackedHand.Value == hand.source.id)
            {
                // Remove a hand by hiding the guidance indicator and nulling out the currentlyTrackedHand field.
                handGuidanceIndicatorGameObject.SetActive(false);
                currentlyTrackedHand = null;
            }
        }

        protected override void OnDestroy()
        {
            InteractionManager.SourceLost -= InteractionManager_SourceLost;
            InteractionManager.SourceUpdated -= InteractionManager_SourceUpdated;
            InteractionManager.SourceReleased -= InteractionManager_SourceReleased;

            base.OnDestroy();
        }
#endif
#endif
    }
}