'use strict';

// eslint-disable-next-line no-unused-vars
const WPFormsUtils = window.WPFormsUtils || ( function( document, window, $ ) {

	/**
	 * Public functions and properties.
	 *
	 * @since 1.7.6
	 *
	 * @type {object}
	 */
	const app = {

		/**
		 * Wrapper to trigger a native or custom event and return the event object.
		 *
		 * @since 1.7.6
		 *
		 * @param {jQuery} $element  Element to trigger event on.
		 * @param {string} eventName Event name to trigger (custom or native).
		 * @param {Array}  args      Trigger arguments.
		 *
		 * @returns {Event} Event object.
		 */
		triggerEvent: function( $element, eventName, args = [] ) {

			let eventObject = new $.Event( eventName );

			$element.trigger( eventObject, args );

			return eventObject;
		},

		/**
		 * Debounce.
		 *
		 * This function comes directly from underscore.js:
		 *
		 * Returns a function, that, as long as it continues to be invoked, will not
		 * be triggered. The function will be called after it stops being called for
		 * N milliseconds. If `immediate` is passed, trigger the function on the
		 * leading edge, instead of the trailing.
		 *
		 * Debouncing is removing unwanted input noise from buttons, switches or other user input.
		 * Debouncing prevents extra activations or slow functions from triggering too often.
		 *
		 * @param {Function} func      The function to be debounced.
		 * @param {int}      wait      The amount of time to delay calling func.
		 * @param {bool}     immediate Whether or not to trigger the function on the leading edge.
		 *
		 * @returns {Function} Returns a function that, as long as it continues to be invoked, will not be triggered.
		 */
		debounce: function( func, wait, immediate ) {

			var timeout;

			return function() {

				var context = this,
					args = arguments;
				var later = function() {

					timeout = null;

					if ( ! immediate ) {
						func.apply( context, args );
					}
				};

				var callNow = immediate && ! timeout;

				clearTimeout( timeout );

				timeout = setTimeout( later, wait );

				if ( callNow ) {
					func.apply( context, args );
				}
			};
		},

		/**
		 * CSS color operations.
		 *
		 * @since 1.8.8
		 *
		 * @type {Object}
		 */
		cssColorsUtils: {
			/**
			 * Checks if the provided color has transparency.
			 *
			 * @since 1.8.8
			 *
			 * @param {string} color            The color to check.
			 * @param {number} opacityThreshold The max opacity value of the color that is considered as transparent.
			 *
			 * @return {boolean} Returns true if the color is transparent.
			 */
			isTransparentColor( color, opacityThreshold = 0.33 ) {
				const rgba = app.cssColorsUtils.getColorAsRGBArray( color );
				const opacity = Number( rgba?.[ 3 ] );

				// Compare the opacity value with the threshold.
				return opacity <= opacityThreshold;
			},

			/**
			 * Get color as an array of RGB(A) values.
			 *
			 * @since 1.8.8
			 *
			 * @param {string} color Color.
			 *
			 * @return {Array|boolean} Color as an array of RGBA values. False on error.
			 */
			getColorAsRGBArray( color ) {
				// Check if the given color is a valid CSS color.
				if ( ! app.cssColorsUtils.isValidColor( color ) ) {
					return false;
				}

				// Remove # from the beginning of the string and remove whitespaces.
				color = color.replace( /^#/, '' ).replaceAll( ' ', '' );
				color = color === 'transparent' ? 'rgba(0,0,0,0)' : color;

				const rgba = color;
				let rgbArray;

				// Check if color is in HEX(A) format.
				const isHex = rgba.match( /[0-9a-f]{6,8}$/ig );

				if ( isHex ) {
					// Search and split HEX(A) color into an array of couples of chars.
					rgbArray = rgba.match( /\w\w/g ).map( ( x ) => parseInt( x, 16 ) );
					rgbArray[ 3 ] = rgbArray[ 3 ] || rgbArray[ 3 ] === 0 ? ( rgbArray[ 3 ] / 255 ).toFixed( 2 ) : 1;
				} else {
					rgbArray = rgba.split( '(' )[ 1 ].split( ')' )[ 0 ].split( ',' );
				}

				return rgbArray;
			},

			/**
			 * Check if the given color is a valid CSS color.
			 *
			 * @since 1.8.8
			 *
			 * @param {string} color Color.
			 *
			 * @return {boolean} True if the given color is a valid CSS color.
			 */
			isValidColor( color ) {
				// Create a temporary DOM element and use `style` property.
				const s = new Option().style;

				s.color = color;

				// Invalid color leads to the empty color property of DOM element style.
				return s.color !== '';
			},

			/**
			 * Get contrast color relative to given color.
			 *
			 * @since 1.8.8
			 *
			 * @param {string} color Color.
			 *
			 * @return {string} True if the given color is a valid CSS color.
			 */
			getContrastColor( color ) {
				const rgba = app.cssColorsUtils.getColorAsRGBArray( color );
				const sum = rgba.reduce( ( a, b ) => a + b, 0 );
				const avg = Math.round( ( sum / 3 ) * ( rgba[ 3 ] ?? 1 ) );

				return avg < 128 ? '#ffffff' : '#000000';
			},

			/**
			 * Add opacity to color string.
			 * Supports formats: RGB, RGBA, HEX, HEXA.
			 *
			 * If the given color has an alpha channel, the new alpha channel will be calculated according to the given opacity.
			 *
			 * @since 1.8.9
			 *
			 * @param {string} color   Color.
			 * @param {string} opacity Opacity.
			 *
			 * @return {string} Color in RGBA format with an added alpha channel according to given opacity.
			 */
			getColorWithOpacity( color, opacity ) {
				color = color.trim();

				const rgbArray = app.cssColorsUtils.getColorAsRGBArray( color );

				if ( ! rgbArray ) {
					return color;
				}

				// Default opacity is 1.
				opacity = ! opacity || opacity.length === 0 ? '1' : opacity.toString();

				const alpha = rgbArray.length === 4 ? parseFloat( rgbArray[ 3 ] ) : 1;

				// Calculate new alpha value.
				const newAlpha = parseFloat( opacity ) * alpha;

				// Combine and return the RGBA color.
				return `rgba(${ rgbArray[ 0 ] },${ rgbArray[ 1 ] },${ rgbArray[ 2 ] },${ newAlpha })`.replace( /\s+/g, '' );
			},
		},
	};

	// Provide access to public functions/properties.
	return app;

}( document, window, jQuery ) );
