// eslint-disable-next-line @typescript-eslint/no-unused-vars
namespace OSFramework.OSUI.Behaviors {
	// SpringAnimation type
	type SpringAnimationKeyframes = {
		frames: number;
		positions: Keyframe[];
	};

	// Spring Animation properties
	type SpringAnimationProperties = {
		friction: number;
		mass: number;
		tension: number;
	};

	// Spring Animation configs
	type SpringAnimationConfigs = {
		addSpringAnimation: boolean;
		springAnimationProperties: SpringAnimationProperties;
	};

	/**
	 * Class to hold the drag information
	 *
	 * @class DragParams
	 */
	class DragParams {
		public DragOrientation = GlobalEnum.Orientation.None;
		public ExpectedDirection = GlobalEnum.Direction.Right;
		public InvalidDrag = false;
		public IsMoving = false;
		public IsOpen = false;
		public IsReadyToTriggerCallback = false;
		public LastX = 0;
		public LastY = 0;
		public MoveX = 0;
		public MoveY = 0;
		public Size = undefined;
		public SpringAnimation: Animation;
		public VerticalDrag = false;
	}

	/**
	 * Class to manage an element's transform animation on drag
	 *
	 * @export
	 * @class AnimateOnDrag
	 */
	export class AnimateOnDrag {
		// Store the transition information
		private _dragParams: DragParams;

		// Store the minimal speed for a swipe to be triggered
		private readonly _swipeTriggerSpeed = 0.3;

		// Target element where the transition will be done
		private _targetElement: HTMLElement;

		constructor(target: HTMLElement) {
			this._targetElement = target;
			this._dragParams = new DragParams();
		}

		// Method to check if current gesture is within pattern boundaries
		private _checkIsDraggingInsideBounds(currentDrag: number): boolean {
			const move = this._dragParams.VerticalDrag ? this._dragParams.MoveY : this._dragParams.MoveX;
			const last = this._dragParams.VerticalDrag ? this._dragParams.LastY : this._dragParams.LastX;
			const isLeftOrUp =
				this._dragParams.ExpectedDirection === GlobalEnum.Direction.Left ||
				this._dragParams.ExpectedDirection === GlobalEnum.Direction.Up;

			const baseThreshold = move + (currentDrag - last);

			// Check correct threshold for each direction
			return isLeftOrUp
				? baseThreshold > -parseInt(this._dragParams.Size) && move + (currentDrag - last) <= 0
				: baseThreshold < parseInt(this._dragParams.Size) && move + (currentDrag - last) >= 0;
		}

		// Method to update the last x and y positions
		private _updateLastPositions(x: number, y: number): void {
			this._dragParams.LastX = x;
			this._dragParams.LastY = y;
		}

		// Method to apply the transform property on target, depending on the orientation
		// eslint-disable-next-line @typescript-eslint/naming-convention
		private _updateUI(): void {
			if (this._dragParams.IsMoving) {
				if (this._dragParams.VerticalDrag) {
					this._targetElement.style.transform = `translateY(${this._dragParams.MoveY}px)`;
				} else {
					this._targetElement.style.transform = `translateX(${this._dragParams.MoveX}px)`;
				}

				requestAnimationFrame(this._updateUI.bind(this));
			}
		}

		/**
		 * Get dragParams object
		 *
		 * @readonly
		 * @type {unknown}
		 * @memberof OSFramework.Behaviors.AnimateOnDrag
		 */
		public get dragParams(): DragParams {
			return this._dragParams;
		}

		/**
		 * Method to handle the gesture end callback
		 *
		 * @param {number} offsetX
		 * @param {number} offsetY
		 * @param {number} timeTaken
		 * @param {GlobalCallbacks.Generic} callback
		 * @param {SpringAnimationConfigs} [springProperties]
		 * @return {*}  {void}
		 * @memberof OSFramework.Behaviors.AnimateOnDrag
		 */
		public onDragEnd(
			offsetX: number,
			offsetY: number,
			timeTaken: number,
			callback: GlobalCallbacks.Generic,
			springProperties?: SpringAnimationConfigs
		): void {
			this._dragParams.IsMoving = false;

			// Remove transitions
			Helper.Dom.Styles.RemoveClass(this._targetElement, Constants.NoTransition);

			// Check if just clicked or swiped in invalid direction
			if ((offsetX === 0 && offsetY === 0) || this._dragParams.InvalidDrag) {
				this._targetElement.style.transform = '';

				return;
			}

			const checkSwipeSpeed =
				(this._dragParams.VerticalDrag ? Math.abs(offsetY) : Math.abs(offsetX)) / timeTaken >
				this._swipeTriggerSpeed;

			const sizeThreshold = -parseInt(this._dragParams.Size) / 2;

			const axisToValidate = this._dragParams.VerticalDrag ? this._dragParams.MoveY : this._dragParams.MoveX;

			// Define a interval for later checks, depending on Sidebar visibility
			const swipedHalfWidth = axisToValidate < sizeThreshold;

			// If swipe was fast enough or with sufficient move, procede to toggleSidebar
			this._dragParams.IsReadyToTriggerCallback = swipedHalfWidth || checkSwipeSpeed;

			if (this._dragParams.IsReadyToTriggerCallback) {
				callback();
			} else if (springProperties?.addSpringAnimation && this._dragParams.IsOpen) {
				// Create the animation object
				this._dragParams.SpringAnimation = SpringAnimation.CreateSpringAnimation(
					this._targetElement,
					offsetX,
					offsetY,
					this._dragParams.VerticalDrag ? GlobalEnum.Orientation.Vertical : GlobalEnum.Orientation.Horizontal,
					springProperties.springAnimationProperties
				);

				// Play the animation
				this._dragParams.SpringAnimation.play();
			}

			this._targetElement.style.transform = '';
		}

		/**
		 * Method to handle the gesture move callback
		 *
		 * @param {number} offsetX
		 * @param {number} offsetY
		 * @param {number} currentX
		 * @param {number} currentY
		 * @param {TouchEvent} event
		 * @return {*}  {void}
		 * @memberof OSFramework.Behaviors.AnimateOnDrag
		 */
		public onDragMove(
			offsetX: number,
			offsetY: number,
			currentX: number,
			currentY: number,
			event: TouchEvent
		): void {
			let _dragDirection;

			// Check X axis direction if not vertical drag
			if (!this._dragParams.VerticalDrag) {
				_dragDirection = offsetX > 0 ? GlobalEnum.Direction.Right : GlobalEnum.Direction.Left;
			} else {
				_dragDirection = offsetY < 0 ? GlobalEnum.Direction.Up : GlobalEnum.Direction.Down;
			}

			// Set direction as invalid if isOpen and swipe is on opposite direction
			this._dragParams.InvalidDrag =
				this._dragParams.IsOpen && _dragDirection !== this._dragParams.ExpectedDirection;

			// Check if swiped in wrong direction
			if (this._dragParams.InvalidDrag) {
				this._updateLastPositions(currentX, currentY);
				return;
			}

			// Check if orientation is set
			if (this._dragParams.DragOrientation === '') {
				const isHorizontal = Math.abs(offsetX) >= Math.abs(offsetY);

				this._dragParams.DragOrientation = isHorizontal
					? GlobalEnum.Orientation.Horizontal
					: GlobalEnum.Orientation.Vertical;

				requestAnimationFrame(this._updateUI.bind(this));
			}

			// Check if is scrolling
			if (
				this._dragParams.VerticalDrag === false &&
				this._dragParams.DragOrientation === GlobalEnum.Orientation.Vertical
			) {
				this._updateLastPositions(currentX, currentY);
				return;
			}

			// Prevent scrolling the page while doing gesture
			event.preventDefault();

			const IsDraggingInsideBounds = this._checkIsDraggingInsideBounds(
				this._dragParams.VerticalDrag ? currentY : currentX
			);

			// Checking if dragging inside bounds
			if (IsDraggingInsideBounds) {
				if (this._dragParams.VerticalDrag) {
					// Update y axis offset
					this._dragParams.MoveY = this._dragParams.MoveY + (currentY - this._dragParams.LastY);
				} else {
					// Update x axis offset
					this._dragParams.MoveX = this._dragParams.MoveX + (currentX - this._dragParams.LastX);
				}
			}

			this._updateLastPositions(currentX, currentY);
		}

		/**
		 * Method to handle the gesture start callback
		 *
		 * @param {boolean} verticalDrag
		 * @param {GlobalEnum.Direction} expectedDirection
		 * @param {number} currentX
		 * @param {number} currentY
		 * @param {boolean} isOpen
		 * @param {string} size
		 * @memberof OSFramework.Behaviors.AnimateOnDrag
		 */
		public onDragStart(
			verticalDrag: boolean,
			expectedDirection: GlobalEnum.Direction,
			currentX: number,
			currentY: number,
			isOpen: boolean,
			size: string
		): void {
			// Set defaults
			this._dragParams.DragOrientation = GlobalEnum.Orientation.None;
			this._dragParams.ExpectedDirection = expectedDirection;
			this._dragParams.IsMoving = true;
			this._dragParams.IsOpen = isOpen;
			this._dragParams.LastX = currentX;
			this._dragParams.LastY = currentY;
			this._dragParams.Size = size;
			this._dragParams.VerticalDrag = verticalDrag;

			if (this._dragParams.SpringAnimation) {
				this._dragParams.SpringAnimation.cancel();
			}

			if (this._dragParams.IsOpen) {
				this._dragParams.MoveX = 0;
				this._dragParams.MoveY = 0;
			} else if (this._dragParams.ExpectedDirection === GlobalEnum.Direction.Left) {
				this._dragParams.MoveX = -parseInt(this._dragParams.Size);
				this._dragParams.MoveY = -parseInt(this._dragParams.Size);
			} else {
				this._dragParams.MoveX = parseInt(this._dragParams.Size);
				this._dragParams.MoveY = parseInt(this._dragParams.Size);
			}

			Helper.Dom.Styles.AddClass(this._targetElement, Constants.NoTransition);
		}
	}

	/**
	 * Class to manage the overlay opacity on a drag transition
	 *
	 * @export
	 * @abstract
	 * @class OverlayTransition
	 */
	export abstract class OverlayTransitionOnDrag {
		/**
		 * Set overlay opacity
		 *
		 * @static
		 * @param {HTMLElement} target
		 * @param {number} currentDragValue
		 * @param {GlobalEnum.Direction} direction
		 * @param {string} size
		 * @memberof OverlayTransition
		 */
		public static Set(
			target: HTMLElement,
			currentDragValue: number,
			direction: GlobalEnum.Direction,
			size: string
		): void {
			const isLeftOrUp = direction === GlobalEnum.Direction.Left || direction === GlobalEnum.Direction.Up;
			const currentOpacity = parseInt(target.style.getPropertyValue(GlobalEnum.CSSVariables.OverlayOpacity));

			const percentageBeforeDif = (Math.abs(currentDragValue) * 100) / parseInt(size);
			const percentage = isLeftOrUp ? 0 + percentageBeforeDif : 100 - percentageBeforeDif;

			const newOpacity = Math.floor(percentage) / 100;

			if (currentOpacity !== newOpacity && newOpacity % 1 !== 0) {
				Helper.Dom.Styles.SetStyleAttribute(target, GlobalEnum.CSSVariables.OverlayOpacity, newOpacity);
			}
		}

		/**
		 * Unset overlay opacity
		 *
		 * @static
		 * @param {HTMLElement} target
		 * @memberof OverlayTransition
		 */
		public static UnSet(target: HTMLElement): void {
			Helper.Dom.Styles.SetStyleAttribute(target, GlobalEnum.CSSVariables.OverlayOpacity, 0);
		}
	}

	/**
	 * Class to manage the creation of a SpringAnimation
	 *
	 * @export
	 * @abstract
	 * @class SpringAnimation
	 */
	export abstract class SpringAnimation {
		// Method to add a spring effect on dragEnd, when the callback expected is not triggered
		// based on: https://www.kirillvasiltsov.com/writing/how-to-create-a-spring-animation-with-web-animation-api/
		private static _createSpringEffect(
			dx: number,
			dy: number,
			orientation: GlobalEnum.Orientation,
			springProperties: SpringAnimationProperties
		): SpringAnimationKeyframes {
			if (dx === 0 && dy === 0) return { positions: [], frames: 0 };

			// These 3 are used to configure the effect and are common on any spring animation
			const tension = springProperties.tension; // Tension, as it refers to how tightly-wound the spring is. The tighter the spring, the more energy is released, leading to a snappy, bouncy animation
			const friction = springProperties.friction; // friction is the force that dampens the motion. Like gravity, as you sprinkle more friction into the universe, the spring becomes less and less bouncy.
			const mass = springProperties.mass; // Mass refers to the heft of the thing we're moving. A heavier object will move more slowly, but it also has more inertia. We use 1 here for a more simple approach

			const spring_length = 0;
			const k = -tension;
			const d = -friction;
			const frame_rate = 1 / 60; // Framerate: we want 60 fps hence the framerate here is at 1/60
			const displacement_threshold = 3; // Damping is the force that slows down and eventually stops an oscillation by dissipating energy
			const isVertical = orientation === GlobalEnum.Orientation.Vertical;

			let velocity = 0;

			// positions is an array of numbers where each number represents the position of the object in a spring motion at a specific frame
			const positions = [];

			let frames = 0;
			let frames_below_threshold = 0;
			let largest_displ;

			// CHange value to be used, depending if is a vertical or horizontal drag
			let directionDisplacement = isVertical ? dy : dx;

			for (let step = 0; step <= 1000; step += 1) {
				const Fspring = k * (directionDisplacement - spring_length);

				const Fdamping = d * velocity;

				const accel = (Fspring + Fdamping) / mass;

				velocity += accel * frame_rate;

				directionDisplacement += velocity * frame_rate;

				positions.push({
					transform: isVertical
						? `translateY(${directionDisplacement}px)`
						: `translateX(${directionDisplacement}px)`,
				});

				// Save the last largest displacement so that we can compare it with threshold later
				largest_displ =
					largest_displ < 0
						? Math.max(largest_displ || -Infinity, Math.sqrt(directionDisplacement ** 2))
						: Math.min(largest_displ || Infinity, Math.sqrt(directionDisplacement ** 2));

				if (Math.abs(largest_displ) < displacement_threshold) {
					frames_below_threshold += 1;
				} else {
					frames_below_threshold = 0; // Reset the frame counter
				}

				if (frames_below_threshold >= 60) {
					frames = step;
					break;
				}
			}

			if (frames === 0) {
				frames = 1000;
			}

			return { positions, frames };
		}

		/**
		 * Method to create a Spring Animation
		 *
		 * @static
		 * @param {HTMLElement} target
		 * @param {number} offsetX
		 * @param {number} offsetY
		 * @param {GlobalEnum.Orientation} orientation
		 * @param {SpringAnimationProperties} springProperties
		 * @return {*}  {Animation}
		 * @memberof SpringAnimation
		 */
		public static CreateSpringAnimation(
			target: HTMLElement,
			offsetX: number,
			offsetY: number,
			orientation: GlobalEnum.Orientation,
			springProperties: SpringAnimationProperties
		): Animation {
			// Create the the position for each frame
			const { positions, frames } = this._createSpringEffect(offsetX, offsetY, orientation, springProperties);

			// Create the keyframe object
			const keyframes = new KeyframeEffect(target, positions, {
				duration: (frames / 60) * 1000,
				fill: GlobalEnum.KeyframesEffectOptions.FillBoth,
				easing: GlobalEnum.KeyframesEffectOptions.EasingLinear,
				iterations: 1,
			});

			// Create the animation object
			return new Animation(keyframes);
		}
	}
}
