// eslint-disable-next-line @typescript-eslint/no-unused-vars
namespace OSFramework.OSUI.Patterns.Submenu {
	/**
	 * Defines the interface for OutSystemsUI Patterns
	 */
	export class Submenu extends AbstractPattern<SubmenuConfig> implements ISubmenu {
		private _eventBalloonKeypress: GlobalCallbacks.Generic;
		private _eventClick: GlobalCallbacks.Generic;
		private _eventKeypress: GlobalCallbacks.Generic;
		private _eventOnMouseEnter: GlobalCallbacks.Generic;
		private _eventOnMouseLeave: GlobalCallbacks.Generic;
		// Store focus manager instance
		private _focusManagerInstance: Behaviors.FocusManager;
		// Store focus trap instance
		private _focusTrapInstance: Behaviors.FocusTrap;
		private _globalEventBody: GlobalCallbacks.Generic;
		private _hasActiveLinks = false;
		private _hasElements = false;
		private _isActive = false;
		private _isOpen = false;
		private _platformEventOnToggleCallback: GlobalCallbacks.OSGeneric;
		private _submenuActiveLinksElement: HTMLElement;
		private _submenuAllLinksElement: HTMLAnchorElement[];
		private _submenuHeaderElement: HTMLElement;
		private _submenuLinksElement: HTMLElement;
		public hasClickOutsideToClose = true;

		constructor(uniqueId: string, configs: JSON) {
			super(uniqueId, new SubmenuConfig(configs));
		}

		// Close submenu, when BodyOnCLick event is triggered
		private _bodyClickCallback(_args: string, e: MouseEvent): void {
			if (this.isBuilt && this._isOpen) {
				if (!this.selfElement.contains(e.target as HTMLElement)) {
					this.close();
				}

				e.preventDefault();
				e.stopPropagation();
			}
		}

		private _checkForActiveLinks(): void {
			this._submenuActiveLinksElement =
				Helper.Dom.ClassSelector(this._submenuLinksElement, Enum.CssClass.PatternActive) ||
				Helper.Dom.ClassSelector(this._submenuHeaderElement, Enum.CssClass.PatternActive);

			// Check if submenu contains elements with active class
			this._hasActiveLinks = !!this._submenuActiveLinksElement;
		}

		// Trigger the submenu at toggle behaviour
		private _clickCallback(): void {
			this._toggleSubmenu();
		}

		// Method to add Focus Trap to Pattern
		private _handleFocusBehavior(): void {
			const opts = {
				focusTargetElement: this._submenuLinksElement,
			} as Behaviors.FocusTrapParams;

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

			this._focusManagerInstance = new Behaviors.FocusManager();

			// Disable tabIndex to the inner focusable elements if its start closed!
			if (this._isOpen === false) {
				// Will handle the tabindex value of the elements inside pattern
				Helper.A11Y.SetElementsTabIndex(false, this._focusTrapInstance.focusableElements);
			}
		}

		// Call methods to close, based ok e.key
		private _keypressBalloonCallback(e: KeyboardEvent): void {
			e.stopPropagation();

			// Close the submenu when pressing Esc
			if (e.key === GlobalEnum.Keycodes.Escape && this._isOpen) {
				this.close();

				this._submenuHeaderElement.focus();
			}
		}

		// Call methods to open or close, based ok e.key and behavior applied
		private _keypressCallback(e: KeyboardEvent): void {
			const _clickedElem: HTMLElement = e.target as HTMLElement;
			const _closestElem: HTMLElement = _clickedElem.closest(Constants.Dot + Enum.CssClass.Pattern);
			const _isEscapedPressed = e.key === GlobalEnum.Keycodes.Escape;
			const _isEnterPressed = e.key === GlobalEnum.Keycodes.Enter;
			const _isTabPressed = e.key === GlobalEnum.Keycodes.Tab;
			const _isShiftPressed = e.shiftKey;
			const _isArrowUp = e.key === GlobalEnum.Keycodes.ArrowUp;
			const _isArrowDown = e.key === GlobalEnum.Keycodes.ArrowDown;
			const _targetAfterArrow = _isArrowUp ? this._focusTrapInstance.focusableElements.length - 1 : 0;

			// Open and select first or last item
			if (_isArrowDown || _isArrowUp) {
				this.open();
				Helper.AsyncInvocation(() => {
					this._focusTrapInstance.focusableElements[_targetAfterArrow].focus();
				});
			}

			// Open the submenu
			if (_isEnterPressed) {
				this._toggleSubmenu();
			}
			// Close the submenu when pressing Esc
			if (_isEscapedPressed && this._isOpen) {
				this.close();

				this._submenuHeaderElement.focus();
			}

			// If navigate to outside of Submenu, close it
			if (_isShiftPressed && _isTabPressed && _clickedElem === this._submenuHeaderElement) {
				if (_closestElem === this.selfElement && this._isOpen) {
					this.close();
				}
			}

			e.stopPropagation();
		}

		// Trigger the submenu after an hover behaviour
		private _onMouseEnterCallback(e: MouseEvent) {
			this.open();
			e.stopPropagation();
		}

		// Close the submenu when the cursor leaves the element
		private _onMouseLeaveCallback(e: MouseEvent) {
			// Leave event is triggered each time we moved from the scope;
			// Here we are validating if we hovering any element under _selfElem
			// If we are not hovering it means that we leave the component
			if (this.selfElement.querySelector(':hover') === null) {
				this.close();
			}

			e.preventDefault();
			e.stopPropagation();
		}

		// Remove submenu as active
		private _removeActive(): void {
			if (this._isActive) {
				Helper.Dom.Styles.RemoveClass(this.selfElement, Enum.CssClass.PatternActive);
				this._isActive = false;
			}
		}

		// Method to remove the event listeners
		private _removeEvents(): void {
			// Remove events only if has elements inside and OpenOnHover is not available
			if (this._hasElements) {
				if (this.configs.OpenOnHover === false || Helper.DeviceInfo.IsTouch) {
					this._submenuHeaderElement.removeEventListener(GlobalEnum.HTMLEvent.Click, this._eventClick);
					this._submenuHeaderElement.removeEventListener(GlobalEnum.HTMLEvent.keyDown, this._eventKeypress);
				}

				if (this.configs.OpenOnHover && Helper.DeviceInfo.IsTouch === false) {
					this.selfElement.removeEventListener(GlobalEnum.HTMLEvent.MouseEnter, this._eventOnMouseEnter);
					this.selfElement.removeEventListener(GlobalEnum.HTMLEvent.MouseLeave, this._eventOnMouseLeave);
				}
			}

			// Remove handler from Event Manager
			Event.DOMEvents.Listeners.GlobalListenerManager.Instance.removeHandler(
				Event.DOMEvents.Listeners.Type.BodyOnClick,
				this._globalEventBody
			);
		}

		// Set submenu as active
		private _setActive(): void {
			if (this._isActive === false) {
				Helper.Dom.Styles.AddClass(this.selfElement, Enum.CssClass.PatternActive);
				this._isActive = true;
			}
		}

		// Show submenu
		private _show(): void {
			if (this._isOpen === false) {
				Helper.Dom.Styles.AddClass(this.selfElement, Enum.CssClass.PatternIsOpen);

				this._submenuHeaderElement.focus();
				this._isOpen = true;

				this._updateA11yProperties();
			}
		}

		// Trigger the submenu behavior based on visibility
		private _toggleSubmenu(): void {
			if (this._isOpen) {
				this.close();
			} else {
				// Make async the method call
				Helper.AsyncInvocation(this.open.bind(this));
			}
		}

		// Set the cssClasses that should be assigned to the element on it's initialization
		private _updateA11yProperties(): void {
			Helper.A11Y.AriaExpanded(this._submenuHeaderElement, this._isOpen.toString());
			Helper.A11Y.AriaHidden(this._submenuLinksElement, (!this._isOpen).toString());

			// Update the tabindex of each link
			this._submenuAllLinksElement.forEach((item: HTMLElement) => {
				if (this._isOpen) {
					Helper.A11Y.TabIndexTrue(item);
					Helper.A11Y.AriaHiddenFalse(item);
				} else {
					Helper.A11Y.TabIndexFalse(item);
					Helper.A11Y.AriaHiddenTrue(item);
				}
			});
		}

		/**
		 * Add the A11Y attributes values
		 *
		 * @protected
		 * @memberof OSFramework.Patterns.Submenu.Submenu
		 */
		protected setA11YProperties(): void {
			// Apply the default A11Y
			Helper.A11Y.AriaHasPopupTrue(this._submenuHeaderElement);
			Helper.A11Y.TabIndexTrue(this._submenuHeaderElement);
			Helper.A11Y.AriaControls(this._submenuHeaderElement, this._submenuLinksElement.id);
			Helper.A11Y.RoleMenuItem(this._submenuHeaderElement);

			// Set the attr that will be used to define the default tabindex element
			Helper.Dom.Attribute.Set(
				this._submenuHeaderElement,
				Constants.FocusableTabIndexDefault,
				Constants.EmptyString
			);

			// Set the role menuitem on each link
			this._submenuAllLinksElement.forEach((item: HTMLElement) => {
				Helper.A11Y.RoleButton(item);
			});

			// Method that as the initial A11Y states and to be used on parameters changed
			this._updateA11yProperties();
		}

		/**
		 * Add Pattern Events
		 *
		 * @protected
		 * @memberof OSFramework.Patterns.Submenu.Submenu
		 */
		protected setCallbacks(): void {
			// Define the callbacks that will be used
			this._eventClick = this._clickCallback.bind(this);
			this._globalEventBody = this._bodyClickCallback.bind(this);
			this._eventBalloonKeypress = this._keypressBalloonCallback.bind(this);
			this._eventKeypress = this._keypressCallback.bind(this);
			this._eventOnMouseEnter = this._onMouseEnterCallback.bind(this);
			this._eventOnMouseLeave = this._onMouseLeaveCallback.bind(this);

			// Add events only if has elements inside
			if (this._hasElements) {
				// OpenOnHover is only available for devices where the hover exists
				if (this.configs.OpenOnHover === false || Helper.DeviceInfo.IsTouch) {
					this._submenuHeaderElement.addEventListener(GlobalEnum.HTMLEvent.Click, this._eventClick);
				}

				this._submenuHeaderElement.addEventListener(GlobalEnum.HTMLEvent.keyDown, this._eventKeypress);
			}
		}

		/**
		 * Update info based on htmlContent
		 *
		 * @protected
		 * @memberof OSFramework.Patterns.Submenu.Submenu
		 */
		protected setHtmlElements(): void {
			this._submenuHeaderElement = Helper.Dom.ClassSelector(this.selfElement, Enum.CssClass.PatternHeader);
			this._submenuLinksElement = Helper.Dom.ClassSelector(this.selfElement, Enum.CssClass.PatternLinks);
			this._submenuAllLinksElement = [...this._submenuLinksElement.querySelectorAll(GlobalEnum.HTMLElement.Link)];

			// Check if submenu has childs
			if (this._submenuLinksElement.children.length > 0) {
				this._hasElements = true;
			}

			// Check if submenu contains elements with active class
			Helper.AsyncInvocation(this._checkForActiveLinks.bind(this));
		}

		/**
		 * Set the cssClasses that should be assigned to the element on it's initialization
		 *
		 * @protected
		 * @memberof OSFramework.Patterns.Submenu.Submenu
		 */
		protected setInitialStates(): void {
			// Add active class to pattern based on links whith active state
			if (this._hasActiveLinks) {
				this._setActive();
			}

			// Add an identifier if the pattern has childs
			if (this._hasElements) {
				Helper.Dom.Styles.AddClass(this.selfElement, Enum.CssClass.PatternIsDropdown);
			} else {
				Helper.Dom.Styles.AddClass(this._submenuLinksElement, Enum.CssClass.PatternIsHidden);
			}

			// In case the class modifier was added by extended class
			if (Helper.Dom.Styles.ContainsClass(this.selfElement, Enum.CssClass.PatternIsOpen) && !this._isOpen) {
				this._isOpen = true;
			}
		}

		/**
		 * Remove all the assigned Events
		 *
		 * @protected
		 * @memberof OSFramework.Patterns.Submenu.Submenu
		 */
		protected unsetCallbacks(): void {
			// Reassign the elements to undefined, preventing memory leaks
			this._eventClick = undefined;
			this._eventBalloonKeypress = undefined;
			this._eventKeypress = undefined;
			this._globalEventBody = undefined;
			this._eventOnMouseEnter = undefined;
			this._eventOnMouseLeave = undefined;
			this._platformEventOnToggleCallback = undefined;
		}

		/**
		 * Reassign the HTML elements to undefined, preventing memory leaks
		 *
		 * @protected
		 * @memberof OSFramework.Patterns.Submenu.Submenu
		 */
		protected unsetHtmlElements(): void {
			this._submenuHeaderElement = undefined;
			this._submenuLinksElement = undefined;
			this._submenuAllLinksElement = undefined;
			this._submenuActiveLinksElement = undefined;
		}

		/**
		 * Method to build the Submenu
		 *
		 * @memberof OSFramework.Patterns.Submenu.Submenu
		 */
		public build(): void {
			super.build();

			this.setHtmlElements();

			this.setInitialStates();

			this._handleFocusBehavior();

			this.setA11YProperties();

			this.setCallbacks();

			this.finishBuild();
		}

		/**
		 * Method to toggle the behaviour to close submenu when clicking the body
		 *
		 * @param {boolean} clickOutsideToClose
		 * @memberof Submenu
		 */
		public clickOutsideToClose(clickOutsideToClose: boolean): void {
			this.hasClickOutsideToClose = clickOutsideToClose;

			// Make sure the event is updated when the API is triggered in runtime
			if (this.hasClickOutsideToClose) {
				Event.DOMEvents.Listeners.GlobalListenerManager.Instance.addHandler(
					Event.DOMEvents.Listeners.Type.BodyOnClick,
					this._globalEventBody
				);
			} else if (this.hasClickOutsideToClose === false) {
				Event.DOMEvents.Listeners.GlobalListenerManager.Instance.removeHandler(
					Event.DOMEvents.Listeners.Type.BodyOnClick,
					this._globalEventBody
				);
			}
		}

		/**
		 * Close Submenu
		 *
		 * @memberof OSFramework.Patterns.Submenu.Submenu
		 */
		public close(): void {
			if (this._isOpen) {
				// Remove the A11Y states to focus trap
				this._focusTrapInstance.disableForA11y();

				if (this.hasClickOutsideToClose) {
					// Remove handler from Event Manager
					Event.DOMEvents.Listeners.GlobalListenerManager.Instance.removeHandler(
						Event.DOMEvents.Listeners.Type.BodyOnClick,
						this._globalEventBody
					);
				}

				Helper.Dom.Styles.RemoveClass(this.selfElement, Enum.CssClass.PatternIsOpen);

				this._isOpen = false;

				this._updateA11yProperties();

				this._focusManagerInstance.setFocusToStoredElement();

				this._submenuLinksElement.removeEventListener(GlobalEnum.HTMLEvent.keyDown, this._eventBalloonKeypress);

				// Trigger the _platformEventOnToggleCallback callback!
				this.triggerPlatformEventCallback(this._platformEventOnToggleCallback, false);
			}
		}

		/**
		 * Destroy the Submenu
		 *
		 * @memberof OSFramework.Patterns.Submenu.Submenu
		 */
		public dispose(): void {
			// Remove event listners
			this._removeEvents();
			this.unsetCallbacks();

			// Remove unused HTML elements
			this.unsetHtmlElements();

			//Destroying the base of pattern
			super.dispose();
		}

		/**
		 * Public Open Submenu
		 *
		 * @memberof OSFramework.Patterns.Submenu.Submenu
		 */
		public open(): void {
			// Add the A11Y states to focus trap
			this._focusTrapInstance.enableForA11y();

			this._focusManagerInstance.storeLastFocusedElement();

			if (this.hasClickOutsideToClose) {
				// Add the body click handler to event manager
				Event.DOMEvents.Listeners.GlobalListenerManager.Instance.addHandler(
					Event.DOMEvents.Listeners.Type.BodyOnClick,
					this._globalEventBody
				);
			}

			this._submenuLinksElement.addEventListener(GlobalEnum.HTMLEvent.keyDown, this._eventBalloonKeypress);

			// Make async the method call
			Helper.AsyncInvocation(this._show.bind(this));

			// Trigger the _platformEventOnToggleCallback callback!
			this.triggerPlatformEventCallback(this._platformEventOnToggleCallback, true);
		}

		/**
		 * Method used to register the provider callback
		 *
		 * @param {string} eventName
		 * @param {GlobalCallbacks.OSGeneric} callback
		 * @memberof Submenu
		 */
		public registerCallback(eventName: string, callback: GlobalCallbacks.OSGeneric): void {
			switch (eventName) {
				case Enum.Events.OnToggle:
					if (this._platformEventOnToggleCallback === undefined) {
						this._platformEventOnToggleCallback = callback;
					}
					break;
				default:
					super.registerCallback(eventName, callback);
			}
		}

		/**
		 * Manage Callbacks needed to show submenu on hover
		 *
		 * @memberof Submenu
		 */
		public setOpenOnHover(): void {
			// OpenOnHover is only available for devices where the hover exists
			if (Helper.DeviceInfo.IsTouch === false) {
				if (this._hasElements) {
					this._submenuHeaderElement.removeEventListener(GlobalEnum.HTMLEvent.Click, this._eventClick);
					this._eventClick = undefined;

					Helper.Dom.Styles.AddClass(this.selfElement, Enum.CssClass.PatternIsHover);
					this.selfElement.addEventListener(GlobalEnum.HTMLEvent.MouseEnter, this._eventOnMouseEnter);
					this.selfElement.addEventListener(GlobalEnum.HTMLEvent.MouseLeave, this._eventOnMouseLeave);
				}
			}
		}

		/**
		 * Trigger on submenu onRender, to update active state
		 *
		 * @memberof OSFramework.Patterns.Submenu.Submenu
		 */
		public updateOnRender(): void {
			if (this.isBuilt) {
				// Check if there are active elements inside
				this._checkForActiveLinks();

				if (this._hasActiveLinks && this._isActive === false) {
					this._setActive();
				} else if (this._hasActiveLinks === false && this._isActive) {
					this._removeActive();
				}
			}
		}
	}
}
