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

// Provides class sap.ui.rta.plugin.Stretch.
sap.ui.define([
	"sap/ui/rta/plugin/Plugin",
	"sap/ui/dt/OverlayRegistry",
	"sap/ui/dt/OverlayUtil",
	"sap/base/util/restricted/_debounce"
], function(
	Plugin,
	OverlayRegistry,
	OverlayUtil,
	_debounce
) {
	"use strict";

	/**
	 * Constructor for a new Stretch plugin.
	 *
	 * @param {string} [sId] id for the new object, generated automatically if no id is given
	 * @param {object} [mSettings] initial settings for the new object
	 *
	 * @class
	 * The Stretch plugin adds functionality/styling required for RTA.
	 * @extends sap.ui.rta.plugin.Plugin
	 *
	 * @author SAP SE
	 * @version 1.136.9
	 *
	 * @constructor
	 * @private
	 * @since 1.60
	 * @alias sap.ui.rta.plugin.Stretch
	 */
	var Stretch = Plugin.extend("sap.ui.rta.plugin.Stretch", /** @lends sap.ui.rta.plugin.Stretch.prototype */ {
		metadata: {
			library: "sap.ui.rta",
			properties: {},
			associations: {
				/**
				 * Stores all candidates for stretching
				 */
				stretchCandidates: {
					type: "sap.ui.core.Control",
					multiple: true
				}
			},
			events: {}
		}
	});

	Stretch.STRETCHSTYLECLASS = "sapUiRtaStretchPaddingTop";

	function startAtSamePosition(oParentOverlay, oOverlay) {
		return (
			oParentOverlay && oParentOverlay.getGeometry() && oOverlay.getGeometry()
			&& oParentOverlay.getGeometry().position.top === oOverlay.getGeometry().position.top
			&& oParentOverlay.getGeometry().position.left === oOverlay.getGeometry().position.left
		);
	}

	function toggleStyleClass(oOverlay, bAddClass) {
		var oElement = oOverlay.getElement();
		if (oElement.addStyleClass && oElement.removeStyleClass) {
			if (bAddClass) {
				oElement.addStyleClass(Stretch.STRETCHSTYLECLASS);
			} else {
				oElement.removeStyleClass(Stretch.STRETCHSTYLECLASS);
			}
		} else {
			var oElementDomRef = oOverlay.getAssociatedDomRef();
			if (oElementDomRef) {
				if (bAddClass) {
					oElementDomRef.classList.add(Stretch.STRETCHSTYLECLASS);
				} else {
					oElementDomRef.classList.remove(Stretch.STRETCHSTYLECLASS);
				}
			}
		}
	}

	/**
	 * Check if the size of an overlay is the same as an array of overlays.
	 * If no array is passed to the functionthe children of the reference overlay are used.
	 * If the control is already stretched we need to remove the padding that we add
	 *
	 * @param {sap.ui.dt.ElementOverlay} oReferenceOverlay - overlay object
	 * @param {sap.ui.dt.ElementOverlay[]} [aChildOverlays] - array of overlay objects that should be checked
	 * @param {boolean} [bIsAlreadyStretched] - indicater if the control is already stretched
	 * @returns {boolean} Returns true if the overlay has the same size as all the children
	 * @private
	 */
	function childrenAreSameSize(oReferenceOverlay, aChildOverlays, bIsAlreadyStretched) {
		var oParentGeometry = oReferenceOverlay.getGeometry();

		if (!oParentGeometry) {
			return false;
		}

		// remove padding if it is already stretched
		var iHeight = oParentGeometry.size.height;
		if (bIsAlreadyStretched) {
			iHeight -= parseInt(window.getComputedStyle(oParentGeometry.domRef, null).getPropertyValue("padding-top"));
		}
		var iParentSize = Math.round(oParentGeometry.size.width) * Math.round(iHeight);
		aChildOverlays ||= OverlayUtil.getAllChildOverlays(oReferenceOverlay);

		var aChildrenGeometry = aChildOverlays.map(function(oChildOverlay) {
			return oChildOverlay.getGeometry();
		});

		var oChildrenGeometry = OverlayUtil.getGeometry(aChildrenGeometry);

		if (!oChildrenGeometry) {
			return false;
		}

		var iChildrenSize = Math.round(oChildrenGeometry.size.width) * Math.round(oChildrenGeometry.size.height);
		return iChildrenSize === iParentSize;
	}

	/**
	 * Checks all the decendants for an editable child. Stops as soon as an editable is found
	 * or the children don't have the same size anymore.
	 *
	 * @param {sap.ui.dt.ElementOverlay} oReferenceOverlay - overlay object
	 * @param {sap.ui.dt.ElementOverlay[]} aChildOverlays - array of child overlay objects
	 * @returns {boolean} Returns true if there is an editable descendant
	 * @private
	 */
	function atLeastOneDescendantEditable(oReferenceOverlay, aChildOverlays) {
		var bAtLeastOneChildEditable = aChildOverlays.some(function(oOverlay) {
			return oOverlay.getEditable() && oOverlay.getGeometry();
		});

		if (bAtLeastOneChildEditable) {
			return true;
		}

		var aChildrensChildrenOverlays = [];
		aChildOverlays.forEach(function(oChildOverlay) {
			aChildrensChildrenOverlays = aChildrensChildrenOverlays.concat(OverlayUtil.getAllChildOverlays(oChildOverlay));
		});

		if (!aChildrensChildrenOverlays.length) {
			return false;
		}
		if (childrenAreSameSize(oReferenceOverlay, aChildrensChildrenOverlays)) {
			return atLeastOneDescendantEditable(oReferenceOverlay, aChildrensChildrenOverlays);
		}
		return false;
	}

	/**
	 * Override for DesignTime setter to attach to synced event
	 *
	 * @param {sap.ui.dt.DesignTime} oDesignTime DesignTime object
	 * @override
	 */
	Stretch.prototype.setDesignTime = function(...aArgs) {
		const [oDesignTime] = aArgs;
		Plugin.prototype.setDesignTime.apply(this, aArgs);

		if (oDesignTime) {
			oDesignTime.attachEventOnce("synced", this._onDTSynced, this);
		}
	};

	Stretch.prototype.exit = function() {
		if (this.getDesignTime()) {
			this.getDesignTime().detachEvent("elementOverlayAdded", this._onElementOverlayChanged);
			this.getDesignTime().detachEvent("elementOverlayMoved", this._onElementOverlayChanged);
			this.getDesignTime().detachEvent("elementPropertyChanged", this._onElementPropertyChanged);
			this.getDesignTime().detachEvent("elementOverlayEditableChanged", this._onElementOverlayEditableChanged);
			this.getDesignTime().detachEvent("elementOverlayDestroyed", this._onElementOverlayDestroyed);
		}
	};

	Stretch.prototype.addStretchCandidate = function(oOverlay) {
		var oElement = oOverlay.getElement();
		if (!this.getStretchCandidates().includes(oElement.getId())) {
			this.addAssociation("stretchCandidates", oElement);
		}
	};

	Stretch.prototype.removeStretchCandidate = function(oOverlay) {
		this.removeAssociation("stretchCandidates", oOverlay.getElement());
		toggleStyleClass(oOverlay, false);
	};

	/**
	 * @override
	 */
	Stretch.prototype.registerElementOverlay = function(...aArgs) {
		const [oOverlay] = aArgs;
		this._checkParentAndAddToStretchCandidates(oOverlay);

		oOverlay.attachElementModified(this._onElementModified, this);

		Plugin.prototype.registerElementOverlay.apply(this, aArgs);
	};

	/**
	 * Additionally to super->deregisterOverlay this method removes the Style-class
	 * @param  {sap.ui.dt.ElementOverlay} oOverlay overlay object
	 * @override
	 */
	Stretch.prototype.deregisterElementOverlay = function(...aArgs) {
		const [oOverlay] = aArgs;
		toggleStyleClass(oOverlay, false);

		Plugin.prototype.deregisterElementOverlay.apply(this, aArgs);
	};

	/**
	 * This plugin does not make any overlay editable
	 * @override
	 */
	Stretch.prototype._isEditable = function() {
		return false;
	};

	Stretch.prototype._onDTSynced = function() {
		this._setStyleClassForAllStretchCandidates();

		this.getDesignTime().attachEvent("elementOverlayAdded", this._onElementOverlayChanged, this);
		this.getDesignTime().attachEvent("elementOverlayMoved", this._onElementOverlayChanged, this);
		this.getDesignTime().attachEvent("elementPropertyChanged", this._onElementPropertyChanged, this);
		this.getDesignTime().attachEvent("elementOverlayEditableChanged", this._onElementOverlayEditableChanged, this);
		this.getDesignTime().attachEvent("elementOverlayDestroyed", this._onElementOverlayDestroyed, this);
	};

	Stretch.prototype._onElementModified = function(oEvent) {
		if (this.getDesignTime().getBusyPlugins().length) {
			return;
		}

		var oParams = oEvent.getParameters();
		var oOverlay = oEvent.getSource();
		if (oParams.type === "afterRendering") {
			// the timeout should be changed to 0 as soon as DT refactoring is done
			this.fnDebounced ||= _debounce(function() {
				this._setStyleClassForAllStretchCandidates(this._getNewStretchCandidates(this._aOverlaysCollected));
				this._aOverlaysCollected = [];
				this.fnDebounced = undefined;
			}.bind(this), 16);

			this._aOverlaysCollected ||= [];

			if (!this._aOverlaysCollected.includes(oOverlay)) {
				this._aOverlaysCollected.push(oOverlay);
				this.fnDebounced();
			}
		}
	};

	Stretch.prototype._onElementOverlayDestroyed = function(oEvent) {
		if (this.getDesignTime().getBusyPlugins().length) {
			return;
		}

		var aNewStretchCandidates = [];
		var oParentOverlay = oEvent.getParameters().elementOverlay.getParentElementOverlay();
		if (oParentOverlay && !oParentOverlay._bIsBeingDestroyed) {
			var aRelevantElements = this._getRelevantOverlays(oParentOverlay)
			.filter(function(oOverlay) {
				return oOverlay.getElement();
			});
			aNewStretchCandidates = this._getNewStretchCandidates(aRelevantElements);
		}

		this._setStyleClassForAllStretchCandidates(aNewStretchCandidates);
	};

	/**
	 * When editable changes on an overlay, all parent-overlays and the overlay itself,
	 * who are already stretch candidates, have to be reevaluated
	 *
	 * @param {sap.ui.base.Event} oEvent event object
	 */
	Stretch.prototype._onElementOverlayEditableChanged = function(oEvent) {
		var oOverlay = OverlayRegistry.getOverlay(oEvent.getParameters().id);
		if (this.getDesignTime().getBusyPlugins().length || !oOverlay) {
			return;
		}

		var aOverlaysToReevaluate = this._getRelevantOverlaysOnEditableChange(oOverlay);
		this._setStyleClassForAllStretchCandidates(aOverlaysToReevaluate);
	};

	Stretch.prototype._onElementPropertyChanged = function(oEvent) {
		var oOverlay = OverlayRegistry.getOverlay(oEvent.getParameters().id);
		if (this.getDesignTime().getBusyPlugins().length || !oOverlay) {
			return;
		}

		var aRelevantOverlays = this._getRelevantOverlays(oOverlay);
		var fnDebounced = _debounce(function() {
			if (!this.bIsDestroyed && !oOverlay.bIsDestroyed) {
				var aNewStretchCandidates =
					this._getNewStretchCandidates(aRelevantOverlays)
					.concat(this._getRelevantOverlaysOnEditableChange(oOverlay));
				aNewStretchCandidates = aNewStretchCandidates.filter(function(sId, iPosition, aAllCandidates) {
					return aAllCandidates.indexOf(sId) === iPosition;
				});
				this._setStyleClassForAllStretchCandidates(aNewStretchCandidates);
			}
		}.bind(this));

		aRelevantOverlays.forEach(function(oOverlay) {
			oOverlay.attachEventOnce("geometryChanged", fnDebounced);
		});
	};

	Stretch.prototype._onElementOverlayChanged = function(oEvent) {
		// overlay might be destroyed until this event listener is called - BCP: 1980286428
		var oOverlay = OverlayRegistry.getOverlay(oEvent.getParameters().id);
		if (this.getDesignTime().getBusyPlugins().length || !oOverlay) {
			return;
		}

		var aRelevantOverlays = this._getRelevantOverlays(oOverlay);
		var aNewStretchCandidates = this._getNewStretchCandidates(aRelevantOverlays);
		this._setStyleClassForAllStretchCandidates(aNewStretchCandidates);
	};

	Stretch.prototype._getRelevantOverlaysOnEditableChange = function(oOverlay) {
		var aRelevantElements = this.getStretchCandidates().includes(oOverlay.getElement().getId()) ? [oOverlay.getElement().getId()] : [];
		var oParentAggregationOverlay = oOverlay.getParentAggregationOverlay();
		if (!oParentAggregationOverlay) {
			return aRelevantElements;
		}

		// if there are siblings that are editable and visible, the change has no effect on the parents
		var aOnlySiblings = oParentAggregationOverlay.getChildren();
		aOnlySiblings.splice(aOnlySiblings.indexOf(oOverlay), 1);
		var bAnySiblingAlreadyEditable = aOnlySiblings.some(function(oOverlay) {
			return oOverlay.getEditable() && oOverlay.getGeometry();
		});

		if (bAnySiblingAlreadyEditable) {
			return aRelevantElements;
		}

		return aRelevantElements.concat(this._getRelevantParents(oOverlay));
	};

	Stretch.prototype._getRelevantParents = function(oOverlay) {
		var aReturn = [];

		// add all parents who are stretch candidates, but stop after 25 parents
		for (var i = 0; i < 25; i++) {
			oOverlay = oOverlay.getParentElementOverlay();
			if (!oOverlay) {
				return aReturn;
			}

			if (!this.getStretchCandidates().includes(oOverlay.getElement().getId())) {
				return aReturn;
			}

			aReturn.push(oOverlay.getElement().getId());
		}
		return aReturn;
	};

	Stretch.prototype._getNewStretchCandidates = function(aOverlays) {
		var aNewStretchCandidates = [];
		aOverlays.forEach(function(oOverlay) {
			if (this._reevaluateStretching(oOverlay)) {
				aNewStretchCandidates.push(oOverlay.getElement().getId());
			}
		}, this);

		return aNewStretchCandidates;
	};

	Stretch.prototype._reevaluateStretching = function(oOverlay) {
		if (!oOverlay.bIsDestroyed) {
			var oElementDomRef = Array.isArray(oOverlay.getAssociatedDomRef())
				? oOverlay.getAssociatedDomRef()[0]
				: oOverlay.getAssociatedDomRef();
			if (oElementDomRef) {
				var bIsStretched = oElementDomRef.classList.contains(Stretch.STRETCHSTYLECLASS);
				var bShouldBeStretched = childrenAreSameSize(oOverlay, undefined, bIsStretched);
				if (bIsStretched && !bShouldBeStretched) {
					this.removeStretchCandidate(oOverlay);
				} else if (!bIsStretched && bShouldBeStretched) {
					this.addStretchCandidate(oOverlay);
					return true;
				}
			}
		}
		return false;
	};

	Stretch.prototype._checkParentAndAddToStretchCandidates = function(oOverlay) {
		var oParentOverlay = oOverlay.getParentElementOverlay();
		var oParentElementDOM = oParentOverlay?.getAssociatedDomRef();
		if (oParentElementDOM) {
			if (startAtSamePosition(oParentOverlay, oOverlay)) {
				if (childrenAreSameSize(oParentOverlay)) {
					this.addStretchCandidate(oParentOverlay);
				}
			}
		}
	};

	/**
	 * Check editable and set the Style-class for padding on the given Elements.
	 * When no array is given all the stretch candidates are evaluated
	 *
	 * @param {sap.ui.core.Control[]} [aStretchCandidates] - array of Ids of stretch candidates
	 * @private
	 */
	Stretch.prototype._setStyleClassForAllStretchCandidates = function(aStretchCandidates) {
		if (!Array.isArray(aStretchCandidates)) {
			aStretchCandidates = this.getStretchCandidates();
		}
		aStretchCandidates.forEach(function(sElementId) {
			var oOverlay = OverlayRegistry.getOverlay(sElementId);
			var aChildOverlays = OverlayUtil.getAllChildOverlays(oOverlay);
			var bAddClass = oOverlay.getEditable() && atLeastOneDescendantEditable(oOverlay, aChildOverlays);

			toggleStyleClass(oOverlay, bAddClass);
		}, this);
	};

	return Stretch;
});