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

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

namespace Microsoft.MixedReality.Toolkit.UI
{
    /// <summary>
    /// A scrollable frame where content scroll is triggered by manual controller click and drag or according to pagination settings.
    /// </summary>
    /// <remarks>Executing also in edit mode to properly catch and mask any new content added to scroll container.</remarks>
    [ExecuteAlways]
    [AddComponentMenu("Scripts/MRTK/SDK/ScrollingObjectCollection")]
    public class ScrollingObjectCollection : MonoBehaviour,
        IMixedRealityPointerHandler,
        IMixedRealitySourceStateHandler,
        IMixedRealityTouchHandler
    {
        /// <summary>
        /// How velocity is applied to a <see cref="ScrollingObjectCollection"/> when a scroll is released.
        /// </summary>
        public enum VelocityType
        {
            FalloffPerFrame = 0,
            FalloffPerItem,
            NoVelocitySnapToItem,
            None
        }

        /// <summary>
        /// The direction in which a <see cref="ScrollingObjectCollection"/> can scroll.
        /// </summary>
        public enum ScrollDirectionType
        {
            UpAndDown = 0,
            LeftAndRight,
        }

        [SerializeField]
        [Tooltip("Enables/disables scrolling with near/far interaction.")]
        private bool canScroll = true;

        /// <summary>
        /// Enables/disables scrolling with near/far interaction.
        /// </summary>
        /// <remarks>Helpful for controls where you may want pagination or list movement without freeform scrolling.</remarks>
        public bool CanScroll
        {
            get { return canScroll; }
            set { canScroll = value; }
        }

        /// <summary>
        /// Edit modes for defining scroll viewable area and scroll interaction boundaries.
        /// </summary>
        public enum EditMode
        {
            Auto = 0, // Use pagination values
            Manual, // Use direct manipulation of the object
        }

        [SerializeField]
        [Tooltip("Edit modes for defining the clipping box masking boundaries. Choose 'Auto' to automatically use pagination values. Choose 'Manual' for enabling direct manipulation of the clipping box object.")]
        private EditMode maskEditMode;

        /// <summary>
        /// Edit modes for defining the clipping box masking boundaries. Choose 'Auto' to automatically use pagination values. Choose 'Manual' for enabling direct manipulation of the clipping box object.
        /// </summary>
        public EditMode MaskEditMode
        {
            get { return maskEditMode; }
            set { maskEditMode = value; }
        }

        [SerializeField]
        [Tooltip("Edit modes for defining the scroll interaction collider boundaries. Choose 'Auto' to automatically use pagination values. Choose 'Manual' for enabling direct manipulation of the collider.")]
        private EditMode colliderEditMode;

        /// <summary>
        /// Edit modes for defining the scroll interaction collider boundaries. Choose 'Auto' to automatically use pagination values. Choose 'Manual' for enabling direct manipulation of the collider.
        /// </summary>
        public EditMode ColliderEditMode
        {
            get { return colliderEditMode; }
            set { colliderEditMode = value; }
        }

        [SerializeField]
        private bool maskEnabled = true;

        /// <summary>
        /// Visibility mode of scroll content. Default value will mask all objects outside of the scroll viewable area.
        /// </summary>
        public bool MaskEnabled
        {
            get { return maskEnabled; }
            set
            {
                if (!value && value != wasMaskEnabled)
                {
                    RestoreContentVisibility();
                }
                wasMaskEnabled = value;
                maskEnabled = value;
            }
        }

        // Helps catching any changes on the mask enabled value made from the inspector.
        // With the custom editor, the mask enabled field is changed before mask enabled setter is called.
        private bool wasMaskEnabled = true;

        [SerializeField]
        [Tooltip("The distance, in meters, the current pointer can travel along the scroll direction before triggering a scroll drag.")]
        [Range(0.0f, 0.2f)]
        private float handDeltaScrollThreshold = 0.02f;

        /// <summary>
        /// The distance, in meters, the current pointer can travel along the scroll direction before triggering a scroll drag.
        /// </summary>
        public float HandDeltaScrollThreshold
        {
            get { return handDeltaScrollThreshold; }
            set { handDeltaScrollThreshold = value; }
        }

        [SerializeField]
        [Tooltip("Withdraw amount, in meters, from the front of the scroll boundary needed to transition from touch engaged to released.")]
        private float releaseThresholdFront = 0.03f;
        /// <summary>
        /// Withdraw amount, in meters, from the front of the scroll boundary needed to transition from touch engaged to released.
        /// </summary>
        public float ReleaseThresholdFront
        {
            get { return releaseThresholdFront; }
            set { releaseThresholdFront = value; }
        }

        [SerializeField]
        [Tooltip("Withdraw amount, in meters, from the back of the scroll boundary needed to transition from touch engaged to released.")]
        private float releaseThresholdBack = 0.20f;
        /// <summary>
        /// Withdraw amount, in meters, from the back of the scroll boundary needed to transition from touch engaged to released.
        /// </summary>
        public float ReleaseThresholdBack
        {
            get { return releaseThresholdBack; }
            set { releaseThresholdBack = value; }
        }

        [SerializeField]
        [Tooltip("Withdraw amount, in meters, from the right or left of the scroll boundary needed to transition from touch engaged to released.")]
        private float releaseThresholdLeftRight = 0.20f;
        /// <summary>
        /// Withdraw amount, in meters, from the right or left of the scroll boundary needed to transition from touch engaged to released.
        /// </summary>
        public float ReleaseThresholdLeftRight
        {
            get { return releaseThresholdLeftRight; }
            set { releaseThresholdLeftRight = value; }
        }

        [SerializeField]
        [Tooltip("Withdraw amount, in meters, from the top or bottom of the scroll boundary needed to transition from touch engaged to released.")]
        private float releaseThresholdTopBottom = 0.20f;
        /// <summary>
        /// Withdraw amount, in meters, from the top or bottom of the scroll boundary needed to transition from touch engaged to released.
        /// </summary>
        public float ReleaseThresholdTopBottom
        {
            get { return releaseThresholdTopBottom; }
            set { releaseThresholdTopBottom = value; }
        }

        [SerializeField]
        [Tooltip("Distance, in meters, to position a local xy plane used to verify if a touch interaction started in the front of the scroll view.")]
        [Range(0.0f, 0.05f)]
        private float frontTouchDistance = 0.005f;
        /// <summary>
        /// Distance, in meters, to position a local xy plane used to verify if a touch interaction started in the front of the scroll view.
        /// </summary>
        public float FrontTouchDistance
        {
            get { return frontTouchDistance; }
            set { frontTouchDistance = value; }
        }

        [SerializeField]
        [Tooltip("The direction in which content should scroll.")]
        private ScrollDirectionType scrollDirection;

        /// <summary>
        /// The direction in which content should scroll.
        /// </summary>
        public ScrollDirectionType ScrollDirection
        {
            get { return scrollDirection; }
            set { scrollDirection = value; }
        }

        [SerializeField]
        [Tooltip("Toggles whether the scrollingObjectCollection will use the Camera OnPreRender event to manage content visibility.")]
        private bool useOnPreRender;

        /// <summary>
        /// Toggles whether Camera OnPreRender callback will be used to manage content visibility.
        /// The fallback is MonoBehaviour.LateUpdate().
        /// </summary>
        /// <remarks>
        /// This is especially helpful if you're trying to scroll dynamically created objects that may be added to the list after LateUpdate,
        /// </remarks>
        public bool UseOnPreRender
        {
            get { return useOnPreRender; }
            set
            {
                if (useOnPreRender == value) { return; }

                if (cameraMethods == null)
                {
                    cameraMethods = CameraCache.Main.gameObject.EnsureComponent<CameraEventRouter>();
                }

                ClipBox.UseOnPreRender = true;

                if (value)
                {
                    cameraMethods.OnCameraPreRender += OnCameraPreRender;
                }
                else
                {
                    cameraMethods.OnCameraPreRender -= OnCameraPreRender;
                }

                useOnPreRender = value;

                ClipBox.UseOnPreRender = useOnPreRender;
            }
        }

        [SerializeField]
        [Tooltip("Amount of (extra) velocity to be applied to scroller")]
        [Range(0.0f, 0.02f)]
        private float velocityMultiplier = 0.008f;

        /// <summary>
        /// Amount of (extra) velocity to be applied to scroller.
        /// </summary>
        /// <remarks>Helpful if you want a small movement to fling the list.</remarks>
        public float VelocityMultiplier
        {
            get { return velocityMultiplier; }
            set { velocityMultiplier = value; }
        }

        [SerializeField]
        [Tooltip("Amount of falloff applied to velocity")]
        [Range(0.0001f, 0.9999f)]
        private float velocityDampen = 0.90f;

        /// <summary>
        /// Amount of drag applied to velocity.
        /// </summary>
        /// <remarks>This can't be 0.0f since that won't allow ANY velocity - set <see cref="TypeOfVelocity"/> to <see cref="VelocityType.None"/>. It can't be 1.0f since that won't allow ANY drag.</remarks>
        public float VelocityDampen
        {
            get { return velocityDampen; }
            set { velocityDampen = value; }
        }

        [SerializeField]
        [Tooltip("The desired type of velocity for the scroller.")]
        private VelocityType typeOfVelocity;

        /// <summary>
        /// The desired type of velocity for the scroller.
        /// </summary>
        public VelocityType TypeOfVelocity
        {
            get { return typeOfVelocity; }
            set { typeOfVelocity = value; }
        }

        [SerializeField]
        [Tooltip("Animation curve for pagination.")]
        private AnimationCurve paginationCurve = new AnimationCurve(
                                                                    new Keyframe(0, 0),
                                                                    new Keyframe(1, 1));
        /// <summary>
        /// Animation curve used to interpolate the pagination and movement methods.
        /// </summary>
        public AnimationCurve PaginationCurve
        {
            get { return paginationCurve; }
            set { paginationCurve = value; }
        }

        [SerializeField]
        [Tooltip("The amount of time (in seconds) the PaginationCurve will take to evaluate.")]
        private float animationLength = 0.25f;

        /// <summary>
        /// The amount of time (in seconds) the <see cref="PaginationCurve"/> will take to evaluate.
        /// </summary>
        public float AnimationLength
        {
            get { return (animationLength < 0) ? 0 : animationLength; }
            set { animationLength = value; }
        }

        [Tooltip("Number of cells in a row on up-down scroll view or number of cells in a column on left-right scroll view.")]
        [SerializeField]
        [FormerlySerializedAs("tiers")]
        [Min(1)]
        private int cellsPerTier = 1;

        /// <summary>
        /// Number of cells in a row on up-down scroll or number of cells in a column on left-right scroll.
        /// </summary>
        public int CellsPerTier
        {
            get
            {
                return cellsPerTier;
            }
            set
            {
                Debug.Assert(value > 0, "Cells per tier should have a positive non zero value");
                cellsPerTier = Mathf.Max(1, value);
            }
        }

        [SerializeField]
        [Tooltip("Number of visible tiers in the scrolling area.")]
        [FormerlySerializedAs("viewableArea")]
        [Min(1)]
        private int tiersPerPage = 2;

        /// <summary>
        /// Number of visible tiers in the scrolling area.
        /// </summary>
        public int TiersPerPage
        {
            get
            {
                return tiersPerPage;
            }
            set
            {
                Debug.Assert(value > 0, "Tiers per page should have a positive non zero value");
                tiersPerPage = Mathf.Max(1, value);
            }
        }

        [Tooltip("Width of the pagination cell.")]
        [SerializeField]
        [Min(0.001f)]
        private float cellWidth = 0.25f;

        /// <summary>
        /// Width of the pagination cell.
        /// </summary>
        public float CellWidth
        {
            get
            {
                return cellWidth;
            }
            set
            {
                Debug.Assert(value > 0, "Cell width should have a positive non zero value");
                cellWidth = Mathf.Max(0.001f, value);
            }
        }

        [Tooltip("Height of the pagination cell.")]
        [SerializeField]
        [Min(0.001f)]
        private float cellHeight = 0.25f;

        /// <summary>
        /// Height of the pagination cell.Hhide
        /// </summary>
        public float CellHeight
        {
            get
            {
                return cellHeight;
            }
            set
            {
                Debug.Assert(cellHeight > 0, "Cell height should have a positive non zero value");
                cellHeight = Mathf.Max(0.001f, value);
            }
        }

        [Tooltip("Depth of cell used for masking out content renderers that are out of bounds.")]
        [SerializeField]
        [Min(0.001f)]
        private float cellDepth = 0.25f;

        /// <summary>
        /// Depth of cell used for masking out content renderers that are out of bounds.
        /// </summary>
        public float CellDepth
        {
            get
            {
                return cellDepth;
            }
            set
            {
                Debug.Assert(value > 0, "Cell depth should have a positive non zero value");
                cellDepth = Mathf.Max(0.001f, value);
            }
        }

        [SerializeField]
        [Tooltip("Multiplier to add more bounce to the overscroll of a list when using VelocityType.FalloffPerFrame or VelocityType.FalloffPerItem.")]
        private float bounceMultiplier = 0.1f;

        /// <summary>
        /// Multiplier to add more bounce to the overscroll of a list when using <see cref="VelocityType.FalloffPerFrame"/> or <see cref="VelocityType.FalloffPerItem"/>.
        /// </summary>
        public float BounceMultiplier
        {
            get { return bounceMultiplier; }
            set { bounceMultiplier = value; }
        }

        // Lerping time interval used for smoothing between positions during scroll drag. Number was empirically defined.
        private const float DragLerpInterval = 0.5f;

        // Lerping time interval used for smoothing between positions during scroll drag passed max and min scroll positions. Number was empirically defined.
        private const float OverDampLerpInterval = 0.9f;

        // Lerping time interval used for smoothing between positions during bouncing. Number was empirically defined.
        private const float BounceLerpInterval = 0.2f;

        /// <summary>
        /// The UnityEvent type the ScrollingObjectCollection sends.
        /// GameObject is the object the fired the scroll.
        /// </summary>
        [System.Serializable]
        public class ScrollEvent : UnityEvent<GameObject> { }

        /// <summary>
        /// Event that is fired on the target object when the ScrollingObjectCollection deems event as a Click.
        /// </summary>
        [Tooltip("Event that is fired on the target object when the ScrollingObjectCollection deems event as a Click.")]
        public ScrollEvent OnClick = new ScrollEvent();

        /// <summary>
        /// Event that is fired on the target object when the ScrollingObjectCollection is touched.
        /// </summary>
        [Tooltip("Event that is fired on the target object when the ScrollingObjectCollection is touched.")]
        public ScrollEvent OnTouchStarted = new ScrollEvent();

        /// <summary>
        /// Event that is fired on the target object when the ScrollingObjectCollection is no longer touched.
        /// </summary>
        [Tooltip("Event that is fired on the target object when the ScrollingObjectCollection is no longer touched.")]
        public ScrollEvent OnTouchEnded = new ScrollEvent();

        /// <summary>
        /// Event that is fired on the target object when the ScrollingObjectCollection is no longer in motion from velocity
        /// </summary>
        [Tooltip("Event that is fired on the target object when the ScrollingObjectCollection is no longer in motion from velocity.")]
        public UnityEvent OnMomentumEnded = new UnityEvent();

        /// <summary>
        /// Event that is fired on the target object when the ScrollingObjectCollection is starting motion with velocity.
        /// </summary>
        [Tooltip("Event that is fired on the target object when the ScrollingObjectCollection is starting motion with velocity.")]
        public UnityEvent OnMomentumStarted = new UnityEvent();

        [SerializeField]
        [HideInInspector]
        private CameraEventRouter cameraMethods;

        // Maximum amount the scroller can travel (vertically)
        private float MaxY
        {
            get
            {
                var max = (contentBounds == null || contentBounds.size.y <= 0) ? 0 :
                        Mathf.Max(0, contentBounds.size.y - TiersPerPage * CellHeight);

                if (maskEditMode == EditMode.Auto)
                {
                    // Making it a multiple of cell height
                    max = Mathf.Round(SafeDivisionFloat(max, CellHeight)) * CellHeight;
                }

                return max;
            }
        }

        // Minimum amount the scroller can travel (vertically) - this will always be zero. Here for readability
        private readonly float minY = 0.0f;

        // Maximum amount the scroller can travel (horizontally) - this will always be zero. Here for readability
        private readonly float maxX = 0.0f;

        // Minimum amount the scroller can travel (horizontally)
        private float MinX
        {
            get
            {
                var max = (contentBounds == null || contentBounds.size.x <= 0) ? 0 :
                     Mathf.Max(0, contentBounds.size.x - TiersPerPage * CellWidth);

                if (maskEditMode == EditMode.Auto)
                {
                    // Making it a multiple of cell width
                    max = Mathf.Round(SafeDivisionFloat(max, CellWidth)) * CellWidth;
                }

                return max * -1.0f;
            }
        }

        // Bounds that wrap all scroll container content. Used for calculating MinX and MaxY.
        private Bounds contentBounds;

        /// <summary>
        /// Index of the first visible cell.
        /// </summary>
        public int FirstVisibleCellIndex
        {
            get
            {
                if (scrollDirection == ScrollDirectionType.UpAndDown)
                {
                    return (int)Mathf.Ceil(ScrollContainer.transform.localPosition.y / CellHeight) * CellsPerTier;
                }
                else
                {
                    // Scroll container most to the right local position has x component equals to zero. This value goes negative as scroll container moves to the left. 
                    return ((int)Mathf.Ceil(Mathf.Abs(ScrollContainer.transform.localPosition.x / CellWidth)) * CellsPerTier);
                }
            }
        }

        /// <summary>
        /// Index of the first hidden cell.
        /// </summary>
        public int FirstHiddenCellIndex
        {
            get
            {
                if (scrollDirection == ScrollDirectionType.UpAndDown)
                {
                    return ((int)Mathf.Floor(ScrollContainer.transform.localPosition.y / CellHeight) * CellsPerTier) + (TiersPerPage * CellsPerTier);
                }
                else
                {
                    return ((int)Mathf.Floor(-ScrollContainer.transform.localPosition.x / CellWidth) * CellsPerTier) + (TiersPerPage * CellsPerTier);
                }
            }
        }

        private BoxCollider scrollingCollider;
        /// <summary>
        /// Scrolling interaction collider used to catch pointer and touch events on empty spaces.
        /// </summary>
        public BoxCollider ScrollingCollider
        {
            get
            {
                if (scrollingCollider == null)
                {
                    scrollingCollider = gameObject.EnsureComponent<BoxCollider>();
                }

                return scrollingCollider;
            }
        }

        // Depth of the scrolling interaction collider. Used for defining a plane depth if 'Auto' collider edit mode is selected.
        private const float ScrollingColliderDepth = 0.001f;

        private NearInteractionTouchable scrollingTouchable;
        /// <summary>
        /// Scrolling interaction touchable used to catch touch events on empty spaces.
        /// </summary>
        public NearInteractionTouchable ScrollingTouchable
        {
            get
            {
                if (scrollingTouchable == null)
                {
                    scrollingTouchable = gameObject.EnsureComponent<NearInteractionTouchable>();
                }

                return scrollingTouchable;
            }
        }

        /// <summary>
        /// The local position of the moving scroll container. Can be used to represent the container drag displacement.
        /// </summary>
        public Vector3 ScrollContainerPosition => ScrollContainer.transform.localPosition;

        // The empty game object that contains our nodes and be scrolled
        [SerializeField]
        [HideInInspector]
        private GameObject scrollContainer;

        private GameObject ScrollContainer
        {
            get
            {
                if (scrollContainer == null)
                {
                    Transform oldContainer = transform.Find("Container");

                    if (oldContainer != null)
                    {
                        scrollContainer = oldContainer.gameObject;
                        Debug.LogWarning(name + " ScrollingObjectCollection found an existing Container object, using it for the list");
                    }
                    else
                    {
                        scrollContainer = new GameObject();
                        scrollContainer.name = "Container";
                        scrollContainer.transform.parent = transform;
                        scrollContainer.transform.localPosition = Vector3.zero;
                        scrollContainer.transform.localRotation = Quaternion.identity;
                    }
                }

                return scrollContainer;
            }
        }

        // The empty game object that contains the ClipppingBox
        [SerializeField]
        [HideInInspector]
        private GameObject clippingObject;

        /// <summary>
        /// The empty GameObject containing the ScrollingObjectCollection's <see cref="Microsoft.MixedReality.Toolkit.Utilities.ClippingBox"/>.
        /// </summary>
        public GameObject ClippingObject
        {
            get
            {
                if (clippingObject == null)
                {
                    Transform oldClippingObj = transform.Find("Clipping Bounds");

                    if (oldClippingObj != null)
                    {
                        clippingObject = oldClippingObj.gameObject;
                        Debug.LogWarning(name + " ScrollingObjectCollection found an existing Clipping object, using it for the list");
                    }
                    else
                    {
                        clippingObject = new GameObject();
                    }

                    clippingObject.name = "Clipping Bounds";
                    clippingObject.transform.parent = transform;
                    clippingObject.transform.localRotation = Quaternion.identity;
                    clippingObject.transform.localPosition = Vector3.zero;
                }

                return clippingObject;
            }
        }

        [SerializeField]
        [HideInInspector]
        private ClippingBox clipBox;

        /// <summary>
        /// The ScrollingObjectCollection's <see cref="Microsoft.MixedReality.Toolkit.Utilities.ClippingBox"/>
        /// that is used for clipping items in and out of the list.
        /// </summary>
        public ClippingBox ClipBox
        {
            get
            {
                if (clipBox == null)
                {
                    clipBox = ClippingObject.EnsureComponent<ClippingBox>();
                    clipBox.ClippingSide = ClippingPrimitive.Side.Outside;
                }

                return clipBox;
            }
        }

        // This collider will be used for checking intersection of the scroll visible area with any content collider or renderer bounds.
        private Collider clippingBoundsCollider;
        private Collider ClippingBoundsCollider
        {
            get
            {
                if (clippingBoundsCollider == null)
                {
                    clippingBoundsCollider = ClippingObject.EnsureComponent<BoxCollider>();
                    clippingBoundsCollider.enabled = false;
                }

                return clippingBoundsCollider;
            }
        }

        // Ratio that defines the outer clipping bounds size relative to the actual clipping bounds.
        // The outer clipping bounds is used for ensuring that content collider that are mostly visible can still stay interactable.
        private readonly float contentVisibilityThresholdRatio = 1.025f;

        private bool oldIsTargetPositionLockedOnFocusLock;

        private readonly HashSet<Renderer> clippedRenderers = new HashSet<Renderer>();

        #region scroll state variables

        /// <summary>
        /// Tracks whether content or scroll background is being interacted with.
        /// </summary>
        public bool IsEngaged { get; private set; } = false;

        /// <summary>
        /// Tracks whether the scroll is being dragged due to a controller movement.
        /// </summary>
        public bool IsDragging { get; private set; } = false;

        /// <summary>
        /// Tracks whether the scroll content or background is touched by a near pointer.
        /// Remains true while the same near pointer does not cross the scrolling release boundaries.
        /// </summary>
        public bool IsTouched { get; private set; } = false;

        /// <summary>
        /// Tracks whether the scroll has any kind of momentum.
        /// True if scroll is being dragged by a controller, the velocity is falling off after a drag release or during pagination movement.
        /// </summary>
        public bool HasMomentum { get; private set; } = false;

        // The position of the scollContainer before we do any updating to it
        private Vector3 initialScrollerPos;

        // The new of the scollContainer before we've set the position / finished the updateloop
        private Vector3 workingScrollerPos;

        // A list of content renderers that need to be added to the clippingBox
        private List<Renderer> renderersToClip = new List<Renderer>();

        // A list of content renderers that need to be removed from the clippingBox
        private List<Renderer> renderersToUnclip = new List<Renderer>();

        private IMixedRealityPointer currentPointer;

        // The initial focused object from scroll content. This may not always be currentPointer.Result.CurrentPointerTarget
        private GameObject initialFocusedObject;

        #endregion scroll state variables

        #region drag position calculation variables

        // Hand position when starting a motion
        private Vector3 initialPointerPos;

        // Hand position previous frame
        private Vector3 lastPointerPos;

        #endregion drag position calculation variables

        #region velocity calculation variables

        // Simple velocity of the scroller: current - last / timeDelta
        private float scrollVelocity = 0.0f;

        // Filtered weight of scroll velocity
        private float avgVelocity = 0.0f;

        // How much we should filter the velocity - yes this is a magic number. Its been tuned so lets leave it.
        private readonly float velocityFilterWeight = 0.97f;

        // Simple state enum to handle velocity falloff logic
        private enum VelocityState
        {
            None = 0,
            Resolving,
            Calculating,
            Bouncing,
            Dragging,
            Animating,
        }

        // Internal enum for tracking the velocity state of the list
        private VelocityState currentVelocityState;

        private VelocityState CurrentVelocityState
        {
            get => currentVelocityState;

            set
            {
                if (value != currentVelocityState)
                {
                    if (value == VelocityState.None)
                    {
                        OnMomentumEnded.Invoke();
                    }
                    else if (currentVelocityState == VelocityState.None)
                    {
                        OnMomentumStarted.Invoke();
                    }
                    previousVelocityState = currentVelocityState;
                    currentVelocityState = value;
                }
            }
        }

        private VelocityState previousVelocityState;

        // Pre calculated destination with velocity and falloff when using per item snapping
        private Vector3 velocityDestinationPos;

        // Velocity container for storing previous filtered velocity
        private float velocitySnapshot;

        #endregion velocity calculation variables

        // The Animation CoRoutine
        private IEnumerator animateScroller;

        /// <summary>
        /// Scroll pagination modes.
        /// </summary>
        public enum PaginationMode
        {
            ByTier = 0, // By number of tiers
            ByPage, // By number of pages
            ToCellIndex // To selected cell
        }

        #region performance variables
        [SerializeField]
        [Tooltip("Disables Gameobjects with Renderer components which are clipped by the clipping box.")]
        private bool disableClippedGameObjects = true;

        /// <summary>
        /// Disables GameObjects with Renderer components which are clipped by the clipping box.
        /// Improves performance significantly by reducing the number of GameObjects that need to be managed in engine.
        /// </summary>
        public bool DisableClippedGameObjects
        {
            get { return disableClippedGameObjects; }
            set { disableClippedGameObjects = value; }
        }

        [SerializeField]
        [Tooltip("Disables the Renderer components of Gameobjects which are clipped by the clipping box.")]
        private bool disableClippedRenderers = false;

        /// <summary>
        /// Disables the Renderer components of Gameobjects which are clipped by the clipping box.
        /// Improves performance by reducing the number of renderers that need to be tracked, while still allowing the
        /// GameObjects associated with those renders to continue updating. Less performant compared to using DisableClippedGameObjects
        /// </summary>
        public bool DisableClippedRenderers
        {
            get { return disableClippedRenderers; }
            set { disableClippedRenderers = value; }
        }

        #endregion performance variables

        #region Setup methods

        /// <summary>
        /// Sets up the scroll clipping object and the interactable components according to the scroll content and chosen settings.
        /// </summary>
        public void UpdateContent()
        {
            UpdateContentBounds();
            SetupScrollingInteractionCollider();
            SetupClippingObject();
            ManageVisibility();
        }

        private void UpdateContentBounds()
        {
            var originalRotation = transform.rotation;
            transform.rotation = Quaternion.identity;

            var childrenRenderers = ScrollContainer.GetComponentsInChildren<Renderer>(true);
            if (childrenRenderers != null)
            {
                contentBounds = new Bounds
                {
                    size = Vector3.zero,
                    center = ClipBox.transform.position
                };

                foreach (var renderer in childrenRenderers)
                {
                    contentBounds.Encapsulate(renderer.bounds);
                }

                Vector3 localSize;

                localSize.y = SafeDivisionFloat(contentBounds.size.y, transform.lossyScale.y);
                localSize.x = SafeDivisionFloat(contentBounds.size.x, transform.lossyScale.x);
                localSize.z = SafeDivisionFloat(contentBounds.size.z, transform.lossyScale.z);

                contentBounds.size = localSize;
            }

            transform.rotation = originalRotation;
        }

        // Setting up the initial transform values for the scrolling interaction collider and near touchable.
        private void SetupScrollingInteractionCollider()
        {
            // Boundaries will be defined by direct manipulation of the scroll interaction components
            if (colliderEditMode == EditMode.Manual)
            {
                return;
            }

            if (scrollDirection == ScrollDirectionType.UpAndDown)
            {
                ScrollingCollider.size = new Vector3(CellWidth * CellsPerTier, CellHeight * TiersPerPage, ScrollingColliderDepth);
            }
            else
            {
                ScrollingCollider.size = new Vector3(CellWidth * TiersPerPage, CellHeight * CellsPerTier, ScrollingColliderDepth);
            }

            Vector3 colliderPosition;
            colliderPosition.x = ScrollingCollider.size.x / 2;
            colliderPosition.y = -ScrollingCollider.size.y / 2;
            colliderPosition.z = cellDepth / 2 + ScrollingColliderDepth;
            ScrollingCollider.center = colliderPosition;

            Vector2 size = new Vector2(
                        Math.Abs(Vector3.Dot(ScrollingCollider.size, ScrollingTouchable.LocalRight)),
                        Math.Abs(Vector3.Dot(ScrollingCollider.size, ScrollingTouchable.LocalUp)));

            Vector3 touchablePosition = colliderPosition;
            touchablePosition.z = -cellDepth / 2;

            ScrollingTouchable.SetBounds(size);
            ScrollingTouchable.SetLocalCenter(touchablePosition);
        }

        /// <summary>
        /// Setting up the initial transform values for the clippingBox.
        /// </summary>
        private void SetupClippingObject()
        {
            // Boundaries will be defined by direct manipulation of the clipping object
            if (maskEditMode == EditMode.Manual)
            {
                return;
            }

            // The bounds of the clipping object, this is to make helper math easier later, it doesn't matter that its AABB since we're really not using it for bounds operations
            Bounds clippingBounds = new Bounds();
            clippingBounds.size = Vector3.one;

            Vector3 viewableCenter = new Vector3();

            // Adjust scale and position of clipping box
            switch (scrollDirection)
            {
                case ScrollDirectionType.UpAndDown:
                default:

                    // Apply the viewable area and column/row multiplier
                    // Use a dummy bounds of one to get the local scale to match;
                    clippingBounds.size = new Vector3((CellWidth * CellsPerTier), (CellHeight * TiersPerPage), CellDepth);
                    ClipBox.transform.localScale = new Bounds(Vector3.zero, Vector3.one).GetScaleToMatchBounds(clippingBounds);

                    break;

                case ScrollDirectionType.LeftAndRight:

                    // Same as above for L <-> R
                    clippingBounds.size = new Vector3(CellWidth * TiersPerPage, CellHeight * CellsPerTier, CellDepth);
                    ClipBox.transform.localScale = new Bounds(Vector3.zero, Vector3.one).GetScaleToMatchBounds(clippingBounds);

                    break;
            }

            // Adjust where the center of the clipping box is
            viewableCenter.x = ClipBox.transform.localScale.x * 0.5f;
            viewableCenter.y = ClipBox.transform.localScale.y * -0.5f;
            viewableCenter.z = 0;

            // Apply new values
            ClipBox.transform.localPosition = viewableCenter;
        }

        #endregion Setup methods

        #region MonoBehaviour Implementation

        private void OnEnable()
        {
            // Register for global input events
            CoreServices.InputSystem?.RegisterHandler<IMixedRealitySourceStateHandler>(this);
            CoreServices.InputSystem?.RegisterHandler<IMixedRealityTouchHandler>(this);
            CoreServices.InputSystem?.RegisterHandler<IMixedRealityPointerHandler>(this);

            if (useOnPreRender)
            {
                ClipBox.UseOnPreRender = true;

                // Subscribe to the preRender callback on the main camera so we can intercept it and make sure we catch
                // any dynamically added content
                if (cameraMethods == null)
                {
                    cameraMethods = CameraCache.Main.gameObject.EnsureComponent<CameraEventRouter>();
                }

                cameraMethods.OnCameraPreRender += OnCameraPreRender;
            }
        }

        private void Start()
        {
            UpdateContent();
        }

        private void Update()
        {
            if (!Application.isPlaying)
            {
                return;
            }

            // Force the scroll container position if no content
            if (ScrollContainer.GetComponentInChildren<Renderer>(true) == null)
            {
                workingScrollerPos = Vector3.zero;
                ApplyPosition(workingScrollerPos);

                return;
            }

            // The scroller has detected input and has a valid pointer
            if (IsEngaged && TryGetPointerPositionOnPlane(out Vector3 currentPointerPos))
            {
                Vector3 handDelta = initialPointerPos - currentPointerPos;
                handDelta = transform.InverseTransformDirection(handDelta);

                if (IsDragging && currentPointer != null) // Changing lock after drag started frame to allow for focus provider to move pointer focus to scroll background before locking
                {
                    currentPointer.IsFocusLocked = true;
                }

                // Lets see if this is gonna be a click or a drag
                // Check the scroller's length state to prevent resetting calculation
                if (!IsDragging)
                {
                    // Grab the delta value we care about
                    float absAxisHandDelta = (scrollDirection == ScrollDirectionType.UpAndDown) ? Mathf.Abs(handDelta.y) : Mathf.Abs(handDelta.x);

                    // Catch an intentional finger in scroller to stop momentum, this isn't a drag its definitely a stop
                    if (absAxisHandDelta > handDeltaScrollThreshold)
                    {
                        scrollVelocity = 0.0f;
                        avgVelocity = 0.0f;

                        IsDragging = true;
                        handDelta = Vector3.zero;

                        CurrentVelocityState = VelocityState.Dragging;

                        // Reset initialHandPos to prevent the scroller from jumping
                        initialScrollerPos = workingScrollerPos = ScrollContainer.transform.localPosition;
                        initialPointerPos = currentPointerPos;
                    }
                }

                if (IsTouched && DetectScrollRelease(currentPointerPos))
                {
                    // We're on the other side of the original touch position. This is a release.
                    if (IsDragging)
                    {
                        // Its a drag release
                        initialScrollerPos = workingScrollerPos;
                        CurrentVelocityState = VelocityState.Calculating;
                    }
                    else
                    {
                        // Its a click release
                        OnClick?.Invoke(initialFocusedObject);
                    }

                    ResetInteraction();
                }
                else if (IsDragging && canScroll)
                {
                    if (scrollDirection == ScrollDirectionType.UpAndDown)
                    {
                        // Lock X, clamp Y
                        float handLocalDelta = SafeDivisionFloat(handDelta.y, transform.lossyScale.y);

                        // Over damp if scroll position out of bounds
                        if (workingScrollerPos.y > MaxY || workingScrollerPos.y < minY)
                        {
                            workingScrollerPos.y = MathUtilities.CLampLerp(initialScrollerPos.y - handLocalDelta, minY, MaxY, OverDampLerpInterval);
                        }
                        else
                        {
                            workingScrollerPos.y = MathUtilities.CLampLerp(initialScrollerPos.y - handLocalDelta, minY, MaxY, DragLerpInterval);
                        }
                        workingScrollerPos.x = 0.0f;
                    }
                    else
                    {
                        // Lock Y, clamp X
                        float handLocalDelta = SafeDivisionFloat(handDelta.x, transform.lossyScale.x);

                        // Over damp if scroll position out of bounds
                        if (workingScrollerPos.x > maxX || workingScrollerPos.x < MinX)
                        {
                            workingScrollerPos.x = MathUtilities.CLampLerp(initialScrollerPos.x - handLocalDelta, MinX, maxX, OverDampLerpInterval);
                        }
                        else
                        {
                            workingScrollerPos.x = MathUtilities.CLampLerp(initialScrollerPos.x - handLocalDelta, MinX, maxX, DragLerpInterval);
                        }
                        workingScrollerPos.y = 0.0f;
                    }

                    // Update the scrollContainer Position
                    ApplyPosition(workingScrollerPos);

                    CalculateVelocity();

                    // Update the prev val for velocity
                    lastPointerPos = currentPointerPos;
                }
            }
            else if ((CurrentVelocityState != VelocityState.None
                      || previousVelocityState != VelocityState.None)
                      && CurrentVelocityState != VelocityState.Animating) // Prevent the Animation coroutine from being overridden
            {
                // We're not engaged, so handle any not touching behavior
                HandleVelocityFalloff();

                // Apply our position
                ApplyPosition(workingScrollerPos);
            }

            // Setting HasMomentum to true if scroll velocity state has changed or any movement happened during this update
            if (CurrentVelocityState != VelocityState.None || previousVelocityState != VelocityState.None)
            {
                HasMomentum = true;
            }

            else
            {
                HasMomentum = false;
            }

            previousVelocityState = CurrentVelocityState;
        }

        private void LateUpdate()
        {
            if (!UseOnPreRender)
            {
                ManageVisibility();
            }
        }

        private void OnDisable()
        {
            // Unregister global input events
            CoreServices.InputSystem?.UnregisterHandler<IMixedRealitySourceStateHandler>(this);
            CoreServices.InputSystem?.UnregisterHandler<IMixedRealityTouchHandler>(this);
            CoreServices.InputSystem?.UnregisterHandler<IMixedRealityPointerHandler>(this);

            // Currently in editor duplicating prefab GameObject containing both TMP and non-TMP children inside the Scrolling Object Collection container causes material life cycle management issues
            // https://github.com/microsoft/MixedRealityToolkit-Unity/issues/9481
            // Thus we do not automatically destroy material controlled by Material Instance if the OnDisable comes from pasting in editor
#if UNITY_EDITOR
            if (!Application.isPlaying)
            {
                bool? isCalledFromPastingGameObject = new System.Diagnostics.StackFrame(1)?.GetMethod()?.Name?.Contains("Paste");
                RestoreContentVisibility(!isCalledFromPastingGameObject.GetValueOrDefault());
            }
            else
            {
                RestoreContentVisibility();
            }
#else
            RestoreContentVisibility();
#endif

            if (useOnPreRender && cameraMethods != null)
            {
                CameraEventRouter cameraMethods = CameraCache.Main.gameObject.EnsureComponent<CameraEventRouter>();
                cameraMethods.OnCameraPreRender -= OnCameraPreRender;
            }
        }

        #endregion MonoBehaviour Implementation

        #region private methods

        /// <summary>
        /// When <see cref="UseOnPreRender"/>, the <see cref="ScrollingObjectCollection"/> subscribes to the <see cref="CameraEventRouter"/> call back for OnCameraPreRender
        /// </summary>
        /// <param name="router">The active <see cref="CameraEventRouter"/> on the camera.</param>
        private void OnCameraPreRender(CameraEventRouter router)
        {
            ManageVisibility();
        }

        // Add or remove renderers from clipping primitive
        private void ReconcileClippingContent()
        {
            if (renderersToClip.Count > 0)
            {
                AddRenderersToClippingObject(renderersToClip);

                renderersToClip.Clear();
            }

            if (renderersToUnclip.Count > 0)
            {
                RemoveRenderersFromClippingObject(renderersToUnclip);

                renderersToUnclip.Clear();
            }
        }

        /// <summary>
        /// Gets the cursor position (pointer end point) on the scrollable plane,
        /// projected onto the direction being scrolled if far pointer.
        /// Returns false if the pointer is null.
        /// </summary>
        private bool TryGetPointerPositionOnPlane(out Vector3 result)
        {
            result = Vector3.zero;

            if (((MonoBehaviour)currentPointer) == null)
            {
                return false;
            }
            if (currentPointer.GetType() == typeof(PokePointer))
            {
                result = currentPointer.Position;
                return true;
            }

            var scrollVector = (scrollDirection == ScrollDirectionType.UpAndDown) ? transform.up : transform.right;

            result = transform.position + Vector3.Project(currentPointer.Position - transform.position, scrollVector);
            return true;
        }

        /// <summary>
        /// Calculates our <see cref="VelocityType"/> falloff
        /// </summary>
        private void HandleVelocityFalloff()
        {
            switch (typeOfVelocity)
            {
                case VelocityType.FalloffPerFrame:

                    HandleFalloffPerFrame();
                    break;

                case VelocityType.FalloffPerItem:
                default:

                    HandleFalloffPerItem();
                    break;

                case VelocityType.NoVelocitySnapToItem:

                    CurrentVelocityState = VelocityState.None;

                    avgVelocity = 0.0f;

                    // Round to the nearest cell
                    if (scrollDirection == ScrollDirectionType.UpAndDown)
                    {
                        workingScrollerPos.y = Mathf.Round(ScrollContainer.transform.localPosition.y / CellHeight) * CellHeight;
                    }
                    else
                    {
                        workingScrollerPos.x = Mathf.Round(ScrollContainer.transform.localPosition.x / CellWidth) * CellWidth;
                    }

                    initialScrollerPos = workingScrollerPos;
                    break;

                case VelocityType.None:

                    CurrentVelocityState = VelocityState.None;

                    avgVelocity = 0.0f;
                    break;
            }

            if (CurrentVelocityState == VelocityState.None)
            {
                workingScrollerPos.y = Mathf.Clamp(workingScrollerPos.y, minY, MaxY);
                workingScrollerPos.x = Mathf.Clamp(workingScrollerPos.x, MinX, maxX);
            }
        }

        /// <summary>
        /// Handles <see cref="ScrollingObjectCollection"/> drag release behavior when <see cref="TypeOfVelocity"/> is set to <see cref="VelocityType.FalloffPerItem"/>
        /// </summary>
        private void HandleFalloffPerItem()
        {
            switch (CurrentVelocityState)
            {
                case VelocityState.Calculating:

                    int numSteps;
                    float newPosAfterVelocity;
                    if (scrollDirection == ScrollDirectionType.UpAndDown)
                    {
                        if (avgVelocity == 0.0f)
                        {
                            // Velocity was cleared out so we should just snap
                            newPosAfterVelocity = ScrollContainer.transform.localPosition.y;
                        }
                        else
                        {
                            // Precalculate where the velocity falloff would land our scrollContainer, then round it to the nearest cell so it feels natural
                            velocitySnapshot = IterateFalloff(avgVelocity, out numSteps);
                            newPosAfterVelocity = initialScrollerPos.y - velocitySnapshot;
                        }

                        velocityDestinationPos.y = (Mathf.Round(newPosAfterVelocity / CellHeight)) * CellHeight;

                        CurrentVelocityState = VelocityState.Resolving;
                    }
                    else
                    {
                        if (avgVelocity == 0.0f)
                        {
                            // Velocity was cleared out so we should just snap
                            newPosAfterVelocity = ScrollContainer.transform.localPosition.x;
                        }
                        else
                        {
                            // Precalculate where the velocity falloff would land our scrollContainer, then round it to the nearest cell so it feels natural
                            velocitySnapshot = IterateFalloff(avgVelocity, out numSteps);
                            newPosAfterVelocity = initialScrollerPos.x + velocitySnapshot;
                        }

                        velocityDestinationPos.x = (Mathf.Round(newPosAfterVelocity / CellWidth)) * CellWidth;

                        CurrentVelocityState = VelocityState.Resolving;
                    }

                    workingScrollerPos = Solver.SmoothTo(scrollContainer.transform.localPosition, velocityDestinationPos, Time.deltaTime, BounceLerpInterval);

                    // Clear the velocity now that we've applied a new position
                    avgVelocity = 0.0f;
                    break;

                case VelocityState.Resolving:

                    if (scrollDirection == ScrollDirectionType.UpAndDown)
                    {
                        if (ScrollContainer.transform.localPosition.y > MaxY
                            || ScrollContainer.transform.localPosition.y < minY)
                        {
                            CurrentVelocityState = VelocityState.Bouncing;
                            velocitySnapshot = 0.0f;
                            break;
                        }
                        else
                        {
                            workingScrollerPos = Solver.SmoothTo(ScrollContainer.transform.localPosition, velocityDestinationPos, Time.deltaTime, BounceLerpInterval);

                            SnapVelocityFinish();
                        }
                    }
                    else
                    {
                        if (ScrollContainer.transform.localPosition.x > maxX + (FrontTouchDistance * bounceMultiplier)
                            || ScrollContainer.transform.localPosition.x < MinX - (FrontTouchDistance * bounceMultiplier))
                        {
                            CurrentVelocityState = VelocityState.Bouncing;
                            velocitySnapshot = 0.0f;
                            break;
                        }
                        else
                        {
                            workingScrollerPos = Solver.SmoothTo(ScrollContainer.transform.localPosition, velocityDestinationPos, Time.deltaTime, BounceLerpInterval);

                            SnapVelocityFinish();
                        }
                    }
                    break;

                case VelocityState.Bouncing:

                    HandleBounceState();
                    break;

                case VelocityState.None:
                default:
                    // clean up our position for next frame
                    initialScrollerPos = workingScrollerPos;
                    break;

            }
        }

        /// <summary>
        /// Handles <see cref="ScrollingObjectCollection"/> drag release behavior when <see cref="TypeOfVelocity"/> is set to <see cref="VelocityType.FalloffPerFrame"/>
        /// </summary>
        private void HandleFalloffPerFrame()
        {
            switch (CurrentVelocityState)
            {
                case VelocityState.Calculating:

                    if (scrollDirection == ScrollDirectionType.UpAndDown)
                    {
                        workingScrollerPos.y = initialScrollerPos.y + avgVelocity;
                    }
                    else
                    {
                        workingScrollerPos.x = initialScrollerPos.x + avgVelocity;
                    }

                    CurrentVelocityState = VelocityState.Resolving;

                    // clean up our position for next frame
                    initialScrollerPos = workingScrollerPos;
                    break;

                case VelocityState.Resolving:

                    if (scrollDirection == ScrollDirectionType.UpAndDown)
                    {
                        if (ScrollContainer.transform.localPosition.y > MaxY + (FrontTouchDistance * bounceMultiplier)
                            || ScrollContainer.transform.localPosition.y < minY - (FrontTouchDistance * bounceMultiplier))
                        {
                            CurrentVelocityState = VelocityState.Bouncing;
                            avgVelocity = 0.0f;
                            break;
                        }
                        else
                        {
                            avgVelocity *= velocityDampen;
                            workingScrollerPos.y = initialScrollerPos.y + avgVelocity;

                            SnapVelocityFinish();

                        }
                    }
                    else
                    {
                        if (ScrollContainer.transform.localPosition.x > maxX + (FrontTouchDistance * bounceMultiplier)
                            || ScrollContainer.transform.localPosition.x < MinX - (FrontTouchDistance * bounceMultiplier))
                        {
                            CurrentVelocityState = VelocityState.Bouncing;
                            avgVelocity = 0.0f;
                            break;
                        }
                        else
                        {
                            avgVelocity *= velocityDampen;
                            workingScrollerPos.x = initialScrollerPos.x + avgVelocity;

                            SnapVelocityFinish();
                        }
                    }

                    // clean up our position for next frame
                    initialScrollerPos = workingScrollerPos;

                    break;

                case VelocityState.Bouncing:

                    HandleBounceState();

                    break;
            }
        }

        /// <summary>
        /// Smooths <see cref="ScrollContainer"/>'s position to the proper clamped edge 
        /// while <see cref="CurrentVelocityState"/> is <see cref="VelocityState.Bouncing"/>.
        /// </summary>
        private void HandleBounceState()
        {
            Vector3 clampedDest = new Vector3(Mathf.Clamp(ScrollContainer.transform.localPosition.x, MinX, maxX), Mathf.Clamp(ScrollContainer.transform.localPosition.y, minY, MaxY), 0.0f);
            if ((scrollDirection == ScrollDirectionType.UpAndDown && Mathf.Approximately(ScrollContainer.transform.localPosition.y, clampedDest.y))
                || (scrollDirection == ScrollDirectionType.LeftAndRight && Mathf.Approximately(ScrollContainer.transform.localPosition.x, clampedDest.x)))
            {
                CurrentVelocityState = VelocityState.None;

                // clean up our position for next frame
                initialScrollerPos = workingScrollerPos = clampedDest;
                return;
            }
            workingScrollerPos.y = Solver.SmoothTo(ScrollContainer.transform.localPosition, clampedDest, Time.deltaTime, BounceLerpInterval).y;
            workingScrollerPos.x = Solver.SmoothTo(ScrollContainer.transform.localPosition, clampedDest, Time.deltaTime, BounceLerpInterval).x;
        }

        /// <summary>
        /// Snaps to the final position of the <see cref="ScrollContainer"/> once velocity as resolved.
        /// </summary>
        private void SnapVelocityFinish()
        {
            if (Vector3.Distance(ScrollContainer.transform.localPosition, workingScrollerPos) > Mathf.Epsilon)
            {
                return;
            }

            if (typeOfVelocity == VelocityType.FalloffPerItem)
            {
                if (scrollDirection == ScrollDirectionType.UpAndDown)
                {
                    // Ensure we've actually snapped the position to prevent an extreme in-between state
                    workingScrollerPos.y = (Mathf.Round(ScrollContainer.transform.localPosition.y / CellHeight)) * CellHeight;
                }
                else
                {
                    workingScrollerPos.x = (Mathf.Round(ScrollContainer.transform.localPosition.x / CellWidth)) * CellWidth;
                }
            }

            CurrentVelocityState = VelocityState.None;
            avgVelocity = 0.0f;

            // clean up our position for next frame
            initialScrollerPos = workingScrollerPos;
        }

        /// <summary>
        /// Wrapper for per frame velocity calculation and filtering.
        /// </summary>
        private void CalculateVelocity()
        {
            // Update simple velocity
            TryGetPointerPositionOnPlane(out Vector3 newPos);

            scrollVelocity = (scrollDirection == ScrollDirectionType.UpAndDown)
                ? (newPos.y - lastPointerPos.y) / Time.deltaTime * velocityMultiplier
                : (newPos.x - lastPointerPos.x) / Time.deltaTime * velocityMultiplier;

            // And filter it...
            avgVelocity = (avgVelocity * (1.0f - velocityFilterWeight)) + (scrollVelocity * velocityFilterWeight);
        }

        /// <summary>
        /// The Animation Override to position our scroller based on manual movement <see cref="PageBy(int, bool)"/>, <see cref="MoveTo(int, bool)"/>,
        /// </summary>
        /// <param name="initialPos">The start position of the scrollContainer</param>
        /// <param name="finalPos">Where we want the scrollContainer to end up, typically this should be <see cref="workingScrollerPos"/></param>
        /// <param name="curve"><see cref="AnimationCurve"/> representing the easing desired</param>
        /// <param name="time">Time for animation, in seconds</param>
        /// <param name="callback">Optional callback action to be invoked after animation coroutine has finished</param>
        private IEnumerator AnimateTo(Vector3 initialPos, Vector3 finalPos, AnimationCurve curve = null, float? time = null, System.Action callback = null)
        {
            if (curve == null)
            {
                curve = paginationCurve;
            }

            if (time == null)
            {
                time = animationLength;
            }

            float counter = 0.0f;
            while (counter <= time)
            {
                workingScrollerPos = Vector3.Lerp(initialPos, finalPos, curve.Evaluate(counter / (float)time));
                ScrollContainer.transform.localPosition = workingScrollerPos;

                counter += Time.deltaTime;
                yield return null;
            }

            // Update our values so they stick
            if (scrollDirection == ScrollDirectionType.UpAndDown)
            {
                workingScrollerPos.y = initialScrollerPos.y = finalPos.y;
            }
            else
            {
                workingScrollerPos.x = initialScrollerPos.x = finalPos.x;
            }

            if (callback != null)
            {
                callback?.Invoke();
            }

            CurrentVelocityState = VelocityState.None;
            animateScroller = null;
        }

        /// <summary>
        /// Checks if the engaged joint has released the scrollable list
        /// </summary>
        private bool DetectScrollRelease(Vector3 pointerPos)
        {
            Vector3 scrollToPointerVector = pointerPos - ClipBox.transform.position;

            // Projecting vector onto every clip box space coordinate and using clip box lossy scale as reference to dimensions to scroll view visible bounds
            // Using dot product to check if pointer is in front or behind the scroll view plane
            bool isScrollRelease = Vector3.Magnitude(Vector3.Project(scrollToPointerVector, ClipBox.transform.up)) > ClipBox.transform.lossyScale.y / 2 + releaseThresholdTopBottom
                                || Vector3.Magnitude(Vector3.Project(scrollToPointerVector, ClipBox.transform.right)) > ClipBox.transform.lossyScale.x / 2 + releaseThresholdLeftRight

                                || (Vector3.Dot(scrollToPointerVector, transform.forward) > 0 ?
                                        Vector3.Magnitude(Vector3.Project(scrollToPointerVector, ClipBox.transform.forward)) > ClipBox.transform.lossyScale.z / 2 + releaseThresholdBack :
                                        Vector3.Magnitude(Vector3.Project(scrollToPointerVector, ClipBox.transform.forward)) > ClipBox.transform.lossyScale.z / 2 + releaseThresholdFront);
            return isScrollRelease;
        }

        private bool HasPassedThroughFrontPlane(PokePointer pokePointer)
        {
            var p = transform.InverseTransformPoint(pokePointer.PreviousPosition);
            return p.z <= -FrontTouchDistance;
        }

        /// <summary>
        /// Adds list of renderers to the ClippingBox
        /// </summary>
        private void AddRenderersToClippingObject(List<Renderer> renderers)
        {
            foreach (var renderer in renderers)
            {
                ClipBox.AddRenderer(renderer);
            }
        }

        /// <summary>
        /// Removes list of renderers from the ClippingBox
        /// </summary>
        private void RemoveRenderersFromClippingObject(List<Renderer> renderers)
        {
            foreach (var renderer in renderers)
            {
                ClipBox.RemoveRenderer(renderer);
            }
        }

        /// <summary>
        /// Removes all renderers currently being clipped by the clipping box
        /// </summary>
        private void ClearClippingBox(bool autoDestroyMaterial = true)
        {
            ClipBox.ClearRenderers(autoDestroyMaterial);
        }

        /// <summary>
        /// Helper to perform division operations and prevent division by 0.
        /// </summary>
        private static int SafeDivisionInt(int numerator, int denominator)
        {
            return (denominator != 0) ? numerator / denominator : 0;
        }

        private float SafeDivisionFloat(float numerator, float denominator)
        {
            return (denominator != 0) ? numerator / denominator : 0;
        }

        /// <summary>
        /// Checks visibility of scroll content by iterating through all content renderers and colliders.
        /// All inactive content objects and colliders are reactivated during visibility restoration.
        /// </summary>
        private void ManageVisibility(bool isRestoringVisibility = false)
        {
            if (!MaskEnabled && !isRestoringVisibility)
            {
                return;
            }

            ClippingBoundsCollider.enabled = true;
            Bounds clippingThresholdBounds = ClippingBoundsCollider.bounds;

            Renderer[] contentRenderers = ScrollContainer.GetComponentsInChildren<Renderer>(true);
            clippedRenderers.Clear();
            clippedRenderers.UnionWith(ClipBox.GetRenderersCopy());

            // Remove all renderers from clipping primitive that are not part of scroll content
            foreach (var clippedRenderer in clippedRenderers)
            {
                if (clippedRenderer != null && !clippedRenderer.transform.IsChildOf(ScrollContainer.transform))
                {
                    if (disableClippedGameObjects)
                    {
                        if (!clippedRenderer.gameObject.activeSelf)
                        {
                            clippedRenderer.gameObject.SetActive(true);
                        }
                    }
                    if (disableClippedRenderers)
                    {
                        if (!clippedRenderer.enabled)
                        {
                            clippedRenderer.enabled = true;
                        }
                    }

                    renderersToUnclip.Add(clippedRenderer);
                }
            }

            // Check render visibility
            foreach (var renderer in contentRenderers)
            {
                // All content renderers should be added to clipping primitive
                if (!isRestoringVisibility && MaskEnabled && !clippedRenderers.Contains(renderer))
                {
                    renderersToClip.Add(renderer);
                }

                // Complete or partially visible renders should be clipped and its game object should be active
                if (isRestoringVisibility
                    || clippingThresholdBounds.ContainsBounds(renderer.bounds)
                    || clippingThresholdBounds.Intersects(renderer.bounds))
                {
                    if (disableClippedGameObjects)
                    {
                        if (!renderer.gameObject.activeSelf)
                        {
                            renderer.gameObject.SetActive(true);
                        }
                    }
                    if (disableClippedRenderers)
                    {
                        if (!renderer.enabled)
                        {
                            renderer.enabled = true;
                        }
                    }
                }

                // Hidden renderer game objects should be inactive
                else
                {
                    if (disableClippedGameObjects)
                    {
                        if (renderer.gameObject.activeSelf)
                        {
                            renderer.gameObject.SetActive(false);
                        }
                    }
                    if (disableClippedRenderers)
                    {
                        if (renderer.enabled)
                        {
                            renderer.enabled = false;
                        }
                    }
                }
            }

            // Check collider visibility
            if (Application.isPlaying)
            {
                // Outer clipping bounds is used to ensure collider has minimum visibility to stay enabled
                Bounds outerClippingThresholdBounds = ClippingBoundsCollider.bounds;
                outerClippingThresholdBounds.size *= contentVisibilityThresholdRatio;

                var colliders = ScrollContainer.GetComponentsInChildren<Collider>(true);
                foreach (var collider in colliders)
                {
                    // Disabling content colliders during drag to stop interaction even if game object is inactive
                    if (!isRestoringVisibility && IsDragging)
                    {
                        if (collider.enabled)
                        {
                            collider.enabled = false;
                        }

                        continue;
                    }

                    // No need to manage collider visibility in case game object is inactive and no pointer is dragging the scroll
                    if (!isRestoringVisibility && !collider.gameObject.activeSelf)
                    {
                        continue;
                    }

                    // Temporary activating for getting bounds
                    var wasColliderEnabled = collider.enabled;

                    if (!wasColliderEnabled)
                    {
                        collider.enabled = true;
                    }

                    // Completely or partially visible colliders should be enabled if scroll is not drag engaged
                    if (isRestoringVisibility || outerClippingThresholdBounds.ContainsBounds(collider.bounds))
                    {
                        if (!wasColliderEnabled)
                        {
                            wasColliderEnabled = true;
                        }
                    }
                    // Hidden colliders should be disabled
                    else
                    {
                        if (wasColliderEnabled)
                        {
                            wasColliderEnabled = false;
                        }
                    }

                    // Update collider state or revert to previous state
                    collider.enabled = wasColliderEnabled;
                }
            }

            ClippingBoundsCollider.enabled = false;

            if (!isRestoringVisibility)
            {
                ReconcileClippingContent();
            }
        }

        /// <summary>
        /// Precalculates the total amount of travel given the scroller's current average velocity and drag.
        /// </summary>
        /// <param name="steps"><see cref="out"/> Number of steps to get our <see cref="avgVelocity"/> to effectively "zero" (0.00001).</param>
        /// <returns>The total distance the <see cref="avgVelocity"/> with <see cref="velocityDampen"/> as drag would travel.</returns>
        private float IterateFalloff(float vel, out int steps)
        {
            // Some day this should be a falloff formula, below is the number of steps. Just can't figure out how to get the right velocity.
            // float numSteps = (Mathf.Log(0.00001f)  - Mathf.Log(Mathf.Abs(avgVelocity))) / Mathf.Log(velocityFalloff);

            float newVal = 0.0f;
            float v = vel;
            steps = 0;

            while (Mathf.Abs(v) > 0.00001)
            {
                v *= velocityDampen;
                newVal += v;
                steps++;
            }

            return newVal;
        }

        /// <summary>
        /// Applies <paramref name="workingPos"/> to the <see cref="Transform.localPosition"/> of our <see cref="scrollContainer"/>
        /// </summary>
        /// <param name="workingPos">The new desired position for <see cref="scrollContainer"/> in local space</param>
        private void ApplyPosition(Vector3 workingPos)
        {
            Vector3 newScrollPos;

            switch (scrollDirection)
            {
                case ScrollDirectionType.UpAndDown:
                default:

                    newScrollPos = new Vector3(ScrollContainer.transform.localPosition.x, workingPos.y, 0.0f);
                    break;

                case ScrollDirectionType.LeftAndRight:

                    newScrollPos = new Vector3(workingPos.x, ScrollContainer.transform.localPosition.y, 0.0f);

                    break;
            }
            ScrollContainer.transform.localPosition = newScrollPos;
        }

        /// <summary>
        /// Resets the interaction state of the ScrollingObjectCollection for the next scroll.
        /// </summary>
        private void ResetInteraction()
        {
            OnTouchEnded?.Invoke(initialFocusedObject);

            // Release the pointer
            if (currentPointer != null) currentPointer.IsFocusLocked = false;
            currentPointer = null;
            initialFocusedObject = null;

            // Clear our states
            IsTouched = false;
            IsEngaged = false;
            IsDragging = false;
        }

        /// <summary>
        /// Resets the scroll offset state of the ScrollingObjectCollection.
        /// </summary>
        private void ResetScrollOffset()
        {
            MoveToIndex(0, false);
            workingScrollerPos = Vector3.zero;
            ApplyPosition(workingScrollerPos);
        }

        /// <summary>
        /// All inactive content objects and colliders are reactivated and renderers are unclipped.
        /// </summary>
        private void RestoreContentVisibility(bool autoDestroyMaterial = true)
        {
            ClearClippingBox(autoDestroyMaterial);
            ManageVisibility(true);
        }

        /// <summary>
        /// Moves the scroll container to the position that makes the tier with the tierIndex the first in the viewable area
        /// </summary>
        private void MoveToTier(int tierIndex, bool animateToPosition = true, System.Action callback = null)
        {
            if (animateScroller != null)
            {
                CurrentVelocityState = VelocityState.None;
                StopAllCoroutines();
            }

            if (scrollDirection == ScrollDirectionType.UpAndDown)
            {
                workingScrollerPos.y = tierIndex * CellHeight;

                // Clamp the working pos since we already have calculated it
                workingScrollerPos.y = Mathf.Clamp(workingScrollerPos.y, minY, MaxY);

                // Zero out the other axes
                workingScrollerPos = workingScrollerPos.Mul(Vector3.up);
            }
            else
            {
                workingScrollerPos.x = tierIndex * CellWidth * -1.0f;

                // Clamp the working pos since we already have calculated it
                workingScrollerPos.x = Mathf.Clamp(workingScrollerPos.x, MinX, maxX);

                // Zero out the other axes
                workingScrollerPos = workingScrollerPos.Mul(Vector3.right);
            }

            if (initialScrollerPos != workingScrollerPos)
            {
                CurrentVelocityState = VelocityState.Animating;

                if (animateToPosition)
                {
                    animateScroller = AnimateTo(ScrollContainer.transform.localPosition, workingScrollerPos, paginationCurve, animationLength, callback);
                    StartCoroutine(animateScroller);
                }
                else
                {
                    CurrentVelocityState = VelocityState.None; // Flagging the instant position change to trigger momentum events
                    initialScrollerPos = workingScrollerPos;
                }

                if (callback != null)
                {
                    callback?.Invoke();
                }
            }

        }

        #endregion private methods

        #region public methods

        /// <summary>
        /// Resets the ScrollingObjectCollection
        /// </summary>
        public void Reset()
        {
            ResetInteraction();
            UpdateContent();
            ResetScrollOffset();
        }

        /// <summary>
        /// Safely adds a child game object to scroll collection.
        /// </summary>
        public void AddContent(GameObject content)
        {
            content.transform.parent = ScrollContainer.transform;
            Reset();
        }

        /// <summary>
        /// Safely removes a child game object from scroll content and clipping box.
        /// </summary>
        public void RemoveItem(GameObject item)
        {
            if (item == null)
            {
                return;
            }
            var itemRenderers = item.GetComponentsInChildren<Renderer>();
            if (itemRenderers != null)
            {
                foreach (var renderer in itemRenderers)
                {
                    renderersToUnclip.Add(renderer);
                }
            }

            item.transform.parent = null;
            Reset();
        }

        /// <summary>
        /// Checks whether the given cell is visible relative to viewable area or page.
        /// </summary>
        /// <param name="cellIndex">the index of the pagination cell</param>
        /// <returns>true when cell is visible</returns>
        public bool IsCellVisible(int cellIndex)
        {
            bool isCellVisible = true;

            if (cellIndex < FirstVisibleCellIndex)
            {
                // It's above the visible area
                isCellVisible = false;
            }
            else if (cellIndex >= FirstHiddenCellIndex)
            {
                // It's below the visible area
                isCellVisible = false;
            }
            return isCellVisible;
        }

        /// <summary>
        /// Moves scroller container by a multiplier of the number of tiers in the viewable area.
        /// </summary>
        /// <param name="numberOfPages">Amount of pages to move by</param>
        /// <param name="animate"> If true, scroller will animate to new position</param>
        /// <param name="callback"> An optional action to pass in to get notified that the <see cref="ScrollingObjectCollection"/> is finished moving</param>
        public void MoveByPages(int numberOfPages, bool animate = true, System.Action callback = null)
        {
            int tierIndex = SafeDivisionInt(FirstVisibleCellIndex, CellsPerTier) + (numberOfPages * TiersPerPage);

            MoveToTier(tierIndex, animate, callback);
        }

        /// <summary>
        /// Moves scroller container a relative number of tiers of cells.
        /// </summary>
        /// <param name="numberOfTiers">Amount of tiers to move by</param>
        /// <param name="animate">if true, scroller will animate to new position</param>
        /// <param name="callback"> An optional action to pass in to get notified that the <see cref="ScrollingObjectCollection"/> is finished moving</param>
        public void MoveByTiers(int numberOfTiers, bool animate = true, System.Action callback = null)
        {
            int tierIndex = SafeDivisionInt(FirstVisibleCellIndex, CellsPerTier) + numberOfTiers;

            MoveToTier(tierIndex, animate, callback);
        }

        /// <summary>
        /// Moves scroller container to a position where the selected cell is in the first tier of the viewable area.
        /// </summary>
        /// <param name="cellIndex">Index of the cell to move to</param>
        /// <param name="animateToPosition">if true, scroller will animate to new position</param>
        /// <param name="callback"> An optional action to pass in to get notified that the <see cref="ScrollingObjectCollection"/> is finished moving</param>
        public void MoveToIndex(int cellIndex, bool animateToPosition = true, System.Action callback = null)
        {
            cellIndex = (cellIndex < 0) ? 0 : cellIndex;
            int tierIndex = SafeDivisionInt(cellIndex, CellsPerTier);

            MoveToTier(tierIndex, animateToPosition, callback);
        }

        #endregion public methods

        #region IMixedRealityPointerHandler implementation

        /// <inheritdoc/>
        void IMixedRealityPointerHandler.OnPointerUp(MixedRealityPointerEventData eventData)
        {
            if (currentPointer == null || eventData.Pointer.PointerId != currentPointer.PointerId)
            {
                return;
            }

            // Release the pointer
            currentPointer.IsTargetPositionLockedOnFocusLock = oldIsTargetPositionLockedOnFocusLock;

            if (!IsTouched && IsEngaged && animateScroller == null)
            {
                if (IsDragging)
                {
                    // Its a drag release
                    initialScrollerPos = workingScrollerPos;
                    CurrentVelocityState = VelocityState.Calculating;
                }

                ResetInteraction();
            }
        }

        /// <inheritdoc/>
        void IMixedRealityPointerHandler.OnPointerDown(MixedRealityPointerEventData eventData)
        {
            // Current pointer owns scroll interaction until scroll release happens. Ignoring any interaction with other pointers.
            if (currentPointer != null)
            {
                return;
            }

            var selectedObject = eventData.Pointer.Result?.CurrentPointerTarget;

            if (selectedObject == null || !selectedObject.transform.IsChildOf(transform))
            {
                return;
            }

            currentPointer = eventData.Pointer;
            oldIsTargetPositionLockedOnFocusLock = currentPointer.IsTargetPositionLockedOnFocusLock;

            if (!(currentPointer is IMixedRealityNearPointer) && currentPointer.Controller.IsRotationAvailable)
            {
                currentPointer.IsTargetPositionLockedOnFocusLock = false;
            }

            initialFocusedObject = selectedObject;
            currentPointer.IsFocusLocked = false; // Unwanted focus locked on children items

            // Reset the scroll state
            scrollVelocity = 0.0f;

            if (TryGetPointerPositionOnPlane(out initialPointerPos))
            {
                initialScrollerPos = ScrollContainer.transform.localPosition;
                CurrentVelocityState = VelocityState.None;

                IsTouched = false;
                IsEngaged = true;
                IsDragging = false;

                OnTouchStarted?.Invoke(initialFocusedObject);
            }
        }

        /// <inheritdoc/>
        /// Pointer Click handled during Update.
        void IMixedRealityPointerHandler.OnPointerClicked(MixedRealityPointerEventData eventData) { }

        /// <inheritdoc/>
        void IMixedRealityPointerHandler.OnPointerDragged(MixedRealityPointerEventData eventData) { }

        #endregion IMixedRealityPointerHandler implementation

        #region IMixedRealityTouchHandler implementation

        /// <inheritdoc/>
        void IMixedRealityTouchHandler.OnTouchStarted(HandTrackingInputEventData eventData)
        {
            // Current pointer owns scroll interaction until scroll release happens. Ignoring any interaction with other pointers.
            if (currentPointer != null)
            {
                return;
            }

            PokePointer pokePointer = PointerUtils.GetPointer<PokePointer>(eventData.Handedness);

            var selectedObject = pokePointer.Result?.CurrentPointerTarget;
            if (selectedObject == null || !selectedObject.transform.IsChildOf(transform))
            {
                return;
            }

            if (!HasPassedThroughFrontPlane(pokePointer))
            {
                return;
            }

            currentPointer = pokePointer;

            StopAllCoroutines();
            CurrentVelocityState = VelocityState.None;
            animateScroller = null;

            if (!IsTouched && !IsEngaged)
            {
                initialPointerPos = currentPointer.Position;
                initialFocusedObject = selectedObject;
                initialScrollerPos = ScrollContainer.transform.localPosition;

                IsTouched = true;
                IsEngaged = true;
                IsDragging = false;

                OnTouchStarted?.Invoke(initialFocusedObject);
            }
        }

        /// <inheritdoc/>
        /// Touch release handled during Update.
        void IMixedRealityTouchHandler.OnTouchCompleted(HandTrackingInputEventData eventData) { }

        /// <inheritdoc/>
        void IMixedRealityTouchHandler.OnTouchUpdated(HandTrackingInputEventData eventData)
        {
            if (currentPointer == null || eventData.SourceId != currentPointer.InputSourceParent.SourceId)
            {
                return;
            }

            if (IsDragging)
            {
                eventData.Use();
            }
        }

        #endregion IMixedRealityTouchHandler implementation

        #region IMixedRealitySourceStateHandler implementation

        void IMixedRealitySourceStateHandler.OnSourceDetected(SourceStateEventData eventData) { }

        void IMixedRealitySourceStateHandler.OnSourceLost(SourceStateEventData eventData)
        {
            if (currentPointer == null || eventData.SourceId != currentPointer.InputSourceParent.SourceId)
            {
                return;
            }

            // We'll consider this a drag release
            if (IsEngaged && animateScroller == null)
            {
                if (IsTouched || IsDragging)
                {
                    // Its a drag release
                    initialScrollerPos = workingScrollerPos;
                }

                ResetInteraction();

                CurrentVelocityState = VelocityState.Calculating;
            }
        }

        #endregion IMixedRealitySourceStateHandler implementation
    }
}
