// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.MixedReality.Toolkit.Physics;
using Microsoft.MixedReality.Toolkit.Utilities;
using Unity.Profiling;
using UnityEngine;

namespace Microsoft.MixedReality.Toolkit.Input
{
    /// <summary>
    /// A near interaction pointer that generates touch events based on touchables in close proximity.
    /// </summary>
    /// <remarks><format type="text/markdown">
    /// _Reachable Objects_ are objects with a both a [BaseNearInteractionTouchable](xref:Microsoft.MixedReality.Toolkit.Input.BaseNearInteractionTouchable) and a collider within [TouchableDistance](xref:Microsoft.MixedReality.Toolkit.Input.PokePointer.TouchableDistance) from the poke pointer (based on [OverlapSphere](https://docs.unity3d.com/ScriptReference/Physics.OverlapSphere.html)).
    ///
    /// If a poke pointer has no [CurrentTouchableObjectDown](xref:Microsoft.MixedReality.Toolkit.Input.PokePointer.CurrentTouchableObjectDown), then it will try to select one from the Reachable Objects based on:
    /// 1. Layer mask priority: Lower-priority layer masks will only be considered if higher-priority layers don't contain any Reachable Objects.
    /// 1. Touchable Distance: the closest object in the highest priority layers is selected based on [DistanceToTouchable](xref:Microsoft.MixedReality.Toolkit.Input.BaseNearInteractionTouchable.DistanceToTouchable*).
    /// 1. Ray Distance: The object becomes the [CurrentTouchableObjectDown](xref:Microsoft.MixedReality.Toolkit.Input.PokePointer.CurrentTouchableObjectDown) once the ray cast distance becomes negative (behind the surface). At this point the [OnTouchStarted](xref:Microsoft.MixedReality.Toolkit.Input.IMixedRealityTouchHandler.OnTouchStarted*) or [OnPointerDown](xref:Microsoft.MixedReality.Toolkit.Input.IMixedRealityPointerHandler.OnPointerDown*) event is raised.
    ///
    /// If a poke pointer _does_  have a [CurrentTouchableObjectDown](xref:Microsoft.MixedReality.Toolkit.Input.PokePointer.CurrentTouchableObjectDown) it will not consider any other object, until the [DistanceToTouchable](xref:Microsoft.MixedReality.Toolkit.Input.BaseNearInteractionTouchable.DistanceToTouchable*) exceeds the [DebounceThreshold](xref:Microsoft.MixedReality.Toolkit.Input.BaseNearInteractionTouchable.DebounceThreshold) (in front of the surface). At this point the active object is cleared and the [OnTouchCompleted](xref:Microsoft.MixedReality.Toolkit.Input.IMixedRealityTouchHandler.OnTouchCompleted*) or [OnPointerUp](xref:Microsoft.MixedReality.Toolkit.Input.IMixedRealityPointerHandler.OnPointerUp*) event is raised.
    /// </format></remarks>
    [AddComponentMenu("Scripts/MRTK/SDK/PokePointer")]
    public class PokePointer : BaseControllerPointer, IMixedRealityNearPointer
    {
        [SerializeField]
        [Tooltip("Maximum distance a which a touchable surface can be interacted with.")]
        protected float touchableDistance = 0.2f;
        /// <summary>
        /// Maximum distance a which a touchable surface can be interacted with.
        /// </summary>
        public float TouchableDistance => touchableDistance;


        [SerializeField]
        [Tooltip("The offset that the poke pointer has from the source pose when the index finger pose is not available.")]
        protected float sourcePoseOffset = 0.075f;
        /// <summary>
        /// The offset that the poke pointer has from the source pose when the index finger pose is not available.
        /// This value puts the pointer slightly in front of the source pose's origin, oriented according to the source pose's rotation
        /// </summary>
        public float SourcePoseOffset => sourcePoseOffset;


        [SerializeField]
        [Tooltip("Maximum number of colliders that can be detected in a scene query.")]
        [Min(1)]
        private int sceneQueryBufferSize = 64;
        /// <summary>
        /// Maximum number of colliders that can be detected in a scene query.
        /// </summary>
        public int SceneQueryBufferSize => sceneQueryBufferSize;

        [SerializeField]
        [Tooltip("Whether to ignore colliders that may be near the pointer, but not actually in the visual FOV. " +
            "This can prevent accidental touches, and will allow hand rays to turn on when you may be near a " +
            "touchable but cannot see it. Visual FOV is defined by cone centered about display center, " +
            "radius equal to half display height.")]
        private bool ignoreCollidersNotInFOV = true;

        /// <summary>
        /// Whether to ignore colliders that may be near the pointer, but not actually in the visual FOV.
        /// This can prevent accidental touches, and will allow hand rays to turn on when you may be near
        /// a touchable but cannot see it. Visual FOV is defined by cone centered about display center,
        /// radius equal to half display height.
        /// </summary>
        public bool IgnoreCollidersNotInFOV
        {
            get => ignoreCollidersNotInFOV;
            set => ignoreCollidersNotInFOV = value;
        }

        [SerializeField]
        [Tooltip("The LayerMasks, in prioritized order, that are used to determine the touchable objects.")]
        private LayerMask[] pokeLayerMasks = { UnityEngine.Physics.DefaultRaycastLayers };
        /// <summary>
        /// The LayerMasks, in prioritized order, that are used to determine the touchable objects.
        /// </summary>
        /// <remarks>
        /// Only [BaseNearInteractionTouchables](xref:Microsoft.MixedReality.Toolkit.Input.BaseNearInteractionTouchable) in one of the LayerMasks will raise touch events.
        /// </remarks>
        [System.Obsolete("Use PrioritizedLayerMasksOverride instead")]
        public LayerMask[] PokeLayerMasks => pokeLayerMasks;

        /// <inheritdoc />
        public override LayerMask[] PrioritizedLayerMasksOverride
        {
            get { return pokeLayerMasks; }
            set { pokeLayerMasks = value; }
        }

        [SerializeField]
        [Tooltip("Specify whether queries for touchable surfaces hit triggers.")]
        protected QueryTriggerInteraction triggerInteraction = QueryTriggerInteraction.UseGlobal;
        /// <summary>
        /// Specify whether queries for touchable surfaces hit triggers.
        /// </summary>
        public QueryTriggerInteraction TriggerInteraction => triggerInteraction;

        private Collider[] queryBuffer;

        private float closestDistance = 0.0f;

        private Vector3 closestNormal = Vector3.forward;

        // previous frame pointer position
        public Vector3 PreviousPosition { get; private set; } = Vector3.zero;

        private BaseNearInteractionTouchable closestProximityTouchable = null;
        /// <summary>
        /// The closest touchable component that has been detected.
        /// </summary>
        /// <remarks>
        /// <para>The closest touchable component limits the set of objects which are currently touchable.
        /// These are all the game objects in the subtree of the closest touchable component's owner object.</para>
        /// </remarks>
        public BaseNearInteractionTouchable ClosestProximityTouchable => closestProximityTouchable;

        private GameObject currentTouchableObjectDown = null;
        /// <summary>
        /// The current object that is being touched.
        /// </summary>
        /// We need to make sure to consistently fire
        /// poke-down / poke-up events for this object. This is also the case when the object within
        /// the same current closest touchable component's changes (e.g. Unity UI control elements).
        public GameObject CurrentTouchableObjectDown => currentTouchableObjectDown;

        private void Awake()
        {
            queryBuffer = new Collider[sceneQueryBufferSize];
        }

        protected void OnValidate()
        {
            touchableDistance = Mathf.Max(touchableDistance, 0);
            sceneQueryBufferSize = Mathf.Max(sceneQueryBufferSize, 1);
        }

        /// <inheritdoc />
        public virtual bool IsNearObject => closestProximityTouchable != null;

        /// <inheritdoc />
        public override bool IsInteractionEnabled => base.IsInteractionEnabled && IsNearObject;

        private static readonly ProfilerMarker OnPreSceneQueryPerfMarker = new ProfilerMarker("[MRTK] PokePointer.OnPreSceneQuery");

        public override void OnPreSceneQuery()
        {
            using (OnPreSceneQueryPerfMarker.Auto())
            {
                if (Rays == null)
                {
                    Rays = new RayStep[1];
                }

                // Find closest touchable
                BaseNearInteractionTouchable newClosestTouchable = null;
                foreach (var layerMask in PrioritizedLayerMasksOverride)
                {
                    if (FindClosestTouchableForLayerMask(layerMask, out newClosestTouchable, out closestDistance, out closestNormal))
                    {
                        break;
                    }
                }

                if (newClosestTouchable != null)
                {
                    // Build ray (poke from in front to the back of the pointer position)
                    NearInteractionTouchableVolume touchableVolume = newClosestTouchable as NearInteractionTouchableVolume;
                    if (touchableVolume != null && (closestDistance < 0.0f))
                    {
                        // When we are inside of a volume, ensure that we actually hit it by placing the origin closely outside the volume.
                        Vector3 start = Position + (-closestDistance * 1.01f) * closestNormal;
                        Vector3 end = Position - touchableVolume.TouchableCollider.bounds.size.magnitude * closestNormal;
                        Rays[0].UpdateRayStep(ref start, ref end);
                    }
                    else
                    {
                        Vector3 start = Position + touchableDistance * closestNormal;
                        Vector3 end = Position - touchableDistance * closestNormal;
                        Rays[0].UpdateRayStep(ref start, ref end);
                    }
                }
                else
                {
                    closestNormal = Rotation * Vector3.forward;
                }

                // Check if the currently touched object is still part of the new touchable.
                if (currentTouchableObjectDown != null)
                {
                    if (!IsObjectPartOfTouchable(currentTouchableObjectDown, newClosestTouchable))
                    {
                        TryRaisePokeUp();
                    }
                }

                // Set new touchable only now: If we have to raise a poke-up event for the previous touchable object,
                // we need to do so using the previous touchable in TryRaisePokeUp().
                closestProximityTouchable = newClosestTouchable;
            }
        }

        private static readonly ProfilerMarker FindClosestTouchableForLayerMaskPerfMarker = new ProfilerMarker("[MRTK] PokePointer.FindClosestTouchableForLayerMask");

        private bool FindClosestTouchableForLayerMask(LayerMask layerMask, out BaseNearInteractionTouchable closest, out float closestDistance, out Vector3 closestNormal)
        {
            using (FindClosestTouchableForLayerMaskPerfMarker.Auto())
            {
                closest = null;
                closestDistance = float.PositiveInfinity;
                closestNormal = Vector3.forward;

                int numColliders = UnityEngine.Physics.OverlapSphereNonAlloc(Position, touchableDistance, queryBuffer, layerMask, triggerInteraction);
                if (numColliders == queryBuffer.Length)
                {
                    Debug.LogWarning($"Maximum number of {numColliders} colliders found in PokePointer overlap query. Consider increasing the query buffer size in the input system settings.");
                }

                Camera mainCam = CameraCache.Main;
                for (int i = 0; i < numColliders; ++i)
                {
                    Collider collider = queryBuffer[i];
#if UNITY_2019_4_OR_NEWER
                    if (collider.TryGetComponent(out BaseNearInteractionTouchable touchable) && touchable != null)
#else
                    BaseNearInteractionTouchable touchable = collider.GetComponent<BaseNearInteractionTouchable>();
                    if (touchable != null)
#endif
                    {
                        if (IgnoreCollidersNotInFOV && !mainCam.IsInFOVCached(collider))
                        {
                            continue;
                        }
                        float distance = touchable.DistanceToTouchable(Position, out Vector3 normal); 
                        
                        // Favor touched volumes, but when there are multiple touched volumes, favor the one with the closest surface.
                        bool bothInside = (distance <= 0f) && (closestDistance <= 0f);
                        bool betterFit = bothInside ? Mathf.Abs(distance) < Mathf.Abs(closestDistance) : distance < closestDistance;
                        if (betterFit)
                        {
                            closest = touchable;
                            closestDistance = distance;
                            closestNormal = normal;
                        }
                    }
                }

                // Unity UI does not provide an equivalent broad-phase test to Physics.OverlapSphere,
                // so we have to use a static instances list to test all NearInteractionTouchableUnityUI
                for (int i = 0; i < NearInteractionTouchableUnityUI.Instances.Count; i++)
                {
                    NearInteractionTouchableUnityUI touchable = NearInteractionTouchableUnityUI.Instances[i];
                    if (touchable.gameObject.IsInLayerMask(layerMask))
                    {
                        float distance = touchable.DistanceToTouchable(Position, out Vector3 normal);
                        if (distance <= touchableDistance && distance < closestDistance)
                        {
                            closest = touchable;
                            closestDistance = distance;
                            closestNormal = normal;
                        }
                    }
                }

                return closest != null;
            }
        }

        private static readonly ProfilerMarker OnPostSceneQueryPerfMarker = new ProfilerMarker("[MRTK] PokePointer.OnPostSceneQuery");

        /// <inheritdoc />
        public override void OnPostSceneQuery()
        {
            using (OnPostSceneQueryPerfMarker.Auto())
            {
                base.OnPostSceneQuery();

                if (!IsActive)
                {
                    return;
                }

                if (Result?.CurrentPointerTarget != null && closestProximityTouchable != null)
                {
                    float distToTouchable;
                    if (closestProximityTouchable is NearInteractionTouchableVolume)
                    {
                        // Volumes can be arbitrary size, so don't rely on the length of the raycast ray
                        // instead just have the volume itself give us the distance.
                        distToTouchable = closestProximityTouchable.DistanceToTouchable(Position, out _);
                    }
                    else
                    {
                        // Start position of the ray is offset by TouchableDistance, subtract to get distance between surface and pointer position.
                        distToTouchable = Vector3.Distance(Result.StartPoint, Result.Details.Point) - touchableDistance;
                    }

                    bool newIsDown = (distToTouchable < 0.0f);
                    bool newIsUp = (distToTouchable > closestProximityTouchable.DebounceThreshold);

                    if (newIsDown)
                    {
                        TryRaisePokeDown();
                    }
                    else if (currentTouchableObjectDown != null)
                    {
                        if (newIsUp)
                        {
                            TryRaisePokeUp();
                        }
                        else
                        {
                            TryRaisePokeDown();
                        }
                    }
                }

                PreviousPosition = Position;
            }
        }

        private static readonly ProfilerMarker OnPreCurrentPointerTargetChangePerfMarker = new ProfilerMarker("[MRTK] PokePointer.OnPreCurrentPointerTargetChange");

        /// <inheritdoc />
        public override void OnPreCurrentPointerTargetChange()
        {
            using (OnPreCurrentPointerTargetChangePerfMarker.Auto())
            {
                // We need to raise the event now, since the pointer's focused object or touchable will change
                // after we leave this function. This will make sure the same object that received the Down event
                // will also receive the Up event.
                TryRaisePokeUp();
            }
        }

        private static readonly ProfilerMarker TryRaisePokeDownPerfMarker = new ProfilerMarker("[MRTK] PokePointer.TryRaisePokeDown");

        private void TryRaisePokeDown()
        {
            using (TryRaisePokeDownPerfMarker.Auto())
            {
                GameObject targetObject = Result.CurrentPointerTarget;

                if (currentTouchableObjectDown == null)
                {
                    // In order to get reliable up/down event behavior, only allow the closest touchable to be touched.
                    if (IsObjectPartOfTouchable(targetObject, closestProximityTouchable))
                    {
                        currentTouchableObjectDown = targetObject;

                        if (closestProximityTouchable.EventsToReceive == TouchableEventType.Pointer)
                        {
                            CoreServices.InputSystem?.RaisePointerDown(this, pointerAction, Handedness);
                        }
                        else if (closestProximityTouchable.EventsToReceive == TouchableEventType.Touch)
                        {
                            CoreServices.InputSystem?.RaiseOnTouchStarted(InputSourceParent, Controller, Handedness, Position);
                        }
                    }
                }
                else
                {
                    RaiseTouchUpdated(targetObject, Position);
                }
            }
        }

        private static readonly ProfilerMarker TryRaisePokeUpPerfMarker = new ProfilerMarker("[MRTK] PokePointer.TryRaisePokeUp");

        private void TryRaisePokeUp()
        {
            using (TryRaisePokeUpPerfMarker.Auto())
            {
                if (currentTouchableObjectDown != null)
                {
                    Debug.Assert(Result.CurrentPointerTarget == currentTouchableObjectDown, "PokeUp will not be raised for correct object.");

                    if (closestProximityTouchable.EventsToReceive == TouchableEventType.Pointer)
                    {
                        CoreServices.InputSystem.RaisePointerClicked(this, pointerAction, 0, Handedness);
                        CoreServices.InputSystem?.RaisePointerUp(this, pointerAction, Handedness);
                    }
                    else if (closestProximityTouchable.EventsToReceive == TouchableEventType.Touch)
                    {
                        CoreServices.InputSystem?.RaiseOnTouchCompleted(InputSourceParent, Controller, Handedness, Position);
                    }

                    currentTouchableObjectDown = null;
                }
            }
        }

        private static readonly ProfilerMarker RaiseTouchUpdatedPerfMarker = new ProfilerMarker("[MRTK] PokePointer.RaiseTouchUpdated");

        private void RaiseTouchUpdated(GameObject targetObject, Vector3 touchPosition)
        {
            using (RaiseTouchUpdatedPerfMarker.Auto())
            {
                if (currentTouchableObjectDown != null)
                {
                    Debug.Assert(Result?.CurrentPointerTarget == currentTouchableObjectDown);

                    if (closestProximityTouchable.EventsToReceive == TouchableEventType.Touch)
                    {
                        CoreServices.InputSystem?.RaiseOnTouchUpdated(InputSourceParent, Controller, Handedness, touchPosition);
                    }
                    else if (closestProximityTouchable.EventsToReceive == TouchableEventType.Pointer)
                    {
                        CoreServices.InputSystem?.RaisePointerDragged(this, pointerAction, Handedness, InputSourceParent);
                    }
                }
            }
        }

        private static readonly ProfilerMarker IsObjectPartOfTouchablePerfMarker = new ProfilerMarker("[MRTK] PokePointer.IsObjectPartOfTouchable");

        private static bool IsObjectPartOfTouchable(GameObject targetObject, BaseNearInteractionTouchable touchable)
        {
            using (IsObjectPartOfTouchablePerfMarker.Auto())
            {
                return targetObject != null && touchable != null &&
                    (targetObject == touchable.gameObject ||
                    // Descendant game objects are touchable as well. In particular, this is needed to be able to send
                    // touch events to Unity UI control elements.
                    (targetObject.transform != null && touchable.gameObject.transform != null &&
                    targetObject.transform.IsChildOf(touchable.gameObject.transform)));
            }
        }

        /// <inheritdoc />
        bool IMixedRealityNearPointer.TryGetNearGraspPoint(out Vector3 position)
        {
            position = Vector3.zero;
            return false;
        }

        /// <inheritdoc />
        bool IMixedRealityNearPointer.TryGetDistanceToNearestSurface(out float distance)
        {
            if (closestProximityTouchable != null)
            {
                distance = closestDistance;
                return true;
            }
            else
            {
                distance = 0.0f;
                return false;
            }
        }

        /// <inheritdoc />
        bool IMixedRealityNearPointer.TryGetNormalToNearestSurface(out Vector3 normal)
        {
            normal = closestNormal;
            return closestProximityTouchable != null;
        }

        private static readonly ProfilerMarker OnSourceLostPerfMarker = new ProfilerMarker("[MRTK] PokePointer.OnSourceLost");

        /// <inheritdoc />
        public override void OnSourceLost(SourceStateEventData eventData)
        {
            using (OnSourceLostPerfMarker.Auto())
            {
                TryRaisePokeUp();

                base.OnSourceLost(eventData);
            }
        }

        private static readonly ProfilerMarker OnSourceDetectedPerfMarker = new ProfilerMarker("[MRTK] PokePointer.OnSourceDetected");

        /// <inheritdoc />
        public override void OnSourceDetected(SourceStateEventData eventData)
        {
            using (OnSourceDetectedPerfMarker.Auto())
            {
                base.OnSourceDetected(eventData);
                PreviousPosition = Position;
            }
        }

        /// <inheritdoc />
        public override void OnSourcePoseChanged(SourcePoseEventData<MixedRealityPose> eventData)
        {
            base.OnSourcePoseChanged(eventData);

            if (SourcePoseDataUsable(eventData))
            {
                transform.position += sourcePoseOffset * transform.forward;
            }
        }

        /// <inheritdoc />
        public override void OnInputDown(InputEventData eventData)
        {
            // Poke pointer should not respond when a button is pressed or hand is pinched
            // It should only dispatch events based on collision with touchables.
        }

        /// <inheritdoc />
        public override void OnInputUp(InputEventData eventData)
        {
            // Poke pointer should not respond when a button is released or hand is un-pinched
            // It should only dispatch events based on collision with touchables.
        }

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

            IsTargetPositionLockedOnFocusLock = false;
        }

        private void OnDrawGizmos()
        {
            if (!IsNearObject)
            {
                return;
            }
            else
            {
                Gizmos.color = Color.green;
            }

            if (closestProximityTouchable != null)
            {
                Gizmos.DrawLine(Position, closestProximityTouchable.transform.position);
            }
        }
    }
}
