/**
 * Registers Dynamic Elements as Gutenberg Blocks
 *
 * @package
 * @author WP Zinc
 */

/**
 * @typedef {import('@wordpress/element').WPElement} WPElement
 * @typedef {import('@wordpress/components').TextControl} TextControl
 * @typedef {import('@wordpress/components').WPZincAutocompleterControl} WPZincAutocompleterControl
 */

// Register Gutenberg Blocks if the Gutenberg Editor is loaded on screen.
// This prevents JS errors if this script is accidentally enqueued on a non-
// Gutenberg editor screen, or the Classic Editor Plugin is active.
if (pageGeneratorProGutenbergEnabled()) {
	if (typeof page_generator_pro_gutenberg !== 'undefined') {
		// Remove some panels if we're using Gutenberg on Content Groups.
		pageGeneratorProGutenbergRemovePanels(
			page_generator_pro_gutenberg.post_type
		);

		// Register each Block in Gutenberg.
		for (const block in page_generator_pro_gutenberg.shortcodes) {
			pageGeneratorProGutenbergRegisterBlock(
				page_generator_pro_gutenberg.shortcodes[block],
				page_generator_pro_gutenberg.block_api_version
			);
		}

		// Register Plugin Sidebars in Gutenberg if we're editing a Post.
		for (const pluginSidebar in page_generator_pro_gutenberg.plugin_sidebars) {
			pageGeneratorProGutenbergRegisterPluginSidebar(
				page_generator_pro_gutenberg.plugin_sidebars[pluginSidebar]
			);
		}

		// Initialize conditional fields.
		page_generator_pro_conditional_fields_initialize();

		// Initialize autocomplete instance on Gutenberg Title field, if Keywords exist.
		// We do this when the Title field is selected, because initializing Tribute's
		// autocomplete instance sooner than this doesn't work.
		const pageGeneratorProContentGroupEditorIsReady = wp.data.subscribe(
			function () {
				// Hacky; can't find a Gutenberg native way to determine if the Post Title field is focused.
				if (jQuery('h1.wp-block-post-title').hasClass('is-selected')) {
					// Post Title editor is ready; initialize autocomplete on Gutenberg Title field, if Keywords exist.
					if (
						typeof wp_zinc_autocomplete_initialize !== 'undefined'
					) {
						wp_zinc_autocomplete_initialize(
							'.editor-visual-editor__post-title-wrapper'
						);
					}

					// Calling the constant will stop subscribing to future events, as we've now initialized
					// Tribute on the Title field, and don't need to initialize it again.
					pageGeneratorProContentGroupEditorIsReady();
				}
			}
		);
	}
}

/**
 * Registers the given block in Gutenberg
 *
 * @since 	2.5.4
 *
 * @param {Object} block      Block.
 * @param {number} apiVersion Block API version.
 */
function pageGeneratorProGutenbergRegisterBlock(block, apiVersion) {
	(function (blocks, editor, element, components) {
		// Define some constants for the various items we'll use.
		const el = element.createElement;
		const { registerBlockType } = blocks;
		const { InspectorControls, useBlockProps } = editor;
		const {
			TextControl,
			SelectControl,
			TextareaControl,
			ToggleControl,
			FormTokenField,
			PanelBody,
			PanelRow,
			Button,
			Card,
			CardBody,
			CardHeader,
		} = components;

		/**
		 * Returns the icon to display for this block, depending
		 * on the supplied block's configuration.
		 *
		 * @since   4.8.2
		 *
		 * @return  {WPElement|string} Either a WordPress element (RawHTML) or a dashicon string.
		 */
		const getIcon = function () {
			// Return a fallback default icon if none is specified for this block.
			if (typeof block.gutenberg_icon === 'undefined') {
				return 'dashicons-tablet';
			}

			// Return HTML element if the icon is an SVG string.
			if (block.gutenberg_icon.search('svg') >= 0) {
				return element.RawHTML({
					children: block.gutenberg_icon,
				});
			}

			// Just return the string, as it's a dashicon CSS class.
			return block.gutenberg_icon;
		};

		/**
		 * Return a field element for the block sidebar, which is displayed in a panel's row
		 * when this block is being edited.
		 *
		 * @since   4.8.2
		 *
		 * @param {Object} props     Block properties.
		 * @param {Object} field     Field attributes.
		 * @param {string} attribute Attribute name to store the field's data in.
		 * @return {Object|boolean}            Field element.
		 */
		const getField = function (props, field, attribute) {
			// If this field has a condition, check if it's met.
			if (typeof field.condition !== 'undefined') {
				// If the condition specified does not exist as a field in this block,
				// the field is misconfigured. Always display.
				if (typeof block.fields[field.condition.key] !== 'undefined') {
					// Assume the condition has not been met for this field to be displayed.
					let condition_met = false;

					// Assert whether the condition is met based on the field type.
					switch (block.fields[field.condition.key].type) {
						case 'toggle':
							// Field's condition value will be 0 or 1.
							// Attributes field value will be false or true.
							condition_met =
								Boolean(Number(field.condition.value)) ===
								props.attributes[field.condition.key];
							break;

						default:
							// Assert based on the condition's value type (array, string, number).
							switch (typeof field.condition.value) {
								case 'object':
									condition_met = Object.values(
										field.condition.value
									).includes(
										props.attributes[field.condition.key]
									);
									break;

								default:
									condition_met =
										field.condition.value ===
										props.attributes[field.condition.key];
									break;
							}
							break;
					}

					// Skip this field if the condition is not met.
					if (!condition_met) {
						return false;
					}
				}
			}

			// Build CSS class name(s).
			const fieldClassNames = [];
			if (typeof field.class !== 'undefined') {
				fieldClassNames.push(field.class);
			}

			// Define some field properties shared across all field types.
			const fieldProperties = {
				id: 'page_generator_pro_' + block.name + '_' + attribute,
				label: field.label,
				help: field.description,
				className: fieldClassNames.join(' '),
				value: props.attributes[attribute],

				// Add __next40pxDefaultSize and __nextHasNoMarginBottom properties,
				// preventing deprecation notices in the block editor and opt in to the new styles
				// from 7.0.
				__next40pxDefaultSize: true,
				__nextHasNoMarginBottom: true,

				onChange(value) {
					if (field.type === 'number') {
						// If value is a blank string i.e. no attribute value was provided,
						// cast it to the field's minimum number setting.
						// This prevents WordPress' block renderer API returning a 400 error
						// because a blank value will be passed as a string, when WordPress
						// expects it to be a numerical value.
						if (value === '') {
							value = field.min;
						}

						// Cast value to integer if a value exists.
						if (value.length > 0) {
							value = Number(value);
						}
					}

					const newValue = {};
					newValue[attribute] = value;
					props.setAttributes(newValue);
				},
			};

			// Add data- attributes.
			if (typeof field.data !== 'undefined') {
				for (const key in field.data) {
					fieldProperties['data-' + key] = field.data[key];
				}
			}

			// Define additional Field Properties and the Field Element,
			// depending on the Field Type (select, textarea, text etc).
			switch (field.type) {
				case 'autocomplete':
					return getAutocompleteField(fieldProperties, field.values);

				case 'autocomplete_textarea':
					return getAutocompleteTextareaField(fieldProperties);

				case 'number':
					return getNumberField(
						fieldProperties,
						field.min,
						field.max,
						field.step
					);

				case 'repeater':
					return getRepeaterFields(
						field.sub_fields,
						props,
						attribute
					);

				case 'select':
					return getSelectField(fieldProperties, field.values);

				case 'select_multiple':
					return getMultipleSelectField(
						fieldProperties,
						field.values,
						props,
						attribute
					);

				case 'text':
					return getTextField(fieldProperties);

				case 'text_multiple':
					return getMultipleTextField(fieldProperties);

				case 'textarea':
					return getTextareaField(fieldProperties);

				case 'toggle':
					return getToggleField(
						fieldProperties,
						props.attributes[attribute]
					);

				default:
					return getTextField(fieldProperties);
			}
		};

		/**
		 * Returns a WPZincAutocompleterControl based on the supplied field properties
		 * and values array, if the WPZincAutocompleterControl is available.
		 *
		 * Falls back to a TextControl if WPZincAutocompleterControl is not available.
		 *
		 * @since 	4.8.2
		 *
		 * @param {Object} fieldProperties Field Properties.
		 * @param {Array}  values          Autocomplete key/value pairs.
		 * @return {WPZincAutocompleterControl|TextControl} Autocomplete field element.
		 */
		const getAutocompleteField = function (fieldProperties, values) {
			// Check autocompleter control exists.
			// If no Keywords are defined, we can't use it.
			if (typeof WPZincAutocompleterControl !== 'undefined') {
				// Define field properties.
				fieldProperties.list = 'autocomplete';
				fieldProperties.type = 'text';
				fieldProperties.options = values;

				// Define field element.
				return el(WPZincAutocompleterControl, fieldProperties);
			}

			// Fallback to a default TextControl.
			return getTextField(fieldProperties);
		};

		/**
		 * Returns a TextareaControl with the wpzinc-autocomplete class, to support autocomplete
		 * suggestions, based on the supplied field properties.
		 *
		 * @since 	4.8.4
		 *
		 * @param {Object} fieldProperties Field Properties.
		 * @return {Object} TextareaControl Element.
		 */
		const getAutocompleteTextareaField = function (fieldProperties) {
			fieldProperties.className = 'wpzinc-autocomplete';

			return el(TextareaControl, fieldProperties);
		};

		/**
		 * Returns a TextControl with type = number, based on the supplied field properties
		 * and min, max and step attributes.
		 *
		 * @since 	4.8.2
		 *
		 * @param {Object} fieldProperties Field Properties.
		 * @param {number} min             Minimum value.
		 * @param {number} max             Maximum value.
		 * @param {number} step            Step value.
		 * @return {Object} TextControl element.
		 */
		const getNumberField = function (fieldProperties, min, max, step) {
			// Define field properties.
			fieldProperties.type = 'number';
			fieldProperties.min = min;
			fieldProperties.max = max;
			fieldProperties.step = step;

			// Define field element.
			return el(TextControl, fieldProperties);
		};

		/**
		 * Returns the repeater field, comprising of rows of fields for existing values,
		 * with Add and Delete buttons.
		 *
		 * @since 	4.8.2
		 *
		 * @param {Object} repeaterFields Repeater Fields.
		 * @param {Object} props          Block properties.
		 * @param {string} attribute      Attribute name to store the JSON encoded repeater fields data in.
		 * @return {Array} 				  Repeater field rows.
		 */
		const getRepeaterFields = function (repeaterFields, props, attribute) {
			// Decode JSON value stored in attribute.
			let repeaterValues =
				props.attributes[attribute].length > 0
					? JSON.parse(props.attributes[attribute])
					: false;
			const repeaterFieldRows = [];

			// Iterate through repeater values to output existing repeater rows.
			if (repeaterValues.length > 0) {
				for (const repeaterFieldRowIndex in repeaterValues) {
					repeaterFieldRows.push(
						el(
							Card,
							{
								key:
									'page_generator_pro_' +
									block.name +
									'_' +
									attribute +
									'_card',
							},
							[
								el(
									CardHeader,
									{
										key:
											'page_generator_pro_' +
											block.name +
											'_' +
											attribute +
											'_card_header',
									},
									'#' + (Number(repeaterFieldRowIndex) + 1)
								),
								el(
									CardBody,
									{
										key:
											'page_generator_pro_' +
											block.name +
											'_' +
											attribute +
											'_card_body',
									},
									getRepeaterFieldRow(
										repeaterFields,
										repeaterValues,
										Number(repeaterFieldRowIndex),
										props,
										attribute
									)
								),
							]
						)
					);
				}
			}

			// Append an 'Add' button to insert a new repeater row.
			repeaterFieldRows.push(
				el(Button, {
					key:
						'page_generator_pro_' +
						block.name +
						'_' +
						attribute +
						'_add',
					isSecondary: true,
					isSmall: true,
					text: 'Add',
					onClick() {
						// Iterate through repeater fields to build a new blank object to append to
						// the repeaterValues.
						const repeaterRowValues = {};
						for (const repeaterKey in repeaterFields) {
							repeaterRowValues[repeaterKey] = '';
						}

						// If no repeater values exist, define a blank array.
						if (!repeaterValues) {
							repeaterValues = [];
						}

						// Append the new blank row.
						repeaterValues.push(repeaterRowValues);

						// Update props attributes.
						const newValue = {};
						newValue[attribute] = JSON.stringify(repeaterValues);
						props.setAttributes(newValue);
					},
				})
			);

			return repeaterFieldRows;
		};

		/**
		 * Returns an existing repeater field's row populated with the supplied values.
		 *
		 * @since 	4.8.2
		 *
		 * @param {Object} repeaterFields   Repeater Fields.
		 * @param {Object} repeaterValues   Repeater Fields data.
		 * @param {number} repeaterRowIndex Repeater's row index for this row.
		 * @param {Object} props            Block properties.
		 * @param {string} attribute        Attribute name to store the JSON encoded repeater fields data in.
		 * @return {Array} Repeater field row.
		 */
		const getRepeaterFieldRow = function (
			repeaterFields,
			repeaterValues,
			repeaterRowIndex,
			props,
			attribute
		) {
			// Iterate through repeater's fields.
			const repeaterFieldRow = [];
			let repeaterFieldElement = null;

			for (const repeaterKey in repeaterFields) {
				const repeaterField = repeaterFields[repeaterKey];

				const subFieldProperties = {
					id: 'page_generator_pro_' + block.name + '_' + repeaterKey,
					label: repeaterField.label,
					help: repeaterField.description,
					value: repeaterValues[repeaterRowIndex][repeaterKey],
					onChange(value) {
						// Update repeater values for this row and key.
						repeaterValues[repeaterRowIndex][repeaterKey] = value;

						// Update props attributes.
						const newValue = {};
						newValue[attribute] = JSON.stringify(repeaterValues);
						props.setAttributes(newValue);
					},
				};

				switch (repeaterField.type) {
					case 'autocomplete':
						repeaterFieldElement = getAutocompleteField(
							subFieldProperties,
							repeaterField.values
						);
						break;

					case 'select':
						repeaterFieldElement = getSelectField(
							subFieldProperties,
							repeaterField.values
						);
						break;

					case 'text':
					default:
						repeaterFieldElement = getTextField(subFieldProperties);
						break;
				}

				repeaterFieldRow.push(
					el(
						PanelRow,
						{
							key:
								'page_generator_pro_' +
								block.name +
								'_' +
								attribute +
								'_' +
								repeaterRowIndex,
						},
						repeaterFieldElement
					)
				);
			}

			// Append a 'Delete' button to delete this repeater row.
			const repeaterFieldRowDeleteConditionButton = el(Button, {
				key:
					'page_generator_pro_' +
					block.name +
					'_' +
					attribute +
					'_delete',
				isSecondary: true,
				isSmall: true,
				text: 'Delete',
				onClick() {
					// Remove repeater row from values.
					repeaterValues.splice(repeaterRowIndex, 1);

					// Update props attributes.
					const newValue = {};
					newValue[attribute] = JSON.stringify(repeaterValues);
					props.setAttributes(newValue);
				},
			});

			repeaterFieldRow.push(repeaterFieldRowDeleteConditionButton);

			return repeaterFieldRow;
		};

		/**
		 * Returns a SelectControl based on the supplied field properties
		 * and option values array.
		 *
		 * @since 	4.8.2
		 *
		 * @param {Object} fieldProperties Field Properties.
		 * @param {Array}  values          Select key/value pairs.
		 * @return {Object} SelectControl element.
		 */
		const getSelectField = function (fieldProperties, values) {
			// Build options for <select> input.
			const fieldOptions = [];
			for (const value in values) {
				fieldOptions.push({
					label: values[value],
					value,
				});
			}

			// Sort field's options alphabetically by label.
			fieldOptions.sort(function (x, y) {
				const a = x.label.toUpperCase(),
					b = y.label.toUpperCase();
				return a.localeCompare(b);
			});

			// Define field properties.
			fieldProperties.options = fieldOptions;

			// Define field element.
			return el(SelectControl, fieldProperties);
		};

		/**
		 * Returns a FormTokenField based on the supplied field properties and
		 * select values, to allow for multiple value selection.
		 *
		 * @since 	4.8.2
		 *
		 * @param {Object} fieldProperties Field Properties.
		 * @param {Array}  values          <select> key/value pairs.
		 * @param {Object} props           Block properties.
		 * @param {string} attribute       Attribute name to store the JSON encoded repeater fields data in.
		 * @return {Object} FormTokenField element.
		 */
		const getMultipleSelectField = function (
			fieldProperties,
			values,
			props,
			attribute
		) {
			// Build values for <select> inputs as a flat array comprising of the format:
			// [key] label.
			// onChange will extract the key from [key] and store it as the value.
			const fieldSuggestions = [];
			for (const value in values) {
				fieldSuggestions.push('[' + value + '] ' + values[value]);
			}

			// Define field properties.
			fieldProperties.suggestions = fieldSuggestions;
			fieldProperties.maxSuggestions = 5;
			fieldProperties.onChange = function (selectedValues) {
				// Extract keys between square brackets, storing as the value.
				const newValues = [],
					length = selectedValues.length;
				for (index = 0; index < length; index++) {
					const matches = selectedValues[index].match(/\[(.*?)\]/);
					if (matches) {
						newValues.push(matches[1]);
					} else {
						newValues.push(selectedValues[index]);
					}
				}

				// Assign to block.
				const newValue = {};
				newValue[attribute] = newValues;
				props.setAttributes(newValue);
			};

			// Define field element.
			return el(FormTokenField, fieldProperties);
		};

		/**
		 * Returns a TextControl based on the supplied field properties.
		 *
		 * @since 	4.8.2
		 *
		 * @param {Object} fieldProperties Field Properties.
		 * @return {Object} TextControl element.
		 */
		const getTextField = function (fieldProperties) {
			fieldProperties.type = 'text';

			return el(TextControl, fieldProperties);
		};

		/**
		 * Returns a TextareaControl based on the supplied field properties.
		 *
		 * @since 	4.8.2
		 *
		 * @param {Object} fieldProperties Field Properties.
		 * @return {Object} TextareaControl element.
		 */
		const getTextareaField = function (fieldProperties) {
			return el(TextareaControl, fieldProperties);
		};

		/**
		 * Returns a FormTokenField based on the supplied field properties.
		 *
		 * @since 	4.8.2
		 *
		 * @param {Object} fieldProperties Field Properties.
		 * @return {Object} FormTokenField element.
		 */
		const getMultipleTextField = function (fieldProperties) {
			return el(FormTokenField, fieldProperties);
		};

		/**
		 * Returns a ToggleControl based on the supplied field properties.
		 *
		 * @since 	4.8.2
		 * @param {Object}  fieldProperties Field Properties.
		 * @param {boolean} isChecked       Whether the toggle is checked.
		 * @return {Object} ToggleControl element.
		 */
		const getToggleField = function (fieldProperties, isChecked) {
			fieldProperties.checked = isChecked;

			return el(ToggleControl, fieldProperties);
		};

		/**
		 * Return an array of rows to display in the given block sidebar's panel when
		 * this block is being edited.
		 *
		 * @since   4.8.2
		 *
		 * @param {Object} props Block properties.
		 * @param {string} panel Panel name.
		 * @return {Array} Panel rows.
		 */
		const getPanelRows = function (props, panel) {
			// Build Inspector Control Panel Rows, one for each Field.
			const rows = [];
			for (const i in block.tabs[panel].fields) {
				const attribute = block.tabs[panel].fields[i], // e.g. 'term'.
					field = block.fields[attribute]; // field array.

				// If this field doesn't exist as an attribute in the block's get_attributes(),
				// this is a non-Gutenberg field (such as a color picker for shortcodes),
				// which should be ignored.
				if (typeof block.attributes[attribute] === 'undefined') {
					continue;
				}

				// Get field.
				const fieldElement = getField(props, field, attribute);

				// If field is false, ignore it i.e. a condition was not met.
				if (!fieldElement) {
					continue;
				}

				// If the field element is a repeater, don't contain it in a PanelRow,
				// as it will already be contained in a PanelRow.
				switch (field.type) {
					case 'repeater':
						rows.push(fieldElement);
						break;

					default:
						rows.push(
							el(
								PanelRow,
								{
									key: attribute,
								},
								fieldElement
							)
						);
				}
			}

			return rows;
		};

		/**
		 * Return an array of panels to display in the block's sidebar when the block
		 * is being edited.
		 *
		 * @since   4.8.2
		 *
		 * @param {Object} props Block properties.
		 * @return {Array} Block sidebar panels.
		 */
		const getPanels = function (props) {
			const panels = [];
			let initialOpen = true;

			// Build Inspector Control Panels.
			for (const panel in block.tabs) {
				const panelRows = getPanelRows(props, panel);

				// If no panel rows exist (e.g. this is a shortcode only panel,
				// for styles, which Gutenberg registers in its own styles tab),
				// don't add this panel.
				if (!panelRows.length) {
					continue;
				}

				panels.push(
					el(
						PanelBody,
						{
							title: block.tabs[panel].label,
							key: panel,
							initialOpen,
						},
						panelRows
					)
				);

				// Don't open any further panels.
				initialOpen = false;
			}

			return panels;
		};

		/**
		 * Display settings sidebar when the block is being edited, and save
		 * changes that are made.
		 *
		 * @since   4.8.2
		 *
		 * @param {Object} props Block properties.
		 * @return {Object} Block settings sidebar elements.
		 */
		const EditBlock = function (props) {
			const blockProps = useBlockProps();

			// If requesting an example of how this block looks (which is requested
			// when the user adds a new block and hovers over this block's icon),
			// show the preview image.
			if (props.attributes.is_gutenberg_example === true) {
				return el(
					'div',
					blockProps,
					el('img', {
						src: block.gutenberg_example_image,
					})
				);
			}

			// Build Inspector Control Panels, which will appear in the Sidebar when editing the Block.
			const panels = getPanels(props);

			// Generate Block Preview.
			let preview = '';

			if (block.register_on_generation_only) {
				// Output a preview that describes the block, and to click on it to open the settings sidebar.
				// The block will be converted into HTML upon generation, so there's no 'true' preview to display.
				preview = el(
					'div',
					{
						className: 'page-generator-pro-block ' + block.name,
					},
					el(
						'div',
						{
							className: 'page-generator-pro-block-title',
						},
						block.title + ' Dynamic Element'
					),
					el(
						'div',
						{
							className: 'page-generator-pro-block-description',
						},
						block.description
					),
					el(
						'div',
						{
							className: 'page-generator-pro-block-description',
						},
						'Click this block to open the settings sidebar.'
					)
				);
			} else {
				// Use the block's PHP's render() function by calling the wp.serverSideRender component, as this will
				// also be called when viewing a generated page.
				preview = el(wp.serverSideRender, {
					block: 'page-generator-pro/' + block.name,
					attributes: props.attributes,
					className: 'page-generator-pro-' + block.name,
					key: 'page-generator-pro-preview-' + block.name,
				});
			}

			// If this block has been selected, open the editor sidebar now.
			if (props.isSelected) {
				if (
					typeof wp.data.dispatch('core/edit-post') !== 'undefined' &&
					wp.data.dispatch('core/edit-post') !== null
				) {
					if (
						!wp.data
							.select('core/edit-post')
							.isEditorSidebarOpened()
					) {
						wp.data
							.dispatch('core/edit-post')
							.openGeneralSidebar('edit-post/block');
					}
				}
			}

			// Return settings sidebar panel with fields and the block preview.
			return editBlockWithPanelsAndPreview(panels, preview, blockProps);
		};

		/**
		 * Display settings sidebar when the block is being edited, and save
		 * changes that are made.
		 *
		 * @since   5.4.2
		 *
		 * @param {Object} panels     Block panels.
		 * @param {Object} preview    Block preview.
		 * @param {Object} blockProps Block properties.
		 * @return {Object}           Block settings sidebar elements.
		 */
		const editBlockWithPanelsAndPreview = function (
			panels,
			preview,
			blockProps
		) {
			return el('div', blockProps, [
				el(
					InspectorControls,
					{
						key:
							'page-generator-pro-inspector-controls-' +
							block.name,
					},
					panels
				),
				el(
					wp.element.Fragment,
					{
						key: 'page-generator-pro-fragment-' + block.name,
					},
					preview
				),
			]);
		};

		/**
		 * Save the block's content.
		 *
		 * @since   5.4.2
		 *
		 * @return {Object}       Block content.
		 */
		const saveBlock = function () {
			if (typeof block.gutenberg_template !== 'undefined') {
				// Use useBlockProps.save() to preserve styling classes and attributes
				// from block supports (colors, typography, spacing, etc.)
				const blockProps = useBlockProps.save();
				return el('div', blockProps, el(InnerBlocks.Content));
			}

			// Deliberate; preview in the editor is determined by the return statement in `edit` above.
			// On the frontend site, the block's render() PHP class is always called, so we dynamically
			// fetch the content.
			return null;
		};

		// Register Block.
		registerBlockType('page-generator-pro/' + block.name, {
			apiVersion,
			title: block.title,
			description: block.description,
			category: block.category,
			icon: getIcon,
			keywords: block.keywords,
			attributes: block.attributes,

			// Required to force a preview.
			example: {
				attributes: {
					is_gutenberg_example: true, // This can be anything.
				},
			},

			// Editor.
			edit: EditBlock,

			// Output.
			save: saveBlock,
		});
	})(
		window.wp.blocks,
		window.wp.blockEditor,
		window.wp.element,
		window.wp.components
	);
}

/**
 * Registers a Plugin Sidebar in Gutenberg.
 *
 * @since 5.5.5
 *
 * @param {Object} sidebar Plugin Sidebars
 */
function pageGeneratorProGutenbergRegisterPluginSidebar(sidebar) {
	(function (plugins, editor, element, components, data) {
		// Define some constants for the various items we'll use.
		const el = element.createElement;
		const { registerPlugin } = plugins;
		const { PluginSidebar } = editor;
		const { TextControl, ToggleControl, PanelBody, PanelRow } = components;
		const { useSelect, useDispatch } = data;

		/**
		 * Returns a PluginDocumentSettingPanel for this plugin, containing
		 * post-level settings.
		 *
		 * @since 5.5.5
		 */
		const RenderPanel = function () {
			// Get all post meta.
			const meta = useSelect(function (wpSelect) {
				return (
					wpSelect('core/editor').getEditedPostAttribute('meta') || {}
				);
			}, []);
			const { editPost: wpEditPost } = useDispatch('core/editor');

			/**
			 * Return a field element for the settings panel.
			 *
			 * @since   5.5.5
			 *
			 * @param {Object} field Field properties.
			 * @param {string} key   Field name.
			 * @return {Object}        Field element.
			 */
			const getField = function (field, key) {
				// Define some field properties shared across all field types.
				const fieldProperties = {
					key,
					id: key,
					label: field.label,
					help: field.description,
					value: meta[key] || field.default_value,

					// Save Post Meta on value change.
					onChange(value) {
						wpEditPost({
							meta: {
								[key]: value,
							},
						});
					},
				};

				// Define additional Field Properties and the Field Element,
				// depending on the Field Type (select, textarea, text etc).
				switch (field.type) {
					case 'toggle':
						fieldProperties.checked = Boolean(
							fieldProperties.value
						);
						fieldProperties.value = 1;
						return el(ToggleControl, fieldProperties);

					default:
						// Return field element.
						return el(TextControl, fieldProperties);
				}
			};

			/**
			 * Return an array of field elements to display in the settings panel.
			 *
			 * @since   5.5.0
			 *
			 * @param {Object} fields Fields to display.
			 * @return {Array}         Panel rows.
			 */
			const getFields = function (fields) {
				const rows = [];

				for (const key in fields) {
					rows.push(
						el(
							PanelRow,
							{
								key,
							},
							getField(fields[key], key)
						)
					);
				}

				return el(PanelBody, {}, rows);
			};

			// Return the settings sidebar panel with fields.
			return el(
				PluginSidebar,
				{
					name: sidebar.name,
					title: sidebar.title,
					className: sidebar.name,
				},
				getFields(sidebar.fields)
			);
		};

		// Register the plugin sidebar.
		registerPlugin(
			'page-generator-pro-' + sidebar.name.replace(/_/g, '-'),
			{
				render: RenderPanel,
			}
		);
	})(
		window.wp.plugins,
		window.wp.editPost,
		window.wp.element,
		window.wp.components,
		window.wp.data
	);
}

/**
 * Checks if the Gutenberg editor is loaded on screen.
 *
 * Returns true when editing a Post, Page or Custom Post Type in the block editor,
 * or using the site editor.
 *
 * @since   5.4.2
 *
 * @return {boolean} Block editor is loaded
 */
function pageGeneratorProGutenbergEnabled() {
	return typeof wp !== 'undefined' && typeof wp.blockEditor !== 'undefined';
}

/**
 * Removes some panels if we're using Gutenberg on Content Groups.
 *
 * @since   5.4.2
 *
 * @param {string} postType Post Type.
 */
function pageGeneratorProGutenbergRemovePanels(postType) {
	if (
		postType === 'page-generator-pro' &&
		typeof wp.editPost !== 'undefined'
	) {
		wp.data.dispatch('core/editor').removeEditorPanel('post-link');
		wp.data.dispatch('core/editor').removeEditorPanel('page-attributes');
		wp.data.dispatch('core/editor').removeEditorPanel('template');
	}
}
