<?php
/**
 * @package Polylang-Pro
 */

use Tribe\Events\Views\V2\Rest_Endpoint;
use Tribe__Events__Main as TEC;
use Tribe__Events__Venue as Venue;
use Tribe__Events__Organizer as Organizer;

/**
 * A class to manage integration with the Event Calendar
 * Version tested: 5.7.0
 *
 * @since 2.2
 */
class PLL_TEC {
	/**
	 * Current language (used to filter the content).
	 *
	 * @var PLL_Language|null
	 */
	protected $curlang;

	/**
	 * The main Polylang object.
	 *
	 * @var PLL_Settings|PLL_Admin|PLL_REST_Request|PLL_Frontend
	 */
	protected $polylang;

	/**
	 * Links Model for translating slugs.
	 *
	 * @var PLL_Translate_Slugs_Model|null
	 */
	protected $slugs_model;

	/**
	 * @var PLL_Admin_Links|null
	 */
	protected $links;

	/**
	 * Cache for the method `is_tec_rest_request()`.
	 *
	 * @var mixed[]
	 */
	protected $is_tec_rest_request = array();

	/**
	 * The list of IDs of translatable slugs dedicated to TEC.
	 *
	 * @var string[]
	 */
	protected $translatable_slug_ids = array();

	/**
	 * The list of post metas to synchronize.
	 *
	 * @var string[]
	 */
	protected static $metas;

	/**
	 * Initializes filters and actions.
	 *
	 * @since 2.2
	 *
	 * @param PLL_Settings|PLL_Admin|PLL_REST_Request|PLL_Frontend $polylang The main Polylang object.
	 * @return void
	 */
	public function init( $polylang ) {
		add_filter( 'pll_get_taxonomies', array( $this, 'translate_taxonomies' ), 10, 2 );
		add_filter( 'pll_get_post_types', array( $this, 'translate_types' ), 10, 2 );

		if ( ! $polylang->model->has_languages() ) {
			// Run only if there is at least one language.
			return;
		}

		$this->polylang              = $polylang;
		$this->curlang               = null;
		$this->slugs_model           = ! empty( $polylang->translate_slugs ) && ! empty( $polylang->translate_slugs->slugs_model ) ? $polylang->translate_slugs->slugs_model : null;
		$this->is_tec_rest_request   = array();
		$this->translatable_slug_ids = array();

		if ( $polylang->links instanceof PLL_Admin_Links ) {
			$this->links = $polylang->links;

			add_filter( 'use_block_editor_for_post', array( $this, 'new_event_translation' ), 5000 ); // Setup TEC translation filters after duplication.
		}

		add_action( 'save_post_' . Venue::POSTTYPE, array( $this, 'set_language' ), 10, 3 );
		add_action( 'save_post_' . Organizer::POSTTYPE, array( $this, 'set_language' ), 10, 3 );

		$tec = TEC::instance();

		if ( empty( $polylang->options['force_lang'] ) ) {
			add_action( 'pll_language_defined', array( $this, 'fix_date_translations' ) );
		}

		self::$metas = array_merge( $tec->metaTags, $tec->venueTags, $tec->organizerTags, array( '_VenueShowMap', '_VenueShowMapLink' ) );

		add_action( 'pre_get_posts', array( $this, 'pre_get_posts' ), 60 ); // After `Tribe__Events__Query->pre_get_posts()`.

		add_filter( 'pll_copy_post_metas', array( $this, 'copy_post_metas' ) );
		add_filter( 'pll_translate_post_meta', array( $this, 'translate_meta' ), 10, 3 );

		// Translate links with translated slugs
		add_action( 'init', array( $this, 'reset_slugs' ), 11 ); // Just after `Tribe__Events__Main->init()`.
		add_filter( 'register_taxonomy_args', array( $this, 'register_taxonomy_args' ), 10, 2 );
		add_filter( 'tribe_events_get_link', array( $this, 'get_link' ) );
		add_filter( 'pll_get_archive_url', array( $this, 'pll_get_archive_url' ), 10, 2 );
		add_filter( 'pll_term_link', array( $this, 'filter_tec_term_link' ), 5, 3 ); // Before `PLL_Translate_Slugs->pll_term_link()`.
		add_filter( 'pll_translated_slugs', array( $this, 'pll_translated_slugs' ), 10, 3 );
		add_filter( 'pll_sanitize_string_translation', array( $this, 'sanitize_string_translation' ), 10, 2 );
		add_filter( 'tribe_events_rewrite_i18n_slugs_raw', array( $this, 'rewrite_slugs' ) );

		// Options to translate.
		$keys = array(
			'dateWithYearFormat'    => 1,
			'dateWithoutYearFormat' => 1,
			'monthAndYearFormat'    => 1,
			'dateTimeSeparator'     => 1,
			'timeRangeSeparator'    => 1,
			'tribeEventsBeforeHTML' => 1,
			'tribeEventsAfterHTML'  => 1,
		);

		$args = array(
			'context'           => 'The Events Calendar',
			'sanitize_callback' => array( $this, 'sanitize_strings' ),
		);

		new PLL_Translate_Option( 'tribe_events_calendar_options', $keys, $args );

		// TEC views V2.
		add_filter( 'tribe_events_rewrite_i18n_domains', '__return_empty_array', 10000 ); // No i18n domains, no translations to deal with.
		add_filter( 'tribe_events_rewrite_i18n_slugs', array( $this, 'fix_escaped_dashes_in_slugs' ), 10, 2 );

		add_filter( 'tribe_events_category_slug', array( $this, 'get_category_slug' ) );
		add_filter( 'tribe_events_tag_slug', array( $this, 'get_tag_slug' ) );

		add_filter( 'tribe_events_views_v2_endpoint_url', array( $this, 'add_missing_lang_to_rest_url' ) );
		add_filter( 'locale', array( $this, 'filter_locale_for_rest' ), 5 );

		add_filter( 'tribe_events_views_v2_publicly_visible_views_query_args', array( $this, 'add_language_to_publicly_visible_views_query_args' ), 5 );
		add_filter( 'tribe_events_views_v2_view_template_vars', array( $this, 'translate_widget_view_more_link' ), 5 );
		add_filter( 'tribe_rewrite_parse_query_vars', array( $this, 'force_language_on_tec_parse_query_vars' ), 10, 3 );
		add_filter( 'tribe_events_views_v2_url_query_args', array( $this, 'add_missing_lang_to_query_arg' ) );
		add_filter( 'tribe_rewrite_pre_canonical_url', array( $this, 'add_missing_lang_to_non_canonical_url' ), 10, 2 );
		add_filter( 'tribe_rewrite_pre_canonical_url', array( $this, 'remove_name_arg_from_non_canonical_url' ), 10, 2 );

		add_filter( 'tribe_rewrite_canonical_url', array( $this, 'fix_language_in_canonical_url' ), 5, 2 );
		add_filter( 'tribe_rewrite_canonical_url', array( $this, 'translate_canonical_url' ), 5, 2 );
	}

	/**
	 * Language and translation management for taxonomies.
	 *
	 * @since 2.2
	 *
	 * @param string[] $taxonomies List of taxonomy names for which Polylang manages language and translations.
	 * @param bool     $hide       True when displaying the list in Polylang settings.
	 * @return string[] List of taxonomy names for which Polylang manages language and translations.
	 */
	public function translate_taxonomies( $taxonomies, $hide ) {
		// Hide from Polylang settings
		return $hide ? array_diff( $taxonomies, array( TEC::TAXONOMY ) ) : array_merge( $taxonomies, array( TEC::TAXONOMY ) );
	}

	/**
	 * Language and translation management for custom post types.
	 *
	 * @since 2.2
	 *
	 * @param string[] $types List of post type names for which Polylang manages language and translations.
	 * @param bool     $hide  True when displaying the list in Polylang settings.
	 * @return string[] List of post type names for which Polylang manages language and translations.
	 */
	public function translate_types( $types, $hide ) {
		$tec_types = array( TEC::POSTTYPE, TEC::VENUE_POST_TYPE, TEC::ORGANIZER_POST_TYPE );
		return $hide ? array_diff( $types, $tec_types ) : array_merge( $types, $tec_types );
	}

	/**
	 * Save the language of Venues and Organizers.
	 * Needed when they are created from the Event form.
	 *
	 * @since 2.2
	 *
	 * @param int     $post_id Post id.
	 * @param WP_Post $post    Post object.
	 * @param bool    $update  Whether it is an update or not.
	 * @return void
	 */
	public function set_language( $post_id, $post, $update ) {
		if ( $update || ! isset( $_POST['post_lang_choice'] ) ) {
			return;
		}

		$post_type_object = get_post_type_object( $post->post_type );

		if ( ! $post_type_object || ! current_user_can( $post_type_object->cap->create_posts ) ) {
			return;
		}

		check_admin_referer( 'pll_language', '_pll_nonce' );
		$lang = $this->polylang->model->get_language( sanitize_key( $_POST['post_lang_choice'] ) );

		if ( ! $lang ) {
			return;
		}

		$this->polylang->model->post->set_language( $post_id, $lang );
	}

	/**
	 * Once the language is set from content, this resets all the date-related translations in TEC to the current lang.
	 * In `Tribe__Date_Utils`, TEC stores these translations in static private properties before the language is set in
	 * PLL. Then these translations are use later, AFTER the language is set in PLL, leading to views exploding due to
	 * array keys not being set.
	 * The filter 'tribe_events_get_days_of_week' can't be used because it doesn't include the function's arg `$format`.
	 *
	 * @since 3.1
	 * @see   tribe_events_get_days_of_week()
	 *
	 * @return void
	 */
	public function fix_date_translations() {
		$properties = array(
			'localized_months_full',
			'localized_months_short',
			'localized_weekdays',
			'localized_months',
		);

		foreach ( $properties as $property ) {
			$property = new ReflectionProperty( Tribe__Date_Utils::class, $property );
			version_compare( PHP_VERSION, '8.1', '<' ) && $property->setAccessible( true );
			$property->setValue( array() );
		}

		TEC::instance()->setup_l10n_strings();
	}

	/**
	 * Adds TEC meta duplication filters when creating a new event translation.
	 *
	 * Hooked to `use_block_editor_for_post` because at this point all required global variables are defined,
	 * allowing `get_data_from_new_post_translation_request()` to work correctly.
	 *
	 * @since 3.7.8
	 *
	 * @param bool $use_block_editor Whether to use the block editor.
	 * @return bool Unmodified $use_block_editor value.
	 */
	public function new_event_translation( $use_block_editor ) {
		if ( empty( $this->links ) ) {
			return $use_block_editor;
		}

		$data = $this->links->get_data_from_new_post_translation_request();
		if ( empty( $data ) ) {
			return $use_block_editor;
		}

		// Add filters to provide default values for event fields from the source event.
		foreach ( self::$metas as $meta ) {
			$filter = str_replace( array( '_Event', '_Organizer', '_Venue' ), array( '', 'Organizer', 'Venue' ), $meta );
			add_filter( 'tribe_get_meta_default_value_' . $filter, array( $this, 'copy_event_meta' ), 10, 4 );
		}

		add_filter( 'tribe_display_event_linked_post_dropdown_id', array( $this, 'translate_linked_post' ) );

		return $use_block_editor;
	}

	/**
	 * Populates default event metas for a newly created event translation
	 *
	 * @since 2.2
	 *
	 * @param mixed  $value  Meta value.
	 * @param int    $id     Post id.
	 * @param string $meta   Meta key.
	 * @param bool   $single Whether to return a single value.
	 * @return mixed
	 */
	public function copy_event_meta( $value, $id, $meta, $single ) {
		if ( ! empty( $_GET['from_post'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
			$value = get_post_meta( (int) $_GET['from_post'], $meta, $single ); // phpcs:ignore WordPress.Security.NonceVerification
		}
		return $value;
	}

	/**
	 * Populates default values for venues and organizers for a newly created event translation.
	 *
	 * @since 2.2
	 *
	 * @param array $posts Array of linked posts.
	 * @return array
	 */
	public function translate_linked_post( $posts ) {
		if ( empty( $posts ) || empty( $this->links ) ) {
			return $posts;
		}

		$data = $this->links->get_data_from_new_post_translation_request();

		if ( empty( $data ) ) {
			return $posts;
		}

		$lang       = $data['new_lang']->slug;
		$post_metas = ! empty( $this->polylang->sync ) && ! empty( $this->polylang->sync->post_metas ) ? $this->polylang->sync->post_metas : false;

		foreach ( $posts as $key => $post_id ) {
			$tr_id = pll_get_post( $post_id, $lang );

			if ( ! empty( $tr_id ) ) {
				$posts[ $key ] = $tr_id;
				continue;
			}

			// If the translated venue or organizer doesn't exist, create it.
			$post = get_post( $post_id, ARRAY_A ); // Output as an array for `wp_insert_post()`.

			if ( empty( $post ) ) {
				// `null` value.
				continue;
			}

			unset( $post['ID'] );

			$tr_id = wp_insert_post( wp_slash( $post ) );

			if ( ! is_int( $tr_id ) ) {
				// `WP_Error` value.
				continue;
			}

			$translations          = pll_get_post_translations( $post_id );
			$translations[ $lang ] = $tr_id;

			pll_set_post_language( $tr_id, $lang );
			pll_save_post_translations( $translations );

			if ( ! empty( $post_metas ) ) {
				$post_metas->copy( $post_id, $tr_id, $lang );
			}

			$posts[ $key ] = $tr_id;
		}

		return $posts;
	}

	/**
	 * Removes date filters when searching for untranslated events in the metabox autocomplete field
	 *
	 * @since 2.2.8
	 *
	 * @return void
	 */
	public function pre_get_posts() {
		if ( wp_doing_ajax() && isset( $_GET['action'] ) && 'pll_posts_not_translated' === $_GET['action'] ) { // phpcs:ignore WordPress.Security.NonceVerification
			// See Tribe__Events__Query::pre_get_posts() when should_remove_date_filters() returns true
			remove_filter( 'posts_where', array( 'Tribe__Events__Query', 'posts_where' ) );
			remove_filter( 'posts_fields', array( 'Tribe__Events__Query', 'posts_fields' ) );
			remove_filter( 'posts_orderby', array( 'Tribe__Events__Query', 'posts_orderby' ) );
		}
	}

	/**
	 * Synchronize event metas.
	 *
	 * @since 2.2
	 *
	 * @param array $metas Custom fields to copy or synchronize.
	 * @return array
	 */
	public function copy_post_metas( $metas ) {
		return array_merge( $metas, self::$metas );
	}

	/**
	 * Translate venues and organizers before they are copied or synchronized.
	 *
	 * @since 2.3
	 *
	 * @param mixed  $value Meta value.
	 * @param string $key   Meta key.
	 * @param string $lang  Language of target.
	 * @return mixed
	 */
	public function translate_meta( $value, $key, $lang ) {
		if ( ( '_EventVenueID' !== $key && '_EventOrganizerID' !== $key ) ) {
			return $value;
		}

		$tr_value = pll_get_post( $value, $lang );
		return $tr_value ?: $value;
	}

	/**
	 * Resets all TEC translated slugs to an English value as the TEC slug translation system
	 * does not work in a multilingual context (TEC 4.4.5 + WP 4.7.3).
	 *
	 * @since 2.2
	 *
	 * @return void
	 */
	public function reset_slugs() {
		$tec = TEC::instance();

		foreach ( $this->get_slugs_to_reset() as $key => $slug ) {
			$tec->$key = $slug;
		}

		// Those are deprecated since TEC 4.0 and should not appear in the list of translatable strings anymore.
		$tec->taxRewriteSlug = $tec->rewriteSlug . '/category';
		$tec->tagRewriteSlug = $tec->rewriteSlug . '/tag';
	}

	/**
	 * Resets the category base rewrite slug in taxonomy.
	 *
	 * @since 2.2
	 *
	 * @param array  $args     Array of arguments for registering a taxonomy.
	 * @param string $taxonomy Taxonomy key.
	 * @return array
	 */
	public function register_taxonomy_args( $args, $taxonomy ) {
		if ( TEC::TAXONOMY === $taxonomy && is_array( $args['rewrite'] ) ) {
			$args['rewrite']['slug'] = TEC::instance()->rewriteSlug . '/category';
		}

		return $args;
	}

	/**
	 * Filters the links to add the language code.
	 *
	 * @since 2.2
	 *
	 * @param string $link Link generated by The Events Calendar.
	 * @return string
	 */
	public function get_link( $link ) {
		$curlang = $this->get_curlang();

		if ( empty( $curlang ) || empty( $this->slugs_model ) ) {
			return $link;
		}

		$link = $this->polylang->links_model->add_language_to_link( $link, $curlang );

		foreach ( $this->get_translatable_slug_ids() as $slug_id ) {
			$link = $this->slugs_model->translate_slug( $link, $curlang, $slug_id );
		}

		return $link;
	}

	/**
	 * Translates slugs in the language switcher.
	 *
	 * @since 2.2
	 *
	 * @param string       $url      Url in the language switcher.
	 * @param PLL_Language $language Language object.
	 * @return string Modified url.
	 */
	public function pll_get_archive_url( $url, $language ) {
		if ( empty( $this->slugs_model ) || ! is_post_type_archive( TEC::POSTTYPE ) ) {
			return $url;
		}

		foreach ( $this->get_translatable_slug_ids() as $slug_id ) {
			$url = $this->slugs_model->switch_translated_slug( $url, $language, $slug_id );
		}

		return $url;
	}

	/**
	 * Modifies term links for the taxonomies used by TEC.
	 * In `PLL_TEC->reset_slugs()` we don't include the full taxonomy slug (`Tribe__Events__Main->taxRewriteSlug`) in
	 * our translated strings, as it is deprecated in TEC 4.0 and also a composite of
	 * `{post type archive slug}/{tax base slug}`. This explains why this method is needed.
	 *
	 * @since 3.1
	 * @see   PLL_TEC->reset_slugs()
	 * @see   PLL_TEC->pll_translated_slugs()
	 *
	 * @param  string       $url  The term link.
	 * @param  PLL_Language $lang The term language.
	 * @param  WP_Term      $term The term object.
	 * @return string
	 */
	public function filter_tec_term_link( $url, $lang, $term ) {
		if ( empty( $this->slugs_model ) || TEC::TAXONOMY !== $term->taxonomy ) {
			return $url;
		}

		$url = $this->slugs_model->translate_slug( $url, $lang, 'archive_' . TEC::POSTTYPE );
		return $this->slugs_model->translate_slug( $url, $lang, 'tribe_category' );
	}

	/**
	 * Fixes the events slug in translatable slugs.
	 * Translates other TEC slugs.
	 *
	 * @since 2.2
	 *
	 * @param array        $slugs    Translated slugs.
	 * @param PLL_Language $language Language object.
	 * @param PLL_MO       $mo       Strings translations object.
	 * @return array
	 */
	public function pll_translated_slugs( $slugs, $language, &$mo ) {
		/**
		 * In `PLL_TEC->reset_slugs()` we don't include the full taxonomy slug (`Tribe__Events__Main->taxRewriteSlug`) in
		 * our translated strings, as it is deprecated in TEC 4.0 and also a composite of
		 * `{post type archive slug}/{tax base slug}`. This is why we unset `$slugs[ TEC::TAXONOMY ]` here.
		 */
		unset( $slugs[ 'archive_' . TEC::POSTTYPE ]['hide'], $slugs[ TEC::TAXONOMY ] );

		$slug = TEC::instance()->getRewriteSlug();

		$slugs[ 'archive_' . TEC::POSTTYPE ]['slug'] = $slug;
		$tr_slug = $mo->translate( $slug );
		$slugs[ 'archive_' . TEC::POSTTYPE ]['translations'][ $language->slug ] = empty( $tr_slug ) ? $slug : $tr_slug;

		foreach ( $this->get_slugs_to_reset() as $slug ) {
			$slugs[ 'tribe_' . $slug ]['slug'] = $slug;
			$tr_slug = $mo->translate( $slug );
			$slugs[ 'tribe_' . $slug ]['translations'][ $language->slug ] = empty( $tr_slug ) ? $slug : $tr_slug;
		}

		return $slugs;
	}

	/**
	 * Performs the sanitization ( before saving in DB ) of slugs translations
	 * The Events Calendar does not accept accents, but let's accept slashes for the event category slug
	 *
	 * @since 1.9
	 *
	 * @param string $translation Translation to sanitize.
	 * @param string $name        Unique name for the string.
	 * @return string
	 */
	public function sanitize_string_translation( $translation, $name ) {
		if ( 'slug_archive_tribe_events' === $name || 0 === strpos( $name, 'slug_tribe_' ) ) {
			$slugs = explode( '/', $translation );
			$slugs = array_map( 'sanitize_title', $slugs );
			return implode( '/', $slugs );
		}
		return $translation;
	}

	/**
	 * Add translated slugs to specific TEC rewrite rules.
	 *
	 * @since 2.2
	 *
	 * @param array $bases Array of arrays of rewrite base slugs.
	 * @return array
	 */
	public function rewrite_slugs( $bases ) {
		$translatable_slugs = $this->get_translatable_slugs();

		if ( empty( $translatable_slugs ) ) {
			return $bases;
		}

		foreach ( $bases as $type => $base ) {
			$default_slug = reset( $base );

			foreach ( $translatable_slugs as $slugs ) {
				if ( $slugs['slug'] === $default_slug ) {
					$bases[ $type ] = array_unique( array_merge( array( $default_slug ), $slugs['translations'] ) );
					break;
				}
			}
		}

		return $bases;
	}

	/**
	 * Translated strings must be sanitized the same way The Events Calendar does before they are saved.
	 * All are of validation_type 'html'.
	 *
	 * @since 2.2
	 *
	 * @param string $translation Translated string.
	 * @param string $name        String name.
	 * @param string $context     String context.
	 * @return string Sanitized translation.
	 */
	public function sanitize_strings( $translation, $name, $context ) {
		if ( 'The Events Calendar' === $context ) {
			$translation = balanceTags( $translation );
		}

		return $translation;
	}

	/**
	 * Filters TEC's base slugs to unescape dashes.
	 *
	 * If `$method` is 'regex', `Tribe__Events__Rewrite->get_bases()` will use `preg_quote()` to get
	 * its slugs ready as regex patterns. However, `-` characters are valid in this context and
	 * should not be escaped (reminder: they come from PLL's string translations).
	 *
	 * @since 3.1
	 * @see   $this->rewrite_slugs()
	 *
	 * @param  string[] $bases  An array of rewrite bases that have been generated.
	 * @param  string   $method The method that's being used to generate the bases; defaults to `regex`.
	 * @return string[]
	 */
	public function fix_escaped_dashes_in_slugs( $bases, $method ) {
		if ( 'regex' !== $method ) {
			return $bases;
		}

		return array_map(
			function ( $base ) {
				return str_replace( '\\-', '-', $base );
			},
			$bases
		);
	}

	/**
	 * Filters the string to be used as the taxonomy slug.
	 * This replaces TEC's translated category slug by the untranslated one, as it is returned by the public method
	 * `Tribe__Events__Main->get_category_slug()`.
	 *
	 * @since 3.1
	 *
	 * @return string
	 */
	public function get_category_slug() {
		return 'category';
	}

	/**
	 * Filters the string to be used as the tag slug.
	 * This replaces TEC's translated tag slug by the untranslated one, as it is returned by a public method
	 * `Tribe__Events__Main->get_tag_slug()`.
	 *
	 * @since 3.1
	 *
	 * @return string
	 */
	public function get_tag_slug() {
		return 'tag';
	}

	/**
	 * Adds the lang to TEC's REST URL.
	 * This provides a way to identify in which language PLL should work in the REST request.
	 *
	 * @since 3.1
	 *
	 * @param  string $url The View endpoint URL, either a REST API URL or a admin-ajax.php fallback URL if REST API
	 *                     is not available.
	 * @return string
	 */
	public function add_missing_lang_to_rest_url( $url ) {
		$curlang = $this->get_curlang();

		if ( empty( $curlang ) ) {
			return $url;
		}

		$lang = $this->get_lang_from_url_query_arg( $url );

		if ( ! empty( $lang ) ) {
			return $url;
		}

		return add_query_arg( array( 'lang' => $curlang->slug ), $url );
	}

	/**
	 * Filters the locale when TEC is performing a REST request.
	 *
	 * @since 3.1
	 *
	 * @param  string $locale The locale ID.
	 * @return string
	 */
	public function filter_locale_for_rest( $locale ) {
		if ( ! $this->is_tec_rest_request() ) {
			return $locale;
		}

		$curlang = $this->get_curlang();

		if ( empty( $curlang->locale ) ) {
			return $locale;
		}

		return $curlang->locale;
	}

	/**
	 * Filters the query arguments that should be applied to the View links to add the missing language.
	 * The added language is the global current language.
	 *
	 * @since 3.1
	 *
	 * @param  mixed[] $url_args The current URL query arguments, created from a filtered version of the current
	 *                           request context.
	 * @return mixed[]
	 */
	public function add_language_to_publicly_visible_views_query_args( $url_args ) {
		$curlang = $this->get_curlang();

		if ( ! empty( $curlang ) ) {
			$url_args['lang'] = $curlang->slug;
		}
		return $url_args;
	}

	/**
	 * Fixes the "upcoming events" widget link.
	 *
	 * @since 3.1
	 *
	 * @param  mixed[] $template_vars An associative array of template variables. Variables will be extracted in the
	 *                                template hence the key will be the name of the variable available in the template.
	 * @return mixed[]
	 */
	public function translate_widget_view_more_link( $template_vars ) {
		if ( ! isset( $template_vars['view_more_link'] ) ) {
			return $template_vars;
		}

		$curlang = $this->get_curlang();

		if ( empty( $curlang ) || empty( $this->slugs_model ) ) {
			return $template_vars;
		}

		$template_vars['view_more_link'] = home_url( '/' . tribe_get_option( 'eventsSlug', 'events' ) );
		$template_vars['view_more_link'] = $this->polylang->links_model->add_language_to_link( $template_vars['view_more_link'], $curlang );
		$template_vars['view_more_link'] = $this->slugs_model->translate_slug( $template_vars['view_more_link'], $curlang, 'archive_' . TEC::POSTTYPE );
		$template_vars['view_more_link'] = user_trailingslashit( $template_vars['view_more_link'] );

		return $template_vars;
	}

	/**
	 * Filters the array of query variables parsed by TEC to force the use of the right language.
	 * For example, `example.com/events?lang=de` would return the default language instead of using the provided query
	 * arg because TEC tries to use the WP rewrite rules to match the query path, and `events` => default language.
	 * However, this is not needed for `example.com/de/events-de/` and `example.com/?lang=de` because the right
	 * language will be set in these cases.
	 *
	 * @since 3.1
	 * @see   Tribe__Rewrite->parse_request()
	 * @see   PLL_TEC->add_language_to_publicly_visible_views_query_args()
	 *
	 * @param  string[] $query_vars       The parsed query vars array.
	 * @param  string[] $extra_query_vars An associative array of extra query vars that will be processed before
	 *                                    the WordPress defined ones.
	 * @param  string   $url              The URL to parse.
	 * @return string[]
	 */
	public function force_language_on_tec_parse_query_vars( $query_vars, $extra_query_vars, $url ) {
		// Find the lang in the URL...
		$lang = $this->get_lang_from_url_query_arg( $url );

		if ( ! empty( $lang ) ) {
			// ... and add it to the query vars.
			$query_vars['lang'] = $lang->slug;
		}

		return $query_vars;
	}

	/**
	 * Adds the lang to TEC's query arguments that will be used to build a View URL.
	 * This insures that a lang arg is available when building the view's URL.
	 *
	 * @since 3.1
	 *
	 * @param  mixed[] $query_args An array of query args that will be used to build the URL for the View.
	 * @return mixed[]
	 */
	public function add_missing_lang_to_query_arg( $query_args ) {
		if ( isset( $query_args['lang'] ) ) {
			return $query_args;
		}

		$curlang = $this->get_curlang();

		$query_args['lang'] = ! empty( $curlang ) ? $curlang->slug : $this->polylang->options['default_lang'];

		return $query_args;
	}

	/**
	 * Adds the lang to the URL passed to `Tribe__Rewrite->get_canonical_url()`.
	 * This insures that a lang arg is available when building a URL.
	 *
	 * @since 3.1
	 *
	 * @param  string|null $canonical_url The canonical URL, defaults to `null`; returning a non `null` value will
	 *                                    make the logic bail and return the value.
	 * @param  string      $url           The input URL to resolve to a canonical one.
	 * @return string|null
	 */
	public function add_missing_lang_to_non_canonical_url( $canonical_url, $url ) {
		$lang = $this->get_lang_from_url_query_arg( $url );

		if ( ! empty( $lang ) ) {
			// All good.
			return $canonical_url;
		}

		$curlang = $this->get_curlang();

		if ( empty( $curlang ) ) {
			// We're screwed.
			return $canonical_url;
		}

		// Re-inject the URL with the current lang. `$this->get_lang_from_url_query_arg()` will prevent an infinite loop.
		return Tribe__Rewrite::instance()->get_canonical_url( add_query_arg( 'lang', $curlang->slug, $url ) );
	}

	/**
	 * Removes the `name` arg from the URL passed to `Tribe__Rewrite->get_canonical_url()` when there is already a
	 * `post_type` arg: this seems to mess up the process.
	 *
	 * @since 3.1
	 *
	 * @param  string|null $canonical_url The canonical URL, defaults to `null`; returning a non `null` value will
	 *                                    make the logic bail and return the value.
	 * @param  string      $url           The input URL to resolve to a canonical one.
	 * @return string|null
	 */
	public function remove_name_arg_from_non_canonical_url( $canonical_url, $url ) {
		$url_query = wp_parse_url( $url, PHP_URL_QUERY );

		if ( empty( $url_query ) ) {
			return $canonical_url;
		}

		$parsed_query = array();
		wp_parse_str( $url_query, $parsed_query );

		if ( empty( $parsed_query ) ) {
			return $canonical_url;
		}

		if ( empty( $parsed_query['post_type'] ) || empty( $parsed_query['name'] ) || ! empty( $parsed_query['ical'] ) ) {
			return $canonical_url;
		}

		if ( TEC::POSTTYPE !== $parsed_query['post_type'] ) {
			return $canonical_url;
		}

		if ( ! empty( $parsed_query['lang'] ) && $parsed_query['lang'] === $parsed_query['name'] ) {
			// ¯\(°_o)/¯.
			$remove = true;
		} elseif ( tribe_get_option( 'eventsSlug' ) === $parsed_query['name'] ) {
			$remove = true;
		}

		if ( empty( $remove ) ) {
			return $canonical_url;
		}

		// Re-inject the URL. Tests against `$parsed_query['name']` will prevent an infinite loop.
		return Tribe__Rewrite::instance()->get_canonical_url( remove_query_arg( 'name', $url ) );
	}

	/**
	 * Filters TEC's canonical URL to fix the language slug in it.
	 * Because of TEC's method to build URLs, using the rewrite rules array, the language slug is not replaced and is
	 * outputted like the rewrite rule pattern: `/(en|fr|de)/`. This filter replaces the pattern by the language
	 * contained in the original URL. If not found in the original URL, falls back to the current language or the default
	 * one.
	 *
	 * @since 3.1
	 *
	 * @param  string $resolved The resolved, canonical URL.
	 * @param  string $url      The original URL to resolve.
	 * @return string
	 */
	public function fix_language_in_canonical_url( $resolved, $url ) {
		$options = $this->polylang->options;

		// Remove the default language if it must be hidden in the URLs.
		$filter    = $options['hide_default'] ? 'hide_default' : '';
		$languages = $this->polylang->model->languages->filter( $filter )->get_list( array( 'fields' => 'slug' ) );

		if ( empty( $languages ) ) {
			return $resolved;
		}

		/**
		 * What we want to modify in the URL.
		 * Ex: `/(en|fr|de)`, `/language/(fr|de)`.
		 */
		$language_path = $options['rewrite'] ? '' : '/language';
		$to_replace    = $language_path . '/(' . implode( '|', $languages ) . ')/';

		if ( strpos( $resolved, $to_replace ) === false ) {
			return $resolved;
		}

		// Find the lang in the URL (or fallback).
		$lang = $this->get_lang_from_url_query_arg( $url );

		if ( empty( $lang ) ) {
			// We need a lang, whichever it is.
			$curlang = $this->get_curlang();
			$lang    = ! empty( $curlang ) ? $curlang->slug : $options['default_lang'];
		} else {
			$lang = $lang->slug;
		}

		// Make the final replacement.
		if ( $options['hide_default'] && $options['default_lang'] === $lang ) {
			// The default language is hidden in the URL.
			$replacement = '/';
		} else {
			$replacement = "{$language_path}/{$lang}/";
		}

		return str_replace( $to_replace, $replacement, $resolved );
	}

	/**
	 * Filters TEC's canonical URL to translate all slugs in it.
	 * This is possible because a `lang` arg is available in the "ugly" URL.
	 *
	 * @since 3.1
	 * @see   Tribe__Events__Rewrite->get_dynamic_matchers()
	 *
	 * @param  string $resolved The resolved, canonical URL.
	 * @param  string $url      The original URL to resolve.
	 * @return string
	 */
	public function translate_canonical_url( $resolved, $url ) {
		if ( empty( $this->slugs_model ) ) {
			return $resolved;
		}

		// Find the lang in the URL (or fallback).
		$lang = $this->get_lang_from_url_query_arg_or_fallback( $url );

		if ( empty( $lang ) ) {
			// What?
			return $resolved;
		}

		// Make sure the language is well formatted.
		$resolved = remove_query_arg( 'lang', $resolved );
		$resolved = $this->polylang->links_model->add_language_to_link( $resolved, $lang );

		foreach ( $this->get_translatable_slugs() as $slug_id => $translations ) {
			$resolved = $this->slugs_model->switch_translated_slug( $resolved, $lang, $slug_id );
		}

		return $resolved;
	}

	/**
	 * Tells if a request is a TEC REST API request.
	 * TEC does a good job for their REST URL by providing a `admin-ajax.php` fallback in case the REST API is not
	 * available. Unfortunately, this choice is late in the process so we have to test the given URL against the 2
	 * possibilities.
	 *
	 * @since 3.1
	 * @see   Tribe\Events\Views\V2\Rest_Endpoint->get_url()
	 *
	 * @param  string $requested_url The requested URL. Falls back to the current URL.
	 * @return bool|null             Whether the request is a TEC REST API request. Null if not ready to answer yet.
	 */
	protected function is_tec_rest_request( $requested_url = null ) {
		if ( ! isset( $this->is_tec_rest_request['views_v2_is_enabled'] ) ) {
			if ( ! function_exists( 'tribe_events_views_v2_is_enabled' ) ) {
				return null;
			}

			$this->is_tec_rest_request['views_v2_is_enabled'] = tribe_events_views_v2_is_enabled();
		}

		if ( ! $this->is_tec_rest_request['views_v2_is_enabled'] ) {
			// If the views V2 are not enabled, no REST requests.
			return false;
		}

		if ( empty( $requested_url ) || ! is_string( $requested_url ) ) {
			// Fall back to the current URL.
			if ( ! isset( $this->is_tec_rest_request['pll_requested_url'] ) ) {
				$this->is_tec_rest_request['pll_requested_url'] = (string) set_url_scheme( pll_get_requested_url() );
			}

			$requested_url = $this->is_tec_rest_request['pll_requested_url'];
		} else {
			$requested_url = (string) set_url_scheme( $requested_url );
		}

		if ( isset( $this->is_tec_rest_request[ 'is:' . $requested_url ] ) ) {
			return $this->is_tec_rest_request[ 'is:' . $requested_url ];
		}

		if ( false === strpos( $requested_url, '/admin-ajax.php' ) ) {
			// Test against the REST URL.
			if ( ! isset( $this->is_tec_rest_request['tec_rest_url_pattern'] ) ) {
				$url = $this->get_tec_rest_url( true );

				if ( empty( $url ) ) {
					// `$wp_rewrite` is probably not set yet.
					return null;
				}

				$this->is_tec_rest_request['tec_rest_url_pattern'] = preg_replace( '@[#?].*$@', '', $url );
				$this->is_tec_rest_request['tec_rest_url_pattern'] = sprintf( '@^%s[/?#]@i', preg_quote( $this->is_tec_rest_request['tec_rest_url_pattern'], '@' ) );
			}

			$this->is_tec_rest_request[ 'is:' . $requested_url ] = (bool) preg_match( $this->is_tec_rest_request['tec_rest_url_pattern'], $requested_url );

			return $this->is_tec_rest_request[ 'is:' . $requested_url ];
		}

		// Test against the admin ajax URL.
		if ( ! isset( $this->is_tec_rest_request['tec_ajax_url_action'] ) ) {
			$this->is_tec_rest_request['tec_ajax_url_action'] = $this->get_tec_rest_url( false );
			$this->is_tec_rest_request['tec_ajax_url_action'] = $this->get_query_arg_from_url( $this->is_tec_rest_request['tec_ajax_url_action'], 'action' );
		}

		if ( empty( $this->is_tec_rest_request['tec_ajax_url_action'] ) ) {
			// Uh?
			$this->is_tec_rest_request[ 'is:' . $requested_url ] = false;
			return false;
		}

		$requested_action = $this->get_query_arg_from_url( $requested_url, 'action' );

		$this->is_tec_rest_request[ 'is:' . $requested_url ] = $requested_action === $this->is_tec_rest_request['tec_ajax_url_action'];

		return $this->is_tec_rest_request[ 'is:' . $requested_url ];
	}

	/**
	 * Returns the current language object.
	 * Can return `null` if not defined yet.
	 *
	 * @since 3.1
	 *
	 * @return PLL_Language|null
	 */
	protected function get_curlang() {
		if ( ! empty( $this->curlang ) ) {
			return $this->curlang;
		}

		if ( ! empty( $_REQUEST['lang'] ) && is_string( $_REQUEST['lang'] ) && Polylang::is_rest_request() ) { // phpcs:ignore WordPress.Security.NonceVerification
			// REST request.
			$curlang = $this->polylang->model->get_language( sanitize_key( $_REQUEST['lang'] ) ); // phpcs:ignore WordPress.Security.NonceVerification

			if ( ! empty( $curlang ) ) {
				$this->curlang           = $curlang;
				$this->polylang->curlang = $this->curlang;
				return $this->curlang;
			}
		}

		if ( ! empty( $this->polylang->curlang ) ) {
			// Global context.
			$this->curlang = &$this->polylang->curlang;
			return $this->curlang;
		}

		if ( empty( $this->polylang->options['force_lang'] ) && $this->polylang instanceof PLL_Frontend && ! did_action( 'pll_language_defined' ) ) {
			// Lang defined by content: too soon.
			return null;
		}

		$curlang = $this->polylang->model->get_default_language();

		if ( empty( $curlang ) ) {
			// We're screwed.
			return null;
		}

		// Default lang.
		$this->curlang = $curlang;
		return $this->curlang;
	}

	/**
	 * Returns the list of slugs that need to be reset in TEC, except the deprecated ones.
	 *
	 * @since 3.1
	 * @see   PLL_TEC->reset_slugs()
	 *
	 * @return string[] Array keys match `Tribe__Events__Main`'s properties name.
	 */
	protected function get_slugs_to_reset() {
		return array(
			'category_slug' => 'category',
			'tag_slug'      => 'tag',
			'monthSlug'     => 'month',
			'listSlug'      => 'list',
			'upcomingSlug'  => 'upcoming',
			'pastSlug'      => 'past',
			'daySlug'       => 'day',
			'todaySlug'     => 'today',
			'featured_slug' => 'featured',
			'all_slug'      => 'all',
		);
	}

	/**
	 * Returns the list of IDs of translatable slugs dedicated to TEC.
	 * Ex: `tribe_venue`, `archive_tribe_events`, `paged`, `tribe_today`.
	 *
	 * @since 3.1
	 *
	 * @return string[]
	 */
	protected function get_translatable_slug_ids() {
		if ( ! empty( $this->translatable_slug_ids ) ) {
			return $this->translatable_slug_ids;
		}

		$slug_ids = array(
			Venue::POSTTYPE,
			Organizer::POSTTYPE,
			TEC::POSTTYPE,
			'archive_' . TEC::POSTTYPE,
			'post_tag',
			'paged',
		);

		foreach ( $this->get_slugs_to_reset() as $slug ) {
			$slug_ids[] = 'tribe_' . $slug;
		}

		$this->translatable_slug_ids = array_combine( $slug_ids, $slug_ids );

		return $this->translatable_slug_ids;
	}

	/**
	 * Returns the list of translatable slugs dedicated to TEC.
	 *
	 * @since 3.1
	 *
	 * @return mixed[]
	 */
	protected function get_translatable_slugs() {
		if ( empty( $this->slugs_model ) ) {
			return array();
		}

		return array_intersect_key( $this->slugs_model->get_translatable_slugs(), $this->get_translatable_slug_ids() );
	}

	/**
	 * Returns the value of the `lang` query arg from the given URL.
	 *
	 * @since 3.1
	 *
	 * @param  string $url       The URL to retrieve the arg from.
	 * @return PLL_Language|null The lang object. Null if not found or invalid.
	 */
	protected function get_lang_from_url_query_arg( $url ) {
		$lang = $this->get_query_arg_from_url( $url, 'lang' );

		if ( empty( $lang ) || ! is_string( $lang ) ) {
			return null;
		}

		$lang = $this->polylang->model->get_language( sanitize_key( $lang ) );

		if ( empty( $lang ) ) {
			return null;
		}

		return $lang;
	}

	/**
	 * Returns the value of the `lang` query arg from the given URL.
	 *
	 * @since 3.1
	 *
	 * @param  string $url       The URL to retrieve the arg from.
	 * @return PLL_Language|null The lang object. Null if not found or invalid.
	 */
	protected function get_lang_from_url_query_arg_or_fallback( $url ) {
		$lang = $this->get_lang_from_url_query_arg( $url );

		if ( ! empty( $lang ) ) {
			return $lang;
		}

		$lang = $this->get_curlang();

		if ( ! empty( $lang ) ) {
			return $lang;
		}

		// Uh?
		$lang = $this->polylang->model->get_default_language();

		if ( ! empty( $lang ) ) {
			return $lang;
		}

		// What?
		return null;
	}

	/**
	 * Returns the value of a query arg from the given URL.
	 *
	 * @since 3.1
	 *
	 * @param  string $url            The URL to retrieve the arg from.
	 * @param  string $query_arg_name The name of the query arg to retrieve.
	 * @return string|null            The raw value of the query arg. Null if not found.
	 */
	protected function get_query_arg_from_url( $url, $query_arg_name ) {
		if ( empty( $url ) || ! is_string( $url ) ) {
			return null;
		}

		if ( empty( $query_arg_name ) || ! is_string( $query_arg_name ) ) {
			return null;
		}

		$url_query = wp_parse_url( $url, PHP_URL_QUERY );

		if ( empty( $url_query ) ) {
			return null;
		}

		$parsed = array();
		wp_parse_str( $url_query, $parsed );

		if ( empty( $parsed[ $query_arg_name ] ) || ! is_string( $parsed[ $query_arg_name ] ) ) {
			return null;
		}

		return sanitize_key( $parsed[ $query_arg_name ] );
	}

	/**
	 * Returns TEC's REST URL.
	 *
	 * @since 3.1
	 *
	 * @param  bool $enable_rest True to get the REST URL. False to get the admin ajax URL.
	 * @return string|null       The REST URL. Null if too soon to be determined: this may happen when requesting the
	 *                           the real REST URL (`$enable_rest` is true) but `$wp_rewrite` is not ready.
	 */
	protected function get_tec_rest_url( $enable_rest ) {
		global $wp_rewrite;

		if ( $enable_rest ) {
			// In this case, `Rest_Endpoint->get_url()` will use `get_rest_url()`.
			if ( ( is_multisite() && get_blog_option( 0, 'permalink_structure' ) ) || get_option( 'permalink_structure' ) ) { // See the same test done in `get_rest_url()`.
				// We test `$wp_rewrite` to prevent `get_rest_url()` to explode.
				if ( ! $wp_rewrite instanceof WP_Rewrite ) {
					return null;
				}
			}
		}

		// Force `Rest_Endpoint->is_available()`'s behavior with this filter callback.
		$priority = 100000;
		$callback = function () use ( $enable_rest ) {
			return (bool) $enable_rest;
		};

		add_filter( 'tribe_events_views_v2_rest_endpoint_available', $callback, $priority );
		$url = ( new Rest_Endpoint() )->get_url();
		remove_filter( 'tribe_events_views_v2_rest_endpoint_available', $callback, $priority );

		return $url;
	}
}
