<?php
namespace Bricks;

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

class Database {
	public static $posts_per_page         = 0;
	public static $all_templates          = []; // @since 2.0
	public static $active_templates       = [];
	public static $default_template_types = [
		'header',
		'footer',
		'archive',
		'search',
		'error',
		'wc_archive',
		'wc_product',
		'wc_cart',
		'wc_cart_empty',
		'wc_form_checkout',
		'wc_form_pay',
		'wc_thankyou',
		'wc_order_receipt',
		// Woo Phase 3
		'wc_account_dashboard',
		'wc_account_orders',
		'wc_account_view_order',
		'wc_account_downloads',
		'wc_account_addresses',
		'wc_account_form_edit_address',
		'wc_account_form_edit_account',
		'wc_account_form_login',
		'wc_account_form_lost_password',
		'wc_account_form_lost_password_confirmation',
		'wc_account_reset_password',
	];

	public static $header_position = 'top';
	public static $global_data     = [];
	public static $page_data       = [
		'preview_or_post_id' => 0,
		'language'           => '', // For WPML/Polylang (@since 1.9.9)
	];
	public static $global_settings = [];
	public static $page_settings   = [];
	public static $adobe_fonts     = [];
	public static $main_query_id   = ''; // Holds the element ID of the main query element set in set_main_archive_query() @since 1.12.2

	public function __construct() {
		// Initialize active templates through helper function (@since 1.12)
		if ( is_array( self::$active_templates ) && empty( self::$active_templates ) ) {
			self::init_active_templates();
		}

		self::get_global_data();

		add_action( 'pre_get_posts', [ $this, 'set_main_archive_query' ] );

		// Set active templates
		add_action( 'wp', [ $this, 'set_active_templates' ] );

		// Set page data (AJAX)
		add_action( 'wp_loaded', [ $this, 'set_ajax_page_data' ] );

		// Set page data (no AJAX)
		add_action( 'wp', [ $this, 'set_page_data' ] );

		// Set page data on REST API calls
		add_action( 'rest_api_init', [ $this, 'set_page_data' ] );

		add_action( 'update_option_' . BRICKS_DB_GLOBAL_CLASSES, [ $this, 'update_option_bricks_global_classes' ], 10, 3 );

		add_filter( 'wp_prepare_themes_for_js', [ $this, 'wp_prepare_themes_for_js' ] );
	}

	/**
	 * Initialize active templates
	 *
	 * @since 1.12
	 */
	public static function init_active_templates() {
		self::$active_templates = [
			'header'              => 0,
			'footer'              => 0,
			'content'             => 0,
			'archive'             => 0,
			'error'               => 0,
			'search'              => 0,
			'section'             => 0, // Use in "Template"
			'password_protection' => 0, // @since 1.11.1
			'popup'               => [], // Array with popup template IDs
		];
	}

	/**
	 * Support autoupdate
	 *
	 * To always show "Enable/disable auto-updates" link for Bricks.
	 * Otherwise, link only shows when an update is available.
	 */
	public function wp_prepare_themes_for_js( $prepared_themes ) {
		// Add auto update support for Bricks theme
		if ( isset( $prepared_themes['bricks']['autoupdate']['supported'] ) ) {
			$prepared_themes['bricks']['autoupdate']['supported'] = true;
		}

		return $prepared_themes;
	}

	/**
	 * Log every save of empty global classes to debug where it's coming from
	 *
	 * Triggered in Bricks via:
	 *
	 * ajax.php:      wp_ajax_bricks_save_post (save post in builder)
	 * templates.php: wp_ajax_bricks_import_template (template import)
	 * converter.php: wp_ajax_bricks_run_converter (run converter from Bricks settings)
	 *
	 * @link https://developer.wordpress.org/reference/hooks/update_option_option/
	 *
	 * @since 1.7
	 */
	public function update_option_bricks_global_classes( $old_value, $new_value, $option_name ) {
		if ( $option_name === BRICKS_DB_GLOBAL_CLASSES ) {
			$old_count = is_array( $old_value ) ? count( $old_value ) : 0;
			$new_count = is_array( $new_value ) ? count( $new_value ) : 0;

			$trash       = get_option( BRICKS_DB_GLOBAL_CLASSES_TRASH, [] );
			$trash_count = count( $trash );

			// Record only global class saves where total number of global classes changed
			if ( $old_count !== $new_count || $trash_count > 0 ) {
				$current_user = wp_get_current_user();

				// Possible AJAX calls: Save post in builder, import templates, run converter
				$new_entry = [
					'timestamp'   => time(),
					'referer'     => wp_get_referer(),
					'action'      => isset( $_POST['action'] ) ? sanitize_text_field( $_POST['action'] ) : '',
					'post_id'     => isset( $_POST['postId'] ) ? intval( $_POST['postId'] ) : '',
					'user_id'     => $current_user ? $current_user->ID : 0,
					'old_count'   => $old_count,
					'new_count'   => $new_count,
					'trash_count' => $trash_count,
				];

				$saves = get_option( 'bricks_global_classes_changes', [] );

				if ( ! is_array( $saves ) ) {
					$saves = [];
				}

				// Keep the first 25 changes
				if ( count( $saves ) >= 25 ) {
					array_shift( $saves );
				}

				$saves[] = $new_entry;

				update_option( 'bricks_global_classes_changes', $saves, false );
			}
		}
	}

	/**
	 * Customize WP Main Query: Set all query_vars by user for archive/search/error template pages
	 * So the pagination will not encounter 404 errors
	 *
	 * @since 1.9.1
	 */
	public function set_main_archive_query( $query ) {
		if ( bricks_is_builder() || is_admin() || ! $query->is_main_query() ) {
			return;
		}

		$post_id              = 0;
		$set_active_templates = $query->is_archive || $query->is_search || $query->is_error || $query->is_home;

		// Archive, Search, Error, Home: Get the active template
		if ( $set_active_templates ) {
			// Set active templates
			self::set_active_templates( $query );

			// This is the template currently being used for archive/search/error/home
			$post_id = ! empty( self::$active_templates['content'] ) ? self::$active_templates['content'] : 0;
		}

		if ( $post_id ) {
			// Check if any Bricks data is set
			$bricks_data = self::get_data( $post_id );

			// Is home page (posts page): Retrieve the Bricks data too (non-standard template setup) (@since 1.10)
			if ( $query->is_home && $query->get_queried_object_id() ) {
				$bricks_posts_page_data = self::get_data( $query->get_queried_object_id() );
				// Merge the data
				if ( is_array( $bricks_posts_page_data ) ) {
					$bricks_data = array_merge( $bricks_data, $bricks_posts_page_data );
				}
			}

			// Start to scan through if any query element is set, main objective is get the query settings for the main archive query
			if ( is_array( $bricks_data ) ) {
				/**
				 * STEP: Get component data
				 *
				 * @since 2.0
				 */
				$bricks_data = self::get_component_data( $bricks_data );

				/**
				 * STEP: Get nested template data
				 *
				 * Now $bricks_data contains all the data from the main template and all nested templates.
				 *
				 * @since 1.9.1
				 */
				$bricks_data = self::get_nested_template_data( $bricks_data );

				/**
				 * STEP: Arrange the $bricks_data array
				 *
				 * $bricks_data is not sorted by position following the builder structure, we do not know which main query settings should be used if more than 1 query ticked the is_archive_main_query
				 *
				 * @since 1.9.1
				 */
				$structured_element_ids = self::elements_sequence_in_builder( $bricks_data );

				// Loop through elements follow builder structure sequence, to get main archive query settings defined by the user, only the first one will be used (@since 1.9.1)
				$archive_query_set = false;

				foreach ( $structured_element_ids as $element_id ) {
					$element = self::get_element_by_id( $element_id, $bricks_data );

					if ( ! $element ) {
						continue;
					}

					// STEP: Get global query settings (@since 2.1)
					if ( isset( $element['settings']['query'] ) ) {
						$element['settings']['query'] = Helpers::maybe_get_global_query_settings( $element['settings']['query'] ?? [] );
					}

					// Certain elements 'is_archive_main_query' is not set inside query key
					if ( isset( $element['settings']['is_archive_main_query'] ) ) {
						// WooCommerce Products element
						if ( $element['name'] === 'woocommerce-products' ) {
							$element['settings']['hasLoop']                        = 1; // #86c01086t; @since 1.10.2
							$element['settings']['query']['is_archive_main_query'] = 1;
							$element['settings']['query']['post_type']             = [ 'product' ]; // #86byx62xu
						}
					}

					// Posts element has no 'hasLoop' key, but it's a main query
					if ( $element['name'] === 'posts' && isset( $element['settings']['query']['is_archive_main_query'] ) ) {
						$element['settings']['hasLoop'] = 1; // #86c03k1ut; @since 1.10.2
					}

					// Exit: Not a query element
					if ( empty( $element['settings']['query'] ) ) {
						continue;
					}
					// Exit: foreach main query is already set once
					if ( $archive_query_set ) {
						break;
					}

					$object_type = $element['settings']['query']['objectType'] ?? 'post';

					/**
					 * Set main archive query
					 * - If hasLoop is set (active query) (@since 1.9.9)
					 * - If is_archive_main_query is set
					 * - If objectType is one of the archive_query_supported_object_types
					 */
					if (
						isset( $element['settings']['hasLoop'] ) &&
						isset( $element['settings']['query']['is_archive_main_query'] ) &&
						in_array( $object_type, Query::archive_query_supported_object_types() )
					) {
						// Unique flag to identify main archive query (@since 1.10.2)
						$query->set( 'brx_main_query', true );

						// Set element ID to identify main query element (@since 1.12.2)
						self::$main_query_id = $element_id;

						// Use the prepared query vars instead of raw element settings (@since 1.8)
						// Skip merge main query 4th parameter (true) (#86c42z22c; #86c3zyd4z; @since 2.0)
						$query_vars = Query::prepare_query_vars_from_settings( $element['settings'], $element_id, $element['name'], true );

						// Check if user set offset (@since 1.10.2)
						$has_offset = isset( $query_vars['offset'] ) && $query_vars['offset'] > 0;

						foreach ( $query_vars as $key => $value ) {
							if ( in_array( $key, Query::archive_query_arguments() ) ) {
								// Merge existing tax_query with Bricks tax_query (@since 1.9.8)
								if ( $key === 'tax_query' ) {
									$current_tax_query = $query->get( 'tax_query' );
									if ( ! empty( $current_tax_query ) ) {
										$value = Query::merge_tax_or_meta_query_vars( $current_tax_query, $value, 'tax' );
									}
								}

								// Skip user offset, calculate later
								if ( $has_offset && $key === 'offset' ) {
									continue;
								}

								$query->set( $key, $value );
							}
						}

						// Search Criteria for search result template (@since 2.2)
						if ( $query->is_search && Search::search_template_has_custom_criteria( $post_id ) ) {
							// Get search-criteria settings from template settings
							$search_term = $query->get( 's', '' );

							// Get post IDs using combined SQL search
							$post_ids = Search::get_search_template_criteria_post_ids( $post_id, $search_term );
							$post_ids = ! empty( $post_ids ) ? $post_ids : [ 0 ];

							$query->set( 'brx_original_search_term', $search_term );

							// To avoid default search behavior interfering
							$query->set( 's', '' );

							// Modify main query to include only the found post IDs
							$query->set( 'post__in', $post_ids );

							// Since we removed the "s" parameter, we need to restore the original search term in search box or it will be empty
							add_filter(
								'get_search_query',
								function( $query ) {
									if ( is_main_query() && is_search() ) {
										global $wp_query;
										$original_term = $wp_query->get( 'brx_original_search_term' );
										if ( $original_term ) {
											return $original_term;
										}
									}
									return $query;
								}
							);

							// Amend orderby only if weight score is used and no other sort is applied by user
							if ( Search::use_weight_score( $post_id ) && ! empty( $post_ids ) && ! isset( $query_vars['brx_sort_applied'] ) ) {
								$query->set( 'orderby', 'post__in' );
								$query->set( 'order', '' );
								$query->set( 'brx_orderby', 'weighted_relevance' );
							}
						}

						/**
						 * Handle offset
						 *
						 * - Calculate offset based on user offset and current page
						 * - Fix found_posts for main query's pagination
						 *
						 * @since 1.10.2
						 */
						if ( $has_offset ) {
							$user_offset = $query_vars['offset'];
							$new_offset  = $user_offset + ( $query->get( 'paged', 1 ) - 1 ) * $query->get( 'posts_per_page' );

							$query->set( 'offset', $new_offset );

							add_filter(
								'found_posts',
								function( $found_posts, $query ) use ( $user_offset ) {
									if ( bricks_is_builder() || is_admin() || ! $query->get( 'brx_main_query' ) ) {
										return $found_posts;
									}
									return $found_posts - $user_offset;
								},
								10,
								2
							);
						}

						/**
						 * Handle random seed
						 *
						 * Generate random seed statement and add to posts_orderby filter and target the main query
						 *
						 * @since 1.9.8
						 */
						if ( Query::use_random_seed( $query_vars ) ) {
							$random_seed_statement = Query::get_random_seed_statement( $element_id, $query_vars );

							if ( ! empty( $random_seed_statement ) ) {
								add_filter(
									'posts_orderby',
									function( $orderby, $query ) use ( $random_seed_statement ) {
										// Exit if it's not main query
										if ( bricks_is_builder() || is_admin() || ! $query->get( 'brx_main_query' ) ) {
											return $orderby;
										}

										return $random_seed_statement;
									},
									10,
									2
								);
							}
						}

						// Set flag to exit foreach
						$archive_query_set = true;
					}
				}
			}
		}

		/**
		 * Re-init active templates
		 *
		 * @see #86bw4pmd0
		 * @since 1.9.2
		 */
		if ( $set_active_templates ) {
			self::init_active_templates();
		}
	}

	/**
	 * Set active templates for use throughout the theme
	 */
	public static function set_active_templates( $post_id = 0 ) {
		// Check if set_active_templates already ran
		if ( isset( self::$active_templates['post_id'] ) ) {
			return;
		}

		if ( ! $post_id || is_object( $post_id ) ) {
			$post_id = get_the_ID();
		}

		// NOTE: Set post ID to posts page. Code will try to find templates for the page defined as the blog page
		if ( is_home() ) {
			$post_id = get_option( 'page_for_posts' );
		}

		$post_id = intval( $post_id );

		$post_type = get_post_type( $post_id );

		$preview_type = ''; // Only applicable to templates

		$content_type = 'content'; // = default content type

		// Check if post is Bricks template
		if ( is_singular( BRICKS_DB_TEMPLATE_SLUG ) ) {
			$template_type = get_post_meta( $post_id, BRICKS_DB_TEMPLATE_TYPE, true );

			if ( in_array( $template_type, [ 'header', 'footer' ] ) ) {
				self::$active_templates[ $template_type ] = $post_id;

				$preview_type = Helpers::get_template_setting( 'templatePreviewType', $post_id );

				switch ( $preview_type ) {
					case 'single':
						$preview_id = Helpers::get_template_setting( 'templatePreviewPostId', $post_id );

						$content_type                      = 'content';
						self::$active_templates['content'] = $preview_id;
						break;

					case 'search':
						$content_type = 'search';
						break;

					case 'archive-recent-posts':
					case 'archive-author':
					case 'archive-date':
					case 'archive-cpt':
					case 'archive-term':
						$content_type = 'archive';
						break;
				}

			} else {
				self::$active_templates['content'] = $post_id;
				$content_type                      = $template_type;
			}
		}

		// All other cases (builder & frontend)
		else {
			// Find content type needed given the current page load query
			$tag_templates = [
				'is_404'               => 'error',
				'is_search'            => 'search',
				'is_home'              => 'content',
				'is_front_page'        => 'content',
				'is_singular'          => 'content',
				'is_product_taxonomy'  => 'wc_archive',
				'is_post_type_archive' => 'archive',
				'is_tax'               => 'archive',
				'is_author'            => 'archive',
				'is_date'              => 'archive',
				'is_archive'           => 'archive',
			];

			foreach ( $tag_templates as $tag => $type ) {
				if ( function_exists( $tag ) && call_user_func( $tag ) ) {
					$content_type = $type;

					if ( 'content' != $type ) {
						$post_type = '';
						$post_id   = 0;
					}

					break;
				}
			}
		}

		// NOTE: Undocumented
		$content_type = apply_filters( 'bricks/database/content_type', $content_type, $post_id );

		// NOTE: Undocumented
		$post_id = apply_filters( 'bricks/builder/data_post_id', $post_id );

		self::$active_templates['post_id']      = $post_id;
		self::$active_templates['post_type']    = $post_type;
		self::$active_templates['content_type'] = $content_type;

		// Get all available templates
		$template_ids = self::get_all_templates_by_type();

		// Preview id is only set if template is using populate content as single (with templatePreviewPostId)
		$preview_id = isset( $preview_id ) ? $preview_id : $post_id;

		$password_protection_template_id = 0;
		$excluded_ids                    = [];

		// For each template part, try to find the best template available
		foreach ( [ 'header', 'footer', 'content' ] as $template_part ) {
			if ( ! empty( self::$active_templates[ $template_part ] ) ) {
				continue;
			}

			$template_id = self::find_template_id( $template_ids, $template_part, $content_type, $preview_id, $preview_type );

			// Check if this is a password protection template (@since 1.11.1)
			if ( Templates::get_template_type( $template_id ) === 'password_protection' && $template_part === 'content' ) {
				// Allow Password_Protection class to decide if the template should be rendered
				if ( Password_Protection::is_active( $template_id ) ) {
					$password_protection_template_id               = $template_id;
					self::$active_templates[ $template_part ]      = $template_id; // Set 'content' to password protection template ID
					self::$active_templates['password_protection'] = $template_id; // Set 'password_protection' to password protection template ID (used in form element to check if the form is in a password protected template)
				} else {
					// If password protection is not active, exclude it and find another template
					$excluded_ids[]                           = $template_id;
					$template_id                              = self::find_template_id( $template_ids, $template_part, $content_type, $preview_id, $preview_type, $excluded_ids );
					self::$active_templates[ $template_part ] = $template_id;
				}
			} else {
				self::$active_templates[ $template_part ] = $template_id;
			}
		}

		// If a password protection template is active, apply exclusions
		if ( $password_protection_template_id ) {
			if ( Password_Protection::should_exclude_template_part( 'header', $password_protection_template_id ) ) {
				self::$active_templates['header'] = 0;
			}
			if ( Password_Protection::should_exclude_template_part( 'footer', $password_protection_template_id ) ) {
				self::$active_templates['footer'] = 0;
			}
		}

		/**
		 * Get all popups
		 *
		 * @since 1.10.2: If maintenance mode is disabled OR popups are explicitly enabled in maintenance mode OR user can bypass maintenance mode.
		 * @since 2.3.3: Use request-level maintenance state, so maintenance bypass filters still load popups.
		 */
		if ( ! Maintenance::is_applied() || self::get_setting( 'maintenanceRenderPopups' ) || Capabilities::current_user_can_bypass_maintenance_mode() ) {
			if ( ! $password_protection_template_id || ! Password_Protection::should_exclude_template_part( 'popup', $password_protection_template_id ) ) {
				self::$active_templates['popup'] = self::find_templates( $template_ids, 'popup', $preview_id, $preview_type );
			}
		}

		// Ensure popup being previewed is included
		if ( Templates::get_template_type( $post_id ) === 'popup' && ! in_array( $post_id, self::$active_templates['popup'] ) ) {
			self::$active_templates['popup'][] = $post_id;
		}

		// If $content_type != header, footer, content, section, popup; set $active_template = content
		if ( isset( $content_type ) && ! in_array( $content_type, [ 'header', 'footer', 'section', 'content', 'popup' ] ) ) {
			self::$active_templates[ $content_type ] = self::$active_templates['content'];
		}

		// No templates defined, set page/cpt content if Bricks is supported
		if ( ! empty( $post_id ) && Helpers::is_post_type_supported( $post_id ) && empty( self::$active_templates['content'] ) ) {
			self::$active_templates['content'] = $post_id;
		}

		/**
		 * Allow to modify the active templates
		 *
		 * @see https://academy.bricksbuilder.io/article/filter-bricks-active_templates/
		 *
		 * @since 1.8.4
		 */
		self::$active_templates = apply_filters( 'bricks/active_templates', self::$active_templates, $post_id, self::$active_templates['content_type'] );

		// Set header position (to use in bricksData.headerPosition)
		if ( self::$active_templates['header'] > 0 ) {
			$header_position       = Helpers::get_template_setting( 'headerPosition', intval( self::$active_templates['header'] ) );
			self::$header_position = isset( $header_position ) && ! empty( $header_position ) ? $header_position : 'top';
		}
	}

	/**
	 * Finds the most suitable template id for a specific context
	 *
	 * @param array  $template_ids Organized by type.
	 * @param string $template_part header, footer or content.
	 * @param string $content_type What type of content is expected: content, archive, search, error.
	 * @param string $post_id Current post_id or preview_id.
	 * @param string $preview_type If template, and populate content is set.
	 * @param array  $excluded_ids Array of template IDs to exclude from consideration. (@since 1.11.1)
	 */
	public static function find_template_id( $template_ids, $template_part, $content_type, $post_id, $preview_type, $excluded_ids = [] ) {
		$found_templates           = []; // Hold all the found template ids for the context, with score 0.low XX.high [score=>template id]
		$disable_default_templates = self::get_setting( 'defaultTemplatesDisabled', false );

		/**
		 * STEP: Check for password protection templates first to ensure highest priority
		 * Password protection templates should override any other matching template
		 *
		 * @since 1.12
		 */
		if ( ! empty( $template_ids['body'] ) ) {
			foreach ( $template_ids['body'] as $template_id ) {
				// Skip excluded template IDs
				if ( in_array( $template_id, $excluded_ids, true ) ) {
					continue;
				}

				// Check if this is a password protection template
				if ( $template_part === 'content' && Templates::get_template_type( $template_id ) === 'password_protection' ) {
					$template_conditions = Helpers::get_template_setting( 'templateConditions', $template_id );

					// Return immediately if conditions match
					if ( ! empty( self::screen_conditions( [], $template_id, $template_conditions, $post_id, $preview_type ) ) ) {
						return $template_id;
					}
				}
			}
		}

		// STEP: Continue with regular template evaluation if no password protection template matched

		// Loop for all the templates and template conditions and assign scores
		// 0 - Default (no condition set)
		// 1 - Default to a specific template type (I'm looking for a search template, and this is type search)
		// 2 - Entire website (condition = any)
		// 8 - Terms, specific archives, children of specific Post ID
		// 9 - Front page
		// 10 - Specific Post ID (best match)

		// 'body' list includes all template types != header, footer, section & popup
		$template_loop_type = $template_part === 'content' ? 'body' : $template_part;

		if ( empty( $template_ids[ $template_loop_type ] ) ) {
			return 0;
		}

		// Check template conditions
		foreach ( $template_ids[ $template_loop_type ] as $template_id ) {
			// Skip excluded template IDs (@since 1.11.1)
			if ( in_array( $template_id, $excluded_ids ) ) {
				continue;
			}

			$template_conditions = Helpers::get_template_setting( 'templateConditions', $template_id );

			if ( ! $template_conditions ) {
				if ( ! $disable_default_templates ) {
					// No conditions, if defaults are possible, set it as default (but don't set a Search template as fallback of a Page content)
					if ( in_array( $template_part, [ 'header', 'footer' ] ) ) {
						$found_templates[0] = $template_id;
					}

					// If template_part is content, and this template type = content_type (search = search) then it might be a good default
					if ( 'content' === $template_part && 'content' !== $content_type && ! empty( $template_ids[ $content_type ] ) && in_array( $template_id, $template_ids[ $content_type ] ) ) {
						$found_templates[1] = $template_id;
					}
				}

				continue;
			}

			$found_templates = self::screen_conditions( $found_templates, $template_id, $template_conditions, $post_id, $preview_type );
		}

		// Return template id with highest score.
		if ( ! empty( $found_templates ) ) {
			$max = max( array_keys( $found_templates ) );

			return $found_templates[ $max ];
		}

		// No template found
		return 0;
	}

	/**
	 * Find all the templates available for a specific context based on the template conditions
	 *
	 * @param array  $template_ids List of templates per template type.
	 * @param string $template_part header, footer or content.
	 */
	public static function find_templates( $template_ids, $template_part, $post_id, $preview_type ) {
		$found_templates = [];

		$template_loop_type = $template_part === 'content' ? 'body' : $template_part;

		if ( empty( $template_ids[ $template_loop_type ] ) ) {
			return [];
		}

		// Check template conditions
		foreach ( $template_ids[ $template_loop_type ] as $template_id ) {
			$template_conditions = Helpers::get_template_setting( 'templateConditions', $template_id );

			$found = self::screen_conditions( [], $template_id, $template_conditions, $post_id, $preview_type );

			if ( ! empty( $found ) ) {
				$found_templates[] = $template_id;
			}
		}

		return $found_templates;
	}

	/**
	 * Get all templates by type
	 * - If Object Cache is enabled, it will return the cached templates
	 * - If Object Cache is not enabled, it will query the database for all templates
	 * - The templates are saved in static variable self::$all_templates so subsequent calls will return the cached templates
	 *
	 * @return array
	 * - Array of template IDs organized by type (header, footer, content, archive)
	 * @since 2.0
	 */
	public static function get_all_templates_by_type() {
		// Last changed timestamp is set on Templates::flush_templates_cache()
		$last_changed = wp_cache_get_last_changed( 'bricks_' . BRICKS_DB_TEMPLATE_SLUG );
		// Undocumented
		$cache_key = apply_filters( 'bricks/database/get_all_templates_cache_key', 'all_templates_' . $last_changed );
		$output    = wp_cache_get( $cache_key, 'bricks' );

		if ( $output === false ) {
			// Maybe template_ids are already set in self::$all_templates (Non object cache)
			if ( isset( self::$all_templates ) && is_array( self::$all_templates ) && ! empty( self::$all_templates ) ) {
				return self::$all_templates;
			}

			// If not, get all templates from the database
			$args = [
				'post_type'      => BRICKS_DB_TEMPLATE_SLUG,
				'posts_per_page' => -1,
				'post_status'    => 'publish',
				'fields'         => 'ids',
			];

			/**
			 * Filter query args for get_posts()
			 *
			 * Currently used by WPML to get correct templates (@see #862j3xyg7)
			 *
			 * @since 1.7
			 */
			$args = apply_filters( 'bricks/database/bricks_get_all_templates_by_type_args', $args );

			$template_ids = get_posts( $args );

			$output = [];

			// Organize templates by type
			foreach ( $template_ids as $t_id ) {
				$type = get_post_meta( $t_id, BRICKS_DB_TEMPLATE_TYPE, true );

				// Skip templates without type
				if ( ! $type ) {
					continue;
				}

				$output[ $type ][] = $t_id;

				if ( ! in_array( $type, [ 'header', 'footer', 'section', 'popup' ] ) ) {
					$output['body'][] = $t_id; // Adds to the 'body' template type all the other types like Content, Archive, Search Results, Error Page as they are a kind of body content
				}
			}

			// PHP Cache: For single request
			self::$all_templates = $output;

			// Object Cache: Set the templates cache for 1 day
			wp_cache_set( $cache_key, $output, 'bricks', DAY_IN_SECONDS );
		}

		return $output;
	}

	/**
	 * Set default header/footer template
	 *
	 * If no template with matching templateCondition(s) has been set.
	 *
	 * Can be disabled via admin setting 'defaultTemplatesDisabled'.
	 *
	 * @since 1.0
	 */
	public static function set_default_template( $template_type = '' ) {
		if ( ! $template_type ) {
			return;
		}

		$disable_default_templates = self::get_setting( 'defaultTemplatesDisabled', false );

		// Return if 'defaultTemplatesDisabled' is set
		$current_template_type = get_post_meta( get_the_ID(), BRICKS_DB_TEMPLATE_TYPE, true );

		if ( $disable_default_templates && $current_template_type !== $template_type ) {
			return;
		}

		$template_ids = get_posts(
			[
				'post_type'      => BRICKS_DB_TEMPLATE_SLUG,
				'posts_per_page' => -1,
				'meta_query'     => [
					[
						'key'   => BRICKS_DB_TEMPLATE_TYPE,
						'value' => $template_type,
					],
				],
				'post_status'    => 'publish',
				'fields'         => 'ids',
			]
		);

		$template_id = count( $template_ids ) ? $template_ids[0] : false;

		if ( $template_id ) {
			self::$active_templates[ $template_type ] = intval( $template_id );
		}
	}

	/**
	 * Helper function to screen a set of template or theme style conditions and check if they apply given the context
	 *
	 * @param array  $found Holds array of found object IDs (the key is the score).
	 * @param string $object_id Could be template_id or the style_id.
	 * @param array  $conditions Template or Theme Style conditions.
	 * @param int    $post_id Real or Preview).
	 * @param string $preview_type The preview type (single, search, archive, etc.).
	 *
	 * @return array Found conditions array ($score => $object_id)
	 */
	public static function screen_conditions( $found, $object_id, $conditions, $post_id, $preview_type ) {
		if ( empty( $conditions ) ) {
			return $found;
		}

		$post_type = get_post_type( $post_id );

		$is_valid = true; // Used to exclude this object if an excluding condition applies

		$scores = []; // Holds scores of this object_id

		$template_settings = Helpers::get_template_settings( $object_id );

		// Check if this is a password protection template
		$pp_template_location = $template_settings['passwordProtectionSource'] ?? 'both';
		$is_pp_template       = $pp_template_location !== 'wordpress' ? Templates::get_template_type( $object_id ) === 'password_protection' : false;

		foreach ( $conditions as $condition ) {
			if ( ! $is_valid ) {
				break;
			}

			// Check if main template condition is set
			if ( ! isset( $condition['main'] ) ) {
				continue;
			}

			$exclude = isset( $condition['exclude'] );

			if ( ! empty( $post_id ) ) {
				// 1. Check if template was set for a specific post ID or children
				if ( $condition['main'] === 'ids' && isset( $condition['ids'] ) ) {
					// WPML: Translate condition IDs to current language (@since 2.0)
					if ( \Bricks\Integrations\Wpml\Wpml::$is_active && has_filter( 'wpml_object_id' ) ) {
						foreach ( $condition['ids'] as &$condition_post_id ) {
							$condition_post_type = get_post_type( $condition_post_id );
							if ( $condition_post_type ) {
								$condition_post_id = apply_filters( 'wpml_object_id', $condition_post_id, $condition_post_type, true );
							}
						}
					}

					// Specific post ID
					if ( in_array( $post_id, $condition['ids'] ) ) {
						$is_valid = ! $exclude;
						$scores[] = 10;
					}

					// Apply to child pages
					elseif ( isset( $condition['idsIncludeChildren'] ) ) {
						$ancestors = get_post_ancestors( $post_id );

						foreach ( $ancestors as $ancestor_id ) {
							if ( in_array( $ancestor_id, $condition['ids'] ) ) {
								$is_valid = ! $exclude;
								$scores[] = 8; // Less important than a template set for a specific ID
								break;
							}
						}
					}
				}

				// 2. Check if template was set for a specific term assigned to the post
				if ( $condition['main'] === 'terms' && isset( $condition['terms'] ) ) {
					$terms = $condition['terms'];

					foreach ( $terms as $term ) {
						$tax_term = explode( '::', $term );
						$taxonomy = $tax_term[0];
						$term_id  = $tax_term[1];

						// WPML: Translate term ID to current language (@since 2.0)
						if ( \Bricks\Integrations\Wpml\Wpml::$is_active && has_filter( 'wpml_object_id' ) && is_numeric( $term_id ) ) {
							$term_id = apply_filters( 'wpml_object_id', $term_id, $taxonomy, true );
						}

						$post_terms = wp_get_post_terms( $post_id, $taxonomy, [ 'fields' => 'ids' ] );

						if ( is_array( $post_terms ) && in_array( $term_id, $post_terms ) ) {
							$is_valid = ! $exclude;
							$scores[] = 8;
						}
					}
				}

				// 3. Check if template applies to a specific post type
				if ( $condition['main'] === 'postType' && isset( $condition['postType'] ) && in_array( $post_type, $condition['postType'] ) ) {
					$is_valid = ! $exclude;
					$scores[] = 7;
				}
			}

			// Archive (any/author/data/term)
			if ( is_archive() && $condition['main'] === 'archiveType' ) {
				if ( ! isset( $condition['archiveType'] ) ) {
					continue;
				}

				// Archive pages include category, tag, author, date, custom post type, and custom taxonomy based archives.
				if ( in_array( 'any', $condition['archiveType'] ) && ( is_archive() || strpos( $preview_type, 'archive' ) !== false ) ) {
					$is_valid = ! $exclude;
					$scores[] = 3;
				}

				// This condition allows for multiple values. Since is_archive includes all the following conditions we need to test them as well
				if ( in_array( 'postType', $condition['archiveType'] ) && ( is_post_type_archive() || $preview_type === 'archive-cpt' ) ) {
					if ( empty( $condition['archivePostTypes'] ) ) {
						$is_valid = ! $exclude;
						$scores[] = 7;
					} else {
						// Previewing a template with content set to a CPT archive
						if ( $preview_type === 'archive-cpt' ) {
							$preview_cpt = Helpers::get_template_setting( 'templatePreviewPostType', $post_id );

							if ( $preview_cpt && in_array( $preview_cpt, $condition['archivePostTypes'] ) ) {
								$is_valid = ! $exclude;
								$scores[] = 8;
							}
						}
						// or, check if the post type archive matches the post type condition
						elseif ( is_post_type_archive( $condition['archivePostTypes'] ) ) {
							$is_valid = ! $exclude;
							$scores[] = 8;
						}
					}
				} elseif ( in_array( 'author', $condition['archiveType'] ) && ( is_author() || $preview_type === 'archive-author' ) ) {
					$is_valid = ! $exclude;
					$scores[] = 8;
				} elseif ( in_array( 'date', $condition['archiveType'] ) && ( is_date() || $preview_type === 'archive-date' ) ) {
					$is_valid = ! $exclude;
					$scores[] = 8;
				} elseif ( in_array( 'term', $condition['archiveType'] ) && ( is_category() || is_tag() || is_tax() || $preview_type === 'archive-term' ) ) {
					// Apply template to selected archive terms
					if ( isset( $condition['archiveTerms'] ) && is_array( $condition['archiveTerms'] ) ) {

						// Previewing a template, with populate content set to archive of term
						if ( $preview_type === 'archive-term' ) {
							// Note the post_id here is the template post Id (because in this archive situation the preview_id was not set)
							$preview_term = Helpers::get_template_setting( 'templatePreviewTerm', $post_id );

							if ( ! empty( $preview_term ) ) {
								$preview_term     = explode( '::', $preview_term );
								$queried_taxonomy = isset( $preview_term[0] ) ? $preview_term[0] : '';
								$queried_term_id  = isset( $preview_term[1] ) ? intval( $preview_term[1] ) : '';

								// WPML: Translate term ID to current language (@since 2.0)
								if ( \Bricks\Integrations\Wpml\Wpml::$is_active && has_filter( 'wpml_object_id' ) && $queried_term_id ) {
									$queried_term_id = apply_filters( 'wpml_object_id', $queried_term_id, $queried_taxonomy, true );
								}
							}
						}

						// All the other situations in frontend: is_category() || is_tag() || is_tax()
						else {
							$queried_object = get_queried_object();

							if ( is_object( $queried_object ) ) {
								$queried_term_id  = intval( $queried_object->term_id );
								$queried_taxonomy = $queried_object->taxonomy;

								// WPML: Translate term ID to current language (@since 2.0)
								if ( \Bricks\Integrations\Wpml\Wpml::$is_active && has_filter( 'wpml_object_id' ) && $queried_term_id ) {
									$queried_term_id = apply_filters( 'wpml_object_id', $queried_term_id, $queried_taxonomy, true );
								}
							}
						}

						// Check if queried taxonomy and term_id matches any of the selected archive terms
						if ( ! empty( $queried_term_id ) && ! empty( $queried_taxonomy ) ) {
							foreach ( $condition['archiveTerms'] as $archive_term ) {
								$term_parts = explode( '::', $archive_term );
								$taxonomy   = $term_parts[0];
								$term_id    = $term_parts[1];

								// WPML: Translate condition term ID to current language (@since 2.0)
								if ( \Bricks\Integrations\Wpml\Wpml::$is_active && has_filter( 'wpml_object_id' ) && is_numeric( $term_id ) && $term_id !== 'all' ) {
									$term_id = apply_filters( 'wpml_object_id', $term_id, $taxonomy, true );
								}

								if ( $queried_taxonomy === $taxonomy ) {
									if ( $queried_term_id === intval( $term_id ) ) {
										$is_valid = ! $exclude;
										$scores[] = 8;
										break;
									}

									// Applied for taxonomy::all (all terms of a taxonomy)
									elseif ( 'all' == $term_id ) {
										$is_valid = ! $exclude;
										$scores[] = 7;
										break;
									}

									// The condition includes child terms, check if the queried term id is child of the term id set in the condition
									elseif ( isset( $condition['archiveTermsIncludeChildren'] ) && term_is_ancestor_of( $term_id, $queried_term_id, $queried_taxonomy ) ) {
										$is_valid = ! $exclude;
										$scores[] = 8;
										break;
									}
								}
							}
						}
					}

					// Apply template to all archives terms
					else {
						$is_valid = ! $exclude;
						$scores[] = 4;
					}
				}

			} // End archive test

			// Check for search
			elseif ( $condition['main'] === 'search' && ( is_search() || $preview_type === 'search' ) ) {
				$is_valid = ! $exclude;
				$scores[] = 8;
			}

			// Check for error
			elseif ( $condition['main'] === 'error' && ( is_404() || $preview_type === 'error' ) ) {
				$is_valid = ! $exclude;
				$scores[] = 8;
			}

			// Check for front page (it might compete with single post rules)
			if ( $condition['main'] === 'frontpage' ) {

				// Only use 'page_on_front' option if we are in an AJAX calls
				if ( bricks_is_ajax_call() || bricks_is_rest_call() ) {
					// Use 'page_on_front' option as is_front_page() is not reliable in AJAX calls
					$front_page_id = get_option( 'page_on_front' );

					// WPML: Translate front page ID to current language (@since 2.0)
					if ( \Bricks\Integrations\Wpml\Wpml::$is_active && has_filter( 'wpml_object_id' ) && $front_page_id ) {
						$front_page_id = apply_filters( 'wpml_object_id', $front_page_id, 'page', true );
					}

					$is_front_page = absint( $post_id ) == absint( $front_page_id );
				} else {
					$is_front_page = is_front_page();
				}

				if ( $is_front_page ) {
					$is_valid = ! $exclude;
					$scores[] = 9;
				}

			}

			// Check for entire website
			if ( $condition['main'] === 'any' ) {
				$is_valid = ! $exclude;
				$scores[] = 2;
			}

			// Is password protection template: For each valid score increase priority by 100
			if ( $is_pp_template ) {
				$scores = array_map(
					function( $score ) {
						return $score + 100; // Add to existing score
					},
					$scores
				);
			}

			/**
			 * For each template (and theme style) condition allow setting a score based on custom template conditions (which are set via: builder/settings/{$this->setting_type}/controls_data)
			 *
			 * https://academy.bricksbuilder.io/article/filter-bricks-screen_conditions-scores/
			 *
			 * @since 1.5.5
			 */
			$scores = apply_filters( 'bricks/screen_conditions/scores', $scores, $condition, $post_id, $preview_type );
		}

		if ( $is_valid ) {
			// Only remove duplicates if the theme styles loading method is set to the default, which is most-specific (@since 2.0)
			if ( ! self::get_setting( 'themeStylesLoadingMethod' ) ) {
				$scores = array_unique( $scores );
			}

			foreach ( $scores as $score ) {
				$found[ $score ] = $object_id;
			}
		}

		return $found;
	}

	/**
	 * Check if header or footer is disabled (via page settings) for the current context
	 *
	 * Page setting keys: headerDisabled, footerDisabled
	 *
	 * @return bool
	 * @since 1.5.4
	 */
	public static function is_template_disabled( $template_type ) {
		$setting_key      = "{$template_type}Disabled";
		$original_post_id = self::$page_data['original_post_id'] ?? 0;

		// Return: Previewing header or footer template
		if ( $original_post_id && Templates::get_template_type( $original_post_id ) === $template_type ) {
			return false;
		}

		$template_id = self::$active_templates['content'] ?? 0;

		/**
		 * Post rendered through template and post has Bricks data: Get page settings from post instead of template
		 *
		 * @since 1.10.2: Exclude archive pages as they can't by edited directly with Bricks
		 */
		if ( $template_id && $template_id !== $original_post_id && ! is_archive() ) {
			$page_settings = get_post_meta( $original_post_id, BRICKS_DB_PAGE_SETTINGS, true );

			if ( isset( $page_settings[ $setting_key ] ) ) {
				return true;
			}
		}

		return isset( self::$page_settings[ $setting_key ] );
	}

	/**
	 * Get template elements
	 *
	 * @since 1.0
	 *
	 * @param string  $content_type Type of content (header, content, footer).
	 * @param boolean $force_post_data Force checking only the specific post data without considering templates.
	 */
	public static function get_template_data( $content_type, $force_post_data = false ) {
		switch ( $content_type ) {
			case 'header':
				if ( self::is_template_disabled( 'header' ) ) {
					return;
				}

				$meta_key = BRICKS_DB_PAGE_HEADER;
				break;

			case 'footer':
				if ( self::is_template_disabled( 'footer' ) ) {
					return;
				}

				$meta_key = BRICKS_DB_PAGE_FOOTER;
				break;

			default:
				$meta_key = BRICKS_DB_PAGE_CONTENT;
				break;
		}

		$template_id = self::$active_templates[ $content_type ] ?? false;

		// Only check active templates if force_post_data is false (@since 1.12.2)
		if ( $force_post_data ) {
			$template_id = false;
		}

		// No template found: Return Bricks content data
		if (
			! is_archive() &&
			! is_search() &&
			! $template_id &&
			$content_type !== 'header' &&
			$content_type !== 'footer'
		) {
			$elements = get_post_meta( get_the_ID(), BRICKS_DB_PAGE_CONTENT, true );
		} else {
			$elements = get_post_meta( $template_id, $meta_key, true );
		}

		return $elements;
	}

	/**
	 * Get Bricks data by post_id and content_area (header/content/footer)
	 *
	 * @since 1.0
	 */
	public static function get_data( $post_id = 0, $content_area = '' ) {
		if ( ! $post_id ) {
			$post_id = get_the_ID();
		}

		$meta_key = self::get_bricks_data_key( $content_area );

		$elements = get_post_meta( $post_id, $meta_key, true );

		return is_array( $elements ) ? $elements : [];
	}

	/**
	 * Get the Bricks data key for a specific template type (header/content/footer)
	 *
	 * @since 1.5.1
	 *
	 * @param string $content_area
	 * @return string
	 */
	public static function get_bricks_data_key( $content_area = '' ) {
		switch ( $content_area ) {
			case 'header':
				$meta_key = BRICKS_DB_PAGE_HEADER;
				break;

			case 'footer':
				$meta_key = BRICKS_DB_PAGE_FOOTER;
				break;

			default:
				$meta_key = BRICKS_DB_PAGE_CONTENT;
				break;
		}

		return $meta_key;
	}

	/**
	 * Get global settings from options table
	 *
	 * @since 1.0
	 */
	public static function get_setting( $key, $default = false ) {
		return isset( self::$global_settings[ $key ] ) ? self::$global_settings[ $key ] : $default;
	}

	/**
	 * Get global queries from the correct site store.
	 *
	 * @since 2.3.2
	 *
	 * @return array
	 */
	public static function get_global_queries() {
		if ( is_multisite() && BRICKS_MULTISITE_USE_MAIN_SITE_GLOBAL_QUERIES ) {
			return get_blog_option( get_main_site_id(), BRICKS_DB_GLOBAL_QUERIES, [] );
		}

		return get_option( BRICKS_DB_GLOBAL_QUERIES, [] );
	}

	/**
	 * Update global queries in the correct site store.
	 *
	 * @since 2.3.2
	 *
	 * @param array $global_queries Global queries.
	 *
	 * @return bool
	 */
	public static function update_global_queries( $global_queries ) {
		if ( is_multisite() && BRICKS_MULTISITE_USE_MAIN_SITE_GLOBAL_QUERIES ) {
			$updated = update_blog_option( get_main_site_id(), BRICKS_DB_GLOBAL_QUERIES, $global_queries );
		} else {
			$updated = update_option( BRICKS_DB_GLOBAL_QUERIES, $global_queries );
		}

		if ( $updated ) {
			self::$global_data['globalQueries'] = $global_queries;
		}

		return $updated;
	}

	/**
	 * Get global data from options table
	 *
	 * @since 1.0
	 */
	public static function get_global_data() {
		// Components
		if ( is_multisite() && BRICKS_MULTISITE_USE_MAIN_SITE_COMPONENTS ) {
			self::$global_data['components'] = get_blog_option( get_main_site_id(), BRICKS_DB_COMPONENTS, [] );
		} else {
			self::$global_data['components'] = get_option( BRICKS_DB_COMPONENTS, [] );
		}

		// Color palette
		if ( is_multisite() && BRICKS_MULTISITE_USE_MAIN_SITE_COLOR_PALETTE ) {
			self::$global_data['colorPalette'] = get_blog_option( get_main_site_id(), BRICKS_DB_COLOR_PALETTE, [] );
		} else {
			self::$global_data['colorPalette'] = get_option( BRICKS_DB_COLOR_PALETTE, [] );
		}

		// Populate with default colors if color palette is not an array (@since 2.2)
		if ( ! is_array( self::$global_data['colorPalette'] ) || empty( self::$global_data['colorPalette'] ) ) {
			self::$global_data['colorPalette'] = self::default_color_palette();
		}

		// Style Manager (@since 2.2)
		self::$global_data['styleManager'] = get_option( BRICKS_DB_STYLE_MANAGER, null );

		// Global queries (@since 2.1)
		self::$global_data['globalQueries'] = self::get_global_queries();

		// Global queries categories (@since 2.1)
		if ( is_multisite() && BRICKS_MULTISITE_USE_MAIN_SITE_GLOBAL_QUERIES_CATEGORIES ) {
			self::$global_data['globalQueriesCategories'] = get_blog_option( get_main_site_id(), BRICKS_DB_GLOBAL_QUERIES_CATEGORIES, [] );
		} else {
			self::$global_data['globalQueriesCategories'] = get_option( BRICKS_DB_GLOBAL_QUERIES_CATEGORIES, [] );
		}

		// Icon sets
		if ( is_multisite() && BRICKS_MULTISITE_USE_MAIN_SITE_ICON_SETS ) {
			self::$global_data['iconSets'] = get_blog_option( get_main_site_id(), BRICKS_DB_ICON_SETS, [] );
		} else {
			self::$global_data['iconSets'] = get_option( BRICKS_DB_ICON_SETS, [] );
		}

		// Custom icons
		if ( is_multisite() && BRICKS_MULTISITE_USE_MAIN_SITE_CUSTOM_ICONS ) {
			self::$global_data['customIcons'] = get_blog_option( get_main_site_id(), BRICKS_DB_CUSTOM_ICONS, [] );
		} else {
			self::$global_data['customIcons'] = get_option( BRICKS_DB_CUSTOM_ICONS, [] );
		}

		// Disabled icon sets
		if ( is_multisite() && BRICKS_MULTISITE_USE_MAIN_SITE_DISABLED_ICON_SETS ) {
			self::$global_data['disabledIconSets'] = get_blog_option( get_main_site_id(), BRICKS_DB_DISABLED_ICON_SETS, [] );
		} else {
			self::$global_data['disabledIconSets'] = get_option( BRICKS_DB_DISABLED_ICON_SETS, [] );
		}

		// Font favorites (@since 2.0)
		if ( is_multisite() && BRICKS_MULTISITE_USE_MAIN_SITE_FONT_FAVORITES ) {
			self::$global_data['fontFavorites'] = get_blog_option( get_main_site_id(), BRICKS_DB_FONT_FAVORITES, [] );
		} else {
			self::$global_data['fontFavorites'] = get_option( BRICKS_DB_FONT_FAVORITES, [] );
		}

		// Global classes
		if ( is_multisite() && BRICKS_MULTISITE_USE_MAIN_SITE_CLASSES ) {
			self::$global_data['globalClasses'] = get_blog_option( get_main_site_id(), BRICKS_DB_GLOBAL_CLASSES, [] );
		} else {
			self::$global_data['globalClasses'] = get_option( BRICKS_DB_GLOBAL_CLASSES, [] );
		}

		// Global classes trash
		if ( is_multisite() && BRICKS_MULTISITE_USE_MAIN_SITE_CLASSES ) {
			self::$global_data['globalClassesTrash'] = get_blog_option( get_main_site_id(), BRICKS_DB_GLOBAL_CLASSES_TRASH, [] );
		} else {
			self::$global_data['globalClassesTrash'] = get_option( BRICKS_DB_GLOBAL_CLASSES_TRASH, [] );
		}

		// Global classes categories
		if ( is_multisite() && BRICKS_MULTISITE_USE_MAIN_SITE_CLASSES_CATEGORIES ) {
			self::$global_data['globalClassesCategories'] = get_blog_option( get_main_site_id(), BRICKS_DB_GLOBAL_CLASSES_CATEGORIES, [] );
		} else {
			self::$global_data['globalClassesCategories'] = get_option( BRICKS_DB_GLOBAL_CLASSES_CATEGORIES, [] );
		}

		// Builder: Global classes locked
		if ( bricks_is_builder() ) {
			if ( is_multisite() && BRICKS_MULTISITE_USE_MAIN_SITE_CLASSES ) {
				self::$global_data['globalClassesLocked'] = get_blog_option( get_main_site_id(), BRICKS_DB_GLOBAL_CLASSES_LOCKED, [] );
			} else {
				self::$global_data['globalClassesLocked'] = get_option( BRICKS_DB_GLOBAL_CLASSES_LOCKED, [] );
			}
		}

		// Builder: Global classes timestamp (@since 1.9.9)
		if ( bricks_is_builder() ) {
			if ( is_multisite() && BRICKS_MULTISITE_USE_MAIN_SITE_CLASSES ) {
				self::$global_data['globalClassesTimestamp'] = get_blog_option( get_main_site_id(), BRICKS_DB_GLOBAL_CLASSES_TIMESTAMP, [] );
			} else {
				self::$global_data['globalClassesTimestamp'] = get_option( BRICKS_DB_GLOBAL_CLASSES_TIMESTAMP, [] );
			}
		}

		// Builder: Global classes user_id (@since 1.9.9)
		if ( bricks_is_builder() ) {
			if ( is_multisite() && BRICKS_MULTISITE_USE_MAIN_SITE_CLASSES ) {
				self::$global_data['globalClassesUser'] = get_blog_option( get_main_site_id(), BRICKS_DB_GLOBAL_CLASSES_USER, [] );
			} else {
				self::$global_data['globalClassesUser'] = get_option( BRICKS_DB_GLOBAL_CLASSES_USER, [] );
			}

			if ( ! empty( self::$global_data['globalClassesUser'] ) ) {
				self::$global_data['globalClassesUser'] = get_userdata( self::$global_data['globalClassesUser'] )->display_name ?? '';
			}
		}

		$default_pseudo_classes = [
			':hover',
			':active',
			':focus',
		];

		if ( is_multisite() && BRICKS_MULTISITE_USE_MAIN_SITE_CLASSES ) {
			self::$global_data['pseudoClasses'] = get_blog_option( get_main_site_id(), BRICKS_DB_PSEUDO_CLASSES, $default_pseudo_classes );
		} else {
			self::$global_data['pseudoClasses'] = get_option( BRICKS_DB_PSEUDO_CLASSES, $default_pseudo_classes );
		}

		// Global elements
		if ( is_multisite() && BRICKS_MULTISITE_USE_MAIN_SITE_GLOBAL_ELEMENTS ) {
			self::$global_data['elements'] = get_blog_option( get_main_site_id(), BRICKS_DB_GLOBAL_ELEMENTS, [] );
		} else {
			self::$global_data['elements'] = get_option( BRICKS_DB_GLOBAL_ELEMENTS, [] );
		}

		// Global settings
		self::$global_data['settings'] = get_option( BRICKS_DB_GLOBAL_SETTINGS, [] );

		// Remove slashes from custom CSS & JS
		if ( is_array( self::$global_data['settings'] ) ) {
			self::$global_data['settings'] = stripslashes_deep( self::$global_data['settings'] );
		}

		// Set global gettings
		self::$global_settings = self::$global_data['settings'];

		/**
		 * Disable lazy load in builder
		 *
		 * To generate template screenshots in builder.
		 *
		 * @since 1.10
		 */
		if ( bricks_is_builder_call() ) {
			self::$global_settings['disableLazyLoad'] = true;
		}

		// Global variables, if not disable in Bricks settings (since 1.9.8)
		if ( ! isset( self::$global_settings['disableVariablesManager'] ) ) {
			if ( is_multisite() && BRICKS_MULTISITE_USE_MAIN_SITE_VARIABLES_CATEGORIES ) {
				self::$global_data['globalVariables'] = get_blog_option( get_main_site_id(), BRICKS_DB_GLOBAL_VARIABLES, [] );
			} else {
				self::$global_data['globalVariables'] = get_option( BRICKS_DB_GLOBAL_VARIABLES, [] );
			}
		}

		// Global variables categories (since 1.9.8)
		if ( is_multisite() && BRICKS_MULTISITE_USE_MAIN_SITE_VARIABLES_CATEGORIES ) {
			self::$global_data['globalVariablesCategories'] = get_blog_option( get_main_site_id(), BRICKS_DB_GLOBAL_VARIABLES_CATEGORIES, [] );
		} else {
			self::$global_data['globalVariablesCategories'] = get_option( BRICKS_DB_GLOBAL_VARIABLES_CATEGORIES, [] );
		}

		// Adobe fonts: If project ID set (@since 1.7.1)
		if ( ! empty( self::$global_settings['adobeFontsProjectId'] ) ) {
			self::$adobe_fonts = get_option( BRICKS_DB_ADOBE_FONTS, [] );
		}
	}

	/**
	 * Default color palette (https://www.materialui.co/colors)
	 *
	 * Only used if no custom colorPalette is saved in db.
	 *
	 * @since 1.0
	 *
	 * @since 2.2: Use 'light' key for colors instead of 'hex'
	 *
	 * @return array
	 */
	public static function default_color_palette() {
		$colors = [
			// Grey
			[
				'light' => '#f5f5f5',
				'raw'   => 'var(--bricks-color-grey-100)'
			],
			[
				'light' => '#e0e0e0',
				'raw'   => 'var(--bricks-color-grey-300)'
			],
			[
				'light' => '#9e9e9e',
				'raw'   => 'var(--bricks-color-grey-500)'
			],
			[
				'light' => '#616161',
				'raw'   => 'var(--bricks-color-grey-700)'
			],
			[
				'light' => '#424242',
				'raw'   => 'var(--bricks-color-grey-800)'
			],
			[
				'light' => '#212121',
				'raw'   => 'var(--bricks-color-grey-900)'
			],

			// Warm colors
			[
				'light' => '#ffeb3b',
				'raw'   => 'var(--bricks-color-yellow)',
			],
			[
				'light' => '#ffc107',
				'raw'   => 'var(--bricks-color-amber)',
			],
			[
				'light' => '#ff9800',
				'raw'   => 'var(--bricks-color-orange)',
			],
			[
				'light' => '#ff5722',
				'raw'   => 'var(--bricks-color-deep-orange)',
			],
			[
				'light' => '#f44336',
				'raw'   => 'var(--bricks-color-red)',
			],
			[
				'light' => '#9c27b0',
				'raw'   => 'var(--bricks-color-purple)',
			],

			// Cool colors
			[
				'light' => '#2196f3',
				'raw'   => 'var(--bricks-color-blue)',
			],
			[
				'light' => '#03a9f4',
				'raw'   => 'var(--bricks-color-light-blue)',
			],
			[
				'light' => '#81D4FA',
				'raw'   => 'var(--bricks-color-sky-blue)',
			],
			[
				'light' => '#4caf50',
				'raw'   => 'var(--bricks-color-green)',
			],
			[
				'light' => '#8bc34a',
				'raw'   => 'var(--bricks-color-light-green)',
			],
			[
				'light' => '#cddc39',
				'raw'   => 'var(--bricks-color-lime)',
			],
		];

		$colors = apply_filters( 'bricks/builder/color_palette', $colors );

		foreach ( $colors as $index => $color ) {
			$color_id               = Helpers::generate_random_id( false );
			$colors[ $index ]['id'] = $color_id;
		}

		$palettes[] = [
			'id'     => Helpers::generate_random_id( false ),
			'name'   => 'Default',
			'colors' => $colors,
		];

		return $palettes;
	}

	/**
	 * Set page data needed for AJAX calls (builder)
	 *
	 * @since 1.3
	 */
	public static function set_ajax_page_data() {
		if (
			! bricks_is_ajax_call() ||
			empty( $_POST['action'] ) ||
			strpos( $_POST['action'], 'bricks_' ) !== 0
		) {
			return;
		}

		// In the "bricks_regenerate_css_file" ajax call, the post ID is set in the "data" property
		$post_id = isset( $_POST['postId'] ) ? intval( $_POST['postId'] ) : ( isset( $_POST['data'] ) && is_numeric( $_POST['data'] ) ? intval( $_POST['data'] ) : 0 );

		self::$page_data['original_post_id'] = $post_id;
		self::$page_data['post_id']          = $post_id;

		/**
		 * Set current page type
		 *
		 * Currently in AJAX calls set it to empty string (can be improved in the future).
		 *
		 * @since 1.8
		 */
		self::$page_data['current_page_type'] = '';

		// Check for template preview post ID
		$template_preview_post_id = Helpers::get_template_setting( 'templatePreviewPostId', $post_id );

		self::$page_data['preview_or_post_id'] = empty( $template_preview_post_id ) ? $post_id : $template_preview_post_id;

		/**
		 * Set current page type if the this is bricks template
		 *
		 * Helpers::is_bricks_template() and Helpers::get_queried_object() rely on this.
		 *
		 * @since 1.9.5
		 */
		if ( Helpers::is_bricks_preview() && get_post_type( $post_id ) === BRICKS_DB_TEMPLATE_SLUG ) {
			self::$page_data['current_page_type'] = 'post';
		}
	}

	/**
	 * Get page data from post meta
	 *
	 * @since 1.0
	 */
	public static function set_page_data( $post_id = 0 ) {
		if ( ! $post_id || is_object( $post_id ) ) {
			$post_id = get_the_ID();
		}

		/**
		 * Frontend: Current page is not a single post page
		 *
		 * E.g.: archive, search results, author page, etc.
		 *
		 * To get the user_id on the author page, we need to get the queried object ID.
		 *
		 * @since 1.7.1
		 */
		if ( ! is_singular() && ! bricks_is_builder_call() ) {
			$post_id = get_queried_object_id();
		}

		// Home: Set post ID to posts page
		if ( is_home() ) {
			$post_id = get_option( 'page_for_posts' );
		}

		// NOTE: Undocumented
		$post_id = apply_filters( 'bricks/builder/data_post_id', $post_id );

		// @since 1.8 - Set current page type
		self::$page_data['current_page_type'] = apply_filters( 'bricks/builder/current_page_type', self::get_current_page_type( get_queried_object() ) );

		// Keep $original_post_id integrity. set_page_data() also runs on Assets::generate_inline_css() for inner templates
		self::$page_data['original_post_id'] = ! empty( self::$page_data['original_post_id'] ) ? self::$page_data['original_post_id'] : $post_id;

		// $preview_or_post_id gets populated with template preview post ID OR original post ID
		$template_preview_post_id = get_post_type( self::$page_data['original_post_id'] ) === BRICKS_DB_TEMPLATE_SLUG ? Helpers::get_template_setting( 'templatePreviewPostId', self::$page_data['original_post_id'] ) : 0;

		self::$page_data['preview_or_post_id'] = empty( $template_preview_post_id ) ? self::$page_data['original_post_id'] : $template_preview_post_id;

		self::$page_data['post_id'] = $post_id;

		// Page header
		$page_header               = self::get_data( $post_id, 'header' );
		self::$page_data['header'] = is_array( $page_header ) && count( $page_header ) ? $page_header : [];

		// Page content
		$page_content               = self::get_data( $post_id, 'content' );
		self::$page_data['content'] = is_array( $page_content ) && count( $page_content ) ? $page_content : [];

		// Page footer
		$page_footer               = self::get_data( $post_id, 'footer' );
		self::$page_data['footer'] = is_array( $page_footer ) && count( $page_footer ) ? $page_footer : [];

		/**
		 * Page settings
		 *
		 * Builder: Use $post_id
		 * Frontend: Use active template ID
		 *
		 * @see #86bx4t5v3
		 */
		$page_settings_id = $post_id;

		if ( ! bricks_is_builder() && ! empty( self::$active_templates['content'] ) ) {
			$page_settings_id = self::$active_templates['content'];
		}

		$page_settings = get_post_meta( $page_settings_id, BRICKS_DB_PAGE_SETTINGS, true );

		self::$page_data['settings'] = is_array( $page_settings ) && count( $page_settings ) ? $page_settings : [];

		/**
		 * Remove slashes from custom JS
		 *
		 * @since 1.9.5: Skip page settings custom CSS as its not auto-escaped as the global settings, which are stored in the options table
		 * @since 1.10: Skip page settings custom JavaScript as well
		 */
		if ( is_array( self::$page_data['settings'] ) ) {
			foreach ( self::$page_data['settings'] as $key => $value ) {
				if ( $key === 'customCss' ||
					$key === 'customScriptsHeader' ||
					$key === 'customScriptsBodyHeader' ||
					$key === 'customScriptsBodyFooter'
				) {
					continue;
				}

				self::$page_data['settings'][ $key ] = stripslashes_deep( $value );
			}
		}

		// Set page gettings
		self::$page_settings = self::$page_data['settings'];
	}

	/**
	 * Return current page type, not considering AJAX calls
	 *
	 * @param object $object Queried object.
	 *
	 * @since 1.8
	 */
	public static function get_current_page_type( $object ) {
		if ( is_search() ) {
			return 'search';
		}

		if ( is_404() ) {
			return '404';
		}

		if ( is_a( $object, 'WP_Post' ) ) {
			return 'post';
		}

		if ( is_a( $object, 'WP_Term' ) ) {
			return 'term';
		}

		if ( is_a( $object, 'WP_User' ) ) {
			return 'user';
		}

		if ( is_a( $object, 'WP_Post_Type' ) ) {
			return 'archive';
		}

		if ( is_object( $object ) ) {
			return strtolower( get_class( $object ) );
		}
	}

	/**
	 * Get Components data with improved performance and nested component support
	 * Currently set to a maximum depth of 10 to prevent infinite recursion
	 * - If the main query set inside 10 nested components then this logic will not work, practically impossible right?
	 *
	 * @since 2.0
	 * @param array $bricks_data The elements data array
	 * @return array The modified elements data with components settings and nested components included
	 */
	public static function get_component_data( $bricks_data = [], $max_depth = 10 ) {
		// Early return if not an array
		if ( ! is_array( $bricks_data ) || empty( $bricks_data ) ) {
			return $bricks_data;
		}

		// Process all components recursively with depth tracking
		return self::process_components_recursive( $bricks_data, [], $max_depth );
	}

	/**
	 * Process components recursively with depth tracking
	 *
	 * @since 2.0
	 * @param array  $elements Elements to process
	 * @param array  $processed_ids IDs of already processed components to prevent circular references
	 * @param int    $depth_remaining Remaining recursion depth
	 * @param string $parent_component_id Parent component ID (for nested components)
	 * @param string $parent_instance_id Parent instance ID (for nested components)
	 * @return array Processed elements with all nested components
	 */
	private static function process_components_recursive( $elements, $processed_ids = [], $depth_remaining = 10, $parent_component_id = '', $parent_instance_id = '' ) {
		// Prevent infinite recursion
		if ( $depth_remaining <= 0 ) {
			return $elements;
		}

		$result          = [];
		$elements_to_add = [];

		// First pass: Process all elements and identify components
		foreach ( $elements as $element ) {
			$component_id = $element['cid'] ?? false;

			// Not a component - add to result unchanged
			if ( ! $component_id ) {
				$result[] = $element;
				continue;
			}

			// Prevent circular references
			$instance_key = $component_id . '_' . ( $element['id'] ?? '' );

			if ( in_array( $instance_key, $processed_ids, true ) ) {
				$result[] = $element;
				continue;
			}

			// Mark this component as processed
			$processed_ids[] = $instance_key;

			// Get component instance
			$component_instance = Helpers::get_component_instance( $element );

			if ( ! $component_instance || empty( $component_instance['elements'] ) ) {
				$result[] = $element;
				continue;
			}

			$component_elements = $component_instance['elements'];

			// Update the component element with settings from component instance
			foreach ( $component_elements as $component_element ) {
				// Set hierarchy tracking data
				$component_element['parentComponent'] = $component_id;
				$component_element['instanceId']      = $element['id'];

				// Track root component for deeper nesting (optional)
				if ( $parent_component_id ) {
					$component_element['rootComponent']  = $parent_component_id;
					$component_element['rootInstanceId'] = $parent_instance_id;
				}

				// If this is the main component element, update the original element
				if ( $component_element['id'] === $component_id ) {
					if ( ! empty( $component_element['settings'] ) ) {
						$element['settings'] = $component_element['settings'];
					}

					if ( ! empty( $component_element['children'] ) ) {
						$element['children'] = $component_element['children'];
					}
				}

				// Otherwise, collect for adding later
				else {
					$elements_to_add[] = $component_element;
				}
			}

			// Add the updated element to results
			$result[] = $element;
		}

		// Second pass: Process all collected nested elements (if any)
		if ( ! empty( $elements_to_add ) ) {
			// Process nested components recursively with reduced depth
			$processed_nested_elements = self::process_components_recursive(
				$elements_to_add,
				$processed_ids,
				$depth_remaining - 1,
				$parent_component_id ?: $component_id,
				$parent_instance_id ?: ( $element['id'] ?? '' )
			);

			// Merge processed nested elements into result
			$result = array_merge( $result, $processed_nested_elements );
		}

		return $result;
	}

	/**
	 * Recursively retrieve nested template data
	 *
	 * @param array $bricks_data The elements data array to search for template elements
	 *
	 * Add 3 more parameters to prvent infinite recursion (#86c99238z; @since 2.3.3)
	 * @param array $processed_template_ids Array of already processed template IDs to prevent infinite loops
	 * @param int   $depth Current recursion depth
	 * @param int   $max_depth Maximum recursion depth to prevent infinite loops (10 for now, can be adjusted as needed)
	 *
	 * @return array
	 *
	 * @since 1.9.1
	 */
	public static function get_nested_template_data( $bricks_data = [], $processed_template_ids = [], $depth = 0, $max_depth = 10 ) {
		// If the input is not an array, return it as is
		if ( ! is_array( $bricks_data ) ) {
			return $bricks_data;
		}

		// Safety check to prevent infinite recursion (#86c99238z; @since 2.3.3)
		if ( $depth > $max_depth ) {
			return $bricks_data;
		}

		// STEP: Find template elements in the array
		$found_template_elements = array_filter(
			$bricks_data,
			function( $element ) {
				return isset( $element['name'] ) && in_array( $element['name'], [ 'template' ] );
			}
		);

		// If no template elements found, return the original array
		if ( empty( $found_template_elements ) ) {
			return $bricks_data;
		}

		// STEP: Retrieve nested template data from the $found_template_elements
		$nested_template_data = [];
		$new_template_found   = false;

		foreach ( $found_template_elements as $element ) {
			$template_id = isset( $element['settings']['template'] ) ? $element['settings']['template'] : false;

			// If no template ID found, skip to the next element
			if ( ! $template_id ) {
				continue;
			}

			// Prevent processing the same template multiple times (in case of circular references) (#86c99238z; @since 2.3.3)
			if ( in_array( $template_id, $processed_template_ids, true ) ) {
				continue;
			}

			$processed_template_ids[] = $template_id; // Mark this template as processed
			$new_template_found       = true;

			// Retrieve the template data using the template ID
			$template_data = get_post_meta( $template_id, BRICKS_DB_PAGE_CONTENT, true );

			// If template data found, merge it into the $nested_template_data
			if ( ! empty( $template_data ) && is_array( $template_data ) ) {
				// Store the template data in $nested_template_data
				$nested_template_data = array_replace_recursive( $nested_template_data, $template_data );
				// Store the template data in the page data (might be used later)
				self::$page_data['template_data'][ $template_id ] = $template_data;
			}
		}

		// If no new template found, return the original array to prevent infinite loops (#86c99238z; @since 2.3.3)
		if ( ! $new_template_found || empty( $nested_template_data ) ) {
			return $bricks_data;
		}

		// STEP: Maybe there are nested template element inside $nested_template_data (recursion)
		$recursive_nested_template_data = self::get_nested_template_data(
			$nested_template_data,
			$processed_template_ids,
			$depth + 1,
			$max_depth
		);

		if ( ! empty( $recursive_nested_template_data ) ) {
			$bricks_data = array_merge_recursive( $bricks_data, $recursive_nested_template_data );
		}

		return $bricks_data;
	}

	/**
	 * Get elements sequence in builder
	 *
	 * This is used to determine the order of elements in the builder.
	 *
	 * @since 1.9.1
	 *
	 * @return array (sequence of ids)
	 */
	public static function elements_sequence_in_builder( $elements ) {
		$top_level_elements = [];

		// Get top level elements
		foreach ( $elements as $element ) {
			if ( ! isset( $element['parent'] ) || empty( $element['parent'] ) ) {
				$top_level_elements[] = $element;
			}
		}

		$sequence_of_ids = [];

		// Get sequence of ids starting from top level elements
		foreach ( $top_level_elements as $element ) {
			$sequence_of_ids[] = $element['id'];
			$sequence_of_ids   = array_merge( $sequence_of_ids, self::get_ids_by_children( $elements, $element ) );
		}

		return $sequence_of_ids;
	}

	/**
	 * Get sequence of ids by children
	 *
	 * @since 1.9.1
	 */
	public static function get_ids_by_children( $elements, $parent_element ) {
		$sequence     = [];
		$children_ids = isset( $parent_element['children'] ) ? $parent_element['children'] : false;
		// Follow the order of the children
		foreach ( $children_ids as $child_id ) {
			$sequence[]    = $child_id;
			$child_element = self::get_element_by_id( $child_id, $elements );

			if ( is_array( $child_element ) && isset( $child_element['children'] ) && ! empty( $child_element['children'] ) ) {
				$sequence = array_merge( $sequence, self::get_ids_by_children( $elements, $child_element ) ); // Recursion
			}
		}

		return $sequence;
	}

	/**
	 * Get the element by id from elements array
	 *
	 * @since 1.9.1
	 */
	public static function get_element_by_id( $element_id, $elements ) {
		$element = array_filter(
			$elements,
			function( $element ) use ( $element_id ) {
				return $element['id'] === $element_id;
			}
		);

		if ( ! empty( $element ) ) {
			return array_shift( $element );
		}

		return false;
	}

	/**
	 * Set page data language for WPML or Polylang
	 * #86c94gr3q
	 *
	 * @since 2.3.2
	 */
	public static function set_page_data_language( $language ) {
		if ( empty( $language ) || ! is_string( $language ) ) {
			return;
		}
		self::$page_data['language'] = sanitize_key( $language );
	}
}
