<?php
/**
 * Get portfolio list
 *
 * @package visual-portfolio/get
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

// Slugify.
if ( ! class_exists( 'Cocur\Slugify\Slugify' ) ) {
	require_once visual_portfolio()->plugin_path . 'vendors/slugify/RuleProvider/RuleProviderInterface.php';
	require_once visual_portfolio()->plugin_path . 'vendors/slugify/RuleProvider/DefaultRuleProvider.php';
	require_once visual_portfolio()->plugin_path . 'vendors/slugify/SlugifyInterface.php';
	require_once visual_portfolio()->plugin_path . 'vendors/slugify/Slugify.php';
}
/**
 * Class Visual_Portfolio_Get
 */
class Visual_Portfolio_Get {
	/**
	 * ID of the current printed portfolio
	 *
	 * @var int
	 */
	private static $id = 0;

	/**
	 * ID of the current printed single filter
	 *
	 * @var int
	 */
	private static $filter_id = 0;

	/**
	 * ID of the current printed single sort
	 *
	 * @var int
	 */
	private static $sort_id = 0;

	/**
	 * Random number for random order.
	 *
	 * @var int
	 */
	private static $rand_seed_session = false;

	/**
	 * Array with already used IDs on the page. Used for option 'Avoid Duplicates'
	 *
	 * @var array
	 */
	private static $used_posts = array();

	/**
	 * Check for main query to avoid duplication.
	 *
	 * @var array
	 */
	private static $check_main_query = true;

	/**
	 * Array with all used layout IDs
	 *
	 * @var array
	 */
	private static $used_layouts = array();

	/**
	 * Get all available layouts.
	 *
	 * @return array
	 */
	public static function get_all_layouts() {
        // phpcs:ignore Squiz.PHP.CommentedOutCode.Found, Squiz.Commenting.BlockComment.NoEmptyLineBefore
		/*
		 * Example:
			array(
				'new_layout' => array(
					'title'    => esc_html__( 'New Layout', 'text_domain' ),
					'controls' => array(
						... controls ...
					),
				),
			)
		 */
		$layouts = apply_filters( 'vpf_extend_layouts', array() );

		// Extend specific layout controls.
		foreach ( $layouts as $name => $layout ) {
			if ( isset( $layout['controls'] ) ) {
                // phpcs:ignore Squiz.PHP.CommentedOutCode.Found, Squiz.Commenting.BlockComment.NoEmptyLineBefore
				/*
				 * Example:
					array(
						... controls ...
					)
				 */
				$layouts[ $name ]['controls'] = apply_filters( 'vpf_extend_layout_' . $name . '_controls', $layout['controls'] );
			}
		}

		return $layouts;
	}

	/**
	 * Get all available items styles.
	 *
	 * @return array
	 */
	public static function get_all_items_styles() {
        // phpcs:ignore Squiz.PHP.CommentedOutCode.Found, Squiz.Commenting.BlockComment.NoEmptyLineBefore
		/*
		 * Example:
			array(
				'new_items_style' => array(
					'title'            => esc_html__( 'New Items Style', 'visual-portfolio' ),
					'builtin_controls' => array(
						'images_rounded_corners' => true,
						'show_title'             => true,
						'show_categories'        => true,
						'show_date'              => true,
						'show_author'            => true,
						'show_comments_count'    => true,
						'show_views_count'       => true,
						'show_reading_time'      => true,
						'show_excerpt'           => true,
						'show_icons'             => false,
						'align'                  => true,
					),
					'controls'         => array(
						... controls ...
					),
				),
			)
		 */
		$items_styles = apply_filters( 'vpf_extend_items_styles', array() );

		// Extend specific item style controls.
		foreach ( $items_styles as $name => $style ) {
			if ( isset( $style['controls'] ) ) {
                // phpcs:ignore Squiz.PHP.CommentedOutCode.Found, Squiz.Commenting.BlockComment.NoEmptyLineBefore
				/*
				 * Example:
					array(
						... controls ...
					)
				 */
				$items_styles[ $name ]['controls'] = apply_filters( 'vpf_extend_item_style_' . $name . '_controls', $style['controls'] );
			}
		}

		return $items_styles;
	}

	/**
	 * Get all available options of post.
	 *
	 * @param array $atts options for portfolio list to print.
	 * @return array
	 */
	public static function get_options( $atts = array() ) {
		$id       = isset( $atts['id'] ) ? $atts['id'] : false;
		$block_id = isset( $atts['block_id'] ) ? $atts['block_id'] : false;

		if ( ! $id && ! $block_id ) {
			return false;
		}

		$result = array();

		$registered = Visual_Portfolio_Controls::get_registered_array();

		// Get default or saved layout options.
		foreach ( $registered as $item ) {
			if ( isset( $atts[ $item['name'] ] ) ) {
				$result[ $item['name'] ] = $atts[ $item['name'] ];
			} else {
				$result[ $item['name'] ] = Visual_Portfolio_Controls::get_registered_value( $item['name'], $block_id ? false : $id );
			}
		}

		if ( ! isset( $result['id'] ) ) {
			$result['id'] = $block_id ? $block_id : $id;
		}

		// filter.
		$result = Visual_Portfolio_Security::sanitize_attributes( apply_filters( 'vpf_get_options', $result, $atts ) );

		return $result;
	}

	/**
	 * Check if portfolio showed in preview mode.
	 */
	public static function is_preview() {
		$frame = false;
		if ( isset( $_POST['vp_preview_nonce'] ) && wp_verify_nonce( sanitize_key( $_POST['vp_preview_nonce'] ), 'vp-ajax-nonce' ) ) {
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
			$frame = isset( $_POST['vp_preview_frame'] ) ? Visual_Portfolio_Security::sanitize_boolean( $_POST['vp_preview_frame'] ) : false;
			$id    = isset( $_POST['vp_preview_frame_id'] ) ? sanitize_text_field( wp_unslash( $_POST['vp_preview_frame_id'] ) ) : false;

			// Elementor preview.
			if ( ! $frame && ! $id && isset( $_REQUEST['vp_preview_type'] ) && 'elementor' === $_REQUEST['vp_preview_type'] ) {
                // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
				$frame = isset( $_REQUEST['vp_preview_frame'] ) ? Visual_Portfolio_Security::sanitize_boolean( $_REQUEST['vp_preview_frame'] ) : false;
			}
		}

		return $frame;
	}

	/**
	 * Allow taxonomies to show in Filter
	 *
	 * @param string $taxonomy taxonomy name.
	 *
	 * @return bool
	 */
	public static function allow_taxonomies_for_filter( $taxonomy ) {
		// check taxonomies from settings.
		$custom_taxonomies        = Visual_Portfolio_Settings::get_option( 'filter_taxonomies', 'vp_general' );
		$custom_taxonomies        = explode( ',', $custom_taxonomies );
		$custom_taxonomies_result = false;
		if ( $custom_taxonomies && ! empty( $custom_taxonomies ) ) {
			foreach ( $custom_taxonomies as $tax ) {
				$custom_taxonomies_result = $custom_taxonomies_result || $taxonomy === $tax;
			}
		}

		return apply_filters(
			'vpf_allow_taxonomy_for_filter',
			$custom_taxonomies_result
				|| strpos( $taxonomy, 'category' ) !== false
				|| strpos( $taxonomy, 'jetpack-portfolio-type' ) !== false
				|| 'product_cat' === $taxonomy,
			$taxonomy
		);
	}

	/**
	 * Prepare config, that will be used for output.
	 *
	 * @param array $atts options for portfolio list to print.
	 *
	 * @return array|bool
	 */
	public static function get_output_config( $atts = array() ) {
		if ( ! is_array( $atts ) ) {
			return '';
		}

		$options = self::get_options( $atts );

		if ( ! $options ) {
			return false;
		}

		do_action( 'vpf_before_get_output', $options );

		self::$used_layouts[] = $options['id'];

		// generate unique ID.
		$uid   = ++self::$id;
		$uid   = hash( 'crc32b', $uid . $options['id'] );
		$class = 'vp-portfolio vp-uid-' . $uid;

		// Add ID to class.
		$class .= ' vp-id-' . $options['id'];

		// Add custom class.
		if ( isset( $atts['class'] ) ) {
			$class .= ' ' . $atts['class'];
		}

		// Add custom CSS from VC.
		if ( function_exists( 'vc_shortcode_custom_css_class' ) && isset( $atts['vc_css'] ) ) {
			$class .= ' ' . vc_shortcode_custom_css_class( $atts['vc_css'] );
		}

		// stretch class.
		if ( $options['stretch'] ) {
			$class .= ' vp-portfolio__stretch';
		}

		// Filter to replace the main layout with custom. Particularly needed for password protection or age verification.
		$custom_output = apply_filters( 'vpf_custom_output', false, $uid, $class, $options );

		if ( $custom_output ) {
			return array(
				'custom_output' => $custom_output,
			);
		}

		$no_image = Visual_Portfolio_Settings::get_option( 'no_image', 'vp_general' );

		// prepare image sizes.
		$img_size_popup    = 'vp_xl_popup';
		$img_size_md_popup = 'vp_md_popup';
		$img_size_sm_popup = 'vp_sm_popup';
		$img_size          = 'vp_xl';
		$columns_count     = false;

		switch ( $options['layout'] ) {
			case 'masonry':
				$columns_count = (int) $options['masonry_columns'];
				break;
			case 'grid':
				$columns_count = (int) $options['grid_columns'];
				break;
			case 'tiles':
				$columns_count = explode( '|', $options['tiles_type'], 1 );
				$columns_count = (int) $columns_count[0];
				break;
		}

		switch ( $columns_count ) {
			case 1:
				$img_size = 'vp_xl';
				break;
			case 2:
				$img_size = 'vp_xl';
				break;
			case 3:
				$img_size = 'vp_xl';
				break;
			case 4:
				$img_size = 'vp_lg';
				break;
			case 5:
				$img_size = 'vp_lg';
				break;
		}

		$is_preview = self::is_preview();
		$start_page = self::get_current_page_number();
		$is_images  = 'images' === $options['content_source'];
		$is_social  = 'social-stream' === $options['content_source'];

		// Get query params.
		$query_opts = self::get_query_params( $options, false, $options['id'] );

		/**
		 * Filter to provide a custom query result object for non-standard content sources.
		 * Return a query-like object with have_posts(), the_post(), reset_postdata() methods
		 * and max_num_pages property, or false to use default WP_Query.
		 *
		 * @param bool|object $custom_query Custom query object or false.
		 * @param array       $query_opts   Query options.
		 * @param array       $options      Portfolio options.
		 */
		$custom_query = apply_filters( 'vpf_custom_query_result', false, $query_opts, $options );

		// This hack exists because wp_reset_postdata() does not work in some situations.
		$old_post = isset( $GLOBALS['post'] ) ? $GLOBALS['post'] : null;

		if ( $is_images || $is_social ) {
			if ( isset( $query_opts['max_num_pages'] ) ) {
				$max_pages = (int) ( $query_opts['max_num_pages'] < $start_page ? $start_page : $query_opts['max_num_pages'] );
			} else {
				$max_pages = $start_page;
			}
		} elseif ( $custom_query ) {
			// Use custom query object provided by extensions.
			$portfolio_query = $custom_query;
			$max_pages       = (int) ( $portfolio_query->max_num_pages < $start_page ? $start_page : $portfolio_query->max_num_pages );
		} else {
			// get Post List.
			$portfolio_query = new WP_Query( $query_opts );

			$max_pages = (int) ( $portfolio_query->max_num_pages < $start_page ? $start_page : $portfolio_query->max_num_pages );
		}

		$next_page_url = ( ! $max_pages || $max_pages >= $start_page + 1 ) ? self::get_pagenum_link(
			array(
				'vp_page' => $start_page + 1,
			)
		) : false;

		$options['start_page']    = $start_page;
		$options['max_pages']     = $max_pages;
		$options['next_page_url'] = $next_page_url;

		/**
		 * Prepare data-attributes.
		 */
		$data_attrs = array(
			'data-vp-layout'             => $options['layout'],
			'data-vp-content-source'     => $options['content_source'],
			'data-vp-items-style'        => $options['items_style'],
			'data-vp-items-click-action' => $options['items_click_action'],
			'data-vp-items-gap'          => $options['items_gap'],
			'data-vp-items-gap-vertical' => $options['items_gap_vertical'],
			'data-vp-pagination'         => $options['pagination'],
			'data-vp-next-page-url'      => $next_page_url,
		);

		if (
			(
				'post-based' === $options['content_source'] &&
				'rand' === $options['posts_order_by']
			) ||
			(
				isset( $options['images'] ) &&
				! empty( $options['images'] ) &&
				is_array( $options['images'] ) &&
				'rand' === $options['images_order_by']
			)
		) {
			$data_attrs['data-vp-random-seed'] = self::get_rand_seed_session();
		}

		if ( 'tiles' === $options['layout'] || $is_preview ) {
			$data_attrs['data-vp-tiles-type'] = $options['tiles_type'];
		}
		if ( 'masonry' === $options['layout'] || $is_preview ) {
			$data_attrs['data-vp-masonry-columns'] = $options['masonry_columns'];

			if ( $options['masonry_images_aspect_ratio'] ) {
				$data_attrs['data-vp-masonry-images-aspect-ratio'] = $options['masonry_images_aspect_ratio'];
			}

			if ( $options['masonry_horizontal_order'] ) {
				$data_attrs['data-vp-masonry-horizontal-order'] = 'true';
			}
		}
		if ( 'grid' === $options['layout'] || $is_preview ) {
			$data_attrs['data-vp-grid-columns'] = $options['grid_columns'];

			if ( $options['grid_images_aspect_ratio'] ) {
				$data_attrs['data-vp-grid-images-aspect-ratio'] = $options['grid_images_aspect_ratio'];
			}
		}
		if ( 'justified' === $options['layout'] || $is_preview ) {
			$data_attrs['data-vp-justified-row-height']           = $options['justified_row_height'];
			$data_attrs['data-vp-justified-row-height-tolerance'] = $options['justified_row_height_tolerance'];
			$data_attrs['data-vp-justified-max-rows-count']       = $options['justified_max_rows_count'];
			$data_attrs['data-vp-justified-last-row']             = $options['justified_last_row'];
		}

		if ( 'slider' === $options['layout'] || $is_preview ) {
			$data_attrs['data-vp-slider-effect'] = $options['slider_effect'];

			switch ( $options['slider_items_height_type'] ) {
				case 'auto':
					$data_attrs['data-vp-slider-items-height'] = 'auto';
					break;
				case 'static':
					$data_attrs['data-vp-slider-items-height']     = ( $options['slider_items_height_static'] ? $options['slider_items_height_static'] : '200' ) . 'px';
					$data_attrs['data-vp-slider-items-min-height'] = $options['slider_items_min_height'];
					break;
				case 'dynamic':
					$data_attrs['data-vp-slider-items-height']     = ( $options['slider_items_height_dynamic'] ? $options['slider_items_height_dynamic'] : '80' ) . '%';
					$data_attrs['data-vp-slider-items-min-height'] = $options['slider_items_min_height'];
					break;
				// no default.
			}

			switch ( $options['slider_slides_per_view_type'] ) {
				case 'auto':
					$data_attrs['data-vp-slider-slides-per-view'] = 'auto';
					break;
				case 'custom':
					$data_attrs['data-vp-slider-slides-per-view'] = $options['slider_slides_per_view_custom'] ? $options['slider_slides_per_view_custom'] : '3';
					break;
				// no default.
			}

			$data_attrs['data-vp-slider-speed']                = $options['slider_speed'];
			$data_attrs['data-vp-slider-autoplay']             = $options['slider_autoplay'];
			$data_attrs['data-vp-slider-autoplay-hover-pause'] = $options['slider_autoplay_hover_pause'] ? 'true' : 'false';
			$data_attrs['data-vp-slider-centered-slides']      = $options['slider_centered_slides'] ? 'true' : 'false';
			$data_attrs['data-vp-slider-loop']                 = $options['slider_loop'] ? 'true' : 'false';
			$data_attrs['data-vp-slider-free-mode']            = $options['slider_free_mode'] ? 'true' : 'false';
			$data_attrs['data-vp-slider-free-mode-sticky']     = $options['slider_free_mode_sticky'] ? 'true' : 'false';
			$data_attrs['data-vp-slider-arrows']               = $options['slider_arrows'] ? 'true' : 'false';
			$data_attrs['data-vp-slider-bullets']              = $options['slider_bullets'] ? 'true' : 'false';
			$data_attrs['data-vp-slider-bullets-dynamic']      = $options['slider_bullets_dynamic'] ? 'true' : 'false';
			$data_attrs['data-vp-slider-mousewheel']           = $options['slider_mousewheel'] ? 'true' : 'false';

			$data_attrs['data-vp-slider-thumbnails'] = $options['slider_thumbnails'] ? 'true' : 'false';

			if ( $options['slider_thumbnails'] ) {
				$data_attrs['data-vp-slider-thumbnails-height'] = 'auto';
				$data_attrs['data-vp-slider-thumbnails-gap']    = $options['slider_thumbnails_gap'] ? $options['slider_thumbnails_gap'] : '0';

				switch ( $options['slider_thumbnails_height_type'] ) {
					case 'auto':
						$data_attrs['data-vp-slider-thumbnails-height'] = 'auto';
						break;
					case 'static':
						$data_attrs['data-vp-slider-thumbnails-height'] = ( $options['slider_thumbnails_height_static'] ? $options['slider_thumbnails_height_static'] : '100' ) . 'px';
						break;
					case 'dynamic':
						$data_attrs['data-vp-slider-thumbnails-height'] = ( $options['slider_thumbnails_height_dynamic'] ? $options['slider_thumbnails_height_dynamic'] : '30' ) . '%';
						break;
					// no default.
				}

				switch ( $options['slider_thumbnails_per_view_type'] ) {
					case 'auto':
						$data_attrs['data-vp-slider-thumbnails-per-view'] = 'auto';
						break;
					case 'custom':
						$data_attrs['data-vp-slider-thumbnails-per-view'] = $options['slider_thumbnails_per_view_custom'] ? $options['slider_thumbnails_per_view_custom'] : '6';
						break;
					// no default.
				}
			}
		}

		// get options for the current style.
		$style_options      = array();
		$style_options_slug = 'items_style_' . $options['items_style'] . '__';
		foreach ( $options as $k => $opt ) {
			// add option to array.
			if ( substr( $k, 0, strlen( $style_options_slug ) ) === $style_options_slug ) {
				$opt_name = str_replace( $style_options_slug, '', $k );

				$style_options[ $opt_name ] = $opt;
			}

			// remove style options from the options list.
			if ( substr( $k, 0, strlen( 'items_style_' ) ) === 'items_style_' ) {
				unset( $options[ $k ] );
			}
		}

        // phpcs:ignore Squiz.PHP.CommentedOutCode.Found, Squiz.Commenting.BlockComment.NoEmptyLineBefore
		/*
		 * Example:
			array(
				'data-vp-my-attribute' => 'data',
			)
		 */
		$data_attrs = apply_filters( 'vpf_extend_portfolio_data_attributes', $data_attrs, $options, $style_options );
		$class      = apply_filters( 'vpf_extend_portfolio_class', $class, $options, $style_options );

		$items_class = 'vp-portfolio__items vp-portfolio__items-style-' . $options['items_style'];

		if ( isset( $style_options['show_overlay'] ) && $style_options['show_overlay'] ) {
			$items_class .= ' vp-portfolio__items-show-overlay-' . $style_options['show_overlay'];
		}
		if ( isset( $style_options['show_caption'] ) && $style_options['show_caption'] ) {
			$items_class .= ' vp-portfolio__items-show-caption-' . $style_options['show_caption'];
		}

		if ( isset( $style_options['show_img_overlay'] ) && $style_options['show_img_overlay'] ) {
			$items_class .= ' vp-portfolio__items-show-img-overlay-' . $style_options['show_img_overlay'];
		}

		$items_class = apply_filters( 'vpf_extend_portfolio_items_class', $items_class, $options, $style_options );

		/**
		 * Prepare each item args.
		 */
		$each_item_args = array(
			'uid'                => '',
			'post_id'            => '',
			'post_type'          => '',
			'url'                => '',
			'aria_label'         => '',
			'title'              => '',
			'excerpt'            => '',
			'content'            => '',
			'comments_count'     => '',
			'comments_url'       => '',
			'author'             => '',
			'author_url'         => '',
			'author_avatar'      => '',
			'views_count'        => '',
			'reading_time'       => '',
			'format'             => '',
			'published'          => '',
			'published_time'     => '',
			'categories'         => array(),
			'filter'             => '',
			'video'              => '',
			'image_id'           => '',
			'img_size_popup'     => $img_size_popup,
			'img_size_md_popup'  => $img_size_md_popup,
			'img_size_sm_popup'  => $img_size_sm_popup,
			'img_size'           => $img_size,
			'no_image'           => $no_image,
			'opts'               => $style_options,
			'vp_opts'            => $options,
		);

		$items = array();

		/**
		 * Filter to provide custom items array for non-standard content sources.
		 * Return an array of items or false to use default processing.
		 * Each item should follow the $each_item_args structure.
		 *
		 * @param bool|array $custom_items   Custom items array or false.
		 * @param array      $each_item_args Default item args template.
		 * @param array      $query_opts     Query options.
		 * @param array      $options        Portfolio options.
		 */
		$custom_items = apply_filters( 'vpf_custom_items', false, $each_item_args, $query_opts, $options );

		if ( is_array( $custom_items ) && ! empty( $custom_items ) ) {
			// Use custom items provided by extensions.
			$items = $custom_items;
		} elseif ( ( $is_images || $is_social ) &&
			isset( $query_opts['images'] ) &&
			is_array( $query_opts['images'] ) &&
			! empty( $query_opts['images'] ) ) {

			foreach ( $query_opts['images'] as $img ) {
				// Get category taxonomies for data filter.
				$filter_values = array();
				$categories    = array();

				if ( isset( $img['categories'] ) && is_array( $img['categories'] ) ) {
					foreach ( $img['categories'] as $cat ) {
						$slug = self::create_slug( $cat );

						if ( ! in_array( $slug, $filter_values, true ) ) {
							// add in filter.
							$filter_values[] = $slug;

							// add in categories array.
							$url = self::get_pagenum_link(
								array(
									'vp_filter' => rawurlencode( $slug ),
									'vp_page'   => 1,
								)
							);

							$categories[] = array(
								'slug'        => $slug,
								'label'       => $cat,
								'description' => '',
								'count'       => '',
								'taxonomy'    => '',
								'id'          => 0,
								'parent'      => 0,
								'url'         => $url,
							);
						}
					}
				}

				$args = array_merge(
					$each_item_args,
					array(
						'uid'            => isset( $img['uid'] ) && $img['uid'] ? $img['uid'] : '',
						'url'            => isset( $img['url'] ) && $img['url'] ? $img['url'] : Visual_Portfolio_Images::wp_get_attachment_image_url( $img['id'], $img_size_popup ),
						'title'          => isset( $img['title'] ) && $img['title'] ? $img['title'] : '',
						'alt'            => isset( $img['alt'] ) ? $img['alt'] : '',
						'content'        => isset( $img['description'] ) && $img['description'] ? $img['description'] : '',
						'format'         => isset( $img['format'] ) && $img['format'] ? $img['format'] : 'standard',
						'published_time' => isset( $img['published_time'] ) && $img['published_time'] ? $img['published_time'] : '',
						'filter'         => implode( ',', $filter_values ),
						'image_id'       => intval( $img['id'] ),
						'focal_point'    => isset( $img['focalPoint'] ) && $img['focalPoint'] ? $img['focalPoint'] : '',
						'allow_popup'    => ! isset( $img['url'] ) || ! $img['url'],
						'categories'     => $categories,
						'author'         => isset( $img['author'] ) && $img['author'] ? $img['author'] : '',
						'author_url'     => isset( $img['author'] ) && isset( $img['author_url'] ) && $img['author'] && $img['author_url'] ? $img['author_url'] : '',
					)
				);

				// Excerpt.
				if ( ! empty( $args['opts']['show_excerpt'] ) ) {
					$args['excerpt'] = self::get_item_excerpt( $args );
				}

				if ( 'video' === $args['format'] && isset( $img['video_url'] ) && $img['video_url'] ) {
					$args['video'] = $img['video_url'];
				}

				$args = apply_filters( 'vpf_image_item_args', $args, $img );

				$items[] = $args;
			}
		} elseif ( isset( $portfolio_query ) ) {
			while ( $portfolio_query->have_posts() ) {
				$portfolio_query->the_post();

				$the_post  = get_post();
				$post_type = get_post_type();

				self::$used_posts[] = get_the_ID();

				// Get category taxonomies for data filter.
				$filter_values  = array();
				$categories     = array();
				$all_taxonomies = get_object_taxonomies( $the_post );

				foreach ( $all_taxonomies as $cat ) {
					// allow only specific taxonomies for filter.
					if ( ! self::allow_taxonomies_for_filter( $cat ) ) {
						continue;
					}

					$category = get_the_terms( $the_post, $cat );

					if ( $category && ! in_array( $category, $filter_values, true ) ) {
						foreach ( $category as $cat_item ) {
							// add in filter.
							$filter_values[] = $cat_item->slug;

							// add in categories array.
							$unique_name  = rawurlencode( $cat_item->taxonomy . ':' ) . $cat_item->slug;
							$url          = self::get_pagenum_link(
								array(
									'vp_filter' => $unique_name,
									'vp_page'   => 1,
								)
							);
							$categories[] = array(
								'slug'        => $cat_item->slug,
								'label'       => $cat_item->name,
								'description' => $cat_item->description,
								'count'       => $cat_item->count,
								'taxonomy'    => $cat_item->taxonomy,
								'id'          => $cat_item->term_id,
								'parent'      => $cat_item->parent,
								'url'         => $url,
							);
						}
					}
				}

				$args = array_merge(
					$each_item_args,
					array(
						'uid'            => hash( 'crc32b', 'post-' . get_the_ID() ),
						'post_id'        => get_the_ID(),
						'post_type'      => $post_type,
						'url'            => get_permalink(),
						'title'          => get_the_title(),
						'content'        => get_the_content(),
						'format'         => get_post_format() ? get_post_format() : 'standard',
						'published_time' => get_the_date( 'Y-m-d H:i:s', $the_post ),
						'filter'         => implode( ',', $filter_values ),
						'image_id'       => 'attachment' === $post_type ? get_the_ID() : get_post_thumbnail_id( get_the_ID() ),
						'focal_point'    => Visual_Portfolio_Custom_Post_Meta::get_featured_image_focal_point( get_the_ID() ),
						'categories'     => $categories,
						'comments_count' => get_comments_number( get_the_ID() ),
						'comments_url'   => get_comments_link( get_the_ID() ),
						'views_count'    => Visual_Portfolio_Custom_Post_Meta::get_views_count( get_the_ID() ),
						'reading_time'   => Visual_Portfolio_Custom_Post_Meta::get_reading_time( get_the_ID() ),
					)
				);

				// Author.
				$author_id = get_the_author_meta( 'ID' );
				if ( $author_id ) {
					$args['author']        = get_the_author();
					$args['author_url']    = get_author_posts_url( $author_id );
					$args['author_avatar'] = get_avatar_url( $author_id, array( 'size' => 50 ) );
				}

				// Excerpt.
				if ( ! empty( $args['opts']['show_excerpt'] ) ) {
					$args['excerpt'] = self::get_item_excerpt( $args );
				}

				$args['allow_popup'] = isset( $args['image_id'] ) && $args['image_id'];

				if ( 'video' === $args['format'] ) {
					$video_url = Visual_Portfolio_Custom_Post_Meta::get_video_format_url( get_the_ID() );
					if ( $video_url ) {
						$args['video']       = $video_url;
						$args['allow_popup'] = true;
					}
				}

				$args = apply_filters( 'vpf_post_item_args', $args, $args['post_id'] );

				$items[] = $args;
			}

			$portfolio_query->reset_postdata();

			// Sometimes, when we use WPBakery Page Builder, without this reset output is wrong.
			wp_reset_postdata();

			// This hack exists because wp_reset_postdata() does not work in some situations.
            // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
			$GLOBALS['post'] = $old_post;
		}

		$notices = array();

		// No items found notice.
		if ( empty( $items ) ) {
			$class .= ' vp-portfolio-not-found';

			// Don't display any output if no items found (works on frontend only).
			if ( $options['no_items_notice'] && ( $is_preview || 'notice' === $options['no_items_action'] ) ) {
				$notices[] = $options['no_items_notice'];
			}
		}

		$errors = array();

		if ( isset( $query_opts['error'] ) && ! empty( $query_opts['error'] ) && ( $is_preview || is_admin_bar_showing() ) ) {
			$class   .= ' vp-portfolio-errors';
			$errors[] = $query_opts['error'];
		}

		$result = array(
			'options'           => $options,
			'style_options'     => $style_options,
			'class'             => $class,
			'data_attrs'        => $data_attrs,
			'items_class'       => $items_class,
			'items'             => $items,
			'notices'           => $notices,
			'errors'            => $errors,
			'img_size_popup'    => $img_size_popup,
			'img_size_md_popup' => $img_size_md_popup,
			'img_size_sm_popup' => $img_size_sm_popup,
			'img_size'          => $img_size,
		);

		return $result;
	}

	/**
	 * Print portfolio by post ID or options
	 *
	 * @param array $atts options for portfolio list to print.
	 *
	 * @return string
	 */
	public static function get( $atts = array() ) {
		$config = self::get_output_config( $atts );

		if ( ! $config ) {
			return '';
		}

		if ( isset( $config['custom_output'] ) ) {
			return $config['custom_output'];
		}

		$options       = $config['options'];
		$style_options = $config['style_options'];
		$data_attrs    = $config['data_attrs'];
		$class         = $config['class'];
		$items         = $config['items'];
		$items_class   = $config['items_class'];
		$notices       = $config['notices'];
		$errors        = $config['errors'];

		// Insert styles and scripts.
		Visual_Portfolio_Assets::enqueue( $atts );

		// No items found.
		if ( empty( $items ) ) {
			if ( empty( $notices ) ) {
				return '';
			}

			ob_start();

			?>
			<div class="<?php echo esc_attr( $class ); ?>">
				<?php
				foreach ( $notices as $notice ) {
					self::notice( $notice );
				}
				?>
			</div>
			<?php

			return ob_get_clean();
		}

		ob_start();

		/**
		 * Wrapper start.
		 */
		do_action( 'vpf_before_wrapper_start', $options, $style_options );

		visual_portfolio()->include_template(
			'items-list/wrapper-start',
			array(
				'options'       => $options,
				'style_options' => $style_options,
				'data_attrs'    => $data_attrs,
				'class'         => $class,
			)
		);

		if ( ! empty( $errors ) ) {
			?>
				<?php
				foreach ( $errors as $error ) {
					self::error( $error );
				}
				?>
			<?php
		}

		do_action( 'vpf_after_wrapper_start', $options, $style_options );

		/**
		 * Top layout elements.
		 */
		self::print_layout_elements( 'top', $options );

		/**
		 * Items wrap.
		 */
		?>
		<div class="vp-portfolio__items-wrap">
			<?php

			/**
			 * Items wrapper start.
			 */
			do_action( 'vpf_before_items_wrapper_start', $options, $style_options );

			visual_portfolio()->include_template(
				'items-list/items-wrapper-start',
				array(
					'options'       => $options,
					'style_options' => $style_options,
					'class'         => $items_class,
				)
			);

			do_action( 'vpf_after_items_wrapper_start', $options, $style_options );

			/**
			 * Each item.
			 */
			if ( is_array( $items ) && ! empty( $items ) ) {
				foreach ( $items as $item_args ) {
					self::each_item( $item_args );
				}
			}

			/**
			 * Items wrapper end.
			 */
			do_action( 'vpf_before_items_wrapper_end', $options, $style_options );

			visual_portfolio()->include_template(
				'items-list/items-wrapper-end',
				array(
					'options'       => $options,
					'style_options' => $style_options,
				)
			);

			do_action( 'vpf_after_items_wrapper_end', $options, $style_options );

			// Slider arrows and bullets.
			if ( 'slider' === $options['layout'] ) {
				if ( $options['slider_arrows'] ) {
					visual_portfolio()->include_template(
						'items-list/layouts/slider/arrows',
						array(
							'options'       => $options,
							'style_options' => $style_options,
						)
					);
				}
				if ( $options['slider_bullets'] ) {
					visual_portfolio()->include_template(
						'items-list/layouts/slider/bullets',
						array(
							'options'       => $options,
							'style_options' => $style_options,
						)
					);
				}
			}

			?>
		</div>
		<?php

		/**
		 * Slider thumbnails.
		 */
		if ( 'slider' === $options['layout'] && $options['slider_thumbnails'] ) {
			$slider_thumbnails = array();

			if ( is_array( $items ) && ! empty( $items ) ) {
				foreach ( $items as $item_args ) {
					$slider_thumbnails[] = $item_args['image_id'];
				}
			}

			visual_portfolio()->include_template(
				'items-list/layouts/slider/thumbnails',
				array(
					'options'       => $options,
					'style_options' => $style_options,
					'thumbnails'    => $slider_thumbnails,
					'img_size'      => $config['img_size'],
				)
			);
		}

		/**
		 * Bottom layout elements.
		 */
		self::print_layout_elements( 'bottom', $options );

		/**
		 * Wrapper end.
		 */
		do_action( 'vpf_before_wrapper_end', $options, $style_options );

		visual_portfolio()->include_template(
			'items-list/wrapper-end',
			array(
				'options'       => $options,
				'style_options' => $style_options,
			)
		);

		do_action( 'vpf_after_wrapper_end', $options, $style_options );

		do_action( 'vpf_after_get_output', $options, $style_options );

		return ob_get_clean();
	}

	/**
	 * Print layout elements like Filter, Sort, Search and Pagination.
	 *
	 * @param string $position elements position.
	 * @param array  $options options for portfolio list to print.
	 */
	public static function print_layout_elements( $position, $options ) {
		$options         = apply_filters( 'vpf_layout_element_options', $options );
		$layout_elements = $options['layout_elements'];

		if ( ! isset( $layout_elements[ $position ] ) || ! isset( $layout_elements[ $position ]['elements'] ) ) {
			return;
		}

		$registered   = Visual_Portfolio_Controls::get_registered_array();
		$control_data = $registered['layout_elements'];
		$class_name   = 'vp-portfolio__layout-elements';

		if ( $position ) {
			$class_name .= ' vp-portfolio__layout-elements-' . $position;
		}

		if ( isset( $layout_elements[ $position ]['align'] ) ) {
			$class_name .= ' vp-portfolio__layout-elements-align-' . $layout_elements[ $position ]['align'];
		}

		ob_start();
		foreach ( $layout_elements[ $position ]['elements'] as $element ) {
			if ( isset( $control_data['options'][ $element ]['render_callback'] ) && is_callable( $control_data['options'][ $element ]['render_callback'] ) ) {
				call_user_func( $control_data['options'][ $element ]['render_callback'], $options, $element, $position );
			}
			do_action( 'vpf_layout_elements', $options, $element, $position );
		}
		$elements_content = ob_get_clean();

		if ( ! $elements_content ) {
			return;
		}

		?>
		<div class="<?php echo esc_attr( $class_name ); ?>">
		<?php

		// In this case, pluggable pagination, filters and sorting templates are displayed.
		// Included templates are cleared and passed as a ready-made cleared string in the callback function called above.
        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		echo $elements_content;

		?>
		</div>
		<?php
	}

	/**
	 * Print portfolio filter by post ID or options
	 *
	 * @param array $atts options for portfolio list to print.
	 *
	 * @return string
	 */
	public static function get_filter( $atts = array() ) {
		$options = self::get_options( $atts );

		$options = array_merge(
			$options,
			array(
				'filter'            => $atts['type'],
				'filter_show_count' => 'true' === $atts['show_count'],
				'filter_text_all'   => $atts['text_all'] ?? esc_attr__( 'All', 'visual-portfolio' ),
			)
		);

		$align = $atts['align'] ?? 'center';

		// generate unique ID.
		$uid = ++self::$filter_id;
		$uid = hash( 'crc32b', $uid . $options['id'] );

		$class = 'vp-single-filter vp-filter-uid-' . $uid . ' vp-id-' . $options['id'];

		// Add custom class.
		if ( isset( $atts['class'] ) ) {
			$class .= ' ' . $atts['class'];
		}

		ob_start();

		?>
		<div class="vp-portfolio__layout-elements vp-portfolio__layout-elements-align-<?php echo esc_attr( $align ); ?>">
			<div class="<?php echo esc_attr( $class ); ?>">
				<?php self::filter( $options ); ?>
			</div>
		</div>
		<?php

		return ob_get_clean();
	}

	/**
	 * Print portfolio sort by post ID or options
	 *
	 * @param array $atts options for portfolio list to print.
	 *
	 * @return string
	 */
	public static function get_sort( $atts = array() ) {
		$options = self::get_options( $atts );

		$options = array_merge(
			$options,
			array(
				'sort' => $atts['type'],
			)
		);

		$align = $atts['align'] ?? 'center';

		// generate unique ID.
		$uid = ++self::$sort_id;
		$uid = hash( 'crc32b', $uid . $options['id'] );

		$class = 'vp-single-sort vp-sort-uid-' . $uid . ' vp-id-' . $options['id'];

		// Add custom class.
		if ( isset( $atts['class'] ) ) {
			$class .= ' ' . $atts['class'];
		}

		ob_start();

		?>
		<div class="vp-portfolio__layout-elements vp-portfolio__layout-elements-align-<?php echo esc_attr( $align ); ?>">
			<div class="<?php echo esc_attr( $class ); ?>">
				<?php self::sort( $options ); ?>
			</div>
		</div>
		<?php

		return ob_get_clean();
	}

	/**
	 * Get current page number
	 * ?vp_page=2
	 *
	 * @return int
	 */
	private static function get_current_page_number() {
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.NonceVerification
		return max( 1, isset( $_GET['vp_page'] ) ? Visual_Portfolio_Security::sanitize_number( $_GET['vp_page'] ) : 1 );
	}

	/**
	 * "rand" orderby don't work fine for paged, so we need to use custom solution.
	 * thanks to https://gist.github.com/hlashbrooke/6298714 .
	 */
	private static function get_rand_seed_session() {
		// already prepared.
		if ( self::$rand_seed_session ) {
			return self::$rand_seed_session;
		}

		// Reset vpf_random_seed on load of initial archive page.
		if ( self::get_current_page_number() === 1 ) {
			if ( isset( self::$rand_seed_session ) ) {
				self::$rand_seed_session = false;
			}
		}

		// Get vpf_random_seed from request variable if it exists.
        // phpcs:ignore WordPress.Security.NonceVerification
		if ( isset( $_REQUEST['vpf_random_seed'] ) && is_numeric( $_REQUEST['vpf_random_seed'] ) ) {
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.NonceVerification
			self::$rand_seed_session = Visual_Portfolio_Security::sanitize_number( $_REQUEST['vpf_random_seed'] );
		}

		// Set new vpf_random_seed if none exists.
		if ( ! self::$rand_seed_session ) {
			self::$rand_seed_session = wp_rand();
		}

		return self::$rand_seed_session;
	}

	/**
	 * Sort associative array by custom field.
	 *
	 * @param array  $data - Sortable array.
	 * @param string $field - Array field by which sorting will take place.
	 * @param string $order - Sorting order (asc and desc).
	 * @return array
	 */
	public static function sort_array_by_field( $data, $field, $order = 'desc' ) {
		if ( ! is_array( $data ) ) {
			return array();
		}
		$array_with_empty_fields     = array();
		$array_with_not_empty_fields = array();

		/**
		 * We add a service key to the properties of array elements to sort fields of equal value by keys.
		 * This is only necessary when sorting in descending order.
		 */
		if ( 'desc' === $order ) {
			foreach ( $data as $key => &$element ) {
				$element['__VP_SORT_ID__'] = $key;
			}
		}

		/**
		 * In some cases,
		 * The function may sort elements with the same values ​​differently in php 7 and php 8.
		 * This is due to the following change in how the usort function works.
		 * If two members compare as equal, they retain their original order.
		 * Prior to PHP 8.0.0, their relative order in the sorted array was undefined.
		 */
		usort(
			$data,
			function ( $a, $b ) use ( $field, $order ) {
				// Primary comparison by field values.
				if ( isset( $a[ $field ] ) && isset( $b[ $field ] ) ) {
					$comparsion = 'asc' === $order ? strcmp( $a[ $field ], $b[ $field ] ) : strcmp( $b[ $field ], $a[ $field ] );

					if ( 0 !== $comparsion ) {
						return $comparsion;
					}
				}

				// Secondary comparison by keys when values are equal.
				if ( 'desc' === $order ) {
					return strcmp( $b['__VP_SORT_ID__'], $a['__VP_SORT_ID__'] );
				}

				return 0;
			}
		);

		// Clearing the array of service keys.
		if ( 'desc' === $order ) {
			foreach ( $data as $key => &$element ) {
				unset( $element['__VP_SORT_ID__'] );
			}
		}

		foreach ( $data as $item ) {
			if ( empty( $item[ $field ] ) ) {
				$array_with_empty_fields[] = $item;
			} else {
				$array_with_not_empty_fields[] = $item;
			}
		}

		$data = array_merge( $array_with_not_empty_fields, $array_with_empty_fields );

		return $data;
	}

	/**
	 * Get query params array.
	 *
	 * @param array $options portfolio options.
	 * @param bool  $for_filter prevent retrieving GET variable if used for filter.
	 * @param int   $layout_id portfolio layout id.
	 *
	 * @return array
	 */
	public static function get_query_params( $options, $for_filter = false, $layout_id = false ) {
		$options    = apply_filters( 'vpf_extend_options_before_query_args', $options, $layout_id );
		$query_opts = array();
		$is_images  = 'images' === $options['content_source'];

		$paged = 0;
		if ( isset( $options['pagination'] ) && $options['pagination'] ) {
			$paged = self::get_current_page_number();
		}
		$count = isset( $options['items_count'] ) ? intval( $options['items_count'] ) : 6;

		if ( $is_images ) {
			$query_opts['images'] = array();

			if ( ! isset( $options['images'] ) || ! is_array( $options['images'] ) ) {
				$options['images'] = array();
			}

			// add unique IDs.
			foreach ( $options['images'] as $k => $img ) {
				$options['images'][ $k ]['uid'] = hash( 'crc32b', 'image-' . $k . $img['id'] );
			}

			if ( $count < 0 ) {
				$count = 99999;
			}

			// Load certain taxonomies.
			$images = array();

            // phpcs:ignore WordPress.Security.NonceVerification
			if ( ! $for_filter && ( isset( $_GET['vp_filter'] ) || isset( $query_opts['vp_filter'] ) ) ) {
                // phpcs:ignore WordPress.Security.NonceVerification
				$category = sanitize_text_field( wp_unslash( $_GET['vp_filter'] ?? $query_opts['vp_filter'] ) );

				foreach ( $options['images'] as $img ) {
					if ( isset( $img['categories'] ) && is_array( $img['categories'] ) ) {
						foreach ( $img['categories'] as $cat ) {
							if ( self::create_slug( $cat ) === $category ) {
								$images[] = $img;
								break;
							}
						}
					}
				}
			} else {
				$images = $options['images'];
			}

			$images_ids = array();
			foreach ( $images as $k => $img ) {
				$images_ids[] = (int) $img['id'];
			}

			// Find all used attachments.
			$all_attachments = get_posts(
				array(
					'post_type'              => 'attachment',
					'posts_per_page'         => -1,
					'paged'                  => -1,
					'post__in'               => $images_ids,
					'update_post_meta_cache' => false,
					'update_post_term_cache' => false,
				)
			);

			// prepare titles and descriptions.
			foreach ( $images as $k => $img ) {
				$has_custom_alt     = array_key_exists( 'alt', $img );
				$item_alt           = $has_custom_alt ? (string) $img['alt'] : '';
				$title_source       = $options['images_titles_source'] ?? 'custom';
				$description_source = $options['images_descriptions_source'] ?? 'custom';
				$img_meta           = array(
					'title'             => '',
					'image_title'       => '',
					'image_description' => '',
					'image_caption'     => '',
					'image_alt'         => '',
					'description'       => '',
					'caption'           => '',
					'alt'               => '',
					'none'              => '',
					'date'              => '',
				);

				// Find current attachment post data.
				$attachment = false;
				foreach ( $all_attachments as $post ) {
					if ( $post->ID === (int) $img['id'] ) {
						$attachment = $post;
						break;
					}
				}

				if ( $attachment ) {
					// get image meta if needed.
					if (
						'none' !== $options['images_titles_source'] ||
						'none' !== $options['images_descriptions_source'] ||

						'image_title' === $options['images_order_by'] ||
						'image_caption' === $options['images_order_by'] ||
						'image_alt' === $options['images_order_by'] ||
						'image_description' === $options['images_order_by']
					) {
						if ( $attachment && 'attachment' === $attachment->post_type ) {
							$img_meta['title']       = $attachment->post_title;
							$img_meta['description'] = $attachment->post_content;
							$img_meta['caption']     = wp_get_attachment_caption( $attachment->ID );
							$img_meta['alt']         = get_post_meta( $attachment->ID, '_wp_attachment_image_alt', true );
						}
					}

					if ( $has_custom_alt && '' !== trim( $item_alt ) ) {
						$img_meta['alt'] = $item_alt;
					}

					// title.
					if ( 'custom' !== $options['images_titles_source'] ) {
						$images[ $k ]['title'] = $img_meta[ $title_source ] ?? '';
					}

					// image title.
					$images[ $k ]['image_title'] = $img_meta['title'] ?? '';

					// description.
					if ( 'custom' !== $options['images_descriptions_source'] ) {
						$images[ $k ]['description'] = $img_meta[ $description_source ] ?? '';
					}

					// image description.
					$images[ $k ]['image_description'] = $img_meta['description'] ?? '';

					// image caption.
					$images[ $k ]['image_caption'] = $img_meta['caption'] ?? '';

					// image alt.
					$images[ $k ]['image_alt'] = $img_meta['alt'] ?? '';

					// add published date.
					$images[ $k ]['published_time'] = get_the_date( 'Y-m-d H:i:s', $attachment );
				}
			}

			// order.
			$custom_order           = false;
			$custom_order_direction = $options['images_order_direction'];

			if ( isset( $options['images_order_by'] ) ) {
				$custom_order = $options['images_order_by'];
			}

			// custom sorting.
            // phpcs:ignore WordPress.Security.NonceVerification
			if ( isset( $_GET['vp_sort'] ) ) {
                // phpcs:ignore WordPress.Security.NonceVerification
				$custom_get_order = sanitize_text_field( wp_unslash( $_GET['vp_sort'] ) );

				switch ( $custom_get_order ) {
					case 'title':
					case 'date':
						$custom_order           = $custom_get_order;
						$custom_order_direction = 'asc';
						break;
					case 'title_desc':
						$custom_order           = 'title';
						$custom_order_direction = 'desc';
						break;
					case 'date_desc':
						$custom_order           = 'date';
						$custom_order_direction = 'desc';
						break;
				}
			}

			if ( $custom_order && ! empty( $images ) ) {
				switch ( $custom_order ) {
					case 'date':
					case 'title':
					case 'description':
					case 'image_title':
					case 'image_caption':
					case 'image_alt':
					case 'image_description':
						if ( 'date' === $custom_order ) {
							$custom_order = 'published_time';
						}

						/**
						 * We've reworked this code to work correctly with empty sortable values.
						 * Now images with filled values ​​will be sorted first.
						 * And empty images will be inserted later at the very end of the array.
						 */
						$images = self::sort_array_by_field( $images, $custom_order, $custom_order_direction );
						break;
					case 'rand':
						// We don't need to randomize order for filter,
						// because filter list will be always changed once AJAX loaded.
						if ( ! $for_filter ) {
                            // phpcs:ignore WordPress.WP.AlternativeFunctions.rand_seeding_mt_srand
							mt_srand( self::get_rand_seed_session() );
							for ( $i = count( $images ) - 1; $i > 0; $i-- ) {
                                // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.rand_mt_rand
								$j            = @mt_rand( 0, $i );
								$tmp          = $images[ $i ];
								$images[ $i ] = $images[ $j ];
								$images[ $j ] = $tmp;
							}
						}

						break;
				}

				if (
					'desc' === $custom_order_direction &&
					(
						'rand' === $custom_order ||
						'default' === $custom_order
					)
				) {
					$images = array_reverse( $images );
				}
			}

			// pages count.
			if ( ! empty( $images ) ) {
				$query_opts['max_num_pages'] = ceil( count( $images ) / $count );
			} else {
				$query_opts['max_num_pages'] = 0;
			}

			$start_from_item = ( $paged - 1 ) * $count;
			$end_on_item     = $start_from_item + $count;

			if ( $for_filter ) {
				$start_from_item = 0;
				$end_on_item     = 99999;
			}

			// get images for current page only.
			foreach ( (array) $images as $k => $img ) {
				$i = $k + 1;
				if ( $i > $start_from_item && $i <= $end_on_item ) {
					$query_opts['images'][] = $img;
				}
			}
		} else {
			$query_opts = array(
				'posts_per_page' => $count,
				'paged'          => $paged,
				'orderby'        => 'post_date',
				'order'          => 'DESC',
				'post_type'      => 'portfolio',
			);

			// Get all available categories for filter.
			if ( $for_filter ) {
				$query_opts['posts_per_page'] = -1;
				$query_opts['paged']          = -1;
			}

			// Post based.
			if ( 'post-based' === $options['content_source'] ) {
				// Exclude IDs.
				if ( ! empty( $options['posts_excluded_ids'] ) ) {
					$query_opts['post__not_in'] = $options['posts_excluded_ids'];
				}

				// Order By.
				switch ( $options['posts_order_by'] ) {
					case 'title':
						$query_opts['orderby'] = 'title';
						break;

					case 'id':
						$query_opts['orderby'] = 'ID';
						break;

					case 'post__in':
						$query_opts['orderby'] = 'post__in';
						break;

					case 'menu_order':
						// We should order by `menu_order` and as fallback order by `post_date`.
						$query_opts['orderby'] = array(
							'menu_order' => $options['posts_order_direction'],
							'post_date'  => 'desc',
						);
						break;

					case 'comment_count':
						$query_opts['orderby'] = 'comment_count';
						break;

					case 'modified':
						$query_opts['orderby'] = 'modified';
						break;

					case 'rand':
						// Update ORDER BY clause to use vpf_random_seed.
						$query_opts['orderby'] = 'RAND(' . self::get_rand_seed_session() . ')';
						break;

					default:
						$query_opts['orderby'] = 'post_date';
						break;
				}

				// Order.
				$query_opts['order'] = $options['posts_order_direction'];

				if ( 'ids' === $options['posts_source'] ) { // IDs.
					$query_opts['post_type']    = 'any';
					$query_opts['post__not_in'] = array();

					if ( ! empty( $options['posts_ids'] ) ) {
						$query_opts['post__in'] = $options['posts_ids'];
					}

					// Ignore sticky posts by default for manual selection.
					$query_opts['ignore_sticky_posts'] = true;
				} elseif ( 'custom_query' === $options['posts_source'] ) { // Custom Query.
					$query_opts['post_type'] = 'any';

					$tmp_arr = array();
					parse_str( html_entity_decode( $options['posts_custom_query'] ), $tmp_arr );
					$query_opts = array_merge( $query_opts, $tmp_arr );
				} elseif ( 'current_query' === $options['posts_source'] ) {
					global $wp_query;

					if ( $wp_query && isset( $wp_query->query_vars ) && is_array( $wp_query->query_vars ) ) {
						$query_vars = $wp_query->query_vars;

						// Unset `offset` because if is set, $wp_query overrides/ignores the paged parameter and breaks pagination.
						if ( isset( $query_vars['offset'] ) ) {
							unset( $query_vars['offset'] );
						}

						// Add post type.
						if ( empty( $query_vars['post_type'] ) && is_singular() ) {
							$query_vars['post_type'] = get_post_type( get_the_ID() );
						}

						// Add pagination paged value.
						if ( $query_opts['paged'] && ( ! isset( $query_vars['paged'] ) || ! $query_vars['paged'] ) ) {
							$query_vars['paged'] = $query_opts['paged'];
						}

						$query_opts = $query_vars;
					}
				} else {
					$query_opts['post_type'] = $options['posts_source'];

					// Post Types Set.
					if ( 'post_types_set' === $options['posts_source'] ) {
						$query_opts['post_type'] = (array) $options['post_types_set'];
					}

					// Taxonomies.
					if ( ! empty( $options['posts_taxonomies'] ) && ! isset( $query_opts['tax_query'] ) ) {
						$terms_list = get_terms(
							get_object_taxonomies( is_array( $query_opts['post_type'] ) ? $query_opts['post_type'] : array( $query_opts['post_type'] ) ),
							array(
								'hide_empty' => false,
							)
						);

                        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
						$query_opts['tax_query'] = array(
							// We save strings like 'or', 'and'
							// but to use it we need these strings in uppercase.
							'relation' => strtoupper( $options['posts_taxonomies_relation'] ),
						);

						// We need this empty array, because when taxonomy selected,
						// and posts don't have this taxonomy, we will see all available posts.
						// Related topic: https://wordpress.org/support/topic/exclude-certain-category-from-filter/.
						if ( 'OR' === $query_opts['tax_query']['relation'] ) {
							$query_opts['tax_query'][] = array();
						}

						foreach ( $options['posts_taxonomies'] as $taxonomy ) {
							$taxonomy_name = null;

							foreach ( $terms_list as $term ) {
								if ( $term->term_id === (int) $taxonomy ) {
									$taxonomy_name = $term->taxonomy;
									continue;
								}
							}

							if ( $taxonomy_name ) {
								$query_opts['tax_query'][] = array(
									'taxonomy' => $taxonomy_name,
									'field'    => 'id',
									'terms'    => $taxonomy,
								);
							}
						}
					}

					// Offset.
					if ( isset( $options['posts_offset'] ) && $options['posts_offset'] ) {
						$query_opts['offset'] = $options['posts_offset'] + ( $paged - 1 ) * $count;
					}
				}

				// Avoid duplicates.
				// We should prevent this when using filter, since all current posts will be excluded
				// from the filter query and we may not see all filter buttons.
				if ( ! $for_filter && $options['posts_avoid_duplicate_posts'] ) {
					$not_id                     = (array) ( isset( $query_opts['post__not_in'] ) ? $query_opts['post__not_in'] : array() );
					$query_opts['post__not_in'] = array_merge( $not_id, self::get_all_used_posts() );

					// Remove posts from post__in.
					if ( isset( $query_opts['post__in'] ) ) {
						$query_opts['post__in'] = array_diff( (array) $query_opts['post__in'], (array) $query_opts['post__not_in'] );
					}
				}
			}

			// Custom sorting.
            // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			if ( isset( $_GET['vp_sort'] ) ) {
                // phpcs:ignore WordPress.Security.NonceVerification.Recommended
				$custom_get_order       = sanitize_text_field( wp_unslash( $_GET['vp_sort'] ) );
				$custom_order           = false;
				$custom_order_direction = false;

				switch ( $custom_get_order ) {
					case 'title':
					case 'date':
						$custom_order           = 'post_' . $custom_get_order;
						$custom_order_direction = 'asc';
						break;
					case 'title_desc':
						$custom_order           = 'post_title';
						$custom_order_direction = 'desc';
						break;
					case 'date_desc':
						$custom_order           = 'post_date';
						$custom_order_direction = 'desc';
						break;
				}

				if ( $custom_order && $custom_order_direction ) {
					$query_opts['orderby'] = $custom_order;
					$query_opts['order']   = $custom_order_direction;
				}
			}

			// Load certain taxonomies using custom filter.
            // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			if ( ! $for_filter && ( isset( $_GET['vp_filter'] ) || isset( $query_opts['vp_filter'] ) ) ) {
                // phpcs:ignore WordPress.Security.NonceVerification.Recommended
				$taxonomies = sanitize_text_field( wp_unslash( $_GET['vp_filter'] ?? $query_opts['vp_filter'] ) );
				$taxonomies = explode( ':', $taxonomies );

				if ( $taxonomies && isset( $taxonomies[0] ) && isset( $taxonomies[1] ) ) {
                    // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
					$query_opts['tax_query'] = array(
						'relation' => 'AND',
						array(
							'taxonomy' => $taxonomies[0],
							'field'    => 'slug',
							'terms'    => $taxonomies[1],
						),
						isset( $query_opts['tax_query'] ) ? $query_opts['tax_query'] : '',
					);
				}
			}
		}

		$query_opts = apply_filters( 'vpf_extend_query_args', $query_opts, $options, $layout_id );

		return $query_opts;
	}

	/**
	 * Print notice
	 *
	 * @param string $notice notice string.
	 */
	public static function notice( $notice ) {
		if ( ! $notice ) {
			return;
		}
		visual_portfolio()->include_template(
			'notices/notices',
			array(
				'notice' => $notice,
			)
		);
	}

	/**
	 * Print error
	 *
	 * @param string $error error string.
	 */
	public static function error( $error ) {
		if ( ! $error ) {
			return;
		}
		visual_portfolio()->include_template(
			'errors/errors',
			array(
				'error' => $error,
			)
		);
	}

	/**
	 * Print filters
	 *
	 * @param array $vp_options current vp_list options.
	 */
	public static function filter( $vp_options ) {
		if ( ! $vp_options['filter'] ) {
			return;
		}

		$is_images  = 'images' === $vp_options['content_source'];
		$is_social  = 'social-stream' === $vp_options['content_source'];
		$query_opts = self::get_query_params( $vp_options, true );

		// Get active item.
		$active_item = self::get_filter_active_item( $query_opts );

		/**
		 * Filter to provide custom filter terms for non-standard content sources.
		 * Return array with 'terms' and 'there_is_active' keys, or false for default.
		 *
		 * @param bool|array $custom_terms Custom terms data or false.
		 * @param array      $query_opts   Query options.
		 * @param mixed      $active_item  Currently active filter item.
		 * @param array      $vp_options   Portfolio options.
		 */
		$custom_filter_terms = apply_filters( 'vpf_custom_filter_terms', false, $query_opts, $active_item, $vp_options );

		if ( is_array( $custom_filter_terms ) ) {
			$term_items = $custom_filter_terms;
		} elseif ( $is_images || $is_social ) {
			$term_items = self::get_images_terms( $query_opts, $active_item );
		} else {
			$portfolio_query = new WP_Query( $query_opts );
			$term_items      = self::get_posts_terms( $portfolio_query, $active_item );
		}

		// Add 'All' active item.
		if ( ! empty( $term_items['terms'] ) && $vp_options['filter_text_all'] ) {
			array_unshift(
				$term_items['terms'],
				array(
					'filter'      => '*',
					'label'       => $vp_options['filter_text_all'],
					'description' => false,
					'count'       => false,
					'id'          => 0,
					'parent'      => 0,
					'active'      => ! $term_items['there_is_active'],
					'url'         => self::get_pagenum_link(
						array(
							'vp_filter' => '',
							'vp_page'   => 1,
						)
					),
					'class'       => 'vp-filter__item' . ( ! $term_items['there_is_active'] ? ' vp-filter__item-active' : '' ),
				)
			);
		}

		// No filters available.
		if ( empty( $term_items['terms'] ) ) {
			return;
		}

		// get options for the current filter.
		$filter_options      = array();
		$filter_options_slug = 'filter_' . $vp_options['filter'] . '__';

		foreach ( $vp_options as $k => $opt ) {
			// add option to array.
			if ( substr( $k, 0, strlen( $filter_options_slug ) ) === $filter_options_slug ) {
				$opt_name                    = str_replace( $filter_options_slug, '', $k );
				$filter_options[ $opt_name ] = $opt;
			}

			// remove style options from the options list.
			if ( substr( $k, 0, strlen( $filter_options_slug ) ) === $filter_options_slug ) {
				unset( $vp_options[ $k ] );
			}
		}

		$args = array(
			'class'      => 'vp-filter',
            // phpcs:ignore Squiz.PHP.CommentedOutCode.Found, Squiz.Commenting.BlockComment.NoEmptyLineBefore
			/*
			 * Example:
				array(
					array(
						'filter'      => '*',
						'label'       => $options['filter_text_all'],
						'description' => false,
						'count'       => false,
						'active'      => true,
						'url'         => Visual_Portfolio_Get::get_pagenum_link(
							array(
								'vp_filter' => '',
								'vp_page' => 1,
							)
						),
						'class'       => 'vp-filter__item',
					),
				)
			 */
			'items'      => apply_filters( 'vpf_extend_filter_items', $term_items['terms'], $vp_options ),
			'show_count' => $vp_options['filter_show_count'],
			'opts'       => $filter_options,
			'vp_opts'    => $vp_options,
		);

		?>
		<div class="vp-portfolio__filter-wrap">
		<?php

		$filter_style_pref = '';

		if ( 'default' !== $vp_options['filter'] ) {
			$filter_style_pref = '/' . $vp_options['filter'];
		}

		visual_portfolio()->include_template( 'items-list/filter' . $filter_style_pref . '/filter', $args );

		// We need to include these styles, since users can insert filters using separate shortcode.
		Visual_Portfolio_Assets::store_used_assets(
			'visual-portfolio-filter-' . $vp_options['filter'],
			'items-list/filter' . $filter_style_pref . '/style',
			'template_style'
		);

		?>
		</div>
		<?php
	}

	/**
	 * Get post terms.
	 *
	 * @param WP_Query $portfolio_query - The WordPress Portfolio Query class.
	 * @param boolean  $active_item - Actime filter item.
	 * @return array
	 */
	public static function get_posts_terms( $portfolio_query, $active_item ) {
		/**
		 * TODO: make caching using set_transient function. Info here - https://wordpress.stackexchange.com/a/145960
		 */
		$term_ids        = array();
		$term_taxonomies = array();
		$terms           = array();
		$there_is_active = false;
		$term_counts     = array(); // Track actual counts from query results.

		// This hack exists because wp_reset_postdata() does not work in some situations.
		$old_post = isset( $GLOBALS['post'] ) ? $GLOBALS['post'] : null;

		while ( $portfolio_query->have_posts() ) {
			$portfolio_query->the_post();
			$current_post_id = get_the_ID();
			$all_taxonomies  = get_object_taxonomies( get_post() );

			foreach ( $all_taxonomies as $cat ) {
				// allow only specific taxonomies for filter.
				if ( ! self::allow_taxonomies_for_filter( $cat ) ) {
					continue;
				}

				// Retrieve terms.
				$category = get_the_terms( get_post(), $cat );
				if ( ! $category ) {
					continue;
				}

				// Prepare each terms array.
				foreach ( $category as $cat_item ) {
					if ( ! in_array( $cat_item->term_id, $term_ids, true ) ) {
						$term_ids[] = $cat_item->term_id;
					}
					if ( ! in_array( $cat_item->taxonomy, $term_taxonomies, true ) ) {
						$term_taxonomies[] = $cat_item->taxonomy;
					}
					// Count posts for this term based on actual query results.
					// Use post ID to avoid double-counting the same post for the same term.
					if ( ! isset( $term_counts[ $cat_item->term_id ] ) ) {
						$term_counts[ $cat_item->term_id ] = array();
					}
					if ( ! in_array( $current_post_id, $term_counts[ $cat_item->term_id ], true ) ) {
						$term_counts[ $cat_item->term_id ][] = $current_post_id;
					}
				}
			}
		}

		$portfolio_query->reset_postdata();

		// Sometimes, when we use WPBakery Page Builder, without this reset output is wrong.
		wp_reset_postdata();

		// stupid hack as wp_reset_postdata() function is not working in some situations...
        // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
		$GLOBALS['post'] = $old_post;

		// Get all available terms and then pick only needed by ID
		// we need this to support reordering plugins.
		$all_terms = get_terms(
			array(
				'taxonomy'   => $term_taxonomies,
				'hide_empty' => true,
			)
		);

		if ( isset( $all_terms ) && is_array( $all_terms ) ) {
			foreach ( $all_terms as $term ) {
				if ( in_array( $term->term_id, $term_ids, true ) ) {
					$unique_name = rawurlencode( $term->taxonomy . ':' ) . $term->slug;

					$url = self::get_pagenum_link(
						array(
							'vp_filter' => $unique_name,
							'vp_page'   => 1,
						)
					);

					$is_active = rawurldecode( $unique_name ) === $active_item;

					$terms[ $unique_name ] = array(
						'filter'      => $term->slug,
						'label'       => $term->name,
						'description' => $term->description,
						'count'       => isset( $term_counts[ $term->term_id ] ) ? count( $term_counts[ $term->term_id ] ) : 0,
						'taxonomy'    => $term->taxonomy,
						'id'          => $term->term_id,
						'parent'      => $term->parent,
						'active'      => $is_active,
						'url'         => $url,
						'class'       => 'vp-filter__item' . ( $is_active ? ' vp-filter__item-active' : '' ),
					);

					if ( $is_active ) {
						$there_is_active = true;
					}
				}
			}
		}

		return array(
			'terms'           => $terms,
			'there_is_active' => $there_is_active,
		);
	}

	/**
	 * Get images terms.
	 *
	 * @param array   $query_opts - Query array params.
	 * @param boolean $active_item - Active filter item.
	 * @return array
	 */
	public static function get_images_terms( $query_opts, $active_item ) {
		$terms           = array();
		$there_is_active = false;
		// calculate categories count.
		$categories_count = array();
		foreach ( $query_opts['images'] as $img ) {
			if ( isset( $img['categories'] ) && is_array( $img['categories'] ) ) {
				foreach ( $img['categories'] as $cat ) {
					$categories_count[ $cat ] = ( isset( $categories_count[ $cat ] ) ? $categories_count[ $cat ] : 0 ) + 1;
				}
			}
		}

		foreach ( $query_opts['images'] as $img ) {
			if ( isset( $img['categories'] ) && is_array( $img['categories'] ) ) {
				foreach ( $img['categories'] as $cat ) {
					$slug = self::create_slug( $cat );
					$url  = self::get_pagenum_link(
						array(
							'vp_filter' => rawurlencode( $slug ),
							'vp_page'   => 1,
						)
					);

					// add in terms array.
					$terms[ $slug ] = array(
						'filter'      => $slug,
						'label'       => $cat,
						'description' => '',
						'count'       => isset( $categories_count[ $cat ] ) && $categories_count[ $cat ] ? $categories_count[ $cat ] : '',
						'taxonomy'    => 'category',
						'id'          => 0,
						'parent'      => 0,
						'active'      => $active_item === $slug,
						'url'         => $url,
						'class'       => 'vp-filter__item' . ( $active_item === $slug ? ' vp-filter__item-active' : '' ),
					);

					if ( $active_item === $slug ) {
						$there_is_active = true;
					}
				}
			}
		}

		return array(
			'terms'           => $terms,
			'there_is_active' => $there_is_active,
		);
	}

	/**
	 * Get filter active item.
	 *
	 * @param array $query_opts - Query array params.
	 * @return boolean
	 */
	public static function get_filter_active_item( $query_opts ) {
		// Get active item.
		$active_item = false;

        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( ( isset( $_GET['vp_filter'] ) || isset( $query_opts['vp_filter'] ) ) ) {
            // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$active_item = sanitize_text_field( wp_unslash( $_GET['vp_filter'] ?? $query_opts['vp_filter'] ) );
		}

		return $active_item;
	}

	/**
	 * Print sort
	 *
	 * @param array $vp_options current vp_list options.
	 */
	public static function sort( $vp_options ) {
		if ( ! $vp_options['sort'] ) {
			return;
		}

		$terms = array();

		// Get active item.
		$active_item = false;

        // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( isset( $_GET['vp_sort'] ) ) {
            // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			$active_item = sanitize_text_field( wp_unslash( $_GET['vp_sort'] ) );
		}

		$sort_items = apply_filters(
			'vpf_extend_sort_items',
			array(
				''           => esc_html__( 'Default sorting', 'visual-portfolio' ),
				'date_desc'  => esc_html__( 'Sort by date (newest)', 'visual-portfolio' ),
				'date'       => esc_html__( 'Sort by date (oldest)', 'visual-portfolio' ),
				'title'      => esc_html__( 'Sort by title (A-Z)', 'visual-portfolio' ),
				'title_desc' => esc_html__( 'Sort by title (Z-A)', 'visual-portfolio' ),
			),
			$vp_options
		);

		foreach ( $sort_items as $slug => $label ) {
			$url = apply_filters(
				'vpf_extend_sort_item_url',
				self::get_pagenum_link(
					array(
						'vp_sort' => rawurlencode( $slug ),
						'vp_page' => 1,
					)
				),
				$slug,
				$vp_options
			);

			$is_active = ! $active_item && ! $slug ? true : $active_item === $slug;

			// add in terms array.
			$terms[ $slug ] = array(
				'sort'        => $slug,
				'label'       => $label,
				'description' => '',
				'active'      => $is_active,
				'url'         => $url,
				'class'       => 'vp-sort__item' . ( $is_active ? ' vp-sort__item-active' : '' ),
			);
		}

		// get options for the current sort.
		$sort_options      = array();
		$sort_options_slug = 'sort_' . $vp_options['sort'] . '__';

		foreach ( $vp_options as $k => $opt ) {
			// add option to array.
			if ( substr( $k, 0, strlen( $sort_options_slug ) ) === $sort_options_slug ) {
				$opt_name                  = str_replace( $sort_options_slug, '', $k );
				$sort_options[ $opt_name ] = $opt;
			}

			// remove style options from the options list.
			if ( substr( $k, 0, strlen( $sort_options_slug ) ) === $sort_options_slug ) {
				unset( $vp_options[ $k ] );
			}
		}

		$args = array(
			'class'   => 'vp-sort',
			'items'   => $terms,
			'opts'    => $sort_options,
			'vp_opts' => $vp_options,
		);

		?>
		<div class="vp-portfolio__sort-wrap">
		<?php

		$sort_style_pref = '';

		if ( 'default' !== $vp_options['sort'] ) {
			$sort_style_pref = '/' . $vp_options['sort'];
		}

		visual_portfolio()->include_template( 'items-list/sort' . $sort_style_pref . '/sort', $args );

		// We need to include these styles, since users can insert sort using separate shortcode.
		Visual_Portfolio_Assets::store_used_assets(
			'visual-portfolio-sort-' . $vp_options['sort'],
			'items-list/sort' . $sort_style_pref . '/style',
			'template_style'
		);

		?>
		</div>
		<?php
	}

	/**
	 * Get item kind label for aria-label generation.
	 *
	 * @param array $args - item args.
	 *
	 * @return string
	 */
	private static function get_item_kind_label( $args ) {
		$format        = $args['format'] ?? '';
		$format_labels = array(
			'video'   => esc_html__( 'video', 'visual-portfolio' ),
			'audio'   => esc_html__( 'audio', 'visual-portfolio' ),
			'gallery' => esc_html__( 'gallery', 'visual-portfolio' ),
		);

		if ( isset( $format_labels[ $format ] ) ) {
			return $format_labels[ $format ];
		}

		$post_type = $args['post_type'] ?? '';

		if ( '' === $post_type && ! empty( $args['post_id'] ) ) {
			$post_type = get_post_type( intval( $args['post_id'] ) );
		}

		if ( $post_type ) {
			$post_type_labels = array(
				'post'    => esc_html__( 'Post', 'visual-portfolio' ),
				'page'    => esc_html__( 'Page', 'visual-portfolio' ),
				'product' => esc_html__( 'Product', 'visual-portfolio' ),
				'shop'    => esc_html__( 'Shop', 'visual-portfolio' ),
				'project' => esc_html__( 'Project', 'visual-portfolio' ),
				'work'    => esc_html__( 'Work', 'visual-portfolio' ),
			);

			if ( isset( $post_type_labels[ $post_type ] ) ) {
				return $post_type_labels[ $post_type ];
			}

			$post_type_fallback = ucwords( str_replace( array( '-', '_' ), ' ', $post_type ) );
			$post_type_fallback = wp_strip_all_tags( $post_type_fallback );

			if ( '' !== trim( $post_type_fallback ) ) {
				return $post_type_fallback;
			}
		}

		$content_source        = $args['vp_opts']['content_source'] ?? '';
		$content_source_labels = array(
			'images'        => esc_html__( 'image', 'visual-portfolio' ),
			'social-stream' => esc_html__( 'social post', 'visual-portfolio' ),
		);

		if ( isset( $content_source_labels[ $content_source ] ) ) {
			return $content_source_labels[ $content_source ];
		}

		return esc_html__( 'item', 'visual-portfolio' );
	}

	/**
	 * Get item aria-label.
	 *
	 * @param array $args - item args.
	 *
	 * @return string
	 */
	private static function get_item_aria_label( $args ) {
		$aria_label = wp_strip_all_tags( $args['aria_label'] ?? '' );

		if ( '' === trim( $aria_label ) ) {
			$aria_label = wp_strip_all_tags( $args['title'] ?? '' );
		}

		if ( '' !== trim( $aria_label ) ) {
			return $aria_label;
		}

		// translators: %s - item type label such as image, post type name, video, etc.
		$aria_label = sprintf( esc_html__( 'Open %s', 'visual-portfolio' ), self::get_item_kind_label( $args ) );

		/**
		 * Filters the generated aria-label for each item link.
		 *
		 * @param string $aria_label generated aria-label.
		 * @param array  $args       item args.
		 */
		return apply_filters( 'vpf_item_aria_label', $aria_label, $args );
	}

	/**
	 * Print each item
	 *
	 * @param array $args current item data.
	 *      'url' - post/image url.
	 *      'title' - post/image title.
	 *      'format' - post/image format.
	 *      'published' - post/image published.
	 *      'published_time' - post/image published time.
	 *      'categories' - categories array.
	 *      'filter' - filters string.
	 *      'video' - video url.
	 *      'image_id' - image id.
	 *      'img_size_popup' - image size for popup.
	 *      'img_size_md_popup' - md image size for popup.
	 *      'img_size_sm_popup' - sm image size for popup.
	 *      'img_size' - image size.
	 *      'no_image' - no image id.
	 *      'opts' - style options.
	 *      'vp_opts' - vp options.
	 */
	private static function each_item( $args ) {
		global $post;

		$is_posts = 'post-based' === $args['vp_opts']['content_source'] || 'portfolio' === $args['vp_opts']['content_source'];

		// In older plugin versions we used the query objects in these templates.
		// And some theme authors used these data to run wp functions to output posts data.
		// In order to add back-compatibility, we need to "restore" such a possibility.
		//
		// Example: https://wordpress.org/support/topic/title-and-link-error-for-blog/.
		$set_post_object = $is_posts && isset( $args['post_id'] ) && $args['post_id'];

		if ( $set_post_object ) {
            // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
			$post = get_post( $args['post_id'] );

			setup_postdata( $post );
		}

		// prepare image.
		$image_attrs = '';
		$custom_alt  = isset( $args['alt'] ) ? trim( (string) $args['alt'] ) : '';

		if ( '' !== $custom_alt ) {
			$image_attrs = array(
				'alt' => $custom_alt,
			);
		}

		$args['image'] = Visual_Portfolio_Images::get_attachment_image( $args['image_id'], $args['img_size'], false, $image_attrs );

		// prepare date.
		if ( isset( $args['opts']['show_date'] ) ) {
			if ( 'human' === $args['opts']['show_date'] ) {
				// translators: %s - published in human format.
				$args['published'] = sprintf( esc_html__( '%s ago', 'visual-portfolio' ), human_time_diff( mysql2date( 'U', $args['published_time'], true ), current_time( 'timestamp' ) ) ); //phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested
			} elseif ( $args['opts']['show_date'] ) {
				$args['published'] = mysql2date( $args['opts']['date_format'] ? $args['opts']['date_format'] : 'F j, Y', $args['published_time'], true );
			}

			// fallback for Visual Portfolio 1.2.1 version.
			$args['opts']['date_human_format'] = 'human' === $args['opts']['show_date'];
			$args['published_human_format']    = $args['published'];
		}

		// add video format args.
		if ( 'video' === $args['format'] && $args['video'] ) {
			$args['format_video_url'] = $args['video'];

			if ( ! $is_posts ) {
				$args['url'] = $args['video'];
			}
		}

		// prepare read more button.
		if ( isset( $args['opts']['show_read_more'] ) && $args['opts']['show_read_more'] ) {
			if ( $is_posts && 'more_tag' === $args['opts']['show_read_more'] ) {
				if ( strpos( get_post_field( 'post_content', $args['post_id'] ), '<!--more-->' ) ) {
					$args['opts']['read_more_url'] = $args['url'] . '#more-' . $args['post_id'];
				} else {
					$args['opts']['show_read_more'] = false;
				}
			} else {
				$args['opts']['read_more_url'] = $args['url'];
			}
		}

		// Click action.
		$args['url_target'] = false;
		$args['url_rel']    = false;

		switch ( $args['vp_opts']['items_click_action'] ) {
			case 'popup_gallery':
				break;
			case false:
				$args['url'] = false;
				break;
			default:
				$args['url_target'] = $args['vp_opts']['items_click_action_url_target'] ? $args['vp_opts']['items_click_action_url_target'] : false;
				$args['url_rel']    = $args['vp_opts']['items_click_action_url_rel'] ? $args['vp_opts']['items_click_action_url_rel'] : false;
				break;
		}

		$args['aria_label'] = self::get_item_aria_label( $args );

		// No Image.
		if ( ! $args['image'] && $args['no_image'] ) {
			$args['image'] = Visual_Portfolio_Images::get_attachment_image( $args['no_image'], $args['img_size'], false, '' );
		}

		// Class.
		$args['class'] = 'vp-portfolio__item-wrap';
		if ( $is_posts ) {
			// post_class functionality.
			$args['class'] = join( ' ', get_post_class( $args['class'], $args['post_id'] ) );
		}

		if ( ! $is_posts && is_array( $args['categories'] ) && ! empty( $args['categories'] ) ) {
			foreach ( $args['categories'] as $category ) {
				$args['class'] .= ' category-' . $category['slug'];
			}
		}

		if ( $args['uid'] ) {
			$args['class'] .= ' vp-portfolio__item-uid-' . esc_attr( $args['uid'] );
		}

		$args = apply_filters( 'vpf_each_item_args', $args );

		// Tag Name.
		$tag_name = $is_posts ? 'article' : 'div';
		$tag_name = apply_filters( 'vpf_each_item_tag_name', $tag_name, $args );

		$attrs = array(
			'class'          => $args['class'],
			'data-vp-filter' => $args['filter'],
		);

		if ( isset( $args['focal_point'] ) && $args['focal_point'] && ! empty( $args['focal_point'] ) ) {
			$attrs['style'] = '--vp-images__object-position: ' . esc_attr( 100 * floatval( $args['focal_point']['x'] ) ) . '% ' . esc_attr( 100 * floatval( $args['focal_point']['y'] ) ) . '%;';
		}

		$attrs = apply_filters( 'vpf_each_item_tag_attrs', $attrs, $args );
		?>

		<<?php echo esc_attr( $tag_name ); ?>
		<?php
		foreach ( $attrs as $name => $val ) {
			echo esc_attr( $name ) . '="' . esc_attr( $val ) . '" ';
		}
		?>
		>
			<?php
			if ( $args['vp_opts']['items_click_action'] && 'url' !== $args['vp_opts']['items_click_action'] ) {
				self::item_popup_data( $args );
			}
			?>
			<?php do_action( 'vpf_before_each_item', $args ); ?>
			<figure class="vp-portfolio__item">
				<?php
				do_action( 'vpf_each_item_start', $args );

				$items_style_pref = '';
				if ( 'default' !== $args['vp_opts']['items_style'] ) {
					$items_style_pref = '/' . $args['vp_opts']['items_style'];
				}
				visual_portfolio()->include_template( 'items-list/items-style' . $items_style_pref . '/image', $args );
				visual_portfolio()->include_template( 'items-list/items-style' . $items_style_pref . '/meta', $args );

				do_action( 'vpf_each_item_end', $args );
				?>
			</figure>
			<?php do_action( 'vpf_after_each_item', $args ); ?>
		</<?php echo esc_attr( $tag_name ); ?>>
		<?php

		if ( $set_post_object ) {
			wp_reset_postdata();
		}
	}

	/**
	 * Print item popup data.
	 *
	 * @param array $args - item args.
	 */
	private static function item_popup_data( $args ) {
		$popup_image  = false;
		$popup_video  = false;
		$popup_output = false;

		if ( isset( $args['allow_popup'] ) && $args['allow_popup'] ) {
			if ( isset( $args['format_video_url'] ) && $args['format_video_url'] ) {
				$popup_video = self::get_popup_video( $args );
			} else {
				$img_id = $args['image_id'] ? $args['image_id'] : $args['no_image'];

				$popup_image = self::get_popup_image( $img_id, $args );
			}
		}

		if ( $popup_image ) {
			$popup_output = self::popup_image_output( $popup_image, $args );
		} elseif ( $popup_video ) {
			$popup_output = self::popup_video_output( $popup_video, $args );
		}

		$popup_output = apply_filters( 'vpf_popup_output', $popup_output, $args );

		echo wp_kses( $popup_output, 'vp_popup' );
	}

	/**
	 * Get item popup image data.
	 *
	 * @param  int   $img_id - image id.
	 * @param  array $args - item args.
	 * @return array|bool
	 */
	public static function get_popup_image( $img_id, $args ) {
		$popup_image = false;
		if ( $img_id ) {
			$attachment = get_post( $args['image_id'] );

			if ( $attachment && 'attachment' === $attachment->post_type ) {
				$img_meta    = wp_get_attachment_image_src( $args['image_id'], $args['img_size_popup'] );
				$img_md_meta = wp_get_attachment_image_src( $args['image_id'], $args['img_size_md_popup'] );
				$img_sm_meta = wp_get_attachment_image_src( $args['image_id'], $args['img_size_sm_popup'] );

				if ( ! $img_meta ) {
					$img_meta = array(
						wp_get_attachment_url( $args['image_id'] ),
						0,
						0,
					);
				}

				if ( ! $img_md_meta ) {
					$img_md_meta = $img_meta;
				}

				if ( ! $img_sm_meta ) {
					$img_sm_meta = $img_meta;
				}

				$popup_image = apply_filters(
					'vpf_popup_image_data',
					array(
						'id'               => $args['image_id'],
						'title'            => $attachment->post_title,
						'description'      => $attachment->post_content,
						'caption'          => wp_get_attachment_caption( $attachment->ID ),
						'alt'              => isset( $args['alt'] ) && '' !== trim( (string) $args['alt'] ) ? trim( (string) $args['alt'] ) : get_post_meta( $attachment->ID, '_wp_attachment_image_alt', true ),
						'url'              => $img_meta[0],
						'srcset'           => wp_get_attachment_image_srcset( $args['image_id'], $args['img_size_popup'] ),
						'width'            => $img_meta[1],
						'height'           => $img_meta[2],
						'md_url'           => $img_md_meta[0],
						'md_width'         => $img_md_meta[1],
						'md_height'        => $img_md_meta[2],
						'sm_url'           => $img_sm_meta[0],
						'sm_width'         => $img_sm_meta[1],
						'sm_height'        => $img_sm_meta[2],
						'item_title'       => $args['title'],
						'item_description' => $args['content'],
						'item_excerpt'     => self::get_item_excerpt( $args ),
						'item_author'      => $args['author'],
						'item_author_url'  => $args['author_url'],
					)
				);
			} elseif ( $args['image_id'] ) {
				$popup_image = apply_filters( 'vpf_popup_custom_image_data', false, $args['image_id'] );

				// Check items title and description availability.
				if ( $popup_image && ! isset( $popup_image['item_title'] ) ) {
					$popup_image['item_title'] = $popup_image['title'] ?? '';
				}
				if ( $popup_image && ! isset( $popup_image['item_description'] ) ) {
					$popup_image['item_description'] = $popup_image['description'] ?? '';
				}
				if ( $popup_image && ! isset( $popup_image['item_excerpt'] ) ) {
					$popup_image['item_excerpt'] = self::get_item_excerpt( $args );
				}
				if ( $popup_image && ! isset( $popup_image['item_author'] ) ) {
					$popup_image['item_author'] = $popup_image['author'] ?? '';
				}
			}
		}
		return $popup_image;
	}

	/**
	 * Get item popup video data.
	 *
	 * @param array $args - item args.
	 *
	 * @return array
	 */
	public static function get_popup_video( $args ) {
		return array(
			'url'              => $args['format_video_url'],
			'poster'           => wp_get_attachment_image_url( $args['image_id'], 'full' ),
			'item_title'       => $args['title'] ?? null,
			'item_description' => $args['content'] ?? null,
			'item_excerpt'     => self::get_item_excerpt( $args ),
			'item_author'      => $args['author'] ?? null,
			'item_author_url'  => $args['author_url'] ?? null,
		);
	}

	/**
	 * Build trimmed item excerpt for grid meta and lightbox sources.
	 * Independent of the Display Excerpt items-style toggle.
	 *
	 * @param array $args Item args.
	 * @return string
	 */
	public static function get_item_excerpt( $args ) {
		$words_count = isset( $args['opts']['excerpt_words_count'] ) ? (int) $args['opts']['excerpt_words_count'] : 15;

		if ( $words_count < 1 ) {
			$words_count = 15;
		}

		// Reuse already computed grid excerpt when present.
		if ( ! empty( $args['excerpt'] ) ) {
			return $args['excerpt'];
		}

		$post_id = isset( $args['post_id'] ) ? (int) $args['post_id'] : 0;

		if ( $post_id ) {
			$raw = has_excerpt( $post_id ) ? get_the_excerpt( $post_id ) : ( $args['content'] ?? '' );

			if ( $raw ) {
				return wp_trim_words( do_shortcode( $raw ), $words_count, '...' );
			}

			return '';
		}

		if ( ! empty( $args['content'] ) ) {
			return wp_trim_words( $args['content'], $words_count, '...' );
		}

		return '';
	}

	/**
	 * Print image popup.
	 *
	 * @param array $image_data - item popup image data.
	 * @param array $args  - item args.
	 *
	 * @return string|bool
	 */
	public static function popup_image_output( $image_data, $args ) {
		$popup_output = false;

		if ( $image_data ) {
			ob_start();

			$title_source       = $args['vp_opts']['items_click_action_popup_title_source'] ? $args['vp_opts']['items_click_action_popup_title_source'] : '';
			$description_source = $args['vp_opts']['items_click_action_popup_description_source'] ? $args['vp_opts']['items_click_action_popup_description_source'] : '';

			visual_portfolio()->include_template(
				'popup/image-popup-data',
				array(
					'title_source'       => $title_source,
					'description_source' => $description_source,
					'image_data'         => $image_data,
					'args'               => $args,
					'opts'               => $args['vp_opts'],
				)
			);

			$popup_output = ob_get_clean();
		}

		return $popup_output;
	}

	/**
	 * Print video popup.
	 *
	 * @param array $video_data - item popup video data.
	 * @param array $args  - item args.
	 *
	 * @return string|bool
	 */
	public static function popup_video_output( $video_data, $args ) {
		$popup_output = false;

		if ( $video_data ) {
			ob_start();

			$title_source       = $args['vp_opts']['items_click_action_popup_title_source'] ? $args['vp_opts']['items_click_action_popup_title_source'] : '';
			$description_source = $args['vp_opts']['items_click_action_popup_description_source'] ? $args['vp_opts']['items_click_action_popup_description_source'] : '';

			visual_portfolio()->include_template(
				'popup/video-popup-data',
				array(
					'title_source'       => $title_source,
					'description_source' => $description_source,
					'video_data'         => $video_data,

					// We need to check existence of args to prevent possible conflicts.
					'args'               => isset( $args ) ? $args : null,
					'opts'               => isset( $args['vp_opts'] ) ? $args['vp_opts'] : null,
				)
			);

			$popup_output = ob_get_clean();
		}

		return $popup_output;
	}

	/**
	 * Print pagination
	 *
	 * @param array $vp_options - current vp_list options.
	 */
	public static function pagination( $vp_options ) {
		if ( ! $vp_options['pagination_style'] || ! $vp_options['pagination'] ) {
			return;
		}

		// get options for the current pagination.
		$pagination_options      = array();
		$pagination_options_slug = 'pagination_' . $vp_options['pagination_style'] . '__';
		foreach ( $vp_options as $k => $opt ) {
			// add option to array.
			if ( substr( $k, 0, strlen( $pagination_options_slug ) ) === $pagination_options_slug ) {
				$opt_name                        = str_replace( $pagination_options_slug, '', $k );
				$pagination_options[ $opt_name ] = $opt;
			}

			// remove style options from the options list.
			if ( substr( $k, 0, strlen( $pagination_options_slug ) ) === $pagination_options_slug ) {
				unset( $vp_options[ $k ] );
			}
		}

		$args = array(
			'type'          => $vp_options['pagination'],
			'next_page_url' => $vp_options['next_page_url'],
			'start_page'    => $vp_options['start_page'],
			'max_pages'     => $vp_options['max_pages'],
			'class'         => 'vp-pagination',
			'opts'          => $pagination_options,
			'vp_opts'       => $vp_options,
		);

		$args = apply_filters( 'vpf_pagination_args', $args, $vp_options );

		// No more posts.
		if ( ! $args['next_page_url'] ) {
			$args['class'] .= ' vp-pagination__no-more';
		}

		?>
		<div class="vp-portfolio__pagination-wrap">
		<?php

		$pagination_style_pref = '';
		if ( 'default' !== $vp_options['pagination_style'] ) {
			$pagination_style_pref = '/' . $vp_options['pagination_style'];
		}

		switch ( $vp_options['pagination'] ) {
			case 'infinite':
			case 'load-more':
				if ( 'infinite' === $vp_options['pagination'] ) {
					$args['text_load']     = $vp_options['pagination_infinite_text_load'];
					$args['text_loading']  = $vp_options['pagination_infinite_text_loading'];
					$args['text_end_list'] = $vp_options['pagination_infinite_text_end_list'];
				} else {
					$args['text_load']     = $vp_options['pagination_load_more_text_load'];
					$args['text_loading']  = $vp_options['pagination_load_more_text_loading'];
					$args['text_end_list'] = $vp_options['pagination_load_more_text_end_list'];
				}

				if ( ! $vp_options['pagination_hide_on_end'] || $args['next_page_url'] ) {
					visual_portfolio()->include_template( 'items-list/pagination' . $pagination_style_pref . '/' . $vp_options['pagination'], $args );
				}

				break;
			default:
				// Scroll to top.
				if ( $vp_options['pagination_paged__scroll_top'] ) {
					$args['class'] .= ' vp-pagination__scroll-top';

					if ( isset( $vp_options['pagination_paged__scroll_top_offset'] ) ) {
						$args['scroll_top_offset'] = $vp_options['pagination_paged__scroll_top_offset'];
					}
				}

				// parse html string and make arrays.
				$filtered_links = self::get_pagination_links( $args, $vp_options );

				if ( ! empty( $filtered_links ) ) {
					$args['items'] = $filtered_links;
					visual_portfolio()->include_template( 'items-list/pagination' . $pagination_style_pref . '/paged', $args );
				}

				break;
		}

		?>
		</div>
		<?php
	}

	/**
	 * Get pagination links.
	 *
	 * @param array $args - Block Arguments.
	 * @param array $vp_options - Block Options.
	 * @return array
	 */
	public static function get_pagination_links( $args, $vp_options ) {
		$pagination_links = paginate_links(
			array(
				'base'      => esc_url_raw(
					str_replace(
						999999999,
						'%#%',
						remove_query_arg(
							'add-to-cart',
							self::get_pagenum_link(
								array(
									'vp_page' => 999999999,
								)
							)
						)
					)
				),
				'format'    => '',
				'type'      => 'array',
				'current'   => $args['start_page'],
				'total'     => $args['max_pages'],
				'prev_text' => '&lt;',
				'next_text' => '&gt;',
				'end_size'  => $args['end_size'] ?? 1,
				'mid_size'  => $args['mid_size'] ?? 2,
			)
		);

		// parse html string and make arrays.
		$filtered_links = array();
		if ( $pagination_links ) {
			foreach ( $pagination_links as $link ) {
				$tag_data = self::extract_tags( $link, array( 'a', 'span' ) );
				$tag_data = ! empty( $tag_data ) ? $tag_data[0] : $tag_data;

				if ( ! empty( $tag_data ) ) {
					$atts  = isset( $tag_data['attributes'] ) ? $tag_data['attributes'] : false;
					$href  = $atts && isset( $atts['href'] ) ? $atts['href'] : false;
					$class = $atts && isset( $atts['class'] ) ? $atts['class'] : '';
					$label = isset( $tag_data['contents'] ) ? $tag_data['contents'] : false;

					$arr = array(
						'url'           => $href,
						'label'         => $label,
						'class'         => 'vp-pagination__item',
						'active'        => strpos( $class, 'current' ) !== false,
						'is_prev_arrow' => strpos( $class, 'prev' ) !== false,
						'is_next_arrow' => strpos( $class, 'next' ) !== false,
						'is_dots'       => strpos( $class, 'dots' ) !== false,
					);

					if ( $arr['active'] ) {
						$arr['class'] .= ' vp-pagination__item-active';
					}
					if ( $arr['is_prev_arrow'] ) {
						$arr['class'] .= ' vp-pagination__item-prev';
					}
					if ( $arr['is_next_arrow'] ) {
						$arr['class'] .= ' vp-pagination__item-next';
					}
					if ( $arr['is_dots'] ) {
						$arr['class'] .= ' vp-pagination__item-dots';
					}

					// skip arrows if disabled.
					if ( ! $vp_options['pagination_paged__show_arrows'] && ( $arr['is_prev_arrow'] || $arr['is_next_arrow'] ) ) {
						continue;
					}

					// skip numbers if disabled.
					if ( ! $vp_options['pagination_paged__show_numbers'] && ! $arr['is_prev_arrow'] && ! $arr['is_next_arrow'] ) {
						continue;
					}

					$filtered_links[] = apply_filters( 'vpf_pagination_item_data', $arr, $args, $vp_options );
				}
			}
		}

		return $filtered_links;
	}

	/**
	 * Return current page url with paged support.
	 *
	 * @param array $query_arg - custom query arg.
	 * @return string
	 */
	public static function get_pagenum_link( $query_arg = array() ) {
		// Use current page url.
		global $wp;
		$current_url = '';

		// We should use REQUEST_URI in case if the user used non-default permalinks.
		// For example, this one:
		// - /index.php/%postname% .
		if ( isset( $_SERVER['HTTP_HOST'], $_SERVER['REQUEST_URI'] ) ) {
			$current_url = esc_url_raw( wp_unslash( ( is_ssl() ? 'https' : 'http' ) . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ) );
		} else {
			$current_url = trailingslashit( home_url( $wp->request ) );

            // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			if ( ! empty( $_GET ) ) {
                // phpcs:ignore WordPress.Security.NonceVerification.Recommended
				$current_url = add_query_arg( array_map( 'sanitize_text_field', wp_unslash( $_GET ) ), $current_url );
			}
		}

		if ( isset( $query_arg['vp_filter'] ) && ! $query_arg['vp_filter'] ) {
			unset( $query_arg['vp_filter'] );
			$current_url = remove_query_arg( 'vp_filter', $current_url );
		}
		if ( isset( $query_arg['vp_sort'] ) && ! $query_arg['vp_sort'] ) {
			unset( $query_arg['vp_sort'] );
			$current_url = remove_query_arg( 'vp_sort', $current_url );
		}
		if ( isset( $query_arg['vp_page'] ) && 1 === $query_arg['vp_page'] ) {
			unset( $query_arg['vp_page'] );
			$current_url = remove_query_arg( 'vp_page', $current_url );
		}

		// Add custom query args.
		$current_url = add_query_arg( $query_arg, $current_url );

		$current_url = apply_filters( 'vpf_get_pagenum_link', $current_url, $query_arg );

		return $current_url;
	}

	/**
	 * Create slug from user input string.
	 *
	 * @param string $str - user string.
	 * @param string $delimiter - words delimiter.
	 *
	 * @return string
	 */
	public static function create_slug( $str, $delimiter = '_' ) {
		$slug = $str;

		if ( class_exists( 'Cocur\Slugify\Slugify' ) ) {
			$slugify = new Cocur\Slugify\Slugify();
			$slug    = $slugify->slugify( $str, $delimiter );

			// Sometimes Slugify can't add slug to string with unicode.
			// We will return the standard string.
			if ( ! $slug ) {
				$slug = $str;
			}
		}

		return $slug;
	}

	/**
	 * Get list with all used posts on the current page.
	 *
	 * @return array
	 */
	public static function get_all_used_posts() {
		// add post IDs from main query.
		if ( self::$check_main_query && ! self::is_preview() ) {
			self::$check_main_query = false;

			global $wp_query;

			if ( isset( $wp_query ) && isset( $wp_query->posts ) ) {
				foreach ( $wp_query->posts as $post ) {
					self::$used_posts[] = $post->ID;
				}
			}
		}

		return self::$used_posts;
	}

	/**
	 * Get list with all used portfolios on the current page.
	 *
	 * @return array
	 */
	public static function get_all_used_layouts() {
		return self::$used_layouts;
	}

	/**
	 * Extract specific HTML tags and their attributes from a string.
	 *
	 * Found in http://w-shadow.com/blog/2009/10/20/how-to-extract-html-tags-and-their-attributes-with-php/
	 *
	 * You can either specify one tag, an array of tag names, or a regular expression that matches the tag name(s).
	 * If multiple tags are specified you must also set the $selfclosing parameter and it must be the same for
	 * all specified tags (so you can't extract both normal and self-closing tags in one go).
	 *
	 * The function returns a numerically indexed array of extracted tags. Each entry is an associative array
	 * with these keys :
	 *  tag_name    - the name of the extracted tag, e.g. "a" or "img".
	 *  offset      - the numberic offset of the first character of the tag within the HTML source.
	 *  contents    - the inner HTML of the tag. This is always empty for self-closing tags.
	 *  attributes  - a name -> value array of the tag's attributes, or an empty array if the tag has none.
	 *  full_tag    - the entire matched tag, e.g. '<a href="http://example.com">example.com</a>'. This key
	 *                will only be present if you set $return_the_entire_tag to true.
	 *
	 * @param string       $html The HTML code to search for tags.
	 * @param string|array $tag The tag(s) to extract.
	 * @param bool         $selfclosing Whether the tag is self-closing or not. Setting it to null will force the script to try and make an educated guess.
	 * @param bool         $return_the_entire_tag Return the entire matched tag in 'full_tag' key of the results array.
	 * @param string       $charset The character set of the HTML code. Defaults to ISO-8859-1.
	 *
	 * @return array An array of extracted tags, or an empty array if no matching tags were found.
	 */
	private static function extract_tags( $html, $tag, $selfclosing = null, $return_the_entire_tag = false, $charset = 'ISO-8859-1' ) {

		if ( is_array( $tag ) ) {
			$tag = implode( '|', $tag );
		}

		// If the user didn't specify if $tag is a self-closing tag we try to auto-detect it by checking against a list of known self-closing tags.
		$selfclosing_tags = array( 'area', 'base', 'basefont', 'br', 'hr', 'input', 'img', 'link', 'meta', 'col', 'param' );
		if ( is_null( $selfclosing ) ) {
			$selfclosing = in_array( $tag, $selfclosing_tags, true );
		}

		// The regexp is different for normal and self-closing tags because I can't figure out how to make a sufficiently robust unified one.
		if ( $selfclosing ) {
			$tag_pattern =
				'@<(?P<tag>' . $tag . ')           # <tag
                (?P<attributes>\s[^>]+)?       # attributes, if any
                \s*/?>                   # /> or just >, being lenient here
                @xsi';
		} else {
			$tag_pattern =
				'@<(?P<tag>' . $tag . ')           # <tag
                (?P<attributes>\s[^>]+)?       # attributes, if any
                \s*>                 # >
                (?P<contents>.*?)         # tag contents
                </(?P=tag)>               # the closing </tag>
                @xsi';
		}

		$attribute_pattern =
			'@
            (?P<name>\w+)                         # attribute name
            \s*=\s*
            (
                (?P<quote>[\"\'])(?P<value_quoted>.*?)(?P=quote)    # a quoted value
                |                           # or
                (?P<value_unquoted>[^\s"\']+?)(?:\s+|$)           # an unquoted value (terminated by whitespace or EOF)
            )
            @xsi';

		// Find all tags.
		if ( ! preg_match_all( $tag_pattern, $html, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE ) ) {
			// Return an empty array if we didn't find anything.
			return array();
		}

		$tags = array();
		foreach ( $matches as $match ) {

			// Parse tag attributes, if any.
			$attributes = array();
			if ( ! empty( $match['attributes'][0] ) ) {

				if ( preg_match_all( $attribute_pattern, $match['attributes'][0], $attribute_data, PREG_SET_ORDER ) ) {
					// Turn the attribute data into a name->value array.
					foreach ( $attribute_data as $attr ) {
						if ( ! empty( $attr['value_quoted'] ) ) {
							$value = $attr['value_quoted'];
						} elseif ( ! empty( $attr['value_unquoted'] ) ) {
							$value = $attr['value_unquoted'];
						} else {
							$value = '';
						}

						// Passing the value through html_entity_decode is handy when you want to extract link URLs or something like that. You might want to remove or modify this call if it doesn't fit your situation.
						$value = html_entity_decode( $value, ENT_QUOTES, $charset );

						$attributes[ $attr['name'] ] = $value;
					}
				}
			}

			$tag = array(
				'tag_name'   => $match['tag'][0],
				'offset'     => $match[0][1],
				'contents'   => ! empty( $match['contents'] ) ? $match['contents'][0] : '',   // empty for self-closing tags.
				'attributes' => $attributes,
			);
			if ( $return_the_entire_tag ) {
				$tag['full_tag'] = $match[0][0];
			}

			$tags[] = $tag;
		}

		return $tags;
	}
}

new Visual_Portfolio_Get();
