using System;
using System.Collections.Generic;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputSystem.Utilities;

////TODO: recorded times are baked *external* times; reset touch when coming out of play mode

////REVIEW: record velocity on touches? or add method to very easily get the data?

////REVIEW: do we need to keep old touches around on activeTouches like the old UnityEngine touch API?

namespace UnityEngine.InputSystem.EnhancedTouch
{
    /// <summary>
    /// A high-level representation of a touch which automatically keeps track of a touch
    /// over time.
    /// </summary>
    /// <remarks>
    /// This API obsoletes the need for manually keeping tracking of touch IDs (<see cref="TouchControl.touchId"/>)
    /// and touch phases (<see cref="TouchControl.phase"/>) in order to tell one touch apart from another.
    ///
    /// Also, this class protects against losing touches. If a touch is shorter-lived than a single input update,
    /// <see cref="Touchscreen"/> may overwrite it with a new touch coming in in the same update whereas this class
    /// will retain all changes that happened on the touchscreen in any particular update.
    ///
    /// The API makes a distinction between "fingers" and "touches". A touch refers to one contact state change event, that is, a
    /// finger beginning to touch the screen (<see cref="TouchPhase.Began"/>), moving on the screen (<see cref="TouchPhase.Moved"/>),
    /// or being lifted off the screen (<see cref="TouchPhase.Ended"/> or <see cref="TouchPhase.Canceled"/>).
    /// A finger, on the other hand, always refers to the Nth contact on the screen.
    ///
    /// A Touch instance is a struct which only contains a reference to the actual data which is stored in unmanaged
    /// memory.
    /// </remarks>
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1724:TypeNamesShouldNotMatchNamespaces")]
    public struct Touch : IEquatable<Touch>
    {
        // The way this works is that at the core, it simply attaches one InputStateHistory per "<Touchscreen>/touch*"
        // control and then presents a public API that crawls over the recorded touch history in various ways.

        /// <summary>
        /// Whether this touch record holds valid data.
        /// </summary>
        /// <value>If true, the data contained in the touch is valid.</value>
        /// <remarks>
        /// Touch data is stored in unmanaged memory as a circular input buffer. This means that when
        /// the buffer runs out of capacity, older touch entries will get reused. When this happens,
        /// existing <c>Touch</c> instances referring to the record become invalid.
        ///
        /// This property can be used to determine whether the record held on to by the <c>Touch</c>
        /// instance is still valid.
        ///
        /// This property will be <c>false</c> for default-initialized <c>Touch</c> instances.
        ///
        /// Note that accessing most of the other properties on this struct when the touch is
        /// invalid will trigger <c>InvalidOperationException</c>.
        /// </remarks>
        public bool valid => m_TouchRecord.valid;

        /// <summary>
        /// The finger used for the touch contact. Null only for default-initialized
        /// instances of the struct.
        /// </summary>
        /// <value>Finger used for the touch contact.</value>
        /// <seealso cref="activeFingers"/>
        public Finger finger => m_Finger;

        /// <summary>
        /// Current phase of the touch.
        /// </summary>
        /// <value>Current phase of the touch.</value>
        /// <remarks>
        /// Every touch goes through a predefined cycle that starts with <see cref="TouchPhase.Began"/>,
        /// then potentially <see cref="TouchPhase.Moved"/> and/or <see cref="TouchPhase.Stationary"/>,
        /// and finally concludes with either <see cref="TouchPhase.Ended"/> or <see cref="TouchPhase.Canceled"/>.
        ///
        /// This property indicates where in the cycle the touch is.
        /// </remarks>
        /// <seealso cref="isInProgress"/>
        /// <seealso cref="TouchControl.phase"/>
        public TouchPhase phase => state.phase;

        /// <summary>
        /// Whether the touch has begun this frame, i.e. whether <see cref="phase"/> is <see cref="TouchPhase.Began"/>.
        /// </summary>
        /// <seealso cref="phase"/>
        /// <seealso cref="ended"/>
        /// <seealso cref="inProgress"/>
        public bool began => phase == TouchPhase.Began;

        /// <summary>
        /// Whether the touch is currently in progress, i.e. whether <see cref="phase"/> is either
        /// <see cref="TouchPhase.Moved"/>, <see cref="TouchPhase.Stationary"/>, or <see cref="TouchPhase.Began"/>.
        /// </summary>
        /// <seealso cref="phase"/>
        /// <seealso cref="began"/>
        /// <seealso cref="ended"/>
        public bool inProgress => phase == TouchPhase.Moved || phase == TouchPhase.Stationary || phase == TouchPhase.Began;

        /// <summary>
        /// Whether the touch has ended this frame, i.e. whether <see cref="phase"/> is either
        /// <see cref="TouchPhase.Ended"/> or <see cref="TouchPhase.Canceled"/>.
        /// </summary>
        /// <seealso cref="phase"/>
        /// <seealso cref="began"/>
        /// <seealso cref="isInProgress"/>
        public bool ended => phase == TouchPhase.Ended || phase == TouchPhase.Canceled;

        /// <summary>
        /// Unique ID of the touch as (usually) assigned by the platform.
        /// </summary>
        /// <value>Unique, non-zero ID of the touch.</value>
        /// <remarks>
        /// Each touch contact that is made with the screen receives its own unique ID which is
        /// normally assigned by the underlying platform.
        ///
        /// Note a platform may reuse touch IDs after their respective touches have finished.
        /// This means that the guarantee of uniqueness is only made with respect to <see cref="activeTouches"/>.
        ///
        /// In particular, all touches in <see cref="history"/> will have the same ID whereas
        /// touches in the a finger's <see cref="Finger.touchHistory"/> may end up having the same
        /// touch ID even though constituting different physical touch contacts.
        /// </remarks>
        /// <seealso cref="TouchControl.touchId"/>
        public int touchId => state.touchId;

        /// <summary>
        /// Normalized pressure of the touch against the touch surface.
        /// </summary>
        /// <value>Pressure level of the touch.</value>
        /// <remarks>
        /// Not all touchscreens are pressure-sensitive. If unsupported, this property will
        /// always return 0.
        ///
        /// In general, touch pressure is supported on mobile platforms only.
        ///
        /// Note that it is possible for the value to go above 1 even though it is considered normalized. The reason is
        /// that calibration on the system can put the maximum pressure point below the physically supported maximum value.
        /// </remarks>
        /// <seealso cref="TouchControl.pressure"/>
        public float pressure => state.pressure;

        /// <summary>
        /// Screen-space radius of the touch.
        /// </summary>
        /// <value>Horizontal and vertical extents of the touch contact.</value>
        /// <remarks>
        /// If supported by the underlying device, this reports the size of the touch contact based on its
        /// <see cref="screenPosition"/> center point. If not supported, this will be <c>default(Vector2)</c>.
        /// </remarks>
        /// <seealso cref="TouchControl.radius"/>
        public Vector2 radius => state.radius;

        /// <summary>
        /// Time in seconds on the same timeline as <c>Time.realTimeSinceStartup</c> when the touch began.
        /// </summary>
        /// <value>Start time of the touch.</value>
        /// <remarks>
        /// This is the value of <see cref="InputEvent.time"/> when the touch started with
        /// <see cref="phase"/> <see cref="TouchPhase.Began"/>.
        /// </remarks>
        /// <seealso cref="TouchControl.startTime"/>
        public double startTime => state.startTime;

        /// <summary>
        /// Time in seconds on the same timeline as <c>Time.realTimeSinceStartup</c> when the touch record was
        /// reported.
        /// </summary>
        /// <value>Time the touch record was reported.</value>
        /// <remarks>
        /// This is the value <see cref="InputEvent.time"/> of the event that signaled the current state
        /// change for the touch.
        /// </remarks>
        public double time => m_TouchRecord.time;

        /// <summary>
        /// The touchscreen on which the touch occurred.
        /// </summary>
        /// <value>Touchscreen associated with the touch.</value>
        public Touchscreen screen => finger.screen;

        /// <summary>
        /// Screen-space position of the touch.
        /// </summary>
        /// <value>Screen-space position of the touch.</value>
        /// <seealso cref="TouchControl.position"/>
        public Vector2 screenPosition => state.position;

        /// <summary>
        /// Screen-space position where the touch started.
        /// </summary>
        /// <value>Start position of the touch.</value>
        /// <seealso cref="TouchControl.startPosition"/>
        public Vector2 startScreenPosition => state.startPosition;

        /// <summary>
        /// Screen-space motion delta of the touch.
        /// </summary>
        /// <value>Screen-space motion delta of the touch.</value>
        /// <remarks>
        /// Note that deltas have behaviors attached to them different from most other
        /// controls. See <see cref="Pointer.delta"/> for details.
        /// </remarks>
        /// <seealso cref="TouchControl.delta"/>
        public Vector2 delta => state.delta;

        /// <summary>
        /// Number of times that the touch has been tapped in succession.
        /// </summary>
        /// <value>Indicates how many taps have been performed one after the other.</value>
        /// <remarks>
        /// Successive taps have to come within <see cref="InputSettings.multiTapDelayTime"/> for them
        /// to increase the tap count. I.e. if a new tap finishes within that time after <see cref="startTime"/>
        /// of the previous touch, the tap count is increased by one. If more than <see cref="InputSettings.multiTapDelayTime"/>
        /// passes after a tap with no successive tap, the tap count is reset to zero.
        /// </remarks>
        /// <seealso cref="TouchControl.tapCount"/>
        public int tapCount => state.tapCount;

        /// <summary>
        /// Whether the touch has performed a tap.
        /// </summary>
        /// <value>Indicates whether the touch has tapped the screen.</value>
        /// <remarks>
        /// A tap is defined as a touch that begins and ends within <see cref="InputSettings.defaultTapTime"/> and
        /// stays within <see cref="InputSettings.tapRadius"/> of its <see cref="startScreenPosition"/>. If this
        /// is the case for a touch, this button is set to 1 at the time the touch goes to <see cref="phase"/>
        /// <see cref="TouchPhase.Ended"/>.
        ///
        /// Resets to 0 only when another touch is started on the control or when the control is reset.
        /// </remarks>
        /// <seealso cref="tapCount"/>
        /// <seealso cref="InputSettings.defaultTapTime"/>
        /// <seealso cref="TouchControl.tap"/>
        public bool isTap => state.isTap;

        /// <summary>
        /// Whether the touch is currently in progress, i.e. has a <see cref="phase"/> of
        /// <see cref="TouchPhase.Began"/>, <see cref="TouchPhase.Moved"/>, or <see cref="TouchPhase.Stationary"/>.
        /// </summary>
        /// <value>Whether the touch is currently ongoing.</value>
        public bool isInProgress
        {
            get
            {
                switch (phase)
                {
                    case TouchPhase.Began:
                    case TouchPhase.Moved:
                    case TouchPhase.Stationary:
                        return true;
                }
                return false;
            }
        }

        internal uint updateStepCount => state.updateStepCount;
        internal uint uniqueId => extraData.uniqueId;

        private unsafe ref TouchState state => ref *(TouchState*)m_TouchRecord.GetUnsafeMemoryPtr();
        private unsafe ref ExtraDataPerTouchState extraData =>
            ref *(ExtraDataPerTouchState*)m_TouchRecord.GetUnsafeExtraMemoryPtr();

        /// <summary>
        /// History for this specific touch.
        /// </summary>
        /// <remarks>
        /// Unlike <see cref="Finger.touchHistory"/>, this gives the history of this touch only.
        /// </remarks>
        public TouchHistory history
        {
            get
            {
                if (!valid)
                    throw new InvalidOperationException("Touch is invalid");
                return finger.GetTouchHistory(this);
            }
        }

        /// <summary>
        /// All touches that are either on-going as of the current frame or have ended in the current frame.
        /// </summary>
        /// <remarks>
        /// A touch that begins in a frame will always have its phase set to <see cref="TouchPhase.Began"/> even
        /// if there was also movement (or even an end/cancellation) for the touch in the same frame.
        ///
        /// A touch that begins and ends in the same frame will have its <see cref="TouchPhase.Began"/> surface
        /// in that frame and then another entry with <see cref="TouchPhase.Ended"/> surface in the
        /// <em>next</em> frame. This logic implies that there can be more active touches than concurrent touches
        /// supported by the hardware/platform.
        ///
        /// A touch that begins and moves in the same frame will have its <see cref="TouchPhase.Began"/> surface
        /// in that frame and then another entry with <see cref="TouchPhase.Moved"/> and the screen motion
        /// surface in the <em>next</em> frame <em>except</em> if the touch also ended in the frame (in which
        /// case <see cref="phase"/> will be <see cref="TouchPhase.Ended"/> instead of <see cref="TouchPhase.Moved"/>).
        ///
        /// Note that the touches reported by this API do <em>not</em> necessarily have to match the contents of
        /// <see href="https://docs.unity3d.com/ScriptReference/Input-touches.html">UnityEngine.Input.touches</see>.
        /// The reason for this is that the <c>UnityEngine.Input</c> API and the Input System API flush their input
        /// queues at different points in time and may thus have a different view on available input. In particular,
        /// the Input System event queue is flushed <em>later</em> in the frame than inputs for <c>UnityEngine.Input</c>
        /// and may thus have newer inputs available. On Android, for example, touch input is gathered from a separate
        /// UI thread and fed into the input system via a "background" event queue that can gather input asynchronously.
        /// Due to this setup, touch events that will reach <c>UnityEngine.Input</c> only in the next frame may have
        /// already reached the Input System.
        ///
        /// <example>
        /// <code>
        /// void Awake()
        /// {
        ///     // Enable EnhancedTouch.
        ///     EnhancedTouchSupport.Enable();
        /// }
        ///
        /// void Update()
        /// {
        ///     foreach (var touch in Touch.activeTouches)
        ///         if (touch.began)
        ///             Debug.Log($"Touch {touch} started this frame");
        ///         else if (touch.ended)
        ///             Debug.Log($"Touch {touch} ended this frame");
        /// }
        /// </code>
        /// </example>
        /// </remarks>
        /// <exception cref="InvalidOperationException"><c>EnhancedTouch</c> has not been enabled via <see cref="EnhancedTouchSupport.Enable"/>.</exception>
        /// <seealso cref="activeFingers"/>
        public static ReadOnlyArray<Touch> activeTouches
        {
            get
            {
                EnhancedTouchSupport.CheckEnabled();
                // We lazily construct the array of active touches.
                s_GlobalState.playerState.UpdateActiveTouches();
                return new ReadOnlyArray<Touch>(s_GlobalState.playerState.activeTouches, 0, s_GlobalState.playerState.activeTouchCount);
            }
        }

        /// <summary>
        /// An array of all possible concurrent touch contacts, i.e. all concurrent touch contacts regardless of whether
        /// they are currently active or not.
        /// </summary>
        /// <remarks>
        /// For querying only active fingers, use <see cref="activeFingers"/>.
        ///
        /// The length of this array will always correspond to the maximum number of concurrent touches supported by the system.
        /// Note that the actual number of physically supported concurrent touches as determined by the current hardware and
        /// operating system may be lower than this number.
        /// </remarks>
        /// <exception cref="InvalidOperationException"><c>EnhancedTouch</c> has not been enabled via <see cref="EnhancedTouchSupport.Enable"/>.</exception>
        /// <seealso cref="activeTouches"/>
        /// <seealso cref="activeFingers"/>
        public static ReadOnlyArray<Finger> fingers
        {
            get
            {
                EnhancedTouchSupport.CheckEnabled();
                return new ReadOnlyArray<Finger>(s_GlobalState.playerState.fingers, 0, s_GlobalState.playerState.totalFingerCount);
            }
        }

        /// <summary>
        /// Set of currently active fingers, i.e. touch contacts that currently have an active touch (as defined by <see cref="activeTouches"/>).
        /// </summary>
        /// <exception cref="InvalidOperationException"><c>EnhancedTouch</c> has not been enabled via <see cref="EnhancedTouchSupport.Enable"/>.</exception>
        /// <seealso cref="activeTouches"/>
        /// <seealso cref="fingers"/>
        public static ReadOnlyArray<Finger> activeFingers
        {
            get
            {
                EnhancedTouchSupport.CheckEnabled();
                // We lazily construct the array of active fingers.
                s_GlobalState.playerState.UpdateActiveFingers();
                return new ReadOnlyArray<Finger>(s_GlobalState.playerState.activeFingers, 0, s_GlobalState.playerState.activeFingerCount);
            }
        }

        /// <summary>
        /// Return the set of <see cref="Touchscreen"/>s on which touch input is monitored.
        /// </summary>
        /// <exception cref="InvalidOperationException"><c>EnhancedTouch</c> has not been enabled via <see cref="EnhancedTouchSupport.Enable"/>.</exception>
        public static IEnumerable<Touchscreen> screens
        {
            get
            {
                EnhancedTouchSupport.CheckEnabled();
                return s_GlobalState.touchscreens;
            }
        }

        /// <summary>
        /// Event that is invoked when a finger touches a <see cref="Touchscreen"/>.
        /// </summary>
        /// <exception cref="InvalidOperationException"><c>EnhancedTouch</c> has not been enabled via <see cref="EnhancedTouchSupport.Enable"/>.</exception>
        /// <seealso cref="onFingerUp"/>
        /// <seealso cref="onFingerMove"/>
        public static event Action<Finger> onFingerDown
        {
            add
            {
                EnhancedTouchSupport.CheckEnabled();
                if (value == null)
                    throw new ArgumentNullException(nameof(value));
                s_GlobalState.onFingerDown.AddCallback(value);
            }
            remove
            {
                EnhancedTouchSupport.CheckEnabled();
                if (value == null)
                    throw new ArgumentNullException(nameof(value));
                s_GlobalState.onFingerDown.RemoveCallback(value);
            }
        }

        /// <summary>
        /// Event that is invoked when a finger stops touching a <see cref="Touchscreen"/>.
        /// </summary>
        /// <exception cref="InvalidOperationException"><c>EnhancedTouch</c> has not been enabled via <see cref="EnhancedTouchSupport.Enable"/>.</exception>
        /// <seealso cref="onFingerDown"/>
        /// <seealso cref="onFingerMove"/>
        public static event Action<Finger> onFingerUp
        {
            add
            {
                EnhancedTouchSupport.CheckEnabled();
                if (value == null)
                    throw new ArgumentNullException(nameof(value));
                s_GlobalState.onFingerUp.AddCallback(value);
            }
            remove
            {
                EnhancedTouchSupport.CheckEnabled();
                if (value == null)
                    throw new ArgumentNullException(nameof(value));
                s_GlobalState.onFingerUp.RemoveCallback(value);
            }
        }

        /// <summary>
        /// Event that is invoked when a finger that is in contact with a <see cref="Touchscreen"/> moves
        /// on the screen.
        /// </summary>
        /// <exception cref="InvalidOperationException"><c>EnhancedTouch</c> has not been enabled via <see cref="EnhancedTouchSupport.Enable"/>.</exception>
        /// <seealso cref="onFingerUp"/>
        /// <seealso cref="onFingerDown"/>
        public static event Action<Finger> onFingerMove
        {
            add
            {
                EnhancedTouchSupport.CheckEnabled();
                if (value == null)
                    throw new ArgumentNullException(nameof(value));
                s_GlobalState.onFingerMove.AddCallback(value);
            }
            remove
            {
                EnhancedTouchSupport.CheckEnabled();
                if (value == null)
                    throw new ArgumentNullException(nameof(value));
                s_GlobalState.onFingerMove.RemoveCallback(value);
            }
        }

        /*
        public static Action<Finger> onFingerTap
        {
            get { throw new NotImplementedException(); }
            set { throw new NotImplementedException(); }
        }
        */

        /// <summary>
        /// The amount of history kept for each single touch.
        /// </summary>
        /// <remarks>
        /// By default, this is zero meaning that no history information is kept for
        /// touches. Setting this to <c>Int32.maxValue</c> will cause all history from
        /// the beginning to the end of a touch being kept.
        /// </remarks>
        public static int maxHistoryLengthPerFinger
        {
            get => s_GlobalState.historyLengthPerFinger;

            ////TODO
            /*set { throw new NotImplementedException(); }*/
        }

        internal Touch(Finger finger, InputStateHistory<TouchState>.Record touchRecord)
        {
            m_Finger = finger;
            m_TouchRecord = touchRecord;
        }

        public override string ToString()
        {
            if (!valid)
                return "<None>";

            return $"{{id={touchId} finger={finger.index} phase={phase} position={screenPosition} delta={delta} time={time}}}";
        }

        public bool Equals(Touch other)
        {
            return Equals(m_Finger, other.m_Finger) && m_TouchRecord.Equals(other.m_TouchRecord);
        }

        public override bool Equals(object obj)
        {
            return obj is Touch other && Equals(other);
        }

        public override int GetHashCode()
        {
            unchecked
            {
                return ((m_Finger != null ? m_Finger.GetHashCode() : 0) * 397) ^ m_TouchRecord.GetHashCode();
            }
        }

        internal static void AddTouchscreen(Touchscreen screen)
        {
            Debug.Assert(!s_GlobalState.touchscreens.ContainsReference(screen), "Already added touchscreen");
            s_GlobalState.touchscreens.AppendWithCapacity(screen, capacityIncrement: 5);

            // Add finger tracking to states.
            s_GlobalState.playerState.AddFingers(screen);
#if UNITY_EDITOR
            s_GlobalState.editorState.AddFingers(screen);
#endif
        }

        internal static void RemoveTouchscreen(Touchscreen screen)
        {
            Debug.Assert(s_GlobalState.touchscreens.ContainsReference(screen), "Did not add touchscreen");

            // Remove from list.
            var index = s_GlobalState.touchscreens.IndexOfReference(screen);
            s_GlobalState.touchscreens.RemoveAtWithCapacity(index);

            // Remove fingers from states.
            s_GlobalState.playerState.RemoveFingers(screen);
#if UNITY_EDITOR
            s_GlobalState.editorState.RemoveFingers(screen);
#endif
        }

        ////TODO: only have this hooked when we actually need it
        internal static void BeginUpdate()
        {
#if UNITY_EDITOR
            if ((InputState.currentUpdateType == InputUpdateType.Editor && s_GlobalState.playerState.updateMask != InputUpdateType.Editor) ||
                (InputState.currentUpdateType != InputUpdateType.Editor && s_GlobalState.playerState.updateMask == InputUpdateType.Editor))
            {
                // Either swap in editor state and retain currently active player state in s_EditorState
                // or swap player state back in.
                MemoryHelpers.Swap(ref s_GlobalState.playerState, ref s_GlobalState.editorState);
            }
#endif

            // If we have any touches in activeTouches that are ended or canceled,
            // we need to clear them in the next frame.
            if (s_GlobalState.playerState.haveActiveTouchesNeedingRefreshNextUpdate)
                s_GlobalState.playerState.haveBuiltActiveTouches = false;
        }

        private readonly Finger m_Finger;
        internal InputStateHistory<TouchState>.Record m_TouchRecord;

        /// <summary>
        /// Holds global (static) touch state.
        /// </summary>
        internal struct GlobalState
        {
            internal InlinedArray<Touchscreen> touchscreens;
            internal int historyLengthPerFinger;
            internal CallbackArray<Action<Finger>> onFingerDown;
            internal CallbackArray<Action<Finger>> onFingerMove;
            internal CallbackArray<Action<Finger>> onFingerUp;

            internal FingerAndTouchState playerState;
#if UNITY_EDITOR
            internal FingerAndTouchState editorState;
#endif
        }

        private static GlobalState CreateGlobalState()
        {   // Convenient method since parameterized construction is default
            return new GlobalState { historyLengthPerFinger = 64 };
        }

        internal static GlobalState s_GlobalState = CreateGlobalState();

        internal static ISavedState SaveAndResetState()
        {
            // Save current state
            var savedState = new SavedStructState<GlobalState>(
                ref s_GlobalState,
                (ref GlobalState state) => s_GlobalState = state,
                () => { /* currently nothing to dispose */ });

            // Reset global state
            s_GlobalState = CreateGlobalState();

            return savedState;
        }

        // In scenarios where we have to support multiple different types of input updates (e.g. in editor or in
        // player when both dynamic and fixed input updates are enabled), we need more than one copy of touch state.
        // We encapsulate the state in this struct so that we can easily swap it.
        //
        // NOTE: Finger instances are per state. This means that you will actually see different Finger instances for
        //       the same finger in two different update types.
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable",
            Justification = "Managed internally")]
        internal struct FingerAndTouchState
        {
            public InputUpdateType updateMask;
            public Finger[] fingers;
            public Finger[] activeFingers;
            public Touch[] activeTouches;
            public int activeFingerCount;
            public int activeTouchCount;
            public int totalFingerCount;
            public uint lastId;
            public bool haveBuiltActiveTouches;
            public bool haveActiveTouchesNeedingRefreshNextUpdate;

            // `activeTouches` adds yet another view of input state that is different from "normal" recorded
            // state history. In this view, touches become stationary in the next update and deltas reset
            // between updates. We solve this by storing state separately for active touches. We *only* do
            // so when `activeTouches` is actually queried meaning that `activeTouches` has no overhead if
            // not used.
            public InputStateHistory<TouchState> activeTouchState;

            public void AddFingers(Touchscreen screen)
            {
                var touchCount = screen.touches.Count;
                ArrayHelpers.EnsureCapacity(ref fingers, totalFingerCount, touchCount);
                for (var i = 0; i < touchCount; ++i)
                {
                    var finger = new Finger(screen, i, updateMask);
                    ArrayHelpers.AppendWithCapacity(ref fingers, ref totalFingerCount, finger);
                }
            }

            public void RemoveFingers(Touchscreen screen)
            {
                var touchCount = screen.touches.Count;
                for (var i = 0; i < fingers.Length; ++i)
                {
                    if (fingers[i].screen != screen)
                        continue;

                    // Release unmanaged memory.
                    for (var n = 0; n < touchCount; ++n)
                        fingers[i + n].m_StateHistory.Dispose();

                    ////REVIEW: leave Fingers in place and reuse the instances?
                    ArrayHelpers.EraseSliceWithCapacity(ref fingers, ref totalFingerCount, i, touchCount);
                    break;
                }

                // Force rebuilding of active touches.
                haveBuiltActiveTouches = false;
            }

            public void Destroy()
            {
                for (var i = 0; i < totalFingerCount; ++i)
                    fingers[i].m_StateHistory.Dispose();
                activeTouchState?.Dispose();
                activeTouchState = null;
            }

            public void UpdateActiveFingers()
            {
                ////TODO: do this only once per update per activeFingers getter

                activeFingerCount = 0;
                for (var i = 0; i < totalFingerCount; ++i)
                {
                    var finger = fingers[i];
                    var lastTouch = finger.currentTouch;
                    if (lastTouch.valid)
                        ArrayHelpers.AppendWithCapacity(ref activeFingers, ref activeFingerCount, finger);
                }
            }

            public unsafe void UpdateActiveTouches()
            {
                if (haveBuiltActiveTouches)
                    return;

                // Clear activeTouches state.
                if (activeTouchState == null)
                {
                    activeTouchState = new InputStateHistory<TouchState>
                    {
                        extraMemoryPerRecord = UnsafeUtility.SizeOf<ExtraDataPerTouchState>()
                    };
                }
                else
                {
                    activeTouchState.Clear();
                    activeTouchState.m_ControlCount = 0;
                    activeTouchState.m_Controls.Clear();
                }
                activeTouchCount = 0;
                haveActiveTouchesNeedingRefreshNextUpdate = false;
                var currentUpdateStepCount = InputUpdate.s_UpdateStepCount;

                ////OPTIMIZE: Handle touchscreens that have no activity more efficiently
                ////FIXME: This is sensitive to history size; we probably need to ensure that the Begans and Endeds/Canceleds of touches are always available to us
                ////       (instead of rebuild activeTouches from scratch each time, may be more useful to update it)

                // Go through fingers and for each one, get the touches that were active this update.
                for (var i = 0; i < totalFingerCount; ++i)
                {
                    ref var finger = ref fingers[i];

                    // NOTE: Many of the operations here are inlined in order to not perform the same
                    //       checks/computations repeatedly.

                    var history = finger.m_StateHistory;
                    var touchRecordCount = history.Count;
                    if (touchRecordCount == 0)
                        continue;

                    // We're walking newest-first through the touch history but want the resulting list of
                    // active touches to be oldest first (so that a record for an ended touch comes before
                    // a record of a new touch started on the same finger). To achieve that, we insert
                    // new touch entries for any finger always at the same index (i.e. we prepend rather
                    // than append).
                    var insertAt = activeTouchCount;

                    // Go back in time through the touch records on the finger and collect any touch
                    // active in the current frame. Note that this may yield *multiple* touches for the
                    // finger as there may be touches that have ended in the frame while in the same
                    // frame, a new touch was started.
                    var currentTouchId = 0;
                    var currentTouchState = default(TouchState*);
                    var touchRecordIndex = history.UserIndexToRecordIndex(touchRecordCount - 1); // Start with last record.
                    var touchRecordHeader = history.GetRecordUnchecked(touchRecordIndex);
                    var touchRecordSize = history.bytesPerRecord;
                    var extraMemoryOffset = touchRecordSize - history.extraMemoryPerRecord;
                    for (var n = 0; n < touchRecordCount; ++n)
                    {
                        if (n != 0)
                        {
                            --touchRecordIndex;
                            if (touchRecordIndex < 0)
                            {
                                // We're wrapping around so buffer must be full. Go to last record in buffer.
                                //touchRecordIndex = history.historyDepth - history.m_HeadIndex - 1;
                                touchRecordIndex = history.historyDepth - 1;
                                touchRecordHeader = history.GetRecordUnchecked(touchRecordIndex);
                            }
                            else
                            {
                                touchRecordHeader = (InputStateHistory.RecordHeader*)((byte*)touchRecordHeader - touchRecordSize);
                            }
                        }

                        // Skip if part of an ongoing touch we've already recorded.
                        var touchState = (TouchState*)touchRecordHeader->statePtrWithoutControlIndex; // History is tied to a single TouchControl.
                        var wasUpdatedThisFrame = touchState->updateStepCount == currentUpdateStepCount;
                        if (touchState->touchId == currentTouchId && !touchState->phase.IsEndedOrCanceled())
                        {
                            // If this is the Began record for the touch and that one happened in
                            // the current frame, we force the touch phase to Began.
                            if (wasUpdatedThisFrame && touchState->phase == TouchPhase.Began)
                            {
                                Debug.Assert(currentTouchState != null, "Must have current touch record at this point");

                                currentTouchState->phase = TouchPhase.Began;
                                currentTouchState->position = touchState->position;
                                currentTouchState->delta = default;

                                haveActiveTouchesNeedingRefreshNextUpdate = true;
                            }

                            // Need to continue here as there may still be Ended touches that need to
                            // be taken into account (as in, there may actually be multiple active touches
                            // for the same finger due to how the polling API works).
                            continue;
                        }

                        // If the touch is older than the current frame and it's a touch that has
                        // ended, we don't need to look further back into the history as anything
                        // coming before that will be equally outdated.
                        if (touchState->phase.IsEndedOrCanceled())
                        {
                            // An exception are touches that both began *and* ended in the previous frame.
                            // For these, we surface the Began in the previous update and the Ended in the
                            // current frame.
                            if (!(touchState->beganInSameFrame && touchState->updateStepCount == currentUpdateStepCount - 1) &&
                                !wasUpdatedThisFrame)
                                break;
                        }

                        // Make a copy of the touch so that we can modify data like deltas and phase.
                        // NOTE: Again, not using AddRecord() for speed.
                        // NOTE: Unlike `history`, `activeTouchState` stores control indices as each active touch
                        //       will correspond to a different TouchControl.
                        var touchExtraState = (ExtraDataPerTouchState*)((byte*)touchRecordHeader + extraMemoryOffset);
                        var newRecordHeader = activeTouchState.AllocateRecord(out var newRecordIndex);
                        var newRecordState = (TouchState*)newRecordHeader->statePtrWithControlIndex;
                        var newRecordExtraState = (ExtraDataPerTouchState*)((byte*)newRecordHeader + activeTouchState.bytesPerRecord - UnsafeUtility.SizeOf<ExtraDataPerTouchState>());
                        newRecordHeader->time = touchRecordHeader->time;
                        newRecordHeader->controlIndex = ArrayHelpers.AppendWithCapacity(ref activeTouchState.m_Controls,
                            ref activeTouchState.m_ControlCount, finger.m_StateHistory.controls[0]);

                        UnsafeUtility.MemCpy(newRecordState, touchState, UnsafeUtility.SizeOf<TouchState>());
                        UnsafeUtility.MemCpy(newRecordExtraState, touchExtraState, UnsafeUtility.SizeOf<ExtraDataPerTouchState>());

                        // If the touch hasn't moved this frame, mark it stationary.
                        // EXCEPT: If we are looked at a Moved touch that also began in the same frame and that
                        //         frame is the one immediately preceding us. In that case, we want to surface the Moved
                        //         as if it happened this frame.
                        var phase = touchState->phase;
                        if ((phase == TouchPhase.Moved || phase == TouchPhase.Began) &&
                            !wasUpdatedThisFrame && !(phase == TouchPhase.Moved && touchState->beganInSameFrame && touchState->updateStepCount == currentUpdateStepCount - 1))
                        {
                            newRecordState->phase = TouchPhase.Stationary;
                            newRecordState->delta = default;
                        }
                        // If the touch wasn't updated this frame, zero out its delta.
                        else if (!wasUpdatedThisFrame && !touchState->beganInSameFrame)
                        {
                            newRecordState->delta = default;
                        }
                        else
                        {
                            // We want accumulated deltas only on activeTouches.
                            newRecordState->delta = newRecordExtraState->accumulatedDelta;
                        }

                        var newRecord = new InputStateHistory<TouchState>.Record(activeTouchState, newRecordIndex, newRecordHeader);
                        var newTouch = new Touch(finger, newRecord);

                        ArrayHelpers.InsertAtWithCapacity(ref activeTouches, ref activeTouchCount, insertAt, newTouch);

                        currentTouchId = touchState->touchId;
                        currentTouchState = newRecordState;

                        // For anything but stationary touches on the activeTouches list, we need a subsequent
                        // update in the next frame.
                        if (newTouch.phase != TouchPhase.Stationary)
                            haveActiveTouchesNeedingRefreshNextUpdate = true;
                    }
                }

                haveBuiltActiveTouches = true;
            }
        }

        internal struct ExtraDataPerTouchState
        {
            public Vector2 accumulatedDelta;

            public uint uniqueId; // Unique ID for touch *record* (i.e. multiple TouchStates having the same touchId will still each have a unique ID).

            ////TODO
            //public uint tapCount;
        }
    }
}