<?php
/**
 * Base class to UI elements that are rendered on the backend and the frontend.
 *
 * @package WP_Ultimo\UI
 * @subpackage Base_Element
 * @since 2.0.0
 */

namespace WP_Ultimo\UI;

use \WP_Ultimo\Database\Sites\Site_Type;

// Exit if accessed directly
defined('ABSPATH') || exit;

/**
 * Base class to UI elements that are rendered on the backend and the frontend.
 *
 * @since 2.0.0
 */
abstract class Base_Element {

	/**
	 * The id of the element.
	 *
	 * Something simple, without prefixes, like 'checkout', or 'pricing-tables'.
	 *
	 * This is used to construct shortcodes by prefixing the id with 'wu_'
	 * e.g. an id checkout becomes the shortcode 'wu_checkout' and
	 * to generate the Gutenberg block by prefixing it with 'wp-ultimo/'
	 * e.g. checkout would become the block 'wp-ultimo/checkout'.
	 *
	 * @since 2.0.0
	 * @var string
	 */
	protected $id;

	/**
	 * Should this element be hidden by default?
	 *
	 * @since 2.0.0
	 * @var boolean
	 */
	protected $hidden_by_default = false;

	/**
	 * Controls whether or not the widget and element should display.
	 *
	 * @since 2.0.0
	 * @var boolean
	 */
	protected $display = true;

	/**
	 * Controls if this is a public element to be used in pages/shortcodes by user.
	 *
	 * @since 2.0.24
	 * @var boolean
	 */
	protected $public = false;

	/**
	 * Keep an array with registered public elements.
	 *
	 * @since 2.0.24
	 * @var boolean
	 */
	protected static $public_elements = array();

	/**
	 * If the element exists, we pre-load the parameters.
	 *
	 * @since 2.0.0
	 * @var false|array
	 */
	protected $pre_loaded_attributes = false;

	/**
	 * Only load (run the setup method) once,
	 *
	 * This is specially true when in the admin context,
	 *
	 * @since 2.0.0
	 * @var boolean
	 */
	protected $loaded = false;

	/**
	 * Keeps a cached list of metabox ids to shave some time.
	 *
	 * @since 2.0.0
	 * @var array
	 */
	protected static $metabox_cache = null;

	/**
	 * Sometimes we need to know if the element was actually loaded
	 * - meaning found on a page - even if tall elements are forcefully
	 * setup.
	 *
	 * @since 2.0.11
	 * @var boolean
	 */
	protected $actually_loaded = false;

	/**
	 * The icon of the UI element.
	 *
	 * E.g. return fa fa-search.
	 *
	 * @since 2.0.0
	 * @param string $context One of the values: block, elementor or bb.
	 * @return string
	 */
	abstract public function get_icon($context = 'block'); // end get_icon;

	/**
	 * The title of the UI element.
	 *
	 * This is used on the Blocks list of Gutenberg.
	 * You should return a string with the localized title.
	 * e.g. return __('My Element', 'wp-ultimo').
	 *
	 * @since 2.0.0
	 * @return string
	 */
	abstract public function get_title();

	/**
	 * The description of the UI element.
	 *
	 * This is also used on the Gutenberg block list
	 * to explain what this block is about.
	 * You should return a string with the localized title.
	 * e.g. return __('Adds a checkout form to the page', 'wp-ultimo').
	 *
	 * @since 2.0.0
	 * @return string
	 */
	abstract public function get_description();

	/**
	 * The list of fields to be added to Gutenberg.
	 *
	 * If you plan to add Gutenberg controls to this block,
	 * you'll need to return an array of fields, following
	 * our fields interface (@see inc/ui/class-field.php).
	 *
	 * You can create new Gutenberg panels by adding fields
	 * with the type 'header'. See the Checkout Elements for reference.
	 *
	 * @see inc/ui/class-checkout-element.php
	 *
	 * Return an empty array if you don't have controls to add.
	 *
	 * @since 2.0.0
	 * @return array
	 */
	abstract public function fields(); // end fields;

	/**
	 * The list of keywords for this element.
	 *
	 * Return an array of strings with keywords describing this
	 * element. Gutenberg uses this to help customers find blocks.
	 *
	 * e.g.:
	 * return array(
	 *  'WP Multisite WaaS',
	 *  'Checkout',
	 *  'Form',
	 *  'Cart',
	 * );
	 *
	 * @since 2.0.0
	 * @return array
	 */
	abstract public function keywords(); // end keywords;

	/**
	 * List of default parameters for the element.
	 *
	 * If you are planning to add controls using the fields,
	 * it might be a good idea to use this method to set defaults
	 * for the parameters you are expecting.
	 *
	 * These defaults will be used inside a 'wp_parse_args' call
	 * before passing the parameters down to the block render
	 * function and the shortcode render function.
	 *
	 * @since 2.0.0
	 * @return array
	 */
	abstract public function defaults(); // end defaults;

	/**
	 * The content to be output on the screen.
	 *
	 * Should return HTML markup to be used to display the block.
	 * This method is shared between the block render method and
	 * the shortcode implementation.
	 *
	 * @since 2.0.0
	 *
	 * @param array       $atts Parameters of the block/shortcode.
	 * @param string|null $content The content inside the shortcode.
	 * @return string
	 */
	abstract public function output($atts, $content = null); // end output;

	// Boilerplate -----------------------------------

	/**
	 * Initializes the singleton.
	 *
	 * @since 2.0.0
	 * @return void
	 */
	public function init() {

		add_action('plugins_loaded', array($this, 'register_form'));

		add_action('init', array($this, 'register_shortcode'));

		add_action('wp_enqueue_scripts', array($this, 'enqueue_element_scripts'));

		add_action("wu_{$this->id}_scripts", array($this, 'register_default_scripts'));

		add_action("wu_{$this->id}_scripts", array($this, 'register_scripts'));

		add_action('wp', array($this, 'maybe_setup'));

		add_action('admin_head', array($this, 'setup_for_admin'), 100);

		add_filter('pre_render_block', array($this, 'setup_for_block_editor'), 100, 2);

		add_action('wu_element_preview', array($this, 'setup_preview'));

		// Init should be the correct time to call this to avoid the deprecated notice from I18N.
		// But it doesn't work for some reason, fix later.
//		add_action('init', function () {
			do_action('wu_element_loaded', $this);
//		} );

		if ($this->public) {

			Base_Element::register_public_element($this);

		} // end if;

	} // end init;

	/**
	 * Register a public element to further use.
	 *
	 * @since 2.0.24
	 * @param mixed $element The element instance to be registered.
	 * @return void
	 */
	public static function register_public_element($element) {

		static::$public_elements[] = $element;

	} // end register_public_element;

	/**
	 * Retrieves the public registered elements.
	 *
	 * @since 2.0.24
	 * @return array
	 */
	public static function get_public_elements() {

		return static::$public_elements;

	} // end get_public_elements;

	/**
	 * Sets blocks up for the block editor.
	 *
	 * @since 2.0.0
	 *
	 * @param null  $short_circuit The value passed.
	 * @param array $block The parsed block data.
	 * @return null
	 */
	public function setup_for_block_editor($short_circuit, $block) {

		$should_load = false;

		if ($block['blockName'] === $this->get_id()) {

			$should_load = true;

		} // end if;

		/**
		 * We might need to add additional blocks later.
		 *
		 * @since 2.0.0
		 * @return array
		 */
		$blocks_to_check = apply_filters('wu_element_block_types_to_check', array(
			'core/shortcode',
			'core/paragraph',
		));

		if (in_array($block['blockName'], $blocks_to_check, true)) {

			if ($this->contains_current_element($block['innerHTML'])) {

				$should_load = true;

			} // end if;

		} // end if;

		if ($should_load) {

			if ($this->is_preview()) {

				$this->setup_preview();

			} else {

				$this->setup();

			} // end if;

		} // end if;

		return $short_circuit;

	} // end setup_for_block_editor;

	/**
	 * Search for an element id on the list of metaboxes.
	 *
	 * Builds a cached list of elements on the first run.
	 * Then uses the cache to run a simple in_array check.
	 *
	 * @since 2.0.0
	 *
	 * @param string $element_id The element ID.
	 * @return bool
	 */
	protected static function search_in_metaboxes($element_id) {

		global $wp_meta_boxes, $pagenow;

		/*
		 * Bail if things don't look normal or in the right context.
		 */
		if (!function_exists('get_current_screen')) {

			return;

		} // end if;

		$screen = get_current_screen();

		/*
		 * First, check on cache, to avoid recalculating it time and time again.
		 */
		if (is_array(self::$metabox_cache)) {

			return in_array($element_id, self::$metabox_cache, true);

		} // end if;

		$contains_metaboxes = wu_get_isset($wp_meta_boxes, $screen->id) || wu_get_isset($wp_meta_boxes, $pagenow);

		$elements_to_cache = array();

		$found = false;

		if (is_array($wp_meta_boxes) && $contains_metaboxes && is_array($wp_meta_boxes[$screen->id])) {

			foreach ($wp_meta_boxes[$screen->id] as $position => $priorities) {

				foreach ($priorities as $priority => $metaboxes) {

					foreach ($metaboxes as $metabox_id => $metabox) {

						$elements_to_cache[] = $metabox_id;

						if ($metabox_id === $element_id) {

							$found = true;

						} // end if;

					} // end foreach;

				} // end foreach;

			} // end foreach;

			/**
			 * Set a local cache so we don't have to loop it all over again.
			 */
			self::$metabox_cache = $elements_to_cache;

		} // end if;

		return $found;

	} // end search_in_metaboxes;

	/**
	 * Setup element on admin pages.
	 *
	 * @since 2.0.0
	 * @return void
	 */
	public function setup_for_admin() {

		if ($this->loaded === true) {

			return;

		} // end if;

		$element_id = "wp-ultimo-{$this->id}-element";

		if (self::search_in_metaboxes($element_id)) {

			$this->loaded = true;

			$this->setup();

		} // end if;

	} // end setup_for_admin;

	/**
	 * Maybe run setup, when the shortcode or block is found.
	 *
	 * @todo check if this is working only when necessary.
	 * @since 2.0.0
	 * @return void
	 */
	public function maybe_setup() {

		global $post;

		if (is_admin() || empty($post)) {

			return;

		} // end if;

		if ($this->contains_current_element($post->post_content, $post)) {

			if ($this->is_preview()) {

				$this->setup_preview();

			} else {

				$this->setup();

			} // end if;

		} // end if;

	} // end maybe_setup;

	/**
	 * Runs early on the request lifecycle as soon as we detect the shortcode is present.
	 *
	 * @since 2.0.0
	 * @return void
	 */
	public function setup() {} // end setup;

	/**
	 * Allows the setup in the context of previews.
	 *
	 * @since 2.0.0
	 * @return void
	 */
	public function setup_preview() {} // end setup_preview;

	/**
	 * Checks content to see if the current element is present.
	 *
	 * This check uses different methods, covering classic shortcodes,
	 * blocks. It also adds a generic filter so developers can
	 * add additional tests for different builders and so on.
	 *
	 * @since 2.0.0
	 *
	 * @param string        $content The content that might contain the element.
	 * @param null|\WP_Post $post The WP Post, if it exists.
	 * @return bool
	 */
	protected function contains_current_element($content, $post = null) {

		/**
		 * If parameters where pre-loaded,
		 * we can skip the entire check and return true.
		 */
		if (is_array($this->pre_loaded_attributes)) {

			return true;

		} // end if;

		/*
		 * First, check for default shortcodes
		 * saved as regular post content.
		 */
		$shortcode = $this->get_shortcode_id();

		if (has_shortcode($content, $shortcode)) {

			$this->pre_loaded_attributes = $this->maybe_extract_arguments($content, 'shortcode');

			$this->actually_loaded = true;

			return true;

		} // end if;

		/*
		 * Handle the Block Editor
		 * and Gutenberg.
		 */
		$block = $this->get_id();

		if (has_block($block, $content)) {

			$this->pre_loaded_attributes = $this->maybe_extract_arguments($content, 'block');

			$this->actually_loaded = true;

			return true;

		} // end if;

		/*
		 * Runs generic version so plugins can extend it.
		 */
		$this->pre_loaded_attributes = $this->maybe_extract_arguments($content, 'other');

		$contains_element = false;

		/**
		 * Last option is to check for the post force setting.
		 */
		if ($post && get_post_meta($post->ID, '_wu_force_elements_loading', true)) {

			$contains_element = true;

		} // end if;

		/**
		 * Allow developers to change the results of the initial search.
		 *
		 * This is useful for third-party builders and such.
		 *
		 * @since 2.0.0
		 * @param bool $contains_elements If the element is contained on the content.
		 * @param string $content The content being examined.
		 * @param self The current element.
		 */
		return apply_filters('wu_contains_element', $contains_element, $content, $this, $post);

	} // end contains_current_element;

	/**
	 * Tries to extract element arguments depending on the element type.
	 *
	 * @since 2.0.0
	 *
	 * @param string $content The content to parse.
	 * @param string $type The element type. Can be one of shortcode, block, and other.
	 * @return false|array
	 */
	protected function maybe_extract_arguments($content, $type = 'shortcode') {

		if ($type === 'shortcode') {

			/**
			 * Tries to parse the shortcode out of the content
			 * passed using the WordPress shortcode regex.
			 */
			$shortcode_regex = get_shortcode_regex(array($this->get_shortcode_id()));

			preg_match_all('/' . $shortcode_regex . '/', $content, $matches, PREG_SET_ORDER);

			return !empty($matches) ? shortcode_parse_atts($matches[0][3]) : false;

		} elseif ($type === 'block') {

			/**
			 * Next, try to parse attrs from blocks
			 * by parsing them out and finding the correct one.
			 */
			$block_content = parse_blocks($content);

			foreach ($block_content as $block) {

				if ($block['blockName'] === $this->get_id()) {

					return $block['attrs'];

				} // end if;

			} // end foreach;

			return false;

		} // end if;

		/**
		 * Adds generic filter to allow developers
		 * to extend this parser to deal with additional
		 * builders or plugins.
		 *
		 * @since 2.0.0
		 * @return false|array
		 */
		return apply_filters('wu_element_maybe_extract_arguments', false, $content, $type, $this);

	} // end maybe_extract_arguments;

	/**
	 * Adds custom CSS to the signup screen.
	 *
	 * @since 2.0.0
	 * @return void
	 */
	public function enqueue_element_scripts() {

		global $post;

		if (!is_a($post, '\WP_Post')) {

			return;

		} // end if;

		$should_enqueue_scripts = apply_filters('wu_element_should_enqueue_scripts', false, $post, $this->get_shortcode_id());

		if ($should_enqueue_scripts || $this->contains_current_element($post->post_content, $post)) {

			/**
			 * Triggers the enqueue scripts hook.
			 *
			 * This is used by the element to hook its
			 * register_scripts method.
			 *
			 * @since 2.0.0
			 */
			do_action("wu_{$this->id}_scripts", $post, $this);

		} // end if;

	} // end enqueue_element_scripts;

	/**
	 * Tries to parse the shortcode content on page load.
	 *
	 * This allow us to have access to parameters before the shortcode
	 * gets actually parsed by the post content functions such as
	 * the_content(). It is useful if you need to access that
	 * date way earlier in the page lifecycle.
	 *
	 * @since 2.0.0
	 *
	 * @param string $name The parameter name.
	 * @param mixed  $default The default value.
	 * @return mixed
	 */
	public function get_pre_loaded_attribute($name, $default = false) {

		if ($this->pre_loaded_attributes === false || !is_array($this->pre_loaded_attributes)) {

			return false;

		} // end if;

		return wu_get_isset($this->pre_loaded_attributes, $name, $default);

	} // end get_pre_loaded_attribute;

	/**
	 * Registers the shortcode.
	 *
	 * @since 2.0.0
	 * @return void
	 */
	public function register_shortcode() {

		if (wu_get_current_site()->get_type() === Site_Type::CUSTOMER_OWNED && is_admin() === false) {

			return;

		} // end if;

		add_shortcode($this->get_shortcode_id(), array($this, 'display'));

	} // end register_shortcode;

	/**
	 * Registers the forms.
	 *
	 * @since 2.0.0
	 * @return void
	 */
	public function register_form() {
		/*
		 * Add Generator Forms
		 */
		wu_register_form("shortcode_{$this->id}", array(
			'render'     => array($this, 'render_generator_modal'),
			'handler'    => '__return_empty_string',
			'capability' => 'manage_network',
		));

		/*
		 * Add Customize Forms
		 */
		wu_register_form("customize_{$this->id}", array(
			'render'     => array($this, 'render_customize_modal'),
			'handler'    => array($this, 'handle_customize_modal'),
			'capability' => 'manage_network',
		));

	} // end register_form;

	/**
	 * Adds the modal to copy the shortcode for this particular element.
	 *
	 * @since 2.0.0
	 * @return void
	 */
	public function render_generator_modal() {

		$fields = $this->fields();

		$defaults = $this->defaults();

		$state = array();

		foreach ($fields as $field_slug => &$field) {

			if ($field['type'] === 'header' || $field['type'] === 'note') {

				unset($fields[$field_slug]);

				continue;

			} // end if;

			/*
			 * Additional State.
			 *
			 * We need to keep track of the state
			 * specially when we're dealing with
			 * complex fields, such as group.
			 */
			$additional_state = array();

			if ($field['type'] === 'group') {

				foreach ($field['fields'] as $sub_field_slug => &$sub_field) {

					$sub_field['html_attr'] = array(
						'v-model.lazy' => "attributes.{$sub_field_slug}",
					);

					$additional_state[$sub_field_slug] = wu_request($sub_field_slug, wu_get_isset($defaults, $sub_field_slug));

				} // end foreach;

				continue;

			} // end if;

			/*
			 * Set v-model
			 */
			$field['html_attr'] = array(
				'v-model.lazy' => "attributes.{$field_slug}",
			);

			$required = wu_get_isset($field, 'required');

			if (wu_get_isset($field, 'required')) {

				$shows = array();

				foreach ($required as $key => $value) {

					$value = is_string($value) ? "\"$value\"" : $value;

					$shows[] = "attributes.{$key} == $value";

				} // end foreach;

				$field['wrapper_html_attr'] = array(
					'v-show' => implode(' && ', $shows),
				);

				$state[$field_slug . '_shortcode_requires'] = $required;

			} // end if;

			$state[$field_slug] = wu_request($field_slug, wu_get_isset($defaults, $field_slug));

		} // end foreach;

		$fields['shortcode_result'] = array(
			'type'            => 'note',
			'wrapper_classes' => 'sm:wu-block',
			'desc'            => '<div class="wu-w-full"><span class="wu-my-1 wu-text-2xs wu-uppercase wu-font-bold wu-block">' . __('Result', 'wp-ultimo') . '</span><pre v-html="shortcode" id="wu-shortcode" style="overflow-x: scroll !important;" class="wu-text-center wu-p-4 wu-m-0 wu-mt-2 wu-rounded wu-content-center wu-bg-gray-800 wu-text-white wu-font-mono wu-border wu-border-solid wu-border-gray-300 wu-max-h-screen wu-overflow-x-scroll"></pre></div>',
		);

		$fields['submit_copy'] = array(
			'type'            => 'submit',
			'title'           => __('Copy Shortcode', 'wp-ultimo'),
			'value'           => 'edit',
			'classes'         => 'button button-primary wu-w-full wu-copy',
			'wrapper_classes' => 'wu-items-end',
			'html_attr'       => array(
				'data-clipboard-action' => 'copy',
				'data-clipboard-target' => '#wu-shortcode',
			),
		);

		$form = new \WP_Ultimo\UI\Form($this->id, $fields, array(
			'views'                 => 'admin-pages/fields',
			'classes'               => 'wu-modal-form wu-widget-list wu-striped wu-m-0 wu-mt-0 wu-w-full',
			'field_wrapper_classes' => 'wu-w-full wu-box-border wu-items-center wu-flex wu-justify-between wu-p-4 wu-m-0 wu-border-t wu-border-l-0 wu-border-r-0 wu-border-b-0 wu-border-gray-300 wu-border-solid',
			'html_attr'             => array(
				'data-wu-app' => "{$this->id}_generator",
				'data-state'  => wu_convert_to_state(array(
					'id'         => $this->get_shortcode_id(),
					'defaults'   => $defaults,
					'attributes' => $state,
				)),
			),
		));

		echo '<div class="wu-styling">';

		$form->render();

		echo '</div>';

	} // end render_generator_modal;

	/**
	 * Adds the modal customize the widget block
	 *
	 * @since 2.0.0
	 * @return void
	 */
	public function render_customize_modal() {

		$fields = array();

		$fields['hide'] = array(
			'type'            => 'toggle',
			'title'           => __('Hide Element', 'wp-ultimo'),
			'desc'            => __('Be careful. Hiding an element from the account page might remove important functionality from your customers\' reach.', 'wp-ultimo'),
			'value'           => $this->hidden_by_default,
			'classes'         => 'button button-primary wu-w-full',
			'wrapper_classes' => 'wu-items-end',
		);

		$fields = array_merge($fields, $this->fields());

		$saved_settings = $this->get_widget_settings();

		$defaults = $this->defaults();

		$state = array_merge($defaults, $saved_settings);

		foreach ($fields as $field_slug => &$field) {

			if ($field['type'] === 'header') {

				unset($fields[$field_slug]);

				continue;

			} // end if;

			$value = wu_get_isset($saved_settings, $field_slug, null);

			if ($value !== null) {

				$field['value'] = $value;

			} // end if;

		} // end foreach;

		$fields['save_line'] = array(
			'type'            => 'group',
			'classes'         => 'wu-justify-between',
			'wrapper_classes' => 'wu-bg-gray-100',
			'fields'          => array(
				'restore' => array(
					'type'            => 'submit',
					'title'           => __('Reset Settings', 'wp-ultimo'),
					'value'           => 'edit',
					'classes'         => 'button',
					'wrapper_classes' => 'wu-mb-0',
				),
				'submit'  => array(
					'type'            => 'submit',
					'title'           => __('Save Changes', 'wp-ultimo'),
					'value'           => 'edit',
					'classes'         => 'button button-primary',
					'wrapper_classes' => 'wu-mb-0',
				),
			),
		);

		$form = new \WP_Ultimo\UI\Form($this->id, $fields, array(
			'views'                 => 'admin-pages/fields',
			'classes'               => 'wu-modal-form wu-widget-list wu-striped wu-m-0 wu-mt-0',
			'field_wrapper_classes' => 'wu-w-full wu-box-border wu-items-center wu-flex wu-justify-between wu-p-4 wu-m-0 wu-border-t wu-border-l-0 wu-border-r-0 wu-border-b-0 wu-border-gray-300 wu-border-solid',
			'html_attr'             => array(
				'data-wu-app' => "{$this->id}_customize",
				'data-state'  => wu_convert_to_state($state),
			),
		));

		echo '<div class="wu-styling">';

		$form->render();

		echo '</div>';

	} // end render_customize_modal;

	/**
	 * Saves the customization settings for a given widget.
	 *
	 * @since 2.0.0
	 * @return void
	 */
	public function handle_customize_modal() {

		$settings = array();

		if (wu_request('submit') !== 'restore') {

			$fields = $this->fields();

			$fields['hide'] = array(
				'type' => 'toggle',
			);

			foreach ($fields as $field_slug => $field) {

				$setting = wu_request($field_slug, false);

				if ($setting !== false || $field['type'] === 'toggle') {

					$settings[$field_slug] = $setting;

				} // end if;

			} // end foreach;

		} // end if;

		$this->save_widget_settings($settings);

		wp_send_json_success(array(
			'send'         => array(
				'scope'         => 'window',
				'function_name' => 'wu_block_ui',
				'data'          => '#wpcontent',
			),
			'redirect_url' => add_query_arg('updated', 1, $_SERVER['HTTP_REFERER']),
		));

	} // end handle_customize_modal;

	/**
	 * Registers scripts and styles necessary to render this.
	 *
	 * @since 2.0.0
	 * @return void
	 */
	public function register_default_scripts() {

		wp_enqueue_style('wu-admin');

	} // end register_default_scripts;

	/**
	 * Registers scripts and styles necessary to render this.
	 *
	 * @since 2.0.0
	 * @return void
	 */
	public function register_scripts() {} // end register_scripts;

	/**
	 * Loads dependencies that might not be available at render time.
	 *
	 * @since 2.0.0
	 * @return void
	 */
	public function dependencies() {} // end dependencies;

	/**
	 * Returns the ID of this UI element.
	 *
	 * @since 2.0.0
	 * @return string
	 */
	public function get_id() {

		return sprintf('wp-ultimo/%s', $this->id);

	} // end get_id;

	/**
	 * Returns the ID of this UI element.
	 *
	 * @since 2.0.0
	 * @return string
	 */
	public function get_shortcode_id() {

		return str_replace('-', '_', sprintf('wu_%s', $this->id));

	} // end get_shortcode_id;

	/**
	 * Treats the attributes before passing them down to the output method.
	 *
	 * @since 2.0.0
	 *
	 * @param array $atts The element attributes.
	 * @return string
	 */
	public function display($atts) {

		if (!$this->should_display()) {

			return; // bail if the display was set to false.

		} // end if;

		$this->dependencies();

		$atts = wp_parse_args($atts, $this->defaults());

		/*
		 * Account for the 'className' Gutenberg attribute.
		 */
		$atts['className'] = trim('wu-' . $this->id . ' wu-element ' . wu_get_isset($atts, 'className', ''));

		/*
		 * Pass down the element so we can use helpers.
		 */
		$atts['element'] = $this;

		return call_user_func(array($this, 'output'), $atts);

	} // end display;

	/**
	 * Retrieves a cleaned up version of the content.
	 *
	 * This method strips out vue reactivity tags and more.
	 *
	 * @since 2.0.0
	 *
	 * @param array $atts The element attributes.
	 * @return string
	 */
	public function display_template($atts) {

		$content = $this->display($atts);

		$content = str_replace(array(
			'v-',
			'data-wu',
			'data-state',
		), 'inactive-', $content);

		$content = str_replace(array(
			'{{',
			'}}',
		), '', $content);

		return $content;

	} // end display_template;

	/**
	 * Checks if we need to display admin management attachments.
	 *
	 * @since 2.0.0
	 *
	 * @return bool
	 */
	public function should_display_customize_controls() {

		return apply_filters('wu_element_display_super_admin_notice', current_user_can('manage_network'), $this);

	} // end should_display_customize_controls;

	/**
	 * Adds the element as a inline block, without the admin widget frame.
	 *
	 * @since 2.0.0
	 *
	 * @param string $screen_id The screen id.
	 * @param string $hook The hook to add the content to. Defaults to admin_notices.
	 * @param array  $atts Array containing the shortcode attributes.
	 * @return void
	 */
	public function as_inline_content($screen_id, $hook = 'admin_notices', $atts = array()) {

		if (!function_exists('get_current_screen')) {

			_doing_it_wrong(__METHOD__, __('An element can not be loaded as inline content unless the get_current_screen() function is already available.', 'wp-ultimo'), '2.0.0');

			return;

		} // end if;

		$screen = get_current_screen();

		if (!$screen || $screen_id !== $screen->id) {

			return;

		} // end if;

		/*
		 * Run the setup in this case;
		 */
		$this->setup();

		if (!$this->should_display()) {

			return; // bail if the display was set to false.

		} // end if;

		if (empty($atts)) {

			$atts = $this->get_widget_settings();

		} // end if;

		$control_classes = '';

		if (wu_get_isset($atts, 'hide', $this->hidden_by_default)) {

			if (!$this->should_display_customize_controls()) {

				return;

			} // end if;

			$control_classes = 'wu-customize-mode wu-opacity-25';

		} // end if;

		add_action($hook, function() use ($atts, $control_classes) {

			echo '<div class="wu-inline-widget">';

				echo '<div class="wu-inline-widget-body ' . $control_classes . '">';

					echo $this->display($atts);

				echo '</div>';

				$this->super_admin_notice();

			echo '</div>';

		});

		do_action("wu_{$this->id}_scripts", null, $this);

	} // end as_inline_content;

	/**
	 * Save the widget options.
	 *
	 * @since 2.0.0
	 *
	 * @param array $settings The settings to save. Key => value array.
	 * @return void
	 */
	public function save_widget_settings($settings) {

		$key = wu_replace_dashes($this->id);

		wu_save_setting("widget_{$key}_settings", $settings);

	} // end save_widget_settings;

	/**
	 * Retrieves the settings for a particular widget.
	 *
	 * @since 2.0.0
	 * @return array
	 */
	public function get_widget_settings() {

		$key = wu_replace_dashes($this->id);

		return wu_get_setting("widget_{$key}_settings", array());

	} // end get_widget_settings;

	/**
	 * Adds the element as a metabox.
	 *
	 * @since 2.0.0
	 *
	 * @param string $screen_id The screen id.
	 * @param string $position Position on the screen.
	 * @param array  $atts Array containing the shortcode attributes.
	 * @return void
	 */
	public function as_metabox($screen_id, $position = 'normal', $atts = array()) {

		$this->setup();

		if (!$this->should_display()) {

			return; // bail if the display was set to false.

		} // end if;

		if (empty($atts)) {

			$atts = $this->get_widget_settings();

		} // end if;

		$control_classes = '';

		if (wu_get_isset($atts, 'hide')) {

			if (!$this->should_display_customize_controls()) {

				return;

			} // end if;

			$control_classes = 'wu-customize-mode wu-opacity-25';

		} // end if;

		add_meta_box(
			"wp-ultimo-{$this->id}-element",
			$this->get_title(),
			function() use ($atts, $control_classes) {

				echo '<div class="wu-metabox-widget ' . $control_classes . '">';

					echo $this->display($atts);

				echo '</div>';

				$this->super_admin_notice();

			},
			$screen_id,
			$position,
			'high'
		);

		do_action("wu_{$this->id}_scripts", null, $this);

	} // end as_metabox;

	/**
	 * Adds note for super admins.
	 *
	 * Adds an admin notice to let the super admin know
	 * how to use the widgets.
	 *
	 * @since 2.0.0
	 * @return void
	 */
	public function super_admin_notice() {

		$should_display = $this->should_display_customize_controls();

		if ($should_display) {

			// translators: %1$s is the URL to the customize modal. %2$s is the URL of the shortcode generation modal
			$message = __('<a class="wubox wu-no-underline" title="Customize" href="%1$s">Customize this element</a>, or <a class="wubox wu-no-underline" title="Shortcode" href="%2$s">generate a shortcode</a> to use it on the front-end!', 'wp-ultimo');

			$message .= wu_tooltip(__('You are seeing this because you are a super admin', 'wp-ultimo'));

			$link_shortcode = wu_get_form_url("shortcode_{$this->id}");
			$link_customize = wu_get_form_url("customize_{$this->id}");

			$text = sprintf(
				$message,
				$link_customize,
				$link_shortcode
			);

			$html = '
				<div class="wu-styling">
						<div class="wu-widget-inset">
							<div class="wubox wu-no-underline wu-p-4 wu-bg-gray-200 wu-block wu-mt-4 wu-text-center wu-text-sm wu-text-gray-600 wu-m-auto wu-border-solid wu-border-0 wu-border-t wu-border-gray-400">
								' . $text . '
							</div>
					</div>
				</div>
			';

			echo $html;

		} // end if;

	} // end super_admin_notice;

	/**
	 * Checks if we are in a preview context.
	 *
	 * @since 2.0.0
	 * @return boolean
	 */
	public function is_preview() {

		$is_preview = false;

		if (did_action('init')) {

			$is_preview = wu_request('preview') && current_user_can('edit_posts');

		} // end if;

		return apply_filters('wu_element_is_preview', false, $this);

	} // end is_preview;

	/**
	 * Get controls whether or not the widget and element should display..
	 *
	 * @since 2.0.0
	 * @return boolean
	 */
	public function should_display() {

		return $this->display || $this->is_preview();

	} // end should_display;

	/**
	 * Set controls whether or not the widget and element should display..
	 *
	 * @since 2.0.0
	 * @param boolean $display Controls whether or not the widget and element should display.
	 * @return void
	 */
	public function set_display($display) {

		$this->display = $display;

	} // end set_display;

	/**
	 * Checks if the current element was actually loaded.
	 *
	 * @since 2.0.11
	 * @return boolean
	 */
	public function is_actually_loaded() {

		return $this->actually_loaded;

	} // end is_actually_loaded;

} // end class Base_Element;