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

using Microsoft.MixedReality.Toolkit.Input;
using Microsoft.MixedReality.Toolkit.Utilities;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;

namespace Microsoft.MixedReality.Toolkit.UI
{
    [AddComponentMenu("Scripts/MRTK/SDK/HandInteractionPanZoom")]
    public class HandInteractionPanZoom :
        BaseFocusHandler, IMixedRealityTouchHandler, IMixedRealityPointerHandler, IMixedRealitySourceStateHandler
    {
        /// <summary>
        /// Internal data stored for each hand or pointer.
        /// </summary>
        protected class HandPanData
        {
            public bool IsActive = true;
            public bool IsSourceNear = false;
            public Vector2 uvOffset = Vector2.zero;
            public Vector2 touchingQuadCoord = Vector2.zero;
            public Vector2 uvTotalOffset = Vector2.zero;
            public Vector3 touchingPoint = Vector3.zero;
            public Vector3 touchingPointSmoothed = Vector3.zero;
            public Vector3 touchingInitialPt = Vector3.zero;
            public Vector3 touchingRayOffset = Vector3.zero;
            public Vector2 touchingInitialUV = Vector2.zero;
            public Vector2 touchingUVOffset = Vector2.zero;
            public Vector2 touchingUVTotalOffset = Vector2.zero;
            public Vector3 initialProjectedOffset = Vector3.zero;
            public IMixedRealityInputSource touchingSource = null;
            public IMixedRealityController currentController = null;
            public IMixedRealityPointer currentPointer = null;
        }

        #region Serialized Fields

        [SerializeField]
        [FormerlySerializedAs("enabled")]
        private bool isEnabled = true;
        /// <summary>
        /// This Property sets and gets whether a the pan/zoom behavior is active.
        /// </summary>
        public bool Enabled { get => isEnabled; set => isEnabled = value; }

        [Header("Behavior")]
        [SerializeField]
        private bool enableZoom = false;

        [SerializeField]
        private bool lockHorizontal = false;

        [SerializeField]
        private bool lockVertical = false;

        [SerializeField]
        [Tooltip("If this is checked, Max Pan Horizontal and Max Pan Vertical are ignored.")]
        private bool unlimitedPan = true;

        [SerializeField]
        [Range(1.0f, 20.0f)]
        private float maxPanHorizontal = 2;

        [SerializeField]
        [Range(1.0f, 20.0f)]
        private float maxPanVertical = 2;

        [SerializeField]
        [Range(0.1f, 1.0f)]
        private float minScale = 0.2f;

        [SerializeField]
        [Range(1.0f, 10.0f)]
        private float maxScale = 1.5f;

        [SerializeField]
        [Range(0.0f, 0.99f)]
        [Tooltip("a value of 0 results in panning coming to a complete stop when released.")]
        private float momentumHorizontal = 0.9f;

        [SerializeField]
        [Tooltip("a value of 0 results in panning coming to a complete stop when released.")]
        [Range(0.0f, 0.99f)]
        private float momentumVertical = 0.9f;

        [SerializeField]
        [Range(0.0f, 99.0f)]
        private float panZoomSmoothing = 80.0f;

        [Header("Visual affordance")]
        [SerializeField]
        [Tooltip("If affordance geometry is desired to emphasize the touch points(leftPoint and rightPoint) and the center point between them (reticle), assign them here.")]
        [FormerlySerializedAs("reticle")]
        private GameObject centerPoint = null;

        [SerializeField]
        private GameObject leftPoint = null;

        [SerializeField]
        private GameObject rightPoint = null;

        [Tooltip("When the slate is touched, what color to change on the ProximityLight center color override to. (Assumes the target material uses a proximity light and proximity light color override)")]
        [SerializeField]
        private Color proximityLightCenterColor = new Color(0.25f, 0.25f, 0.25f, 0.0f);

        [SerializeField]
        [Tooltip("Current scale value. 1 is the original 100%.")]
        private float currentScale;

        /// <summary>
        /// Current scale value. 1 is the original 100%.
        /// </summary>
        public float CurrentScale => currentScale;

        /// <summary>
        /// Returns the current pan delta (pan value - previous pan value)
        /// in UV coordinates (0 being no pan, 1 being pan of the entire slate)
        /// </summary>
        public Vector2 CurrentPanDelta => totalUVOffset;

        [Header("Events")]
        public PanUnityEvent PanStarted = new PanUnityEvent();
        public PanUnityEvent PanStopped = new PanUnityEvent();
        public PanUnityEvent PanUpdated = new PanUnityEvent();

        #endregion Serialized Fields

        #region Private Properties

        private Mesh mesh;
        private MeshFilter meshFilter;
        private BoxCollider boxCollider;

        private bool TouchActive => handDataMap.Count > 0;
        private bool ScaleActive => enableZoom && handDataMap.Count > 1;

        private float previousContactRatio = 1.0f;
        private float initialTouchDistance = 0.0f;
        private Vector2 totalUVOffset = Vector2.zero;
        private Vector2 totalUVScale = Vector2.one;
        private bool affordancesVisible = false;
        private float runningAverageSmoothing = 0.0f;
        private const float percentToDecimal = 0.01f;
        private Material currentMaterial;
        private int proximityLightCenterColorID;
        private Color defaultProximityLightCenterColor;
        private List<Vector2> unTransformedUVs = new List<Vector2>();
        private Dictionary<uint, HandPanData> handDataMap = new Dictionary<uint, HandPanData>();
        private List<Vector2> uvs = new List<Vector2>();
        private List<Vector2> uvsOrig = new List<Vector2>();
        private List<Vector2> uvDeltas = new List<Vector2>();
        private bool oldIsTargetPositionLockedOnFocusLock;

#if UNITY_2019_3_OR_NEWER
        // Quad meshes by default (in 2019 and higher) appear to follow the vertex order
        // specified here: https://docs.unity3d.com/Manual/Example-CreatingaBillboardPlane.html
        // That is, LowerLeft->LowerRight->UpperLeft->UpperRight
        // Note that even though the example on that page is one that creates a quad manually
        // using a specific order of vertices, this order seems to be what a quad mesh defaults to.
        // This was discovered when looking into an issue on the SlateTests, which depend on the
        // projection math within this to be using the correct right and up vectors.
        private const int UpperLeftQuadIndex = 2;
        private const int UpperRightQuadIndex = 3;
        private const int LowerLeftQuadIndex = 0;
#else // !UNITY_2019_3_OR_NEWER
        // Quad meshes in 2018 and lower appear to follow a vertex order that looks like this:
        // [0] "(-0.5, -0.5, 0.0)"
        // [1] "(0.5, 0.5, 0.0)"
        // [2] "(0.5, -0.5, 0.0)"
        // [3] "(-0.5, 0.5, 0.0)"
        // That is, LowerLeft->UpperRight->LowerRight->UpperLeft
        // Note that the ifdefs only cover +/- 2019.3 because that was the min tested version
        // for Unity 2019 - this could very well be needed for 2019.2 and 2019.1, but with 2019.4
        // out at this point, support is mainly on the LTS release.
        private const int UpperLeftQuadIndex = 3;
        private const int UpperRightQuadIndex = 1;
        private const int LowerLeftQuadIndex = 0;
#endif

        #endregion Private Properties

        /// <summary>
        /// This function sets the pan and zoom back to their starting settings.
        /// </summary>
        public void Reset()
        {
            mesh.SetUVs(0, unTransformedUVs);
            totalUVOffset = Vector2.zero;
            totalUVScale = Vector2.one;
            initialTouchDistance = 0.0f;
        }

        #region MonoBehaviour Handlers

        private void Awake()
        {
            Initialize();
        }

        private void Update()
        {
            if (isEnabled)
            {
                if (TouchActive)
                {
                    foreach (uint key in handDataMap.Keys)
                    {
                        if (UpdateHandTouchingPoint(key))
                        {
                            MoveTouch(key);
                        }
                    }

                    totalUVOffset = GetUvOffset();
                }

                UpdateIdle();
                UpdateUVMapping();

                if (!TouchActive && affordancesVisible)
                {
                    SetAffordancesActive(false);
                }

                if (affordancesVisible)
                {
                    if (centerPoint != null)
                    {
                        centerPoint.transform.position = GetContactCenter();
                    }
                    if (leftPoint != null)
                    {
                        leftPoint.transform.position = GetContactForHand(Handedness.Left);
                    }
                    if (rightPoint != null)
                    {
                        rightPoint.transform.position = GetContactForHand(Handedness.Right);
                    }
                }
            }
        }

        #endregion MonoBehaviour Handlers

        #region Private Methods

        private bool TryGetMRControllerRayPoint(HandPanData data, out Vector3 rayPoint)
        {

            if (data.currentPointer != null && data.currentController != null && data.currentController.IsPositionAvailable)
            {
                rayPoint = data.touchingInitialPt + (SnapFingerToQuad(data.currentPointer.Position) - data.initialProjectedOffset);
                return true;
            }

            rayPoint = Vector3.zero;
            return false;
        }

        private bool UpdateHandTouchingPoint(uint sourceId)
        {
            Vector3 tryHandPoint = Vector3.zero;
            bool tryGetSucceeded = false;
            if (handDataMap.ContainsKey(sourceId))
            {
                HandPanData data = handDataMap[sourceId];

                if (data.IsActive)
                {
                    if (data.IsSourceNear)
                    {
                        tryGetSucceeded = TryGetHandPositionFromController(data.currentController, TrackedHandJoint.IndexTip, out tryHandPoint);
                    }
                    else
                    {
                        tryGetSucceeded = TryGetHandPositionFromController(data.currentController, TrackedHandJoint.Palm, out tryHandPoint);
                    }
                    if (!tryGetSucceeded)
                    {
                        tryGetSucceeded = TryGetMRControllerRayPoint(data, out tryHandPoint);
                    }

                    if (tryGetSucceeded)
                    {
                        tryHandPoint = SnapFingerToQuad(tryHandPoint);
                        Vector3 unfilteredTouchPt = data.IsSourceNear ? tryHandPoint : tryHandPoint + data.touchingRayOffset;
                        runningAverageSmoothing = panZoomSmoothing * percentToDecimal;
                        unfilteredTouchPt *= (1.0f - runningAverageSmoothing);
                        data.touchingPointSmoothed = (data.touchingPointSmoothed * runningAverageSmoothing) + unfilteredTouchPt;
                        data.touchingPoint = data.touchingPointSmoothed;
                    }
                }
            }

            return true;
        }

        private bool TryGetHandRayPoint(IMixedRealityController controller, out Vector3 handRayPoint)
        {
            if (controller != null &&
                 controller.InputSource != null &&
                 controller.InputSource.Pointers != null &&
                 controller.InputSource.Pointers.Length > 0 &&
                 controller.InputSource.Pointers[0].Result != null)
            {
                handRayPoint = controller.InputSource.Pointers[0].Result.Details.Point;
                return true;
            }

            handRayPoint = Vector3.zero;
            return false;
        }

        private void Initialize()
        {
            SetAffordancesActive(false);

            // Check for boxcollider
            boxCollider = GetComponent<BoxCollider>();
            if (boxCollider == null)
            {
                Debug.Log("The GameObject that runs this script must have a BoxCollider attached.");
            }
            else
            {
                Renderer renderer = this.GetComponent<Renderer>();
                Material material = (renderer != null) ? renderer.material : null;
                if ((material != null) && (material.mainTexture != null))
                {
                    material.mainTexture.wrapMode = TextureWrapMode.Repeat;
                }
            }

            // Get material
            currentMaterial = this.gameObject.GetComponent<Renderer>().material;
            proximityLightCenterColorID = Shader.PropertyToID("_ProximityLightCenterColorOverride");
            bool materialValid = currentMaterial != null && currentMaterial.HasProperty(proximityLightCenterColorID);
            defaultProximityLightCenterColor = materialValid ?
                currentMaterial.GetColor(proximityLightCenterColorID) :
                new Color(0.0f, 0.0f, 0.0f, 0.0f);

            // Precache references
            meshFilter = gameObject.GetComponent<MeshFilter>();
            if (meshFilter == null)
            {
                Debug.Log("The GameObject: " + this.gameObject.name + " " + "does not have a Mesh component.");
            }
            else
            {
                mesh = meshFilter.mesh;
            }

            mesh.GetUVs(0, unTransformedUVs);
        }

        private void UpdateIdle()
        {
            if (!TouchActive)
            {
                if (Mathf.Abs(totalUVOffset.x) < 0.01f && Mathf.Abs(totalUVOffset.y) < 0.01f)
                {
                    totalUVOffset = Vector2.zero;
                }
                else
                {
                    totalUVOffset = new Vector2(totalUVOffset.x * momentumHorizontal, totalUVOffset.y * momentumVertical);
                    RaisePanning(0);
                }
            }
        }

        private void UpdateUVMapping()
        {
            mesh.GetUVs(0, uvs);
            uvsOrig.Clear();
            uvsOrig.AddRange(uvs);

            Vector2 offsetUVDelta = new Vector2(-totalUVOffset.x, totalUVOffset.y);

            // Scale
            if (ScaleActive)
            {
                var scaleUVCentroid = GetDisplayedUVCentroid(uvs);
                var currentContactRatio = GetUVScaleFromTouches();
                var scaleUVDelta = currentContactRatio / previousContactRatio;
                previousContactRatio = currentContactRatio;

                currentScale = totalUVScale.x / scaleUVDelta;

                // Test for scale limits
                if (currentScale > minScale && currentScale < maxScale)
                {
                    uvDeltas.Clear();
                    for (int i = 0; i < uvs.Count; i++)
                    {
                        Vector2 adjustedScaleUVDelta = ((uvs[i] - scaleUVCentroid) / scaleUVDelta) + scaleUVCentroid - uvs[i];
                        uvDeltas.Add(adjustedScaleUVDelta + offsetUVDelta);
                    }
                    UpdateUV(uvs, uvDeltas);

                    Vector2 upperLeft = uvs[UpperLeftQuadIndex];
                    Vector2 upperRight = uvs[UpperRightQuadIndex];
                    Vector2 lowerLeft = uvs[LowerLeftQuadIndex];
                    totalUVScale.x = upperRight.x - upperLeft.x;
                    totalUVScale.y = upperLeft.y - lowerLeft.y;
                }
            }
            else
            {
                // Scroll
                UpdateUVWithScroll(uvs, offsetUVDelta);
            }

            mesh.SetUVs(0, uvs);
        }

        private void UpdateUVWithScroll(List<Vector2> uvs, Vector2 uvDelta)
        {
            uvDeltas.Clear();
            for (int i = 0; i < uvs.Count; i++)
            {
                uvDeltas.Add(uvDelta);
            }
            UpdateUV(uvs, uvDeltas, true);
        }

        private void UpdateUV(List<Vector2> uvs, List<Vector2> uvDeltas, bool scrollOnly = false)
        {
            Vector2 tiling = currentMaterial != null ? currentMaterial.mainTextureScale : new Vector2(1.0f, 1.0f);

            if (!unlimitedPan)
            {
                bool xLimited = false;
                bool yLimited = false;

                for (int i = 0; i < uvs.Count; i++)
                {
                    var uvTestValue = uvs[i] + uvDeltas[i];
                    if (uvTestValue.x > tiling.x * maxPanHorizontal || uvTestValue.x < -(tiling.x * maxPanHorizontal))
                    {
                        xLimited = true;
                    }
                    if (uvTestValue.y > tiling.y * maxPanVertical || uvTestValue.y < -(tiling.y * maxPanVertical))
                    {
                        yLimited = true;
                    }
                }

                if (scrollOnly)
                {
                    for (int i = 0; i < uvs.Count; ++i)
                    {
                        uvs[i] = new Vector2(xLimited ? uvs[i].x : uvs[i].x + uvDeltas[i].x, yLimited ? uvs[i].y : uvs[i].y + uvDeltas[i].y);
                    }
                }
                else if (!xLimited && !yLimited)
                {
                    for (int i = 0; i < uvs.Count; ++i)
                    {
                        uvs[i] += uvDeltas[i];
                    }
                }
            }
            else
            {
                for (int i = 0; i < uvs.Count; ++i)
                {
                    uvs[i] += uvDeltas[i];
                }
            }
        }

        private float GetUVScaleFromTouches()
        {
            if (!ScaleActive || initialTouchDistance == 0)
            {
                return 0.0f;
            }

            float uvScaleFromTouches = GetContactDistance() / initialTouchDistance;
            return uvScaleFromTouches;
        }

        private void UpdateTouchUVOffset(uint sourceId)
        {
            HandPanData data = handDataMap[sourceId];
            Vector2 currentQuadCoord = GetQuadCoordFromPoint(data.touchingPoint);
            data.uvOffset = currentQuadCoord - data.touchingQuadCoord;
            data.touchingQuadCoord = currentQuadCoord;
        }

        private Vector2 GetUvOffset()
        {
            if (TouchActive && AreSourcesCompatible())
            {
                Vector2 offset = Vector2.zero;
                foreach (uint key in handDataMap.Keys)
                {
                    offset += handDataMap[key].uvOffset;
                }
                offset /= (float)handDataMap.Count;
                return offset;
            }
            return totalUVOffset;
        }

        private Vector3 GetContactCenter()
        {
            Vector3 center = Vector3.zero;

            if (handDataMap.Keys.Count > 0)
            {
                foreach (uint key in handDataMap.Keys)
                {
                    center += handDataMap[key].touchingPoint;
                }

                center /= (float)handDataMap.Keys.Count;
            }

            return center;
        }

        private void SetAffordancesActive(bool active)
        {
            affordancesVisible = active;
            if (centerPoint != null)
            {
                centerPoint.SetActive(affordancesVisible);
            }
            if (leftPoint != null)
            {
                leftPoint.SetActive(affordancesVisible);
            }
            if (rightPoint != null)
            {
                rightPoint.SetActive(affordancesVisible);
            }

            if (currentMaterial != null)
            {
                currentMaterial.SetColor(proximityLightCenterColorID, active ? proximityLightCenterColor : defaultProximityLightCenterColor);
            }
        }

        private Vector3 GetContactForHand(Handedness hand)
        {
            Vector3 handPoint = Vector3.zero;
            if (handDataMap.Keys.Count > 0)
            {
                foreach (uint key in handDataMap.Keys)
                {
                    if (handDataMap[key].currentController.ControllerHandedness == hand)
                    {
                        return handDataMap[key].touchingPoint;
                    }
                }
            }

            return handPoint;
        }

        private bool AreSourcesCompatible()
        {
            int score = 0;
            foreach (uint key in handDataMap.Keys)
            {
                score += handDataMap[key].IsSourceNear ? 1 : 0;
            }
            return (score == 0 || score == handDataMap.Keys.Count);
        }

        private bool AreSourcesNear()
        {
            foreach (uint key in handDataMap.Keys)
            {
                if (handDataMap[key].IsSourceNear == false)
                {
                    return false;
                }
            }
            return true;
        }

        private Vector3 GetTouchPoint()
        {
            if (TouchActive)
            {
                Vector3 touchingPoint = Vector3.zero;
                foreach (uint key in handDataMap.Keys)
                {
                    touchingPoint += handDataMap[key].touchingPoint;
                }
                touchingPoint /= (float)handDataMap.Count;
                return touchingPoint;
            }
            return Vector3.zero;
        }

        private Vector2 GetScaleUVCentroid()
        {
            return GetUVFromPoint(GetTouchPoint());
        }

        private Vector2 GetDisplayedUVCentroid(List<Vector2> uvs)
        {
            Vector2 centroid = Vector2.zero;
            for (int i = 0; i < uvs.Count; ++i)
            {
                centroid += uvs[i];
            }

            return centroid /= (float)uvs.Count;
        }

        private float GetContactDistance()
        {
            if (!ScaleActive || handDataMap.Keys.Count < 2)
            {
                return 0.0f;
            }

            int index = 0;
            Vector3 a = Vector3.zero;
            Vector3 b = Vector3.zero;
            foreach (uint key in handDataMap.Keys)
            {
                if (index == 0)
                {
                    a = handDataMap[key].touchingPoint;
                }
                else if (index == 1)
                {
                    b = handDataMap[key].touchingPoint;
                }

                index++;
            }

            return (b - a).magnitude;
        }

        private Vector2 GetQuadCoordFromPoint(Vector3 point)
        {
            Vector2 quadCoord = GetQuadCoord(point);
            quadCoord = new Vector2(lockHorizontal ? 0.0f : quadCoord.x, lockVertical ? 0.0f : quadCoord.y);
            return quadCoord;
        }

        private Vector2 GetUVFromQuadCoord(Vector2 coord)
        {
            Vector2 uvCoord = Vector2.zero;
            Vector2[] uvs = mesh.uv;
            Vector2 upperLeft = uvs[UpperLeftQuadIndex];
            Vector2 upperRight = uvs[UpperRightQuadIndex];
            Vector2 lowerLeft = uvs[LowerLeftQuadIndex];

            float magVertical = (lowerLeft - upperLeft).magnitude;
            float magHorizontal = (upperRight - upperLeft).magnitude;

            if (!Mathf.Approximately(0, magVertical) && !Mathf.Approximately(0, magHorizontal))
            {
                // Get coord projection on uv coordinates then divide by length to get quad coord 0 to 1
                uvCoord.x = Vector2.Dot(coord - upperLeft, upperRight - upperLeft) / (magHorizontal * magHorizontal);
                uvCoord.y = Vector2.Dot(coord - upperLeft, lowerLeft - upperLeft) / (magVertical * magVertical);
            }

            return uvCoord;
        }

        private Vector2 GetUVFromPoint(Vector3 point)
        {
            Vector2 quadCoord = GetQuadCoordFromPoint(point);
            return GetUVFromQuadCoord(quadCoord);
        }

        private Vector2 GetQuadCoord(Vector3 point)
        {
            Vector2 quadCoord = Vector2.zero;
            Vector3[] vertices = mesh.vertices;
            Vector3 upperLeft = transform.TransformPoint(vertices[UpperLeftQuadIndex]);
            Vector3 upperRight = transform.TransformPoint(vertices[UpperRightQuadIndex]);
            Vector3 lowerLeft = transform.TransformPoint(vertices[LowerLeftQuadIndex]);

            float magVertical = (lowerLeft - upperLeft).magnitude;
            float magHorizontal = (upperRight - upperLeft).magnitude;

            if (!Mathf.Approximately(0, magVertical) && !Mathf.Approximately(0, magHorizontal))
            {
                // Get point projection on vertices coordinates then divide by length to get quad coord 0 to 1
                quadCoord.x = Vector3.Dot(point - upperLeft, upperRight - upperLeft) / (magHorizontal * magHorizontal);
                quadCoord.y = Vector3.Dot(point - upperLeft, lowerLeft - upperLeft) / (magVertical * magVertical);
            }

            return quadCoord;
        }

        private Vector3 SnapFingerToQuad(Vector3 pointToSnap)
        {
            Vector3 planePoint = this.transform.TransformPoint(mesh.vertices[0]);
            Vector3 planeNormal = gameObject.transform.forward;

            return Vector3.ProjectOnPlane(pointToSnap - planePoint, planeNormal) + planePoint;
        }

        private void SetHandDataFromController(IMixedRealityController controller, IMixedRealityPointer pointer, bool isNear)
        {
            HandPanData data = new HandPanData();
            data.IsSourceNear = isNear;
            data.IsActive = true;
            data.touchingSource = controller.InputSource;
            data.currentController = controller;
            data.currentPointer = pointer;

            if (isNear)
            {
                if (TryGetHandPositionFromController(data.currentController, TrackedHandJoint.IndexTip, out Vector3 touchPosition))
                {
                    data.touchingInitialPt = SnapFingerToQuad(touchPosition);
                    data.touchingPointSmoothed = data.touchingInitialPt;
                    data.touchingPoint = data.touchingInitialPt;
                }
            }
            else // Is far
            {
                if (data.currentPointer is GGVPointer)
                {
                    data.touchingInitialPt = SnapFingerToQuad(data.currentPointer.Position);
                    data.touchingPoint = data.touchingInitialPt;
                    data.touchingPointSmoothed = data.touchingInitialPt;
                }
                else if (TryGetHandRayPoint(controller, out Vector3 handRayPt))
                {
                    data.touchingInitialPt = SnapFingerToQuad(handRayPt);
                    data.touchingPoint = data.touchingInitialPt;
                    data.touchingPointSmoothed = data.touchingInitialPt;
                    if (TryGetHandPositionFromController(data.currentController, TrackedHandJoint.Palm, out Vector3 touchPosition))
                    {
                        data.touchingRayOffset = handRayPt - SnapFingerToQuad(touchPosition);
                    }
                }
            }

            // Store value in case of MRController
            if (data.currentPointer != null)
            {
                Vector3 pt = data.currentPointer.Position;
                data.initialProjectedOffset = SnapFingerToQuad(pt);
            }

            data.touchingQuadCoord = GetUVFromPoint(data.touchingPoint);
            data.touchingInitialUV = data.touchingQuadCoord;
            data.touchingUVTotalOffset = totalUVOffset;
            data.touchingUVOffset = data.touchingUVTotalOffset;
            handDataMap.Add(data.touchingSource.SourceId, data);
            initialTouchDistance = GetContactDistance();
            totalUVOffset = Vector2.zero;

            if (handDataMap.Keys.Count > 1)
            {
                if (initialTouchDistance == 0)
                {
                    initialTouchDistance = GetContactDistance();
                }
                else
                {
                    float contactDist = GetContactDistance();
                    initialTouchDistance = contactDist + (initialTouchDistance - contactDist);
                }
                previousContactRatio = 1.0f;
            }

            SetAffordancesActive(isNear);

            StartTouch(data.touchingSource.SourceId);
        }

        private bool TryGetHandPositionFromController(IMixedRealityController controller, TrackedHandJoint joint, out Vector3 position)
        {
            if (controller is IMixedRealityHand hand)
            {
                if (hand.TryGetJoint(joint, out MixedRealityPose pose))
                {
                    position = pose.Position;
                    return true;
                }
            }

            position = Vector3.zero;
            return false;
        }

        #endregion Private Methods

        #region Internal State Handlers

        private void StartTouch(uint sourceId)
        {
            UpdateTouchUVOffset(sourceId);
            RaisePanStarted(sourceId);
        }

        private void EndTouch(uint sourceId)
        {
            if (handDataMap.ContainsKey(sourceId))
            {
                handDataMap.Remove(sourceId);
                RaisePanEnded(0);
            }
        }

        private void EndAllTouches()
        {
            if (handDataMap.Count > 0)
            {
                handDataMap.Clear();
                RaisePanEnded(0);
            }
        }

        private void MoveTouch(uint sourceId)
        {
            UpdateTouchUVOffset(sourceId);
            RaisePanning(sourceId);
        }

        #endregion Internal State Handlers

        #region Fire Events to Listening Objects

        private void RaisePanStarted(uint sourceId)
        {
            HandPanEventData eventData = new HandPanEventData();
            eventData.PanDelta = GetUvOffset();
            PanStarted?.Invoke(eventData);
        }

        private void RaisePanEnded(uint sourceId)
        {
            HandPanEventData eventData = new HandPanEventData();
            eventData.PanDelta = Vector2.zero;
            PanStopped?.Invoke(eventData);
        }

        private void RaisePanning(uint sourceId)
        {
            HandPanEventData eventData = new HandPanEventData();
            eventData.PanDelta = GetUvOffset();
            PanUpdated?.Invoke(eventData);
        }

        #endregion Fire Events to Listening Objects

        #region BaseFocusHandler Methods

        /// <inheritdoc />
        public override void OnFocusEnter(FocusEventData eventData) { }

        /// <inheritdoc />
        public override void OnFocusExit(FocusEventData eventData)
        {
            EndAllTouches();
        }

        #endregion

        #region IMixedRealityTouchHandler
        /// <summary>
        /// In order to receive Touch Events from the IMixedRealityTouchHandler
        /// remember to add a NearInteractionTouchable script to the object that has this script.
        /// </summary>
        public void OnTouchStarted(HandTrackingInputEventData eventData)
        {
            EndTouch(eventData.SourceId);
            SetHandDataFromController(eventData.Controller, null, true);
            eventData.Use();
        }

        public void OnTouchCompleted(HandTrackingInputEventData eventData)
        {
            EndTouch(eventData.SourceId);
            eventData.Use();
        }

        public void OnTouchUpdated(HandTrackingInputEventData eventData) { }

        #endregion IMixedRealityTouchHandler

        #region IMixedRealityInputHandler Methods

        /// <summary>
        /// The Input Event handlers receive Hand Ray events.
        /// </summary>
        public void OnPointerDown(MixedRealityPointerEventData eventData)
        {
            bool isNear = eventData.Pointer is IMixedRealityNearPointer;
            oldIsTargetPositionLockedOnFocusLock = eventData.Pointer.IsTargetPositionLockedOnFocusLock;
            if (!isNear && eventData.Pointer.Controller.IsRotationAvailable)
            {
                eventData.Pointer.IsTargetPositionLockedOnFocusLock = false;
            }
            SetAffordancesActive(false);
            EndTouch(eventData.SourceId);
            SetHandDataFromController(eventData.Pointer.Controller, eventData.Pointer, isNear);
            eventData.Use();
        }

        public void OnPointerUp(MixedRealityPointerEventData eventData)
        {
            eventData.Pointer.IsTargetPositionLockedOnFocusLock = oldIsTargetPositionLockedOnFocusLock;
            EndTouch(eventData.SourceId);
            eventData.Use();
        }

        #endregion IMixedRealityInputHandler Methods

        #region IMixedRealitySourceStateHandler Methods
        public void OnSourceLost(SourceStateEventData eventData)
        {
            EndTouch(eventData.SourceId);
            eventData.Use();
        }

        #endregion IMixedRealitySourceStateHandler Methods

        #region Unused Methods

        public void OnSourceDetected(SourceStateEventData eventData) { }

        public void OnPointerDragged(MixedRealityPointerEventData eventData) { }

        public void OnPointerClicked(MixedRealityPointerEventData eventData) { }

        #endregion Unused Methods
    }
}
