<?php
/**
 * @for     Calendar Template
 * This file contains the hook logic required to create an effective calendar month view.
 *
 * @package TribeEventsCalendar
 *
 */

if ( ! defined( 'ABSPATH' ) ) {
	die( '-1' );
}

if ( ! class_exists( 'Tribe__Events__Template__Month' ) ) {
	/**
	 * Month view template class
	 */
	class Tribe__Events__Template__Month extends Tribe__Events__Template_Factory {
		/**
		 * Month Type Masks
		 */
		const PREVIOUS_MONTH = - 1;
		const CURRENT_MONTH = 0;
		const NEXT_MONTH = 1;

		/**
		 * Prefix for month view ajax actions
		 */
		const AJAX_HOOK = 'tribe_calendar';

		/**
		 * The path to the template file used for the view.
		 * This value is used in Shortcodes/Tribe_Events.php to
		 * locate the correct template file for each shortcode
		 * view.
		 *
		 * @var string
		 */
		public $view_path = 'month/content';

		/**
		 * Number of events per day
		 * @var int
		 * @see tribe_events_month_day_limit
		 */
		private $events_per_day;

		/**
		 * Grid day events
		 * @var array
		 */
		private $event_ids_by_day;

		/**
		 * Array of days of the month
		 * @var array
		 */
		private static $calendar_days = [];

		/**
		 * Internal pointer to current day in the month view loop
		 * @var int
		 */
		private static $current_day = - 1;

		/**
		 * Internal pointer to current week in the month view loop
		 * @var int
		 */
		private static $current_week = - 1;

		/**
		 * Query args
		 * @var array|null
		 */
		protected $args;

		/**
		 * Indicates the array index marking the first entry for the current month.
		 * @var int
		 */
		protected $current_month_begins;

		/**
		 * Indicates the array index marking the last entry for the current month.
		 * @var int
		 */
		protected $current_month_ends;

		/**
		 * CSS class for the month view wrapper
		 * @var string
		 */
		protected $body_class = 'events-gridview';

		/**
		 * Excerpt length on month view tooltips
		 * @var int
		 */
		protected $excerpt_length = 30;

		/**
		 * Static asset packages required for month view functionality
		 * @var array
		 */
		protected $asset_packages = [];

		/**
		 * HTML cache holder
		 * @var Tribe__Template_Part_Cache
		 */
		private $html_cache;

		/**
		 * Number of seconds before the month view cache (when enabled) should be
		 * invalidated.
		 *
		 * @var int
		 */
		private $cache_expiration;

		/**
		 * Whether the HTML cache is enabled
		 * @var boolean
		 */
		private $use_cache;

		/**
		 * The events in this month
		 * @var
		 */
		private $events_in_month;

		/**
		 * The category being viewed on month view
		 * @var
		 */
		private $queried_event_cats;

		/**
		 * The month date that was requested
		 * @var string
		 */
		private $requested_date;

		/**
		 * The first date to show on the calendar grid (may be in the previous month)
		 * @var bool|string
		 */
		private $first_grid_date;

		/**
		 * The last date to show on the calendar grid (may be in the next month)
		 * @var bool|string
		 */
		private $final_grid_date;


		/**
		 * Set the notices used on month view.
		 *
		 * @param array $args Set of $wp_query params for the month view, if none passed then will default to $wp_query.
		 */
		public function __construct( $args = [] ) {

			// set the proper query args
			$this->set_args( $args );

			// include child categories in the query, save categories for reuse
			$this->set_queried_event_cats();

			/**
			 * Controls whether or not month view caching is enabled.
			 *
			 * Filtering this value can be useful if you need to implement
			 * a fine grained caching policy for month view.
			 *
			 * @param boolean $enable
			 * @param array   $args
			 */
			$this->use_cache = apply_filters(
				'tribe_events_enable_month_view_cache',
				$this->should_enable_month_view_cache(),
				$this->args
			);

			// Cache the result of month/content.php
			if ( $this->use_cache ) {
				$this->cache_expiration = apply_filters( 'tribe_events_month_view_transient_expiration', HOUR_IN_SECONDS );

				$cache_id = serialize( $this->args );
				if ( isset( $_REQUEST ) && is_array( $_REQUEST ) ) {
					$cache_id .= serialize( $_REQUEST );
				}
				$cache_id = md5( $cache_id );
				$this->html_cache = new Tribe__Template_Part_Cache( 'month/content.php', $cache_id, $this->cache_expiration, 'save_post' );
			}

			$this->events_per_day  = apply_filters( 'tribe_events_month_day_limit', tribe_get_option( 'monthEventAmount', '3' ) );
			$this->requested_date  = $this->requested_date();
			$this->first_grid_date = self::calculate_first_cell_date( $this->requested_date );
			$this->final_grid_date = self::calculate_final_cell_date( $this->requested_date );


			// get all the ids for the events in this month, speeds up queries
			$this->set_events_in_month();

			// don't enqueue scripts and js when we're not constructing month view,
			// they'll have to be enqueued separately
			if ( ! tribe_is_month() ) {
				$this->asset_packages = [];
			}

			parent::__construct();
		}

		/**
		 * Indicates if month view cache should be enabled or not.
		 *
		 * If the month view cache setting itself is not enabled (or not set) then this
		 * method will always return false.
		 *
		 * In other cases, the default rules are to cache everything in the 2 months past
		 * to 12 months in the future range. This policy can be refined or replaced via
		 * the 'tribe_events_enable_month_view_cache' filter hook.
		 *
		 * @return bool
		 */
		protected function should_enable_month_view_cache() {
			// Respect the month view caching setting
			if ( ! tribe_get_option( 'enable_month_view_cache', true ) ) {
				return false;
			}

			// Default to always caching the current month
			if ( ! isset( $this->args['eventDate'] ) ) {
				return true;
			}

			// If the eventDate argument is not in the expected format then do not cache
			if ( ! preg_match( '/^[0-9]{4}-[0-9]{1,2}$/', $this->args['eventDate'] ) ) {
				return false;
			}

			// If the requested month is more than 2 months in the past, do not cache
			if ( $this->args['eventDate'] < date_i18n( 'Y-m', Tribe__Date_Utils::wp_strtotime( '-2 months' ) ) ) {
				return false;
			}

			// If the requested month is more than 1yr in the future, do not cache
			if ( $this->args['eventDate'] > date_i18n( 'Y-m', Tribe__Date_Utils::wp_strtotime( '+1 year' ) ) ) {
				return false;
			}

			// In all other cases, let's cache it!
			return true;
		}

		/**
		 * Returns an array containing the IDs of all the events in the month.
		 *
		 * @return array
		 */
		public function get_events_in_month_ids() {
			return $this->has_events() ? wp_list_pluck( $this->events_in_month, 'ID' ) : [];
		}

		/**
		 * Add any special hooks for this view
		 * any actions added here should also be removed in the unhook function
		 *
		 */
		protected function hooks() {
			parent::hooks();

			tribe_asset_enqueue( 'the-events-calendar' );

			// Since we set is_post_type_archive to true on month view, this prevents 'Events' from being added to the page title
			add_filter( 'post_type_archive_title', '__return_false', 10 );

			if ( ! empty( $this->events_in_month ) ) {
				add_filter( 'tribe_events_month_has_events', [ $this, 'has_events' ] );
			}

			// Print JSON-LD markup on the `wp_head`
			add_action( 'wp_head', [ $this, 'json_ld_markup' ] );
		}

		/**
		 * When dealing with a place that has multiple events, we need to pass all the events as the first param
		 * to allow the class to echo the correct JSON-LD script
		 *
		 * @return void
		 */
		public function json_ld_markup() {

			if ( ! $this->use_cache ) {
				$this->produce_json_ld_markup( true );
				return;
			}

			$cache     = new Tribe__Cache();
			$prefix = 'events_month_jsonld';
			if ( tribe_is_event_category() ) {
				$prefix = sprintf( 'events_cat_%d_month_jsonld', get_queried_object_id() );
			}
			$cache_key = $this->get_month_view_cache_key( $prefix );
			$json_ld   = $cache->get_transient( $cache_key, 'save_post' );

			if ( ! $json_ld ) {
				$json_ld = $this->produce_json_ld_markup();
				$cache->set_transient( $cache_key, $json_ld, $this->cache_expiration, 'save_post' );
			}

			echo $json_ld;
		}

		/**
		 * Renders or returns the JSON LD markup.
		 *
		 * @param bool $echo
		 *
		 * @return string
		 */
		protected function produce_json_ld_markup( $echo = false ) {
			if ( ! $echo ) {
				ob_start();
			}

			$events = wp_list_pluck( $this->events_in_month, 'ID' );
			if ( tribe_is_event_category() ) {
				$events = $this->filter_by_current_term( $events );
			}

			Tribe__Events__JSON_LD__Event::instance()->markup( $events );

			if ( ! $echo ) {
				return ob_get_clean();
			}
		}

		/**
		 * Remove any event that is not the same as the current term of the array of events, it return a modified events
		 * array with the events that only has the current term or original events if the term is not valid.
		 *
		 * @since 4.6.9
		 *
 		 * @param array
		 *
		 * @return array
		 */
		private function filter_by_current_term( $events ) {
			$taxonomy = get_query_var( 'taxonomy' );
			$current_term = get_term_by( 'id', get_queried_object_id(), $taxonomy );

			// get_term_by returns false if term does not exists or on failure.
			if ( false === $current_term ) {
				return $events;
			}

			foreach ( (array) $events as $key => $event_id ) {
				if ( ! has_term( $current_term->term_id, $taxonomy, $event_id ) ) {
					unset( $events[ $key ] );
				}
			}
			return $events;
		}

		/**
		 * Unhook all the hooks set up on this view
		 *
		 */
		protected function unhook() {
			parent::unhook();
			remove_filter( 'post_type_archive_title', '__return_false', 10 );

			if ( ! empty( $this->events_in_month ) ) {
				remove_filter( 'tribe_events_month_has_events', [ $this, 'has_events' ] );
			}

			remove_action( 'wp_head', [ $this, 'json_ld_markup' ] );
		}

		/**
		 * Set the correct args using either passed args, ajax request, or wp_query
		 *
		 *
		 * @param array $args
		 */
		protected function set_args( $args = [] ) {

			$doing_ajax = ( defined( 'DOING_AJAX' ) && DOING_AJAX ) ? true : false;

			if ( empty( $args ) && $doing_ajax ) {
				$post_status = [ 'publish' ];
				if ( is_user_logged_in() ) {
					$post_status[] = 'private';
				}
				// set the global query var for eventDisplay
				$args = [
					'post_type'    => Tribe__Events__Main::POSTTYPE,
					'eventDisplay' => 'month',
					'eventDate'    => is_array( $_POST['eventDate'] )
						? Tribe__Utils__Array::get( $_POST, [ 'eventDate', 0 ] )
						: $_POST['eventDate'],
					'post_status'  => $post_status,
				];
			}

			if ( empty( $args ) ) {
				// if no args were passed to the constructor, get them from $wp_query
				if ( ! $wp_query = tribe_get_global_query_object() ) {
					return;
				}

				$args = $wp_query->query;

				if ( ! empty( $wp_query->query_vars['meta_query'] ) ) {
					$args['meta_query'] = $wp_query->query_vars['meta_query'];
				}
			}

			$this->args = $args;
		}

		/**
		 * Set the notices used on month view
		 *
		 */
		public function set_notices() {
			// Our focus is on the current month, not the complete range of events included in the current month view
			// (which may include some leading/trailing days of the next and previous months)
			$slice_length         = $this->current_month_ends - $this->current_month_begins + 1;
			$current_month_counts = array_slice( self::$calendar_days, $this->current_month_begins, $slice_length );

			foreach ( $current_month_counts as $day ) {
				if ( $day['events']->have_posts() ) {
					// some events were found, no need to continue
					return;
				}
			}

			// if we got this far, it means no events were found
			$this->nothing_found_notice();
		}

		/**
		 * Sets an appropriate no results found message.
		 *
		 */
		protected function nothing_found_notice() {
			if ( ! empty( $this->args['suppress_nothing_found_notice'] ) ) {
				return;
			}

			$events_label_plural_lowercase = tribe_get_event_label_plural_lowercase();
			list( $search_term, $tax_term, $geographic_term ) = $this->get_search_terms();

			if ( ! empty( $search_term ) ) {
				Tribe__Notices::set_notice( 'event-search-no-results', sprintf( esc_html__( 'There were no results found for %s this month.', 'the-events-calendar' ),
					'<strong>"' . esc_html( urldecode( stripslashes( $search_term ) ) ) . '"</strong>' ) );
			} // if attempting to view a category archive.
			elseif ( ! empty( $tax_term ) ) {
				Tribe__Notices::set_notice( 'events-not-found', sprintf( esc_html__( 'No matching %1$s listed under %2$s. Please try viewing the full calendar for a complete list of events.', 'the-events-calendar' ), $events_label_plural_lowercase, $tax_term ) );
			} else {
				Tribe__Notices::set_notice( 'event-search-no-results', esc_html__( 'There were no results found.', 'the-events-calendar' ) );
			}
		}

		/**
		 * Get the title for month view
		 *
		 * @param string $original_title
		 * @param string $sep
		 *
		 * @return string
		 */
		protected function get_title( $original_title, $sep = null ) {
			$new_title = parent::get_title( $original_title, $sep );
			if ( get_query_var( 'eventDate' ) && has_filter( 'tribe_month_grid_view_title' ) ) {
				_deprecated_function( "The 'tribe_month_grid_view_title' filter", '3.8', " the 'tribe_get_events_title' filter" );
				$title_date = date_i18n( tribe_get_date_option( 'monthAndYearFormat', 'F Y' ), strtotime( get_query_var( 'eventDate' ) ) );
				$new_title  = apply_filters( 'tribe_month_grid_view_title', $new_title, $sep, $title_date );
			} elseif ( has_filter( 'tribe_events_this_month_title' ) ) {
				_deprecated_function( "The 'tribe_events_this_month_title' filter", '3.8', " the 'tribe_get_events_title' filter" );
				$new_title = apply_filters( 'tribe_events_this_month_title', $new_title, $sep );
			}

			return $new_title;
		}

		/**
		 * Get the view more link
		 *
		 * @param integer $date
		 *
		 * @return string
		 */
		private static function view_more_link( $date ) {
			$day_link       = tribe_get_day_link( $date );
			$tribe_bar_args = self::get_tribe_bar_args();

			if ( ! empty( $tribe_bar_args ) ) {
				unset( $tribe_bar_args['tribe_event_display'] );
				$day_link = add_query_arg( $tribe_bar_args, $day_link );
			}
			return esc_url_raw( $day_link );
		}

		/**
		 * Set the queried terms as a class property
		 * Include child categories of the category currently being viewed
		 */
		protected function set_queried_event_cats() {

			// Check the request for tribe_event_category
			if ( ! empty( $_POST['tribe_event_category'] ) ) {
				$this->args[ Tribe__Events__Main::TAXONOMY ] = $_POST['tribe_event_category'];
			} elseif ( ! empty( $_GET['tribe_event_category'] ) ) {
				$this->args[ Tribe__Events__Main::TAXONOMY ] = $_GET['tribe_event_category'];
			}

			$terms   = [];
			$term_id = isset( $this->args[ Tribe__Events__Main::TAXONOMY ] ) ? $this->args[ Tribe__Events__Main::TAXONOMY ] : null;

			// get the term by id if it's an int
			if ( is_int( $term_id ) ) {
				$terms[0] = $term_id;
			} elseif ( is_string( $term_id ) ) {
				// get the term by slug if it's a string
				$term = get_term_by( 'slug', $term_id, Tribe__Events__Main::TAXONOMY );
				if ( ! ( is_wp_error( $term ) || empty( $term ) ) ) {
					$terms[0] = $term->term_id;
				}
			}

			// make sure child terms are included
			if ( ! empty( $terms ) ) {
				$term_children = get_term_children( $terms[0], Tribe__Events__Main::TAXONOMY );
				if ( is_array( $term_children ) ) {
					$terms = array_merge( $terms, $term_children );
				}
			}
			$this->queried_event_cats = $terms;
		}

		/**
		 * Get all the events in the month by directly querying the postmeta table
		 * Also caches the postmeta and terms for the found events
		 */
		protected function set_events_in_month() {
			global $wpdb;

			$cache     = new Tribe__Cache();
			$cache_key = $this->get_month_view_cache_key( 'events_in_month' );

			// We always use the object cache if available
			$cache_getter = 'get';
			$cache_setter = 'set';
			$expiration = 0;

			// If the site owner has explicitly enabled month view caching however let's use
			// transients instead, to guarantee persistence
			if ( $this->use_cache ) {
				$cache_getter = 'get_transient';
				$cache_setter = 'set_transient';
				$expiration = $this->cache_expiration;
			}

			// If we have a cached result, use that
			$cached_events = $cache->$cache_getter( $cache_key, 'save_post' );

			if ( $cached_events !== false ) {
				/**
				 * A simple utility to listen for when events have been successfully pulled from cache.
				 *
				 * @since 4.7.1
				 *
				 * @param array $cached_events The array of event data pulled from cache.
				 */
				do_action( 'tribe_events_set_month_view_events_from_cache', $cached_events );

				$this->events_in_month = $cached_events;
				return;
			}

			$post_stati = [ 'publish' ];
			if ( is_user_logged_in() ) {
				$post_stati[] = 'private';
			}

			$post_stati = implode( "','", $post_stati );
			$ignore_hidden_events_AND = $this->hidden_events_fragment();

			$start_date = tribe_beginning_of_day( $this->first_grid_date );
			$end_date   = tribe_end_of_day( $this->final_grid_date );

			/**
			 * Allow bypassing the events in month SQL query to return events an alternative way.
			 *
			 * @param null|array $events_in_month An array of events in month (default null, run the SQL query like normal)
			 * @param string     $start_date      The start date to filter the queried events by
			 * @param string     $end_date        The end date to filter the queried events by
			 *
			 * @since 4.5.9
			 */
			$events_in_month = apply_filters( 'tribe_events_month_get_events_in_month', null, $start_date, $end_date );


			if ( null === $events_in_month ) {
				$use_utc = Tribe__Timezones::is_mode( 'site' );
				$start_date_key = $use_utc ? '_EventStartDateUTC' : '_EventStartDate';

				$start_date_sql = esc_sql( $start_date );
				$end_date_sql = esc_sql( $end_date );

				$events_request = "SELECT tribe_event_start.post_id as ID,
							tribe_event_start.meta_value as EventStartDate,
							tribe_event_end_date.meta_value as EventEndDate
					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 $ignore_hidden_events_AND tribe_event_start.meta_key = '{$start_date_key}'
					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}'
						)
					)
					AND $wpdb->posts.post_status IN('$post_stati')
					ORDER BY $wpdb->posts.menu_order ASC, DATE(tribe_event_start.meta_value) ASC, TIME(tribe_event_start.meta_value) ASC;
					";

				$events_in_month = $wpdb->get_results( $events_request );
			}

			$this->events_in_month = $events_in_month;

			// cache the postmeta and terms for all these posts in one go
			$event_ids_in_month = wp_list_pluck( $this->events_in_month, 'ID' );
			update_object_term_cache( $event_ids_in_month, Tribe__Events__Main::POSTTYPE );
			update_postmeta_cache( $event_ids_in_month );

			// cache the found events in the object cache
			$cache->$cache_setter( $cache_key, $this->events_in_month, $expiration, 'save_post' );
		}

		/**
		 * Returns a string that can be used as a cache key for the current month.
		 *
		 * @param string $prefix
		 *
		 * @return string
		 */
		protected function get_month_view_cache_key( $prefix ) {
			$grid_start_datetime = tribe_beginning_of_day( $this->first_grid_date );
			$grid_end_datetime   = tribe_end_of_day( $this->final_grid_date );
			return $prefix . '-' . $grid_start_datetime . '-' . $grid_end_datetime;
		}

		/**
		 * Returns a posts-not-in SQL fragment for use in a WHERE clause or else an empty
		 * string if it is unneeded.
		 *
		 * @return string
		 */
		protected function hidden_events_fragment() {
			global $wpdb;

			// Despite the method name, this obtains a list of post IDs to be hidden from *all* event listings
			$ignore_events = Tribe__Events__Query::getHideFromUpcomingEvents();

			// If it is empty we don't need to do anything further
			if ( empty( $ignore_events ) ) {
				return '';
			}

			// Let's ensure they are all absolute integers then collapse into a string
			$ignore_events = implode( ',', array_map( 'absint', $ignore_events ) );

			// Terminate with AND so it can easily be combined with the rest of the WHERE clause
			return " $wpdb->posts.ID NOT IN ( $ignore_events ) AND ";
		}

		/**
		 * Retrieves beginning/end times for a given date
		 *
		 * @param string $date Y-m-d date string
		 * @param string $key Key of cached data to retrieve
		 *
		 * return string|int
		 */
		private function get_cutoff_details( $date, $key ) {
			static $beginnings_and_ends = [];

			if ( empty( $beginnings_and_ends[ $date ] ) ) {
				$beginnings_and_ends[ $date ] = [
					'beginning' => tribe_beginning_of_day( $date ),
					'end'       => tribe_end_of_day( $date ),
				];

				$beginnings_and_ends[ $date ]['beginning_timestamp'] = strtotime( $beginnings_and_ends[ $date ]['beginning'] );
				$beginnings_and_ends[ $date ]['end_timestamp']       = strtotime( $beginnings_and_ends[ $date ]['end'] );
			}

			return $beginnings_and_ends[ $date ][ $key ];
		}

		/**
		 * Breaks the possible collection of events down by grid date
		 *
		 * @param string $date Y-m-d formatted date to retrieve events for
		 *
		 * @return array
		 */
		private function get_event_ids_by_day( $date ) {
			if ( ! $this->event_ids_by_day ) {
				$this->event_ids_by_day = [];

				// Let's loop over all of the events in the month and assign them to days
				foreach ( $this->events_in_month as $event ) {
					// if we're querying by category and the event doesn't have it, skip the event
					if ( ! empty ( $this->queried_event_cats ) ) {
						if ( ! has_term( $this->queried_event_cats, Tribe__Events__Main::TAXONOMY, $event ) ) {
							continue;
						}
					}

					$event_start = strtotime( tribe_get_start_date( $event->ID, true, Tribe__Date_Utils::DBDATETIMEFORMAT ) );
					$event_end   = strtotime( tribe_get_end_date( $event->ID, true, Tribe__Date_Utils::DBDATETIMEFORMAT ) );
					$order = get_post_field( 'menu_order', $event->ID );

					// Builds the Index to allow a better ordering of events
					$order_index = $order . ':' . $event_start . ':' . $event->ID;

					$start = date( 'Y-m-d', $event_start );
					$end = date( 'Y-m-d', $event_end );

					$beginning_of_start           = $this->get_cutoff_details( $start, 'beginning' );
					$beginning_of_start_timestamp = $this->get_cutoff_details( $start, 'beginning_timestamp' );
					$end_of_start                 = $this->get_cutoff_details( $start, 'end' );
					$end_of_start_timestamp       = $this->get_cutoff_details( $start, 'end_timestamp' );
					$beginning_of_end             = $this->get_cutoff_details( $end, 'beginning' );
					$beginning_of_end_timestamp   = $this->get_cutoff_details( $end, 'beginning_timestamp' );

					// if the start of the event is earlier than the beginning of the day, consider the event
					// as starting on the day before
					//
					// Example 1:
					// Assuming a cut-off of 6:00am and an event start date/time of August 2nd @ 5:00am. The
					// "start" DATE would be August 2nd and the beginning of the "start" DATE would be August
					// 2nd @ 6:00am. Therefore, the event start DATE shoud be altered to be a day earlier
					// (August 1st) (Note: the following if statement conditional would be true)
					if ( $event_start < $beginning_of_start_timestamp ) {
						$start = date( 'Y-m-d', strtotime( '-1 day', strtotime( $start ) ) );
					}

					// Subtract a day from the $end if it is:
					// * earlier than the beginning of the start DATE OR
					// * earlier than the beginning of the end DATE OR
					// * earlier than the end of the start DATE (as long as the beginning of the end DATE is greater than that of the start DATE)
					//
					// Example 1:
					// Assuming a cut-off of 6:00am and an event end date/time of August 2nd @ 7:00am. The
					// "end" DATE would be August 2nd and the beginning of the "end" DATE would be August
					// 2nd @ 6:00am. Therefore, the event end DATE shoud remain as August 2nd. (Note: the
					// following if statement conditional would be false)
					//
					// Example 2:
					// Assuming a cut-off of 6:00am and an event end date/time of August 2nd @ 5:00am. The
					// "end" DATE would be August 2nd and the beginning of the "end" DATE would be August
					// 2nd @ 6:00am. Therefore, the event end DATE shoud be altered to be a day earlier
					// (August 1st) (Note: this following if statement conditional would be true)
					if (
						$event_end < $beginning_of_start_timestamp
						|| $event_end < $beginning_of_end_timestamp
						|| (
							$event_end < $end_of_start_timestamp
							&& $beginning_of_end_timestamp > $end_of_start_timestamp
						)
					) {
						$end = date( 'Y-m-d', strtotime( '-1 day', strtotime( $end ) ) );
					}

					// determine if there's a difference in days between start and end
					$diff = strtotime( $end ) - strtotime( $start );

					if ( $diff > 0 ) {
						// There IS a difference. How many days?
						$diff_in_days = $diff / DAY_IN_SECONDS;

						// add the event to each day until the event end
						$new_start = $start;
						for ( $i = 0; $i <= $diff_in_days; $i++ ) {
							if ( ! isset( $this->event_ids_by_day[ $new_start ] ) ) {
								$this->event_ids_by_day[ $new_start ] = [];
							}

							$this->event_ids_by_day[ $new_start ][ $order_index ] = $event->ID;

							$new_start = date( 'Y-m-d', strtotime( '+1 day', strtotime( $new_start ) ) );
						}
					} else {
						// nope. The event is a single day event. Add it to the array
						if ( ! isset( $this->event_ids_by_day[ $start ] ) ) {
							$this->event_ids_by_day[ $start ] = [];
						}

						$this->event_ids_by_day[ $start ][ $order_index ] = $event->ID;
					}
				}

				// Now that we've built our event_ids_by_day, let's array_unique and sort
				foreach ( $this->event_ids_by_day as &$day ) {
					$day = array_unique( $day );
					ksort( $day );
				}
			}

			if ( empty( $this->event_ids_by_day[ $date ] ) ) {
				return [];
			}

			return $this->event_ids_by_day[ $date ];
		}

		/**
		 * Get the events for a single day
		 *
		 * @param string $date
		 *
		 * @return WP_Query
		 */
		private function get_daily_events( $date ) {
			/**
			 * Filters the WP_Query object that will be returned for daily events on a date.
			 *
			 * If the value returned from this filter is not `null` then the method will bail
			 * and return the filter return value.
			 *
			 * @since 4.6.24
			 *
			 * @param WP_Query|null $daily_events The WP_Query object the template will loop on
			 *                                    to print the daily events; initially `null`.
			 * @param string        $date         The day date in `Y-m-d` format.
			 */
			$daily_events = apply_filters( 'tribe_events_month_daily_events', null, $date );
			if ( null !== $daily_events ) {
				return $daily_events;
			}

			$event_ids_on_date = $this->get_event_ids_by_day( $date );

			// post__in doesn't work when it's empty, so just don't run the query if there are no IDs
			if ( empty( $event_ids_on_date ) ) {
				return new WP_Query();
			}

			/**
			 * Since we've already got the post IDs of the events fitting the search criteria
			 * we can unset this to avoid furhter date-based filtering from happening.
			 */
			unset( $this->args['eventDate'] );

			/*
			 * This  will skip updating term and meta caches - those were already
			 * updated in `$this->set_events_in_month()`.
			 * We run another query to make sure the ordering applies correctly.
			 * Expected order of events: sticky events, ongoing multi day events, all day events, then by start time.
			 */
			$args = wp_parse_args(
				[
					'eventDisplay'           => 'month',
					'posts_per_page'         => $this->events_per_day,
					'post__in'               => $event_ids_on_date,
					'update_post_term_cache' => false,
					'update_post_meta_cache' => false,
					'no_found_rows'          => false,
					'do_not_inject_date'     => true,

					// Don't replace `orderby` without taking in cosideration `menu_order`.
					'orderby'                => 'post__in',
				],
				$this->args
			);

			// If the request is false or not set we assume the request is for all events, not just featured ones.
			if (
				tribe( 'tec.featured_events' )->featured_events_requested()
				|| (
					isset( $this->args['featured'] )
					&& tribe_is_truthy( $this->args['featured'] )
				)
			) {
				$args['featured'] = true;
			} else {
				/**
				 * Unset due to how queries featured argument is expected to be non-existent.
				 *
				 * @see #127272
				 */
				if ( isset( $args['featured'] ) ) {
					unset( $args['featured'] );
				}
			}

			/**
			  * Filter Daily Events Query Arguments.
			  *
			  * @param array $args an array of daily events query.
			  *
			  */
			 $args = apply_filters( 'tribe_events_month_daily_events_query_args', $args );

			return tribe_get_events( $args, true );
		}

		/**
		 * Sets up an array of $days based on the current query, that can be used in the calendar loop
		 *
		 */
		public function setup_view() {
			if ( $this->use_cache && $this->html_cache->get() !== false ) {
				return;
			}

			$days = [];

			$date = $this->first_grid_date; // Start with the first grid date

			// Populate complete date range including leading/trailing days from adjacent months
			while ( $date <= $this->final_grid_date ) {

				$day_events = $this->get_daily_events( $date );
				$day = (int) substr( $date, - 2 );

				$prev_month = (int) substr( $date, 5, 2 ) < (int) substr( $this->requested_date, 5, 2 );
				$next_month = (int) substr( $date, 5, 2 ) > (int) substr( $this->requested_date, 5, 2 );

				$month_type = self::CURRENT_MONTH;
				if ( $prev_month ) {
					$month_type = self::PREVIOUS_MONTH;
				}
				if ( $next_month ) {
					$month_type = self::NEXT_MONTH;
				}

				$days[] = [
					'daynum'       => $day,
					'daynum-id'    => Tribe__Events__Utils__Id_Generator::generate_id( $day, $day ),
					'date'         => $date,
					'events'       => $day_events,
					'total_events' => $day_events->found_posts,
					'view_more'    => ( $day_events->found_posts > $this->events_per_day && $this->events_per_day >= 1 ) ? self::view_more_link( $date ) : false,
					'month'        => $month_type,
				];

				// Record the indicies marking the portion of the array relating to the current month
				if ( ! isset( $this->current_month_begins ) && self::CURRENT_MONTH === $month_type ) {
					$this->current_month_begins = count( $days ) - 1;
				}
				if ( isset( $this->current_month_begins ) && ! isset( $this->current_month_ends ) && self::CURRENT_MONTH !== $month_type ) {
					$this->current_month_ends = count( $days ) - 1;
				}

				// Advance forward one day
				$date = date( Tribe__Date_Utils::DBDATEFORMAT, strtotime( "$date +1 day" ) );
			}

			// If the month ended without bleeding into the next month, our current_month_ends property may not be set
			if ( ! isset( $this->current_month_ends ) ) {
				$this->current_month_ends = count( $days ) - 1;
			}

			// store set of found days for use in calendar loop functions
			self::$calendar_days = $days;
		}

		/**
		 * Returns the requested date as a Y-m (yyyy-mm) formatted string.
		 *
		 * If the requested date is invalid (such as 1984-25) the current month is returned instead and
		 * an appropriate notice presented to the user.
		 *
		 * @return string
		 */
		protected function requested_date() {
			// We expect the date to be Y-m (yyyy-mm) format, ie year and date only
			$date = isset( $this->args['eventDate'] ) ? $this->args['eventDate'] : tribe_get_month_view_date();

			// Test and return unmodified if valid
			if ( false !== strtotime( $date . '-01' ) ) {
				return $date;
			} else {
				Tribe__Notices::set_notice( 'requested-date-invalid',
					sprintf( esc_html__( 'The requested date "%s" was not valid &ndash; showing the current month instead', 'the-events-calendar' ), esc_html( $date ) ) );

				return date_i18n( 'Y-m' );
			}
		}

		/**
		 * Loop through the $_REQUEST and find all tribe bar args.
		 *
		 * @return array
		 */
		protected static function get_tribe_bar_args() {
			$tribe_bar_args = [];
			foreach ( $_REQUEST as $key => $value ) {
				if ( $value && strpos( $key, 'tribe' ) === 0 && $key != 'tribe-bar-date' ) {
					$tribe_bar_args[ $key ] = $value;
				}
			}

			return $tribe_bar_args;
		}

		/**
		 * Return the date of the first day in the month view grid.
		 *
		 * This is not necessarily the 1st of the specified month, rather it is the date of the
		 * first grid cell which could be anything upto 6 days earlier than the 1st of the month.
		 *
		 * @param string  $month
		 * @param integer $start_of_week
		 *
		 * @return bool|string (Y-m-d)
		 */
		public static function calculate_first_cell_date( $month, $start_of_week = null ) {
			if ( null === $start_of_week ) {
				$start_of_week = (int) get_option( 'start_of_week', 0 );
			}

			$day_1 = Tribe__Date_Utils::first_day_in_month( $month );
			if ( $day_1 < $start_of_week ) {
				$day_1 += 7;
			}

			$diff = $day_1 - $start_of_week;
			if ( $diff >= 0 ) {
				$diff = "-$diff";
			}

			try {
				$date = new DateTime( $month );
				$date = new DateTime( $date->format( 'Y-m-01' ) );
				$date->modify( "$diff days" );

				return $date->format( Tribe__Date_Utils::DBDATEFORMAT );
			} catch ( Exception $e ) {
				return false;
			}
		}

		/**
		 * Return the date of the first day in the month view grid.
		 *
		 * This is not necessarily the last day of the specified month, rather it is the date of
		 * the final grid cell which could be anything upto 6 days into the next month.
		 *
		 * @param string  $month
		 * @param integer $start_of_week
		 *
		 * @return bool|string (Y-m-d)
		 */
		public static function calculate_final_cell_date( $month, $start_of_week = null ) {
			if ( null === $start_of_week ) {
				$start_of_week = (int) get_option( 'start_of_week', 0 );
			}

			$last_day    = Tribe__Date_Utils::last_day_in_month( $month );
			$end_of_week = Tribe__Date_Utils::week_ends_on( $start_of_week );
			if ( $end_of_week < $last_day ) {
				$end_of_week += 7;
			}

			$diff = $end_of_week - $last_day;
			if ( $diff >= 0 ) {
				$diff = "+$diff";
			}

			try {
				$date = new DateTime( $month );
				$date = new DateTime( $date->format( 'Y-m-t' ) );
				$date->modify( "$diff days" );

				return $date->format( Tribe__Date_Utils::DBDATEFORMAT );
			} catch ( Exception $e ) {
				return false;
			}
		}

		/**
		 * Checks whether there are more calendar days to display
		 *
		 * @return bool True if calendar days are available, false if not.
		 */
		public static function have_days() {
			if ( self::$current_day + 1 < count( self::$calendar_days ) ) {
				return true;
			} elseif ( self::$current_day + 1 == count( self::$calendar_days ) && count( self::$calendar_days ) > 0 ) {
				do_action( 'tribe_events_calendar_loop_end' );
				// Do some cleaning up after the loop
				self::rewind_days();
			}

			return false;
		}

		/**
		 * Advances the internal day counter (and week counter, if appropriate)
		 *
		 */
		public static function the_day() {
			if ( self::have_days() ) {
				self::$current_day ++;
				if ( self::$current_day % 7 == 0 ) {
					self::$current_week ++;
				}
			}
		}

		/**
		 * Rewind the posts and reset post index.
		 *
		 */
		public static function rewind_days() {
			self::$current_day  = - 1;
			self::$current_week = - 1;
		}

		/**
		 * Returns the current day according to self::$current_day
		 *
		 * @return array|boolean
		 */
		public static function get_current_day() {
			if ( count( self::$calendar_days ) && self::$current_day < count( self::$calendar_days ) && isset( self::$calendar_days[ self::$current_day ] ) ) {
				return self::$calendar_days[ self::$current_day ];
			}

			return false;
		}

		/**
		 * Generates and returns a set of classes for the current day.
		 *
		 * @return string
		 */
		public static function day_classes() {
			$current_day  = self::get_current_day();
			$calendar_day = Tribe__Date_Utils::date_only( $current_day['date'] );
			$today        = date_i18n( Tribe__Date_Utils::DBDATEFORMAT );

			// Start by determining which month we're looking at
			if ( $current_day['month'] == self::CURRENT_MONTH ) {
				$classes = 'tribe-events-thismonth';
			} else {
				$classes = 'tribe-events-othermonth';
			}

			// Check if the calendar day is in the past, present, or future
			if ( $calendar_day < $today ) {
				$classes .= ' tribe-events-past';
			} elseif ( $calendar_day === $today ) {
				$classes .= ' tribe-events-present';
			} elseif ( $calendar_day > $today ) {
				$classes .= ' tribe-events-future';
			}

			// The day has some events
			if ( $current_day['total_events'] > 0 ) {
				$classes .= ' tribe-events-has-events';
			}

			// Needed for mobile js
			$daynum   = date( 'd', strtotime( $calendar_day ) );
			$classes .= ' mobile-trigger tribe-event-day-' . $daynum;

			// Determine which column of the grid the day is in
			$column = ( self::$current_day ) - ( self::$current_week * 7 );
			if ( $column > 0 && ( $column % 4 == 0 || $column % 5 == 0 || $column % 6 == 0 ) ) {
				$classes .= ' tribe-events-right';
			}

			return $classes;
		}

		/**
		 * Returns self::$current_week
		 *
		 * @return int $current_week
		 */
		public static function get_current_week() {
			return self::$current_week;
		}

		/**
		 * Generates and returns a set of classes for the current day
		 *
		 * @param  string $classes = ''
		 *
		 * @return string Classes
		 */
		public function event_classes( $classes = '' ) {

			$day = self::get_current_day();

			if ( ! isset( $day['events'] ) ) {
				return $classes;
			}

			$post = $day['events']->post;

			// @todo [BTRIA-593]: Review whether the erasure of any existing classes is generally desirable.
			$classes = [];

			$tribe_cat_slugs = tribe_get_event_cat_slugs( $post->ID );
			foreach ( $tribe_cat_slugs as $tribe_cat_slug ) {
				$classes[] = 'tribe-events-category-' . $tribe_cat_slug;
			}
			$classes = array_merge( $classes, get_post_class( '', $post->ID ) );
			if ( $venue_id = tribe_get_venue_id( $post->ID ) ) {
				$classes[] = 'tribe-events-venue-' . $venue_id;
			}
			foreach ( tribe_get_organizer_ids( $post->ID ) as $organizer_id ) {
				$classes[] = 'tribe-events-organizer-' . $organizer_id;
			}

			if ( $day['events']->current_post + 1 == $day['events']->post_count ) {
				$classes[] = 'tribe-events-last';
			}

			// Mark 'featured' events
			if ( tribe( 'tec.featured_events' )->is_featured( $post->ID ) ) {
				$classes[] = 'tribe-event-featured';
			}

			return $classes;
		}

		/**
		 * Month View Ajax Handler
		 *
		 */
		public function ajax_response() {

			if ( isset( $_POST['eventDate'] ) && $_POST['eventDate'] ) {

				Tribe__Events__Query::init();

				Tribe__Events__Main::instance()->displaying = 'month';

				global $wp_query;

				$wp_query = tribe_get_events( $this->args, true );

				ob_start();

				tribe_get_view( 'month/content' );

				$response = [
					'html'    => ob_get_clean(),
					'success' => true,
					'view'    => 'month',
				];
				apply_filters( 'tribe_events_ajax_response', $response );
				header( 'Content-type: application/json' );
				echo json_encode( $response );
				die();
			}
		}

		public function has_events() {
			return (bool) $this->events_in_month;
		}

		/**
		 * Check if the month has events when all the filters have been applied.
		 *
		 * @since 4.6.19
		 *
		 * @return bool
		 */
		public function has_events_filtered() {

			if ( 'month' !== Tribe__Events__Main::instance()->displaying ) {
				return false;
			}

			if ( ! $wp_query = tribe_get_global_query_object() ) {
				return false;
			}

			// Get the date from the main query
			$event_date = $wp_query->get( 'eventDate' );

			// If we don't have the date, get the current date
			$month = empty( $event_date )
				? tribe_get_month_view_date()
				: $wp_query->get( 'eventDate' );

			// prepare the args for this month
			$args = [
				'eventDisplay'   => 'custom',
				'start_date'     => self::calculate_first_cell_date( $month ),
				'end_date'       => self::calculate_final_cell_date( $month ),
				'posts_per_page' => -1,
				'hide_upcoming'  => true,
			];

			// check if we should take care of taxonomy
			if ( $wp_query->get( Tribe__Events__Main::TAXONOMY, false ) !== false ) {
				$args[ Tribe__Events__Main::TAXONOMY ] = $wp_query->get( Tribe__Events__Main::TAXONOMY );
			}

			// See if we have events for
			$events = tribe_get_events( $args );

			return (bool) $events;

		}

	} // class Tribe__Events__Template__Month
}
