#region License
/* FNA - XNA4 Reimplementation for Desktop Platforms
 * Copyright 2009-2024 Ethan Lee and the MonoGame Team
 *
 * Released under the Microsoft Public License.
 * See LICENSE for details.
 */
#endregion

#region Using Statements
using System;
using System.Collections.Generic;
#endregion

namespace Microsoft.Xna.Framework.Input.Touch
{
	internal static class GestureDetector
	{
		#region Private Static Variables

		// The ID of the active finger
		private static int activeFingerId = TouchPanel.NO_FINGER;

		// The current position of the active finger
		private static Vector2 activeFingerPosition;

		/* In XNA, if the Pinch gesture was disabled mid-pinch,
		 * it would still dispatch a PinchComplete gesture once *all*
		 * fingers were off the screen. (Not just the ones involved
		 * in the pinch.) Kinda weird, right?
		 * 
		 * This flag is used to mimic that behavior.
		 */
		private static bool callBelatedPinchComplete = false;

		// The time when the most recent active Press/Release occurred
		private static DateTime eventTimestamp;

		// The IDs of all fingers currently on the screen
		private static List<int> fingerIds = new List<int>();

		// A flag to cancel Taps if a Double Tap has just occurred
		private static bool justDoubleTapped = false;

		// The position of the active finger at the last Update tick
		private static Vector2 lastUpdatePosition;

		// The position where the user first touched the screen
		private static Vector2 pressPosition;

		// The ID of the second finger (used only for Pinching)
		private static int secondFingerId = TouchPanel.NO_FINGER;

		// The current position of the second finger (used only for Pinching)
		private static Vector2 secondFingerPosition;

		// The current state of gesture detection
		private static GestureState state = GestureState.NONE;

		// The time of the most recent Update tick
		private static DateTime updateTimestamp;

		// The current velocity of the active finger
		private static Vector2 velocity;

		#endregion

		#region Private Constants

		/* How far (in pixels) the user can move their finger in a gesture
		 * before it counts as "moved". This prevents small, accidental
		 * finger movements from interfering with Hold and Tap gestures.
		 */
		private const int MOVE_THRESHOLD = 35;

		/* How fast the finger velocity must be to register as a Flick.
		 * This helps prevent accidental flicks when a drag or tap was
		 * intended.
		 */
		private const int MIN_FLICK_VELOCITY = 100;

		#endregion

		#region Private Enums

		// All possible states of Gesture detection.
		private enum GestureState
		{
			NONE,
			HOLDING,
			HELD,           /* Same as HOLDING, but after a Hold gesture has fired */
			JUST_TAPPED,
			DRAGGING_FREE,
			DRAGGING_H,
			DRAGGING_V,
			PINCHING
		};

		#endregion

		#region Internal Methods

		internal static void OnPressed(int fingerId, Vector2 touchPosition)
		{
			fingerIds.Add(fingerId);

			if (state == GestureState.PINCHING)
			{
				// None of this method applies to active pinches
				return;
			}

			// Set the active finger if there isn't one already
			if (activeFingerId == TouchPanel.NO_FINGER)
			{
				activeFingerId = fingerId;
				activeFingerPosition = touchPosition;
			}
			else
			{
				#region Pinch Initialization

				if (IsGestureEnabled(GestureType.Pinch))
				{
					// Initialize a Pinch
					secondFingerId = fingerId;
					secondFingerPosition = touchPosition;

					state = GestureState.PINCHING;
				}

				#endregion

				// No need to do anything more
				return;
			}

			#region Double Tap Detection

			if (state == GestureState.JUST_TAPPED)
			{
				if (IsGestureEnabled(GestureType.DoubleTap))
				{
					// Must tap again within 300ms of original tap's release
					TimeSpan timeSinceRelease = DateTime.UtcNow - eventTimestamp;
					if (timeSinceRelease <= TimeSpan.FromMilliseconds(300))
					{
						// If the new tap is close to the original tap
						float distance = (touchPosition - pressPosition).Length();
						if (distance <= MOVE_THRESHOLD)
						{
							// Double Tap!
							TouchPanel.EnqueueGesture(new GestureSample(
								GestureType.DoubleTap,
								GetGestureTimestamp(),
								touchPosition,
								Vector2.Zero,
								Vector2.Zero,
								Vector2.Zero,
								fingerId,
								TouchPanel.NO_FINGER
							));

							justDoubleTapped = true;
						}
					}
				}
			}

			#endregion

			state = GestureState.HOLDING;
			pressPosition = touchPosition;
			eventTimestamp = DateTime.UtcNow;
		}

		internal static void OnReleased(int fingerId, Vector2 touchPosition)
		{
			fingerIds.Remove(fingerId);

			// Handle release events seperately for Pinch gestures
			if (state == GestureState.PINCHING)
			{
				OnReleased_Pinch(fingerId, touchPosition);
				return;
			}

			// Did the user lift the active finger?
			if (fingerId == activeFingerId)
			{
				activeFingerId = TouchPanel.NO_FINGER;
			}

			// We're only interested in the very last finger to leave
			if (FNAPlatform.GetNumTouchFingers() > 0)
			{
				return;
			}

			#region Tap Detection

			if (state == GestureState.HOLDING)
			{
				// Which Tap gestures are enabled?
				bool tapEnabled = IsGestureEnabled(GestureType.Tap);
				bool dtapEnabled = IsGestureEnabled(GestureType.DoubleTap);

				if (tapEnabled || dtapEnabled)
				{
					// How long did the user hold the touch?
					TimeSpan timeHeld = DateTime.UtcNow - eventTimestamp;
					if (timeHeld < TimeSpan.FromSeconds(1))
					{
						// Don't register a Tap immediately after a Double Tap
						if (!justDoubleTapped)
						{
							if (tapEnabled)
							{
								// Tap!
								TouchPanel.EnqueueGesture(new GestureSample(
									GestureType.Tap,
									GetGestureTimestamp(),
									touchPosition,
									Vector2.Zero,
									Vector2.Zero,
									Vector2.Zero,
									fingerId,
									TouchPanel.NO_FINGER
								));
							}

							/* Even if Tap isn't enabled, we still
							* need this for Double Tap detection.
							*/
							state = GestureState.JUST_TAPPED;
						}
					}
				}
			}

			// Reset this flag so we can catch Taps in the future
			justDoubleTapped = false;

			#endregion

			#region Flick Detection

			if (IsGestureEnabled(GestureType.Flick))
			{
				// Only flick if the finger is outside the threshold and moving fast
				float distanceFromPress = (touchPosition - pressPosition).Length();
				if (distanceFromPress > MOVE_THRESHOLD &&
					velocity.Length() >= MIN_FLICK_VELOCITY)
				{
					// Flick!
					TouchPanel.EnqueueGesture(new GestureSample(
						GestureType.Flick,
						GetGestureTimestamp(),
						Vector2.Zero,
						Vector2.Zero,
						velocity,
						Vector2.Zero,
						fingerId,
						TouchPanel.NO_FINGER
					));
				}

				// Reset velocity calculation variables
				velocity = Vector2.Zero;
				lastUpdatePosition = Vector2.Zero;
				updateTimestamp = DateTime.MinValue;
			}

			#endregion

			#region Drag Complete Detection

			if (IsGestureEnabled(GestureType.DragComplete))
			{
				bool wasDragging = (state == GestureState.DRAGGING_H ||
									state == GestureState.DRAGGING_V ||
									state == GestureState.DRAGGING_FREE);
				if (wasDragging)
				{
					// Drag Complete!
					TouchPanel.EnqueueGesture(new GestureSample(
						GestureType.DragComplete,
						GetGestureTimestamp(),
						Vector2.Zero,
						Vector2.Zero,
						Vector2.Zero,
						Vector2.Zero,
						fingerId,
						TouchPanel.NO_FINGER
					));
				}
			}

			#endregion

			#region Belated Pinch Complete Detection

			if (callBelatedPinchComplete && IsGestureEnabled(GestureType.PinchComplete))
			{
				TouchPanel.EnqueueGesture(new GestureSample(
					GestureType.PinchComplete,
					GetGestureTimestamp(),
					Vector2.Zero,
					Vector2.Zero,
					Vector2.Zero,
					Vector2.Zero,
					TouchPanel.NO_FINGER,
					TouchPanel.NO_FINGER
				));
			}
			callBelatedPinchComplete = false;

			#endregion

			// Reset the state if we're not anticipating a Double Tap
			if (state != GestureState.JUST_TAPPED)
			{
				state = GestureState.NONE;
			}

			eventTimestamp = DateTime.UtcNow;
		}

		internal static void OnMoved(int fingerId, Vector2 touchPosition, Vector2 delta)
		{
			// Handle move events separately for Pinch gestures
			if (state == GestureState.PINCHING)
			{
				OnMoved_Pinch(fingerId, touchPosition, delta);
				return;
			}

			// Replace the active finger if we lost it
			if (activeFingerId == TouchPanel.NO_FINGER)
			{
				activeFingerId = fingerId;
			}

			// If this finger isn't the active finger
			if (fingerId != activeFingerId)
			{
				// We don't care about it
				return;
			}

			// Update the position
			activeFingerPosition = touchPosition;

			#region Prepare for Dragging

			// Determine which drag gestures are enabled
			bool hdrag = IsGestureEnabled(GestureType.HorizontalDrag);
			bool vdrag = IsGestureEnabled(GestureType.VerticalDrag);
			bool fdrag = IsGestureEnabled(GestureType.FreeDrag);

			if (state == GestureState.HOLDING || state == GestureState.HELD)
			{
				// Prevent accidental drags
				float distanceFromPress = (touchPosition - pressPosition).Length();
				if (distanceFromPress > MOVE_THRESHOLD)
				{
					if (hdrag && (Math.Abs(delta.X) > Math.Abs(delta.Y)))
					{
						// Horizontal Drag!
						state = GestureState.DRAGGING_H;
					}
					else if (vdrag && (Math.Abs(delta.Y) > Math.Abs(delta.X)))
					{
						// Vertical Drag!
						state = GestureState.DRAGGING_V;
					}
					else if (fdrag)
					{
						// Free Drag!
						state = GestureState.DRAGGING_FREE;
					}
					else
					{
						// No drag...
						state = GestureState.NONE;
					}
				}
			}

			#endregion

			#region Drag Detection

			if (state == GestureState.DRAGGING_H && hdrag)
			{
				// Horizontal Dragging!
				TouchPanel.EnqueueGesture(new GestureSample(
					GestureType.HorizontalDrag,
					GetGestureTimestamp(),
					touchPosition,
					Vector2.Zero,
					new Vector2(delta.X, 0),
					Vector2.Zero,
					fingerId,
					TouchPanel.NO_FINGER
				));
			}
			else if (state == GestureState.DRAGGING_V && vdrag)
			{
				// Vertical Dragging!
				TouchPanel.EnqueueGesture(new GestureSample(
					GestureType.VerticalDrag,
					GetGestureTimestamp(),
					touchPosition,
					Vector2.Zero,
					new Vector2(0, delta.Y),
					Vector2.Zero,
					fingerId,
					TouchPanel.NO_FINGER
				));
			}
			else if (state == GestureState.DRAGGING_FREE && fdrag)
			{
				// Free Dragging!
				TouchPanel.EnqueueGesture(new GestureSample(
					GestureType.FreeDrag,
					GetGestureTimestamp(),
					touchPosition,
					Vector2.Zero,
					delta,
					Vector2.Zero,
					fingerId,
					TouchPanel.NO_FINGER
				));
			}

			#endregion

			#region Handle Disabled Drags

			/* Handle the case where the current drag type
			 * was disabled *while* the user was dragging.
			 */
			if ((state == GestureState.DRAGGING_H && !hdrag) ||
				(state == GestureState.DRAGGING_V && !vdrag) ||
				(state == GestureState.DRAGGING_FREE && !fdrag))
			{
				// Reset the state
				state = GestureState.HELD;
			}

			#endregion
		}

		internal static void OnUpdate()
		{
			if (state == GestureState.PINCHING)
			{
				/* Handle the case where the Pinch gesture
				 * was disabled *while* the user was pinching.
				 */
				if (!IsGestureEnabled(GestureType.Pinch))
				{
					state = GestureState.HELD;
					secondFingerId = TouchPanel.NO_FINGER;

					// Still might need to trigger a PinchComplete
					callBelatedPinchComplete = true;
				}

				// No pinches allowed in the rest of this method!
				return;
			}

			// Must have an active finger to proceed
			if (activeFingerId == TouchPanel.NO_FINGER)
			{
				return;
			}

			#region Flick Velocity Calculation

			if (IsGestureEnabled(GestureType.Flick))
			{
				// We need one frame to pass so we can calculate delta time
				if (updateTimestamp != DateTime.MinValue)
				{
					/* The calculation below is mostly taken from MonoGame.
					 * It accumulates velocity after running it through
					 * a low-pass filter to mitigate the effect of
					 * acceleration spikes. This works pretty well,
					 * but on rare occasions the velocity will still
					 * spike by an order of magnitude.
					 * 
					 * In practice this tends to be a non-issue, but
					 * if you *really* need to avoid any spikes, you
					 * may want to consider normalizing the delta
					 * reported in the GestureSample and then scaling it
					 * to min(actualVectorLength, preferredMaxLength).
					 * 
					 * -caleb
					 */

					float dt = (float)(DateTime.UtcNow - updateTimestamp).TotalSeconds;
					Vector2 delta = activeFingerPosition - lastUpdatePosition;
					Vector2 instVelocity = delta / (0.001f + dt);
					velocity += (instVelocity - velocity) * 0.45f;
				}

				lastUpdatePosition = activeFingerPosition;
				updateTimestamp = DateTime.UtcNow;
			}

			#endregion

			#region Hold Detection

			if (IsGestureEnabled(GestureType.Hold) && state == GestureState.HOLDING)
			{
				TimeSpan timeSincePress = DateTime.UtcNow - eventTimestamp;
				if (timeSincePress >= TimeSpan.FromSeconds(1))
				{
					// Hold!
					TouchPanel.EnqueueGesture(new GestureSample(
						GestureType.Hold,
						GetGestureTimestamp(),
						activeFingerPosition,
						Vector2.Zero,
						Vector2.Zero,
						Vector2.Zero,
						activeFingerId,
						TouchPanel.NO_FINGER
					));

					state = GestureState.HELD;
				}
			}

			#endregion
		}

		#endregion

		#region Private Methods

		private static TimeSpan GetGestureTimestamp()
		{
			/* XNA calculates gesture timestamps from
			 * how long the device has been turned on.
			 */
			return TimeSpan.FromTicks(Environment.TickCount);
		}

		private static bool IsGestureEnabled(GestureType gestureType)
		{
			return (TouchPanel.EnabledGestures & gestureType) != 0;
		}

		/* The *_Pinch methods are separate from the standard event methods
		 * because they have to deal with multiple touches. It gets really
		 * messy and ugly if single-touch and multi-touch detection is all
		 * intermingled in the same methods.
		 */

		private static void OnReleased_Pinch(int fingerId, Vector2 touchPosition)
		{
			// We don't care about fingers that aren't part of the pinch
			if (fingerId != activeFingerId && fingerId != secondFingerId)
			{
				return;
			}

			if (IsGestureEnabled(GestureType.PinchComplete))
			{
				// Pinch Complete!
				TouchPanel.EnqueueGesture(new GestureSample(
					GestureType.PinchComplete,
					GetGestureTimestamp(),
					Vector2.Zero,
					Vector2.Zero,
					Vector2.Zero,
					Vector2.Zero,
					activeFingerId,
					secondFingerId
				));
			}

			// If we lost the active finger
			if (fingerId == activeFingerId)
			{
				// Then the second finger becomes the active finger
				activeFingerId = secondFingerId;
				activeFingerPosition = secondFingerPosition;
			}

			// Regardless, we no longer have a second finger
			secondFingerId = TouchPanel.NO_FINGER;

			// Attempt to replace our fallen comrade
			bool replacedSecondFinger = false;
			foreach (int id in fingerIds)
			{
				// Find a finger that's not already spoken for
				if (id != activeFingerId)
				{
					secondFingerId = id;
					replacedSecondFinger = true;
					break;
				}
			}

			if (!replacedSecondFinger)
			{
				// Aaaand we're back to a single touch
				state = GestureState.HELD;
			}
		}

		private static void OnMoved_Pinch(int fingerId, Vector2 touchPosition, Vector2 delta)
		{
			// We only care if the finger moved is involved in the pinch
			if (fingerId != activeFingerId && fingerId != secondFingerId)
			{
				return;
			}

			/* In XNA, each Pinch gesture sample contained a delta
			 * for both fingers. It was somehow able to detect
			 * simultaneous deltas at an OS level. We don't have that
			 * luxury, so instead, each Pinch gesture will contain the
			 * delta information for just _one_ of the fingers.
			 * 
			 * In practice what this means is that you'll get twice as
			 * many Pinch gestures added to the queue (one sample for
			 * each finger). This doesn't matter too much, though,
			 * since the resulting behavior is identical to XNA.
			 * 
			 * -caleb
			 */

			if (fingerId == activeFingerId)
			{
				activeFingerPosition = touchPosition;
				TouchPanel.EnqueueGesture(new GestureSample(
					GestureType.Pinch,
					GetGestureTimestamp(),
					activeFingerPosition,
					secondFingerPosition,
					delta,
					Vector2.Zero,
					activeFingerId,
					secondFingerId
				));
			}
			else
			{
				secondFingerPosition = touchPosition;
				TouchPanel.EnqueueGesture(new GestureSample(
					GestureType.Pinch,
					GetGestureTimestamp(),
					activeFingerPosition,
					secondFingerPosition,
					Vector2.Zero,
					delta,
					activeFingerId,
					secondFingerId
				));
			}
		}

		#endregion
	}
}
