// eslint-disable-next-line @typescript-eslint/no-unused-vars
namespace OSFramework.OSUI.Behaviors {
	// FocusTrap type
	export type FocusTrapParams = {
		canContainOtherPatterns?: boolean;
		focusBottomCallback?: GlobalCallbacks.Generic;
		focusTargetElement: HTMLElement;
		focusTopCallback?: GlobalCallbacks.Generic;
	};

	/**
	 * Class that represents the focus trap.
	 *
	 * @export
	 * @class FocusTrap
	 */
	export class FocusTrap {
		private _canTargetContainOtherPatts = false;
		private _firstFocusableElement: HTMLElement;
		private _focusBottomCallback: GlobalCallbacks.Generic;
		private _focusBottomHandlerCallback: GlobalCallbacks.Generic;
		private _focusTopCallback: GlobalCallbacks.Generic;
		private _focusTopHandlerCallback: GlobalCallbacks.Generic;
		private readonly _focusableElements: HTMLElement[];
		private _hasBeenPassThoughFirstOne = false;
		private _lastFocusableElement: HTMLElement;
		private _predictableBottomElement: HTMLElement;
		private _predictableTopElement: HTMLElement;
		private _targetElement: HTMLElement;

		/**
		 * Creates an instance of FocusTrap.
		 *
		 * @param {FocusTrapParams} opts
		 * @memberof FocusTrap
		 */
		constructor(opts: FocusTrapParams) {
			this._focusableElements = [];

			// Set the callbacks to be used
			this._setCallbacks();

			// Store the focus target element
			this._targetElement = opts.focusTargetElement;

			// Set the callbacks to focusable elements
			this._focusBottomCallback = opts.focusBottomCallback;
			this._focusTopCallback = opts.focusTopCallback;

			// Set the indicator that will reflect if the target element is capable to have other patterns inside
			this._canTargetContainOtherPatts = opts.canContainOtherPatterns || false;

			// Create the elements needed!
			this._buildPredictableElements();
		}

		// Method to create elements
		private _buildPredictableElements(): void {
			// Create the focusable elements
			this._predictableTopElement = document.createElement(GlobalEnum.HTMLElement.Span);
			this._predictableBottomElement = document.createElement(GlobalEnum.HTMLElement.Span);

			// Add the created elements to DOM
			this._targetElement.prepend(this._predictableTopElement);
			this._targetElement.append(this._predictableBottomElement);

			// Set the default properties of focusable elements
			this._setFocusableProperties();

			// Set focusable elements
			this._setFocusableElements();
		}

		// Handler for bottom on top element
		private _focusBottomHandler(): void {
			this._focusHandler(this._predictableBottomElement, this._focusBottomCallback);
		}

		// Method that will handle the focus behaviour
		private _focusHandler(focusableElement: HTMLElement, callback: GlobalCallbacks.Generic): void {
			// Ensure the list of focusable elements is updated!
			this._setFocusableElements();

			// Ensure callback has been not defined in order to manage the focus trap.
			if (callback === undefined) {
				if (focusableElement === this._predictableTopElement && this._hasBeenPassThoughFirstOne === false) {
					this._firstFocusableElement.focus();
					this._hasBeenPassThoughFirstOne = true;
				} else {
					this._lastFocusableElement.focus();
				}

				if (focusableElement === this._predictableBottomElement) {
					this._firstFocusableElement.focus();
					this._hasBeenPassThoughFirstOne = true;
				}
			} else {
				// If a callback has been defined, the focus will be managed on the context of that callback (pattern)
				callback();
			}
		}

		// Handler for focus on top element
		private _focusTopHandler(): void {
			this._focusHandler(this._predictableTopElement, this._focusTopCallback);
		}

		// Method that removes the added event listeners
		private _removeEventListeners(): void {
			this._predictableBottomElement.removeEventListener(
				GlobalEnum.HTMLEvent.Focus,
				this._focusBottomHandlerCallback
			);
			this._predictableTopElement.removeEventListener(GlobalEnum.HTMLEvent.Focus, this._focusTopHandlerCallback);
		}

		// Method to set the callbacks to be used
		private _setCallbacks(): void {
			this._focusBottomHandlerCallback = this._focusBottomHandler.bind(this);
			this._focusTopHandlerCallback = this._focusTopHandler.bind(this);
		}

		// Method to add the event listeners to the created elements
		private _setEventListeners(): void {
			this._predictableBottomElement.addEventListener(
				GlobalEnum.HTMLEvent.Focus,
				this._focusBottomHandlerCallback
			);
			this._predictableTopElement.addEventListener(GlobalEnum.HTMLEvent.Focus, this._focusTopHandlerCallback);
		}

		// Method to set the focusable elements to be used
		private _setFocusableElements(includeTabIndexHidden = false): void {
			// Ensure the list of focusable elements is empty
			this._focusableElements.length = 0;

			// Get all the possible focusable
			const possibleFocusableElements = Helper.Dom.GetFocusableElements(
				this._targetElement,
				includeTabIndexHidden
			);

			// Ensure we get the proper list of focusable elements
			for (const element of possibleFocusableElements) {
				// Get the closest data block from the element
				const closestDataBlock = element.closest(GlobalEnum.DataBlocksTag.DataBlock);
				// Check if the element is a child of the given targetElement
				if (closestDataBlock === this._targetElement || closestDataBlock.contains(this._targetElement)) {
					// Check if element is not a predictable element
					if (element !== this._predictableTopElement && element !== this._predictableBottomElement) {
						this._focusableElements.push(element);
					}
					// If the targetElement is a "special" element, we need to ensure that the focusable elements are not part of the inner patterns
					// Example of having an OverflowMenu inside the Sidebar and also get all the focusable elements from it as a part of the ones at the sideBar context
				} else if (this._canTargetContainOtherPatts) {
					// Ensure the element we bring from the inner pattern is the default tabindex element for it!
					if (Helper.Dom.Attribute.Has(element, Constants.FocusableTabIndexDefault)) {
						this._focusableElements.push(element);
					}
				}
			}

			// Remove the first element from array, because of predictable top element added for trapping
			this._firstFocusableElement = this._focusableElements[0];
			// Remove the last element from array, because of predictable bottom element added for trapping
			this._lastFocusableElement = this._focusableElements[this._focusableElements.length - 1];
		}

		// Method to add properties to HTML elements
		private _setFocusableProperties(): void {
			// Set CSS classes
			Helper.Dom.Styles.AddClass(this._predictableBottomElement, GlobalEnum.FocusTrapClasses.FocusTrapBottom);
			Helper.Dom.Styles.AddClass(this._predictableBottomElement, Constants.AccessibilityHideElementClass);

			Helper.Dom.Styles.AddClass(this._predictableTopElement, GlobalEnum.FocusTrapClasses.FocusTrapTop);
			Helper.Dom.Styles.AddClass(this._predictableTopElement, Constants.AccessibilityHideElementClass);

			// Hide by default the focusable elements
			this.disableForA11y();

			// Add event listeneres to focusable elements
			this._setEventListeners();
		}

		// Method that unsets all the callback defined
		private _unsetCallbacks(): void {
			this._focusBottomCallback = undefined;
			this._focusTopCallback = undefined;
			this._focusBottomHandlerCallback = undefined;
			this._focusTopHandlerCallback = undefined;
		}

		/**
		 *  Set element disabled for A11Y
		 *
		 * @memberof OSFramework.Behaviors.FocusTrap
		 */
		public disableForA11y(): void {
			this._hasBeenPassThoughFirstOne = false;

			// Unset A11Y properties from bottom & top focusable element
			Helper.A11Y.TabIndexFalse(this._predictableBottomElement);
			Helper.A11Y.TabIndexFalse(this._predictableTopElement);

			// Set A11Y AriaHidden as true
			Helper.A11Y.AriaHiddenTrue(this._predictableBottomElement);
			Helper.A11Y.AriaHiddenTrue(this._predictableTopElement);
		}

		/**
		 * Method to remove the event listeners and unset the callbacks
		 *
		 * @memberof OSFramework.Behaviors.FocusTrap
		 */
		public dispose(): void {
			// remove events added
			this._removeEventListeners();
			// unset defined callback
			this._unsetCallbacks();

			// ensure we also remove the html elements
			this._predictableTopElement.remove();
			this._predictableBottomElement.remove();
		}

		/**
		 *  Set element enabled for A11Y
		 *
		 * @memberof OSFramework.Behaviors.FocusTrap
		 */
		public enableForA11y(): void {
			// Set A11Y properties to bottom & top focusable element
			Helper.A11Y.TabIndexTrue(this._predictableBottomElement);
			Helper.A11Y.TabIndexTrue(this._predictableTopElement);

			// Set A11Y AriaHidden as false
			Helper.A11Y.AriaHiddenFalse(this._predictableBottomElement);
			Helper.A11Y.AriaHiddenFalse(this._predictableTopElement);

			// Ensure the list of focusable elements is updated, predictable elements starts with TabIndex Hidden
			this._setFocusableElements(true);
		}

		/**
		 * Getter that allows to obtain the bottomElement reference
		 *
		 * @readonly
		 * @type {HTMLElement}
		 * @memberof OSFramework.Behaviors.FocusTrap
		 */
		public get bottomElement(): HTMLElement {
			return this._predictableBottomElement;
		}

		/**
		 * Getter that allows to obtain the bottomElement reference
		 *
		 * @readonly
		 * @type {HTMLElement}
		 * @memberof OSFramework.Behaviors.FocusTrap
		 */
		public get topElement(): HTMLElement {
			return this._predictableTopElement;
		}

		/**
		 * Getter that allows to obtain the list of focusable elements
		 *
		 * @readonly
		 * @type {HTMLElement}
		 * @memberof OSFramework.Behaviors.FocusTrap
		 */
		public get focusableElements(): HTMLElement[] {
			return this._focusableElements;
		}
	}
}
