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

// Provides control sap.m.RadioButtonGroup.
sap.ui.define([
	'./library',
	'sap/ui/core/Control',
	'sap/ui/core/delegate/ItemNavigation',
	'sap/ui/core/library',
	'sap/ui/base/ManagedObjectObserver',
	'./RadioButton',
	'./RadioButtonGroupRenderer',
	"sap/base/Log"
],
	function(
		library,
		Control,
		ItemNavigation,
		coreLibrary,
		ManagedObjectObserver,
		RadioButton,
		RadioButtonGroupRenderer,
		Log
		) {
			"use strict";

			// shortcut for sap.ui.core.TextDirection
			var TextDirection = coreLibrary.TextDirection;

			// shortcut for sap.ui.core.ValueState
			var ValueState = coreLibrary.ValueState;

			/**
			 * Constructor for a new RadioButtonGroup.
			 *
			 * @param {string} [sId] ID for the new control, generated automatically if no ID is given
			 * @param {object} [mSettings] Initial settings for the new control
			 * A wrapper control for a group of radio buttons.
			 * @class
			 * This control is used as a wrapper for a group of {@link sap.m.RadioButton} controls, which can be used as a single UI element.
			 * You can select only one of the grouped radio buttons at a time.
			 * <h3>Structure</h3>
			 * <ul>
			 * <li>The radio buttons are stored in the <code>buttons</code> aggregation.</li>
			 * <li>By setting the <code>columns</code> property, you can create layouts like a 'matrix', 'vertical' or 'horizontal'.</li>
			 * <li><b>Note:</b>For proper display on all devices, we recommend creating radio button groups with only one row or only one column.</li>
			 * </ul>
			 * <h3>Usage</h3>
			 * <h4>When to use:</h4>
			 * <ul>
			 * <li>You want to attach a single event handler on a group of buttons, rather than on each individual button.</li>
			 * </ul>
			 * <h4>When not to use:</h4>
			 * <ul>
			 * <li>Do not put two radio button groups right next to each other as it is difficult to determine which buttons belong to which group.</li>
			 * </ul>
			 * @extends sap.ui.core.Control
			 * @implements sap.ui.core.IFormContent
			 *
			 * @author SAP SE
			 * @version 1.108.49
			 *
			 * @constructor
			 * @public
			 * @since 1.25.0
			 * @alias sap.m.RadioButtonGroup
			 */
			var RadioButtonGroup = Control.extend("sap.m.RadioButtonGroup", /** @lends sap.m.RadioButtonGroup.prototype */ {
				metadata : {

					interfaces : ["sap.ui.core.IFormContent"],
					library : "sap.m",
					designtime: "sap/m/designtime/RadioButtonGroup.designtime",
					properties : {

						/**
						 * Specifies the width of the RadioButtonGroup.
						 */
						width : {type : "sap.ui.core.CSSSize", group : "Dimension", defaultValue : null},

						/**
						 * Specifies the maximum number of radio buttons displayed in one line.
						 */
						columns : {type : "int", group : "Appearance", defaultValue : 1},

						/**
						 * Specifies whether the user can change the selected value of the RadioButtonGroup.
						 * When the property is set to false, the control obtains visual styles
						 * different from its visual styles for the normal and the disabled state.
						 * Additionally, the control is no longer interactive, but can receive focus.
						 */
						editable : {type : "boolean", group : "Behavior", defaultValue : true},

						/**
						 * Marker for the correctness of the current value e.g., Error, Success, etc.
						 * Changing this property will also change the state of all radio buttons inside the group.
						 * Note: Setting this attribute to sap.ui.core.ValueState.Error when the accessibility feature is enabled,
						 * sets the value of the invalid property for the whole RadioButtonGroup to "true".
						 */
						valueState : {type : "sap.ui.core.ValueState", group : "Data", defaultValue : ValueState.None},

						/**
						 * Determines the index of the selected/checked RadioButton. Default is 0.
						 * If no radio button is selected, the selectedIndex property will return -1.
						 */
						selectedIndex : {type : "int", group : "Data", defaultValue : 0},

						/**
						 * Switches the enabled state of the control. All radio buttons inside a disabled group are disabled.
						 */
						enabled : {type : "boolean", group : "Behavior", defaultValue : true},

						/**
						 * This property specifies the element's text directionality with enumerated options. By default, the control inherits text direction from the DOM.
						 * @since 1.28.0
						 */
						textDirection : {type : "sap.ui.core.TextDirection", group : "Appearance", defaultValue : TextDirection.Inherit}
					},
					defaultAggregation : "buttons",
					aggregations : {

						/**
						 * Returns a list of the RadioButtons in a RadioButtonGroup
						 */
						buttons : {type : "sap.m.RadioButton", multiple : true, singularName : "button", bindable : "bindable"}
					},
					associations : {

						/**
						 * Association to controls / IDs which describe this control (see WAI-ARIA attribute aria-describedby).
						 */
						ariaDescribedBy : {type : "sap.ui.core.Control", multiple : true, singularName : "ariaDescribedBy"},

						/**
						 * Association to controls / IDs which label this control (see WAI-ARIA attribute aria-labelledby).
						 */
						ariaLabelledBy : {type : "sap.ui.core.Control", multiple : true, singularName : "ariaLabelledBy"}
					},
					events : {

						/**
						 * Fires when selection is changed by user interaction.
						 */
						select : {
							parameters : {

								/**
								 * Index of the selected RadioButton.
								 */
								selectedIndex : {type : "int"}
							}
						}
					}
				},

				renderer: RadioButtonGroupRenderer
			});

			RadioButtonGroup.prototype.init = function() {
				this._iSelectionNumber = -1;
				this._oObserver = new ManagedObjectObserver(this._observeChanges.bind(this));
				this._oObserver.observe(this, {
					aggregations: ["buttons"]
				});
			};

			/**
			 * Exits the radio button group.
			 *
			 * @public
			 */
			RadioButtonGroup.prototype.exit = function() {

				this.destroyButtons();

				if (this._oItemNavigation) {
					this.removeDelegate(this._oItemNavigation);
					this._oItemNavigation.destroy();
					delete this._oItemNavigation;
				}

				if (this._oObserver) {
					this._oObserver.disconnect();
					this._oObserver = null;
				}
			};

			/**
			 * Overwrites the onBeforeRendering method.
			 *
			 * @public
			 */
			RadioButtonGroup.prototype.onBeforeRendering = function() {
				var aButtons = this.getButtons();
				var bEditable = this.getEditable();
				var iCurrentSelectedButtonSelectionNumber = -1;

				aButtons.forEach(function (oRadioButton) {
					if (oRadioButton.getSelected()) {
						iCurrentSelectedButtonSelectionNumber = Math.max(iCurrentSelectedButtonSelectionNumber, oRadioButton._iSelectionNumber);
					}
				});

				if (iCurrentSelectedButtonSelectionNumber === -1 && this._iSelectionNumber === -1) {
					this._iSelectionNumber = RadioButton.getNextSelectionNumber();
				}

				aButtons.forEach(function (oRadioButton, i) {
					oRadioButton._setEditableParent(bEditable);
					if (i === this.getSelectedIndex() && this._iSelectionNumber > iCurrentSelectedButtonSelectionNumber && oRadioButton.isPropertyInitial("selected")) {
						oRadioButton.setSelected(true);
					}
				}, this);

				if (this.aRBs){
					var oValueState = this.getValueState();
					this.aRBs.forEach(function (oRB) { oRB.setValueState(oValueState); });
				}
			};

			/**
			 * Overwrites the onAfterRendering
			 *
			 * @public
			 */
			RadioButtonGroup.prototype.onAfterRendering = function() {
				this._initItemNavigation();
			};

			/**
			 * Initializes ItemNavigation, which is necessary for the keyboard handling of the group.
			 *
			 * @private
			 */
			RadioButtonGroup.prototype._initItemNavigation = function() {
				// Collect buttons for ItemNavigation
				var aRBs = this._getVisibleButtons();
				var aDomRefs = [];
				var bHasEnabledRadios = false;
				var bRadioGroupEnabled = this.getEnabled();
				for (var i = 0; i < aRBs.length; i++) {
					aDomRefs.push(aRBs[i].getDomRef());

					// if the i-th radio button is enabled - set the flag to true
					bHasEnabledRadios = bHasEnabledRadios || aRBs[i].getEnabled();
				}

				// if no radio buttons are enabled or the whole group is disabled
				if (!bHasEnabledRadios || !bRadioGroupEnabled) {
					// dismiss item navigation
					if (this._oItemNavigation) {
						this.removeDelegate(this._oItemNavigation);
						this._oItemNavigation.destroy();
						delete this._oItemNavigation;
					}
					return;
				}

				// init ItemNavigation
				if (!this._oItemNavigation) {
					this._oItemNavigation = new ItemNavigation();
					this._oItemNavigation.attachEvent(ItemNavigation.Events.AfterFocus, this._handleAfterFocus, this);
					this.addDelegate(this._oItemNavigation);
				}
				this._oItemNavigation.setRootDomRef(this.getDomRef());
				this._oItemNavigation.setItemDomRefs(aDomRefs);
				this._oItemNavigation.setCycling(true);
				this._oItemNavigation.setColumns(this.getColumns());
				this._oItemNavigation.setSelectedIndex(this._getSelectedIndexInRange());
				this._oItemNavigation.setFocusedIndex(this._getSelectedIndexInRange());
				this._oItemNavigation.setDisabledModifiers({
					sapnext : ["alt", "meta"],
					sapprevious : ["alt", "meta"]
				});
			};

			RadioButtonGroup.prototype._getVisibleButtons = function() {
				return this.getButtons().filter(function (oRB) {
					return oRB.getVisible();
				});
			};

			RadioButtonGroup.prototype._observeChanges = function (oChanges) {
				var oObject = oChanges.object,
					sChangeName = oChanges.name,
					sMutationName = oChanges.mutation,
					oChild = oChanges.child;

				if (oObject === this) {
					if (sMutationName === "insert") {
						this._observeVisibility(oChild);
					} else if (sMutationName === "remove") {
						this._unobserveVisibility(oChild);
					}
				} else if (sChangeName === "visible") {
					this._initItemNavigation();
				}
			};

			RadioButtonGroup.prototype._observeVisibility = function (oControl) {
				this._oObserver.observe(oControl, {
					properties: ["visible"]
				});
			};

			RadioButtonGroup.prototype._unobserveVisibility = function (oControl) {
				this._oObserver.unobserve(oControl, {
					properties: ["visible"]
				});
			};

			/**
			 * Sets the selected sap.m.RadioButton using index.
			 *
			 * @public
			 * @param {number} iSelectedIndex The index of the radio button which has to be selected.
			 * @returns {this} Pointer to the control instance for chaining.
			 */
			RadioButtonGroup.prototype.setSelectedIndex = function(iSelectedIndex) {
				var iIndexOld = this.getSelectedIndex();
				// if a radio button in the group is focused is true, otherwise - false
				var hasFocusedRadioButton = this.getDomRef() && this.getDomRef().contains(document.activeElement);
				var isRadioGroupSelected = this.aRBs && this.aRBs[iSelectedIndex] && this.aRBs[iSelectedIndex].getDomRef();
				var iFocusedIndex;

				if (iSelectedIndex < -1) {
					// invalid negative index -> don't change index.
					Log.warning("Invalid index, will not be changed");
					return this;
				}

				this.setProperty("selectedIndex", iSelectedIndex, true); // no re-rendering
				this._iSelectionNumber = RadioButton.getNextSelectionNumber();
				iFocusedIndex = this._getSelectedIndexInRange();

				// deselect old RadioButton
				if (!isNaN(iIndexOld) && this.aRBs && this.aRBs[iIndexOld]) {
					this.aRBs[iIndexOld].setSelected(false);
					this.aRBs[iIndexOld].setTabIndex(-1);
				}

				// select new one
				if (this.aRBs && this.aRBs[iSelectedIndex]) {
					this.aRBs[iSelectedIndex].setSelected(true);
				}

				if (this._oItemNavigation) {
					this._oItemNavigation.setFocusedIndex(iFocusedIndex);
					this._oItemNavigation.setSelectedIndex(iFocusedIndex);
				}

				// if focus is in the group - focus the selected element
				if (isRadioGroupSelected && hasFocusedRadioButton) {
					this.aRBs[iSelectedIndex].getDomRef().focus();
				}

				return this;
			};

			/**
			 * Sets the selected sap.m.RadioButton using sap.m.RadioButton.
			 *
			 * @public
			 * @param {sap.m.RadioButton} oSelectedButton The item to be selected.
			 * @returns {this} Pointer to the control instance for chaining.
			 */
			RadioButtonGroup.prototype.setSelectedButton = function (oSelectedButton) {
				if (!oSelectedButton) {
					return this.setSelectedIndex(-1);
				}

				var aButtons = this.getButtons();
				for (var i = 0; i < aButtons.length; i++) {
					if (oSelectedButton.getId() == aButtons[i].getId()) {
						return this.setSelectedIndex(i);
					}
				}

				return this;
			};

			/**
			 * Returns the selected radio button.
			 *
			 * @public
			 * @returns {sap.m.RadioButton} The selected radio button.
			 */
			RadioButtonGroup.prototype.getSelectedButton = function() {
				return this.getButtons()[this.getSelectedIndex()];
			};

			/**
			 * Adds a new radio button to the group.
			 *
			 * @public
			 * @param {sap.m.RadioButton} oButton The button which will be added to the group.
			 * @returns {this} Pointer to the control instance for chaining.
			 */
			RadioButtonGroup.prototype.addButton = function(oButton) {
				if (!this.aRBs) {
					this.aRBs = [];
				}

				var iIndex = this.aRBs.length;

				this.aRBs[iIndex] = this._createRadioButton(oButton);

				this.addAggregation("buttons",  this.aRBs[iIndex]);
				return this;
			};

			/**
			 * Adds a new radio button to the group at a specified index.
			 *
			 * @public
			 * @param {sap.m.RadioButton} oButton The radio button which will be added to the group.
			 * @param {number} iIndex The index, at which the radio button will be added.
			 * @returns {this} Pointer to the control instance for chaining.
			 */
			RadioButtonGroup.prototype.insertButton = function(oButton, iIndex) {
				if (!this.aRBs) {
					this.aRBs = [];
				}

				var iLength = this.aRBs.length,
					iMaxLength = this.getButtons().length;

				iIndex = Math.max(Math.min(iIndex, iMaxLength), 0);

				if (!this._bUpdateButtons) {
					if (this.getSelectedIndex() === undefined || iLength == 0) {
						// if not defined -> select first one
						this.setSelectedIndex(0);
					} else if (this.getSelectedIndex() >= iIndex) {
						// If inserted before selected one, move selection index (only change parameter, not RadioButton)
						this.setProperty("selectedIndex", this.getSelectedIndex() + 1, true); // no re-rendering
					}
				}

				if (iIndex >= iLength) {
					this.aRBs[iIndex] = this._createRadioButton(oButton);
				} else {
					// Insert RadioButton: loop backwards over Array and shift everything
					for (var i = (iLength); i > iIndex; i--) {
						this.aRBs[i] = this.aRBs[i - 1];
						if ((i - 1) == iIndex) {
							this.aRBs[i - 1] = this._createRadioButton(oButton);
						}
					}
				}

				this.insertAggregation("buttons", oButton, iIndex);

				return this;
			};

			/**
			 * Creates a copy of the sap.m.RadioButton passed as a first argument
			 *
			 * @private
			 * @param {sap.m.RadioButton} oButton The button from which a radio button will be created.
			 * @returns {sap.m.RadioButton} The created radio button.
			 */
			RadioButtonGroup.prototype._createRadioButton = function(oButton) {
				oButton.setValueState(this.getValueState());
				oButton.setGroupName(this.getId());
				oButton.attachEvent("select", this._handleRBSelect, this);

				return oButton;
			};

			/**
			 * Removes a radio button from the group.
			 *
			 * @public
			 * @returns {sap.m.RadioButton} vElement The removed radio button.
			 */
			RadioButtonGroup.prototype.removeButton = function(vElement) {

				var iIndex = vElement;
				if (typeof (vElement) == "string") { // ID of the element is given
					vElement = sap.ui.getCore().byId(vElement);
				}
				if (typeof (vElement) == "object") { // the element itself is given or has just been retrieved
					iIndex = this.indexOfButton(vElement);
				}

				var oButton = this.removeAggregation("buttons", iIndex);

				if (!this.aRBs) {
					this.aRBs = [];
				}

				if (!this.aRBs[iIndex]) {
					// RadioButton not exists
					return null;
				}

				this.aRBs.splice(iIndex, 1);

				if (!this._bUpdateButtons) {
					if (this.aRBs.length == 0) {
						this.setSelectedIndex(-1);
					} else if (this.getSelectedIndex() == iIndex) {
						// selected one is removed -> select first one
						this.setSelectedIndex(0);
					} else {
						if (this.getSelectedIndex() > iIndex) {
							// If removed before selected one, move selection index (only change parameter, not RadioButton)
							this.setProperty("selectedIndex", this.getSelectedIndex() - 1, true); // no re-rendering
						}
					}
				}

				return oButton;
			};

			/**
			 * Removes all radio buttons.
			 *
			 * @public
			 * @returns {sap.m.RadioButton[]} Array of removed buttons.
			 */
			RadioButtonGroup.prototype.removeAllButtons = function () {
				if (!this._bUpdateButtons) {
					this.setSelectedIndex(-1);
				}

				this.aRBs = [];

				return this.removeAllAggregation("buttons");
			};

			/**
			 * Destroys all radio buttons.
			 *
			 * @public
			 * @returns {this} Pointer to the control instance for chaining.
			 */
			RadioButtonGroup.prototype.destroyButtons = function() {

				this.destroyAggregation("buttons");

				if (this.aRBs) {
					while (this.aRBs.length > 0) {
						this.aRBs[0].destroy();
						this.aRBs.splice(0, 1);
					}
				}

				return this;
			};

			/**
			 * Updates the buttons in the group.
			 *
			 * @public
			 */
			RadioButtonGroup.prototype.updateButtons = function() {
				this._bUpdateButtons = true;
				this.updateAggregation("buttons");
				this._bUpdateButtons = undefined;
			};

			/**
			 * Creates a new instance of RadioButtonGroup, with the same settings as the RadioButtonGroup
			 * on which the method is called.
			 * Event handlers are not cloned.
			 *
			 * @public
			 * @returns {this} New instance of RadioButtonGroup
			 */
			RadioButtonGroup.prototype.clone = function(){

				// on clone don't clone event handler
				var aButtons = this.getButtons();
				var i = 0;
				for (i = 0; i < aButtons.length; i++) {
					aButtons[i].detachEvent("select", this._handleRBSelect, this);
				}

				var oClone = Control.prototype.clone.apply(this, arguments);

				for (i = 0; i < aButtons.length; i++) {
					aButtons[i].attachEvent("select", this._handleRBSelect, this);
				}

				return oClone;
			};

			/**
			 * Select event of single Radio Buttons fires Select Event for group.
			 *
			 * @private
			 * @param {sap.ui.base.Event} oControlEvent Control event.
			 */
			RadioButtonGroup.prototype._handleRBSelect = function(oControlEvent) {

				// find RadioButton in Array to get Index
				for (var i = 0; i < this.aRBs.length; i++) {
					if (this.aRBs[i].getId() == oControlEvent.getParameter("id") && oControlEvent.getParameter("selected")) {
						this.setSelectedIndex(i);
						this.fireSelect({
							selectedIndex : i
						});
						break;
					}
				}
			};

			/**
			 * Sets the editable property of the RadioButtonGroup. Single buttons preserve the value of their editable property.
			 * If the group is set to editable=false the buttons are also displayed and function as read only.
			 * Non editable radio buttons can still obtain focus.
			 *
			 * @name sap.m.RadioButtonGroup.prototype.setEditable
			 * @public
			 * @function
			 * @param {boolean} bEditable Defines whether the radio buttons should be interactive.
			 * @returns {this} Pointer to the control instance for chaining.
			 */

			/**
			 * Sets the enabled property of the RadioButtonGroup. Single buttons preserve internally the value of their enabled property.
			 * If the group is set to enabled=false the buttons are also displayed as disabled and getEnabled returns false.
			 *
			 * @name sap.m.RadioButtonGroup.prototype.setEnabled
			 * @public
			 * @function
			 * @param {boolean} bEnabled Defines whether the radio buttons should be interactive.
			 * @returns {this} Pointer to the control instance for chaining.
			 */

			/**
			 * Handles the event that gets fired by the {@link sap.ui.core.delegate.ItemNavigation} delegate.
			 * Ensures that focused element is selected.
			 *
			 * @private
			 * @param {sap.ui.base.Event} oControlEvent The event that gets fired by the {@link sap.ui.core.delegate.ItemNavigation} delegate.
			 */
			RadioButtonGroup.prototype._handleAfterFocus = function(oControlEvent) {
				var aRBs = this.getButtons();
				var iIndex = oControlEvent.getParameter("index");
				var oEvent = oControlEvent.getParameter("event");

				// buttons that are not visible are not included in the HTML
				// adjust the index to include them
				for (var i = 0; i <= iIndex; i++) {
					if (!aRBs[i].getVisible()) {
						iIndex++;
					}
				}

				// handle only keyboard navigation here
				if (oEvent.keyCode === undefined) {
					return;
				}

				if (iIndex != this.getSelectedIndex()
					&& !(oEvent.ctrlKey || oEvent.metaKey)
					&& this.aRBs[iIndex].getEditable() && this.aRBs[iIndex].getEnabled()
					&& this.getEditable() && this.getEnabled()) {
					// if CTRL key is used do not switch selection
					this.setSelectedIndex(iIndex);
					this.fireSelect({
						selectedIndex : iIndex
					});

					// update tabindex values for all buttons that are part of the item navigation
					this.aRBs
						.filter(function (oRB) { return oRB.getEditable() && oRB.getEnabled(); })
						.forEach(function (oRB) {
							if (oRB === this.aRBs[iIndex]) {
								oRB.setTabIndex(0);
								return;
							}
							oRB.setTabIndex(-1);
						}.bind(this));
				}
			};

			RadioButtonGroup.prototype._getSelectedIndexInRange = function() {
				var aRBs = this.getButtons(),
					iLength = aRBs.length,
					iSelectedIndex = this.getSelectedIndex(),
					iFocusedIndex = -1;

				if (iSelectedIndex <= -1  || iSelectedIndex > iLength - 1) {
					return -1;
				}

				// include only the visible buttons to the focused index
				for (var i = 0; i <= iSelectedIndex; i++) {
					if (aRBs[i].getVisible()) {
						iFocusedIndex++;
					}
				}

				return iFocusedIndex;
			};

			return RadioButtonGroup;

		});