/*!
 * OpenUI5
 * (c) Copyright 2009-2026 SAP SE or an SAP affiliate company.
 * Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
 */

// Provides utility class sap.ui.core.BlockLayerUtils
sap.ui.define([
	'sap/ui/events/jquery/EventTriggerHook',
	"sap/base/Log",
	"sap/ui/core/Lib",
	"sap/ui/thirdparty/jquery"
], function(EventTriggerHook, Log, Library, jQuery) {
	"use strict";

	/**
	 * @alias sap.ui.core.BlockLayerUtils
	 * @static
	 * @private
	 * @ui5-restricted sap.ui.core.Control
	 */
	var BlockLayerUtils = {},
		aPreventedEvents = ["focusin", "focusout", "keydown", "keypress", "keyup", "mousedown", "touchstart", "touchmove", "mouseup", "touchend", "click"],
		rForbiddenTags = /^(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr|tr)$/i;

	/**
	 * Creates a block-state for the given Control and its defined section.
	 * The returned block-state object contains all jQuery objects related to the newly added blocking layer.
	 *
	 * Example:
	 * oBlockState: {
	 * 	$parent: {...}, // The dom ref of the given control,
	 * 	$blockLayer: {...} // The dom ref of the added block-layer
	 * }
	 *
	 * Additional all event-handlers (e.g. Tab, Focus,...) are scoped to the block-state.
	 * When unblock() is called all contained elements/event-handlers will be cleaned-up completely.
	 *
	 * @param  {sap.ui.core.Control} oControl The specified control to block
	 * @param  {string} sBlockedLayerId The block layer ID
	 * @param  {string} sBlockedSection The block section ID
	 * @returns {object|undefined} The block-state object containing the parent and block layer DOM or <code>undefined</code> if no valid control instance is provided.
	 *
	 * @static
	 * @private
	 */
	BlockLayerUtils.block = function(oControl, sBlockedLayerId, sBlockedSection) {
		var oParentDomRef, sTag, oBlockState, oBlockLayerDOM;

		if (oControl) {
			// Retrieves a nested dom ref, but only if the sBlockSection is correctly prefixed with the control-id (best practice)
			oParentDomRef = oControl.getDomRef(sBlockedSection);

			// Fallback if nested dom ref could not be retrieved
			if (!oParentDomRef) {
				 oParentDomRef = oControl.getDomRef();
			}
			// if no blocked section/control DOM could be retrieved -> the control is not part of the dom anymore
			// this might happen in certain scenarios when e.g. a dialog is closed faster than the busyIndicatorDelay
			if (!oParentDomRef) {
				Log.warning("BlockLayer could not be rendered. The outer Control instance is not valid anymore or was not rendered yet.");
				return;
			}
			// Check if DOM Element where the busy indicator is supposed to be placed can handle content
			sTag = oParentDomRef.tagName;

			if (rForbiddenTags.test(sTag)) {
				Log.warning("BusyIndicator cannot be placed in elements with tag '" + sTag + "'.");
				return;
			}

			// the block-state contains all relevant DOM elements for the blocked UI sections
			oBlockLayerDOM = fnAddHTML(oParentDomRef, sBlockedLayerId);

			oBlockState = {
				$parent: jQuery(oParentDomRef),
				$blockLayer: jQuery(oBlockLayerDOM),
				control: oControl
			};

			//check if the control has static position, if this is the case we need to change it,
			//because we relay on relative/absolute/fixed positioning
			if (oBlockState.$parent.css('position') == 'static') {
				if (oParentDomRef.style && oParentDomRef.style.position === "static") {
					oBlockState.originalPosition = 'static';
				}
				oBlockState.$parent.css('position', 'relative');
				oBlockState.positionChanged = true;
			}

			fnHandleInteraction.call(oBlockState, true);
		} else {
			Log.warning("BlockLayer couldn't be created. No Control instance given.");
		}

		return oBlockState;
	};

	/**
	 * Removes the block-state
	 * @param  {object} oBlockState The block-state to be removed
	 * @static
	 * @private
	 */
	BlockLayerUtils.unblock = function(oBlockState) {
		if (oBlockState) {
			// reset the position css attribute to its original value (only used for the value "static")
			if (oBlockState.originalPosition) {
				oBlockState.$parent.css('position', oBlockState.originalPosition);
			} else if (oBlockState.positionChanged) {
				// reset the position set to 'relative' by the BlockLayer itself
				oBlockState.$parent.css('position', "");
			}

			// deregister handlers and :before and :after tabbable spans
			fnHandleInteraction.call(oBlockState, false);

			// restore focus back to control when blocklayer currently has the focus
			if (oBlockState.control && oBlockState.$blockLayer[0] && oBlockState.$blockLayer[0].contains(document.activeElement)) {
				oBlockState.control.focus();
			}

			// remove blocklayer from dom
			oBlockState.$blockLayer.remove();
		}
	};

	/**
	 * Adds the necessary ARIA attributes to the given DOM element.
	 * @param {object} oDOM The DOM element on which the ARIA attributes should be added.
	 * @private
	 */
	BlockLayerUtils.addAriaAttributes = function(oDOM) {
		var oResourceBundle = Library.getResourceBundleFor("sap.ui.core");

		oDOM.setAttribute("role", "progressbar");
		oDOM.setAttribute("aria-valuemin", "0");
		oDOM.setAttribute("aria-valuemax", "100");
		oDOM.setAttribute("aria-valuetext", oResourceBundle.getText("BUSY_VALUE_TEXT"));
		oDOM.setAttribute("alt", "");
		oDOM.setAttribute("tabindex", "0");
		oDOM.setAttribute("title", oResourceBundle.getText("BUSY_TEXT"));
	};

	/**
	 * Toggles busy indicator animation for the shared block layer.
	 * @param  {object} oBlockState The shared block-state on which the animation gets toggled
	 * @param  {boolean} bShow  Flag to indicate whether the animation should be shown or hidden
	 * @private
	 */
	BlockLayerUtils.toggleAnimationStyle = function(oBlockState, bShow) {
		var $BS = jQuery(oBlockState.$blockLayer.get(0));
		if (bShow) {
			// show busy animation in shared block-layer
			// marker class for a standalone block-layer is removed
			$BS.removeClass("sapUiHiddenBusyIndicatorAnimation");
			$BS.removeClass("sapUiBlockLayerOnly");
		} else {
			// Hide animation in shared block layer
			$BS.addClass("sapUiBlockLayerOnly");
			$BS.addClass("sapUiHiddenBusyIndicatorAnimation");
		}
	};

	/**
	 * Adds the BlockLayer to the given DOM.
	 *
	 * @param {object} oBlockSection The DOM to be appended. This can be a DOM section or the DomRef of a control.
	 * @param {string} sBlockedLayerId The controls id
	 * @returns {object} oContainer The block layer DOM
	 *
	 * @private
	 */
	function fnAddHTML (oBlockSection, sBlockedLayerId) {
		var oContainer = document.createElement("div");
		oContainer.id = sBlockedLayerId;
		oContainer.className = "sapUiBlockLayer ";

		BlockLayerUtils.addAriaAttributes(oContainer);

		oBlockSection.appendChild(oContainer);

		return oContainer;
	}

	/**
	 * Suppress interactions on all DOM elements in the blocked section.
	 *
	 * Starting with the fnHandleInteraction call, all following function calls are bound in the context of the block-state object.
	 * Meaning "this" will always reference the block-state object.
	 *
	 * @param {boolean} bEnabled New blocked state
	 * @private
	 */
	function fnHandleInteraction (bEnabled) {
		if (bEnabled) {
			var oParentDOM = this.$parent.get(0);

			if (oParentDOM){
				// Those two elements handle the tab chain so it is not possible to tab behind the busy section.
				this.fnRedirectFocus = redirectFocus.bind(this);
				this.oTabbableBefore = createTabbable(this.fnRedirectFocus);
				this.oTabbableAfter = createTabbable(this.fnRedirectFocus);

				oParentDOM.parentNode.insertBefore(this.oTabbableBefore, oParentDOM);
				oParentDOM.parentNode.insertBefore(this.oTabbableAfter, oParentDOM.nextSibling);

				this._fnSuppressDefaultAndStopPropagationHandler = suppressDefaultAndStopPropagation.bind(this);

				this._aSuppressHandler = registerInteractionHandler.call(this, this._fnSuppressDefaultAndStopPropagationHandler);
			} else {
				Log.warning("fnHandleInteraction called with bEnabled true, but no DOMRef exists!");
			}
		} else {
			if (this.oTabbableBefore) {
				removeTabbable(this.oTabbableBefore, this.fnRedirectFocus);
				delete this.oTabbableBefore;
			}
			if (this.oTabbableAfter) {
				removeTabbable(this.oTabbableAfter, this.fnRedirectFocus);
				delete this.oTabbableAfter;
			}
			delete this.fnRedirectFocus;
			//trigger handler deregistration needs to be done even if DomRef is already destroyed
			deregisterInteractionHandler.call(this, this._fnSuppressDefaultAndStopPropagationHandler);
		}

		/**
		 * Handler which suppresses event bubbling for blocked section
		 *
		 * @param {object} oEvent The event on the suppressed DOM
		 * @private
		 */
		function suppressDefaultAndStopPropagation(oEvent) {
			var bTargetIsBlockLayer = oEvent.target === this.$blockLayer.get(0),
				oTabbable;

			if (bTargetIsBlockLayer && oEvent.type === 'keydown' && oEvent.keyCode === 9) {
				// Special handling for "tab" keydown: redirect to next element before or after busy section
				Log.debug("Local Busy Indicator Event keydown handled: " + oEvent.type);
				oTabbable = oEvent.shiftKey ? this.oTabbableBefore : this.oTabbableAfter;
				oTabbable.setAttribute("tabindex", -1);
				// ignore execution of focus handler
				this.bIgnoreFocus = true;
				oTabbable.focus();
				this.bIgnoreFocus = false;
				oTabbable.setAttribute("tabindex", 0);
				oEvent.stopImmediatePropagation();

			} else if (bTargetIsBlockLayer && (oEvent.type === 'mousedown' || oEvent.type === 'touchstart')) {
				// Do not "preventDefault" to allow to focus busy indicator
				Log.debug("Local Busy Indicator click handled on busy area: " + oEvent.target.id);
				oEvent.stopImmediatePropagation();

			} else {
				Log.debug("Local Busy Indicator Event Suppressed: " + oEvent.type);
				oEvent.preventDefault();
				oEvent.stopImmediatePropagation();
			}
		}

		/**
		 * Captures and redirects focus before it reaches blocked section (from both sides)
		 *
		 * @private
		 */
		function redirectFocus() {
			if (!this.bIgnoreFocus) {
				// Redirect focus onto busy indicator (if not already focused)
				this.$blockLayer.get(0).focus();
			}
		}

		/**
		 * Create a tabbable span for the block section of the control with according focus handling.
		 *
		 * @param {function} fnRedirectFocus Focus handling function
		 * @returns {object} The span element's DOM node
		 * @private
		 */
		function createTabbable(fnRedirectFocus) {
			var oBlockSpan = document.createElement("span");

			oBlockSpan.setAttribute("tabindex", 0);
			oBlockSpan.classList.add("sapUiBlockLayerTabbable");
			oBlockSpan.addEventListener('focusin', fnRedirectFocus);

			return oBlockSpan;
		}

		/**
		 * Create a tabbable span for the block section of the control with according focus handling.
		 *
		 * @param {object} oBlockSpan The span element's DOM node
		 * @param {function} fnRedirectFocus Focus handling function
		 * @private
		 */
		function removeTabbable(oBlockSpan, fnRedirectFocus) {
			if (oBlockSpan.parentNode) {
				oBlockSpan.parentNode.removeChild(oBlockSpan);
			}
			oBlockSpan.removeEventListener('focusin', fnRedirectFocus);
		}

		/**
		 * Register event handler to suppress event within busy section
		 *
		 * @param {function} fnHandler The handler function
		 * @returns {function[]} The suppress handlers
		 */
		function registerInteractionHandler(fnHandler) {
			var aSuppressHandler = [],
				oParentDOM = this.$parent.get(0),
				oBlockLayerDOM = this.$blockLayer.get(0);

			for (var i = 0; i < aPreventedEvents.length; i++) {
				// Add event listeners with "useCapture" settings to suppress events before dispatching/bubbling starts
				oParentDOM.addEventListener(aPreventedEvents[i], fnHandler, {
					capture: true,
					passive: false
				});
				aSuppressHandler.push(EventTriggerHook.suppress(aPreventedEvents[i], oParentDOM, oBlockLayerDOM));
			}
			//for jQuery triggered events we also need the keydown handler
			this.$blockLayer.on('keydown', fnHandler);

			return aSuppressHandler;
		}

		/**
		 * Deregister event handler to suppress event within busy section
		 *
		 * @param {function} fnHandler The handler function
		 */
		function deregisterInteractionHandler(fnHandler) {
			var i,
				oParentDOM = this.$parent.get(0),
				oBlockLayerDOM = this.$blockLayer.get(0);

			if (oParentDOM) {
				for (i = 0; i < aPreventedEvents.length; i++) {
				// Remove event listeners with "useCapture" settings
					oParentDOM.removeEventListener(aPreventedEvents[i], fnHandler, {
						capture: true,
						passive: false
					});
				}
			}
			if (this._aSuppressHandler) {
				for (i = 0; i < this._aSuppressHandler.length; i++) {
					// this part should be done even no DOMRef exists
					EventTriggerHook.release(this._aSuppressHandler[i]);
				}
			}
			if (oBlockLayerDOM) {
				this.$blockLayer.off('keydown', fnHandler);
			}
		}
	}

	return BlockLayerUtils;

});
