<?php
/**
 * @package The_SEO_Framework\Classes\Facade\Generate_Title
 * @subpackage The_SEO_Framework\Getters\Title
 */

namespace The_SEO_Framework;

\defined( 'THE_SEO_FRAMEWORK_PRESENT' ) or die;

/**
 * The SEO Framework plugin
 * Copyright (C) 2015 - 2021 Sybre Waaijer, CyberWire B.V. (https://cyberwire.nl/)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3 as published
 * by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * Class The_SEO_Framework\Generate_Title
 *
 * Generates title SEO data based on content.
 *
 * NOTE: Don't supply $args in any of the methods for non-term taxomies, like author archives.
 *       ID collision isn't accounted for in these scenarios.
 *
 * @since 2.8.0
 */
class Generate_Title extends Generate_Description {

	/**
	 * Returns the meta title from custom fields. Falls back to autogenerated title.
	 *
	 * @since 3.1.0
	 * @since 3.2.2 No longer double-escapes the custom field title.
	 * @since 4.1.0 Added the third $social parameter.
	 * @uses $this->get_custom_field_title()
	 * @uses $this->get_generated_title()
	 *
	 * @param array|null $args   The query arguments. Accepts 'id' and 'taxonomy'.
	 *                           Leave null to autodetermine query.
	 * @param bool       $escape Whether to escape the title.
	 * @param bool       $social Whether the title is meant for social display.
	 * @return string The real title output.
	 */
	public function get_title( $args = null, $escape = true, $social = false ) {

		// phpcs:disable, WordPress.WhiteSpace.PrecisionAlignment
		$title = $this->get_custom_field_title( $args, false, $social )
			  ?: $this->get_generated_title( $args, false, $social );
		// phpcs:enable, WordPress.WhiteSpace.PrecisionAlignment

		return $escape ? $this->escape_title( $title ) : $title;
	}

	/**
	 * Returns the custom user-inputted title.
	 *
	 * @since 3.1.0
	 * @since 4.0.0 Moved the filter to a separated method.
	 * @since 4.1.0 Added the third $social parameter.
	 *
	 * @param array|null $args   The query arguments. Accepts 'id' and 'taxonomy'.
	 *                           Leave null to autodetermine query.
	 * @param bool       $escape Whether to escape the title.
	 * @param bool       $social Whether the title is meant for social display.
	 * @return string The custom field title.
	 */
	public function get_custom_field_title( $args = null, $escape = true, $social = false ) {

		$title = $this->get_filtered_raw_custom_field_title( $args );

		if ( $title ) {
			if ( $this->use_title_protection( $args ) )
				$this->merge_title_protection( $title, $args );

			if ( $this->use_title_pagination( $args ) )
				$this->merge_title_pagination( $title );

			if ( $this->use_title_branding( $args, $social ) )
				$this->merge_title_branding( $title, $args );
		}

		return $escape ? $this->escape_title( $title ) : $title;
	}

	/**
	 * Returns the autogenerated meta title.
	 *
	 * @since 3.1.0
	 * @since 3.2.4 1. Added check for title protection.
	 *              2. Moved check for title pagination.
	 * @since 4.0.0 Moved the filter to a separated method.
	 * @since 4.1.0 Added the third $social parameter.
	 * @uses $this->s_title_raw() : This is the same method used to prepare custom title on save.
	 * @uses $this->get_filtered_raw_generated_title()
	 *
	 * @param array|null $args   The query arguments. Accepts 'id' and 'taxonomy'.
	 *                           Leave null to autodetermine query.
	 * @param bool       $escape Whether to escape the title.
	 * @param bool       $social Whether the title is meant for social display.
	 * @return string The generated title output.
	 */
	public function get_generated_title( $args = null, $escape = true, $social = false ) {

		$title = $this->get_filtered_raw_generated_title( $args );

		if ( $this->use_title_protection( $args ) )
			$this->merge_title_protection( $title, $args );

		if ( $this->use_title_pagination( $args ) )
			$this->merge_title_pagination( $title );

		if ( $this->use_title_branding( $args, $social ) )
			$this->merge_title_branding( $title, $args );

		$title = $this->s_title_raw( $title );

		return $escape ? $this->escape_title( $title ) : $title;
	}

	/**
	 * Returns the raw filtered custom field meta title.
	 *
	 * @since 4.0.0
	 *
	 * @param array|null $args   The query arguments. Accepts 'id' and 'taxonomy'.
	 *                           Leave null to autodetermine query.
	 * @return string The raw generated title output.
	 */
	public function get_filtered_raw_custom_field_title( $args ) {
		/**
		 * Filters the title from custom field, if any.
		 *
		 * @since 3.1.0
		 *
		 * @param string     $title The title.
		 * @param array|null $args  The query arguments. Contains 'id' and 'taxonomy'.
		 *                          Is null when query is autodetermined.
		 */
		return (string) \apply_filters_ref_array(
			'the_seo_framework_title_from_custom_field',
			[
				$this->get_raw_custom_field_title( $args ),
				$args,
			]
		);
	}

	/**
	 * Returns the raw filtered autogenerated meta title.
	 *
	 * @since 4.0.0
	 *
	 * @param array|null $args   The query arguments. Accepts 'id' and 'taxonomy'.
	 *                           Leave null to autodetermine query.
	 * @return string The raw generated title output.
	 */
	public function get_filtered_raw_generated_title( $args ) {
		/**
		 * Filters the title from query.
		 *
		 * @NOTE: This filter doesn't consistently run on the SEO Settings page.
		 *        You may want to avoid this filter for the homepage, by returning the default value.
		 * @since 3.1.0
		 * @param string     $title The title.
		 * @param array|null $args  The query arguments. Contains 'id' and 'taxonomy'.
		 *                          Is null when query is autodetermined.
		 */
		return (string) \apply_filters_ref_array(
			'the_seo_framework_title_from_generation',
			[
				$this->get_raw_generated_title( $args ),
				$args,
			]
		);
	}

	/**
	 * Returns the Twitter meta title.
	 * Falls back to Open Graph title.
	 *
	 * @since 3.0.4
	 * @since 3.1.0 : 1. The first parameter now expects an array.
	 *                2. Now tries to get the homepage social titles.
	 *
	 * @param array|null $args   The query arguments. Accepts 'id' and 'taxonomy'.
	 *                           Leave null to autodetermine query.
	 * @param bool       $escape Whether to escape the title.
	 * @return string Twitter Title.
	 */
	public function get_twitter_title( $args = null, $escape = true ) {

		// phpcs:disable, WordPress.WhiteSpace.PrecisionAlignment
		$title = $this->get_twitter_title_from_custom_field( $args, false )
			  ?: $this->get_generated_twitter_title( $args, false );
		// phpcs:enable, WordPress.WhiteSpace.PrecisionAlignment

		return $escape ? $this->escape_title( $title ) : $title;
	}

	/**
	 * Returns the Twitter meta title from custom field.
	 * Falls back to Open Graph title.
	 *
	 * @since 3.1.0
	 * @see $this->get_twitter_title()
	 *
	 * @param array|null $args   The query arguments. Accepts 'id' and 'taxonomy'.
	 * @param bool       $escape Whether to escape the title.
	 * @return string Twitter Title.
	 */
	protected function get_twitter_title_from_custom_field( $args, $escape ) {

		if ( null === $args ) {
			$title = $this->get_custom_twitter_title_from_query();
		} else {
			$this->fix_generation_args( $args );
			$title = $this->get_custom_twitter_title_from_args( $args );
		}

		return $escape ? $this->escape_title( $title ) : $title;
	}

	/**
	 * Returns the Twitter meta title from custom field, based on query.
	 * Falls back to Open Graph title.
	 *
	 * @since 3.1.0
	 * @since 3.2.2 Now tests for the homepage as page prior getting custom field data.
	 * @since 4.0.0 Added term meta item checks.
	 * @see $this->get_twitter_title()
	 * @see $this->get_twitter_title_from_custom_field()
	 *
	 * @return string Twitter Title.
	 */
	protected function get_custom_twitter_title_from_query() {

		$title = '';
		// phpcs:disable, WordPress.WhiteSpace.PrecisionAlignment
		if ( $this->is_real_front_page() ) {
			if ( $this->is_static_frontpage() ) {
				$title = $this->get_option( 'homepage_twitter_title' )
					  ?: $this->get_post_meta_item( '_twitter_title' )
					  ?: $this->get_option( 'homepage_og_title' )
					  ?: $this->get_post_meta_item( '_open_graph_title' )
					  ?: '';
			} else {
				$title = $this->get_option( 'homepage_twitter_title' )
					  ?: $this->get_option( 'homepage_og_title' )
					  ?: '';
			}
		} elseif ( $this->is_singular() ) {
			$title = $this->get_post_meta_item( '_twitter_title' )
				  ?: $this->get_post_meta_item( '_open_graph_title' )
				  ?: '';
		} elseif ( $this->is_term_meta_capable() ) {
			$title = $this->get_term_meta_item( 'tw_title' )
				  ?: $this->get_term_meta_item( 'og_title' )
				  ?: '';
		}
		// phpcs:enable, WordPress.WhiteSpace.PrecisionAlignment

		return $title;
	}

	/**
	 * Returns the Twitter meta title from custom field, based on arguments.
	 * Falls back to Open Graph title.
	 *
	 * @since 3.1.0
	 * @since 3.2.2 Now tests for the homepage as page prior getting custom field data.
	 * @since 4.0.0 Added term meta item checks.
	 * @see $this->get_twitter_title()
	 * @see $this->get_twitter_title_from_custom_field()
	 *
	 * @param array|null $args The query arguments. Accepts 'id' and 'taxonomy'.
	 * @return string Twitter Title.
	 */
	protected function get_custom_twitter_title_from_args( array $args ) {

		// phpcs:disable, WordPress.WhiteSpace.PrecisionAlignment
		if ( $args['taxonomy'] ) {
			$title = $this->get_term_meta_item( 'tw_title', $args['id'] )
				  ?: $this->get_term_meta_item( 'og_title', $args['id'] )
				  ?: '';
		} else {
			if ( $this->is_static_frontpage( $args['id'] ) ) {
				$title = $this->get_option( 'homepage_twitter_title' )
					  ?: $this->get_post_meta_item( '_twitter_title', $args['id'] )
					  ?: $this->get_option( 'homepage_og_title' )
					  ?: $this->get_post_meta_item( '_open_graph_title', $args['id'] )
					  ?: '';
			} elseif ( $this->is_real_front_page_by_id( $args['id'] ) ) {
				$title = $this->get_option( 'homepage_twitter_title' )
					  ?: $this->get_option( 'homepage_og_title' )
					  ?: '';
			} else {
				$title = $this->get_post_meta_item( '_twitter_title', $args['id'] )
					  ?: $this->get_post_meta_item( '_open_graph_title', $args['id'] )
					  ?: '';
			}
		}
		// phpcs:enable, WordPress.WhiteSpace.PrecisionAlignment

		return $title;
	}

	/**
	 * Returns the autogenerated Twitter meta title.
	 * Falls back to meta title.
	 *
	 * @since 3.0.4
	 * @since 3.1.0 The first parameter now expects an array.
	 * @since 4.1.0 Now appends the "social" argument when getting the title.
	 * @uses $this->get_title()
	 *
	 * @param array|null $args   The query arguments. Accepts 'id' and 'taxonomy'.
	 *                           Leave null to autodetermine query.
	 * @param bool       $escape Whether to escape the title.
	 * @return string The generated Twitter Title.
	 */
	public function get_generated_twitter_title( $args = null, $escape = true ) {
		return $this->get_title( $args, $escape, true );
	}

	/**
	 * Returns the Open Graph meta title.
	 * Falls back to meta title.
	 *
	 * @since 3.0.4
	 * @since 3.1.0 : 1. The first parameter now expects an array.
	 *                2. Now tries to get the homepage social title.
	 * @uses $this->get_generated_open_graph_title()
	 *
	 * @param array|null $args   The query arguments. Accepts 'id' and 'taxonomy'.
	 *                           Leave null to autodetermine query.
	 * @param bool       $escape Whether to escape the title.
	 * @return string Open Graph Title.
	 */
	public function get_open_graph_title( $args = null, $escape = true ) {

		// phpcs:disable, WordPress.WhiteSpace.PrecisionAlignment
		$title = $this->get_open_graph_title_from_custom_field( $args, false )
			  ?: $this->get_generated_open_graph_title( $args, false );
		// phpcs:enable, WordPress.WhiteSpace.PrecisionAlignment

		return $escape ? $this->escape_title( $title ) : $title;
	}

	/**
	 * Returns the Open Graph meta title from custom field.
	 * Falls back to meta title.
	 *
	 * @since 3.1.0
	 * @see $this->get_open_graph_title()
	 *
	 * @param array|null $args   The query arguments. Accepts 'id' and 'taxonomy'.
	 * @param bool       $escape Whether to escape the title.
	 * @return string Open Graph Title.
	 */
	protected function get_open_graph_title_from_custom_field( $args, $escape ) {

		if ( null === $args ) {
			$title = $this->get_custom_open_graph_title_from_query();
		} else {
			$this->fix_generation_args( $args );
			$title = $this->get_custom_open_graph_title_from_args( $args );
		}

		return $escape ? $this->escape_title( $title ) : $title;
	}

	/**
	 * Returns the Twitter meta title from custom field, based on query.
	 * Falls back to meta title.
	 *
	 * @since 3.1.0
	 * @since 3.2.2 Now tests for the homepage as page prior getting custom field data.
	 * @since 4.0.0 Added term meta item checks.
	 * @see $this->get_open_graph_title()
	 * @see $this->get_open_graph_title_from_custom_field()
	 *
	 * @return string Open Graph Title.
	 */
	protected function get_custom_open_graph_title_from_query() {

		$title = '';
		// phpcs:disable, WordPress.WhiteSpace.PrecisionAlignment
		if ( $this->is_real_front_page() ) {
			if ( $this->is_static_frontpage() ) {
				$title = $this->get_option( 'homepage_og_title' )
					  ?: $this->get_post_meta_item( '_open_graph_title' )
					  ?: '';
			} else {
				$title = $this->get_option( 'homepage_og_title' ) ?: '';
			}
		} elseif ( $this->is_singular() ) {
			$title = $this->get_post_meta_item( '_open_graph_title' ) ?: '';
		} elseif ( $this->is_term_meta_capable() ) {
			$title = $this->get_term_meta_item( 'og_title' ) ?: '';
		}
		// phpcs:enable, WordPress.WhiteSpace.PrecisionAlignment

		return $title;
	}

	/**
	 * Returns the Open Graph meta title from custom field, based on query.
	 * Falls back to meta title.
	 *
	 * @since 3.1.0
	 * @since 3.2.2 Now tests for the homepage as page prior getting custom field data.
	 * @since 4.0.0 Added term meta item checks.
	 * @see $this->get_open_graph_title()
	 * @see $this->get_open_graph_title_from_custom_field()
	 *
	 * @param array|null $args The query arguments. Accepts 'id' and 'taxonomy'.
	 * @return string Open Graph Title.
	 */
	protected function get_custom_open_graph_title_from_args( array $args ) {

		$title = '';
		// phpcs:disable, WordPress.WhiteSpace.PrecisionAlignment
		if ( $args['taxonomy'] ) {
			$title = $this->get_term_meta_item( 'og_title', $args['id'] ) ?: '';
		} else {
			if ( $this->is_static_frontpage( $args['id'] ) ) {
				$title = $this->get_option( 'homepage_og_title' )
					  ?: $this->get_post_meta_item( '_open_graph_title', $args['id'] )
					  ?: '';
			} elseif ( $this->is_real_front_page_by_id( $args['id'] ) ) {
				$title = $this->get_option( 'homepage_og_title' ) ?: '';
			} else {
				$title = $this->get_post_meta_item( '_open_graph_title', $args['id'] ) ?: '';
			}
		}
		// phpcs:enable, WordPress.WhiteSpace.PrecisionAlignment

		return $title;
	}

	/**
	 * Returns the autogenerated Open Graph meta title. Falls back to meta title.
	 * Falls back to meta title.
	 *
	 * @since 3.0.4
	 * @since 3.1.0 The first parameter now expects an array.
	 * @since 4.1.0 Now appends the "social" argument when getting the title.
	 * @uses $this->get_title()
	 *
	 * @param array|null $args   The query arguments. Accepts 'id' and 'taxonomy'.
	 *                           Leave null to autodetermine query.
	 * @param bool       $escape Whether to escape the title.
	 * @return string The generated Open Graph Title.
	 */
	public function get_generated_open_graph_title( $args = null, $escape = true ) {
		return $this->get_title( $args, $escape, true );
	}

	/**
	 * Returns the custom user-inputted title.
	 *
	 * This doesn't use the taxonomy arguments, because, wonderously, WordPress
	 * finally admits through their code that terms can be queried using only IDs.
	 *
	 * @since 3.1.0
	 * @internal But, feel free to use it.
	 *
	 * @param array|null $args The query arguments. Accepts 'id' and 'taxonomy'.
	 *                         Leave null to autodetermine query.
	 * @return string The custom field title, if it exists.
	 */
	public function get_raw_custom_field_title( $args = null ) {

		$title = '';

		if ( null === $args ) {
			$title = $this->get_custom_field_title_from_query();
		} else {
			$this->fix_generation_args( $args );
			$title = $this->get_custom_field_title_from_args( $args );
		}

		return $title;
	}

	/**
	 * Gets a custom title, based on current query, without additions or prefixes.
	 *
	 * @since 3.1.0
	 * @since 3.2.2 Now tests for the homepage as page prior getting custom field data.
	 * @internal
	 * @see $this->get_raw_custom_field_title()
	 *
	 * @return string The custom title.
	 */
	protected function get_custom_field_title_from_query() {

		$title = '';
		// phpcs:disable, WordPress.WhiteSpace.PrecisionAlignment
		if ( $this->is_real_front_page() ) {
			if ( $this->is_static_frontpage() ) {
				$title = $this->get_option( 'homepage_title' )
					  ?: $this->get_post_meta_item( '_genesis_title' )
					  ?: '';
			} else {
				$title = $this->get_option( 'homepage_title' ) ?: '';
			}
		} elseif ( $this->is_singular() ) {
			$title = $this->get_post_meta_item( '_genesis_title' ) ?: '';
		} elseif ( $this->is_term_meta_capable() ) {
			$title = $this->get_term_meta_item( 'doctitle' ) ?: '';
		} elseif ( \is_post_type_archive() ) {
			/**
			 * @since 4.0.6
			 * @param string $title The post type archive title.
			 */
			$title = (string) \apply_filters( 'the_seo_framework_pta_title', '' ) ?: '';
		}
		// phpcs:enable, WordPress.WhiteSpace.PrecisionAlignment

		return $title;
	}

	/**
	 * Gets a custom title, based on input arguments query, without additions or prefixes.
	 *
	 * @since 3.1.0
	 * @since 3.1.4 Now uses the 'id' to get custom singular title.
	 * @since 3.2.2 Now tests for the homepage as page prior getting custom field data.
	 * @internal
	 * @see $this->get_raw_custom_field_title()
	 *
	 * @param array $args The query arguments. Accepts 'id' and 'taxonomy'.
	 * @return string The custom title.
	 */
	protected function get_custom_field_title_from_args( array $args ) {

		$title = '';
		// phpcs:disable, WordPress.WhiteSpace.PrecisionAlignment
		if ( $args['taxonomy'] ) {
			$title = $this->get_term_meta_item( 'doctitle', $args['id'] ) ?: '';
		} else {
			if ( $this->is_static_frontpage( $args['id'] ) ) {
				$title = $this->get_option( 'homepage_title' )
					  ?: $this->get_post_meta_item( '_genesis_title', $args['id'] )
					  ?: '';
			} elseif ( $this->is_real_front_page_by_id( $args['id'] ) ) {
				$title = $this->get_option( 'homepage_title' ) ?: '';
			} else {
				$title = $this->get_post_meta_item( '_genesis_title', $args['id'] ) ?: '';
			}
		}
		// phpcs:enable, WordPress.WhiteSpace.PrecisionAlignment

		return $title;
	}

	/**
	 * Generates a title, based on expected or current query, without additions or prefixes.
	 *
	 * @since 3.1.0
	 * @uses $this->generate_title_from_query()
	 * @uses $this->generate_title_from_args()
	 *
	 * @param array|null $args The query arguments. Accepts 'id' and 'taxonomy'.
	 *                         Leave null to autodetermine query.
	 * @return string The generated title.
	 */
	public function get_raw_generated_title( $args = null ) {

		$this->remove_default_title_filters( false, $args );

		if ( null === $args ) {
			$title = $this->generate_title_from_query();
		} else {
			$this->fix_generation_args( $args );
			$title = $this->generate_title_from_args( $args );
		}

		$this->reset_default_title_filters();

		return $title ?: $this->get_static_untitled_title();
	}

	/**
	 * Removes default title filters, for consistent output and sanitation.
	 * Memoizes the filters removed, so it can add them back on reset.
	 *
	 * Performance test: 0.007ms per remove+reset on PHP 8.0, single core VPN.
	 *
	 * @since 3.1.0
	 * @since 4.1.0 Added a second parameter, $args, to help soften the burden of this method.
	 * @internal Only to be used within $this->get_raw_generated_title()
	 *
	 * @param bool       $reset Whether to reset the removed filters.
	 * @param array|null $args  The query arguments. Accepts 'id' and 'taxonomy'.
	 *                          Leave null to autodetermine query.
	 */
	protected function remove_default_title_filters( $reset = false, $args = null ) {

		static $filtered = [];

		if ( $reset ) {
			foreach ( $filtered as $tag => $functions )
				foreach ( $functions as $function => $priorities )
					foreach ( $priorities as $priority )
						\add_filter( $tag, $function, $priority );

			// Reset filters.
			$filtered = [];
		} else {
			if ( null === $args ) {
				$filters = [ 'single_post_title', 'single_cat_title', 'single_tag_title' ];
			} else {
				$this->fix_generation_args( $args );
				if ( 'category' === $args['taxonomy'] ) {
					$filters = [ 'single_cat_title' ];
				} elseif ( 'post_tag' === $args['taxonomy'] ) {
					$filters = [ 'single_tag_title' ];
				} else {
					$filters = [ 'single_post_title' ];
				}
			}

			/**
			 * Texturization happens when outputting and saving the title; however,
			 * we want the raw title, so we won't find unexplainable issues later.
			 */
			$functions = [ 'wptexturize' ];

			if ( ! $this->get_option( 'title_strip_tags' ) )
				$functions[] = 'strip_tags';

			foreach ( $filters as $tag ) {
				foreach ( $functions as $function ) {
					// Only grab 10 of these. Yes, one might transform still on the 11th.
					$it = 10;
					$i  = 0;
					// phpcs:ignore, WordPress.CodeAnalysis.AssignmentInCondition
					while ( $priority = \has_filter( $tag, $function ) ) {
						$filtered[ $tag ][ $function ][] = $priority;
						\remove_filter( $tag, $function, $priority );
						// Some noob might've destroyed \WP_Hook. Safeguard.
						if ( ++$i > $it ) break 1;
					}
				}
			}
		}
	}

	/**
	 * Resets default title filters, for consistent output and sanitation.
	 *
	 * @since 3.1.0
	 * @internal Only to be used within $this->get_raw_generated_title()
	 * @uses $this->remove_default_title_filters()
	 */
	protected function reset_default_title_filters() {
		$this->remove_default_title_filters( true );
	}

	/**
	 * Generates a title, based on current query, without additions or prefixes.
	 *
	 * @since 3.1.0
	 * @internal
	 * @see $this->get_raw_generated_title()
	 *
	 * @return string The generated title.
	 */
	protected function generate_title_from_query() {

		$title = '';

		if ( $this->is_404() ) {
			$title = $this->get_static_404_title();
		} elseif ( $this->is_search() ) {
			$title = $this->get_generated_search_query_title();
		} elseif ( $this->is_real_front_page() ) {
			$title = $this->get_static_front_page_title();
		} elseif ( $this->is_archive() ) {
			$title = $this->get_generated_archive_title();
		} elseif ( $this->is_singular() ) {
			$title = $this->get_generated_single_post_title();
		}

		return $title;
	}

	/**
	 * Generates a title, based on expected query, without additions or prefixes.
	 *
	 * @since 3.1.0
	 * @internal
	 * @see $this->get_raw_generated_title()
	 *
	 * @param array $args The query arguments. Required. Accepts 'id' and 'taxonomy'.
	 * @return string The generated title. Empty if query can't be replicated.
	 */
	protected function generate_title_from_args( array $args ) {

		$title = '';

		if ( $args['taxonomy'] ) {
			$title = $this->get_generated_archive_title( \get_term( $args['id'], $args['taxonomy'] ) );
		} else {
			if ( $this->is_real_front_page_by_id( $args['id'] ) ) {
				$title = $this->get_static_front_page_title();
			} else {
				$title = $this->get_generated_single_post_title( $args['id'] );
			}
		}

		return $title;
	}

	/**
	 * Generates front page title.
	 *
	 * @since 3.1.0
	 * @TODO figure out why we didn't choose to use $this->get_blogname()?
	 *
	 * @return string The generated front page title.
	 */
	public function get_static_front_page_title() {
		return \get_bloginfo( 'name', 'raw' );
	}

	/**
	 * Combobulates archive title prefixes for WP 5.5+.
	 *
	 * @since 4.1.2
	 * @TEMP
	 *
	 * @param string $prefix The archive prefix.
	 * @param string $title  The archive title.
	 * @return string The archive title.
	 */
	protected function _combobulate_wp550_archive_title( $prefix, $title ) {
		return sprintf(
			/* translators: 1: Title prefix. 2: Title. */
			\_x( '%1$s %2$s', 'archive title', 'default' ),
			$prefix,
			$title
		);
	}

	/**
	 * Returns the archive title. Also works in admin.
	 *
	 * @NOTE Taken from WordPress core. Altered to work for metadata.
	 * @see WP Core get_the_archive_title()
	 *
	 * @since 3.1.0
	 * @since 4.0.2 Now asserts the correct tag taxonomy condition.
	 * @since 4.0.5 1: Now no longer uses `get_the_author()` to fetch the author's display name,
	 *                 but uses the provided term object instead.
	 *              2: The first parameter now accepts `\WP_User` objects.
	 * @since 4.1.2 Now supports WP 5.5 archive titles.
	 *
	 * @param \WP_Term|\WP_User|\WP_Error|null $term The Term object or error. Leave null to autodetermine query.
	 * @return string The generated archive title, not escaped.
	 */
	public function get_generated_archive_title( $term = null ) {

		if ( $term && \is_wp_error( $term ) )
			return '';

		if ( \is_null( $term ) ) {
			$_query = true;
			$term   = \get_queried_object();
		} else {
			$_query = false;
		}

		/**
		 * @since 2.6.0
		 *
		 * @param string            $title The short circuit title.
		 * @param \WP_Term|\WP_User $term  The Term object.
		 */
		$title = (string) \apply_filters( 'the_seo_framework_the_archive_title', '', $term );

		if ( $title )
			return $title;

		$_tax       = isset( $term->taxonomy ) ? $term->taxonomy : '';
		$use_prefix = $this->use_generated_archive_prefix( $term );

		// TEMP hack. Let's clean this up later.
		$use_new_string_prefixes = $use_prefix && version_compare( \get_bloginfo( 'version' ), '5.5', '>=' );

		if ( ! $_query ) {
			if ( $_tax ) {
				if ( 'category' === $_tax ) {
					$title = $this->get_generated_single_term_title( $term );

					if ( $use_new_string_prefixes ) {
						$title = $this->_combobulate_wp550_archive_title(
							\_x( 'Category:', 'category archive title prefix', 'default' ),
							$title
						);
					} else {
						/* translators: Category archive title. 1: Category name */
						$title = $use_prefix ? sprintf( \__( 'Category: %s', 'default' ), $title ) : $title;
					}
				} elseif ( 'post_tag' === $_tax ) {
					$title = $this->get_generated_single_term_title( $term );

					if ( $use_new_string_prefixes ) {
						$title = $this->_combobulate_wp550_archive_title(
							\_x( 'Tag:', 'tag archive title prefix', 'default' ),
							$title
						);
					} else {
						/* translators: Tag archive title. 1: Tag name */
						$title = $use_prefix ? sprintf( \__( 'Tag: %s', 'default' ), $title ) : $title;
					}
				} else {
					$title = $this->get_generated_single_term_title( $term );
					$title = $use_prefix ? $this->prepend_tax_label_prefix( $title, $_tax ) : $title;
				}
			} elseif ( $term instanceof \WP_User && isset( $term->display_name ) ) {
				$title = $term->display_name;

				if ( $use_new_string_prefixes ) {
					$title = $this->_combobulate_wp550_archive_title(
						\_x( 'Author:', 'author archive title prefix', 'default' ),
						$title
					);
				} else {
					/* translators: Author archive title. 1: Author name */
					$title = $use_prefix ? sprintf( \__( 'Author: %s', 'default' ), $title ) : $title;
				}
			} else {
				$title = \__( 'Archives', 'default' );
			}
		} else {
			if ( $this->is_category() ) {
				$title = $this->get_generated_single_term_title( $term );

				if ( $use_new_string_prefixes ) {
					$title = $this->_combobulate_wp550_archive_title(
						\_x( 'Category:', 'category archive title prefix', 'default' ),
						$title
					);
				} else {
					/* translators: Category archive title. 1: Category name */
					$title = $use_prefix ? sprintf( \__( 'Category: %s', 'default' ), $title ) : $title;
				}
			} elseif ( $this->is_tag() ) {
				$title = $this->get_generated_single_term_title( $term );

				if ( $use_new_string_prefixes ) {
					$title = $this->_combobulate_wp550_archive_title(
						\_x( 'Tag:', 'tag archive title prefix', 'default' ),
						$title
					);
				} else {
					/* translators: Tag archive title. 1: Tag name */
					$title = $use_prefix ? sprintf( \__( 'Tag: %s', 'default' ), $title ) : $title;
				}
			} elseif ( $this->is_author() ) {
				$title = isset( $term->display_name ) ? $term->display_name : '';

				if ( $use_new_string_prefixes ) {
					$title = $this->_combobulate_wp550_archive_title(
						\_x( 'Author:', 'author archive title prefix', 'default' ),
						$title
					);
				} else {
					/* translators: Author archive title. 1: Author name */
					$title = $use_prefix ? sprintf( \__( 'Author: %s', 'default' ), $title ) : $title;
				}
			} elseif ( $this->is_date() ) {
				if ( $this->is_year() ) {
					$title = \get_the_date( \_x( 'Y', 'yearly archives date format', 'default' ) );

					if ( $use_new_string_prefixes ) {
						$title = $this->_combobulate_wp550_archive_title(
							\_x( 'Year:', 'date archive title prefix', 'default' ),
							$title
						);
					} else {
						/* translators: Yearly archive title. 1: Year */
						$title = $use_prefix ? sprintf( \__( 'Year: %s', 'default' ), $title ) : $title;
					}
				} elseif ( $this->is_month() ) {
					$title = \get_the_date( \_x( 'F Y', 'monthly archives date format', 'default' ) );

					if ( $use_new_string_prefixes ) {
						$title = $this->_combobulate_wp550_archive_title(
							\_x( 'Month:', 'date archive title prefix', 'default' ),
							$title
						);
					} else {
						/* translators: Monthly archive title. 1: Month name and year */
						$title = $use_prefix ? sprintf( \__( 'Month: %s', 'default' ), $title ) : $title;
					}
				} elseif ( $this->is_day() ) {
					$title = \get_the_date( \_x( 'F j, Y', 'daily archives date format', 'default' ) );

					if ( $use_new_string_prefixes ) {
						$title = $this->_combobulate_wp550_archive_title(
							\_x( 'Day:', 'date archive title prefix', 'default' ),
							$title
						);
					} else {
						/* translators: Daily archive title. 1: Date */
						$title = $use_prefix ? sprintf( \__( 'Day: %s', 'default' ), $title ) : $title;
					}
				}
			} elseif ( \is_tax( 'post_format' ) ) {
				if ( \is_tax( 'post_format', 'post-format-aside' ) ) {
					$title = \_x( 'Asides', 'post format archive title', 'default' );
				} elseif ( \is_tax( 'post_format', 'post-format-gallery' ) ) {
					$title = \_x( 'Galleries', 'post format archive title', 'default' );
				} elseif ( \is_tax( 'post_format', 'post-format-image' ) ) {
					$title = \_x( 'Images', 'post format archive title', 'default' );
				} elseif ( \is_tax( 'post_format', 'post-format-video' ) ) {
					$title = \_x( 'Videos', 'post format archive title', 'default' );
				} elseif ( \is_tax( 'post_format', 'post-format-quote' ) ) {
					$title = \_x( 'Quotes', 'post format archive title', 'default' );
				} elseif ( \is_tax( 'post_format', 'post-format-link' ) ) {
					$title = \_x( 'Links', 'post format archive title', 'default' );
				} elseif ( \is_tax( 'post_format', 'post-format-status' ) ) {
					$title = \_x( 'Statuses', 'post format archive title', 'default' );
				} elseif ( \is_tax( 'post_format', 'post-format-audio' ) ) {
					$title = \_x( 'Audio', 'post format archive title', 'default' );
				} elseif ( \is_tax( 'post_format', 'post-format-chat' ) ) {
					$title = \_x( 'Chats', 'post format archive title', 'default' );
				}
			} elseif ( \is_post_type_archive() ) {
				$title = $this->get_generated_post_type_archive_title() ?: $this->get_tax_type_label( $_tax, false );

				if ( $use_new_string_prefixes ) {
					$title = $this->_combobulate_wp550_archive_title(
						\_x( 'Archives:', 'post type archive title prefix', 'default' ),
						$title
					);
				} else {
					/* translators: Post type archive title. 1: Post type name */
					$title = $use_prefix ? sprintf( \__( 'Archives: %s', 'default' ), $title ) : $title;
				}
			} elseif ( $this->is_tax() ) {
				$title = $this->get_generated_single_term_title( $term );
				$title = $use_prefix ? $this->prepend_tax_label_prefix( $title, $_tax ) : $title;
			} else {
				$title = \__( 'Archives', 'default' );
			}
		}

		/**
		 * Filters the archive title.
		 *
		 * @since 3.0.4
		 *
		 * @param string   $title Archive title to be displayed.
		 * @param \WP_Term $term  The term object.
		 */
		return \apply_filters( 'the_seo_framework_generated_archive_title', $title, $term );
	}

	/**
	 * Returns Post Title from ID.
	 *
	 * @NOTE Taken from WordPress core. Altered to work in the Admin area.
	 * @see WP Core single_post_title()
	 *
	 * @since 3.1.0
	 *
	 * @param int|\WP_Post $id The Post ID or post object.
	 * @return string The generated post title.
	 */
	public function get_generated_single_post_title( $id = 0 ) {

		//? Home queries can be tricky. Use get_the_real_ID to be certain.
		$_post = \get_post( $id ?: $this->get_the_real_ID() );
		$title = '';

		if ( isset( $_post->post_title ) ) {
			/**
			 * Filters the page title for a single post.
			 *
			 * @since WP Core 0.71
			 *
			 * @param string   $_post_title The single post page title.
			 * @param \WP_Post $_post       The current queried object as returned by get_queried_object().
			 */
			$title = \apply_filters( 'single_post_title', $_post->post_title, $_post );
		}

		return $title;
	}

	/**
	 * Fetches single term title.
	 *
	 * It can autodetermine the term; so, perform your checks prior calling.
	 *
	 * @NOTE Taken from WordPress core. Altered to work in the Admin area.
	 * @see WP Core single_term_title()
	 * TODO Term names may not be empty. But, if you insert illegal characters when updating/creating a term (e.g. `<tag>`)
	 *      the term name will be empty. When prefixes are added to the term (e.g. `Category:`), only that will be shown.
	 *
	 * @since 3.1.0
	 * @since 4.0.0 No longer redundantly tests the query, now only uses the term input or queried object.
	 * @since 4.0.2 Now asserts the correct tag taxonomy condition.
	 *
	 * @param null|\WP_Term $term The term name, required in the admin area.
	 * @return string The generated single term title.
	 */
	public function get_generated_single_term_title( $term = null ) {

		if ( \is_null( $term ) )
			$term = \get_queried_object();

		$term_name = '';

		if ( isset( $term->name ) ) {
			if ( 'category' === $term->taxonomy ) {
				/**
				 * Filter the category archive page title.
				 *
				 * @since WP Core 2.0.10
				 *
				 * @param string $term_name Category name for archive being displayed.
				 */
				$term_name = \apply_filters( 'single_cat_title', $term->name );
			} elseif ( 'post_tag' === $term->taxonomy ) {
				/**
				 * Filter the tag archive page title.
				 *
				 * @since WP Core 2.3.0
				 *
				 * @param string $term_name Tag name for archive being displayed.
				 */
				$term_name = \apply_filters( 'single_tag_title', $term->name );
			} else {
				/**
				 * Filter the custom taxonomy archive page title.
				 *
				 * @since WP Core 3.1.0
				 *
				 * @param string $term_name Term name for archive being displayed.
				 */
				$term_name = \apply_filters( 'single_term_title', $term->name );
			}
		}

		// Store the prefix sprintf at get_generated_archive_title() instead and set this on title capture failure?
		// We're working around a bug in WordPress here. This should be fixed inside WordPress! Forgo.
		// return strlen( $term_name ) ? $term_name : $this->get_static_untitled_title();
		return $term_name;
	}

	/**
	 * Fetches single term title.
	 *
	 * @NOTE Taken from WordPress core. Altered to work in the Admin area.
	 * @see WP Core post_type_archive_title()
	 *
	 * @since 3.1.0
	 *
	 * @param string $post_type The post type.
	 * @return string The generated post type archive title.
	 */
	public function get_generated_post_type_archive_title( $post_type = '' ) {

		$post_type = $post_type ?: \get_query_var( 'post_type' );

		if ( ! \is_post_type_archive( $post_type ) )
			return '';

		if ( \is_array( $post_type ) )
			$post_type = reset( $post_type );

		$post_type_obj = \get_post_type_object( $post_type );

		/**
		 * Filters the post type archive title.
		 *
		 * @since WP Core 3.1.0
		 *
		 * @param string $post_type_name Post type 'name' label.
		 * @param string $post_type      Post type.
		 */
		$title = \apply_filters( 'post_type_archive_title', $post_type_obj->labels->name, $post_type );

		return $title;
	}

	/**
	 * Returns untitled title.
	 *
	 * @since 3.1.0
	 *
	 * @return string The untitled title.
	 */
	public function get_static_untitled_title() {
		return \__( 'Untitled', 'default' );
	}

	/**
	 * Returns search title.
	 *
	 * @since 3.1.0
	 *
	 * @return string The generated search title, partially escaped.
	 */
	public function get_generated_search_query_title() {
		/* translators: %s: search phrase */
		return sprintf( \__( 'Search Results for &#8220;%s&#8221;', 'default' ), \get_search_query( true ) );
	}

	/**
	 * Returns 404 title.
	 *
	 * @since 2.6.0
	 * @since 3.1.0 No longer accepts parameters, nor has conditions.
	 *
	 * @return string The generated 404 title.
	 */
	public function get_static_404_title() {
		/**
		 * @since 2.5.2
		 * @param string $title The 404 title.
		 */
		return (string) \apply_filters( 'the_seo_framework_404_title', '404' );
	}

	/**
	 * Merges title branding, when allowed.
	 *
	 * @since 3.1.0
	 * @since 3.1.2 Added strict taxonomical check.
	 * @since 3.1.3 Fixed conditional logic.
	 * @uses $this->get_title_branding_from_query()
	 * @uses $this->get_title_branding_from_args()
	 *
	 * @param string     $title The title. Passed by reference.
	 * @param array|null $args  The query arguments. Accepts 'id' and 'taxonomy'.
	 *                          Leave null to autodetermine query.
	 */
	public function merge_title_branding( &$title, $args = null ) {

		if ( null === $args ) {
			$data = $this->get_title_branding_from_query();
		} else {
			$this->fix_generation_args( $args );
			$data = $this->get_title_branding_from_args( $args );
		}

		$title    = trim( $title );
		$addition = trim( $data['addition'] );

		if ( $addition && $title ) {
			$sep = $this->get_title_separator();

			if ( 'left' === $data['seplocation'] ) {
				$title = "$addition $sep $title";
			} else {
				$title = "$title $sep $addition";
			}
		}
	}

	/**
	 * Returns the addition and seplocation from query.
	 *
	 * @since 3.2.2
	 * @see $this->merge_title_branding();
	 *
	 * @return array { 'addition', 'seplocation' }
	 */
	protected function get_title_branding_from_query() {

		if ( $this->is_real_front_page() ) {
			$addition    = $this->get_home_title_additions();
			$seplocation = $this->get_home_title_seplocation();
		} else {
			$addition    = $this->get_blogname();
			$seplocation = $this->get_title_seplocation();
		}

		return compact( 'addition', 'seplocation' );
	}

	/**
	 * Returns the addition and seplocation from arguments.
	 *
	 * @since 3.2.2
	 * @see $this->merge_title_branding();
	 *
	 * @param array $args The query arguments. Accepts 'id' and 'taxonomy'.
	 * @return array { 'addition', 'seplocation' }
	 */
	protected function get_title_branding_from_args( array $args ) {

		if ( ! $args['taxonomy'] && $this->is_real_front_page_by_id( $args['id'] ) ) {
			$addition    = $this->get_home_title_additions();
			$seplocation = $this->get_home_title_seplocation();
		} else {
			$addition    = $this->get_blogname();
			$seplocation = $this->get_title_seplocation();
		}

		return compact( 'addition', 'seplocation' );
	}

	/**
	 * Merges pagination with the title, if paginated.
	 *
	 * @since 3.1.0
	 * @since 3.1.2 Now uses the registered default translation.
	 *
	 * @param string $title The title. Passed by reference.
	 */
	public function merge_title_pagination( &$title ) {

		$page  = $this->page();
		$paged = $this->paged();

		if ( $paged >= 2 || $page >= 2 ) {
			$sep = $this->get_title_separator();

			// phpcs:ignore, WordPress.WP.I18n -- WP didn't add translator code either.
			$paging = sprintf( \__( 'Page %s', 'default' ), max( $paged, $page ) );

			if ( \is_rtl() ) {
				$title = "$paging $sep $title";
			} else {
				$title = "$title $sep $paging";
			}
		}
	}

	/**
	 * Merges title protection prefixes.
	 *
	 * @since 3.1.0
	 * @since 3.1.2 Added strict taxonomical checks for title protection.
	 * @since 3.1.3 Fixed conditional logic.
	 * @see $this->merge_title_prefixes()
	 *
	 * @param string     $title The title. Passed by reference.
	 * @param array|null $args  The query arguments. Accepts 'id' and 'taxonomy'.
	 *                          Leave null to autodetermine query.
	 * @return void
	 */
	public function merge_title_protection( &$title, $args = null ) {

		if ( null === $args ) {
			$id  = $this->get_the_real_ID();
			$tax = $this->get_current_taxonomy();
		} else {
			$this->fix_generation_args( $args );
			$id  = $args['id'];
			$tax = $args['taxonomy'];
		}

		if ( $tax ) return;

		$post = $id ? \get_post( $id ) : null;

		if ( isset( $post->post_password ) && '' !== $post->post_password ) {
			/**
			 * Filters the text prepended to the post title of private posts.
			 *
			 * The filter is only applied on the front end.
			 *
			 * @since WP Core 2.8.0
			 *
			 * @param string  $prepend Text displayed before the post title.
			 *                         Default 'Private: %s'.
			 * @param WP_Post $post    Current post object.
			 */
			// phpcs:ignore, WordPress.WP.I18n -- WordPress doesn't have a comment, either.
			$protected_title_format = (string) \apply_filters( 'protected_title_format', \__( 'Protected: %s', 'default' ), $post );
			$title                  = sprintf( $protected_title_format, $title );
		} elseif ( isset( $post->post_status ) && 'private' === $post->post_status ) {
			/**
			 * Filters the text prepended to the post title of private posts.
			 *
			 * The filter is only applied on the front end.
			 *
			 * @since WP Core 2.8.0
			 *
			 * @param string  $prepend Text displayed before the post title.
			 *                         Default 'Private: %s'.
			 * @param WP_Post $post    Current post object.
			 */
			// phpcs:ignore, WordPress.WP.I18n -- WordPress doesn't have a comment, either.
			$private_title_format = (string) \apply_filters( 'private_title_format', \__( 'Private: %s', 'default' ), $post );
			$title                = sprintf( $private_title_format, $title );
		}
	}

	/**
	 * Gets Title Separator.
	 * Memoizes the return value.
	 *
	 * @since 2.6.0
	 *
	 * @return string The Separator, unescaped.
	 */
	public function get_title_separator() {
		static $sep;
		/**
		 * @since 2.3.9
		 * @param string $eparator The title separator
		 */
		return isset( $sep )
			? $sep
			: $sep = (string) \apply_filters( 'the_seo_framework_title_separator', $this->get_separator( 'title' ) );
	}

	/**
	 * Returns title separator location.
	 *
	 * @since 2.6.0
	 * @since 3.1.0 1. Removed the first $seplocation parameter.
	 *              2. The first parameter is now $home
	 *              3. Removed caching.
	 *              4. Removed filters.
	 * @since 4.0.0 The homepage option's return value is now reversed from expected.
	 *
	 * @param bool $home The home separator location.
	 * @return string The separator location.
	 */
	public function get_title_seplocation( $home = false ) {
		return $home ? $this->get_option( 'home_title_location' ) : $this->get_option( 'title_location' );
	}

	/**
	 * Gets Title Seplocation for the homepage.
	 *
	 * @since 2.6.0
	 * @since 3.1.0 Removed first parameter.
	 * @since 4.0.0 Left is now right, and right is now left.
	 *
	 * @return string The Seplocation for the homepage.
	 */
	public function get_home_title_seplocation() {
		return $this->get_title_seplocation( true );
	}

	/**
	 * Prepends the taxonomy label to the title.
	 *
	 * @since 4.1.0
	 * @since 4.1.2 Now supports WP 5.5 archive titles.
	 *
	 * @param string $title    The title to prepend taxonomy label to.
	 * @param string $taxonomy The taxonomy to get label from.
	 * @return string The title with possibly prepended tax-label.
	 */
	public function prepend_tax_label_prefix( $title, $taxonomy ) {

		$prefix = $this->get_tax_type_label( $taxonomy ) ?: '';

		if ( $prefix ) {
			$use_new_string_prefixes = version_compare( \get_bloginfo( 'version' ), '5.5', '>=' );

			if ( $use_new_string_prefixes ) {
				$title = $this->_combobulate_wp550_archive_title(
					/* translators: %s: Taxonomy singular name. */
					sprintf( \_x( '%s:', 'taxonomy term archive title prefix', 'default' ), $prefix ),
					$title
				);
			} else {
				$title = sprintf(
					/* translators: Taxonomy term archive title. 1: Taxonomy singular name, 2: Current taxonomy term. */
					\__( '%1$s: %2$s', 'default' ),
					$prefix,
					$title
				);
			}
		}

		return $title;
	}

	/**
	 * Determines whether to add or remove title protection prefixes.
	 *
	 * @since 3.2.4
	 * NOTE: This does not guarantee that protection is to be added. Only that it will be considered. Bad method name.
	 * @see $this->merge_title_protection()
	 *
	 * @param array|null $args The query arguments. Accepts 'id' and 'taxonomy'.
	 *                         Leave null to autodetermine query.
	 * @return bool True when prefixes are allowed.
	 */
	public function use_title_protection( $args = null ) {

		if ( null === $args ) {
			$use = $this->is_singular();
		} else {
			$this->fix_generation_args( $args ); // redundant since we only check for a non-autofillable value... use empty( $args['tax..] ) instead?
			$use = $args && ! $args['taxonomy'];
		}

		return $use;
	}

	/**
	 * Determines whether to add or remove title pagination additions.
	 *
	 * @since 3.2.4
	 * NOTE: This does not guarantee that pagination is to be added. Only that it will be considered. Bad method name.
	 * @see $this->merge_title_pagination()
	 *
	 * @param array|null $args The query arguments. Accepts 'id' and 'taxonomy'.
	 *                         Leave null to autodetermine query.
	 * @return bool True when additions are allowed.
	 */
	public function use_title_pagination( $args = null ) {

		//? Only add pagination if the query is autodetermined, and on a real page.
		if ( null === $args ) {
			if ( $this->is_404() || \is_admin() ) {
				$use = false;
			} else {
				$use = true;
			}
		} else {
			$use = false;
		}

		return $use;
	}

	/**
	 * Determines whether to add or remove title branding additions.
	 *
	 * @since 3.1.0
	 * @since 3.1.2 : 1. Added filter.
	 *                2. Added strict taxonomical check.
	 * @since 3.2.2 Now differentiates from query and parameter input.
	 * @since 4.1.0 Added the second $social parameter.
	 * @see $this->merge_title_branding()
	 * @uses $this->use_title_branding_from_query()
	 * @uses $this->use_title_branding_from_args()
	 *
	 * @param array|null $args  The query arguments. Accepts 'id' and 'taxonomy'.
	 *                           Leave null to autodetermine query.
	 * @param bool       $social Whether the title is meant for social display.
	 * @return bool True when additions are allowed.
	 */
	public function use_title_branding( $args = null, $social = false ) {

		$use = true;

		if ( $social ) {
			$use = ! $this->get_option( 'social_title_rem_additions' );
		}

		// When social titles tend to use it, evaluate again from general title settings.
		if ( $use ) {
			if ( null === $args ) {
				$use = $this->use_title_branding_from_query();
			} else {
				$this->fix_generation_args( $args );
				$use = $this->use_title_branding_from_args( $args );
			}
		}

		/**
		 * @since 3.1.2
		 * @since 4.1.0 Added the third $social parameter.
		 * @param string     $use    Whether to use branding.
		 * @param array|null $args   The query arguments. Contains 'id' and 'taxonomy'.
		 *                           Is null when query is autodetermined.
		 * @param bool       $social Whether the title is meant for social display.
		 */
		return \apply_filters_ref_array( 'the_seo_framework_use_title_branding', [ $use, $args, $social ] );
	}

	/**
	 * Determines whether to add or remove title branding additions in the query.
	 *
	 * @since 3.2.2
	 * @since 4.0.0 Added use_taxonomical_title_branding() check.
	 * @since 4.0.2 Removed contemned \is_post_type_archive() check for taxonomical branding.
	 * @see $this->use_title_branding()
	 *
	 * @return bool
	 */
	protected function use_title_branding_from_query() {

		if ( $this->is_real_front_page() ) {
			$use = $this->use_home_page_title_tagline();
		} elseif ( $this->is_singular() ) {
			$use = $this->use_singular_title_branding();
		} elseif ( $this->is_term_meta_capable() ) {
			$use = $this->use_taxonomical_title_branding();
		} else {
			$use = ! $this->get_option( 'title_rem_additions' );
		}

		return $use;
	}

	/**
	 * Determines whether to add or remove title branding additions from provided arguments.
	 *
	 * @since 3.2.2
	 * @since 4.0.0 Added use_taxonomical_title_branding() check.
	 * @see $this->use_title_branding()
	 *
	 * @param array $args The query arguments. Accepts 'id' and 'taxonomy'.
	 * @return bool
	 */
	protected function use_title_branding_from_args( array $args ) {

		if ( $args['taxonomy'] ) {
			$use = $this->use_taxonomical_title_branding( $args['id'] );
		} else {
			if ( $this->is_real_front_page_by_id( $args['id'] ) ) {
				$use = $this->use_home_page_title_tagline();
			} else {
				$use = $this->use_singular_title_branding( $args['id'] );
			}
		}

		return $use;
	}

	/**
	 * Determines whether to use the autogenerated archive title prefix or not.
	 *
	 * @since 3.1.0
	 * @since 4.0.5 1: Added first parameter `$term`.
	 *              2: Added filter.
	 *
	 * @param \WP_Term|\WP_User|null $term The Term object. Leave null to autodermine query.
	 * @return bool
	 */
	public function use_generated_archive_prefix( $term = null ) {

		$term = isset( $term ) ? $term : \get_queried_object();
		$use  = ! $this->get_option( 'title_rem_prefixes' );

		/**
		 * @since 4.0.5
		 * @param string            $use  Whether to use branding.
		 * @param \WP_Term|\WP_User $term The current term.
		 */
		return \apply_filters_ref_array( 'the_seo_framework_use_archive_prefix', [ $use, $term ] );
	}

	/**
	 * Determines whether to add homepage tagline.
	 *
	 * @since 2.6.0
	 * @since 3.0.4 Now checks for `$this->get_home_title_additions()`.
	 *
	 * @return bool
	 */
	public function use_home_page_title_tagline() {
		return $this->get_option( 'homepage_tagline' ) && $this->get_home_title_additions();
	}

	/**
	 * Determines whether to add the title tagline for the post.
	 *
	 * @since 3.1.0
	 *
	 * @param int $id The post ID. Optional.
	 * @return bool
	 */
	public function use_singular_title_branding( $id = 0 ) {
		return ! $this->get_post_meta_item( '_tsf_title_no_blogname', $id ) && ! $this->get_option( 'title_rem_additions' );
	}

	/**
	 * Determines whether to add the title tagline for the term.
	 *
	 * @since 4.0.0
	 *
	 * @param int $id The term ID. Optional.
	 * @return bool
	 */
	public function use_taxonomical_title_branding( $id = 0 ) {
		return ! $this->get_term_meta_item( 'title_no_blog_name', $id ) && ! $this->get_option( 'title_rem_additions' );
	}

	/**
	 * Returns the homepage additions (tagline) from option or bloginfo, when set.
	 * Memoizes the return value.
	 *
	 * @since 4.1.0
	 * @uses $this->get_blogdescription(), that method already trims.
	 *
	 * @return string The trimmed tagline.
	 */
	public function get_home_title_additions() {
		static $cache;
		return isset( $cache ) ? $cache : $cache = $this->s_title_raw(
			trim( $this->get_option( 'homepage_title_tagline' ) )
			?: $this->get_blogdescription()
			?: ''
		);
	}
}
