// eslint-disable-next-line @typescript-eslint/no-unused-vars
namespace OSFramework.OSUI.Feature.Balloon {
	// Type for the Balllon Feature options
	export type BalloonOptions = {
		alignment: GlobalEnum.FloatingAlignment;
		allowedPlacements?: Array<GlobalEnum.FloatingPosition>;
		anchorElem: HTMLElement;
		arrowElem?: HTMLElement;
		focusOptions?: FocusOptions;
		position: GlobalEnum.FloatingPosition | GlobalEnum.Position;
		shape: GlobalEnum.ShapeTypes;
		useTriggerWidth?: boolean;
	};

	// Type for the Focus options
	export type FocusOptions = {
		elemToFocusOnOpen?: HTMLElement;
		focusTrapParams?: Behaviors.FocusTrapParams;
		useFocus: boolean;
	};

	/**
	 * Class for the Balloon Feature
	 *
	 * @export
	 * @class Balloon
	 * @extends {AbstractFeature<PT, BalloonOptions>}
	 * @implements {IBalloon}
	 * @template PT
	 */
	export class Balloon<PT> extends AbstractFeature<PT, BalloonOptions> implements IBalloon {
		// Store the current focused element's index
		private _currentFocusedElementIndex: number;
		// Store the listener callbacks
		private _eventBodyClick: GlobalCallbacks.Generic;
		private _eventOnKeypress: GlobalCallbacks.Generic;
		// On WindowResize Event
		private _eventOnWindowResize: GlobalCallbacks.Generic;
		// Store the Floating Util instance
		private _floatingInstance: Utils.FloatingPosition.FloatingPosition;
		// Store the Floating Util options
		private _floatingOptions: Utils.FloatingPosition.FloatingPositionConfig;
		// Store focus manager instance
		private _focusManagerInstance: Behaviors.FocusManager;
		// FocusTrap Properties
		private _focusTrapInstance: Behaviors.FocusTrap;
		// Store the focusable elements inside the balloon
		private _focusableBalloonElements: NodeListOf<Element>;
		// Flag used to deal with onBodyClick and open api concurrency methods!
		private _isOpenedByApi = false;
		// Store the onTogle custom event
		private _onToggleEvent: GlobalCallbacks.Generic;
		// Store if the pattern is open
		public isOpen = false;

		constructor(featurePattern: PT, featureElem: HTMLElement, options: BalloonOptions) {
			super(featurePattern, featureElem, options);
			this.build();
		}

		// Method to handle the body click callback, that closes the Balloon
		private _bodyClickCallback(_args: string, e: MouseEvent): void {
			const _eventTarget = e.target;

			if (this.featureElem === undefined) {
				return;
			}

			if (
				_eventTarget === this.featureOptions?.anchorElem ||
				this.featureOptions?.anchorElem.contains(_eventTarget as Node) ||
				this._isOpenedByApi ||
				this.featureElem.contains(_eventTarget as HTMLElement)
			) {
				return;
			}

			if (this.isOpen) {
				this._toggleBalloon(false, true);
				e.stopPropagation();
			}
		}

		// Add Focus Trap to the Pattern
		private _handleFocusBehavior(): void {
			const opts = {
				focusTargetElement:
					this.featureOptions.focusOptions.focusTrapParams?.focusTargetElement === undefined
						? this._floatingOptions.AnchorElem.parentElement
						: this.featureOptions.focusOptions.focusTrapParams.focusTargetElement,
				focusBottomCallback: this.featureOptions.focusOptions.focusTrapParams?.focusBottomCallback,
				focusTopCallback: this.featureOptions.focusOptions.focusTrapParams?.focusTopCallback,
			} as Behaviors.FocusTrapParams;

			this._focusTrapInstance = new Behaviors.FocusTrap(opts);

			this._focusManagerInstance = new Behaviors.FocusManager();
		}

	// Validates if the feature parent is of parent type
	private _isParentType(patternClassName: string) {
		return (
			this.featurePattern as unknown as Patterns.AbstractPattern<Patterns.AbstractConfiguration>
		).selfElement.classList.contains(patternClassName);
	}

		// Manage the focus of the elements inside the Balloon
		private _manageFocusInsideBalloon(
			arrowKeyPressed?: GlobalEnum.Keycodes.ArrowDown | GlobalEnum.Keycodes.ArrowUp
		) {
			if (this.featureOptions.focusOptions.elemToFocusOnOpen === undefined) {
				if (this._focusableBalloonElements.length === 0 || arrowKeyPressed === undefined) {
					this._currentFocusedElementIndex = undefined;
				}
				// When ArrowDown is pressed
				else if (arrowKeyPressed === GlobalEnum.Keycodes.ArrowDown) {
					// If the _currentFocusedElementIndex is undefined or is the last one
					// Then move the focus to the first focusable element
					if (
						this._currentFocusedElementIndex === undefined ||
						this._currentFocusedElementIndex >= this._focusableBalloonElements.length - 1
					)
						this._currentFocusedElementIndex = 0;
					// Otherwise, move the focus to the next focusable element
					else this._currentFocusedElementIndex = this._currentFocusedElementIndex + 1;
				}
				// When ArrowUp is pressed
				else if (arrowKeyPressed === GlobalEnum.Keycodes.ArrowUp) {
					// If the _currentFocusedElementIndex is undefined or is the first one
					// Then move the focus to the last focusable element
					if (this._currentFocusedElementIndex === undefined || this._currentFocusedElementIndex === 0)
						this._currentFocusedElementIndex = this._focusableBalloonElements.length - 1;
					// Otherwise, move the focus to the previous focusable element
					else this._currentFocusedElementIndex = this._currentFocusedElementIndex - 1;
				}

				// If the _currentFocusedElementIndex is undefined, focus on the balloon wrapper
				if (this._currentFocusedElementIndex === undefined) {
					OSUI.Helper.AsyncInvocation(() => {
						this.featureElem.focus();
					});
					// Otherwise, focus on the element corresponding ot the _currentFocusedElementIndex
				} else {
					OSUI.Helper.AsyncInvocation(() => {
						(this._focusableBalloonElements[this._currentFocusedElementIndex] as HTMLElement).focus();
					});
				}
			} else {
				this.featureOptions.focusOptions.elemToFocusOnOpen.focus();
			}
		}

		// Call methods to open or close, based on e.key and behaviour applied
		private _onKeypressCallback(e: KeyboardEvent): void {
			const isEscapedPressed = e.key === GlobalEnum.Keycodes.Escape;
			const isArrowDownPressed = e.key === GlobalEnum.Keycodes.ArrowDown;
			const isArrowUpPressed = e.key === GlobalEnum.Keycodes.ArrowUp;

			if (this.isOpen) {
				// Close the Balloon when pressing Esc
				if (isEscapedPressed) {
					this.close();
					e.stopPropagation();
					// Move the focus between the balloon's focusable elements when pressing ArrowDown or ArrowUp
				} else if (isArrowDownPressed || isArrowUpPressed) {
					this._manageFocusInsideBalloon(e.key);
					e.preventDefault(); // Prevent scroll
				}
			}

			e.stopPropagation();
		}

		// Manage the behaviour when there is a window resize!
		private _onWindowResize(): void {
			//Validates if the device is mobile or if is forcing desktop view and if the element is a child of Dropdown Server Side.
			//This prevents closing the ballon on resize (e.g. Keyboard Opens)
			if (
				this._isParentType(Patterns.Dropdown.ServerSide.Enum.CssClass.Pattern) &&
				(Helper.DeviceInfo.IsMobileDevice || Helper.DeviceInfo.HasCursorPointer === false)
			) {
				return;
			}

			// If there is a horizontal resize and the Balloon is open, close it!
			if (this.isOpen) {
				this.close();
			}
		}

		// Method to remove the event listeners
		private _removeEventListeners(): void {
			this.featureElem.removeEventListener(GlobalEnum.HTMLEvent.keyDown, this._eventOnKeypress);
			Event.DOMEvents.Listeners.GlobalListenerManager.Instance.removeHandler(
				Event.DOMEvents.Listeners.Type.BodyOnClick,
				this._eventBodyClick
			);

			if (this.featureOptions.useTriggerWidth && Helper.DeviceInfo.IsMobileDevice === false) {
				Event.DOMEvents.Listeners.GlobalListenerManager.Instance.removeHandler(
					Event.DOMEvents.Listeners.Type.WindowResize,
					this._eventOnWindowResize
				);
			}
		}

		// Add the Accessibility Attributes values
		// eslint-disable-next-line @typescript-eslint/naming-convention
		private _setA11YProperties(): void {
			Helper.Dom.Attribute.Set(this.featureElem, Constants.A11YAttributes.Aria.Hidden, (!this.isOpen).toString());

			if (this.featureOptions.focusOptions.useFocus !== false) {
				if (Helper.DeviceInfo.IsMobileDevice === false) {
					// Will handle the tabindex value of the elements inside pattern
					Helper.A11Y.SetElementsTabIndex(this.isOpen, this._focusTrapInstance.focusableElements);
				} 

				Helper.A11Y.RoleDialog(this.featureElem);

				Helper.Dom.Attribute.Set(
					this.featureElem,
					Constants.A11YAttributes.TabIndex,
					this.isOpen
						? Constants.A11YAttributes.States.TabIndexShow
						: Constants.A11YAttributes.States.TabIndexHidden
				);

				Helper.Dom.Attribute.Set(
					this._floatingOptions.AnchorElem,
					Constants.A11YAttributes.TabIndex,
					this.isOpen
						? Constants.A11YAttributes.States.TabIndexHidden
						: Constants.A11YAttributes.States.TabIndexShow
				);
			}
		}

		// Method to set the ballon width, based on the useTriggerWidth option
		private _setBalloonWidth(): void {
			const _balloonWidth = this.featureOptions.useTriggerWidth
				? this.featureOptions.anchorElem.offsetWidth + GlobalEnum.Units.Pixel
				: GlobalEnum.CssProperties.MaxContent;

			Helper.Dom.Styles.SetStyleAttribute(this.featureElem, Enum.CssCustomProperties.Width, _balloonWidth);
		}

		// Set the callbacks
		private _setCallbacks(): void {
			this._eventBodyClick = this._bodyClickCallback.bind(this);
			this._eventOnKeypress = this._onKeypressCallback.bind(this);
			this._eventOnWindowResize = this._onWindowResize.bind(this);

			// Set custom Balloon event
			this._onToggleEvent = function dispatchCustomEvent(isOpen, balloonElem) {
				const _customEvent = new CustomEvent(GlobalEnum.CustomEvent.BalloonOnToggle, {
					detail: { isOpen: isOpen, balloonElem: balloonElem },
				});
				document.dispatchEvent(_customEvent);
			};
		}

		//  Method to add event listeners
		private _setEventListeners(): void {
			if (this.featureOptions.focusOptions.useFocus !== false) {
				this.featureElem.addEventListener(GlobalEnum.HTMLEvent.keyDown, this._eventOnKeypress);
			}

			if (this.isOpen) {
				Event.DOMEvents.Listeners.GlobalListenerManager.Instance.addHandler(
					Event.DOMEvents.Listeners.Type.BodyOnClick,
					this._eventBodyClick
				);
			}

			if (this.featureOptions.useTriggerWidth && Helper.DeviceInfo.IsMobileDevice === false) {
				// Add the window resize callback that will be used to close the balloon and avoid width differences!
				Event.DOMEvents.Listeners.GlobalListenerManager.Instance.addHandler(
					Event.DOMEvents.Listeners.Type.WindowResize,
					this._eventOnWindowResize
				);
			}
		}

		// Method to toggle the open/close the Balloon
		private _toggleBalloon(
			isOpen: boolean,
			isBodyClick = false,
			arrowKeyPressed?: GlobalEnum.Keycodes.ArrowDown | GlobalEnum.Keycodes.ArrowUp
		): void {
			// Update property
			this.isOpen = isOpen;

			// Toggle class
			if (this.isOpen) {
				Helper.Dom.Styles.AddClass(this.featureElem, Enum.CssClasses.IsOpen);

				if (this.featureOptions.useTriggerWidth) {
					this._setBalloonWidth();
				}

				// Set Floating Util
				this.setFloatingBehaviour();
				// Add event listeners. This is async to prevent unnecessary calls when clicking on triggers
				Helper.AsyncInvocation(this._setEventListeners.bind(this));
			} else {
				Helper.Dom.Styles.RemoveClass(this.featureElem, Enum.CssClasses.IsOpen);
				// Remove Floating Util
				this._floatingInstance.unsetFloatingPosition();
				// remove event listeners
				this._removeEventListeners();
			}

			// Update A11y attributes
			this._setA11YProperties();

			if (this.isOpen) {
				if (this.featureOptions.focusOptions.useFocus !== false && Helper.DeviceInfo.IsMobileDevice === false) {
					// Handle focus trap logic
					this._focusManagerInstance.storeLastFocusedElement();
					this._focusTrapInstance.enableForA11y();
				}

				if (this.featureOptions.focusOptions.useFocus !== false) {
					// Store the focusable elements inside the Balloon
					this._focusableBalloonElements = this.featureElem.querySelectorAll(Constants.FocusableElems);
					// Manage the focus when opening the balloon
					this._manageFocusInsideBalloon(arrowKeyPressed);
				}
			} else if (this.featureOptions.focusOptions.useFocus !== false) {
				if (Helper.DeviceInfo.IsMobileDevice === false) {
					// Handle focus trap logic
					this._focusTrapInstance.disableForA11y();
					// Focus on last element clicked. Async to avoid conflict with closing animation
					Helper.AsyncInvocation(() => {
						this.featureElem.blur();
						if (isBodyClick === false) {
							this._focusManagerInstance.setFocusToStoredElement();
						}
					});
				}

				// Unset focus attributes
				this._focusableBalloonElements = undefined;
				this._currentFocusedElementIndex = undefined;
			}

			// Trigger the Custom Event BalloonOnToggle
			this._onToggleEvent(this.isOpen, this.featureElem);

			// Delay the _isOpenedByApi assignement in order to deal with clickOnBody() and open() api concurrency!
			Helper.AsyncInvocation(() => {
				this._isOpenedByApi = false;
			});
		}

		// Method to unset the callbaks
		private _unsetCallbacks(): void {
			this._eventBodyClick = undefined;
			this._eventOnKeypress = undefined;
			this._onToggleEvent = undefined;
			this._eventOnWindowResize = undefined;
			window[OSFramework.OSUI.GlobalEnum.CustomEvent.BalloonOnToggle] = undefined;
		}

		/**
		 * Method to build the Feature
		 *
		 * @memberof Balloon
		 */
		public build(): void {
			this._setCallbacks();
			this._setEventListeners();
			this.setFloatingConfigs();

			if (this.featureOptions.focusOptions.useFocus !== false && Helper.DeviceInfo.IsMobileDevice === false) {
				this._handleFocusBehavior();
			}

			this._setA11YProperties();
			this._setBalloonWidth();
			this.setBalloonShape();
		}

		/**
		 * Method to close the Balloon
		 *
		 * @memberof Balloon
		 */
		public close(): void {
			if (this.isOpen && this.featureElem !== undefined) {
				this._toggleBalloon(false);
			}
		}

		/**
		 * Destroy the Balloon.
		 *
		 * @memberof Balloon
		 */
		public dispose(): void {
			this._floatingInstance?.dispose();
			this._removeEventListeners();
			this._unsetCallbacks();
			super.dispose();
		}

		/**
		 * Method to open the Balloon
		 *
		 * @memberof Balloon
		 */
		public open(
			isOpenedByApi: boolean,
			arrowKeyPressed?: GlobalEnum.Keycodes.ArrowDown | GlobalEnum.Keycodes.ArrowUp
		): void {
			if (this.isOpen === false && this.featureElem !== undefined) {
				this._isOpenedByApi = isOpenedByApi;
				this._toggleBalloon(true, false, arrowKeyPressed);
			}
		}

		/**
		 * Method to handle the Shape config css variable
		 *
		 * @param {GlobalEnum.ShapeTypes} [shape]
		 * @memberof Balloon
		 */
		public setBalloonShape(shape?: GlobalEnum.ShapeTypes): void {
			if (shape !== undefined) {
				this.featureOptions.shape = shape;
			}

			Helper.Dom.Styles.SetStyleAttribute(
				this.featureElem,
				Enum.CssCustomProperties.Shape,
				'var(--border-radius-' + this.featureOptions.shape + ')'
			);
		}

		/**
		 * Method to set the Floating Util
		 *
		 * @param {boolean} [isUpdate]
		 * @memberof Balloon
		 */
		public setFloatingBehaviour(isUpdate?: boolean): void {
			if (isUpdate || this._floatingInstance === undefined) {
				this.setFloatingConfigs();

				if (isUpdate && this._floatingInstance !== undefined) {
					this._floatingInstance.update(this._floatingOptions);
				} else {
					this._floatingInstance = new Utils.FloatingPosition.Factory.NewFloatingPosition(
						this._floatingOptions,
						Utils.FloatingPosition.Enum.Provider.FloatingUI
					);
				}
			} else {
				this._floatingInstance.build();
			}
		}

		/**
		 * Method to set the Floating configs
		 *
		 * @memberof Balloon
		 */
		public setFloatingConfigs(): void {
			this._floatingOptions = {
				AutoPlacement: this.featureOptions.position === GlobalEnum.FloatingPosition.Auto,
				AnchorElem: this.featureOptions.anchorElem,
				ArrowElem: this.featureOptions.arrowElem,
				AutoPlacementOptions: {
					alignment: this.featureOptions.alignment,
					allowedPlacements: this.featureOptions.allowedPlacements,
				},
				FloatingElem: this.featureElem,
				Position: this.featureOptions.position,
				UpdatePosition: true,
			};
		}

		/**
		 * Method to update the FloatingUI options
		 *
		 * @param {Providers.OSUI.Utils.FloatingUIOptions} [floatingConfigs]
		 * @memberof Balloon
		 */
		public updateFloatingConfigs(floatingConfigs?: Utils.FloatingPosition.FloatingPositionConfig): void {
			if (floatingConfigs !== undefined) {
				this._floatingOptions = floatingConfigs;
			}

			this.setFloatingBehaviour(true);
		}

		/**
		 * Method to update the Position config on the FloatingUI
		 *
		 * @param {GlobalEnum.FloatingPosition} position
		 * @memberof Balloon
		 */
		public updatePositionOption(position: GlobalEnum.FloatingPosition): void {
			this.featureOptions.position = position;
			this.setFloatingBehaviour(true);
		}
	}
}
