<?php
/**
 * Controls the main event query.  Allows for recurring events.
 */

// Don't load directly
if ( ! defined( 'ABSPATH' ) ) {
	die( '-1' );
}

use Tribe__Utils__Array as Arr;

if ( ! class_exists( 'Tribe__Events__Query' ) ) {
	class Tribe__Events__Query {
		/**
		 * @since 4.9.4
		 *
		 * @var array The WP_Query arguments used in the last `getEvents` method
		 *            query.
		 */
		protected static $last_result = [];

		/**
		 * Initialize The Events Calendar query filters and post processing.
		 */
		public static function init() {
			/**
			 * A toggle filter to completely suppress all query filters for the whole request.
			 *
			 * @since 4.9.11
			 *
			 * @param bool $suppress_filters Whether to completely suppress all query filters for the whole request.
			 */
			$suppress_filters = apply_filters( 'tribe_events_suppress_query_filters', false );

			if ( $suppress_filters ) {
				return;
			}

			// if tribe event query add filters
			add_action( 'parse_query', [ __CLASS__, 'parse_query' ], 50 );
			add_action( 'pre_get_posts', [ __CLASS__, 'pre_get_posts' ], 50 );
			// Remove the filter to reset the value of the virtual page if is setup.
			add_filter( 'posts_results', [ __CLASS__, 'posts_results' ] );

			if ( is_admin() ) {
				$cleanup = new Tribe__Events__Recurring_Event_Cleanup();
				$cleanup->toggle_recurring_events();
				unset( $cleanup );
			}
		}

		/**
		 * determines whether a date field can be injected into various parts of a query
		 *
		 * @param $query WP_Query Query object
		 *
		 * @return boolean
		 */
		public static function can_inject_date_field( $query ) {
			$can_inject = true;

			if ( 'ids' === $query->query_vars['fields'] ) {
				$can_inject = false;
			}

			if ( 'id=>parent' === $query->query_vars['fields'] ) {
				$can_inject = false;
			}

			if ( isset( $query->query_vars['do_not_inject_date'] ) && $query->query_vars['do_not_inject_date'] ) {
				$can_inject = false;
			}

			/**
			 * When using the ORM we cannot use EventStartDate injection
			 */
			if ( isset( $query->builder ) && $query->builder instanceof Tribe__Repository ) {
				$can_inject = false;
			}

			/**
			 * Determine whether a date field can be injected into various parts of a query.
			 *
			 * @param boolean  $can_inject Whether the date field can be injected
			 * @param WP_Query $query      Query object
			 *
			 * @since 4.5.8
			 */
			return apply_filters( 'tribe_query_can_inject_date_field', $can_inject, $query );
		}

		/**
		 * Set any query flags
		 *
		 * @param WP_Query $query
		 */
		public static function parse_query( $query ) {
			// If this is set then the class will bail out of any filtering.
			if ( $query->get( 'tribe_suppress_query_filters', false ) ) {
				return $query;
			}

			$helper = Tribe__Admin__Helpers::instance();

			// set paged
			if ( $query->is_main_query() && isset( $_GET['tribe_paged'] ) ) {
				$query->set( 'paged', absint( tribe_get_request_var( 'tribe_paged' ) ) );
			}

			// Return early as we don't want to change a post that is not part of the valid group of event post types.
			$valid_post_types = [
				Tribe__Events__Main::POSTTYPE,
				Tribe__Events__Venue::POSTTYPE,
				Tribe__Events__Organizer::POSTTYPE,
			];
			if (
				$query->is_main_query()
				&& $query->is_single()
				// Make sure we are not in a Post Type declared by the plugin.
				&& false === in_array( $query->get( 'post_type' ), $valid_post_types )
			)  {
				return $query;
			}

			// Don't change query on pages as we might be ina shortcode.
			if ( $query->is_main_query() && $query->is_page() ) {
				return $query;
			}

			// Add tribe events post type to tag queries only in tag archives
			if (
				$query->is_tag
				&& (array) $query->get( 'post_type' ) != [ Tribe__Events__Main::POSTTYPE ]
				&& ! $helper->is_post_type_screen( 'post' )
			) {
				$types = $query->get( 'post_type' );

				if ( empty( $types ) ) {
					$types = [ 'post' ];
				}

				if ( is_array( $types ) && $query->is_main_query() ) {
					$types[] = Tribe__Events__Main::POSTTYPE;
				} elseif ( $query->is_main_query() ) {
					if ( is_string( $types ) ) {
						$types = [ $types, Tribe__Events__Main::POSTTYPE ];
					} else {
						if ( $types != 'any' ) {
							$types = [ 'post', Tribe__Events__Main::POSTTYPE ];
						}
					}
				}

				$query->set( 'post_type', $types );
			}

			$types = ( ! empty( $query->query_vars['post_type'] ) ? (array) $query->query_vars['post_type'] : [] );

			// check if any possibility of this being an event query
			$query->tribe_is_event = ( in_array( Tribe__Events__Main::POSTTYPE, $types ) && count( $types ) < 2 )
				? true // it was an event query
				: false;

			$query->tribe_is_multi_posttype = ( in_array( Tribe__Events__Main::POSTTYPE, $types ) && count( $types ) >= 2 || in_array( 'any', $types ) )
				? true // it's a query for multiple post types, events post type included
				: false;

			if ( 'default' === $query->get( 'eventDisplay' ) ) {
				$default_view = Tribe__Events__Main::instance()->default_view();

				/**
				 * Allows other plugins (and v2 views) to hook in and alter this before we change the query.
				 *
				 * @since 5.12.3
				 *
				 * @param string $default_view The slug of the default view to pass to the query.
				 */
				$default_view = apply_filters( 'tec_events_query_default_view', $default_view );

				$query->set( 'eventDisplay', $default_view );
			}

			// check if any possibility of this being an event category
			$query->tribe_is_event_category = ! empty ( $query->query_vars[ Tribe__Events__Main::TAXONOMY ] )
				? true // it was an event category
				: false;

			$query->tribe_is_event_venue = ( in_array( Tribe__Events__Main::VENUE_POST_TYPE, $types ) )
				? true // it was an event venue
				: false;

			$query->tribe_is_event_organizer = ( in_array( Tribe__Events__Main::ORGANIZER_POST_TYPE, $types ) )
				? true // it was an event organizer
				: false;

			$query->tribe_is_event_query = ( $query->tribe_is_event
			                                 || $query->tribe_is_event_category
			                                 || $query->tribe_is_event_venue
			                                 || $query->tribe_is_event_organizer )
				? true // this is an event query of some type
				: false; // move along, this is not the query you are looking for

			// is the query pulling posts from the past?
			$past_requested = ! empty( $_REQUEST['tribe_event_display'] ) && 'past' === $_REQUEST['tribe_event_display'];
			$display_past   = 'past' === $query->get( 'eventDisplay' );

			if ( $query->is_main_query() && $past_requested ) {
				$query->tribe_is_past = true;
			} elseif ( tribe_is_ajax_view_request() && ( $display_past || $past_requested ) ) {
				$query->tribe_is_past = true;
			} elseif ( $query->get( 'tribe_is_past' ) ) {
				$query->tribe_is_past = true;
			} else {
				$query->tribe_is_past = isset( $query->tribe_is_past ) ? $query->tribe_is_past : false;
			}

			// never allow 404 on month view
			if (
				$query->is_main_query()
				&& 'month' === $query->get( 'eventDisplay' )
				&& ! $query->is_tax
				&& ! $query->tribe_is_event_category
			) {
				$query->is_post_type_archive = true;
				$query->queried_object       = get_post_type_object( Tribe__Events__Main::POSTTYPE );
				$query->queried_object_id    = 0;
			}

			if ( tribe_is_events_front_page() ) {
				$query->is_home = true;
			} elseif ( $query->tribe_is_event_query ) {
				// fixing is_home param
				$query->is_home = empty( $query->query_vars['is_home'] ) ? false : $query->query_vars['is_home'];
				do_action( 'tribe_events_parse_query', $query );
			}
		}

		/**
		 * Is hooked by init() filter to parse the WP_Query arguments for main and alt queries.
		 *
		 * @param object $query WP_Query object args supplied or default
		 *
		 * @return object $query (modified)
		 */
		public static function pre_get_posts( $query ) {
			// If this is set then the class will bail out of any filtering.
			if ( $query->get( 'tribe_suppress_query_filters', false ) ) {
				return $query;
			}

			if ( $query->is_main_query() && is_home() ) {
				/**
				 * The following filter will remove the virtual page from the option page and return a 0 as it's not
				 * set when the SQL query is constructed to avoid having a is_page() instead of a is_home().
				 */
				add_filter( 'option_page_on_front', [ __CLASS__, 'default_page_on_front' ] );
				// check option for including events in the main wordpress loop, if true, add events post type
				if ( tribe_get_option( 'showEventsInMainLoop', false ) && ! get_query_var( 'tribe_events_front_page' ) ) {
					$query->query_vars['post_type']   = isset( $query->query_vars['post_type'] )
						? ( array ) $query->query_vars['post_type']
						: [ 'post' ];

					if ( ! in_array( Tribe__Events__Main::POSTTYPE, $query->query_vars['post_type'] ) ) {
						$query->query_vars['post_type'][] = Tribe__Events__Main::POSTTYPE;
					}
					$query->tribe_is_multi_posttype   = true;
				}
			}

			if ( $query->tribe_is_multi_posttype ) {
				do_action( 'log', 'multi_posttype', 'default', $query->tribe_is_multi_posttype );
				add_filter( 'posts_fields', [ __CLASS__, 'multi_type_posts_fields' ], 10, 2 );
				add_filter( 'posts_join', [ __CLASS__, 'posts_join' ], 10, 2 );
				add_filter( 'posts_join', [ __CLASS__, 'posts_join_venue_organizer' ], 10, 2 );
				add_filter( 'posts_distinct', [ __CLASS__, 'posts_distinct' ] );
				add_filter( 'posts_orderby', [ __CLASS__, 'posts_orderby' ], 10, 2 );
				do_action( 'tribe_events_pre_get_posts', $query );

				return;
			}

			if ( $query->tribe_is_event || $query->tribe_is_event_category ) {
				/**
				 * Filters whether or not to use the Start Date meta hack to include meta table.
				 *
				 * We only add the `postmeta` hack if it's not the main admin events list
				 * because this method filters out drafts without EventStartDate.
				 * For this screen we're doing the JOIN manually in `Tribe__Events__Admin_List`.
				 *
				 * Important to note, this need to happen as the first thing, due to how we depend on
				 * StartDate been the first param on the Meta Query.
				 *
				 * @param boolean $use_hack Whether to include the start date meta or not.
				 * @param \WP_Query|null $query The query that is currently being filtered or `null` if no query is
				 *                              being filtered.
				 *
				 * @since 4.9
				 */
				$include_date_meta = apply_filters( 'tribe_events_query_include_start_date_meta', true, $query );
				if (
					$include_date_meta
					&& ! tribe( 'context' )->is_editing_post( Tribe__Events__Main::POSTTYPE )
				) {
					$date_meta_key = Tribe__Events__Timezones::is_mode( 'site' )
						? '_EventStartDateUTC'
						: '_EventStartDate';

					$meta_query[] = [
						'key'  => $date_meta_key,
						'type' => 'DATETIME',
					];

					$query->set( 'tribe_include_date_meta', true );
				}

				if ( ! ( $query->is_main_query() && 'month' === $query->get( 'eventDisplay' ) ) ) {
					add_filter( 'option_page_on_front', [ __CLASS__, 'default_page_on_front' ] );
					add_filter( 'posts_fields', [ __CLASS__, 'posts_fields' ], 10, 2 );
					add_filter( 'posts_join', [ __CLASS__, 'posts_join' ], 10, 2 );
					add_filter( 'posts_join', [ __CLASS__, 'posts_join_venue_organizer' ], 10, 2 );
					add_filter( 'posts_where', [ __CLASS__, 'posts_where' ], 10, 2 );
					add_filter( 'posts_distinct', [ __CLASS__, 'posts_distinct' ] );
				} else {

					// reduce number of queries triggered by main WP_Query on month view
					$query->set( 'posts_per_page', 1 );
					$query->set( 'no_found_rows', true );
					$query->set( 'cache_results', false );
					$query->set( 'update_post_meta_cache', false );
					$query->set( 'update_post_term_cache', false );
					do_action( 'tribe_events_pre_get_posts', $query );

					return $query;
				}

				// if a user selects a date in the event bar we want it to persist as long as possible
				if ( ! empty( $_REQUEST['tribe-bar-date'] ) ) {
					$query->set( 'eventDate', $_REQUEST['tribe-bar-date'] );
					do_action( 'log', 'changed eventDate to tribe-bar-date', 'tribe-events-query', $_REQUEST['tribe-bar-date'] );
				}

				// if a user provides a search term we want to use that in the search params
				if ( ! empty( $_REQUEST['tribe-bar-search'] ) ) {
					$query->query_vars['s'] = $_REQUEST['tribe-bar-search'];
				}

				$query->set( 'eventDisplay', $query->get( 'eventDisplay', Tribe__Events__Main::instance()->displaying ) );

				// By default we'll hide events marked as "hidden from event listings" unless
				// the query explicity requests they be exposed
				$maybe_hide_events = (bool) $query->get( 'hide_upcoming', true );

				$skip_event_display_filters = is_admin() && $query->is_main_query() && ! tribe_is_ajax_view_request();

				// @todo  [BTRIA-607]: Stop calling EOD cutoff transformations all over the place.
				if ( ! empty( $query->query_vars['eventDisplay'] ) && ! $skip_event_display_filters ) {
					switch ( $query->query_vars['eventDisplay'] ) {
						case 'custom':
							// if the eventDisplay is 'custom', all we're gonna do is make sure the start and end dates are formatted
							$start_date = $query->get( 'start_date' );

							if ( $start_date ) {
								$start_date_string = $start_date instanceof DateTime
									? $start_date->format( Tribe__Date_Utils::DBDATETIMEFORMAT )
									: $start_date;

								$query->set( 'start_date', date_i18n( Tribe__Date_Utils::DBDATETIMEFORMAT, strtotime( $start_date_string ) ) );
							}

							$end_date = $query->get( 'end_date' );

							if ( $end_date ) {
								$end_date_string = $end_date instanceof DateTime
									? $end_date->format( Tribe__Date_Utils::DBDATETIMEFORMAT )
									: $end_date;

								$query->set( 'end_date', date_i18n( Tribe__Date_Utils::DBDATETIMEFORMAT, strtotime( $end_date_string ) ) );
							}
							break;
						case 'month':

							// make sure start and end date are set
							if ( $query->get( 'start_date' ) == '' ) {
								$event_date = ( $query->get( 'eventDate' ) != '' )
									? $query->get( 'eventDate' )
									: date_i18n( Tribe__Date_Utils::DBDATETIMEFORMAT );
								$query->set( 'start_date', tribe_beginning_of_day( $event_date ) );
							}

							if ( $query->get( 'end_date' == '' ) ) {
								$query->set( 'end_date', tribe_end_of_day( $query->get( 'start_date' ) ) );
							}
							$query->set( 'hide_upcoming', $maybe_hide_events );

							break;
						case 'day':
							$event_date = $query->get( 'eventDate' ) != '' ? $query->get( 'eventDate' ) : date( 'Y-m-d', current_time( 'timestamp' ) );
							$query->set( 'eventDate', $event_date );
							$beginning_of_day = strtotime( tribe_beginning_of_day( $event_date ) );
							$query->set( 'start_date', date_i18n( Tribe__Date_Utils::DBDATETIMEFORMAT, $beginning_of_day ) );
							$query->set( 'end_date', tribe_end_of_day( $event_date ) );
							$query->set( 'posts_per_page', - 1 ); // show ALL day posts
							$query->set( 'hide_upcoming', $maybe_hide_events );
							$query->set( 'order', self::set_order( 'ASC', $query ) );
							break;
						case 'single-event':
							if ( $query->get( 'eventDate' ) != '' ) {
								$query->set( 'start_date', $query->get( 'eventDate' ) );
								$query->set( 'eventDate', $query->get( 'eventDate' ) );
							}
							break;
						case 'future':
							$event_date = ( '' !== $query->get( 'eventDate' ) )
								? $query->get( 'eventDate' )
								: date_i18n( Tribe__Date_Utils::DBDATETIMEFORMAT );
							$query->set( 'start_date', ( '' != $query->get( 'eventDate' ) ? tribe_beginning_of_day( $event_date ) : tribe_format_date( current_time( 'timestamp' ), true, 'Y-m-d H:i:00' ) ) );
							$query->set( 'order', self::set_order( 'ASC', $query ) );
							$query->set( 'orderby', self::set_orderby( null, $query ) );
							$query->set( 'hide_upcoming', $maybe_hide_events );
							break;
						case 'all':
							$query->set( 'orderby', self::set_orderby( null, $query ) );
							$query->set( 'order', self::set_order( 'ASC', $query ) );
							$query->set( 'hide_upcoming', $maybe_hide_events );
							$query->set( 'start_date', tribe_format_date( current_time( 'timestamp' ), true, 'Y-m-d H:i:00' ) );
							break;
						case 'list':
						default: // default display query
							if ( '' != $query->get( 'eventDate' ) ) {
								$event_date = $query->get( 'eventDate' );
							} else {
								$event_date = date_i18n( Tribe__Date_Utils::DBDATETIMEFORMAT );
							}

						if ( ! $query->get( 'tribe_remove_date_filters', false ) ) {
							if ( $query->tribe_is_past ) {
								// on past view, set the passed date as the end date
								$query->set( 'start_date', '' );
								$query->set( 'end_date', $event_date );
								$query->set( 'order', self::set_order( 'DESC', $query ) );
							} else {
								if ( '' != $query->get( 'eventDate' ) ) {
									$event_date = tribe_beginning_of_day( $event_date );
								} else {
									$event_date = tribe_format_date( current_time( 'timestamp' ),
									                                 true,
									                                 'Y-m-d H:i:00' );
								}

								$orm_meta_query = tribe_events()->filter_by_ends_after( $event_date );

								$meta_query['ends-after'] = $orm_meta_query['meta_query']['ends-after'];

								$query->set( 'order', self::set_order( 'ASC', $query ) );
							}
						}

						$query->set( 'orderby', self::set_orderby( null, $query ) );
						$query->set( 'hide_upcoming', $maybe_hide_events );
						break;
					}
				} else {
					$query->set( 'hide_upcoming', $maybe_hide_events );
					$query->set( 'start_date', date_i18n( Tribe__Date_Utils::DBDATETIMEFORMAT ) );
					$query->set( 'orderby', self::set_orderby( null, $query ) );
					$query->set( 'order', self::set_order( null, $query ) );
				}

				// eventCat becomes a standard taxonomy query - will need to deprecate and update views eventually
				if ( ! in_array( $query->get( Tribe__Events__Main::TAXONOMY ), [ '', '-1' ] ) ) {
					$tax_query[] = [
						'taxonomy'         => Tribe__Events__Main::TAXONOMY,
						'field'            => 'slug',
						'terms'            => $query->get( Tribe__Events__Main::TAXONOMY ),
						'include_children' => apply_filters( 'tribe_events_query_include_children', true ),
					];
				}
			}

			// filter by Venue ID
			if ( $query->tribe_is_event_query && $query->get( 'venue' ) != '' ) {
				$meta_query[] = [
					'key'   => '_EventVenueID',
					'value' => $query->get( 'venue' ),
				];
			}

			// filter by Organizer ID
			if ( $query->tribe_is_event_query && $query->get( 'organizer' ) != '' ) {
				$meta_query[] = [
					'key'   => '_EventOrganizerID',
					'value' => $query->get( 'organizer' ),
				];
			}

			// enable pagination setup
			if ( $query->tribe_is_event_query && $query->get( 'posts_per_page' ) == '' ) {
				$query->set( 'posts_per_page', (int) tribe_get_option( 'postsPerPage', 10 ) );
			}

			// hide upcoming events from query (only not in admin)
			if ( $query->tribe_is_event_query && $query->get( 'hide_upcoming' ) && ! $query->get( 'suppress_filters' ) ) {
				$hide_upcoming_ids = self::getHideFromUpcomingEvents();
				if ( ! empty( $hide_upcoming_ids ) ) {
					// Merge if there is any items and remove empty items
					$hide_upcoming_ids = array_filter( array_merge( $hide_upcoming_ids, (array) $query->get( 'post__not_in' ) ) );

					$query->set( 'post__not_in', $hide_upcoming_ids );
				}
			}

			if ( $query->tribe_is_event_query && ! empty( $meta_query ) ) {
				// setup default relation for meta queries
				$meta_query['relation'] = 'AND';
				$meta_query_combined    = array_merge( (array) $meta_query, (array) $query->get( 'meta_query' ) );
				$query->set( 'meta_query', $meta_query_combined );
			}

			if ( $query->tribe_is_event_query && ! empty( $tax_query ) ) {
				// setup default relation for tax queries
				$tax_query_combined = array_merge( (array) $tax_query, (array) $query->get( 'tax_query' ) );
				$query->set( 'tax_query', $tax_query_combined );
			}

			if ( $query->tribe_is_event_query ) {
				add_filter( 'posts_orderby', [ __CLASS__, 'posts_orderby' ], 10, 2 );
			}

			if ( $query->tribe_is_event_query ) {
				do_action( 'tribe_events_pre_get_posts', $query );
			}

			/**
			 * If is in the admin remove the event date & upcoming filters, unless is an ajax call
			 * It's important to note that `tribe_remove_date_filters` needs to be set before calling
			 * self::should_remove_date_filters() to allow the date_filters to be actually removed
			 */
			if ( self::should_remove_date_filters( $query ) ) {
				remove_filter( 'option_page_on_front', [ __CLASS__, 'default_page_on_front' ] );
				remove_filter( 'posts_where', [ __CLASS__, 'posts_where' ], 10, 2 );
				remove_filter( 'posts_fields', [ __CLASS__, 'posts_fields' ] );
				remove_filter( 'posts_orderby', [ __CLASS__, 'posts_orderby' ], 10, 2 );
				$query->set( 'post__not_in', '' );

				// set the default order for posts within admin lists
				if ( ! isset( $query->query['order'] ) ) {
					$query->set( 'order', 'DESC' );
				} else {
					// making sure we preserve the order supplied by the query string even if it is overwritten above
					$query->set( 'order', $query->query['order'] );
				}
			}

			return $query;
		}

		/**
		 * Return a false ID when the SQL query is being constructed to avoid create a false positive with a page and
		 * add the virtual ID to the SQL query. Once this one has been added we need to remove the filter as is no
		 * longer required or used.
		 *
		 * This is done after we have results of the posts so the filter can be safely removed at this stage.
		 *
		 * @since 4.6.15
		 *
		 * @param $posts
		 *
		 * @return mixed
		 */
		public static function posts_results( $posts ) {
			if ( tribe( 'tec.front-page-view' )->is_page_on_front() ) {
				remove_filter( 'option_page_on_front', [ __CLASS__, 'default_page_on_front' ] );
			}

			return $posts;
		}

		/**
		 * Returns whether or not the event date & upcoming filters should be removed from the query
		 *
		 * @since 4.0
		 * @param WP_Query $query WP_Query object
		 * @return boolean
		 */
		public static function should_remove_date_filters( $query ) {
			// if the query flag to remove date filters is explicitly set then remove them
			if ( true === $query->get( 'tribe_remove_date_filters', false ) ) {
				return true;
			}

			// if we're doing ajax, let's keep the date filters
			if ( tribe( 'context' )->doing_ajax() ) {
				return false;
			}

			// otherwise, let's remove the date filters if we're in the admin dashboard and the query is
			// an event query on the tribe_events edit page
			return is_admin()
				&& $query->tribe_is_event_query
				&& Tribe__Admin__Helpers::instance()->is_screen( 'edit-' . Tribe__Events__Main::POSTTYPE );
		}

		/**
		 * Adds DISTINCT to the query.
		 *
		 * @param string $distinct The current DISTINCT statement.
		 *
		 * @return string The modified DISTINCT statement.
		 */
		public static function posts_distinct( $distinct ) {
			return 'DISTINCT';
		}

		/**
		 * Adds the proper fields to the FIELDS statement in the query.
		 *
		 * @param string   $field_sql The current/original FIELDS statement.
		 * @param WP_Query $query     The current query object.
		 *
		 * @return string The modified FIELDS statement.
		 */
		public static function posts_fields( $field_sql, $query ) {
			if (
				(
					! empty( $query->tribe_is_event )
					|| ! empty( $query->tribe_is_event_category )
				)
				&& self::can_inject_date_field( $query )
			) {
				$postmeta_table             = self::postmeta_table( $query );
				$fields                     = [];
				$fields['event_start_date'] = "MIN({$postmeta_table}.meta_value) as EventStartDate";
				$fields['event_end_date']   = 'MIN(tribe_event_end_date.meta_value) as EventEndDate';
				$fields                     = apply_filters( 'tribe_events_query_posts_fields', $fields, $query );

				return $field_sql . ', ' . implode( ', ', $fields );
			} else {
				return $field_sql;
			}
		}

		/**
		 * Adds the proper fields to the FIELDS statement in the query.
		 *
		 * @param string   $field_sql The current/original FIELDS statement.
		 * @param WP_Query $query     The current query object.
		 *
		 * @return string The modified FIELDS statement.
		 */
		public static function multi_type_posts_fields( $field_sql, $query ) {
			if (
				! empty( $query->tribe_is_multi_posttype )
				&& self::can_inject_date_field( $query )
			) {
				global $wpdb;
				$postmeta_table = self::postmeta_table( $query );
				$fields         = [];
				$fields[]       = "IF ({$wpdb->posts}.post_type = 'tribe_events', $postmeta_table.meta_value, {$wpdb->posts}.post_date) AS post_date";
				$fields         = apply_filters( 'tribe_events_query_posts_fields', $fields, $query );

				return $field_sql . ', ' . implode( ', ', $fields );
			} else {
				return $field_sql;
			}
		}

		/**
		 * Custom SQL join for event end date
		 *
		 * @param string   $join_sql
		 * @param wp_query $query
		 *
		 * @return string
		 */
		public static function posts_join( $join_sql, $query ) {
			global $wpdb;
			$joins = [];

			$postmeta_table = self::postmeta_table( $query );

			/**
			 * Which param will be queried in the Database for Events Date Start.
			 *
			 * @since  1.0
			 *
			 * @var string
			 */
			$event_start_key = Tribe__Events__Timezones::is_mode( 'site' )
				? '_EventStartDateUTC'
				: '_EventStartDate';

			/**
			 * Which param will be queried in the Database for Events Date End.
			 *
			 * @since  1.0
			 *
			 * @var string
			 */
			$event_end_key = Tribe__Events__Timezones::is_mode( 'site' )
				? '_EventEndDateUTC'
				: '_EventEndDate';

			/**
			 * When the "Use site timezone everywhere" option is checked in events settings,
			 * the UTC time for event start and end times will be used. This filter allows the
			 * disabling of that in certain contexts, so that local (not UTC) event times are used.
			 *
			 * @since 4.6.10
			 * @since 4.9    Commented out these lines below to always use UTC fields
			 *
			 * @param boolean $force_local_tz Whether to force the local TZ.
			 */
			/*
			$force_local_tz = apply_filters( 'tribe_events_query_force_local_tz', false );

			if ( Tribe__Events__Timezones::is_mode( 'site' ) && ! $force_local_tz ) {
				$event_start_key .= 'UTC';
				$event_end_key   .= 'UTC';
			}
			*/

			// if it's a true event query then we want create a join for where conditions
			if ( $query->tribe_is_event || $query->tribe_is_event_category || $query->tribe_is_multi_posttype ) {
				if ( $query->tribe_is_multi_posttype ) {
					// if we're getting multiple post types, we don't need the end date, just get the start date
					// for events-only post type queries, the start date postmeta join is already added by the main query args
					$joins['event_start_date'] = " LEFT JOIN {$wpdb->postmeta} as {$postmeta_table} on {$wpdb->posts}.ID = {$postmeta_table}.post_id AND {$postmeta_table}.meta_key = '$event_start_key'";
				} else {
					// for events-only post type queries, we should also get the end date for display
					$joins['event_end_date'] = " LEFT JOIN {$wpdb->postmeta} as tribe_event_end_date ON ( {$wpdb->posts}.ID = tribe_event_end_date.post_id AND tribe_event_end_date.meta_key = '$event_end_key' ) ";
				}
				$joins = apply_filters( 'tribe_events_query_posts_joins', $joins, $query );

				return $join_sql . implode( '', $joins );
			}

			return $join_sql;
		}

		/**
		 * Custom SQL conditional for event duration meta field
		 *
		 * @param string   $where_sql
		 * @param WP_Query $query
		 *
		 * @return string
		 */
		public static function posts_where( $where_sql, $query ) {
			global $wpdb;

			// if it's a true event query then we to setup where conditions
			// but only if we aren't grabbing a specific post
			if (
				(
					$query->tribe_is_event
					|| $query->tribe_is_event_category
				)
				&& empty( $query->query_vars['name'] )
				&& empty( $query->query_vars['p'] )
			) {

				$postmeta_table = self::postmeta_table( $query );

				$start_date = $query->get( 'start_date' );
				$end_date = $query->get( 'end_date' );
				$use_utc = Tribe__Events__Timezones::is_mode( 'site' );
				$site_tz = $use_utc ? Tribe__Events__Timezones::wp_timezone_string() : null;

				// Sitewide timezone mode: convert the start date - if set - to UTC
				if ( $use_utc && ! empty( $start_date ) ) {
					$start_date = Tribe__Events__Timezones::to_utc( $start_date, $site_tz );
				}

				// Sitewide timezone mode: convert the end date - if set - to UTC
				if ( $use_utc && ! empty( $end_date ) ) {
					$end_date = Tribe__Events__Timezones::to_utc( $end_date, $site_tz );
				}

				// we can't store end date directly because it messes up the distinct clause
				$event_end_date = apply_filters( 'tribe_events_query_end_date_column', 'tribe_event_end_date.meta_value' );

				// event start date
				$event_start_date = "{$postmeta_table}.meta_value";

				// build where conditionals for events if date range params are set
				if ( $start_date != '' && $end_date != '' ) {
					$start_clause  = $wpdb->prepare( "($event_start_date >= %s AND $event_start_date <= %s)", $start_date, $end_date );
					$end_clause    = $wpdb->prepare( "($event_end_date >= %s AND $event_start_date <= %s )", $start_date, $end_date );
					$within_clause = $wpdb->prepare( "($event_start_date < %s AND $event_end_date >= %s )", $start_date, $end_date );
					$where_sql .= " AND ($start_clause OR $end_clause OR $within_clause)";
				} elseif ( 'future' === $query->get( 'eventDisplay' ) && '' !== $start_date ) {
					$start_clause = $wpdb->prepare( "{$postmeta_table}.meta_value >= %s", $start_date );
					$where_sql   .= " AND ($start_clause)";
				} else {
					if ( $start_date != '' ) {
						$start_clause  = $wpdb->prepare( "{$postmeta_table}.meta_value >= %s", $start_date );
						$within_clause = $wpdb->prepare( "({$postmeta_table}.meta_value <= %s AND $event_end_date >= %s )", $start_date, $start_date );
						$where_sql .= " AND ($start_clause OR $within_clause)";
						if ( $query->is_singular() && $query->get( 'eventDate' ) ) {
							$tomorrow        = date( 'Y-m-d', strtotime( $query->get( 'eventDate' ) . ' +1 day' ) );
							$tomorrow_clause = $wpdb->prepare( "{$postmeta_table}.meta_value < %s", $tomorrow );
							$where_sql .= " AND $tomorrow_clause";
						}
					} else {
						if ( $end_date != '' ) {
							$where_sql .= ' AND ' . $wpdb->prepare( "$event_end_date < %s", $end_date );
						}
					}
				}
			}

			remove_filter( 'option_page_on_front', [ __CLASS__, 'default_page_on_front' ] );

			return $where_sql;
		}

		/**
		 * Internal method for properly setting a curated orderby value to $wp_query
		 * Internal method for properly setting a curated orderby value to $wp_query.
		 *
		 * If optional param $default is not provided it will default to 'event_date' - unless a custom
		 * orderby param was specified (via tribe_get_events() for example) - in which case that value
		 * will be used.
		 *
		 * @param string   $default
		 * @param WP_Query $query
		 *
		 * @return string
		 */
		public static function set_orderby( $default = null, $query = null ) {
			// What should $default be?
			if ( null === $default && isset( $query->query['orderby'] ) ) {
				$default = $query->query['orderby'];
			} elseif ( null === $default ) {
				$default = 'event_date';
			}

			$url_param = ! empty( $_GET['orderby'] ) ? $_GET['orderby'] : null;
			$url_param = ! empty( $_GET['tribe-orderby'] ) ? $_GET['tribe-orderby'] : $url_param;
			$url_param = strtolower( $url_param );

			switch ( $url_param ) {
				case 'tribe_sort_ecp_venue_filter':
					$orderby = 'venue';
					break;
				case 'tribe_sort_ecp_organizer_filter':
					$orderby = 'organizer';
					break;
				case 'title':
					$orderby = $url_param;
					break;
				default:
					$orderby = $default;
					break;
			}

			return $orderby;
		}

		/**
		 * Internal method for properly setting a curated order value to $wp_query.
		 *
		 * If optional param $default is not provided it will default to 'ASC' - unless a custom order
		 * was specified (via tribe_get_events() for example) - in which case that value will be used.
		 *
		 * @param string   $default
		 * @param WP_Query $query
		 *
		 * @return string
		 */
		public static function set_order( $default = null, $query = null ) {
			// As of WordPress 4.2 'order' can be set via the 'orderby' as an associative array
			if ( ! empty( $query->query['orderby'] ) && is_array( $query->query['orderby'] ) && ! empty( $query->query['orderby']['meta_value'] ) ) {
				$default = $query->query['orderby']['meta_value'];
			} elseif ( ! empty( $query->query['order'] ) ) {
				$default = $query->query['order'];
			} elseif ( null === $default ) {
				$default = 'ASC';
			}

			$url_param = ! empty( $_GET['order'] ) ? $_GET['order'] : null;
			$url_param = ! empty( $_GET['tribe-order'] ) ? $_GET['tribe-order'] : $url_param;
			$url_param = strtoupper( $url_param );

			$order = in_array( $url_param, [ 'ASC', 'DESC' ] ) ? $url_param : $default;

			return $order;
		}

		/**
		 * Custom SQL order by statement for Event Start Date result order.
		 *
		 * @param string   $order_sql
		 * @param wp_query $query
		 *
		 * @return string
		 */
		public static function posts_orderby( $order_sql, $query ) {
			// If this is set then the class will bail out of any filtering.
			if ( $query->get( 'tribe_suppress_query_filters', false ) ) {
				return $order_sql;
			}

			global $wpdb;

			if ( $query->tribe_is_event || $query->tribe_is_event_category ) {
				$order   = ( isset( $query->query_vars['order'] ) && ! empty( $query->query_vars['order'] ) ) ? $query->query_vars['order'] : $query->get( 'order' );
				$orderby = ( isset( $query->query_vars['orderby'] ) && ! empty( $query->query_vars['orderby'] ) ) ? $query->query_vars['orderby'] : $query->get( 'orderby' );

				if ( self::can_inject_date_field( $query ) && ! $query->tribe_is_multi_posttype ) {
					$old_orderby = $order_sql;
					$order_sql = "EventStartDate {$order}";

					if ( $old_orderby ) {
						$order_sql .= ", {$old_orderby}";
					}
				}

				do_action( 'log', 'orderby', 'default', $orderby );

				switch ( $orderby ) {
					case 'title':
						$order_sql = "{$wpdb->posts}.post_title {$order}, " . $order_sql;
						break;
					case 'menu_order':
						$order_sql = "{$wpdb->posts}.menu_order ASC, " . $order_sql;
						break;
					case 'event_date':
						// we've already setup $order_sql
						break;
					case 'rand':
						$order_sql = 'RAND()';
						break;
				}

				// trim trailing characters
				$order_sql = trim( $order_sql, ", \t\n\r\0\x0B" );
			} else {
				if ( $query->tribe_is_multi_posttype && self::can_inject_date_field( $query ) ) {
					if ( $query->get( 'orderby' ) == 'date' || $query->get( 'orderby' ) == '' ) {
						$order_sql = str_replace( "$wpdb->posts.post_date", 'post_date', $order_sql );
					}
				}
			}

			$order_sql = apply_filters( 'tribe_events_query_posts_orderby', $order_sql, $query );

			return $order_sql;
		}

		/**
		 * Adds a custom SQL join when ordering by venue or organizer is desired.
		 *
		 * @param string   $join_sql
		 * @param wp_query $query
		 *
		 * @return string
		 */
		public static function posts_join_venue_organizer( $join_sql, $query ) {
			// bail if this is not a query for event post type
			if ( $query->get( 'post_type' ) !== Tribe__Events__Main::POSTTYPE ) {
				return $join_sql;
			}

			global $wpdb;

			switch ( $query->get( 'orderby' ) ) {
				case 'venue':
					$join_sql .= " LEFT JOIN {$wpdb->postmeta} tribe_order_by_venue_meta ON {$wpdb->posts}.ID = tribe_order_by_venue_meta.post_id AND tribe_order_by_venue_meta.meta_key='_EventVenueID' LEFT JOIN {$wpdb->posts} tribe_order_by_venue ON tribe_order_by_venue_meta.meta_value = tribe_order_by_venue.ID ";
					break;
				case 'organizer':
					$join_sql .= " LEFT JOIN {$wpdb->postmeta} tribe_order_by_organizer_meta ON {$wpdb->posts}.ID = tribe_order_by_organizer_meta.post_id AND tribe_order_by_organizer_meta.meta_key='_EventOrganizerID' LEFT JOIN {$wpdb->posts} tribe_order_by_organizer ON tribe_order_by_organizer_meta.meta_value = tribe_order_by_organizer.ID ";
					break;
				default:
					return $join_sql;
					break;
			}

			/**
			 * Ensures we add the matching corresponding code to the order clause.
			 *
			 * We use posts_clauses (as opposed to posts_orderby) in this case to avoid
			 * it being overwritten by Tribe__Events__Admin_List methods.
			 *
			 * @see Tribe__Events__Admin_List
			 */
			add_filter( 'posts_clauses', [ __CLASS__, 'posts_orderby_venue_organizer' ], 100, 2 );

			if ( has_filter( 'tribe_events_query_posts_join_orderby' ) ) {
				/**
				 * Historically this filter has only been useful to modify the joins setup
				 * in relation to organizers and venues. In future it may have a more general
				 * application or may be removed.
				 *
				 * @deprecated since 4.0.2
				 *
				 * @var string $join_sql
				 */
				$join_sql = apply_filters( 'tribe_events_query_posts_join_orderby', $join_sql );

				_doing_it_wrong(
					'tribe_events_query_posts_join_orderby (filter)',
					'To modify joins in relation to venues and organizers specifically you are encouraged to use the tribe_events_query_posts_join_venue_organizer hook.',
					'4.0.2'
				);
			}

			/**
			 * Provides an opportunity to modify the join condition added to the query when
			 * ordering by venue or organizer.
			 *
			 * @var string $join_sql
			 */
			return apply_filters( 'tribe_events_query_posts_join_venue_organizer', $join_sql );
		}

		/**
		 * Appends the necessary conditions to the order clause to sort by either venue or
		 * organizer.
		 *
		 * @param  array    $clauses
		 * @param  WP_Query $query
		 * @return string
		 */
		public static function posts_orderby_venue_organizer( array $clauses, $query ) {
			// This filter has to be added for every individual query that requires it
			remove_filter( 'posts_clauses', [ __CLASS__, 'posts_orderby_venue_organizer' ], 100 );

			$order   = ( isset( $query->order ) && ! empty( $query->order ) ) ? $query->order : $query->get( 'order' );
			$orderby = ( isset( $query->orderby ) && ! empty( $query->orderby ) ) ? $query->orderby : $query->get( 'orderby' );

			switch ( $orderby ) {
				case 'venue':
					$clauses['orderby'] = "tribe_order_by_venue.post_title {$order}, " . $clauses['orderby'];
					break;
				case 'organizer':
					$clauses['orderby'] = "tribe_order_by_organizer.post_title {$order}, " . $clauses['orderby'];
					break;
				default:
					return $clauses;
					break;
			}

			// Trim trailing characters
			$clauses['orderby'] = trim( $clauses['orderby'], ", \t\n\r\0\x0B" );

			return $clauses;
		}

		/**
		 * Custom SQL to retrieve post_id list of events marked to be hidden from upcoming lists.
		 *
		 * @return array
		 */
		public static function getHideFromUpcomingEvents() {
			global $wpdb;

			/** @var Tribe__Cache $cache */
			$cache     = tribe( 'cache' );
			$cache_key = 'tribe-hide-from-upcoming-events';
			$found     = $cache->get( $cache_key, 'save_post' );
			if ( is_array( $found ) ) {
				return $found;
			}

			// custom sql to get ids of posts that hide_upcoming_ids
			$hide_upcoming_ids = $wpdb->get_col( "SELECT {$wpdb->postmeta}.post_id FROM {$wpdb->postmeta} WHERE {$wpdb->postmeta}.meta_key = '_EventHideFromUpcoming' AND {$wpdb->postmeta}.meta_value = 'yes'" );
			$hide_upcoming_ids = apply_filters( 'tribe_events_hide_from_upcoming_ids', $hide_upcoming_ids );
			$cache->set( $cache_key, $hide_upcoming_ids, HOUR_IN_SECONDS, 'save_post' );

			return $hide_upcoming_ids;
		}

		/**
		 * Gets the event counts for individual days.
		 *
		 * @param array $args
		 *
		 * @return array The counts array.
		 */
		public static function getEventCounts( $args = [] ) {
			_deprecated_function( __METHOD__, '3.10.1' );
			global $wpdb;
			$date     = date( 'Y-m-d' );
			$defaults = [
				'post_type'         => Tribe__Events__Main::POSTTYPE,
				'start_date'        => tribe_beginning_of_day( $date ),
				'end_date'          => tribe_end_of_day( $date ),
				'display_type'      => 'daily',
				'hide_upcoming_ids' => null,
			];
			$args     = wp_parse_args( $args, $defaults );

			$args['posts_per_page'] = - 1;
			$args['fields']         = 'ids';

			// remove empty args and sort by key, this increases chance of a cache hit
			$args = array_filter( $args, [ __CLASS__, 'filter_args' ] );
			ksort( $args );

			/** @var Tribe__Cache $cache */
			$cache     = tribe( 'cache' );
			$cache_key = 'daily_counts_and_ids_' . serialize( $args );
			$found     = $cache->get( $cache_key, 'save_post' );
			if ( $found ) {

				return $found;
			}

			$cache_key = 'month_post_ids_' . serialize( $args );
			$found     = $cache->get( $cache_key, 'save_post' );
			if ( $found && is_array( $found ) ) {
				$post_ids = $found;
			} else {
				$post_id_query = new WP_Query();
				$post_ids      = $post_id_query->query( $args );
				$cache->set( $cache_key, $post_ids, Tribe__Cache::NON_PERSISTENT, 'save_post' );
			}

			$counts    = [];
			$event_ids = [];
			if ( ! empty( $post_ids ) ) {
				switch ( $args['display_type'] ) {
					case 'daily':
					default :
						$wp_query = tribe_get_global_query_object();

						$output_date_format = '%Y-%m-%d %H:%i:%s';
						$start_date_sql = esc_sql( $post_id_query->query_vars['start_date'] );
						$end_date_sql = esc_sql( $post_id_query->query_vars['end_date'] );
						$sql_ids = esc_sql( implode( ',', array_map( 'intval', $post_ids ) ) );

						$raw_counts = $wpdb->get_results( "
							SELECT 	tribe_event_start.post_id as ID,
									tribe_event_start.meta_value as EventStartDate,
									DATE_FORMAT( tribe_event_end_date.meta_value, '{$output_date_format}') as EventEndDate,
									{$wpdb->posts}.menu_order as menu_order
							FROM $wpdb->postmeta AS tribe_event_start
									LEFT JOIN $wpdb->posts ON (tribe_event_start.post_id = {$wpdb->posts}.ID)
							LEFT JOIN $wpdb->postmeta as tribe_event_end_date ON ( tribe_event_start.post_id = tribe_event_end_date.post_id AND tribe_event_end_date.meta_key = '_EventEndDate' )
							WHERE tribe_event_start.meta_key = '_EventStartDate'
							AND tribe_event_start.post_id IN ( {$sql_ids} )
							AND (
								(
									tribe_event_start.meta_value >= '{$start_date_sql}'
									AND tribe_event_start.meta_value <= '{$end_date_sql}'
								)
								OR (
									tribe_event_end_date.meta_value >= '{$start_date_sql}'
									AND tribe_event_end_date.meta_value <= '{$end_date_sql}'
								)
								OR (
									tribe_event_start.meta_value < '{$start_date_sql}'
									AND tribe_event_end_date.meta_value > '{$end_date_sql}'
								)
							)
							ORDER BY menu_order ASC, DATE(tribe_event_start.meta_value) ASC, TIME(tribe_event_start.meta_value) ASC;"
						);
						$start_date = new DateTime( $post_id_query->query_vars['start_date'] );
						$end_date   = new DateTime( $post_id_query->query_vars['end_date'] );
						$days       = Tribe__Date_Utils::date_diff( $start_date->format( 'Y-m-d' ), $end_date->format( 'Y-m-d' ) );
						$term_id = isset( $wp_query->query_vars[ Tribe__Events__Main::TAXONOMY ] ) ? $wp_query->query_vars[ Tribe__Events__Main::TAXONOMY ] : null;
						$terms = [];
						if ( is_int( $term_id ) ) {
							$terms[0] = $term_id;
						} elseif ( is_string( $term_id ) ) {
							$term = get_term_by( 'slug', $term_id, Tribe__Events__Main::TAXONOMY );
							if ( $term ) {
								$terms[0] = $term->term_id;
							}
						}
						if ( ! empty( $terms ) && is_tax( Tribe__Events__Main::TAXONOMY ) ) {
							$terms = array_merge( $terms, get_term_children( $terms[0], Tribe__Events__Main::TAXONOMY ) );
						}
						for ( $i = 0, $date = $start_date; $i <= $days; $i ++, $date->modify( '+1 day' ) ) {
							$formatted_date = $date->format( 'Y-m-d' );
							$count          = 0;
							$_day_event_ids = [];
							foreach ( $raw_counts as $record ) {

								$event = new stdClass;
								$event->EventStartDate = $record->EventStartDate;
								$event->EventEndDate = $record->EventEndDate;

								$per_day_limit = apply_filters( 'tribe_events_month_day_limit', tribe_get_option( 'monthEventAmount', '3' ) );

								if ( tribe_event_is_on_date( $formatted_date, $event ) ) {
									if ( ! empty ( $terms ) && ! has_term( $terms, Tribe__Events__Main::TAXONOMY, $record->ID ) ) {
										continue;
									}
									if ( count( $_day_event_ids ) < $per_day_limit ) {
										$_day_event_ids[] = $record->ID;
									}
									$count ++;
								}
							}
							$event_ids[ $formatted_date ] = $_day_event_ids;
							$counts[ $formatted_date ]    = $count;
						}
						break;
				}

				// get a unique list of the event IDs that will be displayed, and update all their postmeta and term caches at once
				$final_event_ids = call_user_func_array( 'array_merge', $event_ids );
				$final_event_ids = array_unique( $final_event_ids );
				update_object_term_cache( $final_event_ids, Tribe__Events__Main::POSTTYPE );
				update_postmeta_cache( $final_event_ids );
			}

			// return IDs per day and total counts per day
			$return = [ 'counts' => $counts, 'event_ids' => $event_ids ];

			/** @var Tribe__Cache $cache */
			$cache     = tribe( 'cache' );
			$cache_key = 'daily_counts_and_ids_' . serialize( $args );
			$cache->set( $cache_key, $return, Tribe__Cache::NON_PERSISTENT, 'save_post' );

			return $return;
		}

		/**
		 * Customized WP_Query wrapper to setup event queries with default arguments.
		 *
		 * @param array $args {
		 *		Optional. Array of Query parameters.
		 *
		 *      @type bool $found_posts Return the number of found events.
		 * }
		 * @param bool  $full Whether the full WP_Query object should returned (`true`) or just the
		 *                    found posts (`false`)
		 *
		 * @return array|WP_Query
		 */
		public static function getEvents( $args = [], $full = false ) {
			$defaults = [
				'orderby'              => 'event_date',
				'order'                => 'ASC',
				'posts_per_page'       => tribe_get_option( 'posts_per_page', tribe_get_option( 'postsPerPage', get_option( 'posts_per_page', 10 ) ) ),
				'tribe_render_context' => 'default',
			];

			$args = wp_parse_args( $args, $defaults );
			$event_display = tribe_get_request_var(
				'tribe_event_display',
				Tribe__Utils__Array::get( $args, 'eventDisplay', false )
			);

			$search = tribe_get_request_var( 'tribe-bar-search' );

			/**
			 * @todo Move this to each one of the views and their ajax requests
			 */
			// if a user provides a search term we want to use that in the search params
			if ( ! empty( $search ) ) {
				$args['s'] = $search;
			}

			$return_found_posts = ! empty( $args['found_posts'] );

			if ( $return_found_posts ) {
				unset( $args['found_posts'] );

				$args['posts_per_page'] = 1;
				$args['paged']          = 1;
			}

			// remove empty args and sort by key, this increases chance of a cache hit
			$args = array_filter( $args, [ __CLASS__, 'filter_args' ] );
			ksort( $args );

			/** @var Tribe__Cache $cache */
			$cache     = tribe( 'cache' );
			$cache_key = 'get_events_' . get_current_user_id() . serialize( $args );

			$result = $cache->get( $cache_key, 'save_post' );

			if (
				false !== $result
				&& (
					$result instanceof WP_Query
					|| (
						$return_found_posts
						&& is_int( $result )
					)
				)
			) {
				do_action( 'log', 'cache hit', 'tribe-events-cache', $args );
			} else {
				do_action( 'log', 'no cache hit', 'tribe-events-cache', $args );

				/** @var Tribe__Events__Repositories__Event $event_orm */
				$event_orm = tribe_events();

				$hidden = false;

				if ( isset( $args['tribe_render_context'] ) ) {
					$event_orm->set_render_context( $args['tribe_render_context'] );
				}

				if ( ! empty( $event_display ) ) {
					$event_orm->set_display_context( $event_display );
				}

				// Backcompat defaults.
				if ( isset( $args['hide_upcoming'] ) ) {
					// Negate the hide_upcoming for $hidden
					if ( true !== (boolean) $args['hide_upcoming'] ) {
						$hidden = null;
					}

					unset( $args['hide_upcoming'] );
				}

				$display = Arr::get( $args, 'eventDisplay' );
				$has_date_args = array_filter( [
					Arr::get( $args, 'start_date' ),
					Arr::get( $args, 'startDate' ),
					Arr::get( $args, 'starts_after' ),
					Arr::get( $args, 'starts_before' ),
					Arr::get( $args, 'end_date' ),
					Arr::get( $args, 'endDate' ),
					Arr::get( $args, 'ends_after' ),
					Arr::get( $args, 'ends_before' ),
				] );

				// Support for `eventDisplay = 'upcoming' || 'list'` for backwards compatibility
				if (
					! $has_date_args
					&& in_array( $display, [ 'upcoming', 'list' ] )
				) {
					if ( empty( $args['tribe_is_past'] ) ) {
						$args['start_date'] = 'now';
					}
					unset( $args['eventDisplay'] );
				}

				// Support for `eventDisplay = 'day'` for backwards compatibility
				if (
					! $has_date_args
					&& in_array( $display, [ 'day' ] )
				) {
					$args['start_date'] = 'today';
					unset( $args['eventDisplay'] );
				}

				// Support `tribeHideRecurrence` old param
				if ( isset( $args['tribeHideRecurrence'] ) ) {
					$args['hide_subsequent_recurrences'] = $args['tribeHideRecurrence'];
					unset( $args['tribeHideRecurrence'] );
				}

				if ( isset( $args['start_date'] ) && false === $args['start_date'] ) {
					unset( $args['start_date'] );
				}

				if ( isset( $args['end_date'] ) && false === $args['end_date'] ) {
					unset( $args['end_date'] );
				}

				if ( isset( $args['eventDate'] ) && ! isset( $args['start_date'], $args['end_date'] ) ) {
					$args['on_date'] = $args['eventDate'];
					unset( $args['eventDate'] );
				}

				if ( ! empty( $args['orderby'] ) ) {
					$event_orm->order_by( $args['orderby'] );

					unset( $args['orderby'] );
				}

				if ( 'all' === $event_display  ) {
					if ( empty( $args['post_parent'] ) ) {
						// Make sure the `post_parent` ID is set in /all requests.
						$parent_name = Tribe__Utils__Array::get(
							$args,
							'name',
							Tribe__Utils__Array::get( $args, 'tribe_events', false )
						);

						if ( ! empty( $parent_name ) ) {
							$post_parent         = tribe_events()->where( 'name', $parent_name )->fields( 'ids' )
							                                     ->first();
							$args['post_parent'] = $post_parent;
						}

						// Make sure these are unset to avoid 'post_name' comparisons.
						unset( $args['name'], $args['post_name'], $args['tribe_events'] );
					}

					if ( class_exists( 'Tribe__Events__Pro__Recurrence__Event_Query' ) ) {
						$recurrence_query = new Tribe__Events__Pro__Recurrence__Event_Query();
						$parent_post      = get_post( $args['post_parent'] );
						if ( $parent_post instanceof WP_Post ) {
							$recurrence_query->set_parent_event( $parent_post );
							add_filter( 'posts_where', [ $recurrence_query, 'include_parent_event' ], 100 );
						}
					}
				}

				$is_past = ! empty( $args['tribe_is_past'] ) || 'past' === $event_display;
				if ( $is_past ) {
					$args['order'] = 'DESC';
					/*
					 * If in the context of a "past" view let's try to use, as limit, the same
					 * end date limit passed, if any.
					 */
					$now = isset( $args['ends_before'] ) ? $args['ends_before'] : 'now';
					$pivot_date = tribe_get_request_var( 'tribe-bar-date', $now );
					$date       = Tribe__Date_Utils::build_date_object( $pivot_date );
					// Remove any existing date meta queries.
					if ( isset( $args['meta_query'] ) ) {
						$args['meta_query'] = tribe_filter_meta_query(
							$args['meta_query'],
							[ 'key' => '/_Event(Start|End)Date(UTC)/' ]
						);
					}

					/**
					 * We used to use the `tribe_beginning_of_day` for part of the query.
					 *
					 * Intentionally changed the behavior here to use "now" as part of the code
					 *
					 * @link https://central.tri.be/issues/123950
					 */
					$args['starts_before'] = $date->format( Tribe__Date_Utils::DBDATETIMEFORMAT );
				}

				if ( null !== $hidden ) {
					$event_orm->by( 'hidden', $hidden );
					if ( isset( $args['meta_query'] ) ) {
						$args['meta_query'] = tribe_filter_meta_query(
							$args['meta_query'],
							[ 'key' => '_EventHideFromUpcoming' ]
						);
					}
				}

				/**
				 * Some key arguments have been passed as arrays but will require unpacking.
				 * Due to the dynamic nature of the ORM implementation this is a curated list
				 * that should be updated here. Do not try to move this conditional unpacking logic
				 * in the ORM: this is an issue the proxy function should handle ad-hoc.
				 */
				$requiring_unpack = [ 'date_overlaps', 'runs_between' ];
				foreach ( array_intersect( array_keys( $args ), $requiring_unpack ) as $key ) {
					$event_orm->by( $key, ...$args[ $key ] );
					unset( $args[ $key ] );
				}

				$event_orm->by_args( $args );

				if ( $return_found_posts ) {
					$result = $event_orm->found();
				} else {
					$result = $event_orm->get_query();

					// Set the event display, if any, for back-compatibility purposes.
					if ( ! empty( $event_display ) ) {
						$result->set( 'eventDisplay', $event_display );
					}

					// Run the query.
					$result->get_posts();
					self::$last_result = empty( $result->posts ) ? [] : $result->posts;
				}

				$cache->set( $cache_key, $result, Tribe__Cache::NON_PERSISTENT, 'save_post' );
			}


			if ( $return_found_posts ) {
				return $result;
			}

			if ( ! empty( $result->posts ) ) {
				self::$last_result = empty( $result->posts ) ? [] : $result->posts;
				if ( $full ) {
					return $result;
				}
				return $result->posts;
			}

			if ( $full ) {
				self::$last_result = empty( $result->posts ) ? [] : $result->posts;
				return $result;
			}

			self::$last_result = [];
			return [];
		}

		/**
		 * Determine what postmeta table should be used,
		 * to avoid conflicts with previous postmeta joins
		 *
		 * @return string
		 **/
		private static function postmeta_table( $query ) {
			/** @var \wpdb $wpdb */
			global $wpdb;
			$postmeta_table = $wpdb->postmeta;

			if ( ! $query->tribe_is_multi_posttype ) {
				return $postmeta_table;
			}

			$qv = $query->query_vars;

			// check if are any meta queries
			if ( ! empty( $qv['meta_key'] ) ) {
				$postmeta_table = 'tribe_event_postmeta';
			} else {
				if ( isset( $qv['meta_query'] ) ) {
					if (
						( is_array( $qv['meta_query'] ) && ! empty( $qv['meta_query'] ) ) ||
						( $qv['meta_query'] instanceof WP_Meta_Query && ! empty( $qv['meta_query']->queries ) )
					) {
						$postmeta_table = 'tribe_event_postmeta';
					}
				} else {
					$postmeta_table = $wpdb->postmeta;
				}
			}

			return $postmeta_table;
		}

		/**
		 * Remove empty values from the query args
		 *
		 * @param mixed $arg
		 *
		 * @return bool
		 **/
		private static function filter_args( $arg ) {
			if ( empty( $arg ) && $arg !== false && 0 !== $arg ) {
				return false;
			}

			return true;
		}

		/**
		 * If the user has the Main events page set on the reading options it should return 0 or the default value in
		 * order to avoid to set the:
		 * - p
		 * - page_id
		 *
		 * variables when using  pre_get_posts or posts_where
		 *
		 * This filter is removed when this functions has finished the execution
		 *
		 * @since 4.6.15
		 *
		 * @param $value
		 *
		 * @return int
		 */
		public static function default_page_on_front( $value ) {
			return tribe( 'tec.front-page-view' )->is_virtual_page_id( $value ) ? 0 : $value;
		}

		/**
		 * Reruns the last query used to `getEvents` to fetch
		 * all the found IDs.
		 *
		 * Pagination is ignored; this methods provides a way to
		 * not only count the found posts but to get their ID too.
		 *
		 * @since 4.9.4
		 *
		 * @return array
		 */
		public static function last_found_events() {
			return self::$last_result;
		}
	}
}
